Repository: skylot/jadx Branch: master Commit: 15ea9a56b996 Files: 2269 Total size: 7.1 MB Directory structure: gitextract_b8off970/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── config.yml │ │ ├── decompilation-issue.yml │ │ ├── feature-request.yml │ │ └── jadx-gui-issue.yml │ ├── dependabot.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── build-artifacts.yml │ ├── build-test.yml │ └── release.yml ├── .gitignore ├── .gitlab-ci.yml ├── .jitpack.yml ├── .typos.toml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── SECURITY.md ├── build.gradle.kts ├── buildSrc/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ ├── jadx-java.gradle.kts │ ├── jadx-kotlin.gradle.kts │ ├── jadx-library.gradle.kts │ └── jadx-rewrite.gradle.kts ├── config/ │ ├── checkstyle/ │ │ └── checkstyle.xml │ ├── code-formatter/ │ │ ├── eclipse.importorder │ │ └── eclipse.xml │ └── jflex/ │ ├── .gitignore │ ├── README.md │ ├── SmaliTokenMaker.flex │ └── skeleton.default ├── contrib/ │ └── jadx-gui.desktop ├── gradle/ │ └── wrapper/ │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jadx-cli/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── jadx/ │ │ │ └── cli/ │ │ │ ├── JCommanderWrapper.java │ │ │ ├── JadxAppCommon.java │ │ │ ├── JadxCLI.java │ │ │ ├── JadxCLIArgs.java │ │ │ ├── JadxCLICommands.java │ │ │ ├── LogHelper.java │ │ │ ├── SingleClassMode.java │ │ │ ├── clst/ │ │ │ │ └── ConvertToClsSet.java │ │ │ ├── commands/ │ │ │ │ ├── CommandPlugins.java │ │ │ │ └── ICommand.java │ │ │ ├── config/ │ │ │ │ ├── IJadxConfig.java │ │ │ │ ├── JadxConfigAdapter.java │ │ │ │ └── JadxConfigExclude.java │ │ │ ├── plugins/ │ │ │ │ └── JadxFilesGetter.java │ │ │ └── tools/ │ │ │ └── ConvertArscFile.java │ │ └── resources/ │ │ └── logback.xml │ └── test/ │ ├── java/ │ │ └── jadx/ │ │ ├── cli/ │ │ │ ├── BaseCliIntegrationTest.java │ │ │ ├── JadxCLIArgsTest.java │ │ │ ├── RenameConverterTest.java │ │ │ ├── TestExport.java │ │ │ └── TestInput.java │ │ └── plugins/ │ │ └── tools/ │ │ └── utils/ │ │ └── PluginUtilsTest.java │ └── resources/ │ └── samples/ │ ├── HelloWorld.smali │ ├── hello.dex │ ├── resources-only.apk │ ├── small.apk │ └── test-lib.aar ├── jadx-commons/ │ ├── jadx-app-commons/ │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── jadx/ │ │ └── commons/ │ │ └── app/ │ │ ├── JadxCommonEnv.java │ │ ├── JadxCommonFiles.java │ │ ├── JadxSystemInfo.java │ │ └── JadxTempFiles.java │ └── jadx-zip/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── jadx/ │ └── zip/ │ ├── IZipEntry.java │ ├── IZipParser.java │ ├── ZipContent.java │ ├── ZipReader.java │ ├── ZipReaderFlags.java │ ├── ZipReaderOptions.java │ ├── fallback/ │ │ ├── FallbackZipEntry.java │ │ └── FallbackZipParser.java │ ├── io/ │ │ ├── ByteBufferBackedInputStream.java │ │ └── LimitedInputStream.java │ ├── parser/ │ │ ├── JadxZipEntry.java │ │ ├── JadxZipParser.java │ │ └── ZipDeflate.java │ └── security/ │ ├── DisabledZipSecurity.java │ ├── IJadxZipSecurity.java │ └── JadxZipSecurity.java ├── jadx-core/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── jadx/ │ │ │ ├── api/ │ │ │ │ ├── CommentsLevel.java │ │ │ │ ├── DecompilationMode.java │ │ │ │ ├── ICodeCache.java │ │ │ │ ├── ICodeInfo.java │ │ │ │ ├── ICodeWriter.java │ │ │ │ ├── IDecompileScheduler.java │ │ │ │ ├── JadxArgs.java │ │ │ │ ├── JadxArgsValidator.java │ │ │ │ ├── JadxDecompiler.java │ │ │ │ ├── JavaClass.java │ │ │ │ ├── JavaField.java │ │ │ │ ├── JavaMethod.java │ │ │ │ ├── JavaNode.java │ │ │ │ ├── JavaPackage.java │ │ │ │ ├── JavaVariable.java │ │ │ │ ├── ResourceFile.java │ │ │ │ ├── ResourceFileContainer.java │ │ │ │ ├── ResourceFileContent.java │ │ │ │ ├── ResourceType.java │ │ │ │ ├── ResourcesLoader.java │ │ │ │ ├── args/ │ │ │ │ │ ├── GeneratedRenamesMappingFileMode.java │ │ │ │ │ ├── IntegerFormat.java │ │ │ │ │ ├── ResourceNameSource.java │ │ │ │ │ ├── UseSourceNameAsClassNameAlias.java │ │ │ │ │ └── UserRenamesMappingsMode.java │ │ │ │ ├── data/ │ │ │ │ │ ├── CodeRefType.java │ │ │ │ │ ├── CommentStyle.java │ │ │ │ │ ├── ICodeComment.java │ │ │ │ │ ├── ICodeData.java │ │ │ │ │ ├── ICodeRename.java │ │ │ │ │ ├── IJavaCodeRef.java │ │ │ │ │ ├── IJavaNodeRef.java │ │ │ │ │ ├── IRenameNode.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── JadxCodeComment.java │ │ │ │ │ ├── JadxCodeData.java │ │ │ │ │ ├── JadxCodeRef.java │ │ │ │ │ ├── JadxCodeRename.java │ │ │ │ │ └── JadxNodeRef.java │ │ │ │ ├── deobf/ │ │ │ │ │ ├── IAliasProvider.java │ │ │ │ │ ├── IDeobfCondition.java │ │ │ │ │ ├── IRenameCondition.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── AlwaysRename.java │ │ │ │ │ ├── AnyRenameCondition.java │ │ │ │ │ └── CombineDeobfConditions.java │ │ │ │ ├── gui/ │ │ │ │ │ └── tree/ │ │ │ │ │ └── ITreeNode.java │ │ │ │ ├── impl/ │ │ │ │ │ ├── AnnotatedCodeInfo.java │ │ │ │ │ ├── AnnotatedCodeWriter.java │ │ │ │ │ ├── DelegateCodeCache.java │ │ │ │ │ ├── InMemoryCodeCache.java │ │ │ │ │ ├── NoOpCodeCache.java │ │ │ │ │ ├── SimpleCodeInfo.java │ │ │ │ │ ├── SimpleCodeWriter.java │ │ │ │ │ └── passes/ │ │ │ │ │ ├── DecompilePassWrapper.java │ │ │ │ │ ├── IPassWrapperVisitor.java │ │ │ │ │ └── PreparePassWrapper.java │ │ │ │ ├── metadata/ │ │ │ │ │ ├── ICodeAnnotation.java │ │ │ │ │ ├── ICodeMetadata.java │ │ │ │ │ ├── ICodeNodeRef.java │ │ │ │ │ ├── annotations/ │ │ │ │ │ │ ├── InsnCodeOffset.java │ │ │ │ │ │ ├── NodeDeclareRef.java │ │ │ │ │ │ ├── NodeEnd.java │ │ │ │ │ │ ├── VarNode.java │ │ │ │ │ │ └── VarRef.java │ │ │ │ │ └── impl/ │ │ │ │ │ └── CodeMetadataStorage.java │ │ │ │ ├── plugins/ │ │ │ │ │ ├── CustomResourcesLoader.java │ │ │ │ │ ├── JadxPlugin.java │ │ │ │ │ ├── JadxPluginContext.java │ │ │ │ │ ├── JadxPluginInfo.java │ │ │ │ │ ├── JadxPluginInfoBuilder.java │ │ │ │ │ ├── data/ │ │ │ │ │ │ ├── IJadxFiles.java │ │ │ │ │ │ ├── IJadxPlugins.java │ │ │ │ │ │ └── JadxPluginRuntimeData.java │ │ │ │ │ ├── events/ │ │ │ │ │ │ ├── IJadxEvent.java │ │ │ │ │ │ ├── IJadxEvents.java │ │ │ │ │ │ ├── JadxEventType.java │ │ │ │ │ │ ├── JadxEvents.java │ │ │ │ │ │ └── types/ │ │ │ │ │ │ ├── NodeRenamedByUser.java │ │ │ │ │ │ ├── ReloadProject.java │ │ │ │ │ │ └── ReloadSettingsWindow.java │ │ │ │ │ ├── gui/ │ │ │ │ │ │ ├── ISettingsGroup.java │ │ │ │ │ │ ├── JadxGuiContext.java │ │ │ │ │ │ └── JadxGuiSettings.java │ │ │ │ │ ├── loader/ │ │ │ │ │ │ ├── JadxBasePluginLoader.java │ │ │ │ │ │ └── JadxPluginLoader.java │ │ │ │ │ ├── options/ │ │ │ │ │ │ ├── JadxPluginOptions.java │ │ │ │ │ │ ├── OptionDescription.java │ │ │ │ │ │ ├── OptionFlag.java │ │ │ │ │ │ ├── OptionType.java │ │ │ │ │ │ └── impl/ │ │ │ │ │ │ ├── BaseOptionsParser.java │ │ │ │ │ │ ├── BasePluginOptionsBuilder.java │ │ │ │ │ │ ├── JadxOptionDescription.java │ │ │ │ │ │ └── OptionBuilder.java │ │ │ │ │ ├── pass/ │ │ │ │ │ │ ├── JadxPass.java │ │ │ │ │ │ ├── JadxPassInfo.java │ │ │ │ │ │ ├── impl/ │ │ │ │ │ │ │ ├── OrderedJadxPassInfo.java │ │ │ │ │ │ │ ├── SimpleAfterLoadPass.java │ │ │ │ │ │ │ └── SimpleJadxPassInfo.java │ │ │ │ │ │ └── types/ │ │ │ │ │ │ ├── JadxAfterLoadPass.java │ │ │ │ │ │ ├── JadxDecompilePass.java │ │ │ │ │ │ ├── JadxPassType.java │ │ │ │ │ │ └── JadxPreparePass.java │ │ │ │ │ ├── resources/ │ │ │ │ │ │ ├── IResContainerFactory.java │ │ │ │ │ │ ├── IResTableParserProvider.java │ │ │ │ │ │ └── IResourcesLoader.java │ │ │ │ │ └── utils/ │ │ │ │ │ ├── CommonFileUtils.java │ │ │ │ │ ├── Utils.java │ │ │ │ │ └── ZipSecurity.java │ │ │ │ ├── resources/ │ │ │ │ │ └── ResourceContentType.java │ │ │ │ ├── security/ │ │ │ │ │ ├── IJadxSecurity.java │ │ │ │ │ ├── JadxSecurityFlag.java │ │ │ │ │ └── impl/ │ │ │ │ │ └── JadxSecurity.java │ │ │ │ ├── usage/ │ │ │ │ │ ├── IUsageInfoCache.java │ │ │ │ │ ├── IUsageInfoData.java │ │ │ │ │ ├── IUsageInfoVisitor.java │ │ │ │ │ └── impl/ │ │ │ │ │ ├── EmptyUsageInfoCache.java │ │ │ │ │ └── InMemoryUsageInfoCache.java │ │ │ │ └── utils/ │ │ │ │ ├── CodeUtils.java │ │ │ │ └── tasks/ │ │ │ │ └── ITaskExecutor.java │ │ │ └── core/ │ │ │ ├── Consts.java │ │ │ ├── Jadx.java │ │ │ ├── ProcessClass.java │ │ │ ├── clsp/ │ │ │ │ ├── ClsSet.java │ │ │ │ ├── ClspClass.java │ │ │ │ ├── ClspClassSource.java │ │ │ │ ├── ClspGraph.java │ │ │ │ ├── ClspMethod.java │ │ │ │ └── SimpleMethodDetails.java │ │ │ ├── codegen/ │ │ │ │ ├── AnnotationGen.java │ │ │ │ ├── ClassGen.java │ │ │ │ ├── CodeGen.java │ │ │ │ ├── ConditionGen.java │ │ │ │ ├── InsnGen.java │ │ │ │ ├── MethodGen.java │ │ │ │ ├── NameGen.java │ │ │ │ ├── RegionGen.java │ │ │ │ ├── SimpleModeHelper.java │ │ │ │ ├── TypeGen.java │ │ │ │ ├── json/ │ │ │ │ │ ├── JsonCodeGen.java │ │ │ │ │ ├── JsonMappingGen.java │ │ │ │ │ ├── cls/ │ │ │ │ │ │ ├── JsonClass.java │ │ │ │ │ │ ├── JsonCodeLine.java │ │ │ │ │ │ ├── JsonField.java │ │ │ │ │ │ ├── JsonMethod.java │ │ │ │ │ │ └── JsonNode.java │ │ │ │ │ └── mapping/ │ │ │ │ │ ├── JsonClsMapping.java │ │ │ │ │ ├── JsonFieldMapping.java │ │ │ │ │ ├── JsonMapping.java │ │ │ │ │ └── JsonMthMapping.java │ │ │ │ └── utils/ │ │ │ │ ├── CodeComment.java │ │ │ │ └── CodeGenUtils.java │ │ │ ├── deobf/ │ │ │ │ ├── DeobfAliasProvider.java │ │ │ │ ├── DeobfPresets.java │ │ │ │ ├── DeobfuscatorVisitor.java │ │ │ │ ├── FileTypeDetector.java │ │ │ │ ├── NameMapper.java │ │ │ │ ├── SaveDeobfMapping.java │ │ │ │ └── conditions/ │ │ │ │ ├── AbstractDeobfCondition.java │ │ │ │ ├── AvoidClsAndPkgNamesCollision.java │ │ │ │ ├── BaseDeobfCondition.java │ │ │ │ ├── DeobfLengthCondition.java │ │ │ │ ├── DeobfWhitelist.java │ │ │ │ ├── ExcludeAndroidRClass.java │ │ │ │ ├── ExcludePackageWithTLDNames.java │ │ │ │ └── JadxRenameConditions.java │ │ │ ├── dex/ │ │ │ │ ├── attributes/ │ │ │ │ │ ├── AFlag.java │ │ │ │ │ ├── AType.java │ │ │ │ │ ├── AttrList.java │ │ │ │ │ ├── AttrNode.java │ │ │ │ │ ├── AttributeStorage.java │ │ │ │ │ ├── EmptyAttrStorage.java │ │ │ │ │ ├── FieldInitInsnAttr.java │ │ │ │ │ ├── IAttributeNode.java │ │ │ │ │ ├── ILineAttributeNode.java │ │ │ │ │ └── nodes/ │ │ │ │ │ ├── AnonymousClassAttr.java │ │ │ │ │ ├── ClassTypeVarsAttr.java │ │ │ │ │ ├── CodeFeaturesAttr.java │ │ │ │ │ ├── DeclareVariablesAttr.java │ │ │ │ │ ├── DecompileModeOverrideAttr.java │ │ │ │ │ ├── EdgeInsnAttr.java │ │ │ │ │ ├── EnumClassAttr.java │ │ │ │ │ ├── EnumMapAttr.java │ │ │ │ │ ├── ExcSplitCrossAttr.java │ │ │ │ │ ├── FieldReplaceAttr.java │ │ │ │ │ ├── ForceReturnAttr.java │ │ │ │ │ ├── GenericInfoAttr.java │ │ │ │ │ ├── InlinedAttr.java │ │ │ │ │ ├── JadxCommentsAttr.java │ │ │ │ │ ├── JadxError.java │ │ │ │ │ ├── JumpInfo.java │ │ │ │ │ ├── LineAttrNode.java │ │ │ │ │ ├── LocalVarsDebugInfoAttr.java │ │ │ │ │ ├── LoopInfo.java │ │ │ │ │ ├── LoopLabelAttr.java │ │ │ │ │ ├── MethodBridgeAttr.java │ │ │ │ │ ├── MethodInlineAttr.java │ │ │ │ │ ├── MethodOverrideAttr.java │ │ │ │ │ ├── MethodReplaceAttr.java │ │ │ │ │ ├── MethodThrowsAttr.java │ │ │ │ │ ├── MethodTypeVarsAttr.java │ │ │ │ │ ├── NotificationAttrNode.java │ │ │ │ │ ├── PhiListAttr.java │ │ │ │ │ ├── RegDebugInfoAttr.java │ │ │ │ │ ├── RegionRefAttr.java │ │ │ │ │ ├── RenameReasonAttr.java │ │ │ │ │ ├── SkipMethodArgsAttr.java │ │ │ │ │ ├── SpecialEdgeAttr.java │ │ │ │ │ └── TmpEdgeAttr.java │ │ │ │ ├── info/ │ │ │ │ │ ├── AccessInfo.java │ │ │ │ │ ├── ClassAliasInfo.java │ │ │ │ │ ├── ClassInfo.java │ │ │ │ │ ├── ConstStorage.java │ │ │ │ │ ├── FieldInfo.java │ │ │ │ │ ├── InfoStorage.java │ │ │ │ │ ├── MethodInfo.java │ │ │ │ │ └── PackageInfo.java │ │ │ │ ├── instructions/ │ │ │ │ │ ├── ArithNode.java │ │ │ │ │ ├── ArithOp.java │ │ │ │ │ ├── BaseInvokeNode.java │ │ │ │ │ ├── ConstClassNode.java │ │ │ │ │ ├── ConstStringNode.java │ │ │ │ │ ├── FillArrayData.java │ │ │ │ │ ├── FillArrayInsn.java │ │ │ │ │ ├── FilledNewArrayNode.java │ │ │ │ │ ├── GotoNode.java │ │ │ │ │ ├── IfNode.java │ │ │ │ │ ├── IfOp.java │ │ │ │ │ ├── IndexInsnNode.java │ │ │ │ │ ├── InsnDecoder.java │ │ │ │ │ ├── InsnType.java │ │ │ │ │ ├── InvokeCustomBuilder.java │ │ │ │ │ ├── InvokeCustomNode.java │ │ │ │ │ ├── InvokeCustomRawNode.java │ │ │ │ │ ├── InvokeNode.java │ │ │ │ │ ├── InvokePolymorphicNode.java │ │ │ │ │ ├── InvokeType.java │ │ │ │ │ ├── NewArrayNode.java │ │ │ │ │ ├── PhiInsn.java │ │ │ │ │ ├── SwitchData.java │ │ │ │ │ ├── SwitchInsn.java │ │ │ │ │ ├── TargetInsnNode.java │ │ │ │ │ ├── args/ │ │ │ │ │ │ ├── ArgType.java │ │ │ │ │ │ ├── CodeVar.java │ │ │ │ │ │ ├── InsnArg.java │ │ │ │ │ │ ├── InsnWrapArg.java │ │ │ │ │ │ ├── LiteralArg.java │ │ │ │ │ │ ├── Named.java │ │ │ │ │ │ ├── NamedArg.java │ │ │ │ │ │ ├── PrimitiveType.java │ │ │ │ │ │ ├── RegisterArg.java │ │ │ │ │ │ ├── SSAVar.java │ │ │ │ │ │ ├── Typed.java │ │ │ │ │ │ └── VarName.java │ │ │ │ │ ├── invokedynamic/ │ │ │ │ │ │ ├── CustomLambdaCall.java │ │ │ │ │ │ ├── CustomRawCall.java │ │ │ │ │ │ ├── CustomStringConcat.java │ │ │ │ │ │ └── InvokeCustomUtils.java │ │ │ │ │ ├── java/ │ │ │ │ │ │ └── JsrNode.java │ │ │ │ │ └── mods/ │ │ │ │ │ ├── ConstructorInsn.java │ │ │ │ │ └── TernaryInsn.java │ │ │ │ ├── nodes/ │ │ │ │ │ ├── BlockNode.java │ │ │ │ │ ├── ClassNode.java │ │ │ │ │ ├── Edge.java │ │ │ │ │ ├── FieldNode.java │ │ │ │ │ ├── IBlock.java │ │ │ │ │ ├── IBranchRegion.java │ │ │ │ │ ├── ICodeDataUpdateListener.java │ │ │ │ │ ├── ICodeNode.java │ │ │ │ │ ├── IConditionRegion.java │ │ │ │ │ ├── IContainer.java │ │ │ │ │ ├── IDexNode.java │ │ │ │ │ ├── IFieldInfoRef.java │ │ │ │ │ ├── ILoadable.java │ │ │ │ │ ├── IMethodDetails.java │ │ │ │ │ ├── IPackageUpdate.java │ │ │ │ │ ├── IRegion.java │ │ │ │ │ ├── IUsageInfoNode.java │ │ │ │ │ ├── InsnContainer.java │ │ │ │ │ ├── InsnNode.java │ │ │ │ │ ├── LoadStage.java │ │ │ │ │ ├── MethodNode.java │ │ │ │ │ ├── PackageNode.java │ │ │ │ │ ├── ProcessState.java │ │ │ │ │ ├── RootNode.java │ │ │ │ │ ├── parser/ │ │ │ │ │ │ └── SignatureParser.java │ │ │ │ │ └── utils/ │ │ │ │ │ ├── MethodUtils.java │ │ │ │ │ ├── SelectFromDuplicates.java │ │ │ │ │ └── TypeUtils.java │ │ │ │ ├── regions/ │ │ │ │ │ ├── AbstractRegion.java │ │ │ │ │ ├── Region.java │ │ │ │ │ ├── SwitchRegion.java │ │ │ │ │ ├── SynchronizedRegion.java │ │ │ │ │ ├── TryCatchRegion.java │ │ │ │ │ ├── conditions/ │ │ │ │ │ │ ├── Compare.java │ │ │ │ │ │ ├── ConditionRegion.java │ │ │ │ │ │ ├── IfCondition.java │ │ │ │ │ │ ├── IfInfo.java │ │ │ │ │ │ └── IfRegion.java │ │ │ │ │ └── loops/ │ │ │ │ │ ├── ForEachLoop.java │ │ │ │ │ ├── ForLoop.java │ │ │ │ │ ├── LoopRegion.java │ │ │ │ │ └── LoopType.java │ │ │ │ ├── trycatch/ │ │ │ │ │ ├── CatchAttr.java │ │ │ │ │ ├── ExcHandlerAttr.java │ │ │ │ │ ├── ExceptionHandler.java │ │ │ │ │ ├── TryCatchBlockAttr.java │ │ │ │ │ ├── TryEdge.java │ │ │ │ │ ├── TryEdgeScopeGroupMap.java │ │ │ │ │ └── TryEdgeType.java │ │ │ │ └── visitors/ │ │ │ │ ├── AbstractVisitor.java │ │ │ │ ├── AdjustForIfMergeVisitor.java │ │ │ │ ├── AnonymousClassVisitor.java │ │ │ │ ├── ApplyVariableNames.java │ │ │ │ ├── AttachCommentsVisitor.java │ │ │ │ ├── AttachMethodDetails.java │ │ │ │ ├── AttachTryCatchVisitor.java │ │ │ │ ├── CheckCode.java │ │ │ │ ├── ClassModifier.java │ │ │ │ ├── ConstInlineVisitor.java │ │ │ │ ├── ConstructorVisitor.java │ │ │ │ ├── DeboxingVisitor.java │ │ │ │ ├── DepthTraversal.java │ │ │ │ ├── DotGraphVisitor.java │ │ │ │ ├── EnumVisitor.java │ │ │ │ ├── ExtractFieldInit.java │ │ │ │ ├── FallbackModeVisitor.java │ │ │ │ ├── FixSwitchOverEnum.java │ │ │ │ ├── GenericTypesVisitor.java │ │ │ │ ├── IDexTreeVisitor.java │ │ │ │ ├── InitCodeVariables.java │ │ │ │ ├── InlineMethods.java │ │ │ │ ├── JadxVisitor.java │ │ │ │ ├── MarkMethodsForInline.java │ │ │ │ ├── MethodInvokeVisitor.java │ │ │ │ ├── MethodThrowsVisitor.java │ │ │ │ ├── MethodVisitor.java │ │ │ │ ├── ModVisitor.java │ │ │ │ ├── MoveInlineVisitor.java │ │ │ │ ├── OverrideMethodVisitor.java │ │ │ │ ├── PrepareForCodeGen.java │ │ │ │ ├── ProcessAnonymous.java │ │ │ │ ├── ProcessInstructionsVisitor.java │ │ │ │ ├── ProcessMethodsForInline.java │ │ │ │ ├── ReplaceNewArray.java │ │ │ │ ├── SaveCode.java │ │ │ │ ├── ShadowFieldVisitor.java │ │ │ │ ├── SignatureProcessor.java │ │ │ │ ├── SimplifyVisitor.java │ │ │ │ ├── blocks/ │ │ │ │ │ ├── BlockExceptionHandler.java │ │ │ │ │ ├── BlockFinisher.java │ │ │ │ │ ├── BlockProcessor.java │ │ │ │ │ ├── BlockSplitter.java │ │ │ │ │ ├── DominatorTree.java │ │ │ │ │ ├── FixMultiEntryLoops.java │ │ │ │ │ ├── PostDominatorTree.java │ │ │ │ │ └── ResolveJavaJSR.java │ │ │ │ ├── debuginfo/ │ │ │ │ │ ├── DebugInfoApplyVisitor.java │ │ │ │ │ └── DebugInfoAttachVisitor.java │ │ │ │ ├── finaly/ │ │ │ │ │ ├── CentralityState.java │ │ │ │ │ ├── FinallyExtractInfo.java │ │ │ │ │ ├── InsnsSlice.java │ │ │ │ │ ├── MarkFinallyVisitor.java │ │ │ │ │ ├── SameInstructionsStrategy.java │ │ │ │ │ ├── SameInstructionsStrategyImpl.java │ │ │ │ │ ├── TryCatchEdgeBlockMap.java │ │ │ │ │ └── traverser/ │ │ │ │ │ ├── GlobalTraverserSourceState.java │ │ │ │ │ ├── TraverserController.java │ │ │ │ │ ├── TraverserException.java │ │ │ │ │ ├── factory/ │ │ │ │ │ │ ├── DuplicatedTraverserStateFactory.java │ │ │ │ │ │ └── TraverserStateFactory.java │ │ │ │ │ ├── handlers/ │ │ │ │ │ │ ├── AbstractActivePathTraverserHandler.java │ │ │ │ │ │ ├── AbstractBlockPathTraverserHandler.java │ │ │ │ │ │ ├── AbstractBlockTraverserHandler.java │ │ │ │ │ │ ├── BaseBlockTraverserHandler.java │ │ │ │ │ │ ├── InstructionActivePathTraverserHandler.java │ │ │ │ │ │ ├── MergePathActivePathTraverserHandler.java │ │ │ │ │ │ ├── PredecessorBlockPathTraverserHandler.java │ │ │ │ │ │ └── PredecessorMergeActivePathTraverserHandler.java │ │ │ │ │ ├── state/ │ │ │ │ │ │ ├── AwaitingInsnCompareTraverserState.java │ │ │ │ │ │ ├── ISourceBlockState.java │ │ │ │ │ │ ├── IdentifiedScopeWithTerminatorTraverserState.java │ │ │ │ │ │ ├── NewBlockTraverserState.java │ │ │ │ │ │ ├── NoBlockTraverserState.java │ │ │ │ │ │ ├── RecoveredFromCacheTraverserState.java │ │ │ │ │ │ ├── TerminalTraverserState.java │ │ │ │ │ │ ├── TraverserActivePathState.java │ │ │ │ │ │ ├── TraverserBlockInfo.java │ │ │ │ │ │ ├── TraverserGlobalCommonState.java │ │ │ │ │ │ ├── TraverserState.java │ │ │ │ │ │ └── UnknownAdvanceStrategyTraverserState.java │ │ │ │ │ └── visitors/ │ │ │ │ │ ├── AbstractBlockTraverserVisitor.java │ │ │ │ │ ├── ImplicitInsnBlockTraverserVisitor.java │ │ │ │ │ ├── PathEndBlockTraverserVisitor.java │ │ │ │ │ ├── PredecessorBlockTraverserVisitor.java │ │ │ │ │ └── comparator/ │ │ │ │ │ ├── AbstractTraverserComparatorVisitor.java │ │ │ │ │ └── InstructionBlockComparatorTraverserVisitor.java │ │ │ │ ├── fixaccessmodifiers/ │ │ │ │ │ ├── FixAccessModifiers.java │ │ │ │ │ └── VisibilityUtils.java │ │ │ │ ├── gradle/ │ │ │ │ │ └── NonFinalResIdsVisitor.java │ │ │ │ ├── kotlin/ │ │ │ │ │ └── ProcessKotlinInternals.java │ │ │ │ ├── methods/ │ │ │ │ │ └── MutableMethodDetails.java │ │ │ │ ├── prepare/ │ │ │ │ │ ├── AddAndroidConstants.java │ │ │ │ │ └── CollectConstValues.java │ │ │ │ ├── regions/ │ │ │ │ │ ├── AbstractRegionVisitor.java │ │ │ │ │ ├── CheckRegions.java │ │ │ │ │ ├── CleanRegions.java │ │ │ │ │ ├── DebugRegionCounter.java │ │ │ │ │ ├── DepthRegionTraversal.java │ │ │ │ │ ├── IRegionIterativeVisitor.java │ │ │ │ │ ├── IRegionVisitor.java │ │ │ │ │ ├── IfRegionVisitor.java │ │ │ │ │ ├── LoopRegionVisitor.java │ │ │ │ │ ├── PostProcessRegions.java │ │ │ │ │ ├── ProcessTryCatchRegions.java │ │ │ │ │ ├── RegionMakerVisitor.java │ │ │ │ │ ├── ReturnVisitor.java │ │ │ │ │ ├── SwitchBreakVisitor.java │ │ │ │ │ ├── SwitchOverStringVisitor.java │ │ │ │ │ ├── TernaryMod.java │ │ │ │ │ ├── TracedRegionVisitor.java │ │ │ │ │ ├── maker/ │ │ │ │ │ │ ├── ExcHandlersRegionMaker.java │ │ │ │ │ │ ├── IfRegionMaker.java │ │ │ │ │ │ ├── LoopRegionMaker.java │ │ │ │ │ │ ├── RegionMaker.java │ │ │ │ │ │ ├── RegionStack.java │ │ │ │ │ │ ├── SwitchRegionMaker.java │ │ │ │ │ │ └── SynchronizedRegionMaker.java │ │ │ │ │ └── variables/ │ │ │ │ │ ├── CollectUsageRegionVisitor.java │ │ │ │ │ ├── ProcessVariables.java │ │ │ │ │ ├── UsePlace.java │ │ │ │ │ └── VarUsage.java │ │ │ │ ├── rename/ │ │ │ │ │ ├── CodeRenameVisitor.java │ │ │ │ │ ├── RenameVisitor.java │ │ │ │ │ ├── SourceFileRename.java │ │ │ │ │ └── UserRenames.java │ │ │ │ ├── shrink/ │ │ │ │ │ ├── ArgsInfo.java │ │ │ │ │ ├── CodeShrinkVisitor.java │ │ │ │ │ └── WrapInfo.java │ │ │ │ ├── ssa/ │ │ │ │ │ ├── LiveVarAnalysis.java │ │ │ │ │ ├── RenameState.java │ │ │ │ │ └── SSATransform.java │ │ │ │ ├── typeinference/ │ │ │ │ │ ├── AbstractTypeConstraint.java │ │ │ │ │ ├── BoundEnum.java │ │ │ │ │ ├── FinishTypeInference.java │ │ │ │ │ ├── FixTypesVisitor.java │ │ │ │ │ ├── ITypeBound.java │ │ │ │ │ ├── ITypeBoundDynamic.java │ │ │ │ │ ├── ITypeConstraint.java │ │ │ │ │ ├── ITypeListener.java │ │ │ │ │ ├── TypeBoundCheckCastAssign.java │ │ │ │ │ ├── TypeBoundConst.java │ │ │ │ │ ├── TypeBoundFieldGetAssign.java │ │ │ │ │ ├── TypeBoundInvokeAssign.java │ │ │ │ │ ├── TypeBoundInvokeUse.java │ │ │ │ │ ├── TypeCompare.java │ │ │ │ │ ├── TypeCompareEnum.java │ │ │ │ │ ├── TypeInferenceVisitor.java │ │ │ │ │ ├── TypeInfo.java │ │ │ │ │ ├── TypeSearch.java │ │ │ │ │ ├── TypeSearchState.java │ │ │ │ │ ├── TypeSearchVarInfo.java │ │ │ │ │ ├── TypeUpdate.java │ │ │ │ │ ├── TypeUpdateEntry.java │ │ │ │ │ ├── TypeUpdateFlags.java │ │ │ │ │ ├── TypeUpdateInfo.java │ │ │ │ │ ├── TypeUpdateRegistry.java │ │ │ │ │ └── TypeUpdateResult.java │ │ │ │ └── usage/ │ │ │ │ ├── UsageInfo.java │ │ │ │ ├── UsageInfoVisitor.java │ │ │ │ └── UseSet.java │ │ │ ├── export/ │ │ │ │ ├── ExportGradle.java │ │ │ │ ├── ExportGradleType.java │ │ │ │ ├── GradleInfoStorage.java │ │ │ │ ├── OutDirs.java │ │ │ │ ├── TemplateFile.java │ │ │ │ └── gen/ │ │ │ │ ├── AndroidGradleGenerator.java │ │ │ │ ├── GradleGeneratorTools.java │ │ │ │ ├── IExportGradleGenerator.java │ │ │ │ └── SimpleJavaGradleGenerator.java │ │ │ ├── plugins/ │ │ │ │ ├── AppContext.java │ │ │ │ ├── JadxPluginManager.java │ │ │ │ ├── JadxPluginsData.java │ │ │ │ ├── PluginContext.java │ │ │ │ ├── events/ │ │ │ │ │ ├── JadxEventsImpl.java │ │ │ │ │ └── JadxEventsManager.java │ │ │ │ ├── files/ │ │ │ │ │ ├── IJadxFilesGetter.java │ │ │ │ │ ├── JadxFilesData.java │ │ │ │ │ ├── SingleDirFilesGetter.java │ │ │ │ │ └── TempFilesGetter.java │ │ │ │ └── versions/ │ │ │ │ ├── VerifyRequiredVersion.java │ │ │ │ └── VersionComparator.java │ │ │ ├── utils/ │ │ │ │ ├── BetterName.java │ │ │ │ ├── BlockInsnPair.java │ │ │ │ ├── BlockParentContainer.java │ │ │ │ ├── BlockUtils.java │ │ │ │ ├── CacheStorage.java │ │ │ │ ├── DebugChecks.java │ │ │ │ ├── DebugChecksPass.java │ │ │ │ ├── DebugUtils.java │ │ │ │ ├── DecompilerScheduler.java │ │ │ │ ├── DotGraphUtils.java │ │ │ │ ├── EmptyBitSet.java │ │ │ │ ├── EncodedValueUtils.java │ │ │ │ ├── ErrorsCounter.java │ │ │ │ ├── FileSignature.java │ │ │ │ ├── GsonUtils.java │ │ │ │ ├── ImmutableList.java │ │ │ │ ├── InsnList.java │ │ │ │ ├── InsnRemover.java │ │ │ │ ├── InsnUtils.java │ │ │ │ ├── ListUtils.java │ │ │ │ ├── Pair.java │ │ │ │ ├── PassMerge.java │ │ │ │ ├── RegionUtils.java │ │ │ │ ├── StringUtils.java │ │ │ │ ├── Utils.java │ │ │ │ ├── android/ │ │ │ │ │ ├── AndroidManifestParser.java │ │ │ │ │ ├── AndroidResourcesMap.java │ │ │ │ │ ├── AndroidResourcesUtils.java │ │ │ │ │ ├── AppAttribute.java │ │ │ │ │ ├── ApplicationParams.java │ │ │ │ │ ├── DataInputDelegate.java │ │ │ │ │ ├── ExtDataInput.java │ │ │ │ │ ├── Res9patchStreamDecoder.java │ │ │ │ │ └── TextResMapFile.java │ │ │ │ ├── blocks/ │ │ │ │ │ ├── BlockPair.java │ │ │ │ │ ├── BlockSet.java │ │ │ │ │ └── DFSIteration.java │ │ │ │ ├── exceptions/ │ │ │ │ │ ├── CodegenException.java │ │ │ │ │ ├── DecodeException.java │ │ │ │ │ ├── InvalidDataException.java │ │ │ │ │ ├── JadxArgsValidateException.java │ │ │ │ │ ├── JadxException.java │ │ │ │ │ ├── JadxOverflowException.java │ │ │ │ │ └── JadxRuntimeException.java │ │ │ │ ├── files/ │ │ │ │ │ └── FileUtils.java │ │ │ │ ├── input/ │ │ │ │ │ └── InsnDataUtils.java │ │ │ │ ├── log/ │ │ │ │ │ └── LogUtils.java │ │ │ │ └── tasks/ │ │ │ │ └── TaskExecutor.java │ │ │ └── xmlgen/ │ │ │ ├── BinaryXMLParser.java │ │ │ ├── BinaryXMLStrings.java │ │ │ ├── CommonBinaryParser.java │ │ │ ├── IResTableParser.java │ │ │ ├── ManifestAttributes.java │ │ │ ├── ParserConstants.java │ │ │ ├── ParserStream.java │ │ │ ├── ResContainer.java │ │ │ ├── ResDecoder.java │ │ │ ├── ResNameUtils.java │ │ │ ├── ResTableBinaryParser.java │ │ │ ├── ResTableBinaryParserProvider.java │ │ │ ├── ResXmlGen.java │ │ │ ├── ResourceStorage.java │ │ │ ├── ResourcesSaver.java │ │ │ ├── StringFormattedCheck.java │ │ │ ├── XMLChar.java │ │ │ ├── XmlDeobf.java │ │ │ ├── XmlGenUtils.java │ │ │ └── entry/ │ │ │ ├── EntryConfig.java │ │ │ ├── ProtoValue.java │ │ │ ├── RawNamedValue.java │ │ │ ├── RawValue.java │ │ │ ├── ResourceEntry.java │ │ │ └── ValuesParser.java │ │ └── resources/ │ │ ├── android/ │ │ │ ├── attrs.xml │ │ │ ├── attrs_manifest.xml │ │ │ └── res-map.txt │ │ ├── clst/ │ │ │ └── core.jcst │ │ ├── export/ │ │ │ ├── android/ │ │ │ │ ├── app.build.gradle.tmpl │ │ │ │ ├── build.gradle.tmpl │ │ │ │ ├── lib.build.gradle.tmpl │ │ │ │ └── settings.gradle.tmpl │ │ │ └── java/ │ │ │ ├── build.gradle.kts.tmpl │ │ │ └── settings.gradle.kts.tmpl │ │ └── jadx/ │ │ └── core/ │ │ └── deobf/ │ │ └── conditions/ │ │ └── tlds.txt │ └── test/ │ ├── java/ │ │ └── jadx/ │ │ ├── NotYetImplemented.java │ │ ├── NotYetImplementedExtension.java │ │ ├── api/ │ │ │ ├── JadxArgsValidatorOutDirsTest.java │ │ │ ├── JadxDecompilerTest.java │ │ │ └── JadxInternalAccess.java │ │ ├── core/ │ │ │ ├── deobf/ │ │ │ │ └── NameMapperTest.java │ │ │ ├── dex/ │ │ │ │ ├── info/ │ │ │ │ │ └── AccessInfoTest.java │ │ │ │ ├── instructions/ │ │ │ │ │ └── args/ │ │ │ │ │ └── ArgTypeTest.java │ │ │ │ ├── nodes/ │ │ │ │ │ └── utils/ │ │ │ │ │ ├── SelectFromDuplicatesTest.java │ │ │ │ │ └── TypeUtilsTest.java │ │ │ │ ├── trycatch/ │ │ │ │ │ └── TryCatchBlockAttrTest.java │ │ │ │ └── visitors/ │ │ │ │ └── typeinference/ │ │ │ │ ├── PrimitiveConversionsTests.java │ │ │ │ └── TypeCompareTest.java │ │ │ ├── plugins/ │ │ │ │ └── versions/ │ │ │ │ ├── VerifyRequiredVersionTest.java │ │ │ │ └── VersionComparatorTest.java │ │ │ ├── utils/ │ │ │ │ ├── PassMergeTest.java │ │ │ │ ├── TestBetterName.java │ │ │ │ ├── TestGetBetterClassName.java │ │ │ │ ├── TestGetBetterResourceName.java │ │ │ │ ├── TypeUtilsTest.java │ │ │ │ └── log/ │ │ │ │ └── LogUtilsTest.java │ │ │ └── xmlgen/ │ │ │ ├── ResNameUtilsTest.java │ │ │ ├── ResXmlGenTest.java │ │ │ └── entry/ │ │ │ └── ValuesParserTest.java │ │ └── tests/ │ │ ├── api/ │ │ │ ├── ExportGradleTest.java │ │ │ ├── IntegrationTest.java │ │ │ ├── RaungTest.java │ │ │ ├── SmaliTest.java │ │ │ ├── compiler/ │ │ │ │ ├── ClassFileManager.java │ │ │ │ ├── CompilerOptions.java │ │ │ │ ├── DynamicClassLoader.java │ │ │ │ ├── EclipseCompilerUtils.java │ │ │ │ ├── JavaClassObject.java │ │ │ │ ├── JavaUtils.java │ │ │ │ ├── StringJavaFileObject.java │ │ │ │ └── TestCompiler.java │ │ │ ├── extensions/ │ │ │ │ └── profiles/ │ │ │ │ ├── JadxTestProfilesExtension.java │ │ │ │ ├── TestProfile.java │ │ │ │ └── TestWithProfiles.java │ │ │ └── utils/ │ │ │ ├── TestFilesGetter.java │ │ │ ├── TestUtils.java │ │ │ └── assertj/ │ │ │ ├── JadxAssertions.java │ │ │ ├── JadxClassNodeAssertions.java │ │ │ ├── JadxCodeAssertions.java │ │ │ ├── JadxCodeInfoAssertions.java │ │ │ └── JadxMethodNodeAssertions.java │ │ ├── export/ │ │ │ ├── IllegalCharsForGradleWrapper.java │ │ │ ├── OptionalTargetSdkVersion.java │ │ │ ├── TestApacheHttpClient.java │ │ │ ├── TestNonFinalResIds.java │ │ │ └── VectorDrawablesUseSupportLibrary.java │ │ ├── external/ │ │ │ └── BaseExternalTest.java │ │ ├── functional/ │ │ │ ├── AttributeStorageTest.java │ │ │ ├── JadxClasspathTest.java │ │ │ ├── JadxVisitorsOrderTest.java │ │ │ ├── NameMapperTest.java │ │ │ ├── SignatureParserTest.java │ │ │ ├── StringUtilsTest.java │ │ │ ├── TemplateFileTest.java │ │ │ └── TestIfCondition.java │ │ └── integration/ │ │ ├── android/ │ │ │ ├── TestRFieldAccess.java │ │ │ ├── TestRFieldRestore.java │ │ │ ├── TestRFieldRestore2.java │ │ │ ├── TestRFieldRestore3.java │ │ │ ├── TestResConstReplace.java │ │ │ ├── TestResConstReplace2.java │ │ │ └── TestResConstReplace3.java │ │ ├── annotations/ │ │ │ ├── TestAnnotations.java │ │ │ ├── TestAnnotations2.java │ │ │ ├── TestAnnotationsMix.java │ │ │ ├── TestAnnotationsRename.java │ │ │ ├── TestAnnotationsRenameDef.java │ │ │ ├── TestAnnotationsUsage.java │ │ │ └── TestParamAnnotations.java │ │ ├── arith/ │ │ │ ├── TestArith.java │ │ │ ├── TestArith2.java │ │ │ ├── TestArith3.java │ │ │ ├── TestArith4.java │ │ │ ├── TestArithConst.java │ │ │ ├── TestArithNot.java │ │ │ ├── TestFieldIncrement.java │ │ │ ├── TestFieldIncrement2.java │ │ │ ├── TestFieldIncrement3.java │ │ │ ├── TestNumbersFormat.java │ │ │ ├── TestPrimitivesNegate.java │ │ │ ├── TestSpecialValues.java │ │ │ ├── TestSpecialValues2.java │ │ │ └── TestXor.java │ │ ├── arrays/ │ │ │ ├── TestArrayFill.java │ │ │ ├── TestArrayFill2.java │ │ │ ├── TestArrayFill3.java │ │ │ ├── TestArrayFill4.java │ │ │ ├── TestArrayFillConstReplace.java │ │ │ ├── TestArrayFillNegative.java │ │ │ ├── TestArrayFillWithMove.java │ │ │ ├── TestArrayInit.java │ │ │ ├── TestArrayInitField.java │ │ │ ├── TestArrayInitField2.java │ │ │ ├── TestArrays.java │ │ │ ├── TestArrays2.java │ │ │ ├── TestArrays3.java │ │ │ ├── TestArrays4.java │ │ │ ├── TestFillArrayData.java │ │ │ └── TestMultiDimArrayFill.java │ │ ├── code/ │ │ │ ├── TestArrayAccessReorder.java │ │ │ └── TestCodeCommentStyle.java │ │ ├── conditions/ │ │ │ ├── TestBitwiseAnd.java │ │ │ ├── TestBitwiseOr.java │ │ │ ├── TestBooleanToByte.java │ │ │ ├── TestBooleanToChar.java │ │ │ ├── TestBooleanToDouble.java │ │ │ ├── TestBooleanToFloat.java │ │ │ ├── TestBooleanToInt.java │ │ │ ├── TestBooleanToInt2.java │ │ │ ├── TestBooleanToLong.java │ │ │ ├── TestBooleanToShort.java │ │ │ ├── TestCast.java │ │ │ ├── TestCmpOp.java │ │ │ ├── TestCmpOp2.java │ │ │ ├── TestComplexIf.java │ │ │ ├── TestComplexIf2.java │ │ │ ├── TestComplexIf3.java │ │ │ ├── TestComplexIf4.java │ │ │ ├── TestConditionInLoop.java │ │ │ ├── TestConditions.java │ │ │ ├── TestConditions10.java │ │ │ ├── TestConditions11.java │ │ │ ├── TestConditions12.java │ │ │ ├── TestConditions13.java │ │ │ ├── TestConditions14.java │ │ │ ├── TestConditions15.java │ │ │ ├── TestConditions16.java │ │ │ ├── TestConditions17.java │ │ │ ├── TestConditions18.java │ │ │ ├── TestConditions2.java │ │ │ ├── TestConditions21.java │ │ │ ├── TestConditions3.java │ │ │ ├── TestConditions4.java │ │ │ ├── TestConditions5.java │ │ │ ├── TestConditions6.java │ │ │ ├── TestConditions7.java │ │ │ ├── TestConditions8.java │ │ │ ├── TestConditions9.java │ │ │ ├── TestElseIf.java │ │ │ ├── TestElseIfCodeStyle.java │ │ │ ├── TestIfAndSwitch.java │ │ │ ├── TestIfCodeStyle.java │ │ │ ├── TestIfCodeStyle2.java │ │ │ ├── TestIfElseAndConditionIntermediateInstruction.java │ │ │ ├── TestInnerAssign.java │ │ │ ├── TestInnerAssign2.java │ │ │ ├── TestInnerAssign3.java │ │ │ ├── TestNestedIf.java │ │ │ ├── TestNestedIf2.java │ │ │ ├── TestOutBlock.java │ │ │ ├── TestSimpleConditions.java │ │ │ ├── TestTernary.java │ │ │ ├── TestTernary2.java │ │ │ ├── TestTernary3.java │ │ │ ├── TestTernary4.java │ │ │ ├── TestTernaryInIf.java │ │ │ ├── TestTernaryInIf2.java │ │ │ ├── TestTernaryInIf3.java │ │ │ ├── TestTernaryOneBranchInConstructor.java │ │ │ └── TestTernaryOneBranchInConstructor2.java │ │ ├── debuginfo/ │ │ │ ├── TestLineNumbers.java │ │ │ ├── TestLineNumbers2.java │ │ │ ├── TestLineNumbers3.java │ │ │ ├── TestReturnSourceLine.java │ │ │ └── TestVariablesNames.java │ │ ├── deobf/ │ │ │ ├── TestDontRenameClspOverriddenMethod.java │ │ │ ├── TestFieldFromInnerClass.java │ │ │ ├── TestInheritedMethodRename.java │ │ │ ├── TestMthRename.java │ │ │ ├── TestRenameOverriddenMethod.java │ │ │ ├── TestRenameOverriddenMethod2.java │ │ │ ├── TestRenameOverriddenMethod3.java │ │ │ └── a/ │ │ │ └── TestNegativeRenameCondition.java │ │ ├── enums/ │ │ │ ├── TestEnumKotlinEntries.java │ │ │ ├── TestEnumObfuscated.java │ │ │ ├── TestEnumUsesOtherEnum.java │ │ │ ├── TestEnumWithConstInlining.java │ │ │ ├── TestEnumWithFields.java │ │ │ ├── TestEnums.java │ │ │ ├── TestEnums10.java │ │ │ ├── TestEnums11.java │ │ │ ├── TestEnums2.java │ │ │ ├── TestEnums2a.java │ │ │ ├── TestEnums3.java │ │ │ ├── TestEnums4.java │ │ │ ├── TestEnums5.java │ │ │ ├── TestEnums6.java │ │ │ ├── TestEnums7.java │ │ │ ├── TestEnums8.java │ │ │ ├── TestEnums9.java │ │ │ ├── TestEnumsInterface.java │ │ │ ├── TestEnumsWithAssert.java │ │ │ ├── TestEnumsWithConsts.java │ │ │ ├── TestEnumsWithCustomInit.java │ │ │ ├── TestEnumsWithStaticFields.java │ │ │ ├── TestEnumsWithTernary.java │ │ │ ├── TestInnerEnums.java │ │ │ ├── TestSwitchOverEnum.java │ │ │ └── TestSwitchOverEnum2.java │ │ ├── fallback/ │ │ │ ├── TestFallbackManyNops.java │ │ │ └── TestFallbackMode.java │ │ ├── generics/ │ │ │ ├── TestClassSignature.java │ │ │ ├── TestConstructorGenerics.java │ │ │ ├── TestGeneric8.java │ │ │ ├── TestGenericFields.java │ │ │ ├── TestGenerics.java │ │ │ ├── TestGenerics2.java │ │ │ ├── TestGenerics3.java │ │ │ ├── TestGenerics4.java │ │ │ ├── TestGenerics6.java │ │ │ ├── TestGenerics7.java │ │ │ ├── TestGenerics8.java │ │ │ ├── TestGenericsInArgs.java │ │ │ ├── TestGenericsMthOverride.java │ │ │ ├── TestImportGenericMap.java │ │ │ ├── TestMethodOverride.java │ │ │ ├── TestMissingGenericsTypes2.java │ │ │ ├── TestOuterGeneric.java │ │ │ ├── TestSyntheticOverride.java │ │ │ ├── TestTypeVarsFromOuterClass.java │ │ │ ├── TestTypeVarsFromSuperClass.java │ │ │ └── TestUsageInGenerics.java │ │ ├── inline/ │ │ │ ├── TestConstInline.java │ │ │ ├── TestGetterInlineNegative.java │ │ │ ├── TestInline.java │ │ │ ├── TestInline2.java │ │ │ ├── TestInline3.java │ │ │ ├── TestInline6.java │ │ │ ├── TestInline7.java │ │ │ ├── TestInstanceLambda.java │ │ │ ├── TestIssue86.java │ │ │ ├── TestMethodInline.java │ │ │ ├── TestOverlapSyntheticMethods.java │ │ │ ├── TestOverrideBridgeMerge.java │ │ │ ├── TestSyntheticBridgeRename.java │ │ │ ├── TestSyntheticClassInline.java │ │ │ ├── TestSyntheticInline.java │ │ │ ├── TestSyntheticInline2.java │ │ │ ├── TestSyntheticInline3.java │ │ │ └── TestTernaryCast.java │ │ ├── inner/ │ │ │ ├── TestAnonymousClass.java │ │ │ ├── TestAnonymousClass10.java │ │ │ ├── TestAnonymousClass11.java │ │ │ ├── TestAnonymousClass12.java │ │ │ ├── TestAnonymousClass13.java │ │ │ ├── TestAnonymousClass14.java │ │ │ ├── TestAnonymousClass15.java │ │ │ ├── TestAnonymousClass16.java │ │ │ ├── TestAnonymousClass17.java │ │ │ ├── TestAnonymousClass18.java │ │ │ ├── TestAnonymousClass19.java │ │ │ ├── TestAnonymousClass2.java │ │ │ ├── TestAnonymousClass20.java │ │ │ ├── TestAnonymousClass21.java │ │ │ ├── TestAnonymousClass22.java │ │ │ ├── TestAnonymousClass3.java │ │ │ ├── TestAnonymousClass3a.java │ │ │ ├── TestAnonymousClass4.java │ │ │ ├── TestAnonymousClass5.java │ │ │ ├── TestAnonymousClass6.java │ │ │ ├── TestAnonymousClass7.java │ │ │ ├── TestAnonymousClass8.java │ │ │ ├── TestAnonymousClass9.java │ │ │ ├── TestIncorrectAnonymousClass.java │ │ │ ├── TestInner2Samples.java │ │ │ ├── TestInnerClass.java │ │ │ ├── TestInnerClass2.java │ │ │ ├── TestInnerClass3.java │ │ │ ├── TestInnerClass4.java │ │ │ ├── TestInnerClass5.java │ │ │ ├── TestInnerClassFakeSyntheticConstructor.java │ │ │ ├── TestInnerClassSyntheticConstructor.java │ │ │ ├── TestInnerClassSyntheticRename.java │ │ │ ├── TestInnerConstructorCall.java │ │ │ ├── TestNestedAnonymousClass.java │ │ │ ├── TestOuterConstructorCall.java │ │ │ ├── TestReplaceConstsInAnnotations.java │ │ │ ├── TestReplaceConstsInAnnotations2.java │ │ │ └── TestSyntheticMthRename.java │ │ ├── invoke/ │ │ │ ├── TestCastInOverloadedAccessor.java │ │ │ ├── TestCastInOverloadedInvoke.java │ │ │ ├── TestCastInOverloadedInvoke2.java │ │ │ ├── TestCastInOverloadedInvoke3.java │ │ │ ├── TestCastInOverloadedInvoke4.java │ │ │ ├── TestConstructorWithMoves.java │ │ │ ├── TestHierarchyOverloadedInvoke.java │ │ │ ├── TestInheritedStaticInvoke.java │ │ │ ├── TestInvoke1.java │ │ │ ├── TestInvokeInCatch.java │ │ │ ├── TestInvokeWithWideVars.java │ │ │ ├── TestOverloadedInvoke.java │ │ │ ├── TestOverloadedMethodInvoke.java │ │ │ ├── TestOverloadedMethodInvoke2.java │ │ │ ├── TestPolymorphicInvoke.java │ │ │ ├── TestPolymorphicRangeInvoke.java │ │ │ ├── TestRawCustomInvoke.java │ │ │ ├── TestSuperInvoke.java │ │ │ ├── TestSuperInvoke2.java │ │ │ ├── TestSuperInvokeUnknown.java │ │ │ ├── TestSuperInvokeWithGenerics.java │ │ │ ├── TestVarArg.java │ │ │ └── TestVarArg2.java │ │ ├── java8/ │ │ │ ├── TestLambdaArgs.java │ │ │ ├── TestLambdaConstructor.java │ │ │ ├── TestLambdaExtVar.java │ │ │ ├── TestLambdaExtVar2.java │ │ │ ├── TestLambdaInArray.java │ │ │ ├── TestLambdaInstance.java │ │ │ ├── TestLambdaInstance2.java │ │ │ ├── TestLambdaInstance3.java │ │ │ ├── TestLambdaResugar.java │ │ │ ├── TestLambdaReturn.java │ │ │ └── TestLambdaStatic.java │ │ ├── jbc/ │ │ │ ├── TestDup2x1.java │ │ │ └── TestStackConvert.java │ │ ├── loops/ │ │ │ ├── TestArrayForEach.java │ │ │ ├── TestArrayForEach2.java │ │ │ ├── TestArrayForEach3.java │ │ │ ├── TestArrayForEachNegative.java │ │ │ ├── TestBreakInComplexIf.java │ │ │ ├── TestBreakInComplexIf2.java │ │ │ ├── TestBreakInLoop.java │ │ │ ├── TestBreakInLoop2.java │ │ │ ├── TestBreakInLoop3.java │ │ │ ├── TestBreakInLoop4.java │ │ │ ├── TestBreakInLoop5.java │ │ │ ├── TestBreakInLoop6.java │ │ │ ├── TestBreakWithLabel.java │ │ │ ├── TestComplexWhileLoop.java │ │ │ ├── TestContinueInLoop.java │ │ │ ├── TestDoWhileBreak.java │ │ │ ├── TestDoWhileBreak2.java │ │ │ ├── TestDoWhileBreak3.java │ │ │ ├── TestEndlessLoop.java │ │ │ ├── TestEndlessLoop2.java │ │ │ ├── TestIfInLoop2.java │ │ │ ├── TestIfInLoop3.java │ │ │ ├── TestIfInLoop4.java │ │ │ ├── TestIndexForLoop.java │ │ │ ├── TestIndexedLoop.java │ │ │ ├── TestIterableForEach.java │ │ │ ├── TestIterableForEach2.java │ │ │ ├── TestIterableForEach3.java │ │ │ ├── TestIterableForEach4.java │ │ │ ├── TestLoopCondition.java │ │ │ ├── TestLoopCondition2.java │ │ │ ├── TestLoopCondition3.java │ │ │ ├── TestLoopCondition4.java │ │ │ ├── TestLoopCondition5.java │ │ │ ├── TestLoopConditionInvoke.java │ │ │ ├── TestLoopDetection.java │ │ │ ├── TestLoopDetection2.java │ │ │ ├── TestLoopDetection3.java │ │ │ ├── TestLoopDetection4.java │ │ │ ├── TestLoopDetection5.java │ │ │ ├── TestLoopRestore.java │ │ │ ├── TestLoopRestore2.java │ │ │ ├── TestLoopRestore3.java │ │ │ ├── TestMultiEntryLoop.java │ │ │ ├── TestMultiEntryLoop2.java │ │ │ ├── TestNestedLoops.java │ │ │ ├── TestNestedLoops2.java │ │ │ ├── TestNestedLoops3.java │ │ │ ├── TestNestedLoops4.java │ │ │ ├── TestNestedLoops5.java │ │ │ ├── TestNotIndexedLoop.java │ │ │ ├── TestSequentialLoops.java │ │ │ ├── TestSequentialLoops2.java │ │ │ ├── TestSynchronizedInEndlessLoop.java │ │ │ ├── TestTryCatchInLoop.java │ │ │ └── TestTryCatchInLoop2.java │ │ ├── names/ │ │ │ ├── TestCaseSensitiveChecks.java │ │ │ ├── TestClassNameWithInvalidChar.java │ │ │ ├── TestClassNamesCollision.java │ │ │ ├── TestClassNamesCollision2.java │ │ │ ├── TestCollisionWithJavaLangClasses.java │ │ │ ├── TestConstructorArgNames.java │ │ │ ├── TestDefPkgRename.java │ │ │ ├── TestDuplicateVarNames.java │ │ │ ├── TestDuplicatedNames.java │ │ │ ├── TestFieldCollideWithPackage.java │ │ │ ├── TestLocalVarCollideWithPackage.java │ │ │ ├── TestNameAssign2.java │ │ │ ├── TestReservedClassNames.java │ │ │ ├── TestReservedNames.java │ │ │ ├── TestReservedPackageNames.java │ │ │ ├── TestSameMethodsNames.java │ │ │ ├── pkg/ │ │ │ │ ├── a.java │ │ │ │ └── b.java │ │ │ └── pkg2/ │ │ │ ├── System.java │ │ │ └── TestCls.java │ │ ├── others/ │ │ │ ├── TestAllNops.java │ │ │ ├── TestArgInline.java │ │ │ ├── TestBadClassAccessModifiers.java │ │ │ ├── TestBadMethodAccessModifiers.java │ │ │ ├── TestCastOfNull.java │ │ │ ├── TestClassGen.java │ │ │ ├── TestClassImplementsSignature.java │ │ │ ├── TestClassReGen.java │ │ │ ├── TestCodeComments.java │ │ │ ├── TestCodeComments2.java │ │ │ ├── TestCodeComments2a.java │ │ │ ├── TestCodeCommentsMultiline.java │ │ │ ├── TestCodeCommentsOverride.java │ │ │ ├── TestCodeMetadata.java │ │ │ ├── TestCodeMetadata2.java │ │ │ ├── TestCodeMetadata3.java │ │ │ ├── TestConstReplace.java │ │ │ ├── TestConstStringConcat.java │ │ │ ├── TestConstructor.java │ │ │ ├── TestConstructor2.java │ │ │ ├── TestConstructorBranched.java │ │ │ ├── TestConstructorBranched2.java │ │ │ ├── TestConstructorBranched3.java │ │ │ ├── TestDeadBlockReferencesStart.java │ │ │ ├── TestDeboxing.java │ │ │ ├── TestDeboxing2.java │ │ │ ├── TestDeboxing3.java │ │ │ ├── TestDeboxing4.java │ │ │ ├── TestDeboxing5.java │ │ │ ├── TestDefConstructorNotRemoved.java │ │ │ ├── TestDefConstructorWithAnnotation.java │ │ │ ├── TestDuplicateCast.java │ │ │ ├── TestExplicitOverride.java │ │ │ ├── TestFieldAccessReorder.java │ │ │ ├── TestFieldInit2.java │ │ │ ├── TestFieldInit3.java │ │ │ ├── TestFieldInitInTryCatch.java │ │ │ ├── TestFieldInitNegative.java │ │ │ ├── TestFieldInitOrder.java │ │ │ ├── TestFieldInitOrder2.java │ │ │ ├── TestFieldInitOrderStatic.java │ │ │ ├── TestFieldUsageMove.java │ │ │ ├── TestFixClassAccessModifiers.java │ │ │ ├── TestFloatValue.java │ │ │ ├── TestIfInTry.java │ │ │ ├── TestIfTryInCatch.java │ │ │ ├── TestIncorrectFieldSignature.java │ │ │ ├── TestIncorrectMethodSignature.java │ │ │ ├── TestInlineVarArg.java │ │ │ ├── TestInsnsBeforeSuper.java │ │ │ ├── TestInsnsBeforeSuper2.java │ │ │ ├── TestInsnsBeforeThis.java │ │ │ ├── TestInterfaceDefaultMethod.java │ │ │ ├── TestInvalidExceptions.java │ │ │ ├── TestInvalidExceptions2.java │ │ │ ├── TestIssue13a.java │ │ │ ├── TestIssue13b.java │ │ │ ├── TestJavaDup2x2.java │ │ │ ├── TestJavaDupInsn.java │ │ │ ├── TestJavaJSR.java │ │ │ ├── TestJavaSwap.java │ │ │ ├── TestJsonOutput.java │ │ │ ├── TestLoopInTry.java │ │ │ ├── TestMethodParametersAttribute.java │ │ │ ├── TestMissingExceptions.java │ │ │ ├── TestMoveInline.java │ │ │ ├── TestMultipleNOPs.java │ │ │ ├── TestN21.java │ │ │ ├── TestNullInline.java │ │ │ ├── TestOverridePackagePrivateMethod.java │ │ │ ├── TestOverridePrivateMethod.java │ │ │ ├── TestOverrideStaticMethod.java │ │ │ ├── TestOverrideWithSameName.java │ │ │ ├── TestOverrideWithTwoBases.java │ │ │ ├── TestOverrideWithTwoBases2.java │ │ │ ├── TestPrimitiveCasts.java │ │ │ ├── TestPrimitiveCasts2.java │ │ │ ├── TestRedundantBrackets.java │ │ │ ├── TestRedundantReturn.java │ │ │ ├── TestReturnWrapping.java │ │ │ ├── TestShadowingSuperMember.java │ │ │ ├── TestStaticFieldsInit.java │ │ │ ├── TestStaticMethod.java │ │ │ ├── TestStringBuilderElimination.java │ │ │ ├── TestStringBuilderElimination2.java │ │ │ ├── TestStringBuilderElimination3.java │ │ │ ├── TestStringBuilderElimination4Neg.java │ │ │ ├── TestStringBuilderElimination5.java │ │ │ ├── TestStringConcatJava11.java │ │ │ ├── TestStringConcatWithoutResult.java │ │ │ ├── TestStringConstructor.java │ │ │ ├── TestSuperLoop.java │ │ │ ├── TestSyntheticConstructor.java │ │ │ ├── TestThrows.java │ │ │ ├── TestUsageApacheHttpClient.java │ │ │ ├── TestWrongCode.java │ │ │ └── TestWrongCode2.java │ │ ├── rename/ │ │ │ ├── TestAnonymousInline.java │ │ │ ├── TestConstReplace.java │ │ │ ├── TestFieldRenameFormat.java │ │ │ ├── TestFieldWithGenericRename.java │ │ │ ├── TestRenameEnum.java │ │ │ ├── TestUserRenames.java │ │ │ └── TestUsingSourceFileName.java │ │ ├── special/ │ │ │ └── TestPackageInfoSupport.java │ │ ├── switches/ │ │ │ ├── TestSwitch.java │ │ │ ├── TestSwitch2.java │ │ │ ├── TestSwitch3.java │ │ │ ├── TestSwitch4.java │ │ │ ├── TestSwitchBreak.java │ │ │ ├── TestSwitchBreak2.java │ │ │ ├── TestSwitchBreak3.java │ │ │ ├── TestSwitchBreak4.java │ │ │ ├── TestSwitchContinue.java │ │ │ ├── TestSwitchFallThrough.java │ │ │ ├── TestSwitchInLoop.java │ │ │ ├── TestSwitchInLoop2.java │ │ │ ├── TestSwitchInLoop3.java │ │ │ ├── TestSwitchInLoop4.java │ │ │ ├── TestSwitchInLoop5.java │ │ │ ├── TestSwitchInLoop6.java │ │ │ ├── TestSwitchInLoop7.java │ │ │ ├── TestSwitchInLoop8.java │ │ │ ├── TestSwitchInLoop9.java │ │ │ ├── TestSwitchLabels.java │ │ │ ├── TestSwitchNoDefault.java │ │ │ ├── TestSwitchOverStrings.java │ │ │ ├── TestSwitchOverStrings2.java │ │ │ ├── TestSwitchOverStrings3.java │ │ │ ├── TestSwitchReturnFromCase.java │ │ │ ├── TestSwitchReturnFromCase2.java │ │ │ ├── TestSwitchSimple.java │ │ │ ├── TestSwitchWithFallThroughCase.java │ │ │ ├── TestSwitchWithFallThroughCase2.java │ │ │ ├── TestSwitchWithThrow.java │ │ │ └── TestSwitchWithTryCatch.java │ │ ├── synchronize/ │ │ │ ├── TestNestedSynchronize.java │ │ │ ├── TestSynchronized.java │ │ │ ├── TestSynchronized2.java │ │ │ ├── TestSynchronized3.java │ │ │ ├── TestSynchronized4.java │ │ │ ├── TestSynchronized5.java │ │ │ └── TestSynchronized6.java │ │ ├── trycatch/ │ │ │ ├── TestEmptyCatch.java │ │ │ ├── TestEmptyFinally.java │ │ │ ├── TestFinally.java │ │ │ ├── TestFinally2.java │ │ │ ├── TestFinally3.java │ │ │ ├── TestFinallyExtract.java │ │ │ ├── TestIfInTryCatch.java │ │ │ ├── TestInlineInCatch.java │ │ │ ├── TestLoopInTryCatch.java │ │ │ ├── TestMultiExceptionCatch.java │ │ │ ├── TestMultiExceptionCatch2.java │ │ │ ├── TestMultiExceptionCatchSameJump.java │ │ │ ├── TestNestedTryCatch.java │ │ │ ├── TestNestedTryCatch2.java │ │ │ ├── TestNestedTryCatch3.java │ │ │ ├── TestNestedTryCatch4.java │ │ │ ├── TestNestedTryCatch5.java │ │ │ ├── TestTryAfterDeclaration.java │ │ │ ├── TestTryCatch.java │ │ │ ├── TestTryCatch10.java │ │ │ ├── TestTryCatch11.java │ │ │ ├── TestTryCatch2.java │ │ │ ├── TestTryCatch6.java │ │ │ ├── TestTryCatch7.java │ │ │ ├── TestTryCatch8.java │ │ │ ├── TestTryCatch9.java │ │ │ ├── TestTryCatchFinally.java │ │ │ ├── TestTryCatchFinally10.java │ │ │ ├── TestTryCatchFinally11.java │ │ │ ├── TestTryCatchFinally12.java │ │ │ ├── TestTryCatchFinally13.java │ │ │ ├── TestTryCatchFinally14.java │ │ │ ├── TestTryCatchFinally15.java │ │ │ ├── TestTryCatchFinally16.java │ │ │ ├── TestTryCatchFinally17.java │ │ │ ├── TestTryCatchFinally18.java │ │ │ ├── TestTryCatchFinally19.java │ │ │ ├── TestTryCatchFinally2.java │ │ │ ├── TestTryCatchFinally3.java │ │ │ ├── TestTryCatchFinally4.java │ │ │ ├── TestTryCatchFinally5.java │ │ │ ├── TestTryCatchFinally6.java │ │ │ ├── TestTryCatchFinally7.java │ │ │ ├── TestTryCatchFinally8.java │ │ │ ├── TestTryCatchFinally9.java │ │ │ ├── TestTryCatchInIf.java │ │ │ ├── TestTryCatchInIf2.java │ │ │ ├── TestTryCatchLastInsn.java │ │ │ ├── TestTryCatchMultiException.java │ │ │ ├── TestTryCatchMultiException2.java │ │ │ ├── TestTryCatchNoMoveExc.java │ │ │ ├── TestTryCatchNoMoveExc2.java │ │ │ ├── TestTryCatchStartOnMove.java │ │ │ ├── TestTryWithEmptyCatch.java │ │ │ ├── TestTryWithEmptyCatchTriple.java │ │ │ ├── TestTryWithResources.java │ │ │ ├── TestUnreachableCatch.java │ │ │ └── TestUnreachableCatch2.java │ │ ├── types/ │ │ │ ├── TestArrayTypes.java │ │ │ ├── TestConstInline.java │ │ │ ├── TestConstTypeInference.java │ │ │ ├── TestFieldAccess.java │ │ │ ├── TestFieldCast.java │ │ │ ├── TestGenerics.java │ │ │ ├── TestGenerics2.java │ │ │ ├── TestGenerics3.java │ │ │ ├── TestGenerics4.java │ │ │ ├── TestGenerics5.java │ │ │ ├── TestGenerics6.java │ │ │ ├── TestGenerics7.java │ │ │ ├── TestGenerics8.java │ │ │ ├── TestGenericsInFullInnerCls.java │ │ │ ├── TestInterfacesCast.java │ │ │ ├── TestLongCast.java │ │ │ ├── TestPrimitiveConversion.java │ │ │ ├── TestPrimitiveConversion2.java │ │ │ ├── TestPrimitivesInIf.java │ │ │ ├── TestTypeInheritance.java │ │ │ ├── TestTypeResolver.java │ │ │ ├── TestTypeResolver10.java │ │ │ ├── TestTypeResolver11.java │ │ │ ├── TestTypeResolver12.java │ │ │ ├── TestTypeResolver13.java │ │ │ ├── TestTypeResolver14.java │ │ │ ├── TestTypeResolver15.java │ │ │ ├── TestTypeResolver16.java │ │ │ ├── TestTypeResolver17.java │ │ │ ├── TestTypeResolver18.java │ │ │ ├── TestTypeResolver19.java │ │ │ ├── TestTypeResolver2.java │ │ │ ├── TestTypeResolver20.java │ │ │ ├── TestTypeResolver21.java │ │ │ ├── TestTypeResolver22.java │ │ │ ├── TestTypeResolver23.java │ │ │ ├── TestTypeResolver24.java │ │ │ ├── TestTypeResolver25.java │ │ │ ├── TestTypeResolver26.java │ │ │ ├── TestTypeResolver3.java │ │ │ ├── TestTypeResolver4.java │ │ │ ├── TestTypeResolver5.java │ │ │ ├── TestTypeResolver6.java │ │ │ ├── TestTypeResolver6a.java │ │ │ ├── TestTypeResolver7.java │ │ │ ├── TestTypeResolver8.java │ │ │ └── TestTypeResolver9.java │ │ ├── usethis/ │ │ │ ├── TestDontInlineThis.java │ │ │ ├── TestInlineThis.java │ │ │ ├── TestInlineThis2.java │ │ │ └── TestRedundantThis.java │ │ └── variables/ │ │ ├── TestThisBranchDup.java │ │ ├── TestVariables2.java │ │ ├── TestVariables3.java │ │ ├── TestVariables4.java │ │ ├── TestVariables5.java │ │ ├── TestVariables6.java │ │ ├── TestVariablesDeclAnnotation.java │ │ ├── TestVariablesDefinitions.java │ │ ├── TestVariablesDefinitions2.java │ │ ├── TestVariablesGeneric.java │ │ ├── TestVariablesIfElseChain.java │ │ ├── TestVariablesInInlinedAssign.java │ │ ├── TestVariablesInLoop.java │ │ └── TestVariablesUsageWithLoops.java │ ├── raung/ │ │ ├── enums/ │ │ │ └── TestEnums11.raung │ │ ├── java8/ │ │ │ └── TestLambdaInstance3.raung │ │ ├── jbc/ │ │ │ └── TestStackConvert.raung │ │ ├── loops/ │ │ │ └── TestLoopRestore2.raung │ │ └── others/ │ │ ├── TestClassImplementsSignature.raung │ │ ├── TestJavaDup2x2.raung │ │ ├── TestJavaJSR.raung │ │ ├── TestJavaSwap.raung │ │ └── TestStringConcatJava11.raung │ ├── resources/ │ │ ├── logback.xml │ │ ├── manifest/ │ │ │ ├── IllegalCharsForGradleWrapper.xml │ │ │ ├── MinSdkVersion25.xml │ │ │ ├── OptionalTargetSdkVersion.xml │ │ │ └── strings.xml │ │ ├── mockito-extensions/ │ │ │ └── org.mockito.plugins.MockMaker │ │ └── test-samples/ │ │ ├── app-with-fake-dex.apk │ │ └── hello.dex │ └── smali/ │ ├── arith/ │ │ ├── TestArithConst.smali │ │ ├── TestArithNot.smali │ │ └── TestXor.smali │ ├── arrays/ │ │ ├── TestArrayFillWithMove/ │ │ │ └── TestCls.smali │ │ ├── TestArrayInitField2.smali │ │ └── TestFillArrayData/ │ │ └── TestCls.smali │ ├── conditions/ │ │ ├── TestBooleanToByte.smali │ │ ├── TestBooleanToChar.smali │ │ ├── TestBooleanToDouble.smali │ │ ├── TestBooleanToFloat.smali │ │ ├── TestBooleanToInt.smali │ │ ├── TestBooleanToInt2.smali │ │ ├── TestBooleanToLong.smali │ │ ├── TestBooleanToShort.smali │ │ ├── TestComplexIf.smali │ │ ├── TestComplexIf2.smali │ │ ├── TestComplexIf3.smali │ │ ├── TestComplexIf4.smali │ │ ├── TestConditions18.smali │ │ ├── TestConditions21.smali │ │ ├── TestIfAndSwitch.smali │ │ ├── TestIfCodeStyle.smali │ │ ├── TestIfCodeStyle2.smali │ │ ├── TestIfElseAndConditionIntermediateInstruction.smali │ │ ├── TestInnerAssign3.smali │ │ ├── TestOutBlock.smali │ │ ├── TestTernary4.smali │ │ ├── TestTernaryInIf2.smali │ │ ├── TestTernaryInIf3.smali │ │ └── TestTernaryOneBranchInConstructor2.smali │ ├── debuginfo/ │ │ └── TestVariablesNames.smali │ ├── enums/ │ │ ├── TestEnumKotlinEntries.smali │ │ ├── TestEnumObfuscated.smali │ │ ├── TestEnumUsesOtherEnum.smali │ │ ├── TestEnumWithFields.smali │ │ ├── TestEnums10.smali │ │ ├── TestEnums5.smali │ │ ├── TestEnums8.smali │ │ ├── TestEnumsWithStaticFields.smali │ │ └── TestSwitchOverEnum/ │ │ ├── Count.smali │ │ └── TestSwitchOverEnum.smali │ ├── fallback/ │ │ └── TestFallbackManyNops.smali │ ├── generics/ │ │ ├── TestClassSignature.smali │ │ ├── TestMethodOverride.smali │ │ ├── TestMissingGenericsTypes2.smali │ │ └── TestSyntheticOverride/ │ │ ├── KotlinFunction1.smali │ │ └── TestSyntheticOverride.smali │ ├── inline/ │ │ ├── TestGetterInlineNegative.smali │ │ ├── TestInline7.smali │ │ ├── TestInstanceLambda/ │ │ │ ├── Lambda.smali │ │ │ └── TestCls.smali │ │ ├── TestMethodInline/ │ │ │ ├── A.smali │ │ │ ├── B.smali │ │ │ └── C.smali │ │ ├── TestOverlapSyntheticMethods.smali │ │ ├── TestOverrideBridgeMerge.smali │ │ ├── TestSyntheticClassInline/ │ │ │ ├── A.smali │ │ │ └── B.smali │ │ └── TestSyntheticInline3/ │ │ ├── KotlinFunction1.smali │ │ ├── TestSyntheticInline3$onCreate$1.smali │ │ └── TestSyntheticInline3.smali │ ├── inner/ │ │ ├── TestAnonymousClass14/ │ │ │ ├── OuterCls$1.smali │ │ │ ├── OuterCls$TestCls.smali │ │ │ └── OuterCls.smali │ │ ├── TestAnonymousClass19/ │ │ │ ├── ATestCls.smali │ │ │ └── Lambda$TestCls$1.smali │ │ ├── TestIncorrectAnonymousClass/ │ │ │ ├── TestCls$1.smali │ │ │ └── TestCls.smali │ │ ├── TestInnerClassFakeSyntheticConstructor.smali │ │ ├── TestInnerClassSyntheticRename.smali │ │ ├── TestNestedAnonymousClass/ │ │ │ ├── A.smali │ │ │ ├── B.smali │ │ │ └── C.smali │ │ └── TestSyntheticMthRename/ │ │ ├── TestCls$A.smali │ │ ├── TestCls$I.smali │ │ └── TestCls.smali │ ├── invoke/ │ │ ├── TestCastInOverloadedInvoke2.smali │ │ ├── TestConstructorWithMoves.smali │ │ ├── TestPolymorphicInvoke.smali │ │ └── TestRawCustomInvoke.smali │ ├── loops/ │ │ ├── TestBreakInLoop6.smali │ │ ├── TestEndlessLoop2.smali │ │ ├── TestIfInLoop4.smali │ │ ├── TestLoopCondition5.smali │ │ ├── TestLoopRestore.smali │ │ ├── TestLoopRestore3.smali │ │ ├── TestMultiEntryLoop.smali │ │ └── TestMultiEntryLoop2.smali │ ├── names/ │ │ ├── TestCaseSensitiveChecks/ │ │ │ ├── 1.smali │ │ │ └── 2.smali │ │ ├── TestClassNameWithInvalidChar/ │ │ │ ├── a.smali │ │ │ └── b.smali │ │ ├── TestDefPkgRename/ │ │ │ ├── a.smali │ │ │ └── b.smali │ │ ├── TestDuplicatedNames.smali │ │ ├── TestFieldCollideWithPackage/ │ │ │ ├── 1.smali │ │ │ └── 2.smali │ │ ├── TestLocalVarCollideWithPackage/ │ │ │ ├── 1.smali │ │ │ ├── 2.smali │ │ │ └── 3.smali │ │ ├── TestReservedClassNames.smali │ │ ├── TestReservedNames.smali │ │ └── TestReservedPackageNames/ │ │ └── a.smali │ ├── others/ │ │ ├── TestAllNops.smali │ │ ├── TestBadClassAccessModifiers/ │ │ │ ├── A.smali │ │ │ ├── B$BB$BBB.smali │ │ │ ├── B$BB.smali │ │ │ └── B.smali │ │ ├── TestBadMethodAccessModifiers/ │ │ │ ├── TestCls$A.smali │ │ │ ├── TestCls$B.smali │ │ │ └── TestCls.smali │ │ ├── TestConstructor.smali │ │ ├── TestConstructor2/ │ │ │ ├── A.smali │ │ │ └── TestConstructor2.smali │ │ ├── TestConstructorBranched.smali │ │ ├── TestConstructorBranched2.smali │ │ ├── TestConstructorBranched3.smali │ │ ├── TestDeadBlockReferencesStart.smali │ │ ├── TestExplicitOverride.smali │ │ ├── TestFieldInitOrder2.smali │ │ ├── TestFieldUsageMove.smali │ │ ├── TestFixClassAccessModifiers/ │ │ │ ├── Cls.smali │ │ │ ├── InnerCls.smali │ │ │ └── TestCls.smali │ │ ├── TestIncorrectFieldSignature.smali │ │ ├── TestIncorrectMethodSignature.smali │ │ ├── TestInlineVarArg.smali │ │ ├── TestInsnsBeforeSuper/ │ │ │ ├── A.smali │ │ │ └── B.smali │ │ ├── TestInsnsBeforeSuper2.smali │ │ ├── TestInsnsBeforeThis.smali │ │ ├── TestInvalidExceptions.smali │ │ ├── TestInvalidExceptions2.smali │ │ ├── TestMissingExceptions.smali │ │ ├── TestMoveInline.smali │ │ ├── TestMultipleNOPs/ │ │ │ └── test.smali │ │ ├── TestN21.smali │ │ ├── TestOverridePackagePrivateMethod/ │ │ │ ├── A.smali │ │ │ ├── B.smali │ │ │ └── C.smali │ │ ├── TestOverrideWithSameName/ │ │ │ ├── A.smali │ │ │ ├── B.smali │ │ │ └── C.smali │ │ ├── TestSuperLoop/ │ │ │ ├── A.smali │ │ │ └── B.smali │ │ ├── TestSyntheticConstructor/ │ │ │ ├── BuggyConstructor.smali │ │ │ └── Test.smali │ │ └── TestUsageApacheHttpClient.smali │ ├── rename/ │ │ └── TestUsingSourceFileName/ │ │ └── b.smali │ ├── special/ │ │ └── TestPackageInfoSupport/ │ │ ├── pkg1.smali │ │ ├── pkg2.smali │ │ └── pkg3.smali │ ├── switches/ │ │ ├── TestSwitchOverStrings3.smali │ │ └── TestSwitchOverStrings4.smali │ ├── synchronize/ │ │ ├── TestNestedSynchronize.smali │ │ ├── TestSynchronized4.smali │ │ ├── TestSynchronized5.smali │ │ └── TestSynchronized6.smali │ ├── trycatch/ │ │ ├── TestEmptyCatch.smali │ │ ├── TestFinally3.smali │ │ ├── TestLoopInTryCatch.smali │ │ ├── TestMultiExceptionCatchSameJump.smali │ │ ├── TestNestedTryCatch4.smali │ │ ├── TestNestedTryCatch5.smali │ │ ├── TestTryCatch10.smali │ │ ├── TestTryCatchFinally10.smali │ │ ├── TestTryCatchFinally15.smali │ │ ├── TestTryCatchLastInsn.smali │ │ ├── TestTryCatchMultiException2.smali │ │ ├── TestTryCatchNoMoveExc.smali │ │ ├── TestTryCatchNoMoveExc2.smali │ │ ├── TestTryCatchStartOnMove.smali │ │ ├── TestTryWithEmptyCatchTriple.smali │ │ └── TestUnreachableCatch.smali │ ├── types/ │ │ ├── TestConstInline.smali │ │ ├── TestGenerics2.smali │ │ ├── TestGenericsInFullInnerCls/ │ │ │ ├── FieldCls.smali │ │ │ ├── ba.smali │ │ │ ├── bb.smali │ │ │ ├── bc.smali │ │ │ └── n.smali │ │ ├── TestPrimitiveConversion.smali │ │ ├── TestPrimitiveConversion2.smali │ │ ├── TestTypeResolver10.smali │ │ ├── TestTypeResolver14.smali │ │ ├── TestTypeResolver15.smali │ │ ├── TestTypeResolver16.smali │ │ ├── TestTypeResolver17.smali │ │ ├── TestTypeResolver20/ │ │ │ ├── Sequence.smali │ │ │ └── TestTypeResolver20.smali │ │ ├── TestTypeResolver21.smali │ │ ├── TestTypeResolver24/ │ │ │ ├── T1.smali │ │ │ ├── T2.smali │ │ │ └── Test1.smali │ │ ├── TestTypeResolver25.smali │ │ ├── TestTypeResolver5.smali │ │ └── TestTypeResolver8/ │ │ ├── A.smali │ │ ├── B.smali │ │ └── TestCls.smali │ └── variables/ │ ├── TestThisBranchDup.smali │ ├── TestVariables6.smali │ ├── TestVariablesGeneric.smali │ └── TestVariablesInLoop.smali ├── jadx-gui/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── jadx/ │ │ │ └── gui/ │ │ │ ├── JadxGUI.java │ │ │ ├── JadxWrapper.java │ │ │ ├── cache/ │ │ │ │ ├── code/ │ │ │ │ │ ├── CodeCacheMode.java │ │ │ │ │ ├── CodeStringCache.java │ │ │ │ │ ├── FixedCodeCache.java │ │ │ │ │ └── disk/ │ │ │ │ │ ├── BufferCodeCache.java │ │ │ │ │ ├── CodeMetadataAdapter.java │ │ │ │ │ ├── DiskCodeCache.java │ │ │ │ │ └── adapters/ │ │ │ │ │ ├── ArgTypeAdapter.java │ │ │ │ │ ├── ClassNodeAdapter.java │ │ │ │ │ ├── CodeAnnotationAdapter.java │ │ │ │ │ ├── DataAdapter.java │ │ │ │ │ ├── DataAdapterHelper.java │ │ │ │ │ ├── FieldNodeAdapter.java │ │ │ │ │ ├── InsnCodeOffsetAdapter.java │ │ │ │ │ ├── MethodNodeAdapter.java │ │ │ │ │ ├── NodeDeclareRefAdapter.java │ │ │ │ │ ├── NodeEndAdapter.java │ │ │ │ │ ├── VarNodeAdapter.java │ │ │ │ │ └── VarRefAdapter.java │ │ │ │ ├── manager/ │ │ │ │ │ ├── CacheEntry.java │ │ │ │ │ └── CacheManager.java │ │ │ │ └── usage/ │ │ │ │ ├── CachedMethodRef.java │ │ │ │ ├── ClsUsageData.java │ │ │ │ ├── CollectUsageData.java │ │ │ │ ├── FldRef.java │ │ │ │ ├── FldUsageData.java │ │ │ │ ├── MthRef.java │ │ │ │ ├── MthUsageData.java │ │ │ │ ├── RawUsageData.java │ │ │ │ ├── UsageCacheMode.java │ │ │ │ ├── UsageData.java │ │ │ │ ├── UsageFileAdapter.java │ │ │ │ └── UsageInfoCache.java │ │ │ ├── device/ │ │ │ │ ├── debugger/ │ │ │ │ │ ├── ArtAdapter.java │ │ │ │ │ ├── BreakpointManager.java │ │ │ │ │ ├── DbgUtils.java │ │ │ │ │ ├── DebugController.java │ │ │ │ │ ├── DebugSettings.java │ │ │ │ │ ├── EventListenerAdapter.java │ │ │ │ │ ├── LogcatController.java │ │ │ │ │ ├── RegisterObserver.java │ │ │ │ │ ├── RuntimeType.java │ │ │ │ │ ├── SmaliDebugger.java │ │ │ │ │ ├── SmaliDebuggerException.java │ │ │ │ │ ├── SuspendInfo.java │ │ │ │ │ └── smali/ │ │ │ │ │ ├── RegisterInfo.java │ │ │ │ │ ├── Smali.java │ │ │ │ │ ├── SmaliMethodNode.java │ │ │ │ │ ├── SmaliRegister.java │ │ │ │ │ └── SmaliWriter.java │ │ │ │ └── protocol/ │ │ │ │ ├── ADB.java │ │ │ │ ├── ADBDevice.java │ │ │ │ └── ADBDeviceInfo.java │ │ │ ├── events/ │ │ │ │ ├── JadxGuiEvents.java │ │ │ │ ├── services/ │ │ │ │ │ └── RenameService.java │ │ │ │ └── types/ │ │ │ │ ├── JadxGuiEventsImpl.java │ │ │ │ └── TreeUpdate.java │ │ │ ├── jobs/ │ │ │ │ ├── BackgroundExecutor.java │ │ │ │ ├── Cancelable.java │ │ │ │ ├── CancelableBackgroundTask.java │ │ │ │ ├── DecompileTask.java │ │ │ │ ├── ExportTask.java │ │ │ │ ├── IBackgroundTask.java │ │ │ │ ├── ITaskInfo.java │ │ │ │ ├── ITaskProgress.java │ │ │ │ ├── InternalTask.java │ │ │ │ ├── LoadTask.java │ │ │ │ ├── ProcessResult.java │ │ │ │ ├── ProgressUpdater.java │ │ │ │ ├── SilentTask.java │ │ │ │ ├── SimpleTask.java │ │ │ │ ├── TaskProgress.java │ │ │ │ ├── TaskStatus.java │ │ │ │ └── TaskWithExtraOnFinish.java │ │ │ ├── logs/ │ │ │ │ ├── ILogListener.java │ │ │ │ ├── IssuesListener.java │ │ │ │ ├── LimitedQueue.java │ │ │ │ ├── LogAppender.java │ │ │ │ ├── LogCollector.java │ │ │ │ ├── LogEvent.java │ │ │ │ ├── LogMode.java │ │ │ │ ├── LogOptions.java │ │ │ │ └── LogPanel.java │ │ │ ├── plugins/ │ │ │ │ ├── context/ │ │ │ │ │ ├── CodePopupAction.java │ │ │ │ │ ├── CommonGuiPluginsContext.java │ │ │ │ │ ├── GuiPluginContext.java │ │ │ │ │ ├── GuiSettingsContext.java │ │ │ │ │ ├── ITreeInputCategory.java │ │ │ │ │ └── TreePopupMenuEntry.java │ │ │ │ ├── mappings/ │ │ │ │ │ ├── JInputMapping.java │ │ │ │ │ └── RenameMappingsGui.java │ │ │ │ └── quark/ │ │ │ │ ├── QuarkDialog.java │ │ │ │ ├── QuarkManager.java │ │ │ │ ├── QuarkReportData.java │ │ │ │ ├── QuarkReportNode.java │ │ │ │ └── QuarkReportPanel.java │ │ │ ├── report/ │ │ │ │ ├── ExceptionData.java │ │ │ │ ├── ExceptionDialog.java │ │ │ │ └── JadxExceptionHandler.java │ │ │ ├── search/ │ │ │ │ ├── ISearchMethod.java │ │ │ │ ├── ISearchProvider.java │ │ │ │ ├── SearchJob.java │ │ │ │ ├── SearchSettings.java │ │ │ │ ├── SearchTask.java │ │ │ │ └── providers/ │ │ │ │ ├── BaseSearchProvider.java │ │ │ │ ├── ClassSearchProvider.java │ │ │ │ ├── CodeSearchProvider.java │ │ │ │ ├── CommentSearchProvider.java │ │ │ │ ├── FieldSearchProvider.java │ │ │ │ ├── MergedSearchProvider.java │ │ │ │ ├── MethodSearchProvider.java │ │ │ │ ├── ResourceFilter.java │ │ │ │ └── ResourceSearchProvider.java │ │ │ ├── settings/ │ │ │ │ ├── JadxConfigExcludeExport.java │ │ │ │ ├── JadxGUIArgs.java │ │ │ │ ├── JadxProject.java │ │ │ │ ├── JadxSettings.java │ │ │ │ ├── JadxSettingsData.java │ │ │ │ ├── JadxUpdateChannel.java │ │ │ │ ├── LineNumbersMode.java │ │ │ │ ├── TabStateViewAdapter.java │ │ │ │ ├── WindowLocation.java │ │ │ │ ├── XposedCodegenLanguage.java │ │ │ │ ├── data/ │ │ │ │ │ ├── ITabStatePersist.java │ │ │ │ │ ├── ProjectData.java │ │ │ │ │ ├── SaveOptionEnum.java │ │ │ │ │ ├── ShortcutsWrapper.java │ │ │ │ │ ├── TabViewState.java │ │ │ │ │ └── ViewPoint.java │ │ │ │ ├── font/ │ │ │ │ │ ├── FontAdapter.java │ │ │ │ │ └── FontSettings.java │ │ │ │ └── ui/ │ │ │ │ ├── JadxSettingsWindow.java │ │ │ │ ├── SettingsGroup.java │ │ │ │ ├── SettingsTree.java │ │ │ │ ├── SettingsTreeNode.java │ │ │ │ ├── SubSettingsGroup.java │ │ │ │ ├── cache/ │ │ │ │ │ ├── CacheSettingsGroup.java │ │ │ │ │ ├── CachesTable.java │ │ │ │ │ ├── CachesTableModel.java │ │ │ │ │ ├── CachesTableRenderer.java │ │ │ │ │ └── TableRow.java │ │ │ │ ├── font/ │ │ │ │ │ ├── FontChooserHack.java │ │ │ │ │ └── JadxFontDialog.java │ │ │ │ ├── plugins/ │ │ │ │ │ ├── AvailablePluginNode.java │ │ │ │ │ ├── BasePluginListNode.java │ │ │ │ │ ├── InstallPluginDialog.java │ │ │ │ │ ├── InstalledPluginNode.java │ │ │ │ │ ├── LoadedPluginNode.java │ │ │ │ │ ├── PluginAction.java │ │ │ │ │ ├── PluginSettings.java │ │ │ │ │ ├── PluginSettingsGroup.java │ │ │ │ │ └── TitleNode.java │ │ │ │ └── shortcut/ │ │ │ │ ├── ShortcutEdit.java │ │ │ │ └── ShortcutsSettingsGroup.java │ │ │ ├── tree/ │ │ │ │ └── TreeExpansionService.java │ │ │ ├── treemodel/ │ │ │ │ ├── ApkSignatureNode.java │ │ │ │ ├── CodeNode.java │ │ │ │ ├── JClass.java │ │ │ │ ├── JEditableNode.java │ │ │ │ ├── JField.java │ │ │ │ ├── JInputFile.java │ │ │ │ ├── JInputFiles.java │ │ │ │ ├── JInputSmaliFile.java │ │ │ │ ├── JInputs.java │ │ │ │ ├── JLoadableNode.java │ │ │ │ ├── JMethod.java │ │ │ │ ├── JNode.java │ │ │ │ ├── JPackage.java │ │ │ │ ├── JRenameNode.java │ │ │ │ ├── JResSearchNode.java │ │ │ │ ├── JResource.java │ │ │ │ ├── JRoot.java │ │ │ │ ├── JSources.java │ │ │ │ ├── JSubResource.java │ │ │ │ ├── JVariable.java │ │ │ │ └── TextNode.java │ │ │ ├── ui/ │ │ │ │ ├── HeapUsageBar.java │ │ │ │ ├── JadxEventQueue.java │ │ │ │ ├── MainDropTarget.java │ │ │ │ ├── MainWindow.java │ │ │ │ ├── action/ │ │ │ │ │ ├── ActionCategory.java │ │ │ │ │ ├── ActionModel.java │ │ │ │ │ ├── CodeAreaAction.java │ │ │ │ │ ├── CommentSearchAction.java │ │ │ │ │ ├── FindUsageAction.java │ │ │ │ │ ├── FridaAction.java │ │ │ │ │ ├── GoToDeclarationAction.java │ │ │ │ │ ├── IShortcutAction.java │ │ │ │ │ ├── JNodeAction.java │ │ │ │ │ ├── JadxAutoCompletion.java │ │ │ │ │ ├── JadxGuiAction.java │ │ │ │ │ ├── JsonPrettifyAction.java │ │ │ │ │ ├── RenameAction.java │ │ │ │ │ ├── ViewCallGraphAction.java │ │ │ │ │ ├── ViewClassInheritanceGraphAction.java │ │ │ │ │ ├── ViewClassMethodGraphAction.java │ │ │ │ │ ├── ViewControlFlowGraphAction.java │ │ │ │ │ ├── ViewRawControlFlowGraphAction.java │ │ │ │ │ ├── ViewRegionControlFlowGraphAction.java │ │ │ │ │ └── XposedAction.kt │ │ │ │ ├── cellrenders/ │ │ │ │ │ ├── MethodRenderHelper.java │ │ │ │ │ ├── MethodsListRenderer.java │ │ │ │ │ └── PathHighlightTreeCellRenderer.java │ │ │ │ ├── codearea/ │ │ │ │ │ ├── AbstractCodeArea.java │ │ │ │ │ ├── AbstractCodeContentPanel.java │ │ │ │ │ ├── BinaryContentPanel.java │ │ │ │ │ ├── ClassCodeContentPanel.java │ │ │ │ │ ├── CodeArea.java │ │ │ │ │ ├── CodeContentPanel.java │ │ │ │ │ ├── CodeLinkGenerator.java │ │ │ │ │ ├── CodePanel.java │ │ │ │ │ ├── CommentAction.java │ │ │ │ │ ├── ConvertNumberAction.java │ │ │ │ │ ├── EditorViewState.java │ │ │ │ │ ├── JNodePopupBuilder.java │ │ │ │ │ ├── JNodePopupListener.java │ │ │ │ │ ├── JadxTokenMaker.java │ │ │ │ │ ├── MouseHoverHighlighter.java │ │ │ │ │ ├── SearchBar.java │ │ │ │ │ ├── SimpleTokenMaker.java │ │ │ │ │ ├── SmaliArea.java │ │ │ │ │ ├── SmaliFoldParser.java │ │ │ │ │ ├── SmaliTokenMaker.java │ │ │ │ │ ├── SourceLineFormatter.java │ │ │ │ │ ├── UsageDialogPlusAction.java │ │ │ │ │ ├── mode/ │ │ │ │ │ │ └── JCodeMode.java │ │ │ │ │ ├── sync/ │ │ │ │ │ │ ├── CodeMetadataRange.java │ │ │ │ │ │ ├── CodePanelSyncee.java │ │ │ │ │ │ ├── CodePanelSyncer.java │ │ │ │ │ │ ├── CodePanelSyncerAbstractFactory.java │ │ │ │ │ │ ├── CodeSyncHighlighter.java │ │ │ │ │ │ ├── DebugLineJavaSyncer.java │ │ │ │ │ │ ├── DebugLineSmaliSyncer.java │ │ │ │ │ │ ├── IToJavaSyncStrategy.java │ │ │ │ │ │ ├── IToSmaliSyncStrategy.java │ │ │ │ │ │ ├── InsnOffsetJavaSyncer.java │ │ │ │ │ │ ├── InsnOffsetSmaliSyncer.java │ │ │ │ │ │ ├── JavaSyncer.java │ │ │ │ │ │ ├── SmaliSyncer.java │ │ │ │ │ │ └── fallback/ │ │ │ │ │ │ ├── AbstractCodeAreaLine.java │ │ │ │ │ │ ├── AbstractCodeAreaToken.java │ │ │ │ │ │ ├── ClassDeclaration.java │ │ │ │ │ │ ├── FallbackSyncException.java │ │ │ │ │ │ ├── FallbackSyncer.java │ │ │ │ │ │ ├── IDeclaration.java │ │ │ │ │ │ ├── JavaCodeAreaLine.java │ │ │ │ │ │ ├── JavaCodeAreaToken.java │ │ │ │ │ │ ├── MethodDeclaration.java │ │ │ │ │ │ ├── SmaliAreaLine.java │ │ │ │ │ │ └── SmaliAreaToken.java │ │ │ │ │ └── theme/ │ │ │ │ │ ├── DynamicCodeAreaTheme.java │ │ │ │ │ ├── EditorThemeManager.java │ │ │ │ │ ├── FallbackEditorTheme.java │ │ │ │ │ ├── IEditorTheme.java │ │ │ │ │ ├── RSTABundledTheme.java │ │ │ │ │ ├── RSTAThemeXML.java │ │ │ │ │ └── ThemeIdAndName.java │ │ │ │ ├── dialog/ │ │ │ │ │ ├── ADBDialog.java │ │ │ │ │ ├── AboutDialog.java │ │ │ │ │ ├── CallGraphDialog.java │ │ │ │ │ ├── CharsetDialog.java │ │ │ │ │ ├── ClassInheritanceGraphDialog.java │ │ │ │ │ ├── ClassMethodGraphDialog.java │ │ │ │ │ ├── CommentDialog.java │ │ │ │ │ ├── CommonDialog.java │ │ │ │ │ ├── CommonSearchDialog.java │ │ │ │ │ ├── ControlFlowGraphDialog.java │ │ │ │ │ ├── ExcludePkgDialog.java │ │ │ │ │ ├── GotoAddressDialog.java │ │ │ │ │ ├── GraphDialog.java │ │ │ │ │ ├── LogViewerDialog.java │ │ │ │ │ ├── MethodsDialog.java │ │ │ │ │ ├── RenameDialog.java │ │ │ │ │ ├── SearchDialog.java │ │ │ │ │ ├── SetValueDialog.java │ │ │ │ │ ├── UsageDialog.java │ │ │ │ │ └── UsageDialogPlus.java │ │ │ │ ├── export/ │ │ │ │ │ ├── ExportProjectDialog.java │ │ │ │ │ └── ExportProjectProperties.java │ │ │ │ ├── filedialog/ │ │ │ │ │ ├── CustomFileChooser.java │ │ │ │ │ ├── CustomFileDialog.java │ │ │ │ │ ├── FileDialogWrapper.java │ │ │ │ │ ├── FileNameMultiExtensionFilter.java │ │ │ │ │ └── FileOpenMode.java │ │ │ │ ├── hexviewer/ │ │ │ │ │ ├── BinEdCodeAreaAssessor.java │ │ │ │ │ ├── HexEditorHeader.java │ │ │ │ │ ├── HexInspectorPanel.java │ │ │ │ │ ├── HexPreviewPanel.java │ │ │ │ │ ├── HexSearchBar.java │ │ │ │ │ └── search/ │ │ │ │ │ ├── BinarySearch.java │ │ │ │ │ ├── SearchCondition.java │ │ │ │ │ ├── SearchParameters.java │ │ │ │ │ └── service/ │ │ │ │ │ ├── BinarySearchService.java │ │ │ │ │ └── BinarySearchServiceImpl.java │ │ │ │ ├── menu/ │ │ │ │ │ ├── HiddenMenuItem.java │ │ │ │ │ ├── JadxMenu.java │ │ │ │ │ └── JadxMenuBar.java │ │ │ │ ├── panel/ │ │ │ │ │ ├── ContentPanel.java │ │ │ │ │ ├── FontPanel.java │ │ │ │ │ ├── HtmlPanel.java │ │ │ │ │ ├── IDebugController.java │ │ │ │ │ ├── IViewStateSupport.java │ │ │ │ │ ├── ImagePanel.java │ │ │ │ │ ├── IssuesPanel.java │ │ │ │ │ ├── JDebuggerPanel.java │ │ │ │ │ ├── LogcatPanel.java │ │ │ │ │ ├── ProgressPanel.java │ │ │ │ │ ├── SimpleCodePanel.java │ │ │ │ │ └── UndisplayedStringsPanel.java │ │ │ │ ├── popupmenu/ │ │ │ │ │ ├── JClassExportType.java │ │ │ │ │ ├── JClassPopupMenu.java │ │ │ │ │ ├── JPackagePopupMenu.java │ │ │ │ │ ├── JResourcePopupMenu.java │ │ │ │ │ ├── RecentProjectsMenuListener.java │ │ │ │ │ └── VarTreePopupMenu.java │ │ │ │ ├── startpage/ │ │ │ │ │ ├── RecentProjectItem.java │ │ │ │ │ ├── RecentProjectListCellRenderer.java │ │ │ │ │ ├── StartPageNode.java │ │ │ │ │ └── StartPagePanel.java │ │ │ │ ├── tab/ │ │ │ │ │ ├── EditorSyncManager.java │ │ │ │ │ ├── ITabStatesListener.java │ │ │ │ │ ├── LogTabStates.java │ │ │ │ │ ├── NavigationController.java │ │ │ │ │ ├── QuickTabsBaseNode.java │ │ │ │ │ ├── QuickTabsBookmarkParentNode.java │ │ │ │ │ ├── QuickTabsChildNode.java │ │ │ │ │ ├── QuickTabsOpenParentNode.java │ │ │ │ │ ├── QuickTabsParentNode.java │ │ │ │ │ ├── QuickTabsPinParentNode.java │ │ │ │ │ ├── QuickTabsTree.java │ │ │ │ │ ├── TabBlueprint.java │ │ │ │ │ ├── TabComponent.java │ │ │ │ │ ├── TabbedPane.java │ │ │ │ │ ├── TabsController.java │ │ │ │ │ └── dnd/ │ │ │ │ │ ├── TabDndController.java │ │ │ │ │ ├── TabDndGestureListener.java │ │ │ │ │ ├── TabDndGhostPane.java │ │ │ │ │ ├── TabDndGhostType.java │ │ │ │ │ ├── TabDndSourceListener.java │ │ │ │ │ ├── TabDndTargetListener.java │ │ │ │ │ └── TabDndTransferable.java │ │ │ │ └── treenodes/ │ │ │ │ ├── SummaryNode.java │ │ │ │ └── UndisplayedStringsNode.java │ │ │ ├── update/ │ │ │ │ └── JadxUpdate.kt │ │ │ └── utils/ │ │ │ ├── CacheObject.java │ │ │ ├── CaretPositionFix.java │ │ │ ├── CertificateManager.java │ │ │ ├── DefaultPopupMenuListener.java │ │ │ ├── DesktopEntryUtils.java │ │ │ ├── FontUtils.java │ │ │ ├── HexUtils.java │ │ │ ├── ILoadListener.java │ │ │ ├── IOUtils.java │ │ │ ├── Icons.java │ │ │ ├── IconsCache.java │ │ │ ├── JNodeCache.java │ │ │ ├── JumpManager.java │ │ │ ├── JumpPosition.java │ │ │ ├── LafManager.java │ │ │ ├── LangLocale.java │ │ │ ├── Link.java │ │ │ ├── NLS.java │ │ │ ├── ObjectPool.java │ │ │ ├── OverlayIcon.java │ │ │ ├── PathTypeAdapter.java │ │ │ ├── RectangleTypeAdapter.java │ │ │ ├── RelativePathTypeAdapter.java │ │ │ ├── SimpleListener.java │ │ │ ├── TextStandardActions.java │ │ │ ├── UiUtils.java │ │ │ ├── cache/ │ │ │ │ └── ValueCache.java │ │ │ ├── dbg/ │ │ │ │ └── UIWatchDog.java │ │ │ ├── files/ │ │ │ │ └── JadxFiles.java │ │ │ ├── fileswatcher/ │ │ │ │ ├── FilesWatcher.java │ │ │ │ └── LiveReloadWorker.java │ │ │ ├── layout/ │ │ │ │ └── WrapLayout.java │ │ │ ├── pkgs/ │ │ │ │ ├── JRenamePackage.java │ │ │ │ └── PackageHelper.java │ │ │ ├── plugins/ │ │ │ │ ├── CloseablePlugins.java │ │ │ │ ├── CollectPlugins.java │ │ │ │ ├── PluginWithOptions.java │ │ │ │ ├── SettingsGroupPluginWrap.java │ │ │ │ └── TreeInputsHelper.java │ │ │ ├── res/ │ │ │ │ └── ResTableHelper.java │ │ │ ├── rx/ │ │ │ │ ├── CustomDisposable.java │ │ │ │ ├── DebounceUpdate.java │ │ │ │ └── RxUtils.java │ │ │ ├── shortcut/ │ │ │ │ ├── Shortcut.java │ │ │ │ └── ShortcutsController.java │ │ │ ├── tools/ │ │ │ │ └── SyncNLSLines.java │ │ │ └── ui/ │ │ │ ├── ActionHandler.java │ │ │ ├── DocumentUpdateListener.java │ │ │ ├── FileOpenerHelper.java │ │ │ ├── MousePressedHandler.java │ │ │ ├── NodeLabel.java │ │ │ ├── SimpleMenuItem.java │ │ │ └── ZoomActions.java │ │ └── resources/ │ │ ├── files/ │ │ │ └── jadx-gui.desktop.tmpl │ │ └── i18n/ │ │ ├── Messages_de_DE.properties │ │ ├── Messages_en_US.properties │ │ ├── Messages_es_ES.properties │ │ ├── Messages_id_ID.properties │ │ ├── Messages_ko_KR.properties │ │ ├── Messages_pt_BR.properties │ │ ├── Messages_ru_RU.properties │ │ ├── Messages_zh_CN.properties │ │ └── Messages_zh_TW.properties │ └── test/ │ ├── java/ │ │ └── jadx/ │ │ └── gui/ │ │ ├── TestI18n.java │ │ ├── device/ │ │ │ └── debugger/ │ │ │ └── smali/ │ │ │ └── DbgSmaliTest.java │ │ ├── ui/ │ │ │ └── codearea/ │ │ │ └── ConvertNumberActionTest.java │ │ ├── update/ │ │ │ └── TestJadxUpdate.kt │ │ └── utils/ │ │ ├── CertificateManagerTest.java │ │ ├── JumpManagerTest.java │ │ ├── cache/ │ │ │ └── code/ │ │ │ ├── DiskCodeCacheTest.java │ │ │ └── disk/ │ │ │ └── adapters/ │ │ │ └── DataAdapterHelperTest.java │ │ └── pkgs/ │ │ └── TestJRenamePackage.java │ ├── resources/ │ │ ├── certificate-test/ │ │ │ ├── CERT.DSA │ │ │ ├── CERT.RSA │ │ │ └── EMPTY.txt │ │ └── logback-test.xml │ └── smali/ │ ├── params.smali │ └── switch.smali ├── jadx-plugins/ │ ├── jadx-aab-input/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── jadx/ │ │ │ └── plugins/ │ │ │ └── input/ │ │ │ └── aab/ │ │ │ ├── AabInputPlugin.java │ │ │ ├── ResTableProtoParserProvider.java │ │ │ ├── factories/ │ │ │ │ ├── ProtoAppDependenciesResContainerFactory.java │ │ │ │ ├── ProtoAssetsConfigResContainerFactory.java │ │ │ │ ├── ProtoBundleConfigResContainerFactory.java │ │ │ │ ├── ProtoNativeConfigResContainerFactory.java │ │ │ │ ├── ProtoTableResContainerFactory.java │ │ │ │ └── ProtoXmlResContainerFactory.java │ │ │ └── parsers/ │ │ │ ├── CommonProtoParser.java │ │ │ ├── ResTableProtoParser.java │ │ │ └── ResXmlProtoParser.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── services/ │ │ └── jadx.api.plugins.JadxPlugin │ ├── jadx-apkm-input/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── jadx/ │ │ │ └── plugins/ │ │ │ └── input/ │ │ │ └── apkm/ │ │ │ ├── ApkmCustomCodeInput.kt │ │ │ ├── ApkmCustomResourcesLoader.kt │ │ │ ├── ApkmInputPlugin.kt │ │ │ ├── ApkmManifest.kt │ │ │ └── ApkmUtils.kt │ │ └── resources/ │ │ └── META-INF/ │ │ └── services/ │ │ └── jadx.api.plugins.JadxPlugin │ ├── jadx-apks-input/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── jadx/ │ │ │ └── plugins/ │ │ │ └── input/ │ │ │ └── apks/ │ │ │ ├── ApksCustomCodeInput.kt │ │ │ ├── ApksCustomResourcesLoader.kt │ │ │ └── ApksInputPlugin.kt │ │ └── resources/ │ │ └── META-INF/ │ │ └── services/ │ │ └── jadx.api.plugins.JadxPlugin │ ├── jadx-dex-input/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── jadx/ │ │ │ │ └── plugins/ │ │ │ │ └── input/ │ │ │ │ └── dex/ │ │ │ │ ├── DexException.java │ │ │ │ ├── DexFileLoader.java │ │ │ │ ├── DexInputOptions.java │ │ │ │ ├── DexInputPlugin.java │ │ │ │ ├── DexLoadResult.java │ │ │ │ ├── DexReader.java │ │ │ │ ├── insns/ │ │ │ │ │ ├── DexInsnData.java │ │ │ │ │ ├── DexInsnFormat.java │ │ │ │ │ ├── DexInsnInfo.java │ │ │ │ │ ├── DexInsnMnemonics.java │ │ │ │ │ ├── DexOpcodes.java │ │ │ │ │ └── payloads/ │ │ │ │ │ └── DexArrayPayload.java │ │ │ │ ├── sections/ │ │ │ │ │ ├── DexAnnotationsConvert.java │ │ │ │ │ ├── DexClassData.java │ │ │ │ │ ├── DexCodeReader.java │ │ │ │ │ ├── DexConsts.java │ │ │ │ │ ├── DexFieldData.java │ │ │ │ │ ├── DexHeader.java │ │ │ │ │ ├── DexHeaderV41.java │ │ │ │ │ ├── DexMethodData.java │ │ │ │ │ ├── DexMethodProto.java │ │ │ │ │ ├── DexMethodRef.java │ │ │ │ │ ├── SectionReader.java │ │ │ │ │ ├── annotations/ │ │ │ │ │ │ ├── AnnotationsParser.java │ │ │ │ │ │ ├── AnnotationsUtils.java │ │ │ │ │ │ └── EncodedValueParser.java │ │ │ │ │ └── debuginfo/ │ │ │ │ │ ├── DebugInfoParser.java │ │ │ │ │ └── DexLocalVar.java │ │ │ │ ├── smali/ │ │ │ │ │ ├── InsnFormatter.java │ │ │ │ │ ├── InsnFormatterInfo.java │ │ │ │ │ ├── SmaliCodeWriter.java │ │ │ │ │ ├── SmaliInsnFormat.java │ │ │ │ │ └── SmaliPrinter.java │ │ │ │ └── utils/ │ │ │ │ ├── DataReader.java │ │ │ │ ├── DexCheckSum.java │ │ │ │ ├── IDexData.java │ │ │ │ ├── Leb128.java │ │ │ │ ├── MUtf8.java │ │ │ │ ├── SimpleDexData.java │ │ │ │ └── SmaliUtils.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── jadx.api.plugins.JadxPlugin │ │ └── test/ │ │ ├── java/ │ │ │ └── jadx/ │ │ │ └── plugins/ │ │ │ └── input/ │ │ │ └── dex/ │ │ │ ├── DexInputPluginTest.java │ │ │ └── utils/ │ │ │ └── SmaliTestUtils.java │ │ └── resources/ │ │ └── samples/ │ │ ├── app-with-fake-dex.apk │ │ ├── hello.dex │ │ └── test.smali │ ├── jadx-input-api/ │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── jadx/ │ │ └── api/ │ │ └── plugins/ │ │ └── input/ │ │ ├── ICodeLoader.java │ │ ├── JadxCodeInput.java │ │ ├── data/ │ │ │ ├── AccessFlags.java │ │ │ ├── AccessFlagsScope.java │ │ │ ├── ICallSite.java │ │ │ ├── ICatch.java │ │ │ ├── IClassData.java │ │ │ ├── ICodeReader.java │ │ │ ├── IDebugInfo.java │ │ │ ├── IFieldData.java │ │ │ ├── IFieldRef.java │ │ │ ├── ILocalVar.java │ │ │ ├── IMethodData.java │ │ │ ├── IMethodHandle.java │ │ │ ├── IMethodProto.java │ │ │ ├── IMethodRef.java │ │ │ ├── IResourceData.java │ │ │ ├── ISeqConsumer.java │ │ │ ├── ITry.java │ │ │ ├── MethodHandleType.java │ │ │ ├── annotations/ │ │ │ │ ├── AnnotationVisibility.java │ │ │ │ ├── EncodedType.java │ │ │ │ ├── EncodedValue.java │ │ │ │ ├── IAnnotation.java │ │ │ │ └── JadxAnnotation.java │ │ │ ├── attributes/ │ │ │ │ ├── IJadxAttrType.java │ │ │ │ ├── IJadxAttribute.java │ │ │ │ ├── JadxAttrType.java │ │ │ │ ├── PinnedAttribute.java │ │ │ │ └── types/ │ │ │ │ ├── AnnotationDefaultAttr.java │ │ │ │ ├── AnnotationDefaultClassAttr.java │ │ │ │ ├── AnnotationMethodParamsAttr.java │ │ │ │ ├── AnnotationsAttr.java │ │ │ │ ├── ExceptionsAttr.java │ │ │ │ ├── InnerClassesAttr.java │ │ │ │ ├── InnerClsInfo.java │ │ │ │ ├── MethodParametersAttr.java │ │ │ │ ├── SignatureAttr.java │ │ │ │ └── SourceFileAttr.java │ │ │ └── impl/ │ │ │ ├── CallSite.java │ │ │ ├── CatchData.java │ │ │ ├── DebugInfo.java │ │ │ ├── EmptyCodeLoader.java │ │ │ ├── FieldRefHandle.java │ │ │ ├── InputUtils.java │ │ │ ├── JadxFieldRef.java │ │ │ ├── ListConsumer.java │ │ │ ├── MergeCodeLoader.java │ │ │ ├── MethodRefHandle.java │ │ │ └── TryData.java │ │ └── insns/ │ │ ├── InsnData.java │ │ ├── InsnIndexType.java │ │ ├── Opcode.java │ │ └── custom/ │ │ ├── IArrayPayload.java │ │ ├── ICustomPayload.java │ │ ├── ISwitchPayload.java │ │ └── impl/ │ │ └── SwitchPayload.java │ ├── jadx-java-convert/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── jadx/ │ │ │ └── plugins/ │ │ │ └── input/ │ │ │ └── javaconvert/ │ │ │ ├── AsmUtils.java │ │ │ ├── ConvertResult.java │ │ │ ├── D8Converter.java │ │ │ ├── DxConverter.java │ │ │ ├── JavaConvertLoader.java │ │ │ ├── JavaConvertOptions.java │ │ │ └── JavaConvertPlugin.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── services/ │ │ └── jadx.api.plugins.JadxPlugin │ ├── jadx-java-input/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── jadx/ │ │ │ │ └── plugins/ │ │ │ │ └── input/ │ │ │ │ └── java/ │ │ │ │ ├── JavaClassReader.java │ │ │ │ ├── JavaInputLoader.java │ │ │ │ ├── JavaInputPlugin.java │ │ │ │ ├── JavaLoadResult.java │ │ │ │ ├── data/ │ │ │ │ │ ├── ClassOffsets.java │ │ │ │ │ ├── ConstPoolReader.java │ │ │ │ │ ├── ConstantType.java │ │ │ │ │ ├── DataReader.java │ │ │ │ │ ├── JavaClassData.java │ │ │ │ │ ├── JavaFieldData.java │ │ │ │ │ ├── JavaMethodData.java │ │ │ │ │ ├── JavaMethodProto.java │ │ │ │ │ ├── JavaMethodRef.java │ │ │ │ │ ├── attributes/ │ │ │ │ │ │ ├── AttributesReader.java │ │ │ │ │ │ ├── EncodedValueReader.java │ │ │ │ │ │ ├── IJavaAttribute.java │ │ │ │ │ │ ├── IJavaAttributeReader.java │ │ │ │ │ │ ├── JavaAttrStorage.java │ │ │ │ │ │ ├── JavaAttrType.java │ │ │ │ │ │ ├── debuginfo/ │ │ │ │ │ │ │ ├── JavaLocalVar.java │ │ │ │ │ │ │ ├── LineNumberTableAttr.java │ │ │ │ │ │ │ ├── LocalVarTypesAttr.java │ │ │ │ │ │ │ └── LocalVarsAttr.java │ │ │ │ │ │ ├── stack/ │ │ │ │ │ │ │ ├── StackFrame.java │ │ │ │ │ │ │ ├── StackFrameType.java │ │ │ │ │ │ │ ├── StackMapTableReader.java │ │ │ │ │ │ │ ├── StackValueType.java │ │ │ │ │ │ │ └── TypeInfoReader.java │ │ │ │ │ │ └── types/ │ │ │ │ │ │ ├── CodeAttr.java │ │ │ │ │ │ ├── ConstValueAttr.java │ │ │ │ │ │ ├── IgnoredAttr.java │ │ │ │ │ │ ├── JavaAnnotationDefaultAttr.java │ │ │ │ │ │ ├── JavaAnnotationsAttr.java │ │ │ │ │ │ ├── JavaBootstrapMethodsAttr.java │ │ │ │ │ │ ├── JavaExceptionsAttr.java │ │ │ │ │ │ ├── JavaInnerClsAttr.java │ │ │ │ │ │ ├── JavaMethodParametersAttr.java │ │ │ │ │ │ ├── JavaParamAnnsAttr.java │ │ │ │ │ │ ├── JavaSignatureAttr.java │ │ │ │ │ │ ├── JavaSourceFileAttr.java │ │ │ │ │ │ ├── StackMapTableAttr.java │ │ │ │ │ │ └── data/ │ │ │ │ │ │ └── RawBootstrapMethod.java │ │ │ │ │ └── code/ │ │ │ │ │ ├── ArrayType.java │ │ │ │ │ ├── CodeDecodeState.java │ │ │ │ │ ├── JavaCodeReader.java │ │ │ │ │ ├── JavaInsnData.java │ │ │ │ │ ├── JavaInsnInfo.java │ │ │ │ │ ├── JavaInsnsRegister.java │ │ │ │ │ ├── StackState.java │ │ │ │ │ ├── decoders/ │ │ │ │ │ │ ├── IJavaInsnDecoder.java │ │ │ │ │ │ ├── InvokeDecoder.java │ │ │ │ │ │ ├── LoadConstDecoder.java │ │ │ │ │ │ ├── LookupSwitchDecoder.java │ │ │ │ │ │ ├── TableSwitchDecoder.java │ │ │ │ │ │ └── WideDecoder.java │ │ │ │ │ └── trycatch/ │ │ │ │ │ ├── JavaSingleCatch.java │ │ │ │ │ └── JavaTryData.java │ │ │ │ └── utils/ │ │ │ │ ├── DescriptorParser.java │ │ │ │ ├── DisasmUtils.java │ │ │ │ ├── JavaClassParseException.java │ │ │ │ └── ModifiedUTF8Decoder.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── jadx.api.plugins.JadxPlugin │ │ └── test/ │ │ └── java/ │ │ └── jadx/ │ │ └── plugins/ │ │ └── input/ │ │ └── java/ │ │ ├── CustomLoadTest.java │ │ └── utils/ │ │ ├── DescriptorParserTest.java │ │ └── ModifiedUTF8DecoderTest.java │ ├── jadx-kotlin-metadata/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ ├── kotlin/ │ │ │ │ └── jadx/ │ │ │ │ └── plugins/ │ │ │ │ └── kotlin/ │ │ │ │ └── metadata/ │ │ │ │ ├── KotlinMetadataOptions.kt │ │ │ │ ├── KotlinMetadataPlugin.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── KotlinMetadataConsts.kt │ │ │ │ │ └── KotlinRenameResults.kt │ │ │ │ ├── pass/ │ │ │ │ │ ├── KotlinMetadataDecompilePass.kt │ │ │ │ │ └── KotlinMetadataPreparePass.kt │ │ │ │ └── utils/ │ │ │ │ ├── KmClassWrapper.kt │ │ │ │ ├── KmExt.kt │ │ │ │ ├── KotlinMetadataExt.kt │ │ │ │ ├── KotlinMetadataUtils.kt │ │ │ │ ├── KotlinUtils.kt │ │ │ │ ├── LogExt.kt │ │ │ │ └── ToStringParser.kt │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── jadx.api.plugins.JadxPlugin │ │ └── test/ │ │ ├── kotlin/ │ │ │ ├── TestJavaParser.kt │ │ │ └── TestKotlinMetadata.kt │ │ └── smali/ │ │ └── deobf/ │ │ └── TestKotlinMetadata/ │ │ ├── a$b.smali │ │ └── a.smali │ ├── jadx-kotlin-source-debug-extension/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ ├── kotlin/ │ │ │ │ └── jadx/ │ │ │ │ └── plugins/ │ │ │ │ └── kotlin/ │ │ │ │ └── smap/ │ │ │ │ ├── KotlinSmapOptions.kt │ │ │ │ ├── KotlinSmapPlugin.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── ClassAliasRename.kt │ │ │ │ │ ├── Constants.kt │ │ │ │ │ ├── SMAP.kt │ │ │ │ │ └── SourceInfo.kt │ │ │ │ ├── pass/ │ │ │ │ │ └── KotlinSourceDebugExtensionPass.kt │ │ │ │ └── utils/ │ │ │ │ ├── Extensions.kt │ │ │ │ ├── KotlinSmapUtils.kt │ │ │ │ └── SMAPParser.kt │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── jadx.api.plugins.JadxPlugin │ │ └── test/ │ │ ├── kotlin/ │ │ │ └── TestSourceDebugExtension.kt │ │ └── smali/ │ │ └── deobf/ │ │ └── TestKotlinSourceDebugExtension/ │ │ └── C6.smali │ ├── jadx-raung-input/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── jadx/ │ │ │ └── plugins/ │ │ │ └── input/ │ │ │ └── raung/ │ │ │ ├── RaungConvert.java │ │ │ └── RaungInputPlugin.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── services/ │ │ └── jadx.api.plugins.JadxPlugin │ ├── jadx-rename-mappings/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── jadx/ │ │ │ │ └── plugins/ │ │ │ │ └── mappings/ │ │ │ │ ├── RenameMappingsData.java │ │ │ │ ├── RenameMappingsOptions.java │ │ │ │ ├── RenameMappingsPlugin.java │ │ │ │ ├── load/ │ │ │ │ │ ├── ApplyMappingsPass.java │ │ │ │ │ ├── CodeMappingsPass.java │ │ │ │ │ └── LoadMappingsPass.java │ │ │ │ ├── save/ │ │ │ │ │ └── MappingExporter.java │ │ │ │ └── utils/ │ │ │ │ ├── DalvikToJavaBytecodeUtils.java │ │ │ │ └── VariablesUtils.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── jadx.api.plugins.JadxPlugin │ │ └── test/ │ │ ├── java/ │ │ │ └── jadx/ │ │ │ └── plugins/ │ │ │ └── mappings/ │ │ │ ├── BaseRenameMappingsTest.java │ │ │ └── TestInnerClassRename.java │ │ └── resources/ │ │ ├── inner-cls-rename/ │ │ │ ├── base.smali │ │ │ ├── enigma.mapping │ │ │ └── inner.smali │ │ └── logback-test.xml │ ├── jadx-smali-input/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── jadx/ │ │ │ └── plugins/ │ │ │ └── input/ │ │ │ └── smali/ │ │ │ ├── SmaliConvert.java │ │ │ ├── SmaliInputOptions.java │ │ │ ├── SmaliInputPlugin.java │ │ │ └── SmaliUtils.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── services/ │ │ └── jadx.api.plugins.JadxPlugin │ └── jadx-xapk-input/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── java/ │ │ └── jadx/ │ │ └── plugins/ │ │ └── input/ │ │ └── xapk/ │ │ ├── XApkCustomInput.java │ │ ├── XApkInputPlugin.java │ │ ├── XApkLoader.java │ │ └── data/ │ │ ├── SplitApk.java │ │ ├── XApkData.java │ │ └── XApkManifest.java │ └── resources/ │ └── META-INF/ │ └── services/ │ └── jadx.api.plugins.JadxPlugin ├── jadx-plugins-tools/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── jadx/ │ │ └── plugins/ │ │ └── tools/ │ │ ├── JadxExternalPluginsLoader.java │ │ ├── JadxPluginsList.java │ │ ├── JadxPluginsTools.java │ │ ├── data/ │ │ │ ├── JadxInstalledPlugins.java │ │ │ ├── JadxPluginListCache.java │ │ │ ├── JadxPluginListEntry.java │ │ │ ├── JadxPluginMetadata.java │ │ │ └── JadxPluginUpdate.java │ │ ├── resolvers/ │ │ │ ├── IJadxPluginResolver.java │ │ │ ├── README.md │ │ │ ├── ResolversRegistry.java │ │ │ ├── file/ │ │ │ │ └── LocalFileResolver.java │ │ │ └── github/ │ │ │ ├── GithubReleaseResolver.java │ │ │ ├── GithubTools.java │ │ │ ├── LocationInfo.java │ │ │ └── data/ │ │ │ ├── Asset.java │ │ │ └── Release.java │ │ └── utils/ │ │ ├── PluginFiles.java │ │ └── PluginUtils.java │ └── test/ │ ├── java/ │ │ └── jadx/ │ │ └── plugins/ │ │ └── tools/ │ │ └── resolvers/ │ │ └── github/ │ │ └── GithubToolsTest.java │ └── resources/ │ └── github/ │ └── plugins-list-good.json └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: https://EditorConfig.org root = true [*] end_of_line = lf insert_final_newline = true indent_style = tab tab_width = 4 charset = utf-8 trim_trailing_whitespace = true [*.java] ij_java_continuation_indent_size = 8 ij_java_use_single_class_imports = true ij_java_class_count_to_use_import_on_demand = 99 ij_java_names_count_to_use_import_on_demand = 99 ij_java_packages_to_use_import_on_demand = * [*.kt] ij_kotlin_continuation_indent_size = 8 ij_kotlin_name_count_to_use_star_import = 99 ij_kotlin_name_count_to_use_star_import_for_members = 99 ij_kotlin_packages_to_use_import_on_demand = * [*.yml] indent_style = space indent_size = 2 [*.bat] end_of_line = crlf ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf *.java text eol=lf diff=java *.kt text eol=lf diff=kotlin *.kts text eol=lf diff=kotlin gradlew text eol=lf *.bat text eol=crlf *.png binary *.jar binary ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/decompilation-issue.yml ================================================ name: Decompilation issue description: Create a report to help us improve jadx decompiler title: '[core] ' labels: - Core - bug body: - type: markdown attributes: value: | **Checks before submit** - check [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A) section on wiki - try [latest unstable build](https://nightly.link/skylot/jadx/workflows/build-artifacts/master), maybe issue already fixed - search existing issues by exception message - type: textarea id: details attributes: label: Issue details placeholder: >- Describe issue validations: required: true - type: textarea id: logs attributes: label: Relevant log output or stacktrace description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: java - type: textarea id: sample attributes: label: Provide sample and class/method full name description: | - sample: attach or provide a link - full name of class or method with issue - other details which may help to reproduce issue - type: input id: jadx-version attributes: label: Jadx version ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.yml ================================================ name: Feature Request description: Suggest an idea for jadx title: '[feature] ' labels: - 'new feature' body: - type: textarea id: details attributes: label: Describe your idea placeholder: Feature details validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/jadx-gui-issue.yml ================================================ name: jadx-gui issue description: Create a bug report about issue found in jadx-gui title: '[gui] ' labels: - GUI - bug body: - type: markdown attributes: value: | **Checks before submit** - check [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A) section on wiki - try [latest unstable build](https://nightly.link/skylot/jadx/workflows/build-artifacts/master), maybe issue already fixed - search existing issues by exception message - type: textarea id: details attributes: label: Issue details placeholder: Describe issue and how to reproduce it validations: required: true - type: input id: jadx-version attributes: label: Jadx version placeholder: check `Help->About` validations: required: true - type: input id: java-version attributes: label: Java version placeholder: check `Help->About` validations: required: true - type: checkboxes id: os attributes: label: OS options: - label: Windows - label: Linux - label: macOS ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: # Set update schedule for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/pull_request_template.md ================================================ :exclamation: Please review the [guidelines for contributing](https://github.com/skylot/jadx/blob/master/CONTRIBUTING.md#Pull-Request-Process) ### Description Please describe your pull request. Reference issue it fixes. ================================================ FILE: .github/workflows/build-artifacts.yml ================================================ name: Build Artifacts on: push: branches: [ master, build-test ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK uses: actions/setup-java@v5 with: distribution: temurin java-version: 25 - name: Set jadx version run: | JADX_REV=$(git rev-list --count HEAD) JADX_VERSION="r${JADX_REV}.${GITHUB_SHA:0:7}" echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Build run: ./gradlew dist distWin env: JADX_BUILD_JAVA_VERSION: 11 - name: Save bundle artifact uses: actions/upload-artifact@v7 with: name: ${{ format('jadx-{0}', env.JADX_VERSION) }} # Waiting fix for https://github.com/actions/upload-artifact/issues/39 to upload zip file # Upload unpacked files for now path: build/jadx/**/* if-no-files-found: error retention-days: 14 - name: Save Windows bundle artifact uses: actions/upload-artifact@v7 with: name: ${{ format('jadx-gui-{0}-no-jre-win', env.JADX_VERSION) }} # Upload unpacked files for now path: jadx-gui/build/jadx-gui-win/* if-no-files-found: error retention-days: 14 build-win-bundle: runs-on: windows-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up JDK uses: oracle-actions/setup-java@v1 with: release: 25 - name: Print Java version shell: bash run: java -version - name: Set jadx version shell: bash run: | JADX_REV=$(git rev-list --count HEAD) JADX_VERSION="r${JADX_REV}.${GITHUB_SHA:0:7}" echo "JADX_VERSION=$JADX_VERSION" >> $GITHUB_ENV - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Build run: ./gradlew dist -PbundleJRE=true - name: Save Windows with JRE bundle artifact uses: actions/upload-artifact@v7 with: name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} # Upload unpacked files for now path: jadx-gui/build/jadx-gui-with-jre-win/* if-no-files-found: error retention-days: 14 ================================================ FILE: .github/workflows/build-test.yml ================================================ name: Build Test on: push: branches: [ master, build-test ] pull_request: branches: [ master ] jobs: tests: strategy: matrix: os: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 with: distribution: temurin java-version: 25 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Build run: ./gradlew build dist distWin env: JADX_BUILD_JAVA_VERSION: 11 ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - "v*.*.*" # additional permissions for provided GitHub token to create new release permissions: contents: write jobs: build-release-win-bundle: runs-on: windows-latest steps: - uses: actions/checkout@v6 - name: Set up JDK uses: oracle-actions/setup-java@v1 with: release: 25 - name: Set jadx version uses: actions/github-script@v8 with: script: | const jadxVersion = context.ref.split('/').pop().substring(1) core.exportVariable('JADX_VERSION', jadxVersion); - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Build run: ./gradlew dist -PbundleJRE=true - name: Save JRE bundle artifact uses: actions/upload-artifact@v7 with: name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} path: ${{ format('build/distWinWithJre/jadx-gui-{0}-with-jre-win.zip', env.JADX_VERSION) }} if-no-files-found: error retention-days: 1 release: needs: build-release-win-bundle runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 with: distribution: temurin java-version: 25 - name: Set jadx version and release name uses: actions/github-script@v8 with: script: | const jadxVersion = context.ref.split('/').pop().substring(1) core.exportVariable('JADX_VERSION', jadxVersion); - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Build run: ./gradlew dist distWin env: JADX_BUILD_JAVA_VERSION: 11 - name: Download Windows JRE bundle uses: actions/download-artifact@v8 with: name: ${{ format('jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} path: ${{ format('build/jadx-gui-{0}-with-jre-win', env.JADX_VERSION) }} - run: | cd build pwd ls -l ls jadx-gui-*-with-jre-win mv jadx-gui-*-with-jre-win/jadx-gui-*-with-jre-win.zip . mv distWin/jadx-gui-*-win.zip . ls -l *.zip - name: Release uses: softprops/action-gh-release@v2 with: name: ${{ env.JADX_VERSION }} draft: true fail_on_unmatched_files: true files: build/jadx-*.zip ================================================ FILE: .gitignore ================================================ # Eclipse files .classpath .project .settings/ # IntelliJ Idea files .idea/ .run/ out/ *.iml *.ipr *.iws .attach_pid* *.hprof **/.DS_Store bin/ target/ build/ classes/ idea/ .gradle/ .kotlin/ node_modules/ .vscode/ jadx-output/ *-tmp/ **/tmp/ *.jobf *.jadx *.class *.jar *.dump *.log *.cfg *.orig quark.json cliff.toml jadx-gui/src/main/resources/logback.xml ================================================ FILE: .gitlab-ci.yml ================================================ variables: GRADLE_OPTS: "-Dorg.gradle.daemon=false" TERM: "dumb" before_script: - chmod +x gradlew stages: - test build-test: stage: test image: eclipse-temurin:21 script: JADX_BUILD_JAVA_VERSION=11 JADX_TEST_JAVA_VERSION=11 ./gradlew clean build dist distWin ================================================ FILE: .jitpack.yml ================================================ jdk: - openjdk11 install: - echo "Jitpack is not supported. Use artifacts from Maven Central (https://search.maven.org/search?q=jadx), check usage help at https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library" - ./gradlew intentional-fail ================================================ FILE: .typos.toml ================================================ # Config for 'typos' spellchecker (https://github.com/crate-ci/typos) [default.extend-words] IPUT = "IPUT" Laf = "Laf" Darcula="Darcula" [default] extend-ignore-identifiers-re = [ "finaly", # intentional package name ] [files] extend-exclude = [ "config/", "jadx-core/src/main/resources/", "jadx-core/src/test/", "jadx-gui/src/main/resources/i18n/", "!jadx-gui/src/main/resources/i18n/Messages_en_US.properties", ] ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Please note, we have [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. ## Open Issue 1. Before proceed, please do: - check [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A) section on wiki - search existing issues by exception message 2. Describe error: - full name of method or class with error - full java stacktrace (no need to copy method fallback code (commented pseudocode)) - **IMPORTANT!:** attach or provide link to apk file (double check apk version) **Note**: GitHub don't allow attaching files with `.apk` extension, but you can change extension by adding `.zip` at the end :) ## Pull Request Process 1. Please don't submit any code style fixes or dependencies updates changes. 1. Use only features and API from Java 11 or below. 1. Make sure your code is correctly formatted, see description here: [Code Formatting](https://github.com/skylot/jadx/wiki/Code-Formatting). 1. Make sure your changes are passing build: `./gradlew clean build dist` ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: NOTICE ================================================ The majority of jadx is written and copyrighted by me (Skylot) and released under the Apache 2.0 license (see LICENSE file for full license text): ******************************************************************************* Copyright 2015, Skylot Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ******************************************************************************* Various portions of the code including dx library are taken from the Android Open Source Project, and are used in accordance with the following license: ******************************************************************************* Copyright (C) 2007 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ******************************************************************************* Other binary libraries used in 'jadx' ===================================== JCommander library (http://jcommander.org/) released under the following license: ******************************************************************************* Copyright 2012, Cedric Beust Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ******************************************************************************* SLF4J source code and binaries are distributed under the following license: ******************************************************************************* Copyright (c) 2004-2011 QOS.ch All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ******************************************************************************* Logback source code and binaries are dual-licensed under the EPL v1.0 and the LGPL 2.1, or more formally: ******************************************************************************* Logback: the reliable, generic, fast and flexible logging framework. Copyright (C) 1999-2012, QOS.ch. All rights reserved. This program and the accompanying materials are dual-licensed under either the terms of the Eclipse Public License v1.0 as published by the Eclipse Foundation or (per the licensee's choosing) under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation. ******************************************************************************* ASM library: ******************************************************************************* Copyright (c) 2000-2011 INRIA, France Telecom All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holders nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ******************************************************************************* Jadx-gui components =================== RSyntaxTextArea library (https://github.com/bobbylight/RSyntaxTextArea) licensed under modified BSD license: ******************************************************************************* Copyright (c) 2012, Robert Futrell All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ******************************************************************************* Concurrent Trees (https://code.google.com/p/concurrent-trees/) licenced under Apache License 2.0: ******************************************************************************* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ******************************************************************************* Image Viewer (https://github.com/kazocsaba/imageviewer) ******************************************************************************* Copyright (c) 2008-2012 Kazó Csaba Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ******************************************************************************* JFontChooser Component - http://sourceforge.jp/projects/jfontchooser/ Icons copied from several places: - Eclipse Project (JDT UI) - licensed under EPL v1.0 (http://www.eclipse.org/legal/epl-v10.html) - famfamfam silk icon set (http://www.famfamfam.com/lab/icons/silk/) - licensed under Creative Commons Attribution 2.5 License (http://creativecommons.org/licenses/by/2.5/) ================================================ FILE: README.md ================================================ ## JADX ![Build status](https://img.shields.io/github/actions/workflow/status/skylot/jadx/build-artifacts.yml) ![GitHub contributors](https://img.shields.io/github/contributors/skylot/jadx) ![GitHub all releases](https://img.shields.io/github/downloads/skylot/jadx/total) ![GitHub release (latest by SemVer)](https://img.shields.io/github/downloads/skylot/jadx/latest/total) ![Latest release](https://img.shields.io/github/release/skylot/jadx.svg) [![Maven Central](https://img.shields.io/maven-central/v/io.github.skylot/jadx-core)](https://search.maven.org/search?q=g:io.github.skylot%20AND%20jadx) ![Java 11+](https://img.shields.io/badge/Java-11%2B-blue) [![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) **jadx** - Dex to Java decompiler Command line and GUI tools for producing Java source code from Android Dex and Apk files > [!WARNING] > Please note that in most cases **jadx** can't decompile all 100% of the code, so errors will occur.
> Check [Troubleshooting guide](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A#decompilation-issues) for workarounds. **Main features:** - decompile Dalvik bytecode to Java code from APK, dex, aar, aab and zip files - decode `AndroidManifest.xml` and other resources from `resources.arsc` - deobfuscator included **jadx-gui features:** - view decompiled code with highlighted syntax - jump to declaration - find usage - full text search - smali debugger, check [wiki page](https://github.com/skylot/jadx/wiki/Smali-debugger) for setup and usage Jadx-gui key bindings can be found [here](https://github.com/skylot/jadx/wiki/JADX-GUI-Key-bindings) See these features in action here: [jadx-gui features overview](https://github.com/skylot/jadx/wiki/jadx-gui-features-overview) ### Download - release from [github: ![Latest release](https://img.shields.io/github/release/skylot/jadx.svg)](https://github.com/skylot/jadx/releases/latest) - latest [unstable build ![GitHub commits since tagged version (branch)](https://img.shields.io/github/commits-since/skylot/jadx/latest/master)](https://nightly.link/skylot/jadx/workflows/build-artifacts/master) After download unpack zip file go to `bin` directory and run: - `jadx` - command line version - `jadx-gui` - UI version On Windows run `.bat` files with double-click\ **Note:** ensure you have installed Java 11 or later 64-bit version. For Windows, you can download it from [oracle.com](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) (select x64 Installer). ### Install - Arch Linux [![Arch Linux package](https://img.shields.io/archlinux/v/extra/any/jadx)](https://archlinux.org/packages/extra/any/jadx/) [![AUR Version](https://img.shields.io/aur/version/jadx-git)](https://aur.archlinux.org/packages/jadx-git) ```bash sudo pacman -S jadx ``` - macOS [![homebrew version](https://img.shields.io/homebrew/v/jadx)](https://formulae.brew.sh/formula/jadx) ```bash brew install jadx ``` - Flathub [![Flathub Version](https://img.shields.io/flathub/v/com.github.skylot.jadx)](https://flathub.org/apps/com.github.skylot.jadx) ```bash flatpak install flathub com.github.skylot.jadx ``` ### Use jadx as a library You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library) ### Build from source JDK 11 or higher must be installed: ``` git clone https://github.com/skylot/jadx.git cd jadx ./gradlew dist ``` (on Windows, use `gradlew.bat` instead of `./gradlew`) Scripts for run jadx will be placed in `build/jadx/bin` and also packed to `build/jadx-.zip` ### Usage ``` jadx[-gui] [command] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts) commands (use ' --help' for command options): plugins - manage jadx plugins options: -d, --output-dir - output directory -ds, --output-dir-src - output directory for sources -dr, --output-dir-res - output directory for resources -r, --no-res - do not decode resources -s, --no-src - do not decompile source code -j, --threads-count - processing threads count, default: 16 --single-class - decompile a single class, full name, raw or alias --single-class-output - file or dir for write if decompile a single class --output-format - can be 'java' or 'json', default: java -e, --export-gradle - save as gradle project (set '--export-gradle-type' to 'auto') --export-gradle-type - Gradle project template for export: 'auto' - detect automatically 'android-app' - Android Application (apk) 'android-library' - Android Library (aar) 'simple-java' - simple Java -m, --decompilation-mode - code output mode: 'auto' - trying best options (default) 'restructure' - restore code structure (normal java code) 'simple' - simplified instructions (linear, with goto's) 'fallback' - raw instructions without modifications --show-bad-code - show inconsistent code (incorrectly decompiled) --no-xml-pretty-print - do not prettify XML --no-imports - disable use of imports, always write entire package name --no-debug-info - disable debug info parsing and processing --add-debug-lines - add comments with debug line numbers if available --no-inline-anonymous - disable anonymous classes inline --no-inline-methods - disable methods inline --no-move-inner-classes - disable move inner classes into parent --no-inline-kotlin-lambda - disable inline for Kotlin lambdas --no-finally - don't extract finally block --no-restore-switch-over-string - don't restore switch over string --no-replace-consts - don't replace constant value with matching constant field --escape-unicode - escape non latin characters in strings (with \u) --respect-bytecode-access-modifiers - don't change original access modifiers --mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory --mappings-mode - set mode for handling the deobfuscation mapping file: 'read' - just read, user can always save manually (default) 'read-and-autosave-every-change' - read and autosave after every change 'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project 'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file) --deobf - activate deobfuscation --deobf-min - min length of name, renamed if shorter, default: 3 --deobf-max - max length of name, renamed if longer, default: 64 --deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px --deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension --deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file: 'read' - read if found, don't save (default) 'read-or-save' - read if found, save otherwise (don't overwrite) 'overwrite' - don't read, always save 'ignore' - don't read and don't save --deobf-res-name-source - better name source for resources: 'auto' - automatically select best name (default) 'resources' - use resources names 'code' - use R class fields names --use-source-name-as-class-name-alias - use source name as class name alias: 'always' - always use source name if it's available 'if-better' - use source name if it seems better than the current one 'never' - never use source name, even if it's available --source-name-repeat-limit - allow using source name if it appears less than a limit number, default: 10 --use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply --use-headers-for-detect-resource-extensions - Use headers for detect resource extensions if resource obfuscated --rename-flags - fix options (comma-separated list of): 'case' - fix case sensitivity issues (according to --fs-case-sensitive option), 'valid' - rename java identifiers to make them valid, 'printable' - remove non-printable chars from identifiers, or single 'none' - to disable all renames or single 'all' - to enable all (default) --integer-format - how integers are displayed: 'auto' - automatically select (default) 'decimal' - use decimal 'hexadecimal' - use hexadecimal --type-update-limit - type update limit count (per one instruction), default: 10 --fs-case-sensitive - treat filesystem as case sensitive, false by default --cfg - save methods control flow graph to dot file --raw-cfg - save methods control flow graph (use raw instructions) -f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated) --use-dx - use dx/d8 to convert java bytecode --comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info --log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress -v, --verbose - verbose output (set --log-level to DEBUG) -q, --quiet - turn off output (set --log-level to QUIET) --disable-plugins - comma separated list of plugin ids to disable --config - load configuration from file, can be: path to '.json' file short name - uses file with this name from config directory 'none' - to disable config loading --save-config - save current options into configuration file and exit, can be: empty - for default config path to '.json' file short name - file will be saved in config directory --print-files - print files and directories used by jadx (config, cache, temp) --version - print jadx version -h, --help - print this help Plugin options (-P=): dex-input: Load .dex and .apk files - dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes java-convert: Convert .class, .jar and .aar files to dex - java-convert.mode - convert mode, values: [dx, d8, both], default: both - java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no kotlin-metadata: Use kotlin.Metadata annotation for code generation - kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes - kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes - kotlin-metadata.fields - rename fields, values: [yes, no], default: yes - kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes - kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes - kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes - kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes kotlin-smap: Use kotlin.SourceDebugExtension annotation for rename class alias - kotlin-smap.class-alias-source-dbg - rename class alias from SourceDebugExtension, values: [yes, no], default: no rename-mappings: various mappings support - rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, PROGUARD_FILE, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, INTELLIJ_MIGRATION_MAP_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO - rename-mappings.invert - invert mapping on load, values: [yes, no], default: no smali-input: Load .smali files - smali-input.api-level - Android API level, default: 27 Environment variables: JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files JADX_DISABLE_ZIP_SECURITY - set to 'true' to disable all security checks for zip files JADX_ZIP_MAX_ENTRIES_COUNT - maximum allowed number of entries in zip files (default: 100 000) JADX_CONFIG_DIR - custom config directory, using system by default JADX_CACHE_DIR - custom cache directory, using system by default JADX_TMP_DIR - custom temp directory, using system by default Examples: jadx -d out classes.dex jadx --rename-flags "none" classes.dex jadx --rename-flags "valid, printable" classes.dex jadx --log-level ERROR app.apk jadx -Pdex-input.verify-checksum=no app.apk ``` These options also work in jadx-gui running from command line and override options from preferences' dialog Usage for `plugins` command ``` usage: plugins [options] options: -i, --install - install plugin with locationId -j, --install-jar - install plugin from jar file -l, --list - list installed plugins -a, --available - list available plugins from jadx-plugins-list (aka marketplace) -u, --update - update installed plugins --uninstall - uninstall plugin with pluginId --disable - disable plugin with pluginId --enable - enable plugin with pluginId --list-all - list all plugins including bundled and dropins --list-versions - fetch latest versions of plugin from locationId (will download all artefacts, limited to 10) -h, --help - print this help ``` ### Troubleshooting Please check wiki page [Troubleshooting Q&A](https://github.com/skylot/jadx/wiki/Troubleshooting-Q&A) ### Contributing To support this project you can: - Post thoughts about new features/optimizations that important to you - Submit decompilation issues, please read before proceed: [Open issue](CONTRIBUTING.md#Open-Issue) - Open pull request, please follow these rules: [Pull Request Process](CONTRIBUTING.md#Pull-Request-Process) --------------------------------------- *Licensed under the Apache 2.0 License* ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability To report a security issue, please open a [new security advisory](https://github.com/skylot/jadx/security/advisories/new). Please fill the steps you took to create the issue, affected versions, and, if known, mitigations for the issue. We will check and respond within 3 working days. If the issue is confirmed as a vulnerability, we will apply required mitigations at the next release. ================================================ FILE: build.gradle.kts ================================================ import com.diffplug.gradle.spotless.FormatExtension import com.diffplug.gradle.spotless.SpotlessExtension import com.diffplug.spotless.LineEnding import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform import java.util.Locale plugins { id("com.github.ben-manes.versions") version "0.53.0" id("se.patrikerdes.use-latest-versions") version "0.2.19" id("com.diffplug.spotless") version "6.25.0" } val jadxVersion by extra { System.getenv("JADX_VERSION") ?: "dev" } println("jadx version: $jadxVersion") version = jadxVersion val jadxBuildJavaVersion by extra { getBuildJavaVersion() } fun getBuildJavaVersion(): Int? { val envVarName = "JADX_BUILD_JAVA_VERSION" val buildJavaVer = System.getenv(envVarName)?.toInt() ?: return null if (buildJavaVer < 11) { throw GradleException("'$envVarName' can't be set to lower than 11") } println("Set Java toolchain for jadx build to version '$buildJavaVer'") return buildJavaVer } allprojects { apply(plugin = "java") apply(plugin = "checkstyle") apply(plugin = "com.diffplug.spotless") apply(plugin = "com.github.ben-manes.versions") apply(plugin = "se.patrikerdes.use-latest-versions") repositories { mavenCentral() } configure { java { importOrderFile("$rootDir/config/code-formatter/eclipse.importorder") eclipse().configFile("$rootDir/config/code-formatter/eclipse.xml") removeUnusedImports() commonFormatOptions() } kotlin { ktlint().editorConfigOverride(mapOf("indent_style" to "tab")) commonFormatOptions() } kotlinGradle { ktlint() commonFormatOptions() } format("misc") { target("**/*.gradle", "**/*.xml", "**/.gitignore", "**/.properties") targetExclude(".gradle/**", ".idea/**", "*/build/**") commonFormatOptions() } } tasks.named("dependencyUpdates") { rejectVersionIf { // disallow release candidates as upgradable versions from stable versions isNonStable(candidate.version) && !isNonStable(currentVersion) } } } fun FormatExtension.commonFormatOptions() { lineEndings = LineEnding.UNIX encoding = Charsets.UTF_8 trimTrailingWhitespace() endWithNewline() } fun isNonStable(version: String): Boolean { val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase(Locale.getDefault()).contains(it) } val regex = "^[0-9,.v-]+(-r)?$".toRegex() val isStable = stableKeyword || regex.matches(version) return isStable.not() } val distWinConfiguration: Configuration by configurations.creating { isCanBeConsumed = false } val distWinWithJreConfiguration: Configuration by configurations.creating { isCanBeConsumed = false } dependencies { distWinConfiguration(project(":jadx-gui", "distWinConfiguration")) distWinWithJreConfiguration(project(":jadx-gui", "distWinWithJreConfiguration")) } val copyArtifacts by tasks.registering(Copy::class) { val jarCliPattern = "jadx-cli-(.*)-all.jar".toPattern() from(tasks.getByPath(":jadx-cli:installShadowDist")) { exclude("**/*.jar") filter { line -> jarCliPattern.matcher(line).replaceAll("jadx-$1-all.jar") .replace("-jar \"\\\"\$CLASSPATH\\\"\"", "-cp \"\\\"\$CLASSPATH\\\"\" jadx.cli.JadxCLI") .replace("-jar \"%CLASSPATH%\"", "-cp \"%CLASSPATH%\" jadx.cli.JadxCLI") } } val jarGuiPattern = "jadx-gui-(.*)-all.jar".toPattern() from(tasks.getByPath(":jadx-gui:installShadowDist")) { exclude("**/*.jar") filter { line -> jarGuiPattern.matcher(line).replaceAll("jadx-$1-all.jar") } } from(tasks.getByPath(":jadx-gui:installShadowDist")) { include("**/*.jar") rename("jadx-gui-(.*)-all.jar", "jadx-$1-all.jar") } from(layout.projectDirectory) { include("README.md") include("LICENSE") } into(layout.buildDirectory.dir("jadx")) duplicatesStrategy = DuplicatesStrategy.EXCLUDE } val pack by tasks.registering(Zip::class) { from(copyArtifacts) archiveFileName.set("jadx-$jadxVersion.zip") destinationDirectory.set(layout.buildDirectory) } val distWin by tasks.registering(Zip::class) { group = "jadx" description = "Build Windows bundle" from(distWinConfiguration) destinationDirectory.set(layout.buildDirectory.dir("distWin")) archiveFileName.set("jadx-gui-$jadxVersion-win.zip") duplicatesStrategy = DuplicatesStrategy.EXCLUDE } val distWinWithJre by tasks.registering(Zip::class) { description = "Build Windows with JRE bundle" from(distWinWithJreConfiguration) destinationDirectory.set(layout.buildDirectory.dir("distWinWithJre")) archiveFileName.set("jadx-gui-$jadxVersion-with-jre-win.zip") duplicatesStrategy = DuplicatesStrategy.EXCLUDE } val dist by tasks.registering { group = "jadx" description = "Build jadx distribution zip bundles" dependsOn(pack) val os = DefaultNativePlatform.getCurrentOperatingSystem() if (os.isWindows) { if (project.hasProperty("bundleJRE")) { println("Build win bundle with JRE") dependsOn(distWinWithJre) } else { dependsOn(distWin) } } } val cleanBuildDir by tasks.registering(Delete::class) { delete(layout.buildDirectory) } tasks.getByName("clean").dependsOn(cleanBuildDir) ================================================ FILE: buildSrc/build.gradle.kts ================================================ plugins { `kotlin-dsl` } dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10") implementation("org.openrewrite:plugin:6.19.1") } repositories { gradlePluginPortal() } ================================================ FILE: buildSrc/src/main/kotlin/jadx-java.gradle.kts ================================================ import org.gradle.api.tasks.testing.logging.TestExceptionFormat plugins { java checkstyle id("jadx-rewrite") } val jadxVersion: String by rootProject.extra val jadxBuildJavaVersion: Int? by rootProject.extra group = "io.github.skylot" version = jadxVersion dependencies { implementation("org.slf4j:slf4j-api:2.0.17") compileOnly("org.jetbrains:annotations:26.0.2") testImplementation("ch.qos.logback:logback-classic:1.5.22") testImplementation("org.assertj:assertj-core:3.27.6") testImplementation("org.junit.jupiter:junit-jupiter:5.13.3") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testCompileOnly("org.jetbrains:annotations:26.0.2") } repositories { mavenCentral() // required for: aapt-proto, r8, smali google() } java { jadxBuildJavaVersion?.let { buildJavaVer -> toolchain { languageVersion = JavaLanguageVersion.of(buildJavaVer) } } sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } tasks { compileJava { options.encoding = "UTF-8" // options.compilerArgs = listOf("-Xlint:deprecation") } jar { manifest { attributes("jadx-version" to jadxVersion) } } test { useJUnitPlatform() maxParallelForks = Runtime.getRuntime().availableProcessors() testLogging { showExceptions = true exceptionFormat = TestExceptionFormat.FULL showCauses = true } } } ================================================ FILE: buildSrc/src/main/kotlin/jadx-kotlin.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("jadx-java") id("org.jetbrains.kotlin.jvm") } dependencies { implementation(kotlin("stdlib")) implementation(kotlin("reflect")) // don't work from plugin classloader } kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } ================================================ FILE: buildSrc/src/main/kotlin/jadx-library.gradle.kts ================================================ plugins { id("jadx-java") id("java-library") id("maven-publish") id("signing") } val jadxVersion: String by rootProject.extra group = "io.github.skylot" version = jadxVersion java { withJavadocJar() withSourcesJar() } publishing { publications { create("mavenJava") { artifactId = project.name from(components["java"]) versionMapping { usage("java-api") { fromResolutionOf("runtimeClasspath") } usage("java-runtime") { fromResolutionResult() } } pom { name.set(project.name) description.set(project.description ?: "Dex to Java decompiler") url.set("https://github.com/skylot/jadx") licenses { license { name.set("The Apache License, Version 2.0") url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") } } developers { developer { id.set("skylot") name.set("Skylot") email.set(project.properties["libEmail"].toString()) url.set("https://github.com/skylot") } } scm { connection.set("scm:git:git://github.com/skylot/jadx.git") developerConnection.set("scm:git:ssh://github.com:skylot/jadx.git") url.set("https://github.com/skylot/jadx") } } } } repositories { maven { val releasesRepoUrl = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") val snapshotsRepoUrl = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl credentials { username = project.properties["ossrhUser"].toString() password = project.properties["ossrhPassword"].toString() } } } } signing { isRequired = gradle.taskGraph.hasTask("publish") sign(publishing.publications["mavenJava"]) } tasks.javadoc { val stdOptions = options as StandardJavadocDocletOptions stdOptions.addBooleanOption("html5", true) // disable 'missing' warnings stdOptions.addStringOption("Xdoclint:all,-missing", "-quiet") } ================================================ FILE: buildSrc/src/main/kotlin/jadx-rewrite.gradle.kts ================================================ plugins { id("org.openrewrite.rewrite") } repositories { mavenCentral() } dependencies { rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:3.24.0") rewrite("org.openrewrite.recipe:rewrite-logging-frameworks:3.20.0") rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.24.0") rewrite("org.openrewrite.recipe:rewrite-static-analysis:2.24.0") } tasks { rewrite { // exclusion("src/test/java/jadx/tests/integration") // activeRecipe("org.openrewrite.java.migrate.Java8toJava11") // checkstyle auto fix // activeRecipe("org.openrewrite.staticanalysis.CodeCleanup") // setCheckstyleConfigFile(file("$rootDir/config/checkstyle/checkstyle.xml")) // logging // activeRecipe("org.openrewrite.java.logging.slf4j.Slf4jBestPractices") // activeRecipe("org.openrewrite.java.logging.slf4j.LoggersNamedForEnclosingClass") // activeRecipe("org.openrewrite.java.logging.slf4j.ParameterizedLogging") // activeRecipe("org.openrewrite.java.logging.PrintStackTraceToLogError") // testing activeRecipe("org.openrewrite.java.testing.assertj.Assertj") } } ================================================ FILE: config/checkstyle/checkstyle.xml ================================================ ================================================ FILE: config/code-formatter/eclipse.importorder ================================================ #Import Order 0=java 1=javax 2=org 3=com 4= 5=jadx 6=\# ================================================ FILE: config/code-formatter/eclipse.xml ================================================ ================================================ FILE: config/jflex/.gitignore ================================================ SmaliTokenMaker.java ================================================ FILE: config/jflex/README.md ================================================ Refer to the following instructions to modify and use to generate SmaliTokenMarker ```shell jflex SmaliTokenMaker.flex --skel skeleton.default ``` ================================================ FILE: config/jflex/SmaliTokenMaker.flex ================================================ /* * Generated on 11/22/21, 8:58 PM */ package jadx.gui.ui.codearea; import java.io.*; import javax.swing.text.Segment; import org.fife.ui.rsyntaxtextarea.*; /* * 用于Smali代码高亮 * MartinKay@qq.com */ %% %public %class SmaliTokenMaker %extends AbstractJFlexCTokenMaker %unicode /* Case sensitive */ %type org.fife.ui.rsyntaxtextarea.Token %{ /** * Constructor. This must be here because JFlex does not generate a * no-parameter constructor. */ public SmaliTokenMaker() { } /** * Adds the token specified to the current linked list of tokens. * * @param tokenType The token's type. * @see #addToken(int, int, int) */ private void addHyperlinkToken(int start, int end, int tokenType) { int so = start + offsetShift; addToken(zzBuffer, start,end, tokenType, so, true); } /** * Adds the token specified to the current linked list of tokens. * * @param tokenType The token's type. */ private void addToken(int tokenType) { addToken(zzStartRead, zzMarkedPos-1, tokenType); } /** * Adds the token specified to the current linked list of tokens. * * @param tokenType The token's type. * @see #addHyperlinkToken(int, int, int) */ private void addToken(int start, int end, int tokenType) { int so = start + offsetShift; addToken(zzBuffer, start,end, tokenType, so, false); } /** * Adds the token specified to the current linked list of tokens. * * @param array The character array. * @param start The starting offset in the array. * @param end The ending offset in the array. * @param tokenType The token's type. * @param startOffset The offset in the document at which this token * occurs. * @param hyperlink Whether this token is a hyperlink. */ public void addToken(char[] array, int start, int end, int tokenType, int startOffset, boolean hyperlink) { super.addToken(array, start,end, tokenType, startOffset, hyperlink); zzStartRead = zzMarkedPos; } /** * {@inheritDoc} */ public String[] getLineCommentStartAndEnd(int languageIndex) { return new String[] { "#", null }; } /** * Returns the first token in the linked list of tokens generated * from text. This method must be implemented by * subclasses so they can correctly implement syntax highlighting. * * @param text The text from which to get tokens. * @param initialTokenType The token type we should start with. * @param startOffset The offset into the document at which * text starts. * @return The first Token in a linked list representing * the syntax highlighted text. */ public Token getTokenList(Segment text, int initialTokenType, int startOffset) { resetTokenList(); this.offsetShift = -text.offset + startOffset; // Start off in the proper state. int state = Token.NULL; switch (initialTokenType) { /* No multi-line comments */ /* No documentation comments */ default: state = Token.NULL; } s = text; try { yyreset(zzReader); yybegin(state); return yylex(); } catch (IOException ioe) { ioe.printStackTrace(); return new TokenImpl(); } } /** * Refills the input buffer. * * @return true if EOF was reached, otherwise * false. */ private boolean zzRefill() { return zzCurrentPos>=s.offset+s.count; } /** * Resets the scanner to read from a new input stream. * Does not close the old reader. * * All internal variables are reset, the old input stream * cannot be reused (internal buffer is discarded and lost). * Lexical state is set to YY_INITIAL. * * @param reader the new input stream */ public final void yyreset(Reader reader) { // 's' has been updated. zzBuffer = s.array; /* * We replaced the line below with the two below it because zzRefill * no longer "refills" the buffer (since the way we do it, it's always * "full" the first time through, since it points to the segment's * array). So, we assign zzEndRead here. */ //zzStartRead = zzEndRead = s.offset; zzStartRead = s.offset; zzEndRead = zzStartRead + s.count - 1; zzCurrentPos = zzMarkedPos = zzPushbackPos = s.offset; zzLexicalState = YYINITIAL; zzReader = reader; zzAtBOL = true; zzAtEOF = false; } %} Letter = [A-Za-z] LetterOrUnderscore = ({Letter}|"_") NonzeroDigit = [1-9] Digit = ("0"|{NonzeroDigit}) HexDigit = ({Digit}|[A-Fa-f]) OctalDigit = ([0-7]) AnyCharacterButApostropheOrBackSlash = ([^\\']) AnyCharacterButDoubleQuoteOrBackSlash = ([^\\\"\n]) EscapedSourceCharacter = ("u"{HexDigit}{HexDigit}{HexDigit}{HexDigit}) Escape = ("\\"(([btnfr\"'\\])|([0123]{OctalDigit}?{OctalDigit}?)|({OctalDigit}{OctalDigit}?)|{EscapedSourceCharacter})) NonSeparator = ([^\t\f\r\n\ \(\)\{\}\[\]\;\,\.\=\>\<\!\~\?\:\+\-\*\/\&\|\^\%\"\']|"#"|"\\") IdentifierStart = ({LetterOrUnderscore}|"$") IdentifierPart = ({IdentifierStart}|{Digit}|("\\"{EscapedSourceCharacter})) LineTerminator = (\n) WhiteSpace = ([ \t\f]+) CharLiteral = ([\']({AnyCharacterButApostropheOrBackSlash}|{Escape})[\']) UnclosedCharLiteral = ([\'][^\'\n]*) ErrorCharLiteral = ({UnclosedCharLiteral}[\']) StringLiteral = ([\"]({AnyCharacterButDoubleQuoteOrBackSlash}|{Escape})*[\"]) UnclosedStringLiteral = ([\"]([\\].|[^\\\"])*[^\"]?) ErrorStringLiteral = ({UnclosedStringLiteral}[\"]) /* No multi-line comments */ /* No documentation comments */ LineCommentBegin = "#" IntegerLiteral = ({Digit}+) HexLiteral = (0x{HexDigit}+) FloatLiteral = (({Digit}+)("."{Digit}+)?(e[+-]?{Digit}+)? | ({Digit}+)?("."{Digit}+)(e[+-]?{Digit}+)?) ErrorNumberFormat = (({IntegerLiteral}|{HexLiteral}|{FloatLiteral}){NonSeparator}+) BooleanLiteral = ("true"|"false") Separator = ([\(\)\{\}\[\]]) Separator2 = ([\;,.]) Identifier = ({IdentifierStart}{IdentifierPart}*) URLGenDelim = ([:\/\?#\[\]@]) URLSubDelim = ([\!\$&'\(\)\*\+,;=]) URLUnreserved = ({LetterOrUnderscore}|{Digit}|[\-\.\~]) URLCharacter = ({URLGenDelim}|{URLSubDelim}|{URLUnreserved}|[%]) URLCharacters = ({URLCharacter}*) URLEndCharacter = ([\/\$]|{Letter}|{Digit}) URL = (((https?|f(tp|ile))"://"|"www.")({URLCharacters}{URLEndCharacter})?) /* Custom Regex */ /* fully-qualified name Rules */ SimpleName = ([a-zA-Z0-9_$]*) QUALIFIED_TYPE_NAME = ("L"({SimpleName}{SLASH})*{SimpleName}*";") /* Types */ VOID_TYPE = ("V") BOOLEAN_TYPE = ("Z") BYTE_TYPE = ("B") SHORT_TYPE = ("S") CHAR_TYPE = ("C") INT_TYPE = ("I") LONG_TYPE = ("J") FLOAT_TYPE = ("F") DOUBLE_TYPE = ("D") /* Multi Args Types Highlight */ MULTI_ARGS_TYPES = (({BOOLEAN_TYPE}|{BYTE_TYPE}|{SHORT_TYPE}|{CHAR_TYPE}|{INT_TYPE}|{LONG_TYPE}|{FLOAT_TYPE}|{DOUBLE_TYPE})+); /* Types fully-qualified name */ COMPOUND_METHOD_ARG_LITERAL = (({BOOLEAN_TYPE}|{BYTE_TYPE}|{SHORT_TYPE}|{CHAR_TYPE}|{INT_TYPE}|{LONG_TYPE}|{FLOAT_TYPE}|{DOUBLE_TYPE})+{QUALIFIED_TYPE_NAME}) LBRACK = ("[") RBRACK = ("]") LPAREN = ("(") RPAREN = (")") LBRACE = ("{") RBRACE = ("}") COLON = (":") ASSIGN = ("=") DOT = (".") SUB = ("-") COMMA = (",") SLASH = ("/") LT = ("<") GT = (">") ARROW = ("->") SEMI = (";") ARROW_FUNCTION = ({ARROW}[a-zA-Z_$<>]*) CustomSeparator = ({Separator}|{ARROW_FUNCTION}) /* Register */ VREGISTER = ("v"("0"|[1-9])*) PREGISTER = ("p"("0"|[1-9])*) /* Flags */ FLAG_PSWITCH = (":pswitch_"{SimpleName}) FLAG_PSWITCH_DATA = (":pswitch_data_"{SimpleName}) FLAG_GOTO = (":goto_"{SimpleName}) FLAG_COND = (":cond_"{SimpleName}) FLAG_TRY_START = (":try_start_"{SimpleName}) FLAG_TRY_END = (":try_end_"{SimpleName}) FLAG_CATCH = (":catch_"{SimpleName}) FLAG_CATCHALL = (":catchall_"{SimpleName}) FLAG_ARRAY = (":array_"{SimpleName}) /* No string state */ /* No char state */ /* No MLC state */ /* No documentation comment state */ %state EOL_COMMENT %% { /* Keywords Instructions Highlight */ "nop" | "move" | "move/from16" | "move/16" | "move-wide" | "move-wide/from16" | "move-wide/16" | "move-object" | "move-object/from16" | "move-object/16" | "move-result" | "move-result-wide" | "move-result-object" | "move-exception" | "return-void" | "return" | "return-wide" | "return-object" | "const/4" | "const/16" | "const" | "const/high16" | "const-wide/16" | "const-wide/32" | "const-wide" | "const-wide/high16" | "const-string" | "const-string/jumbo" | "const-class" | "monitor-enter" | "monitor-exit" | "check-cast" | "instance-of" | "array-length" | "new-instance" | "new-array" | "filled-new-array" | "filled-new-array/range" | "fill-array-data" | "throw" | "goto" | "goto/16" | "goto/32" | "cmpl-float" | "cmpg-float" | "cmpl-double" | "cmpg-double" | "cmp-long" | "if-eq" | "if-ne" | "if-lt" | "if-ge" | "if-gt" | "if-le" | "if-eqz" | "if-nez" | "if-ltz" | "if-gez" | "if-gtz" | "if-lez" | "aget" | "aget-wide" | "aget-object" | "aget-boolean" | "aget-byte" | "aget-char" | "aget-short" | "aput" | "aput-wide" | "aput-object" | "aput-boolean" | "aput-byte" | "aput-char" | "aput-short" | "iget" | "iget-wide" | "iget-object" | "iget-boolean" | "iget-byte" | "iget-char" | "iget-short" | "iput" | "iput-wide" | "iput-object" | "iput-boolean" | "iput-byte" | "iput-char" | "iput-short" | "sget" | "sget-wide" | "sget-object" | "sget-boolean" | "sget-byte" | "sget-char" | "sget-short" | "sput" | "sput-wide" | "sput-object" | "sput-boolean" | "sput-byte" | "sput-char" | "sput-short" | "invoke-virtual" | "invoke-super" | "invoke-direct" | "invoke-static" | "invoke-interface" | "invoke-virtual/range" | "invoke-super/range" | "invoke-direct/range" | "invoke-static/range" | "invoke-interface/range" | "neg-int" | "not-int" | "neg-long" | "not-long" | "neg-float" | "neg-double" | "int-to-long" | "int-to-float" | "int-to-double" | "long-to-int" | "long-to-float" | "long-to-double" | "float-to-int" | "float-to-long" | "float-to-double" | "double-to-int" | "double-to-long" | "double-to-float" | "int-to-byte" | "int-to-char" | "int-to-short" | "add-int" | "sub-int" | "mul-int" | "div-int" | "rem-int" | "and-int" | "or-int" | "xor-int" | "shl-int" | "shr-int" | "ushr-int" | "add-long" | "sub-long" | "mul-long" | "div-long" | "rem-long" | "and-long" | "or-long" | "xor-long" | "shl-long" | "shr-long" | "ushr-long" | "add-float" | "sub-float" | "mul-float" | "div-float" | "rem-float" | "add-double" | "sub-double" | "mul-double" | "div-double" | "rem-double" | "add-int/2addr" | "sub-int/2addr" | "mul-int/2addr" | "div-int/2addr" | "rem-int/2addr" | "and-int/2addr" | "or-int/2addr" | "xor-int/2addr" | "shl-int/2addr" | "shr-int/2addr" | "ushr-int/2addr" | "add-long/2addr" | "sub-long/2addr" | "mul-long/2addr" | "div-long/2addr" | "rem-long/2addr" | "and-long/2addr" | "or-long/2addr" | "xor-long/2addr" | "shl-long/2addr" | "shr-long/2addr" | "ushr-long/2addr" | "add-float/2addr" | "sub-float/2addr" | "mul-float/2addr" | "div-float/2addr" | "rem-float/2addr" | "add-double/2addr" | "sub-double/2addr" | "mul-double/2addr" | "div-double/2addr" | "rem-double/2addr" | "add-int/lit16" | "rsub-int" | "mul-int/lit16" | "div-int/lit16" | "rem-int/lit16" | "and-int/lit16" | "or-int/lit16" | "xor-int/lit16" | "add-int/lit8" | "rsub-int/lit8" | "mul-int/lit8" | "div-int/lit8" | "rem-int/lit8" | "and-int/lit8" | "or-int/lit8" | "xor-int/lit8" | "shl-int/lit8" | "shr-int/lit8" | "ushr-int/lit8" | "invoke-polymorphic" | "invoke-polymorphic/range" | "invoke-custom" | "invoke-custom/range" | "const-method-handle" | "const-method-type" | "packed-switch" | "sparse-switch" { addToken(Token.FUNCTION); } /* Keywords Modifiers(IDENTIFIER标识符、修饰符) Highlight */ "public" | "private" | "protected" | "final" | "annotation" | "static" | "synthetic" | "constructor" | "abstract" | "enum" | "interface" | "transient" | "bridge" | "declared-synchronized" | "volatile" | "strictfp" | "varargs" | "native" { addToken(Token.RESERVED_WORD); } /* Keywords Directives Highlight */ ".method" | ".end method" | ".implements" | ".class" | ".prologue" | ".source" | ".super" | ".field" | ".end field" | ".registers" | ".locals" | ".param" | ".line" | ".catch" | ".catchall" | ".annotation" | ".end annotation" | ".local" | ".end local" | ".restart local" | ".packed-switch" | ".end packed-switch" | ".array-data" | ".end array-data" | ".sparse-switch" | ".end sparse-switch" | ".end param" { addToken(Token.RESERVED_WORD_2); } /* VARIABLE Register Highlight */ {VREGISTER} | {PREGISTER} { addToken(Token.VARIABLE); } /* Data types Highlight */ {QUALIFIED_TYPE_NAME} | {COMPOUND_METHOD_ARG_LITERAL} | {MULTI_ARGS_TYPES} | {QUALIFIED_TYPE_NAME} | {VOID_TYPE} | {BOOLEAN_TYPE} | {BYTE_TYPE} | {SHORT_TYPE} | {CHAR_TYPE} | {INT_TYPE} | {LONG_TYPE} | {FLOAT_TYPE} | {DOUBLE_TYPE} { addToken(Token.DATA_TYPE); } /* FLAGS */ {FLAG_PSWITCH} | {FLAG_PSWITCH_DATA} | {FLAG_GOTO} | {FLAG_COND} | {FLAG_TRY_START} | {FLAG_TRY_END} | {FLAG_CATCHALL} | {FLAG_ARRAY} | {FLAG_CATCH} { addToken(Token.MARKUP_TAG_NAME); } /* Functions */ /* No functions */ {BooleanLiteral} { addToken(Token.LITERAL_BOOLEAN); } {LineTerminator} { addNullToken(); return firstToken; } {Identifier} { addToken(Token.IDENTIFIER); } {WhiteSpace} { addToken(Token.WHITESPACE); } /* String/Character literals. */ {CharLiteral} { addToken(Token.LITERAL_CHAR); } {UnclosedCharLiteral} { addToken(Token.ERROR_CHAR); addNullToken(); return firstToken; } {ErrorCharLiteral} { addToken(Token.ERROR_CHAR); } {StringLiteral} { addToken(Token.LITERAL_STRING_DOUBLE_QUOTE); } {UnclosedStringLiteral} { addToken(Token.ERROR_STRING_DOUBLE); addNullToken(); return firstToken; } {ErrorStringLiteral} { addToken(Token.ERROR_STRING_DOUBLE); } /* Comment literals. */ /* No multi-line comments */ /* No documentation comments */ {LineCommentBegin} { start = zzMarkedPos-1; yybegin(EOL_COMMENT); } /* Separators. */ {CustomSeparator} { addToken(Token.SEPARATOR); } {Separator2} { addToken(Token.IDENTIFIER); } /* Operators. */ "!" | ";" | "." | "=" | "/" | "'" | "(" | ")" | "," | "->" | ";->" | "<" | ">" | "@" | "[" | "]" | "{" | "}" { addToken(Token.OPERATOR); } /* Numbers */ {IntegerLiteral} { addToken(Token.LITERAL_NUMBER_DECIMAL_INT); } {HexLiteral} { addToken(Token.LITERAL_NUMBER_HEXADECIMAL); } {FloatLiteral} { addToken(Token.LITERAL_NUMBER_FLOAT); } {ErrorNumberFormat} { addToken(Token.ERROR_NUMBER_FORMAT); } /* Ended with a line not in a string or comment. */ <> { addNullToken(); return firstToken; } /* Catch any other (unhandled) characters. */ . { addToken(Token.IDENTIFIER); } } /* No char state */ /* No string state */ /* No multi-line comment state */ /* No documentation comment state */ { [^hwf\n]+ {} {URL} { int temp=zzStartRead; addToken(start,zzStartRead-1, Token.COMMENT_EOL); addHyperlinkToken(temp,zzMarkedPos-1, Token.COMMENT_EOL); start = zzMarkedPos; } [hwf] {} \n { addToken(start,zzStartRead-1, Token.COMMENT_EOL); addNullToken(); return firstToken; } <> { addToken(start,zzStartRead-1, Token.COMMENT_EOL); addNullToken(); return firstToken; } } ================================================ FILE: config/jflex/skeleton.default ================================================ /** This character denotes the end of file */ public static final int YYEOF = -1; /** initial size of the lookahead buffer */ --- private static final int ZZ_BUFFERSIZE = ...; /** lexical states */ --- lexical states, charmap /* error codes */ private static final int ZZ_UNKNOWN_ERROR = 0; private static final int ZZ_NO_MATCH = 1; private static final int ZZ_PUSHBACK_2BIG = 2; /* error messages for the codes above */ private static final String ZZ_ERROR_MSG[] = { "Unkown internal scanner error", "Error: could not match input", "Error: pushback value was too large" }; --- isFinal list /** the input device */ private java.io.Reader zzReader; /** the current state of the DFA */ private int zzState; /** the current lexical state */ private int zzLexicalState = YYINITIAL; /** this buffer contains the current text to be matched and is the source of the yytext() string */ private char zzBuffer[]; /** the textposition at the last accepting state */ private int zzMarkedPos; /** the textposition at the last state to be included in yytext */ private int zzPushbackPos; /** the current text position in the buffer */ private int zzCurrentPos; /** startRead marks the beginning of the yytext() string in the buffer */ private int zzStartRead; /** endRead marks the last character in the buffer, that has been read from input */ private int zzEndRead; /** number of newlines encountered up to the start of the matched text */ private int yyline; /** the number of characters up to the start of the matched text */ private int yychar; /** * the number of characters from the last newline up to the start of the * matched text */ private int yycolumn; /** * zzAtBOL == true <=> the scanner is currently at the beginning of a line */ private boolean zzAtBOL = true; /** zzAtEOF == true <=> the scanner is at the EOF */ private boolean zzAtEOF; --- user class code /** * Creates a new scanner * There is also a java.io.InputStream version of this constructor. * * @param in the java.io.Reader to read input from. */ --- constructor declaration /** * Closes the input stream. */ public final void yyclose() throws java.io.IOException { zzAtEOF = true; /* indicate end of file */ zzEndRead = zzStartRead; /* invalidate buffer */ if (zzReader != null) zzReader.close(); } /** * Enters a new lexical state * * @param newState the new lexical state */ public final void yybegin(int newState) { zzLexicalState = newState; } public final int yystate() { return zzLexicalState; } /** * Returns the text matched by the current regular expression. */ public final String yytext() { return new String( zzBuffer, zzStartRead, zzMarkedPos-zzStartRead ); } /** * Returns the character at position pos from the * matched text. * * It is equivalent to yytext().charAt(pos), but faster * * @param pos the position of the character to fetch. * A value from 0 to yylength()-1. * * @return the character at position pos */ public final char yycharat(int pos) { return zzBuffer[zzStartRead+pos]; } /** * Returns the length of the matched text region. */ public final int yylength() { return zzMarkedPos-zzStartRead; } /** * Reports an error that occured while scanning. * * In a wellformed scanner (no or only correct usage of * yypushback(int) and a match-all fallback rule) this method * will only be called with things that "Can't Possibly Happen". * If this method is called, something is seriously wrong * (e.g. a JFlex bug producing a faulty scanner etc.). * * Usual syntax/scanner level error handling should be done * in error fallback rules. * * @param errorCode the code of the errormessage to display */ --- zzScanError declaration String message; try { message = ZZ_ERROR_MSG[errorCode]; } catch (ArrayIndexOutOfBoundsException e) { message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR]; } --- throws clause } /** * Pushes the specified amount of characters back into the input stream. * * They will be read again by then next call of the scanning method * * @param number the number of characters to be read again. * This number must not be greater than yylength()! */ --- yypushback decl (contains zzScanError exception) if ( number > yylength() ) zzScanError(ZZ_PUSHBACK_2BIG); zzMarkedPos -= number; } --- zzDoEOF /** * Resumes scanning until the next regular expression is matched, * the end of input is encountered or an I/O-Error occurs. * * @return the next token * @exception java.io.IOException if any I/O-Error occurs */ --- yylex declaration int zzInput; int zzAction; // cached fields: int zzCurrentPosL; int zzMarkedPosL; int zzEndReadL = zzEndRead; char [] zzBufferL = zzBuffer; char [] zzCMapL = ZZ_CMAP; --- local declarations while (true) { zzMarkedPosL = zzMarkedPos; --- start admin (line, char, col count) zzAction = -1; zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL; --- start admin (lexstate etc) zzForAction: { while (true) { --- next input, line, col, char count, next transition, isFinal action zzAction = zzState; zzMarkedPosL = zzCurrentPosL; --- line count update } } } // store back cached position zzMarkedPos = zzMarkedPosL; --- char count update --- actions default: if (zzInput == YYEOF && zzStartRead == zzCurrentPos) { zzAtEOF = true; --- eofvalue } else { --- no match } } } } --- main } ================================================ FILE: contrib/jadx-gui.desktop ================================================ [Desktop Entry] Name=JADX GUI Comment=Dex to Java decompiler Icon=jadx Exec=jadx-gui %f Terminal=false Type=Application Categories=Development;Java; Keywords=Java;Decompiler; StartupWMClass=jadx-gui-JadxGUI ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ org.gradle.warning.mode=all org.gradle.parallel=true org.gradle.caching=true ### Disable configuration cache for now: causing issues with spotless and version plugins # org.gradle.configuration-cache=true # org.gradle.configuration-cache.problems=warn # Flags for google-java-format (optimize imports by spotless) for Java >= 16. # Java < 9 will ignore unsupported flags (thanks to -XX:+IgnoreUnrecognizedVMOptions) org.gradle.jvmargs=-XX:+IgnoreUnrecognizedVMOptions \ --add-exports='jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED' \ --add-exports='jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED' \ --add-exports='jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED' \ --add-exports='jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED' \ --add-exports='jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: jadx-cli/build.gradle.kts ================================================ plugins { id("jadx-java") id("jadx-library") id("application") // use shadow only for application scripts, jar will be copied from jadx-gui id("com.gradleup.shadow") version "8.3.8" } dependencies { implementation(project(":jadx-core")) implementation(project(":jadx-plugins-tools")) implementation(project(":jadx-commons:jadx-app-commons")) runtimeOnly(project(":jadx-plugins:jadx-dex-input")) runtimeOnly(project(":jadx-plugins:jadx-java-input")) runtimeOnly(project(":jadx-plugins:jadx-java-convert")) runtimeOnly(project(":jadx-plugins:jadx-smali-input")) runtimeOnly(project(":jadx-plugins:jadx-rename-mappings")) runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata")) runtimeOnly(project(":jadx-plugins:jadx-kotlin-source-debug-extension")) runtimeOnly(project(":jadx-plugins:jadx-xapk-input")) runtimeOnly(project(":jadx-plugins:jadx-aab-input")) runtimeOnly(project(":jadx-plugins:jadx-apkm-input")) runtimeOnly(project(":jadx-plugins:jadx-apks-input")) implementation("org.jcommander:jcommander:2.0") implementation("ch.qos.logback:logback-classic:1.5.22") implementation("com.google.code.gson:gson:2.13.2") } application { applicationName = "jadx" mainClass.set("jadx.cli.JadxCLI") applicationDefaultJvmArgs = listOf( "-XX:+IgnoreUnrecognizedVMOptions", "-Xms256M", "-XX:MaxRAMPercentage=70.0", "-XX:ParallelGCThreads=3", // disable zip checks (#1962) "-Djdk.util.zip.disableZip64ExtraFieldValidation=true", // Foreign API access for 'directories' library (Windows only) "--enable-native-access=ALL-UNNAMED", ) applicationDistribution.from("$rootDir") { include("README.md") include("NOTICE") include("LICENSE") } } tasks.shadowJar { // shadow jar not needed configurations = listOf() } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java ================================================ package jadx.cli; import java.io.PrintStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Supplier; import org.jetbrains.annotations.Nullable; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterDescription; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameterized; import jadx.api.JadxDecompiler; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.options.OptionDescription; import jadx.core.plugins.JadxPluginManager; import jadx.core.plugins.PluginContext; import jadx.core.utils.Utils; public class JCommanderWrapper { private final JCommander jc; private final JadxCLIArgs argsObj; public JCommanderWrapper(JadxCLIArgs argsObj) { JCommander.Builder builder = JCommander.newBuilder().addObject(argsObj); builder.acceptUnknownOptions(true); // workaround for "default" command JadxCLICommands.append(builder); this.jc = builder.build(); this.argsObj = argsObj; } public boolean parse(String[] args) { try { String[] fixedArgs = fixArgsForEmptySaveConfig(args); jc.parse(fixedArgs); applyFiles(argsObj); return true; } catch (ParameterException e) { System.err.println("Arguments parse error: " + e.getMessage()); return false; } } public void overrideProvided(JadxCLIArgs obj) { applyFiles(obj); for (ParameterDescription parameter : jc.getParameters()) { if (parameter.isAssigned()) { overrideProperty(obj, parameter); } } } public boolean processCommands() { String parsedCommand = jc.getParsedCommand(); if (parsedCommand == null) { return false; } return JadxCLICommands.process(this, jc, parsedCommand); } /** * The main parameter parsing doesn't work if accepting unknown options */ private void applyFiles(JadxCLIArgs argsObj) { argsObj.setFiles(jc.getUnknownOptions()); } /** * Override assigned field value to obj */ private static void overrideProperty(JadxCLIArgs obj, ParameterDescription parameter) { Parameterized parameterized = parameter.getParameterized(); Object providedValue = parameterized.get(parameter.getObject()); Object newValue = mergeValues(parameterized.getType(), providedValue, () -> parameterized.get(obj)); parameterized.set(obj, newValue); } @SuppressWarnings({ "rawtypes", "unchecked" }) private static Object mergeValues(Class type, Object value, Supplier prevValueProvider) { if (type.isAssignableFrom(Map.class)) { // merge maps instead replacing whole map Map prevMap = (Map) prevValueProvider.get(); return Utils.mergeMaps(prevMap, (Map) value); // value map will override keys in prevMap } // simple override return value; } /** * Workaround to allow empty value (i.e. zero arity) for '--save-config' option * Insert empty string arg if another option start right after this one, or it is a last one. */ private String[] fixArgsForEmptySaveConfig(String[] args) { int len = args.length; for (int i = 0; i < len; i++) { String arg = args[i]; if (arg.equals("--save-config")) { int next = i + 1; if (next == len) { return insertEmptyArg(args, next, true); } if (next < len) { String nextArg = args[next]; if (nextArg.startsWith("-")) { return insertEmptyArg(args, next, false); } } break; } } return args; } private static String[] insertEmptyArg(String[] args, int i, boolean add) { List strings = new ArrayList<>(Arrays.asList(args)); if (add) { strings.add(""); } else { strings.add(i, ""); } return strings.toArray(new String[0]); } public void printUsage() { LogHelper.setLogLevel(LogHelper.LogLevelEnum.ERROR); // mute logger while printing help // print usage in not sorted fields order (by default sorted by description) PrintStream out = System.out; out.println(); out.println("jadx - dex to java decompiler, version: " + JadxDecompiler.getVersion()); out.println(); out.println("usage: jadx [command] [options] " + jc.getMainParameterDescription()); out.println("commands (use ' --help' for command options):"); for (String command : jc.getCommands().keySet()) { out.println(" " + command + "\t - " + jc.getUsageFormatter().getCommandDescription(command)); } out.println(); int maxNamesLen = printOptions(jc, out, true); out.println(appendPluginOptions(maxNamesLen)); out.println(); out.println("Environment variables:"); out.println(" JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files"); out.println(" JADX_DISABLE_ZIP_SECURITY - set to 'true' to disable all security checks for zip files"); out.println(" JADX_ZIP_MAX_ENTRIES_COUNT - maximum allowed number of entries in zip files (default: 100 000)"); out.println(" JADX_CONFIG_DIR - custom config directory, using system by default"); out.println(" JADX_CACHE_DIR - custom cache directory, using system by default"); out.println(" JADX_TMP_DIR - custom temp directory, using system by default"); out.println(); out.println("Examples:"); out.println(" jadx -d out classes.dex"); out.println(" jadx --rename-flags \"none\" classes.dex"); out.println(" jadx --rename-flags \"valid, printable\" classes.dex"); out.println(" jadx --log-level ERROR app.apk"); out.println(" jadx -Pdex-input.verify-checksum=no app.apk"); } public void printUsage(JCommander subCommander) { PrintStream out = System.out; out.println("usage: " + subCommander.getProgramName() + " [options]"); printOptions(subCommander, out, false); } private static int printOptions(JCommander jc, PrintStream out, boolean addDefaults) { out.println("options:"); List params = jc.getParameters(); Map paramsMap = new HashMap<>(params.size()); int maxNamesLen = 0; for (ParameterDescription p : params) { paramsMap.put(p.getParameterized().getName(), p); int len = p.getNames().length(); String valueDesc = getValueDesc(p); if (valueDesc != null) { len += 1 + valueDesc.length(); } maxNamesLen = Math.max(maxNamesLen, len); } maxNamesLen += 3; Object args = jc.getObjects().get(0); for (Field f : getFields(args.getClass())) { String name = f.getName(); ParameterDescription p = paramsMap.get(name); if (p == null || p.getParameter().hidden()) { continue; } StringBuilder opt = new StringBuilder(); opt.append(" ").append(p.getNames()); String valueDesc = getValueDesc(p); if (valueDesc != null) { opt.append(' ').append(valueDesc); } addSpaces(opt, maxNamesLen - opt.length()); String description = p.getDescription(); if (description.contains("\n")) { String[] lines = description.split("\n"); opt.append("- ").append(lines[0]); for (int i = 1; i < lines.length; i++) { opt.append('\n'); addSpaces(opt, maxNamesLen + 2); opt.append(lines[i]); } } else { opt.append("- ").append(description); } if (addDefaults) { String defaultValue = getDefaultValue(args, f); if (defaultValue != null && !defaultValue.isEmpty() && !description.contains("(default)")) { opt.append(", default: ").append(defaultValue); } } out.println(opt); } return maxNamesLen; } private static @Nullable String getValueDesc(ParameterDescription p) { Parameter parameterAnnotation = p.getParameterAnnotation(); return parameterAnnotation == null ? null : parameterAnnotation.defaultValueDescription(); } /** * Get all declared fields of the specified class and all super classes */ private static List getFields(Class clazz) { List fieldList = new ArrayList<>(); while (clazz != null) { fieldList.addAll(Arrays.asList(clazz.getDeclaredFields())); clazz = clazz.getSuperclass(); } return fieldList; } @Nullable private static String getDefaultValue(Object args, Field f) { try { Class fieldType = f.getType(); if (fieldType == int.class) { return Integer.toString(f.getInt(args)); } if (fieldType == String.class) { return (String) f.get(args); } if (Enum.class.isAssignableFrom(fieldType)) { Enum val = (Enum) f.get(args); if (val != null) { return val.name().toLowerCase(Locale.ROOT); } } } catch (Exception e) { // ignore } return null; } private static void addSpaces(StringBuilder str, int count) { for (int i = 0; i < count; i++) { str.append(' '); } } private String appendPluginOptions(int maxNamesLen) { StringBuilder sb = new StringBuilder(); // load and init all options plugins to print all options try (JadxDecompiler decompiler = new JadxDecompiler(argsObj.toJadxArgs())) { JadxPluginManager pluginManager = decompiler.getPluginManager(); pluginManager.load(decompiler.getArgs().getPluginLoader()); pluginManager.initAll(); try { for (PluginContext context : pluginManager.getAllPluginContexts()) { JadxPluginOptions options = context.getOptions(); if (options != null) { appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen); } } } finally { pluginManager.unloadAll(); } } if (sb.length() == 0) { return ""; } return "\nPlugin options (-P=):" + sb; } private boolean appendPlugin(JadxPluginInfo pluginInfo, JadxPluginOptions options, StringBuilder out, int maxNamesLen) { List descs = options.getOptionsDescriptions(); if (descs.isEmpty()) { return false; } out.append("\n "); out.append(pluginInfo.getPluginId()).append(": ").append(pluginInfo.getDescription()); for (OptionDescription desc : descs) { StringBuilder opt = new StringBuilder(); opt.append(" - ").append(desc.name()); addSpaces(opt, maxNamesLen - opt.length()); opt.append("- ").append(desc.description()); if (!desc.values().isEmpty()) { opt.append(", values: ").append(desc.values()); } if (desc.defaultValue() != null) { opt.append(", default: ").append(desc.defaultValue()); } out.append("\n").append(opt); } return true; } } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/JadxAppCommon.java ================================================ package jadx.cli; import java.util.Set; import jadx.api.JadxArgs; import jadx.api.security.JadxSecurityFlag; import jadx.api.security.impl.JadxSecurity; import jadx.commons.app.JadxCommonEnv; import jadx.zip.security.DisabledZipSecurity; import jadx.zip.security.IJadxZipSecurity; import jadx.zip.security.JadxZipSecurity; public class JadxAppCommon { public static void applyEnvVars(JadxArgs jadxArgs) { Set flags = JadxSecurityFlag.all(); IJadxZipSecurity zipSecurity; boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false); if (disableXmlSecurity) { flags.remove(JadxSecurityFlag.SECURE_XML_PARSER); // TODO: not related to 'xml security', but kept for compatibility flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE); } boolean disableZipSecurity = JadxCommonEnv.getBool("JADX_DISABLE_ZIP_SECURITY", false); if (disableZipSecurity) { flags.remove(JadxSecurityFlag.SECURE_ZIP_READER); zipSecurity = DisabledZipSecurity.INSTANCE; } else { JadxZipSecurity jadxZipSecurity = new JadxZipSecurity(); int maxZipEntriesCount = JadxCommonEnv.getInt("JADX_ZIP_MAX_ENTRIES_COUNT", -2); if (maxZipEntriesCount != -2) { jadxZipSecurity.setMaxEntriesCount(maxZipEntriesCount); } int zipBombMinUncompressedSize = JadxCommonEnv.getInt("JADX_ZIP_BOMB_MIN_UNCOMPRESSED_SIZE", -2); if (zipBombMinUncompressedSize != -2) { jadxZipSecurity.setZipBombMinUncompressedSize(zipBombMinUncompressedSize); } int setZipBombDetectionFactor = JadxCommonEnv.getInt("JADX_ZIP_BOMB_DETECTION_FACTOR", -2); if (setZipBombDetectionFactor != -2) { jadxZipSecurity.setZipBombDetectionFactor(setZipBombDetectionFactor); } zipSecurity = jadxZipSecurity; } jadxArgs.setSecurity(new JadxSecurity(flags, zipSecurity)); } } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/JadxCLI.java ================================================ package jadx.cli; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.NoOpCodeCache; import jadx.api.impl.SimpleCodeWriter; import jadx.api.usage.impl.EmptyUsageInfoCache; import jadx.cli.LogHelper.LogLevelEnum; import jadx.cli.config.JadxConfigAdapter; import jadx.cli.plugins.JadxFilesGetter; import jadx.core.utils.exceptions.JadxArgsValidateException; import jadx.plugins.tools.JadxExternalPluginsLoader; public class JadxCLI { private static final Logger LOG = LoggerFactory.getLogger(JadxCLI.class); public static void main(String[] args) { int result = 1; try { result = execute(args); } finally { System.exit(result); } } public static int execute(String[] args) { return execute(args, null); } public static int execute(String[] args, @Nullable Consumer argsMod) { try { JadxCLIArgs cliArgs = JadxCLIArgs.processArgs(args, new JadxCLIArgs(), new JadxConfigAdapter<>(JadxCLIArgs.class, "cli")); if (cliArgs == null) { return 0; } JadxArgs jadxArgs = buildArgs(cliArgs); if (argsMod != null) { argsMod.accept(jadxArgs); } return runSave(jadxArgs, cliArgs); } catch (JadxArgsValidateException e) { LOG.error("Incorrect arguments: {}", e.getMessage()); return 1; } catch (Throwable e) { LOG.error("Process error:", e); return 1; } } private static JadxArgs buildArgs(JadxCLIArgs cliArgs) { JadxArgs jadxArgs = cliArgs.toJadxArgs(); jadxArgs.setCodeCache(new NoOpCodeCache()); jadxArgs.setUsageInfoCache(new EmptyUsageInfoCache()); jadxArgs.setPluginLoader(new JadxExternalPluginsLoader()); jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE); initCodeWriterProvider(jadxArgs); JadxAppCommon.applyEnvVars(jadxArgs); return jadxArgs; } private static int runSave(JadxArgs jadxArgs, JadxCLIArgs cliArgs) { try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) { jadx.load(); if (checkForErrors(jadx)) { return 2; } if (!SingleClassMode.process(jadx, cliArgs)) { save(jadx); } int errorsCount = jadx.getErrorsCount(); if (errorsCount != 0) { jadx.printErrorsReport(); LOG.error("finished with errors, count: {}", errorsCount); return 3; } LOG.info("done"); return 0; } } private static void initCodeWriterProvider(JadxArgs jadxArgs) { switch (jadxArgs.getOutputFormat()) { case JAVA: jadxArgs.setCodeWriterProvider(SimpleCodeWriter::new); break; case JSON: // needed for code offsets and source lines jadxArgs.setCodeWriterProvider(AnnotatedCodeWriter::new); break; } } private static boolean checkForErrors(JadxDecompiler jadx) { if (jadx.getRoot().getClasses().isEmpty()) { if (jadx.getArgs().isSkipResources()) { LOG.error("Load failed! No classes for decompile!"); return true; } if (!jadx.getArgs().isSkipSources()) { LOG.warn("No classes to decompile; decoding resources only"); jadx.getArgs().setSkipSources(true); } } int errorsCount = jadx.getErrorsCount(); if (errorsCount > 0) { LOG.error("Loading finished with errors! Count: {}", errorsCount); // continue processing } return false; } private static void save(JadxDecompiler jadx) { if (LogHelper.getLogLevel() == LogLevelEnum.QUIET) { jadx.save(); } else { LOG.info("processing ..."); jadx.save(500, (done, total) -> { int progress = (int) (done * 100.0 / total); System.out.printf("INFO - progress: %d of %d (%d%%)\r", done, total, progress); }); // dumb line clear :) System.out.print(" \r"); } } } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java ================================================ package jadx.cli; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.beust.jcommander.DynamicParameter; import com.beust.jcommander.IStringConverter; import com.beust.jcommander.Parameter; import jadx.api.CommentsLevel; import jadx.api.DecompilationMode; import jadx.api.JadxArgs; import jadx.api.JadxArgs.RenameEnum; import jadx.api.JadxArgs.UseKotlinMethodsForVarNames; import jadx.api.JadxDecompiler; import jadx.api.args.GeneratedRenamesMappingFileMode; import jadx.api.args.IntegerFormat; import jadx.api.args.ResourceNameSource; import jadx.api.args.UseSourceNameAsClassNameAlias; import jadx.api.args.UserRenamesMappingsMode; import jadx.cli.config.IJadxConfig; import jadx.cli.config.JadxConfigAdapter; import jadx.cli.config.JadxConfigExclude; import jadx.commons.app.JadxCommonFiles; import jadx.commons.app.JadxTempFiles; import jadx.core.deobf.conditions.DeobfWhitelist; import jadx.core.export.ExportGradleType; import jadx.core.utils.exceptions.JadxArgsValidateException; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; public class JadxCLIArgs implements IJadxConfig { private static final Logger LOG = LoggerFactory.getLogger(JadxCLIArgs.class); @JadxConfigExclude @Parameter(description = " (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk, .apkm, .jadx.kts)") protected List files = Collections.emptyList(); @JadxConfigExclude @Parameter(names = { "-d", "--output-dir" }, description = "output directory") protected String outDir; @JadxConfigExclude @Parameter(names = { "-ds", "--output-dir-src" }, description = "output directory for sources") protected String outDirSrc; @JadxConfigExclude @Parameter(names = { "-dr", "--output-dir-res" }, description = "output directory for resources") protected String outDirRes; @Parameter(names = { "-r", "--no-res" }, description = "do not decode resources") protected boolean skipResources = false; @Parameter(names = { "-s", "--no-src" }, description = "do not decompile source code") protected boolean skipSources = false; @Parameter(names = { "-j", "--threads-count" }, description = "processing threads count") protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT; @JadxConfigExclude @Parameter(names = { "--single-class" }, description = "decompile a single class, full name, raw or alias") protected String singleClass = null; @JadxConfigExclude @Parameter(names = { "--single-class-output" }, description = "file or dir for write if decompile a single class") protected String singleClassOutput = null; @Parameter(names = { "--output-format" }, description = "can be 'java' or 'json'") protected String outputFormat = "java"; @Parameter(names = { "-e", "--export-gradle" }, description = "save as gradle project (set '--export-gradle-type' to 'auto')") protected boolean exportAsGradleProject = false; @Parameter( names = { "--export-gradle-type" }, description = "Gradle project template for export:" + "\n 'auto' - detect automatically" + "\n 'android-app' - Android Application (apk)" + "\n 'android-library' - Android Library (aar)" + "\n 'simple-java' - simple Java", converter = ExportGradleTypeConverter.class ) protected @Nullable ExportGradleType exportGradleType = null; @Parameter( names = { "-m", "--decompilation-mode" }, description = "code output mode:" + "\n 'auto' - trying best options (default)" + "\n 'restructure' - restore code structure (normal java code)" + "\n 'simple' - simplified instructions (linear, with goto's)" + "\n 'fallback' - raw instructions without modifications", converter = DecompilationModeConverter.class ) protected DecompilationMode decompilationMode = DecompilationMode.AUTO; @Parameter(names = { "--show-bad-code" }, description = "show inconsistent code (incorrectly decompiled)") protected boolean showInconsistentCode = false; @Parameter(names = { "--no-xml-pretty-print" }, description = "do not prettify XML") protected boolean skipXmlPrettyPrint = false; @Parameter(names = { "--no-imports" }, description = "disable use of imports, always write entire package name") protected boolean useImports = true; @Parameter(names = { "--no-debug-info" }, description = "disable debug info parsing and processing") protected boolean debugInfo = true; @Parameter(names = { "--add-debug-lines" }, description = "add comments with debug line numbers if available") protected boolean addDebugLines = false; @Parameter(names = { "--no-inline-anonymous" }, description = "disable anonymous classes inline") protected boolean inlineAnonymousClasses = true; @Parameter(names = { "--no-inline-methods" }, description = "disable methods inline") protected boolean inlineMethods = true; @Parameter(names = { "--no-move-inner-classes" }, description = "disable move inner classes into parent") protected boolean moveInnerClasses = true; @Parameter(names = { "--no-inline-kotlin-lambda" }, description = "disable inline for Kotlin lambdas") protected boolean allowInlineKotlinLambda = true; @Parameter(names = "--no-finally", description = "don't extract finally block") protected boolean extractFinally = true; @Parameter(names = "--no-restore-switch-over-string", description = "don't restore switch over string") protected boolean restoreSwitchOverString = true; @Parameter(names = "--no-replace-consts", description = "don't replace constant value with matching constant field") protected boolean replaceConsts = true; @Parameter(names = { "--escape-unicode" }, description = "escape non latin characters in strings (with \\u)") protected boolean escapeUnicode = false; @Parameter(names = { "--respect-bytecode-access-modifiers" }, description = "don't change original access modifiers") protected boolean respectBytecodeAccessModifiers = false; @Parameter( names = { "--mappings-path" }, description = "deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory" ) protected Path userRenamesMappingsPath; @Parameter( names = { "--mappings-mode" }, description = "set mode for handling the deobfuscation mapping file:" + "\n 'read' - just read, user can always save manually (default)" + "\n 'read-and-autosave-every-change' - read and autosave after every change" + "\n 'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project" + "\n 'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file)" ) protected UserRenamesMappingsMode userRenamesMappingsMode = UserRenamesMappingsMode.getDefault(); @Parameter(names = { "--deobf" }, description = "activate deobfuscation") protected boolean deobfuscationOn = false; @Parameter(names = { "--deobf-min" }, description = "min length of name, renamed if shorter") protected int deobfuscationMinLength = 3; @Parameter(names = { "--deobf-max" }, description = "max length of name, renamed if longer") protected int deobfuscationMaxLength = 64; @Parameter( names = { "--deobf-whitelist" }, description = "space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation" ) protected String deobfuscationWhitelistStr = DeobfWhitelist.DEFAULT_STR; @JadxConfigExclude @Parameter( names = { "--deobf-cfg-file" }, description = "deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format)," + " default: same dir and name as input file with '.jobf' extension" ) protected String generatedRenamesMappingFile; @Parameter( names = { "--deobf-cfg-file-mode" }, description = "set mode for handling the JADX auto-generated names' deobfuscation map file:" + "\n 'read' - read if found, don't save (default)" + "\n 'read-or-save' - read if found, save otherwise (don't overwrite)" + "\n 'overwrite' - don't read, always save" + "\n 'ignore' - don't read and don't save", converter = DeobfuscationMapFileModeConverter.class ) protected GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.getDefault(); @SuppressWarnings("DeprecatedIsStillUsed") @Parameter( names = { "--deobf-use-sourcename" }, description = "use source file name as class name alias." + "\nDEPRECATED, use \"--use-source-name-as-class-name-alias\" instead", hidden = true ) @Deprecated protected Boolean deobfuscationUseSourceNameAsAlias = null; @Parameter( names = { "--deobf-res-name-source" }, description = "better name source for resources:" + "\n 'auto' - automatically select best name (default)" + "\n 'resources' - use resources names" + "\n 'code' - use R class fields names", converter = ResourceNameSourceConverter.class ) protected ResourceNameSource resourceNameSource = ResourceNameSource.AUTO; @Parameter( names = { "--use-source-name-as-class-name-alias" }, description = "use source name as class name alias:" + "\n 'always' - always use source name if it's available" + "\n 'if-better' - use source name if it seems better than the current one" + "\n 'never' - never use source name, even if it's available", converter = UseSourceNameAsClassNameConverter.class ) protected UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = null; @Parameter( names = { "--source-name-repeat-limit" }, description = "allow using source name if it appears less than a limit number" ) protected int sourceNameRepeatLimit = 10; @Parameter( names = { "--use-kotlin-methods-for-var-names" }, description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide", converter = UseKotlinMethodsForVarNamesConverter.class ) protected UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY; @Parameter( names = { "--use-headers-for-detect-resource-extensions" }, description = "Use headers for detect resource extensions if resource obfuscated" ) protected boolean useHeadersForDetectResourceExtensions = false; @Parameter( names = { "--rename-flags" }, description = "fix options (comma-separated list of):" + "\n 'case' - fix case sensitivity issues (according to --fs-case-sensitive option)," + "\n 'valid' - rename java identifiers to make them valid," + "\n 'printable' - remove non-printable chars from identifiers," + "\nor single 'none' - to disable all renames" + "\nor single 'all' - to enable all (default)", listConverter = RenameConverter.class ) protected Set renameFlags = EnumSet.allOf(RenameEnum.class); @Parameter( names = { "--integer-format" }, description = "how integers are displayed:" + "\n 'auto' - automatically select (default)" + "\n 'decimal' - use decimal" + "\n 'hexadecimal' - use hexadecimal", converter = IntegerFormatConverter.class ) protected IntegerFormat integerFormat = IntegerFormat.AUTO; @Parameter(names = { "--type-update-limit" }, description = "type update limit count (per one instruction)") protected int typeUpdatesLimitCount = 10; @Parameter(names = { "--fs-case-sensitive" }, description = "treat filesystem as case sensitive, false by default") protected boolean fsCaseSensitive = false; @Parameter(names = { "--cfg" }, description = "save methods control flow graph to dot file") protected boolean cfgOutput = false; @Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)") protected boolean rawCfgOutput = false; @Parameter(names = { "-f", "--fallback" }, description = "set '--decompilation-mode' to 'fallback' (deprecated)") protected boolean fallbackMode = false; @Parameter(names = { "--use-dx" }, description = "use dx/d8 to convert java bytecode") protected boolean useDx = false; @Parameter( names = { "--comments-level" }, description = "set code comments level, values: error, warn, info, debug, user-only, none", converter = CommentsLevelConverter.class ) protected CommentsLevel commentsLevel = CommentsLevel.INFO; @Parameter( names = { "--log-level" }, description = "set log level, values: quiet, progress, error, warn, info, debug", converter = LogLevelConverter.class ) protected LogHelper.LogLevelEnum logLevel = LogHelper.LogLevelEnum.PROGRESS; @JadxConfigExclude @Parameter(names = { "-v", "--verbose" }, description = "verbose output (set --log-level to DEBUG)") protected boolean verbose = false; @JadxConfigExclude @Parameter(names = { "-q", "--quiet" }, description = "turn off output (set --log-level to QUIET)") protected boolean quiet = false; @JadxConfigExclude @Parameter(names = { "--disable-plugins" }, description = "comma separated list of plugin ids to disable") protected String disablePlugins = ""; @JadxConfigExclude @Parameter( names = { "--config" }, defaultValueDescription = "", description = "load configuration from file, can be:" + "\n path to '.json' file" + "\n short name - uses file with this name from config directory" + "\n 'none' - to disable config loading" ) protected String config = ""; @JadxConfigExclude @Parameter( names = { "--save-config" }, defaultValueDescription = "", description = "save current options into configuration file and exit, can be:" + "\n empty - for default config" + "\n path to '.json' file" + "\n short name - file will be saved in config directory" ) protected String saveConfig = null; @JadxConfigExclude @Parameter(names = { "--print-files" }, description = "print files and directories used by jadx (config, cache, temp)") protected boolean printFiles = false; @JadxConfigExclude @Parameter(names = { "--version" }, description = "print jadx version") protected boolean printVersion = false; @JadxConfigExclude @Parameter(names = { "-h", "--help" }, description = "print this help", help = true) protected boolean printHelp = false; @DynamicParameter(names = "-P", description = "Plugin options", hidden = true) protected Map pluginOptions = new HashMap<>(); /** * Obsolete method without config support, * prefer {@link #processArgs(String[], JadxCLIArgs, JadxConfigAdapter)} */ public boolean processArgs(String[] args) { return processArgs(args, this, null) != null; } public static @Nullable T processArgs( String[] args, T argsObj, @Nullable JadxConfigAdapter configAdapter) { JCommanderWrapper jcw = new JCommanderWrapper(argsObj); if (!jcw.parse(args)) { return null; } applyArgs(argsObj); // process commands and early exit flags if (!argsObj.process(jcw)) { return null; } if (configAdapter != null) { if (argsObj.printFiles) { printFilesAndDirs(configAdapter.getDefaultConfigFileName()); return null; } if (!argsObj.config.equalsIgnoreCase("none")) { // load config file and merge with command line args try { configAdapter.useConfigRef(argsObj.config); T configObj = configAdapter.load(); if (configObj != null) { jcw.overrideProvided(configObj); argsObj = configObj; } } catch (Exception e) { LOG.error("Config load failed, continue with default values", e); } } } // verify result object argsObj.verify(); applyArgs(argsObj); // save config if requested if (argsObj.saveConfig != null) { saveConfig(argsObj, configAdapter); return null; } return argsObj; } private static void applyArgs(T argsObj) { // apply log levels LogHelper.initLogLevel(argsObj); LogHelper.applyLogLevels(); } public boolean process(JCommanderWrapper jcw) { if (jcw.processCommands()) { return false; } if (printHelp) { jcw.printUsage(); return false; } if (printVersion) { System.out.println(JadxDecompiler.getVersion()); return false; } // unknown options added to 'files', run checks for (String fileName : files) { if (fileName.startsWith("-")) { throw new JadxArgsValidateException("Unknown option: " + fileName); } } return true; } private static void printFilesAndDirs(String defaultConfigFileName) { System.out.println("Files and directories used by jadx:"); System.out.println(" - default config file: " + JadxCommonFiles.getConfigDir().resolve(defaultConfigFileName).toAbsolutePath()); System.out.println(" - config directory: " + JadxCommonFiles.getConfigDir().toAbsolutePath()); System.out.println(" - cache directory: " + JadxCommonFiles.getCacheDir().toAbsolutePath()); System.out.println(" - temp directory: " + JadxTempFiles.getTempRootDir().getParent().toAbsolutePath()); } public void verify() { if (threadsCount <= 0) { throw new JadxArgsValidateException("Threads count must be positive, got: " + threadsCount); } } private static void saveConfig(T argsObj, @Nullable JadxConfigAdapter configAdapter) { if (configAdapter == null) { throw new JadxRuntimeException("Config adapter set to null, can't save config"); } configAdapter.useConfigRef(argsObj.saveConfig); configAdapter.save(argsObj); System.out.println("Config saved to " + configAdapter.getConfigPath().toAbsolutePath()); } public JadxArgs toJadxArgs() { JadxArgs args = new JadxArgs(); args.setInputFiles(files.stream().map(FileUtils::toFile).collect(Collectors.toList())); args.setOutDir(FileUtils.toFile(outDir)); args.setOutDirSrc(FileUtils.toFile(outDirSrc)); args.setOutDirRes(FileUtils.toFile(outDirRes)); args.setOutputFormat(JadxArgs.OutputFormatEnum.valueOf(outputFormat.toUpperCase())); args.setThreadsCount(threadsCount); args.setSkipSources(skipSources); args.setSkipResources(skipResources); if (fallbackMode) { args.setDecompilationMode(DecompilationMode.FALLBACK); } else { args.setDecompilationMode(decompilationMode); } args.setShowInconsistentCode(showInconsistentCode); args.setCfgOutput(cfgOutput); args.setRawCFGOutput(rawCfgOutput); args.setReplaceConsts(replaceConsts); if (userRenamesMappingsPath != null) { args.setUserRenamesMappingsPath(userRenamesMappingsPath); } args.setUserRenamesMappingsMode(userRenamesMappingsMode); args.setDeobfuscationOn(deobfuscationOn); args.setGeneratedRenamesMappingFile(FileUtils.toFile(generatedRenamesMappingFile)); args.setGeneratedRenamesMappingFileMode(generatedRenamesMappingFileMode); args.setDeobfuscationMinLength(deobfuscationMinLength); args.setDeobfuscationMaxLength(deobfuscationMaxLength); args.setDeobfuscationWhitelist(Arrays.asList(deobfuscationWhitelistStr.split(" "))); args.setUseSourceNameAsClassNameAlias(getUseSourceNameAsClassNameAlias()); args.setUseHeadersForDetectResourceExtensions(useHeadersForDetectResourceExtensions); args.setSourceNameRepeatLimit(sourceNameRepeatLimit); args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames); args.setResourceNameSource(resourceNameSource); args.setEscapeUnicode(escapeUnicode); args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers); args.setExportGradleType(exportGradleType); if (exportAsGradleProject && exportGradleType == null) { args.setExportGradleType(ExportGradleType.AUTO); } args.setSkipXmlPrettyPrint(skipXmlPrettyPrint); args.setUseImports(useImports); args.setDebugInfo(debugInfo); args.setInsertDebugLines(addDebugLines); args.setInlineAnonymousClasses(inlineAnonymousClasses); args.setInlineMethods(inlineMethods); args.setMoveInnerClasses(moveInnerClasses); args.setAllowInlineKotlinLambda(allowInlineKotlinLambda); args.setExtractFinally(extractFinally); args.setRestoreSwitchOverString(restoreSwitchOverString); args.setRenameFlags(buildEnumSetForRenameFlags()); args.setFsCaseSensitive(fsCaseSensitive); args.setCommentsLevel(commentsLevel); args.setIntegerFormat(integerFormat); args.setTypeUpdatesLimitCount(typeUpdatesLimitCount); args.setUseDxInput(useDx); args.setPluginOptions(pluginOptions); args.setDisabledPlugins(Arrays.stream(disablePlugins.split(",")).map(String::trim).collect(Collectors.toSet())); return args; } private EnumSet buildEnumSetForRenameFlags() { EnumSet set = EnumSet.noneOf(RenameEnum.class); set.addAll(renameFlags); return set; } public List getFiles() { return files; } public void setFiles(List files) { this.files = files; } public String getOutDir() { return outDir; } public String getOutDirSrc() { return outDirSrc; } public String getOutDirRes() { return outDirRes; } public String getSingleClass() { return singleClass; } public String getSingleClassOutput() { return singleClassOutput; } public boolean isSkipResources() { return skipResources; } public void setSkipResources(boolean skipResources) { this.skipResources = skipResources; } public boolean isSkipSources() { return skipSources; } public void setSkipSources(boolean skipSources) { this.skipSources = skipSources; } public int getThreadsCount() { return threadsCount; } public void setThreadsCount(int threadsCount) { this.threadsCount = threadsCount; } public boolean isFallbackMode() { return fallbackMode; } public boolean isUseDx() { return useDx; } public void setUseDx(boolean useDx) { this.useDx = useDx; } public DecompilationMode getDecompilationMode() { return decompilationMode; } public void setDecompilationMode(DecompilationMode decompilationMode) { this.decompilationMode = decompilationMode; } public boolean isShowInconsistentCode() { return showInconsistentCode; } public void setShowInconsistentCode(boolean showInconsistentCode) { this.showInconsistentCode = showInconsistentCode; } public boolean isUseImports() { return useImports; } public void setUseImports(boolean useImports) { this.useImports = useImports; } public boolean isDebugInfo() { return debugInfo; } public void setDebugInfo(boolean debugInfo) { this.debugInfo = debugInfo; } public boolean isAddDebugLines() { return addDebugLines; } public void setAddDebugLines(boolean addDebugLines) { this.addDebugLines = addDebugLines; } public boolean isInlineAnonymousClasses() { return inlineAnonymousClasses; } public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) { this.inlineAnonymousClasses = inlineAnonymousClasses; } public boolean isInlineMethods() { return inlineMethods; } public void setInlineMethods(boolean inlineMethods) { this.inlineMethods = inlineMethods; } public boolean isMoveInnerClasses() { return moveInnerClasses; } public void setMoveInnerClasses(boolean moveInnerClasses) { this.moveInnerClasses = moveInnerClasses; } public boolean isAllowInlineKotlinLambda() { return allowInlineKotlinLambda; } public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) { this.allowInlineKotlinLambda = allowInlineKotlinLambda; } public boolean isExtractFinally() { return extractFinally; } public void setExtractFinally(boolean extractFinally) { this.extractFinally = extractFinally; } public boolean isRestoreSwitchOverString() { return restoreSwitchOverString; } public void setRestoreSwitchOverString(boolean restoreSwitchOverString) { this.restoreSwitchOverString = restoreSwitchOverString; } public Path getUserRenamesMappingsPath() { return userRenamesMappingsPath; } public void setUserRenamesMappingsPath(Path userRenamesMappingsPath) { this.userRenamesMappingsPath = userRenamesMappingsPath; } public UserRenamesMappingsMode getUserRenamesMappingsMode() { return userRenamesMappingsMode; } public void setUserRenamesMappingsMode(UserRenamesMappingsMode userRenamesMappingsMode) { this.userRenamesMappingsMode = userRenamesMappingsMode; } public boolean isDeobfuscationOn() { return deobfuscationOn; } public void setDeobfuscationOn(boolean deobfuscationOn) { this.deobfuscationOn = deobfuscationOn; } public int getDeobfuscationMinLength() { return deobfuscationMinLength; } public void setDeobfuscationMinLength(int deobfuscationMinLength) { this.deobfuscationMinLength = deobfuscationMinLength; } public int getDeobfuscationMaxLength() { return deobfuscationMaxLength; } public void setDeobfuscationMaxLength(int deobfuscationMaxLength) { this.deobfuscationMaxLength = deobfuscationMaxLength; } public String getDeobfuscationWhitelistStr() { return deobfuscationWhitelistStr; } public void setDeobfuscationWhitelistStr(String deobfuscationWhitelistStr) { this.deobfuscationWhitelistStr = deobfuscationWhitelistStr; } public String getGeneratedRenamesMappingFile() { return generatedRenamesMappingFile; } public void setGeneratedRenamesMappingFile(String generatedRenamesMappingFile) { this.generatedRenamesMappingFile = generatedRenamesMappingFile; } public GeneratedRenamesMappingFileMode getGeneratedRenamesMappingFileMode() { return generatedRenamesMappingFileMode; } public void setGeneratedRenamesMappingFileMode(GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode) { this.generatedRenamesMappingFileMode = generatedRenamesMappingFileMode; } public int getSourceNameRepeatLimit() { return sourceNameRepeatLimit; } public void setSourceNameRepeatLimit(int sourceNameRepeatLimit) { this.sourceNameRepeatLimit = sourceNameRepeatLimit; } public UseSourceNameAsClassNameAlias getUseSourceNameAsClassNameAlias() { if (useSourceNameAsClassNameAlias != null) { return useSourceNameAsClassNameAlias; } else if (deobfuscationUseSourceNameAsAlias != null) { // noinspection deprecation return UseSourceNameAsClassNameAlias.create(deobfuscationUseSourceNameAsAlias); } else { return UseSourceNameAsClassNameAlias.getDefault(); } } public void setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias) { this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias; } /** * @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead. */ @Deprecated public boolean isDeobfuscationUseSourceNameAsAlias() { return getUseSourceNameAsClassNameAlias().toBoolean(); } public void setDeobfuscationUseSourceNameAsAlias(Boolean deobfuscationUseSourceNameAsAlias) { this.deobfuscationUseSourceNameAsAlias = deobfuscationUseSourceNameAsAlias; } public ResourceNameSource getResourceNameSource() { return resourceNameSource; } public void setResourceNameSource(ResourceNameSource resourceNameSource) { this.resourceNameSource = resourceNameSource; } public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() { return useKotlinMethodsForVarNames; } public void setUseKotlinMethodsForVarNames(UseKotlinMethodsForVarNames useKotlinMethodsForVarNames) { this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames; } public IntegerFormat getIntegerFormat() { return integerFormat; } public void setIntegerFormat(IntegerFormat integerFormat) { this.integerFormat = integerFormat; } public int getTypeUpdatesLimitCount() { return typeUpdatesLimitCount; } public void setTypeUpdatesLimitCount(int typeUpdatesLimitCount) { this.typeUpdatesLimitCount = typeUpdatesLimitCount; } public boolean isEscapeUnicode() { return escapeUnicode; } public void setEscapeUnicode(boolean escapeUnicode) { this.escapeUnicode = escapeUnicode; } public boolean isCfgOutput() { return cfgOutput; } public void setCfgOutput(boolean cfgOutput) { this.cfgOutput = cfgOutput; } public boolean isRawCfgOutput() { return rawCfgOutput; } public void setRawCfgOutput(boolean rawCfgOutput) { this.rawCfgOutput = rawCfgOutput; } public boolean isReplaceConsts() { return replaceConsts; } public void setReplaceConsts(boolean replaceConsts) { this.replaceConsts = replaceConsts; } public boolean isRespectBytecodeAccessModifiers() { return respectBytecodeAccessModifiers; } public void setRespectBytecodeAccessModifiers(boolean respectBytecodeAccessModifiers) { this.respectBytecodeAccessModifiers = respectBytecodeAccessModifiers; } public boolean isExportAsGradleProject() { return exportAsGradleProject; } public void setExportAsGradleProject(boolean exportAsGradleProject) { this.exportAsGradleProject = exportAsGradleProject; } public boolean isSkipXmlPrettyPrint() { return skipXmlPrettyPrint; } public void setSkipXmlPrettyPrint(boolean skipXmlPrettyPrint) { this.skipXmlPrettyPrint = skipXmlPrettyPrint; } public boolean isRenameCaseSensitive() { return renameFlags.contains(RenameEnum.CASE); } public boolean isRenameValid() { return renameFlags.contains(RenameEnum.VALID); } public boolean isRenamePrintable() { return renameFlags.contains(RenameEnum.PRINTABLE); } public boolean isFsCaseSensitive() { return fsCaseSensitive; } public void setFsCaseSensitive(boolean fsCaseSensitive) { this.fsCaseSensitive = fsCaseSensitive; } public boolean isUseHeadersForDetectResourceExtensions() { return useHeadersForDetectResourceExtensions; } public void setUseHeadersForDetectResourceExtensions(boolean useHeadersForDetectResourceExtensions) { this.useHeadersForDetectResourceExtensions = useHeadersForDetectResourceExtensions; } public CommentsLevel getCommentsLevel() { return commentsLevel; } public void setCommentsLevel(CommentsLevel commentsLevel) { this.commentsLevel = commentsLevel; } public LogHelper.LogLevelEnum getLogLevel() { return logLevel; } public void setLogLevel(LogHelper.LogLevelEnum logLevel) { this.logLevel = logLevel; } public Map getPluginOptions() { return pluginOptions; } public void setPluginOptions(Map pluginOptions) { this.pluginOptions = pluginOptions; } public String getDisablePlugins() { return disablePlugins; } public void setDisablePlugins(String disablePlugins) { this.disablePlugins = disablePlugins; } public void setExportGradleType(@Nullable ExportGradleType exportGradleType) { this.exportGradleType = exportGradleType; } public void setOutputFormat(String outputFormat) { this.outputFormat = outputFormat; } public Set getRenameFlags() { return renameFlags; } public void setRenameFlags(Set renameFlags) { this.renameFlags = renameFlags; } public String getConfig() { return config; } static class RenameConverter implements IStringConverter> { private final String paramName; RenameConverter(String paramName) { this.paramName = paramName; } @Override public Set convert(String value) { if (value.equalsIgnoreCase("NONE")) { return EnumSet.noneOf(RenameEnum.class); } if (value.equalsIgnoreCase("ALL")) { return EnumSet.allOf(RenameEnum.class); } Set set = EnumSet.noneOf(RenameEnum.class); for (String s : value.split(",")) { try { set.add(RenameEnum.valueOf(s.trim().toUpperCase(Locale.ROOT))); } catch (Exception e) { throw new JadxArgsValidateException( '\'' + s + "' is unknown for parameter " + paramName + ", possible values are " + enumValuesString(RenameEnum.values())); } } return set; } } public static class CommentsLevelConverter extends BaseEnumConverter { public CommentsLevelConverter() { super(CommentsLevel::valueOf, CommentsLevel::values); } } public static class UseKotlinMethodsForVarNamesConverter extends BaseEnumConverter { public UseKotlinMethodsForVarNamesConverter() { super(UseKotlinMethodsForVarNames::valueOf, UseKotlinMethodsForVarNames::values); } } public static class DeobfuscationMapFileModeConverter extends BaseEnumConverter { public DeobfuscationMapFileModeConverter() { super(GeneratedRenamesMappingFileMode::valueOf, GeneratedRenamesMappingFileMode::values); } } public static class ResourceNameSourceConverter extends BaseEnumConverter { public ResourceNameSourceConverter() { super(ResourceNameSource::valueOf, ResourceNameSource::values); } } public static class UseSourceNameAsClassNameConverter extends BaseEnumConverter { public UseSourceNameAsClassNameConverter() { super(UseSourceNameAsClassNameAlias::valueOf, UseSourceNameAsClassNameAlias::values); } } public static class DecompilationModeConverter extends BaseEnumConverter { public DecompilationModeConverter() { super(DecompilationMode::valueOf, DecompilationMode::values); } } public static class ExportGradleTypeConverter extends BaseEnumConverter { public ExportGradleTypeConverter() { super(ExportGradleType::valueOf, ExportGradleType::values); } } public static class LogLevelConverter extends BaseEnumConverter { public LogLevelConverter() { super(LogHelper.LogLevelEnum::valueOf, LogHelper.LogLevelEnum::values); } } public static class IntegerFormatConverter extends BaseEnumConverter { public IntegerFormatConverter() { super(IntegerFormat::valueOf, IntegerFormat::values); } } public abstract static class BaseEnumConverter> implements IStringConverter { private final Function parse; private final Supplier values; public BaseEnumConverter(Function parse, Supplier values) { this.parse = parse; this.values = values; } @Override public E convert(String value) { try { return parse.apply(stringAsEnumName(value)); } catch (Exception e) { throw new JadxArgsValidateException( '\'' + value + "' is unknown, possible values are: " + enumValuesString(values.get())); } } } public static String enumValuesString(Enum[] values) { return Stream.of(values) .map(v -> v.name().replace('_', '-').toLowerCase(Locale.ROOT)) .collect(Collectors.joining(", ")); } private static String stringAsEnumName(String value) { // inverse of enumValuesString conversion return value.replace('-', '_').toUpperCase(Locale.ROOT); } } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/JadxCLICommands.java ================================================ package jadx.cli; import java.util.LinkedHashMap; import java.util.Map; import com.beust.jcommander.JCommander; import jadx.cli.commands.CommandPlugins; import jadx.cli.commands.ICommand; import jadx.core.utils.exceptions.JadxArgsValidateException; public class JadxCLICommands { private static final Map COMMANDS_MAP = new LinkedHashMap<>(); static { JadxCLICommands.register(new CommandPlugins()); } public static void register(ICommand command) { COMMANDS_MAP.put(command.name(), command); } public static void append(JCommander.Builder builder) { COMMANDS_MAP.forEach(builder::addCommand); } public static boolean process(JCommanderWrapper jcw, JCommander jc, String parsedCommand) { ICommand command = COMMANDS_MAP.get(parsedCommand); if (command == null) { throw new JadxArgsValidateException("Unknown command: " + parsedCommand + ". Expected one of: " + COMMANDS_MAP.keySet()); } JCommander subCommander = jc.getCommands().get(parsedCommand); command.process(jcw, subCommander); return true; } } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/LogHelper.java ================================================ package jadx.cli; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.LoggerFactory; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import jadx.api.JadxDecompiler; public class LogHelper { private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(LogHelper.class); public enum LogLevelEnum { QUIET(Level.OFF), PROGRESS(Level.OFF), ERROR(Level.ERROR), WARN(Level.WARN), INFO(Level.INFO), DEBUG(Level.DEBUG); private final Level level; LogLevelEnum(Level level) { this.level = level; } public Level getLevel() { return level; } } @Nullable("For disable log level control") private static LogLevelEnum logLevelValue; public static void initLogLevel(JadxCLIArgs args) { logLevelValue = getLogLevelFromArgs(args); } private static LogLevelEnum getLogLevelFromArgs(JadxCLIArgs args) { if (isCustomLogConfig()) { return null; } if (args.quiet) { args.logLevel = LogLevelEnum.QUIET; } else if (args.verbose) { args.logLevel = LogLevelEnum.DEBUG; } return args.logLevel; } public static void setLogLevel(LogLevelEnum newLogLevel) { logLevelValue = newLogLevel; applyLogLevel(logLevelValue); } public static void applyLogLevels() { if (logLevelValue == null) { return; } applyLogLevel(logLevelValue); if (logLevelValue == LogLevelEnum.PROGRESS) { fixForShowProgress(); } } /** * Show progress: change to 'INFO' for control classes */ private static void fixForShowProgress() { setLevelForClass(JadxCLI.class, Level.INFO); setLevelForClass(JadxDecompiler.class, Level.INFO); setLevelForClass(SingleClassMode.class, Level.INFO); // show warnings and errors from input plugins setLevelForPackage("jadx.plugins.input", Level.WARN); } private static void applyLogLevel(@NotNull LogLevelEnum logLevel) { Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); rootLogger.setLevel(logLevel.getLevel()); } @Nullable public static LogLevelEnum getLogLevel() { return logLevelValue; } public static void setLevelForClass(Class cls, Level level) { ((Logger) LoggerFactory.getLogger(cls)).setLevel(level); } public static void setLevelForPackage(String pkgName, Level level) { ((Logger) LoggerFactory.getLogger(pkgName)).setLevel(level); } /** * Try to detect if user provide custom logback config via -Dlogback.configurationFile= */ private static boolean isCustomLogConfig() { try { String logbackConfig = System.getProperty("logback.configurationFile"); if (logbackConfig == null) { return false; } LOG.debug("Use custom log config: {}", logbackConfig); return true; } catch (Exception e) { LOG.error("Failed to detect custom log config", e); } return false; } } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/SingleClassMode.java ================================================ package jadx.cli; import java.io.File; import java.util.List; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.JadxDecompiler; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.visitors.SaveCode; import jadx.core.utils.exceptions.JadxArgsValidateException; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; public class SingleClassMode { private static final Logger LOG = LoggerFactory.getLogger(SingleClassMode.class); public static boolean process(JadxDecompiler jadx, JadxCLIArgs cliArgs) { String singleClass = cliArgs.getSingleClass(); String singleClassOutput = cliArgs.getSingleClassOutput(); if (singleClass == null && singleClassOutput == null) { return false; } ClassNode clsForProcess; if (singleClass != null) { clsForProcess = jadx.getRoot().resolveClass(singleClass); if (clsForProcess == null) { clsForProcess = jadx.getRoot().getClasses().stream() .filter(cls -> cls.getClassInfo().getAliasFullName().equals(singleClass)) .findFirst().orElse(null); } if (clsForProcess == null) { throw new JadxArgsValidateException("Input class not found: " + singleClass); } if (clsForProcess.contains(AFlag.DONT_GENERATE)) { throw new JadxArgsValidateException("Input class can't be saved by current jadx settings (marked as DONT_GENERATE)"); } if (clsForProcess.isInner()) { clsForProcess = clsForProcess.getTopParentClass(); LOG.warn("Input class is inner, parent class will be saved: {}", clsForProcess.getFullName()); } } else { // singleClassOutput is set // expect only one class to be loaded List classes = jadx.getRoot().getClasses().stream() .filter(c -> !c.isInner() && !c.contains(AFlag.DONT_GENERATE)) .collect(Collectors.toList()); int size = classes.size(); if (size == 1) { clsForProcess = classes.get(0); } else { throw new JadxArgsValidateException("Found " + size + " classes, single class output can't be used"); } } ICodeInfo codeInfo; try { codeInfo = clsForProcess.decompile(); } catch (Exception e) { throw new JadxRuntimeException("Class decompilation failed", e); } String fileExt = SaveCode.getFileExtension(jadx.getRoot()); File out; if (singleClassOutput == null) { out = new File(jadx.getArgs().getOutDirSrc(), clsForProcess.getClassInfo().getAliasFullPath() + fileExt); } else { if (singleClassOutput.endsWith(fileExt)) { // treat as file name out = new File(singleClassOutput); } else { // treat as directory out = new File(singleClassOutput, clsForProcess.getShortName() + fileExt); } } File resultOut = FileUtils.prepareFile(out); if (clsForProcess.getClassInfo().hasAlias()) { LOG.info("Saving class '{}' (alias: '{}') to file '{}'", clsForProcess.getClassInfo().getFullName(), clsForProcess.getFullName(), resultOut.getAbsolutePath()); } else { LOG.info("Saving class '{}' to file '{}'", clsForProcess.getFullName(), resultOut.getAbsolutePath()); } SaveCode.save(codeInfo.getCodeStr(), resultOut); return true; } } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/clst/ConvertToClsSet.java ================================================ package jadx.cli.clst; import java.nio.file.Path; import java.nio.file.Paths; import java.util.EnumSet; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.args.UseSourceNameAsClassNameAlias; import jadx.core.clsp.ClsSet; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.files.FileUtils; /** * Utility class for convert dex or jar to jadx classes set (.jcst) */ public class ConvertToClsSet { private static final Logger LOG = LoggerFactory.getLogger(ConvertToClsSet.class); public static void usage() { LOG.info(" "); LOG.info("Arguments to update core.jcst: " + " " + "/jadx-core/src/main/resources/clst/core.jcst " + "/platforms/android-/android.jar" + "/platforms/android-/optional/android.car.jar " + "/platforms/android-/optional/org.apache.http.legacy.jar"); } public static void main(String[] args) { if (args.length != 5) { usage(); System.exit(1); } int androidApiLevel = Integer.parseInt(args[0]); List inputPaths = Stream.of(args).skip(1).map(Paths::get).collect(Collectors.toList()); Path output = inputPaths.remove(0); JadxArgs jadxArgs = new JadxArgs(); jadxArgs.setInputFiles(FileUtils.toFiles(inputPaths)); // disable not needed passes executed at prepare stage jadxArgs.setDeobfuscationOn(false); jadxArgs.setRenameFlags(EnumSet.noneOf(JadxArgs.RenameEnum.class)); jadxArgs.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.NEVER); jadxArgs.setMoveInnerClasses(false); jadxArgs.setInlineAnonymousClasses(false); jadxArgs.setInlineMethods(false); // don't require/load class set file jadxArgs.setLoadJadxClsSetFile(false); try (JadxDecompiler decompiler = new JadxDecompiler(jadxArgs)) { decompiler.load(); RootNode root = decompiler.getRoot(); ClsSet set = new ClsSet(root); set.setAndroidApiLevel(androidApiLevel); set.loadFrom(root); set.save(output); LOG.info("Output: {}", output); LOG.info("done"); } catch (Exception e) { LOG.error("Failed with error", e); } } } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/commands/CommandPlugins.java ================================================ package jadx.cli.commands; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import jadx.api.plugins.JadxPluginInfo; import jadx.cli.JCommanderWrapper; import jadx.cli.LogHelper; import jadx.core.utils.StringUtils; import jadx.plugins.tools.JadxPluginsList; import jadx.plugins.tools.JadxPluginsTools; import jadx.plugins.tools.data.JadxPluginListEntry; import jadx.plugins.tools.data.JadxPluginMetadata; import jadx.plugins.tools.data.JadxPluginUpdate; @Parameters(commandDescription = "manage jadx plugins") public class CommandPlugins implements ICommand { @Parameter(names = { "-i", "--install" }, description = "install plugin with locationId", defaultValueDescription = "") protected String install; @Parameter(names = { "-j", "--install-jar" }, description = "install plugin from jar file", defaultValueDescription = "") protected String installJar; @Parameter(names = { "-l", "--list" }, description = "list installed plugins") protected boolean list; @Parameter(names = { "-a", "--available" }, description = "list available plugins from jadx-plugins-list (aka marketplace)") protected boolean available; @Parameter(names = { "-u", "--update" }, description = "update installed plugins") protected boolean update; @Parameter(names = { "--uninstall" }, description = "uninstall plugin with pluginId", defaultValueDescription = "") protected String uninstall; @Parameter(names = { "--disable" }, description = "disable plugin with pluginId", defaultValueDescription = "") protected String disable; @Parameter(names = { "--enable" }, description = "enable plugin with pluginId", defaultValueDescription = "") protected String enable; @Parameter(names = { "--list-all" }, description = "list all plugins including bundled and dropins") protected boolean listAll; @Parameter( names = { "--list-versions" }, description = "fetch latest versions of plugin from locationId (will download all artefacts, limited to 10)", defaultValueDescription = "" ) protected String listVersions; @Parameter(names = { "-h", "--help" }, description = "print this help", help = true) protected boolean printHelp = false; @Override public String name() { return "plugins"; } @SuppressWarnings("UnnecessaryReturnStatement") @Override public void process(JCommanderWrapper jcw, JCommander subCommander) { if (printHelp) { jcw.printUsage(subCommander); return; } Set unknownOptions = new HashSet<>(subCommander.getUnknownOptions()); boolean verbose = unknownOptions.remove("-v") || unknownOptions.remove("--verbose"); LogHelper.setLogLevel(verbose ? LogHelper.LogLevelEnum.DEBUG : LogHelper.LogLevelEnum.INFO); if (!unknownOptions.isEmpty()) { System.out.println("Error: found unknown options: " + unknownOptions); } if (install != null) { installPlugin(install); return; } if (installJar != null) { installPlugin("file:" + installJar); return; } if (uninstall != null) { boolean uninstalled = JadxPluginsTools.getInstance().uninstall(uninstall); System.out.println(uninstalled ? "Uninstalled" : "Plugin not found"); return; } if (update) { List updates = JadxPluginsTools.getInstance().updateAll(); if (updates.isEmpty()) { System.out.println("No updates"); } else { System.out.println("Installed updates: " + updates.size()); for (JadxPluginUpdate update : updates) { System.out.println(" " + update.getPluginId() + ": " + update.getOldVersion() + " -> " + update.getNewVersion()); } } return; } if (list) { printPlugins(JadxPluginsTools.getInstance().getInstalled()); return; } if (listAll) { printAllPlugins(); return; } if (listVersions != null) { printVersions(listVersions, 10); return; } if (available) { List availableList = JadxPluginsList.getInstance().get(); System.out.println("Available plugins: " + availableList.size()); for (JadxPluginListEntry plugin : availableList) { System.out.println(" - " + plugin.getName() + ": " + plugin.getDescription() + " (" + plugin.getLocationId() + ")"); } return; } if (disable != null) { if (JadxPluginsTools.getInstance().changeDisabledStatus(disable, true)) { System.out.println("Plugin '" + disable + "' disabled."); } else { System.out.println("Plugin '" + disable + "' already disabled."); } return; } if (enable != null) { if (JadxPluginsTools.getInstance().changeDisabledStatus(enable, false)) { System.out.println("Plugin '" + enable + "' enabled."); } else { System.out.println("Plugin '" + enable + "' already enabled."); } return; } } private static void printPlugins(List installed) { System.out.println("Installed plugins: " + installed.size()); for (JadxPluginMetadata plugin : installed) { StringBuilder sb = new StringBuilder(); sb.append(" - ").append(plugin.getPluginId()); String version = plugin.getVersion(); if (version != null) { sb.append(" (").append(version).append(')'); } if (plugin.isDisabled()) { sb.append(" (disabled)"); } sb.append(" - ").append(plugin.getName()); sb.append(": ").append(formatDescription(plugin.getDescription())); System.out.println(sb); } } private void printVersions(String locationId, int limit) { System.out.println("Loading ..."); List versions = JadxPluginsTools.getInstance().getVersionsByLocation(locationId, 1, limit); if (versions.isEmpty()) { System.out.println("No versions found"); return; } JadxPluginMetadata plugin = versions.get(0); System.out.println("Versions for plugin id: " + plugin.getPluginId()); for (JadxPluginMetadata version : versions) { StringBuilder sb = new StringBuilder(); sb.append(" - ").append(version.getVersion()); String reqVer = version.getRequiredJadxVersion(); if (StringUtils.notBlank(reqVer)) { sb.append(", require jadx: ").append(reqVer); } System.out.println(sb); } } private static void printAllPlugins() { List installed = JadxPluginsTools.getInstance().getInstalled(); printPlugins(installed); Set installedSet = installed.stream().map(JadxPluginMetadata::getPluginId).collect(Collectors.toSet()); List plugins = JadxPluginsTools.getInstance().getAllPluginsInfo(); System.out.println("Other plugins: " + plugins.size()); for (JadxPluginInfo plugin : plugins) { if (!installedSet.contains(plugin.getPluginId())) { System.out.println(" - " + plugin.getPluginId() + " - " + plugin.getName() + ": " + formatDescription(plugin.getDescription())); } } } private static String formatDescription(String desc) { if (desc.contains("\n")) { // remove new lines desc = desc.replaceAll("\\R+", " "); } int maxLen = 512; if (desc.length() > maxLen) { // truncate very long descriptions desc = desc.substring(0, maxLen) + " ..."; } return desc; } private void installPlugin(String locationId) { JadxPluginMetadata plugin = JadxPluginsTools.getInstance().install(locationId); System.out.println("Plugin installed: " + plugin.getPluginId() + ":" + plugin.getVersion()); } } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/commands/ICommand.java ================================================ package jadx.cli.commands; import com.beust.jcommander.JCommander; import jadx.cli.JCommanderWrapper; public interface ICommand { String name(); void process(JCommanderWrapper jcw, JCommander subCommander); } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/config/IJadxConfig.java ================================================ package jadx.cli.config; /** * Marker interface for jadx config objects */ public interface IJadxConfig { } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/config/JadxConfigAdapter.java ================================================ package jadx.cli.config; import java.nio.file.Files; import java.nio.file.Path; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import com.google.gson.ExclusionStrategy; import com.google.gson.FieldAttributes; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.stream.JsonReader; import jadx.commons.app.JadxCommonFiles; import jadx.core.utils.GsonUtils; import jadx.core.utils.exceptions.JadxArgsValidateException; import jadx.core.utils.exceptions.JadxRuntimeException; public class JadxConfigAdapter { private static final ExclusionStrategy GSON_EXCLUSION_STRATEGY = new ExclusionStrategy() { @Override public boolean shouldSkipField(FieldAttributes f) { return f.getAnnotation(JadxConfigExclude.class) != null; } @Override public boolean shouldSkipClass(Class clazz) { return false; } }; private final Class configCls; private final String defaultConfigFileName; private final Gson gson; private Path configPath; public JadxConfigAdapter(Class configCls, String defaultConfigName) { this(configCls, defaultConfigName, gsonBuilder -> { }); } public JadxConfigAdapter(Class configCls, String defaultConfigName, Consumer applyGsonOptions) { this.configCls = configCls; this.defaultConfigFileName = defaultConfigName + ".json"; GsonBuilder gsonBuilder = GsonUtils.defaultGsonBuilder(); gsonBuilder.setExclusionStrategies(GSON_EXCLUSION_STRATEGY); applyGsonOptions.accept(gsonBuilder); this.gson = gsonBuilder.create(); } public void useConfigRef(String configRef) { this.configPath = resolveConfigRef(configRef); } public Path getConfigPath() { return configPath; } public String getDefaultConfigFileName() { return defaultConfigFileName; } public @Nullable T load() { if (!Files.isRegularFile(configPath)) { // file not found return null; } try (JsonReader reader = gson.newJsonReader(Files.newBufferedReader(configPath))) { return gson.fromJson(reader, configCls); } catch (Exception e) { throw new JadxRuntimeException("Failed to load config file: " + configPath, e); } } public void save(T configObject) { try { String jsonStr = gson.toJson(configObject, configCls); // don't use stream writer here because serialization errors will corrupt config Files.writeString(configPath, jsonStr); } catch (Exception e) { throw new JadxRuntimeException("Failed to save config file: " + configPath, e); } } public String objectToJsonString(T configObject) { return gson.toJson(configObject, configCls); } public T jsonStringToObject(String jsonStr) { return gson.fromJson(jsonStr, configCls); } private Path resolveConfigRef(String configRef) { if (configRef == null || configRef.isEmpty()) { // use default config file return JadxCommonFiles.getConfigDir().resolve(defaultConfigFileName); } if (configRef.contains("/") || configRef.contains("\\")) { if (!configRef.toLowerCase().endsWith(".json")) { throw new JadxArgsValidateException("Config file extension should be '.json'"); } return Path.of(configRef); } // treat as a short name return JadxCommonFiles.getConfigDir().resolve(configRef + ".json"); } } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/config/JadxConfigExclude.java ================================================ package jadx.cli.config; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface JadxConfigExclude { } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/plugins/JadxFilesGetter.java ================================================ package jadx.cli.plugins; import java.nio.file.Path; import jadx.commons.app.JadxCommonFiles; import jadx.commons.app.JadxTempFiles; import jadx.core.plugins.files.IJadxFilesGetter; public class JadxFilesGetter implements IJadxFilesGetter { public static final JadxFilesGetter INSTANCE = new JadxFilesGetter(); @Override public Path getConfigDir() { return JadxCommonFiles.getConfigDir(); } @Override public Path getCacheDir() { return JadxCommonFiles.getCacheDir(); } @Override public Path getTempDir() { return JadxTempFiles.getTempRootDir(); } private JadxFilesGetter() { // singleton } } ================================================ FILE: jadx-cli/src/main/java/jadx/cli/tools/ConvertArscFile.java ================================================ package jadx.cli.tools; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.android.TextResMapFile; import jadx.core.xmlgen.ResTableBinaryParser; import jadx.zip.IZipEntry; import jadx.zip.ZipContent; import jadx.zip.ZipReader; import static jadx.core.utils.files.FileUtils.expandDirs; /** * Utility class for convert '.arsc' to simple text file with mapping id to resource name */ public class ConvertArscFile { private static final Logger LOG = LoggerFactory.getLogger(ConvertArscFile.class); private static int rewritesCount; public static void usage() { LOG.info(" "); LOG.info(""); LOG.info("Note: If res-map already exists - it will be merged and updated"); } public static void main(String[] args) throws IOException { if (args.length < 2) { usage(); System.exit(1); } List inputPaths = Stream.of(args).map(Paths::get).collect(Collectors.toList()); Path resMapFile = inputPaths.remove(0); List inputResFiles = filterAndSort(expandDirs(inputPaths)); Map resMap; if (Files.isReadable(resMapFile)) { resMap = TextResMapFile.read(resMapFile); } else { resMap = new HashMap<>(); } LOG.info("Input entries count: {}", resMap.size()); RootNode root = new RootNode(new JadxArgs()); // not really needed ZipReader zipReader = new ZipReader(); rewritesCount = 0; for (Path resFile : inputResFiles) { ResTableBinaryParser resTableParser = new ResTableBinaryParser(root, true); if (resFile.getFileName().toString().endsWith(".jar")) { // Load resources.arsc from android.jar try (ZipContent zip = zipReader.open(resFile.toFile())) { IZipEntry entry = zip.searchEntry("resources.arsc"); if (entry == null) { LOG.error("Failed to load \"resources.arsc\" from {}", resFile); continue; } try (InputStream inputStream = entry.getInputStream()) { resTableParser.decode(inputStream); } } } else { // Load resources.arsc from extracted file try (InputStream inputStream = Files.newInputStream(resFile)) { resTableParser.decode(inputStream); } } Map singleResMap = resTableParser.getResStorage().getResourcesNames(); mergeResMaps(resMap, singleResMap); LOG.info("{} entries count: {}, after merge: {}", resFile.getFileName(), singleResMap.size(), resMap.size()); } LOG.info("Output entries count: {}", resMap.size()); LOG.info("Total rewrites count: {}", rewritesCount); TextResMapFile.write(resMapFile, resMap); LOG.info("Result file size: {} B", resMapFile.toFile().length()); LOG.info("done"); } private static List filterAndSort(List inputPaths) { return inputPaths.stream() .filter(p -> { String fileName = p.getFileName().toString(); return fileName.endsWith(".arsc") || fileName.endsWith(".jar"); }) .sorted() .collect(Collectors.toList()); } private static void mergeResMaps(Map mainResMap, Map newResMap) { for (Map.Entry entry : newResMap.entrySet()) { Integer id = entry.getKey(); String name = entry.getValue(); String prevName = mainResMap.put(id, name); if (prevName != null && !name.equals(prevName)) { LOG.debug("Rewrite id: {} from: '{}' to: '{}'", Integer.toHexString(id), prevName, name); rewritesCount++; } } } } ================================================ FILE: jadx-cli/src/main/resources/logback.xml ================================================ %-5level - %msg%n ================================================ FILE: jadx-cli/src/test/java/jadx/cli/BaseCliIntegrationTest.java ================================================ package jadx.cli; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.loader.JadxBasePluginLoader; import jadx.core.plugins.files.SingleDirFilesGetter; import jadx.core.utils.Utils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; public class BaseCliIntegrationTest { private static final Logger LOG = LoggerFactory.getLogger(BaseCliIntegrationTest.class); static final PathMatcher LOG_ALL_FILES = path -> { LOG.debug("File in result dir: {}", path); return true; }; @TempDir Path testDir; Path outputDir; @BeforeEach public void setUp() { outputDir = testDir.resolve("output"); } int execJadxCli(String sampleName, String... options) { return execJadxCli(buildArgs(List.of(options), sampleName)); } int execJadxCli(String[] args) { return JadxCLI.execute(args, jadxArgs -> { // don't use global config and plugins jadxArgs.setFilesGetter(new SingleDirFilesGetter(testDir)); jadxArgs.setPluginLoader(new JadxBasePluginLoader()); }); } String[] buildArgs(List options, String... inputSamples) { List args = new ArrayList<>(options); args.add("-v"); args.add("-d"); args.add(outputDir.toAbsolutePath().toString()); for (String inputSample : inputSamples) { try { URL resource = getClass().getClassLoader().getResource(inputSample); assertThat(resource).isNotNull(); String sampleFile = resource.toURI().getRawPath(); args.add(sampleFile); } catch (URISyntaxException e) { fail("Failed to load sample: " + inputSample, e); } } return args.toArray(new String[0]); } void decompile(String... inputSamples) throws IOException { int result = execJadxCli(buildArgs(List.of(), inputSamples)); assertThat(result).isEqualTo(0); List resultJavaFiles = collectJavaFilesInDir(outputDir); assertThat(resultJavaFiles).isNotEmpty(); // do not copy input files as resources for (Path path : collectFilesInDir(outputDir, LOG_ALL_FILES)) { for (String inputSample : inputSamples) { assertThat(path.toAbsolutePath().toString()).doesNotContain(inputSample); } } } static void printFiles(List files) { LOG.info("Output files (count: {}):", files.size()); for (Path file : files) { LOG.info(" {}", file); } LOG.info(""); } String pathToUniformString(Path path) { return path.toString().replace('\\', '/'); } Path printFileContent(Path file) { try { String content = Files.readString(outputDir.resolve(file)); String spacer = Utils.strRepeat("=", 70); LOG.info("File content: {}\n{}\n{}\n{}", file, spacer, content, spacer); return file; } catch (IOException e) { throw new RuntimeException("Failed to load file: " + file, e); } } static List collectJavaFilesInDir(Path dir) throws IOException { PathMatcher javaMatcher = dir.getFileSystem().getPathMatcher("glob:**.java"); return collectFilesInDir(dir, javaMatcher); } static List collectAllFilesInDir(Path dir) throws IOException { try (Stream pathStream = Files.walk(dir)) { List files = pathStream .filter(Files::isRegularFile) .map(dir::relativize) .collect(Collectors.toList()); printFiles(files); return files; } } static List collectFilesInDir(Path dir, PathMatcher matcher) throws IOException { try (Stream pathStream = Files.walk(dir)) { List files = pathStream .filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) .filter(matcher::matches) .collect(Collectors.toList()); printFiles(files); return files; } } } ================================================ FILE: jadx-cli/src/test/java/jadx/cli/JadxCLIArgsTest.java ================================================ package jadx.cli; import java.util.Collections; import java.util.Map; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static jadx.core.utils.Utils.newConstStringMap; import static org.assertj.core.api.Assertions.assertThat; public class JadxCLIArgsTest { private static final Logger LOG = LoggerFactory.getLogger(JadxCLIArgsTest.class); @Test public void testInvertedBooleanOption() { assertThat(parse("--no-replace-consts").isReplaceConsts()).isFalse(); assertThat(parse("").isReplaceConsts()).isTrue(); } @Test public void testEscapeUnicodeOption() { assertThat(parse("--escape-unicode").isEscapeUnicode()).isTrue(); assertThat(parse("").isEscapeUnicode()).isFalse(); } @Test public void testSrcOption() { assertThat(parse("--no-src").isSkipSources()).isTrue(); assertThat(parse("-s").isSkipSources()).isTrue(); assertThat(parse("").isSkipSources()).isFalse(); } @Test public void testOptionsOverride() { assertThat(override(new JadxCLIArgs(), "--no-imports").isUseImports()).isFalse(); assertThat(override(new JadxCLIArgs(), "--no-debug-info").isDebugInfo()).isFalse(); assertThat(override(new JadxCLIArgs(), "").isUseImports()).isTrue(); JadxCLIArgs args = new JadxCLIArgs(); args.useImports = false; assertThat(override(args, "--no-imports").isUseImports()).isFalse(); args.debugInfo = false; assertThat(override(args, "--no-debug-info").isDebugInfo()).isFalse(); args = new JadxCLIArgs(); args.useImports = false; assertThat(override(args, "").isUseImports()).isFalse(); } @Test public void testPluginOptionsOverride() { // add key to empty base map checkPluginOptionsMerge( Collections.emptyMap(), "-Poption=otherValue", newConstStringMap("option", "otherValue")); // override one key checkPluginOptionsMerge( newConstStringMap("option", "value"), "-Poption=otherValue", newConstStringMap("option", "otherValue")); // merge different keys checkPluginOptionsMerge( Collections.singletonMap("option1", "value1"), "-Poption2=otherValue2", newConstStringMap("option1", "value1", "option2", "otherValue2")); // merge and override checkPluginOptionsMerge( newConstStringMap("option1", "value1", "option2", "value2"), "-Poption2=otherValue2", newConstStringMap("option1", "value1", "option2", "otherValue2")); } private void checkPluginOptionsMerge(Map baseMap, String providedArgs, Map expectedMap) { JadxCLIArgs args = new JadxCLIArgs(); args.pluginOptions = baseMap; Map resultMap = override(args, providedArgs).getPluginOptions(); assertThat(resultMap).isEqualTo(expectedMap); } private JadxCLIArgs parse(String... args) { return parse(new JadxCLIArgs(), args); } private JadxCLIArgs parse(JadxCLIArgs jadxArgs, String... args) { return check(jadxArgs, jadxArgs.processArgs(args)); } private JadxCLIArgs override(JadxCLIArgs jadxArgs, String... args) { return check(jadxArgs, overrideProvided(jadxArgs, args)); } private static boolean overrideProvided(JadxCLIArgs jadxArgs, String[] args) { JCommanderWrapper jcw = new JCommanderWrapper(new JadxCLIArgs()); if (!jcw.parse(args)) { return false; } jcw.overrideProvided(jadxArgs); return jadxArgs.process(jcw); } private static JadxCLIArgs check(JadxCLIArgs jadxArgs, boolean res) { assertThat(res).isTrue(); LOG.info("Jadx args: {}", jadxArgs.toJadxArgs()); return jadxArgs; } } ================================================ FILE: jadx-cli/src/test/java/jadx/cli/RenameConverterTest.java ================================================ package jadx.cli; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import jadx.api.JadxArgs.RenameEnum; import jadx.cli.JadxCLIArgs.RenameConverter; import jadx.core.utils.exceptions.JadxArgsValidateException; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; public class RenameConverterTest { private RenameConverter converter; @BeforeEach public void init() { converter = new RenameConverter("someParam"); } @Test public void all() { Set set = converter.convert("all"); assertThat(set).hasSize(3); assertThat(set).contains(RenameEnum.CASE); assertThat(set).contains(RenameEnum.VALID); assertThat(set).contains(RenameEnum.PRINTABLE); } @Test public void none() { Set set = converter.convert("none"); assertThat(set).isEmpty(); } @Test public void wrong() { JadxArgsValidateException thrown = assertThrows(JadxArgsValidateException.class, () -> converter.convert("wrong"), "Expected convert() to throw, but it didn't"); assertThat(thrown.getMessage()).isEqualTo("'wrong' is unknown for parameter someParam, possible values are case, valid, printable"); } } ================================================ FILE: jadx-cli/src/test/java/jadx/cli/TestExport.java ================================================ package jadx.cli; import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class TestExport extends BaseCliIntegrationTest { @Test public void testBasicExport() throws Exception { int result = execJadxCli("samples/small.apk"); assertThat(result).isEqualTo(0); assertThat(collectAllFilesInDir(outputDir)) .map(this::pathToUniformString) .haveExactly(2, new Condition<>(f -> f.startsWith("sources/") && f.endsWith(".java"), "sources")) .haveExactly(10, new Condition<>(f -> f.startsWith("resources/"), "resources")) .haveExactly(1, new Condition<>(f -> f.equals("resources/AndroidManifest.xml"), "manifest")) .hasSize(12); } @Test public void testGradleExportApk() throws Exception { int result = execJadxCli("samples/small.apk", "--export-gradle"); assertThat(result).isEqualTo(0); assertThat(collectAllFilesInDir(outputDir)) .describedAs("check output files") .map(this::pathToUniformString) .haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes")) .haveExactly(0, new Condition<>(f -> f.endsWith("classes.dex"), "dex files")) .hasSize(15); } @Test public void testGradleExportAAR() throws Exception { int result = execJadxCli("samples/test-lib.aar", "--export-gradle"); assertThat(result).isEqualTo(0); assertThat(collectAllFilesInDir(outputDir)) .describedAs("check output files") .map(this::printFileContent) .map(this::pathToUniformString) .haveExactly(1, new Condition<>(f -> f.startsWith("lib/src/main/java/") && f.endsWith(".java"), "java")) .haveExactly(0, new Condition<>(f -> f.endsWith(".jar"), "jar files")) .hasSize(8); } @Test public void testGradleExportSimpleJava() throws Exception { int result = execJadxCli("samples/HelloWorld.class", "--export-gradle"); assertThat(result).isEqualTo(0); assertThat(collectAllFilesInDir(outputDir)) .describedAs("check output files") .map(this::printFileContent) .map(this::pathToUniformString) .haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java")) .haveExactly(0, new Condition<>(f -> f.endsWith(".class"), "class files")) .haveExactly(1, new Condition<>(f -> f.equals("settings.gradle.kts"), "settings")) .haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle.kts"), "build")) .hasSize(3); } @Test public void testGradleExportInvalidType() throws Exception { int result = execJadxCli("samples/HelloWorld.class", "--export-gradle-type", "android-app"); assertThat(result).isEqualTo(0); // expect output in 'android-app' template, but most fields will be set to UNKNOWN. assertThat(collectAllFilesInDir(outputDir)) .describedAs("check output files") .map(this::printFileContent) .map(this::pathToUniformString) .haveExactly(1, new Condition<>(f -> f.endsWith(".java") && f.startsWith("app/src/main/java/"), "java")) .haveExactly(1, new Condition<>(f -> f.equals("settings.gradle"), "settings")) .haveExactly(1, new Condition<>(f -> f.equals("build.gradle"), "build")) .haveExactly(1, new Condition<>(f -> f.equals("app/build.gradle"), "app build")) .hasSize(4); } } ================================================ FILE: jadx-cli/src/test/java/jadx/cli/TestInput.java ================================================ package jadx.cli; import java.nio.file.Path; import java.util.List; import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class TestInput extends BaseCliIntegrationTest { @Test public void testHelp() { int result = execJadxCli(new String[] { "--help" }); assertThat(result).isEqualTo(0); } @Test public void testApkInput() throws Exception { int result = execJadxCli(buildArgs(List.of(), "samples/small.apk")); assertThat(result).isEqualTo(0); assertThat(collectAllFilesInDir(outputDir)) .describedAs("check output files") .map(p -> p.getFileName().toString()) .haveExactly(2, new Condition<>(f -> f.endsWith(".java"), "java classes")) .haveExactly(9, new Condition<>(f -> f.endsWith(".xml"), "xml resources")) .haveExactly(1, new Condition<>(f -> f.equals("AndroidManifest.xml"), "manifest")) .hasSize(12); } @Test public void testDexInput() throws Exception { decompile("samples/hello.dex"); } @Test public void testSmaliInput() throws Exception { decompile("samples/HelloWorld.smali"); } @Test public void testClassInput() throws Exception { decompile("samples/HelloWorld.class"); } @Test public void testMultipleInput() throws Exception { decompile("samples/hello.dex", "samples/HelloWorld.smali"); } @Test public void testFallbackMode() throws Exception { int result = execJadxCli(buildArgs(List.of("-f"), "samples/hello.dex")); assertThat(result).isEqualTo(0); List files = collectJavaFilesInDir(outputDir); assertThat(files).hasSize(1); } @Test public void testSimpleMode() throws Exception { int result = execJadxCli(buildArgs(List.of("--decompilation-mode", "simple"), "samples/hello.dex")); assertThat(result).isEqualTo(0); List files = collectJavaFilesInDir(outputDir); assertThat(files).hasSize(1); } @Test public void testResourceOnly() throws Exception { int result = execJadxCli(buildArgs(List.of(), "samples/resources-only.apk")); assertThat(result).isEqualTo(0); List files = collectFilesInDir(outputDir, path -> path.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml")); assertThat(files).isNotEmpty(); } } ================================================ FILE: jadx-cli/src/test/java/jadx/plugins/tools/utils/PluginUtilsTest.java ================================================ package jadx.plugins.tools.utils; import org.junit.jupiter.api.Test; import static jadx.plugins.tools.utils.PluginUtils.extractVersion; import static org.assertj.core.api.Assertions.assertThat; class PluginUtilsTest { @Test public void testExtractVersion() { assertThat(extractVersion("plugin-name-v1.2.3.jar")).isEqualTo("1.2.3"); assertThat(extractVersion("plugin-name-v1.2.jar")).isEqualTo("1.2"); assertThat(extractVersion("1.2.3.jar")).isEqualTo("1.2.3"); } } ================================================ FILE: jadx-cli/src/test/resources/samples/HelloWorld.smali ================================================ .class Lsmali/HelloWorld; .super Ljava/lang/Object; .source "HelloWorld.java" .method constructor ()V .registers 1 .line 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public static main([Ljava/lang/String;)V .registers 2 .line 3 sget-object p0, Ljava/lang/System;->out:Ljava/io/PrintStream; const-string v0, "Hello, World" invoke-virtual {p0, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V .line 4 return-void .end method ================================================ FILE: jadx-commons/jadx-app-commons/README.md ================================================ ## jadx app commons This module contains common utilities used in jadx apps (cli and gui) and not needed in jadx-code module: - `JadxCommonFiles` - wrapper for `dev.dirs:directories` lib to get 'config' and 'cache' directories in cross-platform way - `JadxCommonEnv` - utils for work with environment variables ================================================ FILE: jadx-commons/jadx-app-commons/build.gradle.kts ================================================ plugins { id("jadx-library") } dependencies { implementation("io.get-coursier.util:directories-jni:0.1.4") } ================================================ FILE: jadx-commons/jadx-app-commons/src/main/java/jadx/commons/app/JadxCommonEnv.java ================================================ package jadx.commons.app; public class JadxCommonEnv { public static String get(String varName, String defValue) { String strValue = System.getenv(varName); return isNullOrEmpty(strValue) ? defValue : strValue; } public static boolean getBool(String varName, boolean defValue) { String strValue = System.getenv(varName); if (isNullOrEmpty(strValue)) { return defValue; } return strValue.equalsIgnoreCase("true"); } public static int getInt(String varName, int defValue) { String strValue = System.getenv(varName); if (isNullOrEmpty(strValue)) { return defValue; } return Integer.parseInt(strValue); } private static boolean isNullOrEmpty(String value) { return value == null || value.isEmpty(); } } ================================================ FILE: jadx-commons/jadx-app-commons/src/main/java/jadx/commons/app/JadxCommonFiles.java ================================================ package jadx.commons.app; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.function.Function; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import dev.dirs.ProjectDirectories; import dev.dirs.impl.Windows; import dev.dirs.impl.WindowsPowerShell; import dev.dirs.jni.WindowsJni; public class JadxCommonFiles { private static final Logger LOG = LoggerFactory.getLogger(JadxCommonFiles.class); private static final Path CONFIG_DIR; private static final Path CACHE_DIR; public static Path getConfigDir() { return CONFIG_DIR; } public static Path getCacheDir() { return CACHE_DIR; } static { DirsLoader loader = new DirsLoader(); loader.init(); CONFIG_DIR = loader.getConfigDir(); CACHE_DIR = loader.getCacheDir(); } private static final class DirsLoader { private @Nullable ProjectDirectories dirs; private Path configDir; private Path cacheDir; public void init() { try { configDir = loadEnvDir("JADX_CONFIG_DIR", pd -> pd.configDir); cacheDir = loadEnvDir("JADX_CACHE_DIR", pd -> pd.cacheDir); } catch (Exception e) { throw new RuntimeException("Failed to init common directories", e); } } private Path loadEnvDir(String envVar, Function dirFunc) throws IOException { String envDir = JadxCommonEnv.get(envVar, null); String dirStr; if (envDir != null) { dirStr = envDir; } else { dirStr = dirFunc.apply(loadDirs()); } Path path = Path.of(dirStr).toAbsolutePath(); Files.createDirectories(path); return path; } private synchronized ProjectDirectories loadDirs() { ProjectDirectories currentDirs = dirs; if (currentDirs != null) { return currentDirs; } LOG.debug("Loading system dirs ..."); long start = System.currentTimeMillis(); ProjectDirectories loadedDirs = ProjectDirectories.from("io.github", "skylot", "jadx", DirsLoader::getWinDirs); if (LOG.isDebugEnabled()) { LOG.debug("Loaded system dirs ({}ms): config: {}, cache: {}", System.currentTimeMillis() - start, loadedDirs.configDir, loadedDirs.cacheDir); } dirs = loadedDirs; return loadedDirs; } /** * Return JNI, Foreign or PowerShell implementation */ private static Windows getWinDirs() { Windows defSup = Windows.getDefaultSupplier().get(); if (defSup instanceof WindowsPowerShell) { if (JadxSystemInfo.IS_AMD64) { // JNI library compiled for x86-64 return new WindowsJni(); } } return defSup; } public Path getCacheDir() { return cacheDir; } public Path getConfigDir() { return configDir; } } } ================================================ FILE: jadx-commons/jadx-app-commons/src/main/java/jadx/commons/app/JadxSystemInfo.java ================================================ package jadx.commons.app; import java.util.Locale; public class JadxSystemInfo { public static final String JAVA_VM = System.getProperty("java.vm.name", "?"); public static final String JAVA_VER = System.getProperty("java.version", "?"); public static final String OS_NAME = System.getProperty("os.name", "?"); public static final String OS_ARCH = System.getProperty("os.arch", "?"); public static final String OS_VERSION = System.getProperty("os.version", "?"); private static final String OS_NAME_LOWER = OS_NAME.toLowerCase(Locale.ENGLISH); public static final boolean IS_WINDOWS = OS_NAME_LOWER.startsWith("windows"); public static final boolean IS_MAC = OS_NAME_LOWER.startsWith("mac"); public static final boolean IS_LINUX = !IS_WINDOWS && !IS_MAC; public static final boolean IS_UNIX = !IS_WINDOWS; private static final String OS_ARCH_LOWER = OS_NAME.toLowerCase(Locale.ENGLISH); public static final boolean IS_AMD64 = OS_ARCH_LOWER.equals("amd64"); public static final boolean IS_ARM64 = OS_ARCH_LOWER.equals("aarch64"); private JadxSystemInfo() { } } ================================================ FILE: jadx-commons/jadx-app-commons/src/main/java/jadx/commons/app/JadxTempFiles.java ================================================ package jadx.commons.app; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; public class JadxTempFiles { private static final String JADX_TMP_INSTANCE_PREFIX = "jadx-instance-"; private static final Path TEMP_ROOT_DIR = createTempRootDir(); public static Path getTempRootDir() { return TEMP_ROOT_DIR; } private static Path createTempRootDir() { try { String jadxTmpDir = System.getenv("JADX_TMP_DIR"); Path dir; if (jadxTmpDir != null) { Path customTmpRootDir = Paths.get(jadxTmpDir); Files.createDirectories(customTmpRootDir); dir = Files.createTempDirectory(customTmpRootDir, JADX_TMP_INSTANCE_PREFIX); } else { dir = Files.createTempDirectory(JADX_TMP_INSTANCE_PREFIX); } dir.toFile().deleteOnExit(); return dir; } catch (Exception e) { throw new RuntimeException("Failed to create temp root directory", e); } } } ================================================ FILE: jadx-commons/jadx-zip/README.md ================================================ ## jadx zip Custom zip reader implementation to fight tampering and provide additional security checks ================================================ FILE: jadx-commons/jadx-zip/build.gradle.kts ================================================ plugins { id("jadx-library") } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/IZipEntry.java ================================================ package jadx.zip; import java.io.File; import java.io.InputStream; public interface IZipEntry { /** * Zip entry name */ String getName(); /** * Uncompressed bytes */ byte[] getBytes(); /** * Stream of uncompressed bytes. */ InputStream getInputStream(); long getCompressedSize(); long getUncompressedSize(); boolean isDirectory(); File getZipFile(); /** * Return true if {@link #getBytes()} method is more optimal to use other than * {@link #getInputStream()} */ boolean preferBytes(); } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/IZipParser.java ================================================ package jadx.zip; import java.io.Closeable; import java.io.IOException; public interface IZipParser extends Closeable { ZipContent open() throws IOException; } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipContent.java ================================================ package jadx.zip; import java.io.Closeable; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ZipContent implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(ZipContent.class); private final IZipParser zipParser; private final List entries; private final Map entriesMap; public ZipContent(IZipParser zipParser, List entries) { this.zipParser = zipParser; this.entries = entries; this.entriesMap = buildNameMap(zipParser, entries); } private static Map buildNameMap(IZipParser zipParser, List entries) { Map map = new HashMap<>(entries.size()); for (IZipEntry entry : entries) { String name = entry.getName(); IZipEntry prevEntry = map.put(name, entry); if (prevEntry != null) { LOG.warn("Found duplicate entry: {} in {}", name, zipParser); } } return map; } public List getEntries() { return entries; } public @Nullable IZipEntry searchEntry(String fileName) { return entriesMap.get(fileName); } @Override public void close() throws IOException { zipParser.close(); } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReader.java ================================================ package jadx.zip; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; import org.jetbrains.annotations.Nullable; import jadx.zip.fallback.FallbackZipParser; import jadx.zip.parser.JadxZipParser; import jadx.zip.security.IJadxZipSecurity; import jadx.zip.security.JadxZipSecurity; /** * Jadx wrapper to provide custom zip parser ({@link JadxZipParser}) * with fallback to default Java implementation. */ public class ZipReader { private final ZipReaderOptions options; public ZipReader() { this(ZipReaderOptions.getDefault()); } public ZipReader(Set flags) { this(new ZipReaderOptions(new JadxZipSecurity(), flags)); } public ZipReader(IJadxZipSecurity security) { this(new ZipReaderOptions(security, ZipReaderFlags.none())); } public ZipReader(ZipReaderOptions options) { this.options = options; } @SuppressWarnings("resource") public ZipContent open(File zipFile) throws IOException { try { JadxZipParser jadxParser = new JadxZipParser(zipFile, options); IZipParser detectedParser = detectParser(zipFile, jadxParser); if (detectedParser != jadxParser) { jadxParser.close(); } return detectedParser.open(); } catch (Exception e) { if (options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) { throw new IOException("Failed to open zip: " + zipFile, e); } // switch to fallback parser return buildFallbackParser(zipFile).open(); } } /** * Visit valid entries in a zip file. * Return not null value from visitor to stop iteration. */ public @Nullable R visitEntries(File file, Function visitor) { try (ZipContent content = open(file)) { for (IZipEntry entry : content.getEntries()) { R result = visitor.apply(entry); if (result != null) { return result; } } } catch (Exception e) { throw new RuntimeException("Failed to process zip file: " + file.getAbsolutePath(), e); } return null; } public void readEntries(File file, BiConsumer visitor) { visitEntries(file, entry -> { if (!entry.isDirectory()) { try (InputStream in = entry.getInputStream()) { visitor.accept(entry, in); } catch (Exception e) { throw new RuntimeException("Failed to process zip entry: " + entry, e); } } return null; }); } public ZipReaderOptions getOptions() { return options; } private IZipParser detectParser(File zipFile, JadxZipParser jadxParser) { if (zipFile.getName().endsWith(".apk") || options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) { return jadxParser; } if (!jadxParser.canOpen()) { return buildFallbackParser(zipFile); } // default if (options.getFlags().contains(ZipReaderFlags.FALLBACK_AS_DEFAULT)) { return buildFallbackParser(zipFile); } return jadxParser; } private FallbackZipParser buildFallbackParser(File zipFile) { return new FallbackZipParser(zipFile, options); } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReaderFlags.java ================================================ package jadx.zip; import java.util.EnumSet; import java.util.Set; public enum ZipReaderFlags { /** * Search all local file headers by signature without reading * 'central directory' and 'end of central directory' entries */ IGNORE_CENTRAL_DIR_ENTRIES, /** * Enable additional checks to verify zip data and report possible tampering */ REPORT_TAMPERING, /** * Use fallback (java built-in implementation) parser as default. * Custom implementation will be used for '*.apk' files only. */ FALLBACK_AS_DEFAULT, /** * Use only jadx custom parser and do not switch to fallback on errors. */ DONT_USE_FALLBACK; public static Set none() { return EnumSet.noneOf(ZipReaderFlags.class); } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReaderOptions.java ================================================ package jadx.zip; import java.util.Set; import jadx.zip.security.IJadxZipSecurity; import jadx.zip.security.JadxZipSecurity; public class ZipReaderOptions { public static ZipReaderOptions getDefault() { return new ZipReaderOptions(new JadxZipSecurity(), ZipReaderFlags.none()); } private final IJadxZipSecurity zipSecurity; private final Set flags; public ZipReaderOptions(IJadxZipSecurity zipSecurity, Set flags) { this.zipSecurity = zipSecurity; this.flags = flags; } public IJadxZipSecurity getZipSecurity() { return zipSecurity; } public Set getFlags() { return flags; } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/fallback/FallbackZipEntry.java ================================================ package jadx.zip.fallback; import java.io.File; import java.io.InputStream; import java.util.zip.ZipEntry; import jadx.zip.IZipEntry; public class FallbackZipEntry implements IZipEntry { private final FallbackZipParser parser; private final ZipEntry zipEntry; public FallbackZipEntry(FallbackZipParser parser, ZipEntry zipEntry) { this.parser = parser; this.zipEntry = zipEntry; } public ZipEntry getZipEntry() { return zipEntry; } @Override public String getName() { return zipEntry.getName(); } @Override public boolean preferBytes() { return false; } @Override public byte[] getBytes() { return parser.getBytes(this); } @Override public InputStream getInputStream() { return parser.getInputStream(this); } @Override public long getCompressedSize() { return zipEntry.getCompressedSize(); } @Override public long getUncompressedSize() { return zipEntry.getSize(); } @Override public boolean isDirectory() { return zipEntry.isDirectory(); } @Override public File getZipFile() { return parser.getZipFile(); } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/fallback/FallbackZipParser.java ================================================ package jadx.zip.fallback; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.zip.IZipEntry; import jadx.zip.IZipParser; import jadx.zip.ZipContent; import jadx.zip.ZipReaderOptions; import jadx.zip.io.LimitedInputStream; import jadx.zip.security.IJadxZipSecurity; public class FallbackZipParser implements IZipParser { private static final Logger LOG = LoggerFactory.getLogger(FallbackZipParser.class); private final File file; private final IJadxZipSecurity zipSecurity; private final boolean useLimitedDataStream; private ZipFile zipFile; public FallbackZipParser(File file, ZipReaderOptions options) { this.file = file; this.zipSecurity = options.getZipSecurity(); this.useLimitedDataStream = zipSecurity.useLimitedDataStream(); } @Override public ZipContent open() throws IOException { zipFile = new ZipFile(file); int maxEntriesCount = zipSecurity.getMaxEntriesCount(); if (maxEntriesCount == -1) { maxEntriesCount = Integer.MAX_VALUE; } List list = new ArrayList<>(); Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { FallbackZipEntry zipEntry = new FallbackZipEntry(this, entries.nextElement()); if (isValidEntry(zipEntry)) { list.add(zipEntry); if (list.size() > maxEntriesCount) { throw new IllegalStateException("Max entries count limit exceeded: " + list.size()); } } } return new ZipContent(this, list); } private boolean isValidEntry(IZipEntry zipEntry) { boolean validEntry = zipSecurity.isValidEntry(zipEntry); if (!validEntry) { LOG.warn("Zip entry '{}' is invalid and excluded from processing", zipEntry); } return validEntry; } public byte[] getBytes(FallbackZipEntry entry) { try (InputStream is = getEntryStream(entry)) { return is.readAllBytes(); } catch (Exception e) { throw new RuntimeException("Failed to read bytes for entry: " + entry.getName(), e); } } public InputStream getInputStream(FallbackZipEntry entry) { try { return getEntryStream(entry); } catch (Exception e) { throw new RuntimeException("Failed to open input stream for entry: " + entry.getName(), e); } } private InputStream getEntryStream(FallbackZipEntry entry) throws IOException { InputStream entryStream = zipFile.getInputStream(entry.getZipEntry()); InputStream stream; if (useLimitedDataStream) { stream = new LimitedInputStream(entryStream, entry.getUncompressedSize()); } else { stream = entryStream; } return new BufferedInputStream(stream); } public File getZipFile() { return file; } @Override public void close() throws IOException { try { if (zipFile != null) { zipFile.close(); } } finally { zipFile = null; } } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/io/ByteBufferBackedInputStream.java ================================================ package jadx.zip.io; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; public class ByteBufferBackedInputStream extends InputStream { private final ByteBuffer buf; private int markedPosition = 0; public ByteBufferBackedInputStream(ByteBuffer buf) { this.buf = buf; } public int read() throws IOException { if (!buf.hasRemaining()) { return -1; } return buf.get() & 0xFF; } @SuppressWarnings("NullableProblems") public int read(byte[] bytes, int off, int len) throws IOException { if (!buf.hasRemaining()) { return -1; } int readLen = Math.min(len, buf.remaining()); buf.get(bytes, off, readLen); return readLen; } @Override public boolean markSupported() { return true; } @Override public synchronized void mark(int unused) { markedPosition = buf.position(); } @Override public synchronized void reset() { buf.position(markedPosition); } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/io/LimitedInputStream.java ================================================ package jadx.zip.io; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; public class LimitedInputStream extends FilterInputStream { private final long maxSize; private long currentPos; private long markPos; public LimitedInputStream(InputStream in, long maxSize) { super(in); this.maxSize = maxSize; } private void addAndCheckPos(long count) { currentPos += count; if (currentPos > maxSize) { throw new IllegalStateException("Read limit exceeded"); } } @Override public int read() throws IOException { int data = super.read(); if (data != -1) { addAndCheckPos(1); } return data; } @SuppressWarnings("NullableProblems") @Override public int read(byte[] b, int off, int len) throws IOException { int count = super.read(b, off, len); if (count > 0) { addAndCheckPos(count); } return count; } @Override public long skip(long n) throws IOException { long skipped = super.skip(n); if (skipped > 0) { addAndCheckPos(skipped); } return skipped; } @Override public void mark(int readLimit) { super.mark(readLimit); markPos = currentPos; } @Override public void reset() throws IOException { super.reset(); currentPos = markPos; } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/JadxZipEntry.java ================================================ package jadx.zip.parser; import java.io.File; import java.io.InputStream; import jadx.zip.IZipEntry; public final class JadxZipEntry implements IZipEntry { private final JadxZipParser parser; private final String fileName; private final int compressMethod; private final int entryStart; private final int dataStart; private final long compressedSize; private final long uncompressedSize; JadxZipEntry(JadxZipParser parser, String fileName, int entryStart, int dataStart, int compressMethod, long compressedSize, long uncompressedSize) { this.parser = parser; this.fileName = fileName; this.entryStart = entryStart; this.dataStart = dataStart; this.compressMethod = compressMethod; this.compressedSize = compressedSize; this.uncompressedSize = uncompressedSize; } public boolean isSizesValid() { if (compressedSize <= 0) { return false; } if (uncompressedSize <= 0) { return false; } return compressedSize <= uncompressedSize; } public String getName() { return fileName; } @Override public long getCompressedSize() { return compressedSize; } @Override public long getUncompressedSize() { return uncompressedSize; } @Override public boolean isDirectory() { return fileName.endsWith("/"); } @Override public boolean preferBytes() { return true; } @Override public byte[] getBytes() { return parser.getBytes(this); } @Override public InputStream getInputStream() { return parser.getInputStream(this); } public int getEntryStart() { return entryStart; } public int getDataStart() { return dataStart; } public int getCompressMethod() { return compressMethod; } @Override public File getZipFile() { return parser.getZipFile(); } @Override public String toString() { return parser.getZipFile().getName() + ':' + fileName; } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/JadxZipParser.java ================================================ package jadx.zip.parser; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.zip.IZipEntry; import jadx.zip.IZipParser; import jadx.zip.ZipContent; import jadx.zip.ZipReaderFlags; import jadx.zip.ZipReaderOptions; import jadx.zip.fallback.FallbackZipParser; import jadx.zip.io.ByteBufferBackedInputStream; import jadx.zip.io.LimitedInputStream; import jadx.zip.security.IJadxZipSecurity; /** * Custom and simple zip parser to fight tampering. * Many zip features aren't supported: * - Compression methods other than STORE or DEFLATE * - Zip64 * - Checksum verification * - Multi file archives */ public final class JadxZipParser implements IZipParser { private static final Logger LOG = LoggerFactory.getLogger(JadxZipParser.class); private static final byte LOCAL_FILE_HEADER_START = 0x50; private static final int LOCAL_FILE_HEADER_SIGN = 0x04034b50; private static final int CD_SIGN = 0x02014b50; private static final int END_OF_CD_SIGN = 0x06054b50; private final File zipFile; private final ZipReaderOptions options; private final IJadxZipSecurity zipSecurity; private final Set flags; private final boolean verify; private final boolean useLimitedDataStream; private RandomAccessFile file; private FileChannel fileChannel; private ByteBuffer byteBuffer; private int endOfCDStart = -2; private @Nullable ZipContent fallbackZipContent; public JadxZipParser(File zipFile, ZipReaderOptions options) { this.zipFile = zipFile; this.options = options; this.zipSecurity = options.getZipSecurity(); this.flags = options.getFlags(); this.verify = options.getFlags().contains(ZipReaderFlags.REPORT_TAMPERING); this.useLimitedDataStream = zipSecurity.useLimitedDataStream(); } @Override public ZipContent open() throws IOException { load(); try { int maxEntriesCount = zipSecurity.getMaxEntriesCount(); if (maxEntriesCount == -1) { maxEntriesCount = Integer.MAX_VALUE; } List entries; if (flags.contains(ZipReaderFlags.IGNORE_CENTRAL_DIR_ENTRIES)) { entries = searchLocalFileHeaders(maxEntriesCount); } else { entries = loadFromCentralDirs(maxEntriesCount); } return new ZipContent(this, entries); } catch (Exception e) { if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) { throw new IOException("Failed to open zip: " + zipFile + ", error: " + e.getMessage(), e); } LOG.warn("Zip open failed, switching to fallback parser, zip: {}", zipFile, e); return initFallbackParser(); } } @SuppressWarnings("RedundantIfStatement") public boolean canOpen() { try { load(); int eocdStart = searchEndOfCDStart(); ByteBuffer buf = byteBuffer; buf.position(eocdStart + 4); int diskNum = readU2(buf); if (diskNum == 0xFFFF) { // Zip64 return false; } return true; } catch (Exception e) { LOG.warn("Jadx parser can't open zip file: {}", zipFile, e); return false; } } private boolean isValidEntry(JadxZipEntry zipEntry) { boolean validEntry = zipSecurity.isValidEntry(zipEntry); if (!validEntry) { LOG.warn("Zip entry '{}' is invalid and excluded from processing", zipEntry); } return validEntry; } private void load() throws IOException { if (byteBuffer != null) { // already loaded return; } file = new RandomAccessFile(zipFile, "r"); long size = file.length(); if (size >= Integer.MAX_VALUE) { throw new IOException("Zip file is too big"); } int fileLen = (int) size; if (fileLen < 100 * 1024 * 1024) { // load files smaller than 100MB directly into memory byte[] bytes = new byte[fileLen]; file.readFully(bytes); byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer(); file.close(); file = null; } else { // for big files - use a memory mapped file fileChannel = file.getChannel(); byteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); } byteBuffer.order(ByteOrder.LITTLE_ENDIAN); } private List searchLocalFileHeaders(int maxEntriesCount) { List entries = new ArrayList<>(); while (true) { int start = searchEntryStart(); if (start == -1) { return entries; } JadxZipEntry zipEntry = loadFileEntry(start); if (isValidEntry(zipEntry)) { entries.add(zipEntry); if (entries.size() > maxEntriesCount) { throw new IllegalStateException("Max entries count limit exceeded: " + entries.size()); } } } } private List loadFromCentralDirs(int maxEntriesCount) throws IOException { int eocdStart = searchEndOfCDStart(); if (eocdStart < 0) { throw new RuntimeException("End of central directory not found"); } ByteBuffer buf = byteBuffer; buf.position(eocdStart + 10); int entriesCount = readU2(buf); buf.position(eocdStart + 16); int cdOffset = buf.getInt(); if (entriesCount > maxEntriesCount) { throw new IllegalStateException("Max entries count limit exceeded: " + entriesCount); } List entries = new ArrayList<>(entriesCount); buf.position(cdOffset); for (int i = 0; i < entriesCount; i++) { JadxZipEntry zipEntry = loadCDEntry(); if (isValidEntry(zipEntry)) { entries.add(zipEntry); } } return entries; } private JadxZipEntry loadCDEntry() { ByteBuffer buf = byteBuffer; int start = buf.position(); buf.position(start + 28); int fileNameLen = readU2(buf); int extraFieldLen = readU2(buf); int commentLen = readU2(buf); buf.position(start + 42); int fileEntryStart = buf.getInt(); int entryEnd = start + 46 + fileNameLen + extraFieldLen + commentLen; JadxZipEntry entry = loadFileEntry(fileEntryStart); if (verify) { compareCDAndLFH(buf, start, entry); } if (!entry.isSizesValid()) { entry = fixEntryFromCD(entry, start); } buf.position(entryEnd); return entry; } private JadxZipEntry fixEntryFromCD(JadxZipEntry entry, int start) { ByteBuffer buf = byteBuffer; buf.position(start + 10); int comprMethod = readU2(buf); buf.position(start + 20); int comprSize = buf.getInt(); int unComprSize = buf.getInt(); return new JadxZipEntry(this, entry.getName(), start, entry.getDataStart(), comprMethod, comprSize, unComprSize); } private static void compareCDAndLFH(ByteBuffer buf, int start, JadxZipEntry entry) { buf.position(start + 10); int comprMethod = readU2(buf); if (comprMethod != entry.getCompressMethod()) { LOG.warn("Compression method differ in CD {} and LFH {} for {}", comprMethod, entry.getCompressMethod(), entry); } buf.position(start + 20); int comprSize = buf.getInt(); int unComprSize = buf.getInt(); if (comprSize != entry.getCompressedSize()) { LOG.warn("Compressed size differ in CD {} and LFH {} for {}", comprSize, entry.getCompressedSize(), entry); } if (unComprSize != entry.getUncompressedSize()) { LOG.warn("Uncompressed size differ in CD {} and LFH {} for {}", unComprSize, entry.getUncompressedSize(), entry); } } private JadxZipEntry loadFileEntry(int start) { ByteBuffer buf = byteBuffer; buf.position(start + 8); int comprMethod = readU2(buf); buf.position(start + 18); int comprSize = buf.getInt(); int unComprSize = buf.getInt(); int fileNameLen = readU2(buf); int extraFieldLen = readU2(buf); String fileName = readString(buf, fileNameLen); int dataStart = start + 30 + fileNameLen + extraFieldLen; buf.position(dataStart + comprSize); return new JadxZipEntry(this, fileName, start, dataStart, comprMethod, comprSize, unComprSize); } private int searchEndOfCDStart() throws IOException { if (endOfCDStart != -2) { return endOfCDStart; } ByteBuffer buf = byteBuffer; int pos = buf.limit() - 22; int minPos = Math.max(0, pos - 0xffff); while (true) { buf.position(pos); int sign = buf.getInt(); if (sign == END_OF_CD_SIGN) { endOfCDStart = pos; return pos; } pos--; if (pos < minPos) { throw new IOException("End of central directory record not found"); } } } private int searchEntryStart() { ByteBuffer buf = byteBuffer; while (true) { int start = buf.position(); if (start + 4 > buf.limit()) { return -1; } byte b = buf.get(); if (b == LOCAL_FILE_HEADER_START) { buf.position(start); int sign = buf.getInt(); if (sign == LOCAL_FILE_HEADER_SIGN) { return start; } } } } synchronized InputStream getInputStream(JadxZipEntry entry) { if (verify) { verifyEntry(entry); } InputStream stream; if (entry.getCompressMethod() == 8) { try { stream = ZipDeflate.decompressEntryToStream(byteBuffer, entry); } catch (Exception e) { entryParseFailed(entry, e); return useFallbackParser(entry).getInputStream(); } } else { // treat any other compression methods values as UNCOMPRESSED stream = bufferToStream(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize()); } if (useLimitedDataStream) { return new LimitedInputStream(stream, entry.getUncompressedSize()); } return stream; } synchronized byte[] getBytes(JadxZipEntry entry) { if (verify) { verifyEntry(entry); } if (entry.getCompressMethod() == 8) { try { return ZipDeflate.decompressEntryToBytes(byteBuffer, entry); } catch (Exception e) { entryParseFailed(entry, e); return useFallbackParser(entry).getBytes(); } } // treat any other compression methods values as UNCOMPRESSED return bufferToBytes(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize()); } private static void verifyEntry(JadxZipEntry entry) { int compressMethod = entry.getCompressMethod(); if (compressMethod == 0) { if (entry.getCompressedSize() != entry.getUncompressedSize()) { LOG.warn("Not equal sizes for STORE method: compressed: {}, uncompressed: {}, entry: {}", entry.getCompressedSize(), entry.getUncompressedSize(), entry); } } else if (compressMethod != 8) { LOG.warn("Unknown compress method: {} in entry: {}", compressMethod, entry); } } private void entryParseFailed(JadxZipEntry entry, Exception e) { if (isEncrypted(entry)) { throw new RuntimeException("Entry is encrypted, failed to decompress: " + entry, e); } if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) { throw new RuntimeException("Failed to decompress zip entry: " + entry + ", error: " + e.getMessage(), e); } LOG.warn("Entry '{}' parse failed, switching to fallback parser", entry, e); } @SuppressWarnings("resource") private IZipEntry useFallbackParser(JadxZipEntry entry) { LOG.debug("useFallbackParser used for {}", entry); IZipEntry zipEntry = initFallbackParser().searchEntry(entry.getName()); if (zipEntry == null) { throw new RuntimeException("Fallback parser can't find entry: " + entry); } return zipEntry; } @SuppressWarnings("resource") private ZipContent initFallbackParser() { if (fallbackZipContent == null) { try { fallbackZipContent = new FallbackZipParser(zipFile, options).open(); } catch (Exception e) { throw new RuntimeException("Fallback parser failed to open file: " + zipFile, e); } } return fallbackZipContent; } private boolean isEncrypted(JadxZipEntry entry) { int flags = readFlags(entry); return (flags & 1) != 0; } private int readFlags(JadxZipEntry entry) { ByteBuffer buf = byteBuffer; buf.position(entry.getEntryStart() + 6); return readU2(buf); } static byte[] bufferToBytes(ByteBuffer buf, int start, int size) { byte[] data = new byte[size]; buf.position(start); buf.get(data); return data; } static InputStream bufferToStream(ByteBuffer buf, int start, int size) { buf.position(start); ByteBuffer streamBuf = buf.slice(); streamBuf.limit(size); return new ByteBufferBackedInputStream(streamBuf); } private static int readU2(ByteBuffer buf) { return buf.getShort() & 0xFFFF; } private static String readString(ByteBuffer buf, int fileNameLen) { byte[] bytes = new byte[fileNameLen]; buf.get(bytes); return new String(bytes, StandardCharsets.UTF_8); } @Override public void close() throws IOException { try { if (fileChannel != null) { fileChannel.close(); } if (file != null) { file.close(); } if (fallbackZipContent != null) { fallbackZipContent.close(); } } finally { fileChannel = null; file = null; byteBuffer = null; endOfCDStart = -2; fallbackZipContent = null; } } public File getZipFile() { return zipFile; } @Override public String toString() { return "JadxZipParser{" + zipFile + '}'; } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/ZipDeflate.java ================================================ package jadx.zip.parser; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.zip.DataFormatException; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; import static jadx.zip.parser.JadxZipParser.bufferToStream; final class ZipDeflate { private static final int BUFFER_SIZE = 4096; static byte[] decompressEntryToBytes(ByteBuffer buf, JadxZipEntry entry) throws DataFormatException { buf.position(entry.getDataStart()); ByteBuffer entryBuf = buf.slice(); entryBuf.limit((int) entry.getCompressedSize()); if (entry.getUncompressedSize() > Integer.MAX_VALUE) { throw new DataFormatException("Entry too large: " + entry.getUncompressedSize()); } byte[] out = new byte[(int) entry.getUncompressedSize()]; Inflater inflater = new Inflater(true); inflater.setInput(entryBuf); int written = inflater.inflate(out); inflater.end(); if (written != out.length) { throw new DataFormatException("Unexpected size of decompressed entry: " + entry + ", got: " + written + ", expected: " + out.length); } return out; } static InputStream decompressEntryToStream(ByteBuffer buf, JadxZipEntry entry) { InputStream stream = bufferToStream(buf, entry.getDataStart(), (int) entry.getCompressedSize()); Inflater inflater = new Inflater(true); return new InflaterInputStream(stream, inflater, BUFFER_SIZE); } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/security/DisabledZipSecurity.java ================================================ package jadx.zip.security; import java.io.File; import jadx.zip.IZipEntry; public class DisabledZipSecurity implements IJadxZipSecurity { public static final DisabledZipSecurity INSTANCE = new DisabledZipSecurity(); @Override public boolean isValidEntry(IZipEntry entry) { return true; } @Override public boolean isValidEntryName(String entryName) { return true; } @Override public boolean isInSubDirectory(File baseDir, File file) { return true; } @Override public boolean useLimitedDataStream() { return false; } @Override public int getMaxEntriesCount() { return -1; } } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/security/IJadxZipSecurity.java ================================================ package jadx.zip.security; import java.io.File; import jadx.zip.IZipEntry; public interface IJadxZipSecurity { /** * Check if zip entry is valid and safe to process */ boolean isValidEntry(IZipEntry entry); /** * Check if the zip entry name is valid. * This check should be part of {@link #isValidEntry(IZipEntry)} method. */ boolean isValidEntryName(String entryName); /** * Use limited InputStream for entry uncompressed data */ boolean useLimitedDataStream(); /** * Max entries count expected in a zip file, fail zip open if the limit exceeds. * Return -1 to disable entries count check. */ int getMaxEntriesCount(); /** * Check if a file will be inside baseDir after a system resolves its path */ boolean isInSubDirectory(File baseDir, File file); } ================================================ FILE: jadx-commons/jadx-zip/src/main/java/jadx/zip/security/JadxZipSecurity.java ================================================ package jadx.zip.security; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.zip.IZipEntry; public class JadxZipSecurity implements IJadxZipSecurity { private static final Logger LOG = LoggerFactory.getLogger(JadxZipSecurity.class); private static final Path CWD = Paths.get(".").toAbsolutePath().normalize(); /** * The size of uncompressed zip entry shouldn't be bigger of compressed in zipBombDetectionFactor * times */ private int zipBombDetectionFactor = 100; /** * Zip entries that have an uncompressed size of less than zipBombMinUncompressedSize are considered * safe */ private int zipBombMinUncompressedSize = 25 * 1024 * 1024; private int maxEntriesCount = 100_000; private boolean useLimitedDataStream = true; @Override public boolean isValidEntry(IZipEntry entry) { return isValidEntryName(entry.getName()) && !isZipBomb(entry); } @Override public boolean useLimitedDataStream() { return useLimitedDataStream; } @Override public int getMaxEntriesCount() { return maxEntriesCount; } /** * Checks that entry name contains no any traversals and prevents cases like "../classes.dex", * to limit output only to the specified directory */ @Override public boolean isValidEntryName(String entryName) { if (entryName.contains("..")) { // quick pre-check if (entryName.contains("../") || entryName.contains("..\\")) { LOG.error("Path traversal attack detected in entry: '{}'", entryName); return false; } } // Path traversal check as presented on // https://www.heise.de/en/background/Secure-Coding-Best-practices-for-using-Java-NIO-against-path-traversal-9996787.html try { Path entryPath = CWD.resolve(entryName).normalize(); if (entryPath.startsWith(CWD)) { return true; } } catch (Exception e) { // check failed LOG.error("Invalid file name or path traversal attack detected: {} - error: {}", entryName, e.getMessage()); return false; } LOG.error("Invalid file name or path traversal attack detected: {}", entryName); return false; } @Override public boolean isInSubDirectory(File baseDir, File file) { try { return isInSubDirectoryInternal(baseDir.getCanonicalFile(), file.getCanonicalFile()); } catch (IOException e) { return false; } } public boolean isZipBomb(IZipEntry entry) { long compressedSize = entry.getCompressedSize(); long uncompressedSize = entry.getUncompressedSize(); boolean invalidSize = compressedSize < 0 || uncompressedSize < 0; boolean possibleZipBomb = uncompressedSize >= zipBombMinUncompressedSize && compressedSize * zipBombDetectionFactor < uncompressedSize; if (invalidSize || possibleZipBomb) { LOG.error("Potential zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}", compressedSize, uncompressedSize, entry.getName()); return true; } return false; } private static boolean isInSubDirectoryInternal(File baseDir, File file) { File current = file; while (true) { if (current == null) { return false; } if (current.equals(baseDir)) { return true; } current = current.getParentFile(); } } public void setMaxEntriesCount(int maxEntriesCount) { this.maxEntriesCount = maxEntriesCount; } public void setZipBombDetectionFactor(int zipBombDetectionFactor) { this.zipBombDetectionFactor = zipBombDetectionFactor; } public void setZipBombMinUncompressedSize(int zipBombMinUncompressedSize) { this.zipBombMinUncompressedSize = zipBombMinUncompressedSize; } public void setUseLimitedDataStream(boolean useLimitedDataStream) { this.useLimitedDataStream = useLimitedDataStream; } } ================================================ FILE: jadx-core/build.gradle.kts ================================================ plugins { id("jadx-library") } dependencies { api(project(":jadx-plugins:jadx-input-api")) api(project(":jadx-commons:jadx-zip")) implementation("com.google.code.gson:gson:2.13.2") testImplementation("org.apache.commons:commons-lang3:3.20.0") testImplementation(project(":jadx-plugins:jadx-dex-input")) // 'ClassNotFound' error is raised if set as 'testRuntime' // for the plugins below when running the tests from vscode. testImplementation(project(":jadx-plugins:jadx-smali-input")) testImplementation(project(":jadx-plugins:jadx-java-convert")) testImplementation(project(":jadx-plugins:jadx-java-input")) testImplementation(project(":jadx-plugins:jadx-raung-input")) testImplementation("org.eclipse.jdt:ecj") { version { prefer("3.33.0") strictly("[3.33, 3.34[") // from 3.34 compiled with Java 17 } } testImplementation("tools.profiler:async-profiler:4.2") } val jadxTestJavaVersion = getTestJavaVersion() fun getTestJavaVersion(): Int? { val envVarName = "JADX_TEST_JAVA_VERSION" val testJavaVer = System.getenv(envVarName)?.toInt() ?: return null val currentJavaVer = java.toolchain.languageVersion.get().asInt() if (testJavaVer < currentJavaVer) { throw GradleException("'$envVarName' can't be set to lower version than $currentJavaVer") } println("Set Java toolchain for core tests to version '$testJavaVer'") return testJavaVer } tasks.named("test") { jadxTestJavaVersion?.let { testJavaVer -> javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(testJavaVer) } } // disable cache to allow test's rerun, // because most tests are integration and depends on plugins and environment outputs.cacheIf { false } // exclude temp tests exclude("**/tmp/*") } ================================================ FILE: jadx-core/src/main/java/jadx/api/CommentsLevel.java ================================================ package jadx.api; public enum CommentsLevel { NONE, USER_ONLY, ERROR, WARN, INFO, DEBUG; public boolean filter(CommentsLevel limit) { return this.ordinal() <= limit.ordinal(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/DecompilationMode.java ================================================ package jadx.api; public enum DecompilationMode { /** * Trying best options (default) */ AUTO, /** * Restore code structure (normal java code) */ RESTRUCTURE, /** * Simplified instructions (linear with goto's) */ SIMPLE, /** * Raw instructions without modifications */ FALLBACK; public boolean isSpecial() { switch (this) { case AUTO: case RESTRUCTURE: return false; case SIMPLE: case FALLBACK: return true; default: throw new RuntimeException("Unexpected decompilation mode: " + this); } } } ================================================ FILE: jadx-core/src/main/java/jadx/api/ICodeCache.java ================================================ package jadx.api; import java.io.Closeable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public interface ICodeCache extends Closeable { void add(String clsFullName, ICodeInfo codeInfo); void remove(String clsFullName); @NotNull ICodeInfo get(String clsFullName); @Nullable String getCode(String clsFullName); boolean contains(String clsFullName); } ================================================ FILE: jadx-core/src/main/java/jadx/api/ICodeInfo.java ================================================ package jadx.api; import jadx.api.impl.SimpleCodeInfo; import jadx.api.metadata.ICodeMetadata; public interface ICodeInfo { ICodeInfo EMPTY = new SimpleCodeInfo(""); String getCodeStr(); ICodeMetadata getCodeMetadata(); boolean hasMetadata(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/ICodeWriter.java ================================================ package jadx.api; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; public interface ICodeWriter { boolean isMetadataSupported(); ICodeWriter startLine(); ICodeWriter startLine(char c); ICodeWriter startLine(String str); ICodeWriter startLineWithNum(int sourceLine); ICodeWriter addMultiLine(String str); ICodeWriter add(String str); ICodeWriter add(char c); ICodeWriter add(ICodeWriter code); ICodeWriter newLine(); ICodeWriter addIndent(); void incIndent(); void decIndent(); int getIndent(); void setIndent(int indent); /** * Return current line (only if metadata is supported) */ int getLine(); /** * Return start line position (only if metadata is supported) */ int getLineStartPos(); void attachDefinition(ICodeNodeRef obj); void attachAnnotation(ICodeAnnotation obj); void attachLineAnnotation(ICodeAnnotation obj); void attachSourceLine(int sourceLine); ICodeInfo finish(); String getCodeStr(); int getLength(); StringBuilder getRawBuf(); @ApiStatus.Internal Map getRawAnnotations(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/IDecompileScheduler.java ================================================ package jadx.api; import java.util.List; public interface IDecompileScheduler { List> buildBatches(List classes); } ================================================ FILE: jadx-core/src/main/java/jadx/api/JadxArgs.java ================================================ package jadx.api; import java.io.Closeable; import java.io.File; import java.nio.file.Path; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.args.GeneratedRenamesMappingFileMode; import jadx.api.args.IntegerFormat; import jadx.api.args.ResourceNameSource; import jadx.api.args.UseSourceNameAsClassNameAlias; import jadx.api.args.UserRenamesMappingsMode; import jadx.api.data.ICodeData; import jadx.api.deobf.IAliasProvider; import jadx.api.deobf.IRenameCondition; import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.InMemoryCodeCache; import jadx.api.plugins.loader.JadxBasePluginLoader; import jadx.api.plugins.loader.JadxPluginLoader; import jadx.api.security.IJadxSecurity; import jadx.api.security.JadxSecurityFlag; import jadx.api.security.impl.JadxSecurity; import jadx.api.usage.IUsageInfoCache; import jadx.api.usage.impl.InMemoryUsageInfoCache; import jadx.core.deobf.DeobfAliasProvider; import jadx.core.deobf.conditions.DeobfWhitelist; import jadx.core.deobf.conditions.JadxRenameConditions; import jadx.core.export.ExportGradleType; import jadx.core.plugins.PluginContext; import jadx.core.plugins.files.IJadxFilesGetter; import jadx.core.plugins.files.TempFilesGetter; import jadx.core.utils.files.FileUtils; public class JadxArgs implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(JadxArgs.class); public static final int DEFAULT_THREADS_COUNT = Math.max(1, Runtime.getRuntime().availableProcessors() / 2); public static final String DEFAULT_NEW_LINE_STR = System.lineSeparator(); public static final String DEFAULT_INDENT_STR = " "; public static final String DEFAULT_OUT_DIR = "jadx-output"; public static final String DEFAULT_SRC_DIR = "sources"; public static final String DEFAULT_RES_DIR = "resources"; private List inputFiles = new ArrayList<>(1); private File outDir; private File outDirSrc; private File outDirRes; private ICodeCache codeCache = new InMemoryCodeCache(); /** * Usage data cache. Saves use places of classes, methods and fields between code reloads. * Can be set to {@link jadx.api.usage.impl.EmptyUsageInfoCache} if code reload not needed. */ private IUsageInfoCache usageInfoCache = new InMemoryUsageInfoCache(); private Function codeWriterProvider = AnnotatedCodeWriter::new; private int threadsCount = DEFAULT_THREADS_COUNT; private boolean cfgOutput = false; private boolean rawCFGOutput = false; private boolean showInconsistentCode = false; private boolean useImports = true; private boolean debugInfo = true; private boolean insertDebugLines = false; private boolean extractFinally = true; private boolean inlineAnonymousClasses = true; private boolean inlineMethods = true; private boolean allowInlineKotlinLambda = true; private boolean moveInnerClasses = true; private boolean skipResources = false; private boolean skipSources = false; private boolean useHeadersForDetectResourceExtensions; /** * Predicate that allows to filter the classes to be process based on their full name */ private Predicate classFilter = null; /** * Save dependencies for classes accepted by {@code classFilter} */ private boolean includeDependencies = false; private Path userRenamesMappingsPath = null; private UserRenamesMappingsMode userRenamesMappingsMode = UserRenamesMappingsMode.getDefault(); private boolean deobfuscationOn = false; private UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.getDefault(); private int sourceNameRepeatLimit = 10; private File generatedRenamesMappingFile = null; private GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.getDefault(); private ResourceNameSource resourceNameSource = ResourceNameSource.AUTO; private int deobfuscationMinLength = 0; private int deobfuscationMaxLength = Integer.MAX_VALUE; /** * List of classes and packages (ends with '.*') to exclude from deobfuscation */ private List deobfuscationWhitelist = DeobfWhitelist.DEFAULT_LIST; /** * Nodes alias provider for deobfuscator and rename visitor */ private IAliasProvider aliasProvider = new DeobfAliasProvider(); /** * Condition to rename node in deobfuscator */ private IRenameCondition renameCondition = JadxRenameConditions.buildDefault(); private boolean escapeUnicode = false; private boolean replaceConsts = true; private boolean respectBytecodeAccModifiers = false; private @Nullable ExportGradleType exportGradleType = null; private boolean restoreSwitchOverString = true; private boolean skipXmlPrettyPrint = false; private boolean fsCaseSensitive; public enum RenameEnum { CASE, VALID, PRINTABLE } private Set renameFlags = EnumSet.allOf(RenameEnum.class); public enum OutputFormatEnum { JAVA, JSON } private OutputFormatEnum outputFormat = OutputFormatEnum.JAVA; private DecompilationMode decompilationMode = DecompilationMode.AUTO; private ICodeData codeData; private String codeNewLineStr = DEFAULT_NEW_LINE_STR; private String codeIndentStr = DEFAULT_INDENT_STR; private CommentsLevel commentsLevel = CommentsLevel.INFO; private IntegerFormat integerFormat = IntegerFormat.AUTO; /** * Maximum updates allowed total in method per one instruction. * Should be more or equal 1, default value is 10. */ private int typeUpdatesLimitCount = 10; private boolean useDxInput = false; public enum UseKotlinMethodsForVarNames { DISABLE, APPLY, APPLY_AND_HIDE } private UseKotlinMethodsForVarNames useKotlinMethodsForVarNames = UseKotlinMethodsForVarNames.APPLY; /** * Additional files structure info. * Defaults to tmp dirs. */ private IJadxFilesGetter filesGetter = TempFilesGetter.INSTANCE; /** * Additional data validation and security checks */ private IJadxSecurity security = new JadxSecurity(JadxSecurityFlag.all()); /** * Don't save files (can be using for performance testing) */ private boolean skipFilesSave = false; /** * Run additional expensive checks to verify internal invariants and info integrity */ private boolean runDebugChecks = false; /** * Passes to exclude from processing. */ private final List disabledPasses = new ArrayList<>(); private Map pluginOptions = new HashMap<>(); private Set disabledPlugins = new HashSet<>(); private JadxPluginLoader pluginLoader = new JadxBasePluginLoader(); private boolean loadJadxClsSetFile = true; public JadxArgs() { // use default options } public void setRootDir(File rootDir) { setOutDir(rootDir); setOutDirSrc(new File(rootDir, DEFAULT_SRC_DIR)); setOutDirRes(new File(rootDir, DEFAULT_RES_DIR)); } @Override public void close() { try { inputFiles = null; if (codeCache != null) { codeCache.close(); } if (usageInfoCache != null) { usageInfoCache.close(); } if (pluginLoader != null) { pluginLoader.close(); } } catch (Exception e) { LOG.error("Failed to close JadxArgs", e); } finally { codeCache = null; usageInfoCache = null; } } public List getInputFiles() { return inputFiles; } public void addInputFile(File inputFile) { this.inputFiles.add(inputFile); } public void setInputFile(File inputFile) { addInputFile(inputFile); } public void setInputFiles(List inputFiles) { this.inputFiles = inputFiles; } public File getOutDir() { return outDir; } public void setOutDir(File outDir) { this.outDir = outDir; } public File getOutDirSrc() { return outDirSrc; } public void setOutDirSrc(File outDirSrc) { this.outDirSrc = outDirSrc; } public File getOutDirRes() { return outDirRes; } public void setOutDirRes(File outDirRes) { this.outDirRes = outDirRes; } public int getThreadsCount() { return threadsCount; } public void setThreadsCount(int threadsCount) { this.threadsCount = Math.max(1, threadsCount); // make sure threadsCount >= 1 } public boolean isCfgOutput() { return cfgOutput; } public void setCfgOutput(boolean cfgOutput) { this.cfgOutput = cfgOutput; } public boolean isRawCFGOutput() { return rawCFGOutput; } public void setRawCFGOutput(boolean rawCFGOutput) { this.rawCFGOutput = rawCFGOutput; } public boolean isFallbackMode() { return decompilationMode == DecompilationMode.FALLBACK; } /** * Deprecated: use 'decompilation mode' property */ @Deprecated public void setFallbackMode(boolean fallbackMode) { if (fallbackMode) { this.decompilationMode = DecompilationMode.FALLBACK; } } public boolean isShowInconsistentCode() { return showInconsistentCode; } public void setShowInconsistentCode(boolean showInconsistentCode) { this.showInconsistentCode = showInconsistentCode; } public boolean isUseImports() { return useImports; } public void setUseImports(boolean useImports) { this.useImports = useImports; } public boolean isDebugInfo() { return debugInfo; } public void setDebugInfo(boolean debugInfo) { this.debugInfo = debugInfo; } public boolean isInsertDebugLines() { return insertDebugLines; } public void setInsertDebugLines(boolean insertDebugLines) { this.insertDebugLines = insertDebugLines; } public boolean isInlineAnonymousClasses() { return inlineAnonymousClasses; } public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) { this.inlineAnonymousClasses = inlineAnonymousClasses; } public boolean isInlineMethods() { return inlineMethods; } public void setInlineMethods(boolean inlineMethods) { this.inlineMethods = inlineMethods; } public boolean isAllowInlineKotlinLambda() { return allowInlineKotlinLambda; } public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) { this.allowInlineKotlinLambda = allowInlineKotlinLambda; } public boolean isMoveInnerClasses() { return moveInnerClasses; } public void setMoveInnerClasses(boolean moveInnerClasses) { this.moveInnerClasses = moveInnerClasses; } public boolean isExtractFinally() { return extractFinally; } public void setExtractFinally(boolean extractFinally) { this.extractFinally = extractFinally; } public boolean isSkipResources() { return skipResources; } public void setSkipResources(boolean skipResources) { this.skipResources = skipResources; } public boolean isSkipSources() { return skipSources; } public void setSkipSources(boolean skipSources) { this.skipSources = skipSources; } public void setIncludeDependencies(boolean includeDependencies) { this.includeDependencies = includeDependencies; } public boolean isIncludeDependencies() { return includeDependencies; } public Predicate getClassFilter() { return classFilter; } public void setClassFilter(Predicate classFilter) { this.classFilter = classFilter; } public Path getUserRenamesMappingsPath() { return userRenamesMappingsPath; } public void setUserRenamesMappingsPath(Path path) { this.userRenamesMappingsPath = path; } public UserRenamesMappingsMode getUserRenamesMappingsMode() { return userRenamesMappingsMode; } public void setUserRenamesMappingsMode(UserRenamesMappingsMode mode) { this.userRenamesMappingsMode = mode; } public boolean isDeobfuscationOn() { return deobfuscationOn; } public void setDeobfuscationOn(boolean deobfuscationOn) { this.deobfuscationOn = deobfuscationOn; } public boolean isDeobfuscationForceSave() { return generatedRenamesMappingFileMode == GeneratedRenamesMappingFileMode.OVERWRITE; } public void setDeobfuscationForceSave(boolean deobfuscationForceSave) { if (deobfuscationForceSave) { this.generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.OVERWRITE; } } public GeneratedRenamesMappingFileMode getGeneratedRenamesMappingFileMode() { return generatedRenamesMappingFileMode; } public void setGeneratedRenamesMappingFileMode(GeneratedRenamesMappingFileMode mode) { this.generatedRenamesMappingFileMode = mode; } public UseSourceNameAsClassNameAlias getUseSourceNameAsClassNameAlias() { return useSourceNameAsClassNameAlias; } public void setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias) { this.useSourceNameAsClassNameAlias = useSourceNameAsClassNameAlias; } public int getSourceNameRepeatLimit() { return sourceNameRepeatLimit; } public void setSourceNameRepeatLimit(int sourceNameRepeatLimit) { this.sourceNameRepeatLimit = sourceNameRepeatLimit; } /** * @deprecated Use {@link #getUseSourceNameAsClassNameAlias()} instead. */ @Deprecated public boolean isUseSourceNameAsClassAlias() { return getUseSourceNameAsClassNameAlias().toBoolean(); } /** * @deprecated Use {@link #setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias)} instead. */ @Deprecated public void setUseSourceNameAsClassAlias(boolean useSourceNameAsClassAlias) { final var useSourceNameAsClassNameAlias = UseSourceNameAsClassNameAlias.create(useSourceNameAsClassAlias); setUseSourceNameAsClassNameAlias(useSourceNameAsClassNameAlias); } public int getDeobfuscationMinLength() { return deobfuscationMinLength; } public void setDeobfuscationMinLength(int deobfuscationMinLength) { this.deobfuscationMinLength = deobfuscationMinLength; } public int getDeobfuscationMaxLength() { return deobfuscationMaxLength; } public void setDeobfuscationMaxLength(int deobfuscationMaxLength) { this.deobfuscationMaxLength = deobfuscationMaxLength; } public List getDeobfuscationWhitelist() { return this.deobfuscationWhitelist; } public void setDeobfuscationWhitelist(List deobfuscationWhitelist) { this.deobfuscationWhitelist = deobfuscationWhitelist; } public File getGeneratedRenamesMappingFile() { return generatedRenamesMappingFile; } public void setGeneratedRenamesMappingFile(File file) { this.generatedRenamesMappingFile = file; } public ResourceNameSource getResourceNameSource() { return resourceNameSource; } public void setResourceNameSource(ResourceNameSource resourceNameSource) { this.resourceNameSource = resourceNameSource; } public IAliasProvider getAliasProvider() { return aliasProvider; } public void setAliasProvider(IAliasProvider aliasProvider) { this.aliasProvider = aliasProvider; } public IRenameCondition getRenameCondition() { return renameCondition; } public void setRenameCondition(IRenameCondition renameCondition) { this.renameCondition = renameCondition; } public boolean isEscapeUnicode() { return escapeUnicode; } public void setEscapeUnicode(boolean escapeUnicode) { this.escapeUnicode = escapeUnicode; } public boolean isReplaceConsts() { return replaceConsts; } public void setReplaceConsts(boolean replaceConsts) { this.replaceConsts = replaceConsts; } public boolean isRespectBytecodeAccModifiers() { return respectBytecodeAccModifiers; } public void setRespectBytecodeAccModifiers(boolean respectBytecodeAccModifiers) { this.respectBytecodeAccModifiers = respectBytecodeAccModifiers; } public boolean isExportAsGradleProject() { return exportGradleType != null; } public void setExportAsGradleProject(boolean exportAsGradleProject) { if (exportAsGradleProject) { if (exportGradleType == null) { exportGradleType = ExportGradleType.AUTO; } } else { exportGradleType = null; } } public @Nullable ExportGradleType getExportGradleType() { return exportGradleType; } public void setExportGradleType(@Nullable ExportGradleType exportGradleType) { this.exportGradleType = exportGradleType; } public boolean isRestoreSwitchOverString() { return restoreSwitchOverString; } public void setRestoreSwitchOverString(boolean restoreSwitchOverString) { this.restoreSwitchOverString = restoreSwitchOverString; } public boolean isSkipXmlPrettyPrint() { return skipXmlPrettyPrint; } public void setSkipXmlPrettyPrint(boolean skipXmlPrettyPrint) { this.skipXmlPrettyPrint = skipXmlPrettyPrint; } public boolean isFsCaseSensitive() { return fsCaseSensitive; } public void setFsCaseSensitive(boolean fsCaseSensitive) { this.fsCaseSensitive = fsCaseSensitive; } public boolean isRenameCaseSensitive() { return renameFlags.contains(RenameEnum.CASE); } public void setRenameCaseSensitive(boolean renameCaseSensitive) { updateRenameFlag(renameCaseSensitive, RenameEnum.CASE); } public boolean isRenameValid() { return renameFlags.contains(RenameEnum.VALID); } public void setRenameValid(boolean renameValid) { updateRenameFlag(renameValid, RenameEnum.VALID); } public boolean isRenamePrintable() { return renameFlags.contains(RenameEnum.PRINTABLE); } public void setRenamePrintable(boolean renamePrintable) { updateRenameFlag(renamePrintable, RenameEnum.PRINTABLE); } private void updateRenameFlag(boolean enabled, RenameEnum flag) { if (enabled) { renameFlags.add(flag); } else { renameFlags.remove(flag); } } public void setRenameFlags(Set renameFlags) { this.renameFlags = renameFlags; } public Set getRenameFlags() { return renameFlags; } public OutputFormatEnum getOutputFormat() { return outputFormat; } public boolean isJsonOutput() { return outputFormat == OutputFormatEnum.JSON; } public void setOutputFormat(OutputFormatEnum outputFormat) { this.outputFormat = outputFormat; } public DecompilationMode getDecompilationMode() { return decompilationMode; } public void setDecompilationMode(DecompilationMode decompilationMode) { this.decompilationMode = decompilationMode; } public ICodeCache getCodeCache() { return codeCache; } public void setCodeCache(ICodeCache codeCache) { this.codeCache = codeCache; } public Function getCodeWriterProvider() { return codeWriterProvider; } public void setCodeWriterProvider(Function codeWriterProvider) { this.codeWriterProvider = codeWriterProvider; } public IUsageInfoCache getUsageInfoCache() { return usageInfoCache; } public void setUsageInfoCache(IUsageInfoCache usageInfoCache) { this.usageInfoCache = usageInfoCache; } public ICodeData getCodeData() { return codeData; } public void setCodeData(ICodeData codeData) { this.codeData = codeData; } public String getCodeIndentStr() { return codeIndentStr; } public void setCodeIndentStr(String codeIndentStr) { this.codeIndentStr = codeIndentStr; } public String getCodeNewLineStr() { return codeNewLineStr; } public void setCodeNewLineStr(String codeNewLineStr) { this.codeNewLineStr = codeNewLineStr; } public CommentsLevel getCommentsLevel() { return commentsLevel; } public void setCommentsLevel(CommentsLevel commentsLevel) { this.commentsLevel = commentsLevel; } public IntegerFormat getIntegerFormat() { return integerFormat; } public void setIntegerFormat(IntegerFormat format) { this.integerFormat = format; } public int getTypeUpdatesLimitCount() { return typeUpdatesLimitCount; } public void setTypeUpdatesLimitCount(int typeUpdatesLimitCount) { this.typeUpdatesLimitCount = Math.max(1, typeUpdatesLimitCount); } public boolean isUseDxInput() { return useDxInput; } public void setUseDxInput(boolean useDxInput) { this.useDxInput = useDxInput; } public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() { return useKotlinMethodsForVarNames; } public void setUseKotlinMethodsForVarNames(UseKotlinMethodsForVarNames useKotlinMethodsForVarNames) { this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames; } public IJadxFilesGetter getFilesGetter() { return filesGetter; } public void setFilesGetter(IJadxFilesGetter filesGetter) { this.filesGetter = filesGetter; } public IJadxSecurity getSecurity() { return security; } public void setSecurity(IJadxSecurity security) { this.security = security; } public boolean isSkipFilesSave() { return skipFilesSave; } public void setSkipFilesSave(boolean skipFilesSave) { this.skipFilesSave = skipFilesSave; } public boolean isRunDebugChecks() { return runDebugChecks; } public void setRunDebugChecks(boolean runDebugChecks) { this.runDebugChecks = runDebugChecks; } public List getDisabledPasses() { return disabledPasses; } public Map getPluginOptions() { return pluginOptions; } public void setPluginOptions(Map pluginOptions) { this.pluginOptions = pluginOptions; } public Set getDisabledPlugins() { return disabledPlugins; } public void setDisabledPlugins(Set disabledPlugins) { this.disabledPlugins = disabledPlugins; } public JadxPluginLoader getPluginLoader() { return pluginLoader; } public void setPluginLoader(JadxPluginLoader pluginLoader) { this.pluginLoader = pluginLoader; } public boolean isLoadJadxClsSetFile() { return loadJadxClsSetFile; } public void setLoadJadxClsSetFile(boolean loadJadxClsSetFile) { this.loadJadxClsSetFile = loadJadxClsSetFile; } public void setUseHeadersForDetectResourceExtensions(boolean useHeadersForDetectResourceExtensions) { this.useHeadersForDetectResourceExtensions = useHeadersForDetectResourceExtensions; } public boolean isUseHeadersForDetectResourceExtensions() { return useHeadersForDetectResourceExtensions; } /** * Hash of all options that can change result code */ public String makeCodeArgsHash(@Nullable JadxDecompiler decompiler) { String argStr = "args:" + decompilationMode + useImports + showInconsistentCode + inlineAnonymousClasses + inlineMethods + moveInnerClasses + allowInlineKotlinLambda + deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength + deobfuscationWhitelist + useSourceNameAsClassNameAlias + sourceNameRepeatLimit + resourceNameSource + useHeadersForDetectResourceExtensions + useKotlinMethodsForVarNames + insertDebugLines + extractFinally + debugInfo + escapeUnicode + replaceConsts + restoreSwitchOverString + respectBytecodeAccModifiers + fsCaseSensitive + renameFlags + commentsLevel + useDxInput + integerFormat + typeUpdatesLimitCount + "|" + buildPluginsHash(decompiler); return FileUtils.md5Sum(argStr); } private static String buildPluginsHash(@Nullable JadxDecompiler decompiler) { if (decompiler == null) { return ""; } return decompiler.getPluginManager().getResolvedPluginContexts() .stream() .map(PluginContext::getInputsHash) .collect(Collectors.joining(":")); } @Override public String toString() { return "JadxArgs{" + "inputFiles=" + inputFiles + ", outDir=" + outDir + ", outDirSrc=" + outDirSrc + ", outDirRes=" + outDirRes + ", threadsCount=" + threadsCount + ", decompilationMode=" + decompilationMode + ", showInconsistentCode=" + showInconsistentCode + ", useImports=" + useImports + ", skipResources=" + skipResources + ", skipSources=" + skipSources + ", includeDependencies=" + includeDependencies + ", userRenamesMappingsPath=" + userRenamesMappingsPath + ", userRenamesMappingsMode=" + userRenamesMappingsMode + ", deobfuscationOn=" + deobfuscationOn + ", generatedRenamesMappingFile=" + generatedRenamesMappingFile + ", generatedRenamesMappingFileMode=" + generatedRenamesMappingFileMode + ", resourceNameSource=" + resourceNameSource + ", useSourceNameAsClassNameAlias=" + useSourceNameAsClassNameAlias + ", sourceNameRepeatLimit=" + sourceNameRepeatLimit + ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames + ", insertDebugLines=" + insertDebugLines + ", extractFinally=" + extractFinally + ", deobfuscationMinLength=" + deobfuscationMinLength + ", deobfuscationMaxLength=" + deobfuscationMaxLength + ", deobfuscationWhitelist=" + deobfuscationWhitelist + ", escapeUnicode=" + escapeUnicode + ", replaceConsts=" + replaceConsts + ", restoreSwitchOverString=" + restoreSwitchOverString + ", respectBytecodeAccModifiers=" + respectBytecodeAccModifiers + ", exportGradleType=" + exportGradleType + ", skipXmlPrettyPrint=" + skipXmlPrettyPrint + ", fsCaseSensitive=" + fsCaseSensitive + ", renameFlags=" + renameFlags + ", outputFormat=" + outputFormat + ", commentsLevel=" + commentsLevel + ", codeCache=" + codeCache + ", codeWriter=" + codeWriterProvider.apply(this).getClass().getSimpleName() + ", useDxInput=" + useDxInput + ", pluginOptions=" + pluginOptions + ", cfgOutput=" + cfgOutput + ", rawCFGOutput=" + rawCFGOutput + ", useHeadersForDetectResourceExtensions=" + useHeadersForDetectResourceExtensions + ", typeUpdatesLimitCount=" + typeUpdatesLimitCount + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/JadxArgsValidator.java ================================================ package jadx.api; import java.io.File; import java.util.List; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.exceptions.JadxArgsValidateException; public class JadxArgsValidator { private static final Logger LOG = LoggerFactory.getLogger(JadxArgsValidator.class); public static void validate(JadxDecompiler jadx) { JadxArgs args = jadx.getArgs(); checkInputFiles(jadx, args); validateOutDirs(args); if (LOG.isDebugEnabled()) { LOG.debug("Effective jadx args: {}", args); } } private static void checkInputFiles(JadxDecompiler jadx, JadxArgs args) { List inputFiles = args.getInputFiles(); if (inputFiles.isEmpty() && jadx.getCustomCodeLoaders().isEmpty()) { throw new JadxArgsValidateException("Please specify input file"); } for (File file : inputFiles) { checkFile(file); } } private static void validateOutDirs(JadxArgs args) { File outDir = args.getOutDir(); File srcDir = args.getOutDirSrc(); File resDir = args.getOutDirRes(); if (outDir == null) { if (srcDir != null) { outDir = srcDir; } else if (resDir != null) { outDir = resDir; } else { outDir = makeDirFromInput(args); } args.setOutDir(outDir); } if (srcDir == null) { args.setOutDirSrc(new File(args.getOutDir(), JadxArgs.DEFAULT_SRC_DIR)); } if (resDir == null) { args.setOutDirRes(new File(args.getOutDir(), JadxArgs.DEFAULT_RES_DIR)); } checkDir(args.getOutDir(), "Output"); checkDir(args.getOutDirSrc(), "Source output"); checkDir(args.getOutDirRes(), "Resources output"); } @NotNull private static File makeDirFromInput(JadxArgs args) { String outDirName; List inputFiles = args.getInputFiles(); if (inputFiles.isEmpty()) { outDirName = JadxArgs.DEFAULT_OUT_DIR; } else { File file = inputFiles.get(0); String name = file.getName(); int pos = name.lastIndexOf('.'); if (pos != -1) { outDirName = name.substring(0, pos); } else { outDirName = name + '-' + JadxArgs.DEFAULT_OUT_DIR; } } LOG.info("output directory: {}", outDirName); return new File(outDirName); } private static void checkFile(File file) { if (!file.exists()) { throw new JadxArgsValidateException("File not found " + file.getAbsolutePath()); } } private static void checkDir(File dir, String desc) { if (dir != null && dir.exists() && !dir.isDirectory()) { throw new JadxArgsValidateException(desc + " directory exists as file " + dir); } } private JadxArgsValidator() { } } ================================================ FILE: jadx-core/src/main/java/jadx/api/JadxDecompiler.java ================================================ package jadx.api; import java.io.Closeable; import java.io.File; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.function.Predicate; import java.util.stream.Collectors; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.api.metadata.annotations.VarNode; import jadx.api.metadata.annotations.VarRef; import jadx.api.plugins.CustomResourcesLoader; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.events.IJadxEvents; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.pass.JadxPass; import jadx.api.plugins.pass.types.JadxAfterLoadPass; import jadx.api.plugins.pass.types.JadxPassType; import jadx.api.utils.tasks.ITaskExecutor; import jadx.core.Jadx; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.SaveCode; import jadx.core.export.ExportGradle; import jadx.core.export.OutDirs; import jadx.core.plugins.JadxPluginManager; import jadx.core.plugins.PluginContext; import jadx.core.plugins.events.JadxEventsImpl; import jadx.core.utils.DecompilerScheduler; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.core.utils.tasks.TaskExecutor; import jadx.core.xmlgen.ResourcesSaver; import jadx.zip.ZipReader; /** * Jadx API usage example: * *
 * 
 *
 * JadxArgs args = new JadxArgs();
 * args.getInputFiles().add(new File("test.apk"));
 * args.setOutDir(new File("jadx-test-output"));
 * try (JadxDecompiler jadx = new JadxDecompiler(args)) {
 *    jadx.load();
 *    jadx.save();
 * }
 * 
 * 
*

* Instead of 'save()' you can iterate over decompiled classes: * *

 * 
 *
 *  for(JavaClass cls : jadx.getClasses()) {
 *      System.out.println(cls.getCode());
 *  }
 * 
 * 
*/ public final class JadxDecompiler implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(JadxDecompiler.class); private final JadxArgs args; private final JadxPluginManager pluginManager; private final List loadedInputs = new ArrayList<>(); private final ZipReader zipReader; private RootNode root; private List classes; private List resources; private final IDecompileScheduler decompileScheduler = new DecompilerScheduler(); private final ResourcesLoader resourcesLoader; private final List customCodeLoaders = new ArrayList<>(); private final List customResourcesLoaders = new ArrayList<>(); private final Map> customPasses = new HashMap<>(); private final List closeableList = new ArrayList<>(); private IJadxEvents events = new JadxEventsImpl(); public JadxDecompiler() { this(new JadxArgs()); } public JadxDecompiler(JadxArgs args) { this.args = Objects.requireNonNull(args); this.pluginManager = new JadxPluginManager(this); this.resourcesLoader = new ResourcesLoader(this); this.zipReader = new ZipReader(args.getSecurity()); } public void load() { reset(); JadxArgsValidator.validate(this); LOG.info("loading ..."); FileUtils.updateTempRootDir(args.getFilesGetter().getTempDir()); loadPlugins(); loadInputFiles(); root = new RootNode(this); root.init(); // load classes and resources root.loadClasses(loadedInputs); root.loadResources(resourcesLoader, getResources()); root.finishClassLoad(); root.initClassPath(); // init passes root.mergePasses(customPasses); root.runPreDecompileStage(); root.initPasses(); loadFinished(); } /** * Reload passes and plugins without processing classes and inputs */ public void reloadPasses() { LOG.info("reloading (passes only) ..."); customPasses.clear(); root.resetPasses(); events.reset(); unloadPlugins(); loadPlugins(); root.mergePasses(customPasses); root.restartVisitors(); root.initPasses(); loadFinished(); } private void loadInputFiles() { loadedInputs.clear(); List inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath); List inputFiles = FileUtils.expandDirs(inputPaths); long start = System.currentTimeMillis(); for (PluginContext plugin : pluginManager.getResolvedPluginContexts()) { for (JadxCodeInput codeLoader : plugin.getCodeInputs()) { try { ICodeLoader loader = codeLoader.loadFiles(inputFiles); if (loader != null && !loader.isEmpty()) { loadedInputs.add(loader); } } catch (Exception e) { LOG.warn("Failed to load code for plugin: {}", plugin, e); } } } loadedInputs.addAll(customCodeLoaders); if (LOG.isDebugEnabled()) { LOG.debug("Loaded using {} inputs plugin in {} ms", loadedInputs.size(), System.currentTimeMillis() - start); } } private void reset() { unloadPlugins(); root = null; classes = null; resources = null; events.reset(); } @Override public void close() { reset(); closeAll(loadedInputs); closeAll(customCodeLoaders); closeAll(customResourcesLoaders); closeAll(closeableList); FileUtils.deleteDirIfExists(args.getFilesGetter().getTempDir()); args.close(); FileUtils.clearTempRootDir(); } private void closeAll(List list) { try { for (Closeable closeable : list) { try { closeable.close(); } catch (Exception e) { LOG.warn("Fail to close '{}'", closeable, e); } } } finally { list.clear(); } } private void loadPlugins() { pluginManager.providesSuggestion("java-input", args.isUseDxInput() ? "java-convert" : "java-input"); pluginManager.load(args.getPluginLoader()); if (LOG.isDebugEnabled()) { LOG.debug("Resolved plugins: {}", pluginManager.getResolvedPluginContexts()); } pluginManager.initResolved(); if (LOG.isDebugEnabled()) { List passes = customPasses.values().stream().flatMap(Collection::stream) .map(p -> p.getInfo().getName()).collect(Collectors.toList()); LOG.debug("Loaded custom passes: {} {}", passes.size(), passes); } } private void unloadPlugins() { pluginManager.unloadResolved(); } private void loadFinished() { LOG.debug("Load finished"); List list = customPasses.get(JadxAfterLoadPass.TYPE); if (list != null) { for (JadxPass pass : list) { ((JadxAfterLoadPass) pass).init(this); } } } @SuppressWarnings("unused") public void registerPlugin(JadxPlugin plugin) { pluginManager.register(plugin); } public static String getVersion() { return Jadx.getVersion(); } public void save() { save(!args.isSkipSources(), !args.isSkipResources()); } public interface ProgressListener { void progress(long done, long total); } @SuppressWarnings("BusyWait") public void save(int intervalInMillis, ProgressListener listener) { try { ITaskExecutor tasks = getSaveTaskExecutor(); tasks.execute(); long total = tasks.getTasksCount(); while (tasks.isRunning()) { listener.progress(tasks.getProgress(), total); Thread.sleep(intervalInMillis); } } catch (InterruptedException e) { LOG.error("Save interrupted", e); Thread.currentThread().interrupt(); } } public void saveSources() { save(true, false); } public void saveResources() { save(false, true); } private void save(boolean saveSources, boolean saveResources) { ITaskExecutor executor = getSaveTasks(saveSources, saveResources); executor.execute(); executor.awaitTermination(); } public ITaskExecutor getSaveTaskExecutor() { return getSaveTasks(!args.isSkipSources(), !args.isSkipResources()); } @Deprecated(forRemoval = true) public ExecutorService getSaveExecutor() { ITaskExecutor executor = getSaveTaskExecutor(); executor.execute(); return executor.getInternalExecutor(); } @Deprecated(forRemoval = true) public List getSaveTasks() { return Collections.singletonList(this::save); } private TaskExecutor getSaveTasks(boolean saveSources, boolean saveResources) { if (root == null) { throw new JadxRuntimeException("No loaded files"); } OutDirs outDirs; ExportGradle gradleExport; if (args.getExportGradleType() != null) { gradleExport = new ExportGradle(root, args.getOutDir(), getResources()); outDirs = gradleExport.init(); } else { gradleExport = null; outDirs = new OutDirs(args.getOutDirSrc(), args.getOutDirRes()); outDirs.makeDirs(); } TaskExecutor executor = new TaskExecutor(); executor.setThreadsCount(args.getThreadsCount()); if (saveResources) { // save resources first because decompilation can stop or fail appendResourcesSaveTasks(executor, outDirs.getResOutDir()); } if (saveSources) { appendSourcesSave(executor, outDirs.getSrcOutDir()); } if (gradleExport != null) { executor.addSequentialTask(gradleExport::generateGradleFiles); } return executor; } private void appendResourcesSaveTasks(ITaskExecutor executor, File outDir) { if (args.isSkipFilesSave()) { return; } // process AndroidManifest.xml first to load complete resource ids table for (ResourceFile resourceFile : getResources()) { if (resourceFile.getType() == ResourceType.MANIFEST) { new ResourcesSaver(this, outDir, resourceFile).run(); break; } } Set inputFileNames = args.getInputFiles().stream() .map(File::getAbsolutePath) .collect(Collectors.toSet()); Set codeSources = collectCodeSources(); List tasks = new ArrayList<>(); for (ResourceFile resourceFile : getResources()) { ResourceType resType = resourceFile.getType(); if (resType == ResourceType.MANIFEST) { // already processed continue; } String resOriginalName = resourceFile.getOriginalName(); if (resType != ResourceType.ARSC && inputFileNames.contains(resOriginalName)) { // ignore resource made from an input file continue; } if (codeSources.contains(resOriginalName)) { // don't output code source resources (.dex, .class, etc) // do not trust file extensions, use only sources set as class inputs continue; } tasks.add(new ResourcesSaver(this, outDir, resourceFile)); } executor.addParallelTasks(tasks); } private Set collectCodeSources() { Set set = new HashSet<>(); for (ClassNode cls : root.getClasses(true)) { if (cls.getClsData() == null) { // exclude synthetic classes continue; } String inputFileName = cls.getInputFileName(); if (inputFileName.endsWith(".class")) { // cut .class name to get source .jar file // current template: ":<.jar>:" // TODO: add property to set file name or reference to resource name int endIdx = inputFileName.lastIndexOf(':'); if (endIdx != -1) { int startIdx = inputFileName.lastIndexOf(':', endIdx - 1) + 1; inputFileName = inputFileName.substring(startIdx, endIdx); } } set.add(inputFileName); } return set; } private void appendSourcesSave(ITaskExecutor executor, File outDir) { List classes = getClasses(); List processQueue = filterClasses(classes); List> batches; try { batches = decompileScheduler.buildBatches(processQueue); } catch (Exception e) { throw new JadxRuntimeException("Decompilation batches build failed", e); } List decompileTasks = new ArrayList<>(batches.size()); for (List decompileBatch : batches) { decompileTasks.add(() -> { for (JavaClass cls : decompileBatch) { try { ClassNode clsNode = cls.getClassNode(); ICodeInfo code = clsNode.getCode(); SaveCode.save(outDir, clsNode, code); } catch (Exception e) { LOG.error("Error saving class: {}", cls, e); } } }); } executor.addParallelTasks(decompileTasks); } private List filterClasses(List classes) { Predicate classFilter = args.getClassFilter(); List list = new ArrayList<>(classes.size()); for (JavaClass cls : classes) { ClassNode clsNode = cls.getClassNode(); if (clsNode.contains(AFlag.DONT_GENERATE)) { continue; } if (classFilter != null && !classFilter.test(clsNode.getClassInfo().getFullName())) { if (!args.isIncludeDependencies()) { clsNode.add(AFlag.DONT_GENERATE); } continue; } list.add(cls); } return list; } public synchronized List getClasses() { if (root == null) { return Collections.emptyList(); } if (classes == null) { List classNodeList = root.getClasses(); List clsList = new ArrayList<>(classNodeList.size()); for (ClassNode classNode : classNodeList) { if (!classNode.contains(AFlag.DONT_GENERATE) && !classNode.isInner()) { clsList.add(convertClassNode(classNode)); } } classes = Collections.unmodifiableList(clsList); } return classes; } public List getClassesWithInners() { return Utils.collectionMap(root.getClasses(), this::convertClassNode); } public synchronized List getResources() { if (resources == null) { if (root == null) { return Collections.emptyList(); } resources = resourcesLoader.load(root); } return resources; } public List getPackages() { return Utils.collectionMap(root.getPackages(), this::convertPackageNode); } public int getErrorsCount() { if (root == null) { return 0; } return root.getErrorsCounter().getErrorCount(); } public int getWarnsCount() { if (root == null) { return 0; } return root.getErrorsCounter().getWarnsCount(); } public void printErrorsReport() { if (root == null) { return; } root.getClsp().printMissingClasses(); root.getErrorsCounter().printReport(); } /** * Internal API. Not Stable! */ @ApiStatus.Internal public RootNode getRoot() { return root; } /** * Get JavaClass by ClassNode without loading and decompilation */ @ApiStatus.Internal synchronized JavaClass convertClassNode(ClassNode cls) { JavaClass javaClass = cls.getJavaNode(); if (javaClass == null) { javaClass = cls.isInner() ? new JavaClass(cls, convertClassNode(cls.getParentClass())) : new JavaClass(cls, this); cls.setJavaNode(javaClass); } return javaClass; } @ApiStatus.Internal synchronized JavaField convertFieldNode(FieldNode fld) { JavaField javaField = fld.getJavaNode(); if (javaField == null) { JavaClass parentCls = convertClassNode(fld.getParentClass()); javaField = new JavaField(parentCls, fld); fld.setJavaNode(javaField); } return javaField; } @ApiStatus.Internal synchronized JavaMethod convertMethodNode(MethodNode mth) { JavaMethod javaMethod = mth.getJavaNode(); if (javaMethod == null) { javaMethod = new JavaMethod(convertClassNode(mth.getParentClass()), mth); mth.setJavaNode(javaMethod); } return javaMethod; } @ApiStatus.Internal synchronized JavaPackage convertPackageNode(PackageNode pkg) { JavaPackage foundPkg = pkg.getJavaNode(); if (foundPkg != null) { return foundPkg; } List clsList = Utils.collectionMap(pkg.getClasses(), this::convertClassNode); List clsListNoDup = Utils.collectionMap(pkg.getClassesNoDup(), this::convertClassNode); int subPkgsCount = pkg.getSubPackages().size(); List subPkgs = subPkgsCount == 0 ? Collections.emptyList() : new ArrayList<>(subPkgsCount); JavaPackage javaPkg = new JavaPackage(pkg, clsList, clsListNoDup, subPkgs); if (subPkgsCount != 0) { // add subpackages after parent to avoid endless recursion for (PackageNode subPackage : pkg.getSubPackages()) { subPkgs.add(convertPackageNode(subPackage)); } } pkg.setJavaNode(javaPkg); return javaPkg; } @Nullable public JavaClass searchJavaClassByOrigFullName(String fullName) { return getRoot().getClasses().stream() .filter(cls -> cls.getClassInfo().getFullName().equals(fullName)) .findFirst() .map(this::convertClassNode) .orElse(null); } @Nullable public ClassNode searchClassNodeByOrigFullName(String fullName) { return getRoot().getClasses().stream() .filter(cls -> cls.getClassInfo().getFullName().equals(fullName)) .findFirst() .orElse(null); } // returns parent if class contains DONT_GENERATE flag. @Nullable public JavaClass searchJavaClassOrItsParentByOrigFullName(String fullName) { ClassNode node = getRoot().getClasses().stream() .filter(cls -> cls.getClassInfo().getFullName().equals(fullName)) .findFirst() .orElse(null); if (node != null) { if (node.contains(AFlag.DONT_GENERATE)) { return convertClassNode(node.getTopParentClass()); } else { return convertClassNode(node); } } return null; } @Nullable public JavaClass searchJavaClassByAliasFullName(String fullName) { return getRoot().getClasses().stream() .filter(cls -> cls.getClassInfo().getAliasFullName().equals(fullName)) .findFirst() .map(this::convertClassNode) .orElse(null); } @Nullable public JavaNode getJavaNodeByRef(ICodeNodeRef ann) { return getJavaNodeByCodeAnnotation(null, ann); } @Nullable public JavaNode getJavaNodeByCodeAnnotation(@Nullable ICodeInfo codeInfo, @Nullable ICodeAnnotation ann) { if (ann == null) { return null; } switch (ann.getAnnType()) { case CLASS: return convertClassNode((ClassNode) ann); case METHOD: return convertMethodNode((MethodNode) ann); case FIELD: return convertFieldNode((FieldNode) ann); case PKG: return convertPackageNode((PackageNode) ann); case DECLARATION: return getJavaNodeByCodeAnnotation(codeInfo, ((NodeDeclareRef) ann).getNode()); case VAR: return resolveVarNode((VarNode) ann); case VAR_REF: return resolveVarRef(codeInfo, (VarRef) ann); case OFFSET: // offset annotation don't have java node object return null; default: throw new JadxRuntimeException("Unknown annotation type: " + ann.getAnnType() + ", class: " + ann.getClass()); } } private JavaVariable resolveVarNode(VarNode varNode) { JavaMethod javaNode = convertMethodNode(varNode.getMth()); return new JavaVariable(javaNode, varNode); } @Nullable private JavaVariable resolveVarRef(ICodeInfo codeInfo, VarRef varRef) { if (codeInfo == null) { throw new JadxRuntimeException("Missing code info for resolve VarRef: " + varRef); } ICodeAnnotation varNodeAnn = codeInfo.getCodeMetadata().getAt(varRef.getRefPos()); if (varNodeAnn != null && varNodeAnn.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) { ICodeNodeRef nodeRef = ((NodeDeclareRef) varNodeAnn).getNode(); if (nodeRef.getAnnType() == ICodeAnnotation.AnnType.VAR) { return resolveVarNode((VarNode) nodeRef); } } return null; } List convertNodes(Collection nodesList) { return nodesList.stream() .map(this::getJavaNodeByRef) .filter(Objects::nonNull) .collect(Collectors.toList()); } @Nullable public JavaNode getJavaNodeAtPosition(ICodeInfo codeInfo, int pos) { ICodeAnnotation ann = codeInfo.getCodeMetadata().getAt(pos); return getJavaNodeByCodeAnnotation(codeInfo, ann); } @Nullable public JavaNode getClosestJavaNode(ICodeInfo codeInfo, int pos) { ICodeAnnotation ann = codeInfo.getCodeMetadata().getClosestUp(pos); return getJavaNodeByCodeAnnotation(codeInfo, ann); } @Nullable public JavaNode getEnclosingNode(ICodeInfo codeInfo, int pos) { ICodeNodeRef obj = codeInfo.getCodeMetadata().getNodeAt(pos); if (obj == null) { return null; } return getJavaNodeByRef(obj); } public void reloadCodeData() { root.notifyCodeDataListeners(); } public JadxArgs getArgs() { return args; } public JadxPluginManager getPluginManager() { return pluginManager; } public IDecompileScheduler getDecompileScheduler() { return decompileScheduler; } public IJadxEvents events() { return events; } public void setEventsImpl(IJadxEvents eventsImpl) { this.events = eventsImpl; } public void addCustomCodeLoader(ICodeLoader customCodeLoader) { customCodeLoaders.add(customCodeLoader); } public List getCustomCodeLoaders() { return customCodeLoaders; } public void addCustomResourcesLoader(CustomResourcesLoader loader) { if (customResourcesLoaders.contains(loader)) { return; } customResourcesLoaders.add(loader); } public List getCustomResourcesLoaders() { return customResourcesLoaders; } public void addCustomPass(JadxPass pass) { customPasses.computeIfAbsent(pass.getPassType(), l -> new ArrayList<>()).add(pass); } public ResourcesLoader getResourcesLoader() { return resourcesLoader; } public ZipReader getZipReader() { return zipReader; } public void addCloseable(Closeable closeable) { closeableList.add(closeable); } @Override public String toString() { return "jadx decompiler " + getVersion(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/JavaClass.java ================================================ package jadx.api; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.AnonymousClassAttr; import jadx.core.dex.attributes.nodes.InlinedAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.ListUtils; public final class JavaClass implements JavaNode { private static final Logger LOG = LoggerFactory.getLogger(JavaClass.class); private final JadxDecompiler decompiler; private final ClassNode cls; private final JavaClass parent; private List innerClasses = Collections.emptyList(); private List inlinedClasses = Collections.emptyList(); private List fields = Collections.emptyList(); private List methods = Collections.emptyList(); private boolean listsLoaded; JavaClass(ClassNode classNode, JadxDecompiler decompiler) { this.decompiler = decompiler; this.cls = classNode; this.parent = null; } /** * Inner classes constructor */ JavaClass(ClassNode classNode, JavaClass parent) { this.decompiler = null; this.cls = classNode; this.parent = parent; } public String getCode() { return getCodeInfo().getCodeStr(); } public @NotNull ICodeInfo getCodeInfo() { ICodeInfo code = load(); if (code != null) { return code; } return cls.decompile(); } public void decompile() { load(); } public synchronized ICodeInfo reload() { listsLoaded = false; return cls.reloadCode(); } public void unload() { listsLoaded = false; cls.unloadCode(); } public boolean isNoCode() { return cls.contains(AFlag.DONT_GENERATE); } public boolean isInner() { return cls.isInner(); } public synchronized String getSmali() { return cls.getDisassembledCode(); } @Override public boolean isOwnCodeAnnotation(ICodeAnnotation ann) { if (ann.getAnnType() == ICodeAnnotation.AnnType.CLASS) { return ann.equals(cls); } return false; } @Override public ICodeNodeRef getCodeNodeRef() { return cls; } /** * Internal API. Not Stable! */ @ApiStatus.Internal public ClassNode getClassNode() { return cls; } /** * Decompile class and loads internal lists of fields, methods, etc. * Do nothing if already loaded. * * @return code info if decompilation was executed, null otherwise */ private synchronized @Nullable ICodeInfo load() { if (listsLoaded) { return null; } ICodeInfo code; if (cls.getState().isProcessComplete()) { // already decompiled -> class internals loaded code = null; } else { code = cls.decompile(); } loadLists(); return code; } private void loadLists() { listsLoaded = true; JadxDecompiler rootDecompiler = getRootDecompiler(); int inClsCount = cls.getInnerClasses().size(); if (inClsCount != 0) { List list = new ArrayList<>(inClsCount); for (ClassNode inner : cls.getInnerClasses()) { if (!inner.contains(AFlag.DONT_GENERATE)) { JavaClass javaClass = rootDecompiler.convertClassNode(inner); javaClass.loadLists(); list.add(javaClass); } } this.innerClasses = Collections.unmodifiableList(list); } int inlinedClsCount = cls.getInlinedClasses().size(); if (inlinedClsCount != 0) { List list = new ArrayList<>(inlinedClsCount); for (ClassNode inner : cls.getInlinedClasses()) { JavaClass javaClass = rootDecompiler.convertClassNode(inner); javaClass.loadLists(); list.add(javaClass); } this.inlinedClasses = Collections.unmodifiableList(list); } int fieldsCount = cls.getFields().size(); if (fieldsCount != 0) { List flds = new ArrayList<>(fieldsCount); for (FieldNode f : cls.getFields()) { if (!f.contains(AFlag.DONT_GENERATE)) { flds.add(rootDecompiler.convertFieldNode(f)); } } this.fields = Collections.unmodifiableList(flds); } int methodsCount = cls.getMethods().size(); if (methodsCount != 0) { List mths = new ArrayList<>(methodsCount); for (MethodNode m : cls.getMethods()) { if (!m.contains(AFlag.DONT_GENERATE)) { mths.add(rootDecompiler.convertMethodNode(m)); } } mths.sort(Comparator.comparing(JavaMethod::getName)); this.methods = Collections.unmodifiableList(mths); } } JadxDecompiler getRootDecompiler() { if (parent != null) { return parent.getRootDecompiler(); } return decompiler; } public ICodeAnnotation getAnnotationAt(int pos) { return getCodeInfo().getCodeMetadata().getAt(pos); } public Map getUsageMap() { Map map = getCodeInfo().getCodeMetadata().getAsMap(); if (map.isEmpty() || decompiler == null) { return Collections.emptyMap(); } Map resultMap = new HashMap<>(map.size()); for (Map.Entry entry : map.entrySet()) { int codePosition = entry.getKey(); ICodeAnnotation obj = entry.getValue(); if (obj instanceof ICodeNodeRef) { JavaNode node = getRootDecompiler().getJavaNodeByRef((ICodeNodeRef) obj); if (node != null) { resultMap.put(codePosition, node); } } } return resultMap; } public List getUsePlacesFor(ICodeInfo codeInfo, JavaNode javaNode) { if (!codeInfo.hasMetadata()) { return Collections.emptyList(); } List result = new ArrayList<>(); codeInfo.getCodeMetadata().searchDown(0, (pos, ann) -> { if (javaNode.isOwnCodeAnnotation(ann)) { result.add(pos); } return null; }); return result; } @Override public List getUseIn() { return getRootDecompiler().convertNodes(cls.getUseIn()); } public Integer getSourceLine(int decompiledLine) { return getCodeInfo().getCodeMetadata().getLineMapping().get(decompiledLine); } @Override public String getName() { return cls.getShortName(); } @Override public String getFullName() { return cls.getFullName(); } public String getRawName() { return cls.getRawName(); } public String getPackage() { return cls.getPackage(); } public JavaPackage getJavaPackage() { return cls.getPackageNode().getJavaNode(); } @Override public JavaClass getDeclaringClass() { return parent; } public JavaClass getOriginalTopParentClass() { return parent == null ? this : parent.getOriginalTopParentClass(); } /** * Return top parent class which contains code of this class. * Code parent can be different from original parent after move or inline * * @return this if already a top class */ @Override public JavaClass getTopParentClass() { JavaClass codeParent = getCodeParent(); return codeParent == null ? this : codeParent.getTopParentClass(); } /** * Return parent class which contains code of this class. * Code parent can be different for original parent after move or inline */ public @Nullable JavaClass getCodeParent() { AnonymousClassAttr anonymousClsAttr = cls.get(AType.ANONYMOUS_CLASS); if (anonymousClsAttr != null) { // moved to usage class return getRootDecompiler().convertClassNode(anonymousClsAttr.getOuterCls()); } InlinedAttr inlinedAttr = cls.get(AType.INLINED); if (inlinedAttr != null) { return getRootDecompiler().convertClassNode(inlinedAttr.getInlineCls()); } return parent; } public AccessInfo getAccessInfo() { return cls.getAccessFlags(); } public List getInnerClasses() { load(); return innerClasses; } public List getInlinedClasses() { load(); return inlinedClasses; } public List getFields() { load(); return fields; } public List getMethods() { load(); return methods; } @Nullable public JavaMethod searchMethodByShortId(String shortId) { MethodNode methodNode = cls.searchMethodByShortId(shortId); if (methodNode == null) { return null; } return getRootDecompiler().convertMethodNode(methodNode); } public List getDependencies() { JadxDecompiler d = getRootDecompiler(); return ListUtils.map(cls.getDependencies(), d::convertClassNode); } public int getTotalDepsCount() { return cls.getTotalDepsCount(); } @Override public void removeAlias() { cls.removeAlias(); } @Override public int getDefPos() { return cls.getDefPosition(); } @Override public boolean equals(Object o) { return this == o || o instanceof JavaClass && cls.equals(((JavaClass) o).cls); } @Override public int hashCode() { return cls.hashCode(); } @Override public String toString() { return getFullName(); } /** * Detect if calling load() would trigger a potentially expensive decompilation operation. */ public boolean loadingWouldRequireDecompilation() { if (listsLoaded) { // lists are already poplulated, so it's safe regardless of the state of the class itself return false; } if (cls.getState().isProcessComplete()) { // decompilation has already finished return false; } return true; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/JavaField.java ================================================ package jadx.api; import java.util.List; import org.jetbrains.annotations.ApiStatus; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.FieldNode; public final class JavaField implements JavaNode { private final FieldNode field; private final JavaClass parent; JavaField(JavaClass cls, FieldNode f) { this.field = f; this.parent = cls; } @Override public String getName() { return field.getAlias(); } @Override public String getFullName() { return parent.getFullName() + '.' + getName(); } public String getRawName() { return field.getName(); } @Override public JavaClass getDeclaringClass() { return parent; } @Override public JavaClass getTopParentClass() { return parent.getTopParentClass(); } public AccessInfo getAccessFlags() { return field.getAccessFlags(); } public ArgType getType() { return ArgType.tryToResolveClassAlias(field.root(), field.getType()); } @Override public int getDefPos() { return field.getDefPosition(); } @Override public List getUseIn() { return getDeclaringClass().getRootDecompiler().convertNodes(field.getUseIn()); } @Override public void removeAlias() { this.field.getFieldInfo().removeAlias(); } @Override public boolean isOwnCodeAnnotation(ICodeAnnotation ann) { if (ann.getAnnType() == ICodeAnnotation.AnnType.FIELD) { return ann.equals(field); } return false; } @Override public ICodeNodeRef getCodeNodeRef() { return field; } /** * Internal API. Not Stable! */ @ApiStatus.Internal public FieldNode getFieldNode() { return field; } @Override public int hashCode() { return field.hashCode(); } @Override public boolean equals(Object o) { return this == o || o instanceof JavaField && field.equals(((JavaField) o).field); } @Override public String toString() { return field.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/JavaMethod.java ================================================ package jadx.api; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.jetbrains.annotations.ApiStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.api.plugins.input.data.IMethodRef; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.Utils; public final class JavaMethod implements JavaNode { private static final Logger LOG = LoggerFactory.getLogger(JavaMethod.class); private final MethodNode mth; private final JavaClass parent; JavaMethod(JavaClass cls, MethodNode m) { this.parent = cls; this.mth = m; } @Override public String getName() { return mth.getAlias(); } @Override public String getFullName() { return mth.getMethodInfo().getFullName(); } @Override public JavaClass getDeclaringClass() { return parent; } @Override public JavaClass getTopParentClass() { return parent.getTopParentClass(); } public AccessInfo getAccessFlags() { return mth.getAccessFlags(); } public List getArguments() { List infoArgTypes = mth.getMethodInfo().getArgumentsTypes(); if (infoArgTypes.isEmpty()) { return Collections.emptyList(); } List arguments = mth.getArgTypes(); return Utils.collectionMap(arguments, type -> ArgType.tryToResolveClassAlias(mth.root(), type)); } public ArgType getReturnType() { ArgType retType = mth.getReturnType(); return ArgType.tryToResolveClassAlias(mth.root(), retType); } @Override public List getUseIn() { return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUseIn()); } public List getUsed() { return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUsed()); } public List getUnresolvedUsed() { return mth.getUnresolvedUsed(); } public boolean callsSelf() { return mth.callsSelf(); } public List getOverrideRelatedMethods() { MethodOverrideAttr ovrdAttr = mth.get(AType.METHOD_OVERRIDE); if (ovrdAttr == null) { return Collections.emptyList(); } JadxDecompiler decompiler = getDeclaringClass().getRootDecompiler(); return ovrdAttr.getRelatedMthNodes() .stream() .map(decompiler::convertMethodNode) .collect(Collectors.toList()); } public boolean isConstructor() { return mth.getMethodInfo().isConstructor(); } public boolean isClassInit() { return mth.getMethodInfo().isClassInit(); } @Override public int getDefPos() { return mth.getDefPosition(); } public String getCodeStr() { return mth.getCodeStr(); } @Override public void removeAlias() { this.mth.getMethodInfo().removeAlias(); } @Override public boolean isOwnCodeAnnotation(ICodeAnnotation ann) { if (ann.getAnnType() == ICodeAnnotation.AnnType.METHOD) { return ann.equals(mth); } return false; } @Override public ICodeNodeRef getCodeNodeRef() { return mth; } /** * Internal API. Not Stable! */ @ApiStatus.Internal public MethodNode getMethodNode() { return mth; } @Override public int hashCode() { return mth.hashCode(); } @Override public boolean equals(Object o) { return this == o || o instanceof JavaMethod && mth.equals(((JavaMethod) o).mth); } @Override public String toString() { return mth.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/JavaNode.java ================================================ package jadx.api; import java.util.List; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; public interface JavaNode { ICodeNodeRef getCodeNodeRef(); String getName(); String getFullName(); JavaClass getDeclaringClass(); JavaClass getTopParentClass(); int getDefPos(); List getUseIn(); void removeAlias(); boolean isOwnCodeAnnotation(ICodeAnnotation ann); } ================================================ FILE: jadx-core/src/main/java/jadx/api/JavaPackage.java ================================================ package jadx.api; import java.util.ArrayList; import java.util.List; import java.util.Objects; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.NotNull; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.core.dex.info.PackageInfo; import jadx.core.dex.nodes.PackageNode; public final class JavaPackage implements JavaNode, Comparable { private final PackageNode pkgNode; private final List classes; private final List clsListNoDup; private final List subPkgs; JavaPackage(PackageNode pkgNode, List classes, List subPkgs) { this(pkgNode, classes, classes, subPkgs); } JavaPackage(PackageNode pkgNode, List classes, List clsListNoDup, List subPkgs) { this.pkgNode = pkgNode; this.classes = classes; this.clsListNoDup = clsListNoDup; this.subPkgs = subPkgs; } @Override public String getName() { return pkgNode.getAliasPkgInfo().getName(); } @Override public String getFullName() { return pkgNode.getAliasPkgInfo().getFullName(); } public String getRawName() { return pkgNode.getPkgInfo().getName(); } public String getRawFullName() { return pkgNode.getPkgInfo().getFullName(); } public List getSubPackages() { return subPkgs; } public List getClasses() { return classes; } public List getClassesNoDup() { return clsListNoDup; } public boolean isRoot() { return pkgNode.isRoot(); } public boolean isLeaf() { return pkgNode.isLeaf(); } public boolean isDefault() { return getFullName().isEmpty(); } public void rename(String alias) { pkgNode.rename(alias); } @Override public void removeAlias() { pkgNode.removeAlias(); } public boolean isParentRenamed() { PackageInfo parent = pkgNode.getPkgInfo().getParentPkg(); PackageInfo aliasParent = pkgNode.getAliasPkgInfo().getParentPkg(); return !Objects.equals(parent, aliasParent); } public boolean isDescendantOf(JavaPackage ancestor) { JavaPackage current = this; while (current != null) { if (ancestor.equals(current)) { return true; } if (current.getPkgNode().getParentPkg() == null) { current = null; } else { current = current.getPkgNode().getParentPkg().getJavaNode(); } } return false; } @Override public ICodeNodeRef getCodeNodeRef() { return pkgNode; } @Internal public PackageNode getPkgNode() { return pkgNode; } @Override public JavaClass getDeclaringClass() { return null; } @Override public JavaClass getTopParentClass() { return null; } @Override public int getDefPos() { return 0; } @Override public List getUseIn() { List list = new ArrayList<>(); addUseIn(list); return list; } public void addUseIn(List list) { list.addAll(classes); for (JavaPackage subPkg : subPkgs) { subPkg.addUseIn(list); } } @Override public boolean isOwnCodeAnnotation(ICodeAnnotation ann) { return false; } @Override public int compareTo(@NotNull JavaPackage o) { return pkgNode.compareTo(o.pkgNode); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } JavaPackage that = (JavaPackage) o; return pkgNode.equals(that.pkgNode); } @Override public int hashCode() { return pkgNode.hashCode(); } @Override public String toString() { return pkgNode.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/JavaVariable.java ================================================ package jadx.api; import java.util.Collections; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.VarNode; import jadx.api.metadata.annotations.VarRef; import jadx.core.dex.instructions.args.ArgType; public class JavaVariable implements JavaNode { private final JavaMethod mth; private final VarNode varNode; public JavaVariable(JavaMethod mth, VarNode varNode) { this.mth = mth; this.varNode = varNode; } public JavaMethod getMth() { return mth; } public int getReg() { return varNode.getReg(); } public int getSsa() { return varNode.getSsa(); } @Override public @Nullable String getName() { return varNode.getName(); } @Override public ICodeNodeRef getCodeNodeRef() { return varNode; } @ApiStatus.Internal public VarNode getVarNode() { return varNode; } @Override public String getFullName() { return varNode.getType() + " " + varNode.getName() + " (r" + varNode.getReg() + "v" + varNode.getSsa() + ")"; } public ArgType getType() { return ArgType.tryToResolveClassAlias(mth.getMethodNode().root(), varNode.getType()); } @Override public JavaClass getDeclaringClass() { return mth.getDeclaringClass(); } @Override public JavaClass getTopParentClass() { return mth.getTopParentClass(); } @Override public int getDefPos() { return varNode.getDefPosition(); } @Override public List getUseIn() { return Collections.singletonList(mth); } @Override public void removeAlias() { varNode.setName(null); } @Override public boolean isOwnCodeAnnotation(ICodeAnnotation ann) { if (ann.getAnnType() == ICodeAnnotation.AnnType.VAR_REF) { VarRef varRef = (VarRef) ann; return varRef.getRefPos() == getDefPos(); } return false; } @Override public int hashCode() { return varNode.hashCode(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof JavaVariable)) { return false; } return varNode.equals(((JavaVariable) o).varNode); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/ResourceFile.java ================================================ package jadx.api; import java.io.File; import org.jetbrains.annotations.Nullable; import jadx.core.deobf.FileTypeDetector; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxException; import jadx.core.xmlgen.ResContainer; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.zip.IZipEntry; public class ResourceFile { private final JadxDecompiler decompiler; private final String name; private ResourceType type; private @Nullable IZipEntry zipEntry; private String deobfName; public static ResourceFile createResourceFile(JadxDecompiler decompiler, File file, ResourceType type) { return new ResourceFile(decompiler, file.getAbsolutePath(), type); } public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) { if (!decompiler.getArgs().getSecurity().isValidEntryName(name)) { return null; } return new ResourceFile(decompiler, name, type); } protected ResourceFile(JadxDecompiler decompiler, String name, ResourceType type) { this.decompiler = decompiler; this.name = name; this.type = type; } public String getOriginalName() { return name; } public String getDeobfName() { return deobfName != null ? deobfName : name; } public void setDeobfName(String resFullName) { this.deobfName = resFullName; } public ResourceType getType() { return type; } public ResContainer loadContent() { return ResourcesLoader.loadContent(decompiler, this); } public boolean setAlias(ResourceEntry entry, boolean useHeaders) { StringBuilder sb = new StringBuilder(); sb.append("res/").append(entry.getTypeName()).append(entry.getConfig()); sb.append("/").append(entry.getKeyName()); if (useHeaders) { try { int maxBytesToReadLimit = 4096; byte[] bytes = ResourcesLoader.decodeStream(this, (size, is) -> { int bytesToRead; if (size > 0) { bytesToRead = (int) Math.min(size, maxBytesToReadLimit); } else if (size == 0) { bytesToRead = 0; } else { bytesToRead = maxBytesToReadLimit; } if (bytesToRead == 0) { return new byte[0]; } return is.readNBytes(bytesToRead); }); String fileExtension = FileTypeDetector.detectFileExtension(bytes); if (!StringUtils.isEmpty(fileExtension)) { sb.append(fileExtension); } else { sb.append(getExtFromName(name)); } } catch (JadxException ignored) { } } else { sb.append(getExtFromName(name)); } String alias = sb.toString(); if (!alias.equals(name)) { setDeobfName(alias); type = ResourceType.getFileType(alias); return true; } return false; } private String getExtFromName(String name) { // the image .9.png extension always saved, when resource shrinking by aapt2 if (name.contains(".9.png")) { return ".9.png"; } int lastDot = name.lastIndexOf('.'); if (lastDot != -1) { return name.substring(lastDot); } return ""; } public @Nullable IZipEntry getZipEntry() { return zipEntry; } void setZipEntry(@Nullable IZipEntry zipEntry) { this.zipEntry = zipEntry; } public JadxDecompiler getDecompiler() { return decompiler; } @Override public String toString() { return "ResourceFile{name='" + name + '\'' + ", type=" + type + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/ResourceFileContainer.java ================================================ package jadx.api; import jadx.core.xmlgen.ResContainer; public class ResourceFileContainer extends ResourceFile { private final ResContainer container; public ResourceFileContainer(String name, ResourceType type, ResContainer container) { super(null, name, type); this.container = container; } @Override public ResContainer loadContent() { return container; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/ResourceFileContent.java ================================================ package jadx.api; import jadx.core.xmlgen.ResContainer; public class ResourceFileContent extends ResourceFile { private final ICodeInfo content; public ResourceFileContent(String name, ResourceType type, ICodeInfo content) { super(null, name, type); this.content = content; } @Override public ResContainer loadContent() { return ResContainer.textResource(getDeobfName(), content); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/ResourceType.java ================================================ package jadx.api; import java.util.HashMap; import java.util.Locale; import java.util.Map; import jadx.api.resources.ResourceContentType; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.api.resources.ResourceContentType.CONTENT_BINARY; import static jadx.api.resources.ResourceContentType.CONTENT_TEXT; import static jadx.api.resources.ResourceContentType.CONTENT_UNKNOWN; public enum ResourceType { CODE(CONTENT_BINARY, ".dex", ".jar", ".class"), XML(CONTENT_TEXT, ".xml"), ARSC(CONTENT_TEXT, ".arsc"), APK(CONTENT_BINARY, ".apk", ".apkm", ".apks"), FONT(CONTENT_BINARY, ".ttf", ".ttc", ".otf"), IMG(CONTENT_BINARY, ".png", ".gif", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff"), ARCHIVE(CONTENT_BINARY, ".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz", ".tgz", ".bz2"), VIDEOS(CONTENT_BINARY, ".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"), SOUNDS(CONTENT_BINARY, ".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"), JSON(CONTENT_TEXT, ".json"), TEXT(CONTENT_TEXT, ".txt", ".ini", ".conf", ".yaml", ".properties", ".js", ".java", ".kt", ".md"), HTML(CONTENT_TEXT, ".html", ".htm"), LIB(CONTENT_BINARY, ".so"), MANIFEST(CONTENT_TEXT), UNKNOWN_BIN(CONTENT_BINARY, ".bin"), UNKNOWN(CONTENT_UNKNOWN); private final ResourceContentType contentType; private final String[] exts; ResourceType(ResourceContentType contentType, String... exts) { this.contentType = contentType; this.exts = exts; } public ResourceContentType getContentType() { return contentType; } public String[] getExts() { return exts; } private static final Map EXT_MAP = new HashMap<>(); static { for (ResourceType type : ResourceType.values()) { for (String ext : type.getExts()) { ResourceType prev = EXT_MAP.put(ext, type); if (prev != null) { throw new JadxRuntimeException("Duplicate extension in ResourceType: " + ext); } } } } public static ResourceType getFileType(String fileName) { if (fileName.endsWith("/resources.pb")) { return ARSC; } int dot = fileName.lastIndexOf('.'); if (dot != -1) { String ext = fileName.substring(dot).toLowerCase(Locale.ROOT); ResourceType resType = EXT_MAP.get(ext); if (resType != null) { if (resType == XML && fileName.equals("AndroidManifest.xml")) { return MANIFEST; } return resType; } } return UNKNOWN; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/ResourcesLoader.java ================================================ package jadx.api; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.impl.SimpleCodeInfo; import jadx.api.plugins.CustomResourcesLoader; import jadx.api.plugins.resources.IResContainerFactory; import jadx.api.plugins.resources.IResTableParserProvider; import jadx.api.plugins.resources.IResourcesLoader; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; import jadx.core.utils.android.Res9patchStreamDecoder; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.core.xmlgen.BinaryXMLParser; import jadx.core.xmlgen.IResTableParser; import jadx.core.xmlgen.ResContainer; import jadx.core.xmlgen.ResTableBinaryParserProvider; import jadx.zip.IZipEntry; import jadx.zip.ZipContent; import static jadx.core.utils.files.FileUtils.READ_BUFFER_SIZE; import static jadx.core.utils.files.FileUtils.copyStream; // TODO: move to core package public final class ResourcesLoader implements IResourcesLoader { private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class); private final JadxDecompiler decompiler; private final List resTableParserProviders = new ArrayList<>(); private final List resContainerFactories = new ArrayList<>(); private BinaryXMLParser binaryXmlParser; ResourcesLoader(JadxDecompiler decompiler) { this.decompiler = decompiler; this.resTableParserProviders.add(new ResTableBinaryParserProvider()); } List load(RootNode root) { init(root); List inputFiles = decompiler.getArgs().getInputFiles(); List list = new ArrayList<>(inputFiles.size()); for (File file : inputFiles) { loadFile(list, file); } return list; } private void init(RootNode root) { for (IResTableParserProvider resTableParserProvider : resTableParserProviders) { try { resTableParserProvider.init(root); } catch (Exception e) { throw new JadxRuntimeException("Failed to init res table provider: " + resTableParserProvider); } } for (IResContainerFactory resContainerFactory : resContainerFactories) { try { resContainerFactory.init(root); } catch (Exception e) { throw new JadxRuntimeException("Failed to init res container factory: " + resContainerFactory); } } } public interface ResourceDecoder { T decode(long size, InputStream is) throws IOException; } @Override public void addResContainerFactory(IResContainerFactory resContainerFactory) { resContainerFactories.add(resContainerFactory); } @Override public void addResTableParserProvider(IResTableParserProvider resTableParserProvider) { resTableParserProviders.add(resTableParserProvider); } public static T decodeStream(ResourceFile rf, ResourceDecoder decoder) throws JadxException { try { IZipEntry zipEntry = rf.getZipEntry(); if (zipEntry != null) { try (InputStream inputStream = zipEntry.getInputStream()) { return decoder.decode(zipEntry.getUncompressedSize(), inputStream); } } else { File file = new File(rf.getOriginalName()); try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { return decoder.decode(file.length(), inputStream); } } } catch (Exception e) { throw new JadxException("Error decode: " + rf.getOriginalName(), e); } } static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf) { try { ResourcesLoader resLoader = jadxRef.getResourcesLoader(); return decodeStream(rf, (size, is) -> resLoader.loadContent(rf, is)); } catch (JadxException e) { LOG.error("Decode error", e); ICodeWriter cw = jadxRef.getRoot().makeCodeWriter(); cw.add("Error decode ").add(rf.getType().toString().toLowerCase()); Utils.appendStackTrace(cw, e.getCause()); return ResContainer.textResource(rf.getDeobfName(), cw.finish()); } } private ResContainer loadContent(ResourceFile resFile, InputStream inputStream) throws IOException { for (IResContainerFactory customFactory : resContainerFactories) { ResContainer resContainer = customFactory.create(resFile, inputStream); if (resContainer != null) { return resContainer; } } switch (resFile.getType()) { case MANIFEST: case XML: ICodeInfo content = loadBinaryXmlParser().parse(inputStream); return ResContainer.textResource(resFile.getDeobfName(), content); case ARSC: return decodeTable(resFile, inputStream).decodeFiles(); case IMG: return decodeImage(resFile, inputStream); default: return ResContainer.resourceFileLink(resFile); } } public IResTableParser decodeTable(ResourceFile resFile, InputStream is) throws IOException { if (resFile.getType() != ResourceType.ARSC) { throw new IllegalArgumentException("Unexpected resource type for decode: " + resFile.getType() + ", expect '.pb'/'.arsc'"); } IResTableParser parser = null; for (IResTableParserProvider provider : resTableParserProviders) { parser = provider.getParser(resFile); if (parser != null) { break; } } if (parser == null) { throw new JadxRuntimeException("Unknown type of resource file: " + resFile.getOriginalName()); } parser.setBaseFileName(resFile.getDeobfName()); parser.decode(is); return parser; } private static ResContainer decodeImage(ResourceFile rf, InputStream inputStream) { String name = rf.getDeobfName(); if (name.endsWith(".9.png")) { try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { Res9patchStreamDecoder decoder = new Res9patchStreamDecoder(); if (decoder.decode(inputStream, os)) { return ResContainer.decodedData(rf.getDeobfName(), os.toByteArray()); } } catch (Exception e) { LOG.error("Failed to decode 9-patch png image, path: {}", name, e); } } return ResContainer.resourceFileLink(rf); } private void loadFile(List list, File file) { if (file == null || file.isDirectory()) { return; } // Try to load the resources with a custom loader first for (CustomResourcesLoader loader : decompiler.getCustomResourcesLoaders()) { if (loader.load(this, list, file)) { LOG.debug("Custom loader used for {}", file.getAbsolutePath()); return; } } // If no custom decoder was able to decode the resources, use the default decoder defaultLoadFile(list, file, ""); } public void defaultLoadFile(List list, File file, String subDir) { if (FileUtils.isZipFile(file)) { try { ZipContent zipContent = decompiler.getZipReader().open(file); // do not close a zip now, entry content will be read later decompiler.addCloseable(zipContent); for (IZipEntry entry : zipContent.getEntries()) { addEntry(list, file, entry, subDir); } } catch (Exception e) { throw new RuntimeException("Failed to open zip file: " + file.getAbsolutePath(), e); } } else { ResourceType type = ResourceType.getFileType(file.getAbsolutePath()); list.add(ResourceFile.createResourceFile(decompiler, file, type)); } } public void addEntry(List list, File zipFile, IZipEntry entry, String subDir) { if (entry.isDirectory()) { return; } String name = entry.getName(); ResourceType type = ResourceType.getFileType(name); ResourceFile rf = ResourceFile.createResourceFile(decompiler, subDir + name, type); if (rf != null) { rf.setZipEntry(entry); list.add(rf); } } public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException { return loadToCodeWriter(is, StandardCharsets.UTF_8); } public static ICodeInfo loadToCodeWriter(InputStream is, Charset charset) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE); copyStream(is, baos); return new SimpleCodeInfo(baos.toString(charset)); } private synchronized BinaryXMLParser loadBinaryXmlParser() { if (binaryXmlParser == null) { binaryXmlParser = new BinaryXMLParser(decompiler.getRoot()); } return binaryXmlParser; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/args/GeneratedRenamesMappingFileMode.java ================================================ package jadx.api.args; public enum GeneratedRenamesMappingFileMode { /** * Load if found, don't save (default) */ READ, /** * Load if found, save only if new (don't overwrite) */ READ_OR_SAVE, /** * Don't load, always save */ OVERWRITE, /** * Don't load and don't save */ IGNORE; public static GeneratedRenamesMappingFileMode getDefault() { return READ; } public boolean shouldRead() { return this == READ || this == READ_OR_SAVE; } public boolean shouldWrite() { return this == READ_OR_SAVE || this == OVERWRITE; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/args/IntegerFormat.java ================================================ package jadx.api.args; public enum IntegerFormat { AUTO, DECIMAL, HEXADECIMAL; public boolean isHexadecimal() { return this == HEXADECIMAL; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/args/ResourceNameSource.java ================================================ package jadx.api.args; /** * Resources original name source (for deobfuscation) */ public enum ResourceNameSource { /** * Automatically select best name (default) */ AUTO, /** * Force use resources provided names */ RESOURCES, /** * Force use resources names from R class */ CODE, } ================================================ FILE: jadx-core/src/main/java/jadx/api/args/UseSourceNameAsClassNameAlias.java ================================================ package jadx.api.args; import jadx.core.utils.exceptions.JadxRuntimeException; public enum UseSourceNameAsClassNameAlias { ALWAYS, IF_BETTER, NEVER; public static UseSourceNameAsClassNameAlias getDefault() { return NEVER; } /** * @deprecated Use {@link UseSourceNameAsClassNameAlias} directly. */ @Deprecated public boolean toBoolean() { switch (this) { case IF_BETTER: return true; case NEVER: return false; case ALWAYS: throw new JadxRuntimeException("No match between " + this + " and boolean"); default: throw new JadxRuntimeException("Unhandled strategy: " + this); } } /** * @deprecated Use {@link UseSourceNameAsClassNameAlias} directly. */ @Deprecated public static UseSourceNameAsClassNameAlias create(boolean useSourceNameAsAlias) { return useSourceNameAsAlias ? IF_BETTER : NEVER; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/args/UserRenamesMappingsMode.java ================================================ package jadx.api.args; public enum UserRenamesMappingsMode { /** * Just read, user can save manually (default) */ READ, /** * Read and autosave after every change */ READ_AND_AUTOSAVE_EVERY_CHANGE, /** * Read and autosave before exiting the app or closing the project */ READ_AND_AUTOSAVE_BEFORE_CLOSING, /** * Don't load and don't save */ IGNORE; public static UserRenamesMappingsMode getDefault() { return READ; } public boolean shouldRead() { return this != IGNORE; } public boolean shouldWrite() { return this == READ_AND_AUTOSAVE_EVERY_CHANGE || this == READ_AND_AUTOSAVE_BEFORE_CLOSING; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/CodeRefType.java ================================================ package jadx.api.data; public enum CodeRefType { MTH_ARG, VAR, CATCH, INSN, } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/CommentStyle.java ================================================ package jadx.api.data; public enum CommentStyle { /** *
	 * // comment
	 * 
*/ LINE("// ", "// ", ""), // @formatter:off /** *
	 * /*
	 *  * comment
	 *  */
	 * 
*/ // @formatter:on BLOCK("/*\n * ", " * ", "\n */"), /** *
	 * /* comment */
	 * 
*/ BLOCK_CONDENSED("/* ", " * ", " */"), // @formatter:off /** *
	 * /**
	 *  * comment
	 *  */
	 * 
*/ // @formatter:on JAVADOC("/**\n * ", " * ", "\n */"), /** *
	 * /** comment */
	 * 
*/ JAVADOC_CONDENSED("/** ", " * ", " */"); private final String start; private final String onNewLine; private final String end; CommentStyle(String start, String onNewLine, String end) { this.start = start; this.onNewLine = onNewLine; this.end = end; } public String getStart() { return start; } public String getOnNewLine() { return onNewLine; } public String getEnd() { return end; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/ICodeComment.java ================================================ package jadx.api.data; import org.jetbrains.annotations.Nullable; public interface ICodeComment extends Comparable { IJavaNodeRef getNodeRef(); @Nullable IJavaCodeRef getCodeRef(); String getComment(); CommentStyle getStyle(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/ICodeData.java ================================================ package jadx.api.data; import java.util.List; public interface ICodeData { List getComments(); List getRenames(); boolean isEmpty(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/ICodeRename.java ================================================ package jadx.api.data; import org.jetbrains.annotations.Nullable; public interface ICodeRename extends Comparable { IJavaNodeRef getNodeRef(); @Nullable IJavaCodeRef getCodeRef(); String getNewName(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/IJavaCodeRef.java ================================================ package jadx.api.data; import org.jetbrains.annotations.NotNull; public interface IJavaCodeRef extends Comparable { CodeRefType getAttachType(); int getIndex(); @Override default int compareTo(@NotNull IJavaCodeRef o) { return Integer.compare(getIndex(), o.getIndex()); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/IJavaNodeRef.java ================================================ package jadx.api.data; public interface IJavaNodeRef extends Comparable { enum RefType { CLASS, FIELD, METHOD, PKG } RefType getType(); String getDeclaringClass(); String getShortId(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/IRenameNode.java ================================================ package jadx.api.data; public interface IRenameNode { void rename(String newName); } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/impl/JadxCodeComment.java ================================================ package jadx.api.data.impl; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.data.CommentStyle; import jadx.api.data.ICodeComment; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef; public class JadxCodeComment implements ICodeComment { private IJavaNodeRef nodeRef; @Nullable private IJavaCodeRef codeRef; private String comment; private CommentStyle style = CommentStyle.LINE; public JadxCodeComment(IJavaNodeRef nodeRef, String comment) { this(nodeRef, null, comment); } public JadxCodeComment(IJavaNodeRef nodeRef, String comment, CommentStyle style) { this(nodeRef, null, comment, style); } public JadxCodeComment(IJavaNodeRef nodeRef, @Nullable IJavaCodeRef codeRef, String comment) { this(nodeRef, codeRef, comment, CommentStyle.LINE); } public JadxCodeComment(IJavaNodeRef nodeRef, @Nullable IJavaCodeRef codeRef, String comment, CommentStyle style) { this.nodeRef = nodeRef; this.codeRef = codeRef; this.comment = comment; this.style = style; } public JadxCodeComment() { // for json deserialization } @Override public IJavaNodeRef getNodeRef() { return nodeRef; } public void setNodeRef(IJavaNodeRef nodeRef) { this.nodeRef = nodeRef; } @Nullable @Override public IJavaCodeRef getCodeRef() { return codeRef; } public void setCodeRef(@Nullable IJavaCodeRef codeRef) { this.codeRef = codeRef; } @Override public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } @Override public CommentStyle getStyle() { return style; } public void setStyle(CommentStyle style) { this.style = style; } @Override public int compareTo(@NotNull ICodeComment other) { int cmpNodeRef = this.getNodeRef().compareTo(other.getNodeRef()); if (cmpNodeRef != 0) { return cmpNodeRef; } if (this.getCodeRef() != null && other.getCodeRef() != null) { return this.getCodeRef().compareTo(other.getCodeRef()); } return this.getComment().compareTo(other.getComment()); } @Override public String toString() { return "JadxCodeComment{" + nodeRef + ", ref=" + codeRef + ", comment='" + comment + '\'' + ", style=" + style + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/impl/JadxCodeData.java ================================================ package jadx.api.data.impl; import java.util.Collections; import java.util.List; import jadx.api.data.ICodeComment; import jadx.api.data.ICodeData; import jadx.api.data.ICodeRename; public class JadxCodeData implements ICodeData { private List comments = Collections.emptyList(); private List renames = Collections.emptyList(); @Override public List getComments() { return comments; } public void setComments(List comments) { this.comments = comments; } @Override public List getRenames() { return renames; } public void setRenames(List renames) { this.renames = renames; } @Override public boolean isEmpty() { return comments.isEmpty() && renames.isEmpty(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/impl/JadxCodeRef.java ================================================ package jadx.api.data.impl; import jadx.api.JavaVariable; import jadx.api.data.CodeRefType; import jadx.api.data.IJavaCodeRef; import jadx.api.metadata.annotations.VarNode; public class JadxCodeRef implements IJavaCodeRef { public static JadxCodeRef forInsn(int offset) { return new JadxCodeRef(CodeRefType.INSN, offset); } public static JadxCodeRef forMthArg(int argIndex) { return new JadxCodeRef(CodeRefType.MTH_ARG, argIndex); } public static JadxCodeRef forVar(int regNum, int ssaVersion) { return new JadxCodeRef(CodeRefType.VAR, regNum << 16 | ssaVersion); } public static JadxCodeRef forVar(JavaVariable javaVariable) { return forVar(javaVariable.getReg(), javaVariable.getSsa()); } public static JadxCodeRef forVar(VarNode varNode) { return forVar(varNode.getReg(), varNode.getSsa()); } public static JadxCodeRef forCatch(int handlerOffset) { return new JadxCodeRef(CodeRefType.CATCH, handlerOffset); } private CodeRefType attachType; private int index; public JadxCodeRef(CodeRefType attachType, int index) { this.attachType = attachType; this.index = index; } public JadxCodeRef() { // used for json serialization } public CodeRefType getAttachType() { return attachType; } public void setAttachType(CodeRefType attachType) { this.attachType = attachType; } @Override public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof JadxCodeRef)) { return false; } JadxCodeRef other = (JadxCodeRef) o; return getIndex() == other.getIndex() && getAttachType() == other.getAttachType(); } @Override public int hashCode() { return 31 * getAttachType().hashCode() + getIndex(); } @Override public String toString() { return "JadxCodeRef{" + "attachType=" + attachType + ", index=" + index + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/impl/JadxCodeRename.java ================================================ package jadx.api.data.impl; import java.util.Objects; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.data.ICodeRename; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef; public class JadxCodeRename implements ICodeRename { private IJavaNodeRef nodeRef; @Nullable private IJavaCodeRef codeRef; private String newName; public JadxCodeRename(IJavaNodeRef nodeRef, String newName) { this(nodeRef, null, newName); } public JadxCodeRename(IJavaNodeRef nodeRef, @Nullable IJavaCodeRef codeRef, String newName) { this.nodeRef = nodeRef; this.codeRef = codeRef; this.newName = newName; } public JadxCodeRename() { // used in json serialization } @Override public IJavaNodeRef getNodeRef() { return nodeRef; } public void setNodeRef(IJavaNodeRef nodeRef) { this.nodeRef = nodeRef; } @Override public IJavaCodeRef getCodeRef() { return codeRef; } public void setCodeRef(IJavaCodeRef codeRef) { this.codeRef = codeRef; } @Override public String getNewName() { return newName; } public void setNewName(String newName) { this.newName = newName; } @Override public int compareTo(@NotNull ICodeRename other) { int cmpNodeRef = this.getNodeRef().compareTo(other.getNodeRef()); if (cmpNodeRef != 0) { return cmpNodeRef; } if (this.getCodeRef() != null && other.getCodeRef() != null) { return this.getCodeRef().compareTo(other.getCodeRef()); } return this.getNewName().compareTo(other.getNewName()); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof ICodeRename)) { return false; } ICodeRename other = (ICodeRename) o; return getNodeRef().equals(other.getNodeRef()) && Objects.equals(getCodeRef(), other.getCodeRef()); } @Override public int hashCode() { return 31 * getNodeRef().hashCode() + Objects.hashCode(getCodeRef()); } @Override public String toString() { return "JadxCodeRename{" + nodeRef + ", codeRef=" + codeRef + ", newName='" + newName + '\'' + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/data/impl/JadxNodeRef.java ================================================ package jadx.api.data.impl; import java.util.Comparator; import java.util.Objects; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.JavaClass; import jadx.api.JavaField; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.api.data.IJavaNodeRef; public class JadxNodeRef implements IJavaNodeRef { @Nullable public static JadxNodeRef forJavaNode(JavaNode javaNode) { if (javaNode instanceof JavaClass) { return forCls((JavaClass) javaNode); } if (javaNode instanceof JavaMethod) { return forMth((JavaMethod) javaNode); } if (javaNode instanceof JavaField) { return forFld((JavaField) javaNode); } return null; } public static JadxNodeRef forCls(JavaClass cls) { return new JadxNodeRef(RefType.CLASS, getClassRefStr(cls), null); } public static JadxNodeRef forCls(String clsFullName) { return new JadxNodeRef(RefType.CLASS, clsFullName, null); } public static JadxNodeRef forMth(JavaMethod mth) { return new JadxNodeRef(RefType.METHOD, getClassRefStr(mth.getDeclaringClass()), mth.getMethodNode().getMethodInfo().getShortId()); } public static JadxNodeRef forFld(JavaField fld) { return new JadxNodeRef(RefType.FIELD, getClassRefStr(fld.getDeclaringClass()), fld.getFieldNode().getFieldInfo().getShortId()); } public static JadxNodeRef forPkg(String pkgFullName) { return new JadxNodeRef(RefType.PKG, pkgFullName, ""); } private static String getClassRefStr(JavaClass cls) { return cls.getClassNode().getClassInfo().getRawName(); } private RefType refType; private String declClass; @Nullable private String shortId; public JadxNodeRef(RefType refType, String declClass, @Nullable String shortId) { this.refType = refType; this.declClass = declClass; this.shortId = shortId; } public JadxNodeRef() { // for json deserialization } @Override public RefType getType() { return refType; } public void setRefType(RefType refType) { this.refType = refType; } @Override public String getDeclaringClass() { return declClass; } public void setDeclClass(String declClass) { this.declClass = declClass; } @Nullable @Override public String getShortId() { return shortId; } public void setShortId(@Nullable String shortId) { this.shortId = shortId; } private static final Comparator COMPARATOR = Comparator .comparing(IJavaNodeRef::getType) .thenComparing(IJavaNodeRef::getDeclaringClass) .thenComparing(IJavaNodeRef::getShortId); @Override public int compareTo(@NotNull IJavaNodeRef other) { return COMPARATOR.compare(this, other); } @Override public int hashCode() { return Objects.hash(refType, declClass, shortId); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof JadxNodeRef)) { return false; } JadxNodeRef that = (JadxNodeRef) o; return refType == that.refType && Objects.equals(declClass, that.declClass) && Objects.equals(shortId, that.shortId); } @Override public String toString() { switch (refType) { case CLASS: case PKG: return declClass; case FIELD: case METHOD: return declClass + "->" + shortId; default: return "unknown node ref type"; } } } ================================================ FILE: jadx-core/src/main/java/jadx/api/deobf/IAliasProvider.java ================================================ package jadx.api.deobf; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; public interface IAliasProvider { default void init(RootNode root) { // optional } String forPackage(PackageNode pkg); String forClass(ClassNode cls); String forField(FieldNode fld); String forMethod(MethodNode mth); /** * Optional method to set initial max indexes loaded from mapping */ default void initIndexes(int pkg, int cls, int fld, int mth) { // optional } } ================================================ FILE: jadx-core/src/main/java/jadx/api/deobf/IDeobfCondition.java ================================================ package jadx.api.deobf; import jadx.api.deobf.impl.CombineDeobfConditions; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; /** * Utility interface to simplify merging several rename conditions to build {@link IRenameCondition} * instance with {@link CombineDeobfConditions#combine(IDeobfCondition...)}. */ public interface IDeobfCondition { enum Action { NO_ACTION, FORCE_RENAME, FORBID_RENAME, } void init(RootNode root); Action check(PackageNode pkg); Action check(ClassNode cls); Action check(FieldNode fld); Action check(MethodNode mth); } ================================================ FILE: jadx-core/src/main/java/jadx/api/deobf/IRenameCondition.java ================================================ package jadx.api.deobf; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; public interface IRenameCondition { void init(RootNode root); boolean shouldRename(PackageNode pkg); boolean shouldRename(ClassNode cls); boolean shouldRename(FieldNode fld); boolean shouldRename(MethodNode mth); } ================================================ FILE: jadx-core/src/main/java/jadx/api/deobf/impl/AlwaysRename.java ================================================ package jadx.api.deobf.impl; import jadx.api.deobf.IRenameCondition; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; public class AlwaysRename implements IRenameCondition { public static final IRenameCondition INSTANCE = new AlwaysRename(); private AlwaysRename() { } @Override public void init(RootNode root) { } @Override public boolean shouldRename(PackageNode pkg) { return true; } @Override public boolean shouldRename(ClassNode cls) { return true; } @Override public boolean shouldRename(FieldNode fld) { return true; } @Override public boolean shouldRename(MethodNode mth) { return true; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/deobf/impl/AnyRenameCondition.java ================================================ package jadx.api.deobf.impl; import java.util.function.BiPredicate; import jadx.api.deobf.IRenameCondition; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.IDexNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; public class AnyRenameCondition implements IRenameCondition { private final BiPredicate predicate; public AnyRenameCondition(BiPredicate predicate) { this.predicate = predicate; } @Override public void init(RootNode root) { } @Override public boolean shouldRename(PackageNode pkg) { return predicate.test(pkg.getAliasPkgInfo().getName(), pkg); } @Override public boolean shouldRename(ClassNode cls) { return predicate.test(cls.getAlias(), cls); } @Override public boolean shouldRename(FieldNode fld) { return predicate.test(fld.getAlias(), fld); } @Override public boolean shouldRename(MethodNode mth) { return predicate.test(mth.getAlias(), mth); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/deobf/impl/CombineDeobfConditions.java ================================================ package jadx.api.deobf.impl; import java.util.Arrays; import java.util.List; import java.util.function.Function; import jadx.api.deobf.IDeobfCondition; import jadx.api.deobf.IRenameCondition; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; public class CombineDeobfConditions implements IRenameCondition { public static IRenameCondition combine(List conditions) { return new CombineDeobfConditions(conditions); } public static IRenameCondition combine(IDeobfCondition... conditions) { return new CombineDeobfConditions(Arrays.asList(conditions)); } private final List conditions; private CombineDeobfConditions(List conditions) { if (conditions == null || conditions.isEmpty()) { throw new IllegalArgumentException("Conditions list can't be empty"); } this.conditions = conditions; } private boolean combineFunc(Function check) { for (IDeobfCondition c : conditions) { switch (check.apply(c)) { case NO_ACTION: // ignore break; case FORCE_RENAME: return true; case FORBID_RENAME: return false; } } return false; } @Override public void init(RootNode root) { conditions.forEach(c -> c.init(root)); } @Override public boolean shouldRename(PackageNode pkg) { return combineFunc(c -> c.check(pkg)); } @Override public boolean shouldRename(ClassNode cls) { return combineFunc(c -> c.check(cls)); } @Override public boolean shouldRename(FieldNode fld) { return combineFunc(c -> c.check(fld)); } @Override public boolean shouldRename(MethodNode mth) { return combineFunc(c -> c.check(mth)); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/gui/tree/ITreeNode.java ================================================ package jadx.api.gui.tree; import javax.swing.Icon; import javax.swing.tree.TreeNode; import org.jetbrains.annotations.Nullable; import jadx.api.metadata.ICodeNodeRef; public interface ITreeNode extends TreeNode { /** * Locale independent node identifier */ String getID(); /** * Node title */ String getName(); /** * Node icon */ Icon getIcon(); /** * Related code node reference. */ @Nullable ICodeNodeRef getCodeNodeRef(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeInfo.java ================================================ package jadx.api.impl; import java.util.Map; import jadx.api.ICodeInfo; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.impl.CodeMetadataStorage; public class AnnotatedCodeInfo implements ICodeInfo { private final String code; private final ICodeMetadata metadata; public AnnotatedCodeInfo(String code, Map lineMapping, Map annotations) { this.code = code; this.metadata = CodeMetadataStorage.build(lineMapping, annotations); } @Override public String getCodeStr() { return code; } @Override public ICodeMetadata getCodeMetadata() { return metadata; } @Override public boolean hasMetadata() { return metadata != ICodeMetadata.EMPTY; } @Override public String toString() { return code; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/impl/AnnotatedCodeWriter.java ================================================ package jadx.api.impl; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.core.utils.StringUtils; public class AnnotatedCodeWriter extends SimpleCodeWriter implements ICodeWriter { private int line = 1; private int offset; private Map annotations = Collections.emptyMap(); private Map lineMap = Collections.emptyMap(); public AnnotatedCodeWriter(JadxArgs args) { super(args); } @Override public boolean isMetadataSupported() { return true; } @Override public AnnotatedCodeWriter addMultiLine(String str) { if (str.contains(newLineStr)) { buf.append(str.replace(newLineStr, newLineStr + indentStr)); line += StringUtils.countMatches(str, newLineStr); offset = 0; } else { buf.append(str); } return this; } @Override public AnnotatedCodeWriter add(String str) { buf.append(str); offset += str.length(); return this; } @Override public AnnotatedCodeWriter add(char c) { buf.append(c); offset++; return this; } @Override public ICodeWriter add(ICodeWriter cw) { if (!cw.isMetadataSupported()) { buf.append(cw.getCodeStr()); return this; } AnnotatedCodeWriter code = (AnnotatedCodeWriter) cw; line--; int startPos = getLength(); for (Map.Entry entry : code.annotations.entrySet()) { int pos = entry.getKey(); int newPos = startPos + pos; attachAnnotation(entry.getValue(), newPos); } for (Map.Entry entry : code.lineMap.entrySet()) { attachSourceLine(line + entry.getKey(), entry.getValue()); } line += code.line; offset = code.offset; buf.append(code.buf); return this; } @Override protected void addLine() { buf.append(newLineStr); line++; offset = 0; } @Override protected AnnotatedCodeWriter addLineIndent() { buf.append(indentStr); offset += indentStr.length(); return this; } @Override public int getLine() { return line; } @Override public int getLineStartPos() { return getLength() - offset; } @Override public void attachDefinition(ICodeNodeRef obj) { if (obj == null) { return; } attachAnnotation(new NodeDeclareRef(obj)); } @Override public void attachAnnotation(ICodeAnnotation obj) { if (obj == null) { return; } attachAnnotation(obj, getLength()); } @Override public void attachLineAnnotation(ICodeAnnotation obj) { if (obj == null) { return; } attachAnnotation(obj, getLineStartPos()); } private void attachAnnotation(ICodeAnnotation obj, int pos) { if (annotations.isEmpty()) { annotations = new HashMap<>(); } annotations.put(pos, obj); } @Override public void attachSourceLine(int sourceLine) { if (sourceLine == 0) { return; } attachSourceLine(line, sourceLine); } private void attachSourceLine(int decompiledLine, int sourceLine) { if (lineMap.isEmpty()) { lineMap = new TreeMap<>(); } lineMap.put(decompiledLine, sourceLine); } @Override public ICodeInfo finish() { String code = buf.toString(); buf = null; return new AnnotatedCodeInfo(code, lineMap, annotations); } @Override public Map getRawAnnotations() { return annotations; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/impl/DelegateCodeCache.java ================================================ package jadx.api.impl; import java.io.IOException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeCache; import jadx.api.ICodeInfo; public abstract class DelegateCodeCache implements ICodeCache { protected final ICodeCache backCache; public DelegateCodeCache(ICodeCache backCache) { this.backCache = backCache; } @Override public void add(String clsFullName, ICodeInfo codeInfo) { backCache.add(clsFullName, codeInfo); } @Override public void remove(String clsFullName) { backCache.remove(clsFullName); } @Override public @NotNull ICodeInfo get(String clsFullName) { return backCache.get(clsFullName); } @Override @Nullable public String getCode(String clsFullName) { return backCache.getCode(clsFullName); } @Override public boolean contains(String clsFullName) { return backCache.contains(clsFullName); } @Override public void close() throws IOException { backCache.close(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/impl/InMemoryCodeCache.java ================================================ package jadx.api.impl; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeCache; import jadx.api.ICodeInfo; public class InMemoryCodeCache implements ICodeCache { private final Map storage = new ConcurrentHashMap<>(); @Override public void add(String clsFullName, ICodeInfo codeInfo) { storage.put(clsFullName, codeInfo); } @Override public void remove(String clsFullName) { storage.remove(clsFullName); } @NotNull @Override public ICodeInfo get(String clsFullName) { ICodeInfo codeInfo = storage.get(clsFullName); if (codeInfo == null) { return ICodeInfo.EMPTY; } return codeInfo; } @Override public @Nullable String getCode(String clsFullName) { ICodeInfo codeInfo = storage.get(clsFullName); if (codeInfo == null) { return null; } return codeInfo.getCodeStr(); } @Override public boolean contains(String clsFullName) { return storage.containsKey(clsFullName); } @Override public void close() throws IOException { storage.clear(); } @Override public String toString() { return "InMemoryCodeCache: size=" + storage.size(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/impl/NoOpCodeCache.java ================================================ package jadx.api.impl; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeCache; import jadx.api.ICodeInfo; public class NoOpCodeCache implements ICodeCache { public static final NoOpCodeCache INSTANCE = new NoOpCodeCache(); @Override public void add(String clsFullName, ICodeInfo codeInfo) { // do nothing } @Override public void remove(String clsFullName) { // do nothing } @Override @NotNull public ICodeInfo get(String clsFullName) { return ICodeInfo.EMPTY; } @Override public @Nullable String getCode(String clsFullName) { return null; } @Override public boolean contains(String clsFullName) { return false; } @Override public void close() { // do nothing } @Override public String toString() { return "NoOpCodeCache"; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/impl/SimpleCodeInfo.java ================================================ package jadx.api.impl; import jadx.api.ICodeInfo; import jadx.api.metadata.ICodeMetadata; public class SimpleCodeInfo implements ICodeInfo { private final String code; public SimpleCodeInfo(String code) { this.code = code; } @Override public String getCodeStr() { return code; } @Override public ICodeMetadata getCodeMetadata() { return ICodeMetadata.EMPTY; } @Override public boolean hasMetadata() { return false; } @Override public String toString() { return code; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/impl/SimpleCodeWriter.java ================================================ package jadx.api.impl; import java.util.Collections; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.core.utils.Utils; /** * CodeWriter implementation without meta information support */ public class SimpleCodeWriter implements ICodeWriter { private static final Logger LOG = LoggerFactory.getLogger(SimpleCodeWriter.class); protected StringBuilder buf = new StringBuilder(); protected String indentStr = ""; protected int indent = 0; protected final boolean insertLineNumbers; protected final String singleIndentStr; protected final String newLineStr; public SimpleCodeWriter(JadxArgs args) { this.insertLineNumbers = args.isInsertDebugLines(); this.singleIndentStr = args.getCodeIndentStr(); this.newLineStr = args.getCodeNewLineStr(); if (insertLineNumbers) { incIndent(3); add(indentStr); } } /** * Constructor with JadxArgs should be used. */ @Deprecated public SimpleCodeWriter() { this.insertLineNumbers = false; this.singleIndentStr = JadxArgs.DEFAULT_INDENT_STR; this.newLineStr = JadxArgs.DEFAULT_NEW_LINE_STR; } @Override public boolean isMetadataSupported() { return false; } @Override public SimpleCodeWriter startLine() { addLine(); addLineIndent(); return this; } @Override public SimpleCodeWriter startLine(char c) { startLine(); add(c); return this; } @Override public SimpleCodeWriter startLine(String str) { startLine(); add(str); return this; } @Override public SimpleCodeWriter startLineWithNum(int sourceLine) { if (sourceLine == 0) { startLine(); return this; } if (this.insertLineNumbers) { newLine(); attachSourceLine(sourceLine); int start = getLength(); add("/* ").add(Integer.toString(sourceLine)).add(" */ "); int len = getLength() - start; if (indentStr.length() > len) { add(indentStr.substring(len)); } } else { startLine(); attachSourceLine(sourceLine); } return this; } @Override public SimpleCodeWriter addMultiLine(String str) { if (str.contains(newLineStr)) { buf.append(str.replace(newLineStr, newLineStr + indentStr)); } else { buf.append(str); } return this; } @Override public SimpleCodeWriter add(String str) { buf.append(str); return this; } @Override public SimpleCodeWriter add(char c) { buf.append(c); return this; } @Override public ICodeWriter add(ICodeWriter cw) { buf.append(cw.getCodeStr()); return this; } @Override public SimpleCodeWriter newLine() { addLine(); return this; } @Override public SimpleCodeWriter addIndent() { add(singleIndentStr); return this; } protected void addLine() { buf.append(newLineStr); } protected SimpleCodeWriter addLineIndent() { buf.append(indentStr); return this; } private void updateIndent() { this.indentStr = Utils.strRepeat(singleIndentStr, indent); } @Override public void incIndent() { incIndent(1); } @Override public void decIndent() { decIndent(1); } private void incIndent(int c) { this.indent += c; updateIndent(); } private void decIndent(int c) { this.indent -= c; if (this.indent < 0) { LOG.warn("Indent < 0"); this.indent = 0; } updateIndent(); } @Override public int getIndent() { return indent; } @Override public void setIndent(int indent) { this.indent = indent; updateIndent(); } @Override public int getLine() { return 0; } @Override public int getLineStartPos() { return 0; } @Override public void attachDefinition(ICodeNodeRef obj) { // no op } @Override public void attachAnnotation(ICodeAnnotation obj) { // no op } @Override public void attachLineAnnotation(ICodeAnnotation obj) { // no op } @Override public void attachSourceLine(int sourceLine) { // no op } @Override public ICodeInfo finish() { String code = getStringWithoutFirstEmptyLine(); buf = null; return new SimpleCodeInfo(code); } private String getStringWithoutFirstEmptyLine() { int len = newLineStr.length(); if (buf.length() > len && buf.substring(0, len).equals(newLineStr)) { return buf.substring(len); } return buf.toString(); } @Override public int getLength() { return buf.length(); } @Override public StringBuilder getRawBuf() { return buf; } @Override public Map getRawAnnotations() { return Collections.emptyMap(); } @Override public String getCodeStr() { return buf.toString(); } @Override public String toString() { return getCodeStr(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/impl/passes/DecompilePassWrapper.java ================================================ package jadx.api.impl.passes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.pass.JadxPass; import jadx.api.plugins.pass.types.JadxDecompilePass; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.exceptions.JadxException; public class DecompilePassWrapper extends AbstractVisitor implements IPassWrapperVisitor { private static final Logger LOG = LoggerFactory.getLogger(DecompilePassWrapper.class); private final JadxDecompilePass decompilePass; public DecompilePassWrapper(JadxDecompilePass decompilePass) { this.decompilePass = decompilePass; } @Override public JadxPass getPass() { return decompilePass; } @Override public void init(RootNode root) throws JadxException { try { decompilePass.init(root); } catch (StackOverflowError | Exception e) { LOG.error("Error in decompile pass init: {}", this, e); } } @Override public boolean visit(ClassNode cls) throws JadxException { try { return decompilePass.visit(cls); } catch (StackOverflowError | Exception e) { cls.addError("Error in decompile pass: " + this, e); return false; } } @Override public void visit(MethodNode mth) throws JadxException { try { decompilePass.visit(mth); } catch (StackOverflowError | Exception e) { mth.addError("Error in decompile pass: " + this, e); } } @Override public String getName() { return decompilePass.getInfo().getName(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/impl/passes/IPassWrapperVisitor.java ================================================ package jadx.api.impl.passes; import jadx.api.plugins.pass.JadxPass; import jadx.core.dex.visitors.IDexTreeVisitor; public interface IPassWrapperVisitor extends IDexTreeVisitor { JadxPass getPass(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/impl/passes/PreparePassWrapper.java ================================================ package jadx.api.impl.passes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.pass.JadxPass; import jadx.api.plugins.pass.types.JadxPreparePass; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.exceptions.JadxException; public class PreparePassWrapper extends AbstractVisitor implements IPassWrapperVisitor { private static final Logger LOG = LoggerFactory.getLogger(PreparePassWrapper.class); private final JadxPreparePass preparePass; public PreparePassWrapper(JadxPreparePass preparePass) { this.preparePass = preparePass; } @Override public JadxPass getPass() { return preparePass; } @Override public void init(RootNode root) throws JadxException { try { preparePass.init(root); } catch (Exception e) { LOG.error("Error in prepare pass init: {}", this, e); } } @Override public String getName() { return preparePass.getInfo().getName(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/metadata/ICodeAnnotation.java ================================================ package jadx.api.metadata; public interface ICodeAnnotation { enum AnnType { CLASS, FIELD, METHOD, PKG, VAR, VAR_REF, DECLARATION, OFFSET, END // class or method body end } AnnType getAnnType(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/metadata/ICodeMetadata.java ================================================ package jadx.api.metadata; import java.util.Map; import java.util.function.BiFunction; import org.jetbrains.annotations.Nullable; import jadx.api.metadata.impl.CodeMetadataStorage; public interface ICodeMetadata { ICodeMetadata EMPTY = CodeMetadataStorage.empty(); @Nullable ICodeAnnotation getAt(int position); @Nullable ICodeAnnotation getClosestUp(int position); @Nullable ICodeAnnotation searchUp(int position, ICodeAnnotation.AnnType annType); @Nullable ICodeAnnotation searchUp(int position, int limitPos, ICodeAnnotation.AnnType annType); /** * Iterate code annotations from {@code startPos} to smaller positions. * * @param visitor * return not null value to stop iterations */ @Nullable T searchUp(int startPos, BiFunction visitor); /** * Iterate code annotations from {@code startPos} to higher positions. * * @param visitor * return not null value to stop iterations */ @Nullable T searchDown(int startPos, BiFunction visitor); /** * Get current node at position (can be enclosing class or method) */ @Nullable ICodeNodeRef getNodeAt(int position); /** * Any definition of class or method below position */ @Nullable ICodeNodeRef getNodeBelow(int position); Map getAsMap(); Map getLineMapping(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/metadata/ICodeNodeRef.java ================================================ package jadx.api.metadata; public interface ICodeNodeRef extends ICodeAnnotation { int getDefPosition(); void setDefPosition(int pos); } ================================================ FILE: jadx-core/src/main/java/jadx/api/metadata/annotations/InsnCodeOffset.java ================================================ package jadx.api.metadata.annotations; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeWriter; import jadx.api.metadata.ICodeAnnotation; import jadx.core.dex.nodes.InsnNode; public class InsnCodeOffset implements ICodeAnnotation { public static void attach(ICodeWriter code, InsnNode insn) { if (insn == null) { return; } if (code.isMetadataSupported()) { InsnCodeOffset ann = from(insn); if (ann != null) { code.attachLineAnnotation(ann); } } } public static void attach(ICodeWriter code, int offset) { if (offset >= 0 && code.isMetadataSupported()) { code.attachLineAnnotation(new InsnCodeOffset(offset)); } } @Nullable public static InsnCodeOffset from(InsnNode insn) { int offset = insn.getOffset(); if (offset < 0) { return null; } return new InsnCodeOffset(offset); } private final int offset; public InsnCodeOffset(int offset) { this.offset = offset; } public int getOffset() { return offset; } @Override public AnnType getAnnType() { return AnnType.OFFSET; } @Override public String toString() { return "offset=" + offset; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/metadata/annotations/NodeDeclareRef.java ================================================ package jadx.api.metadata.annotations; import java.util.Objects; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; public class NodeDeclareRef implements ICodeAnnotation { private final ICodeNodeRef node; private int defPos; public NodeDeclareRef(ICodeNodeRef node) { this.node = Objects.requireNonNull(node); } public ICodeNodeRef getNode() { return node; } public int getDefPos() { return defPos; } public void setDefPos(int defPos) { this.defPos = defPos; } @Override public AnnType getAnnType() { return AnnType.DECLARATION; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof NodeDeclareRef)) { return false; } return node.equals(((NodeDeclareRef) o).node); } @Override public int hashCode() { return node.hashCode(); } @Override public String toString() { return "NodeDeclareRef{" + node + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/metadata/annotations/NodeEnd.java ================================================ package jadx.api.metadata.annotations; import jadx.api.metadata.ICodeAnnotation; public class NodeEnd implements ICodeAnnotation { public static final NodeEnd VALUE = new NodeEnd(); private NodeEnd() { } @Override public AnnType getAnnType() { return AnnType.END; } @Override public String toString() { return "END"; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/metadata/annotations/VarNode.java ================================================ package jadx.api.metadata.annotations; import org.jetbrains.annotations.Nullable; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.MethodNode; /** * Variable info */ public class VarNode implements ICodeNodeRef { @Nullable public static VarNode get(MethodNode mth, RegisterArg reg) { SSAVar ssaVar = reg.getSVar(); if (ssaVar == null) { return null; } return get(mth, ssaVar); } @Nullable public static VarNode get(MethodNode mth, CodeVar codeVar) { return get(mth, codeVar.getAnySsaVar()); } @Nullable public static VarNode get(MethodNode mth, SSAVar ssaVar) { CodeVar codeVar = ssaVar.getCodeVar(); if (codeVar.isThis()) { return null; } VarNode cachedVarNode = codeVar.getCachedVarNode(); if (cachedVarNode != null) { return cachedVarNode; } VarNode newVarNode = new VarNode(mth, ssaVar); codeVar.setCachedVarNode(newVarNode); return newVarNode; } @Nullable public static ICodeAnnotation getRef(MethodNode mth, RegisterArg reg) { VarNode varNode = get(mth, reg); if (varNode == null) { return null; } return varNode.getVarRef(); } private final MethodNode mth; private final int reg; private final int ssa; private final ArgType type; private @Nullable String name; private int defPos; private final VarRef varRef; protected VarNode(MethodNode mth, SSAVar ssaVar) { this(mth, ssaVar.getRegNum(), ssaVar.getVersion(), ssaVar.getCodeVar().getType(), ssaVar.getCodeVar().getName()); } public VarNode(MethodNode mth, int reg, int ssa, ArgType type, String name) { this.mth = mth; this.reg = reg; this.ssa = ssa; this.type = type; this.name = name; this.varRef = VarRef.fromVarNode(this); } public MethodNode getMth() { return mth; } public int getReg() { return reg; } public int getSsa() { return ssa; } public ArgType getType() { return type; } @Nullable public String getName() { return name; } public void setName(String name) { this.name = name; } public VarRef getVarRef() { return varRef; } @Override public int getDefPosition() { return defPos; } @Override public void setDefPosition(int pos) { this.defPos = pos; } @Override public AnnType getAnnType() { return AnnType.VAR; } @Override public int hashCode() { int h = 31 * getReg() + getSsa(); return 31 * h + mth.hashCode(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof VarNode)) { return false; } VarNode other = (VarNode) o; return getReg() == other.getReg() && getSsa() == other.getSsa() && getMth().equals(other.getMth()); } @Override public String toString() { return "VarNode{r" + reg + 'v' + ssa + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/metadata/annotations/VarRef.java ================================================ package jadx.api.metadata.annotations; import jadx.api.metadata.ICodeAnnotation; /** * Variable reference by position of VarNode in code metadata. *
* Because on creation position not yet known, * VarRef created using VarNode as a source of ref pos during serialization. *
* On metadata deserialization created with ref pos directly. */ public abstract class VarRef implements ICodeAnnotation { public static VarRef fromPos(int refPos) { if (refPos == 0) { throw new IllegalArgumentException("Zero refPos"); } return new FixedVarRef(refPos); } public static VarRef fromVarNode(VarNode varNode) { return new RelatedVarRef(varNode); } public abstract int getRefPos(); @Override public AnnType getAnnType() { return AnnType.VAR_REF; } public static final class FixedVarRef extends VarRef { private final int refPos; public FixedVarRef(int refPos) { this.refPos = refPos; } @Override public int getRefPos() { return refPos; } } public static final class RelatedVarRef extends VarRef { private final VarNode varNode; public RelatedVarRef(VarNode varNode) { this.varNode = varNode; } @Override public int getRefPos() { return varNode.getDefPosition(); } @Override public String toString() { return "VarRef{" + varNode + ", name=" + varNode.getName() + ", mth=" + varNode.getMth() + '}'; } } @Override public String toString() { return "VarRef{" + getRefPos() + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/metadata/impl/CodeMetadataStorage.java ================================================ package jadx.api.metadata.impl; import java.util.Collections; import java.util.Comparator; import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; import java.util.function.BiFunction; import org.jetbrains.annotations.Nullable; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeAnnotation.AnnType; import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.core.utils.Utils; public class CodeMetadataStorage implements ICodeMetadata { public static ICodeMetadata build(Map lines, Map map) { if (map.isEmpty() && lines.isEmpty()) { return ICodeMetadata.EMPTY; } Comparator reverseCmp = Comparator.comparingInt(Integer::intValue).reversed(); NavigableMap navMap = new TreeMap<>(reverseCmp); navMap.putAll(map); return new CodeMetadataStorage(lines, navMap); } public static ICodeMetadata empty() { return new CodeMetadataStorage(Collections.emptyMap(), Collections.emptyNavigableMap()); } // -> private final Map lines; // -> // the key is what is returned by AbstractCodeArea#getCaretPos() when clicking in a code panel. private final NavigableMap navMap; private CodeMetadataStorage(Map lines, NavigableMap navMap) { this.lines = lines; this.navMap = navMap; } @Override public ICodeAnnotation getAt(int position) { return navMap.get(position); } @Override public @Nullable ICodeAnnotation getClosestUp(int position) { Map.Entry entryBefore = navMap.higherEntry(position); return entryBefore != null ? entryBefore.getValue() : null; } @Override public @Nullable ICodeAnnotation searchUp(int position, AnnType annType) { for (ICodeAnnotation v : navMap.tailMap(position, true).values()) { if (v.getAnnType() == annType) { return v; } } return null; } @Override public @Nullable ICodeAnnotation searchUp(int position, int limitPos, AnnType annType) { for (ICodeAnnotation v : navMap.subMap(position, true, limitPos, true).values()) { if (v.getAnnType() == annType) { return v; } } return null; } @Override public @Nullable T searchUp(int startPos, BiFunction visitor) { for (Map.Entry entry : navMap.tailMap(startPos, true).entrySet()) { T value = visitor.apply(entry.getKey(), entry.getValue()); if (value != null) { return value; } } return null; } @Override public @Nullable T searchDown(int startPos, BiFunction visitor) { NavigableMap map = navMap.headMap(startPos, true).descendingMap(); for (Map.Entry entry : map.entrySet()) { T value = visitor.apply(entry.getKey(), entry.getValue()); if (value != null) { return value; } } return null; } @Override public ICodeNodeRef getNodeAt(int position) { int nesting = 0; for (ICodeAnnotation ann : navMap.tailMap(position, true).values()) { switch (ann.getAnnType()) { case END: nesting++; break; case DECLARATION: ICodeNodeRef node = ((NodeDeclareRef) ann).getNode(); AnnType nodeType = node.getAnnType(); if (nodeType == AnnType.CLASS || nodeType == AnnType.METHOD) { if (nesting == 0) { return node; } nesting--; } break; } } return null; } @Override public ICodeNodeRef getNodeBelow(int position) { for (ICodeAnnotation ann : navMap.headMap(position, true).descendingMap().values()) { if (ann.getAnnType() == AnnType.DECLARATION) { ICodeNodeRef node = ((NodeDeclareRef) ann).getNode(); AnnType nodeType = node.getAnnType(); if (nodeType == AnnType.CLASS || nodeType == AnnType.METHOD) { return node; } } } return null; } @Override public NavigableMap getAsMap() { return navMap; } @Override public Map getLineMapping() { return lines; } @Override public String toString() { return "CodeMetadata{\nlines=" + lines + "\nannotations=\n " + Utils.listToString(navMap.descendingMap().entrySet(), "\n ") + "\n}"; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/CustomResourcesLoader.java ================================================ package jadx.api.plugins; import java.io.Closeable; import java.io.File; import java.util.List; import jadx.api.ResourceFile; import jadx.api.ResourcesLoader; public interface CustomResourcesLoader extends Closeable { /** * Load resources from file to list of ResourceFile * * @param list list to add loaded resources * @param file file to load * @return true if file was loaded */ boolean load(ResourcesLoader loader, List list, File file); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/JadxPlugin.java ================================================ package jadx.api.plugins; import jadx.api.plugins.pass.types.JadxAfterLoadPass; import jadx.api.plugins.pass.types.JadxPreparePass; /** * Base interface for all jadx plugins *
* To create new plugin implement this interface and add to resources * a {@code META-INF/services/jadx.api.plugins.JadxPlugin} file with a full name of your class. */ public interface JadxPlugin { /** * Method for provide plugin information, like name and description. * Can be invoked several times. */ JadxPluginInfo getPluginInfo(); /** * Init plugin. * Use {@link JadxPluginContext} to register passes, code inputs and options. * For long operation, prefer {@link JadxPreparePass} or {@link JadxAfterLoadPass} instead. */ void init(JadxPluginContext context); /** * Plugin unload handler. * Can be used to clean up resources on plugin unloading. */ default void unload() { // optional method } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java ================================================ package jadx.api.plugins; import java.util.function.Supplier; import org.jetbrains.annotations.Nullable; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.plugins.data.IJadxFiles; import jadx.api.plugins.data.IJadxPlugins; import jadx.api.plugins.events.IJadxEvents; import jadx.api.plugins.gui.JadxGuiContext; import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.pass.JadxPass; import jadx.api.plugins.resources.IResourcesLoader; import jadx.zip.ZipReader; public interface JadxPluginContext { JadxArgs getArgs(); JadxDecompiler getDecompiler(); void addPass(JadxPass pass); void addCodeInput(JadxCodeInput codeInput); void registerOptions(JadxPluginOptions options); /** * Function to calculate hash of all options which can change output code. * Hash for input files ({@link JadxArgs#getInputFiles()}) and registered options * calculated by default implementations. */ void registerInputsHashSupplier(Supplier supplier); /** * Customize resource loading */ IResourcesLoader getResourcesLoader(); /** * Access to jadx-gui specific methods */ @Nullable JadxGuiContext getGuiContext(); /** * Subscribe and send events */ IJadxEvents events(); /** * Access to registered plugins and runtime data */ IJadxPlugins plugins(); /** * Access to plugin specific files and directories */ IJadxFiles files(); /** * Custom jadx zip reader to fight tampering and provide additional security checks */ ZipReader getZipReader(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfo.java ================================================ package jadx.api.plugins; import org.jetbrains.annotations.Nullable; public class JadxPluginInfo { private final String pluginId; private final String name; private final String description; private String homepage; /** * Conflicting plugins should have the same 'provides' property; only one will be loaded */ private String provides; /** * Minimum required jadx version to run this plugin. *
* Format: ", r". * Example: "1.5.1, r2305" * * @see wiki * page * for details. */ private @Nullable String requiredJadxVersion; public JadxPluginInfo(String id, String name, String description) { this(id, name, description, "", id); } public JadxPluginInfo(String pluginId, String name, String description, String provides) { this(pluginId, name, description, "", provides); } public JadxPluginInfo(String pluginId, String name, String description, String homepage, String provides) { this.pluginId = pluginId; this.name = name; this.description = description; this.homepage = homepage; this.provides = provides; } public String getPluginId() { return pluginId; } public String getName() { return name; } public String getDescription() { return description; } public String getHomepage() { return homepage; } public void setHomepage(String homepage) { this.homepage = homepage; } public String getProvides() { return provides; } public void setProvides(String provides) { this.provides = provides; } public @Nullable String getRequiredJadxVersion() { return requiredJadxVersion; } public void setRequiredJadxVersion(@Nullable String requiredJadxVersion) { this.requiredJadxVersion = requiredJadxVersion; } @Override public String toString() { return pluginId + ": " + name + " - '" + description + '\''; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfoBuilder.java ================================================ package jadx.api.plugins; import java.util.Objects; import org.jetbrains.annotations.Nullable; import jadx.core.plugins.versions.VerifyRequiredVersion; public class JadxPluginInfoBuilder { private String pluginId; private String name; private String description; private String homepage = ""; private @Nullable String requiredJadxVersion; private @Nullable String provides; /** * Start building method */ public static JadxPluginInfoBuilder pluginId(String pluginId) { JadxPluginInfoBuilder builder = new JadxPluginInfoBuilder(); builder.pluginId = Objects.requireNonNull(pluginId); return builder; } private JadxPluginInfoBuilder() { } public JadxPluginInfoBuilder name(String name) { this.name = Objects.requireNonNull(name); return this; } public JadxPluginInfoBuilder description(String description) { this.description = Objects.requireNonNull(description); return this; } public JadxPluginInfoBuilder homepage(String homepage) { this.homepage = homepage; return this; } public JadxPluginInfoBuilder provides(String provides) { this.provides = provides; return this; } public JadxPluginInfoBuilder requiredJadxVersion(String versions) { this.requiredJadxVersion = versions; return this; } public JadxPluginInfo build() { Objects.requireNonNull(pluginId, "PluginId is required"); Objects.requireNonNull(name, "Name is required"); Objects.requireNonNull(description, "Description is required"); if (provides == null) { provides = pluginId; } if (requiredJadxVersion != null) { VerifyRequiredVersion.verify(requiredJadxVersion); } JadxPluginInfo pluginInfo = new JadxPluginInfo(pluginId, name, description, homepage, provides); pluginInfo.setRequiredJadxVersion(requiredJadxVersion); return pluginInfo; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/data/IJadxFiles.java ================================================ package jadx.api.plugins.data; import java.nio.file.Path; public interface IJadxFiles { /** * Plugin cache directory. */ Path getPluginCacheDir(); /** * Plugin config directory. */ Path getPluginConfigDir(); /** * Plugin temp directory. */ Path getPluginTempDir(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/data/IJadxPlugins.java ================================================ package jadx.api.plugins.data; import jadx.api.plugins.JadxPlugin; public interface IJadxPlugins { JadxPluginRuntimeData getById(String pluginId); JadxPluginRuntimeData getProviding(String provideId);

P getInstance(Class

pluginCls); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/data/JadxPluginRuntimeData.java ================================================ package jadx.api.plugins.data; import java.io.Closeable; import java.nio.file.Path; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.options.JadxPluginOptions; /** * Runtime plugin data. */ public interface JadxPluginRuntimeData { boolean isInitialized(); String getPluginId(); JadxPlugin getPluginInstance(); JadxPluginInfo getPluginInfo(); List getCodeInputs(); @Nullable JadxPluginOptions getOptions(); String getInputsHash(); /** * Convenient method to simplify code loading from custom files. */ ICodeLoader loadCodeFiles(List files, @Nullable Closeable closeable); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/events/IJadxEvent.java ================================================ package jadx.api.plugins.events; public interface IJadxEvent { JadxEventType getType(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/events/IJadxEvents.java ================================================ package jadx.api.plugins.events; import java.util.function.Consumer; public interface IJadxEvents { /** * Send an event object. * For public event types check {@link JadxEvents} class. */ void send(IJadxEvent event); /** * Register listener for specific event. * For public event types check {@link JadxEvents} class. */ void addListener(JadxEventType eventType, Consumer listener); /** * Remove listener for specific event. * Listener should be same or equal object. */ void removeListener(JadxEventType eventType, Consumer listener); /** * Clear all listeners. */ void reset(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/events/JadxEventType.java ================================================ package jadx.api.plugins.events; public abstract class JadxEventType { public static JadxEventType create() { return new JadxEventType<>() { }; } public static JadxEventType create(String name) { return new JadxEventType<>() { @Override public String toString() { return name; } }; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/events/JadxEvents.java ================================================ package jadx.api.plugins.events; import jadx.api.plugins.events.types.NodeRenamedByUser; import jadx.api.plugins.events.types.ReloadProject; import jadx.api.plugins.events.types.ReloadSettingsWindow; import jadx.api.plugins.gui.ISettingsGroup; import jadx.api.plugins.gui.JadxGuiSettings; import static jadx.api.plugins.events.JadxEventType.create; /** * Typed and extendable enumeration of event types */ public class JadxEvents { /** * Notify about renaming done by user (GUI only). */ public static final JadxEventType NODE_RENAMED_BY_USER = create("NODE_RENAMED_BY_USER"); /** * Request reload of a current project (GUI only). */ public static final JadxEventType RELOAD_PROJECT = create("RELOAD_PROJECT"); /** * Request reload of a settings window (GUI only). * Useful for a reload custom settings group which was set with * {@link JadxGuiSettings#setCustomSettingsGroup(ISettingsGroup)}. */ public static final JadxEventType RELOAD_SETTINGS_WINDOW = create("RELOAD_SETTINGS_WINDOW"); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/events/types/NodeRenamedByUser.java ================================================ package jadx.api.plugins.events.types; import org.jetbrains.annotations.Nullable; import jadx.api.metadata.ICodeNodeRef; import jadx.api.plugins.events.IJadxEvent; import jadx.api.plugins.events.JadxEventType; import jadx.api.plugins.events.JadxEvents; public class NodeRenamedByUser implements IJadxEvent { private final ICodeNodeRef node; private final String oldName; private final String newName; /** * Optional JRenameNode instance */ private @Nullable Object renameNode; /** * Request reset name to original */ private boolean resetName = false; public NodeRenamedByUser(ICodeNodeRef node, String oldName, String newName) { this.node = node; this.oldName = oldName; this.newName = newName; } public ICodeNodeRef getNode() { return node; } public String getOldName() { return oldName; } public String getNewName() { return newName; } public Object getRenameNode() { return renameNode; } public void setRenameNode(Object renameNode) { this.renameNode = renameNode; } public boolean isResetName() { return resetName; } public void setResetName(boolean resetName) { this.resetName = resetName; } @Override public JadxEventType getType() { return JadxEvents.NODE_RENAMED_BY_USER; } @Override public String toString() { return "NodeRenamedByUser{" + node + ", '" + oldName + "' -> '" + newName + '\'' + (resetName ? ", reset name" : "") + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/events/types/ReloadProject.java ================================================ package jadx.api.plugins.events.types; import jadx.api.plugins.events.IJadxEvent; import jadx.api.plugins.events.JadxEventType; import jadx.api.plugins.events.JadxEvents; public class ReloadProject implements IJadxEvent { public static final ReloadProject EVENT = new ReloadProject(); private ReloadProject() { // singleton } @Override public JadxEventType getType() { return JadxEvents.RELOAD_PROJECT; } @Override public String toString() { return "RELOAD_PROJECT"; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/events/types/ReloadSettingsWindow.java ================================================ package jadx.api.plugins.events.types; import jadx.api.plugins.events.IJadxEvent; import jadx.api.plugins.events.JadxEventType; import jadx.api.plugins.events.JadxEvents; public class ReloadSettingsWindow implements IJadxEvent { public static final ReloadSettingsWindow INSTANCE = new ReloadSettingsWindow(); private ReloadSettingsWindow() { // singleton } @Override public JadxEventType getType() { return JadxEvents.RELOAD_SETTINGS_WINDOW; } @Override public String toString() { return "RELOAD_SETTINGS_WINDOW"; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/gui/ISettingsGroup.java ================================================ package jadx.api.plugins.gui; import java.util.Collections; import java.util.List; import javax.swing.JComponent; /** * Settings page customization */ public interface ISettingsGroup { /** * Node name */ String getTitle(); /** * Custom page component */ JComponent buildComponent(); /** * Optional child nodes list */ default List getSubGroups() { return Collections.emptyList(); } /** * Settings close handler. * Apply settings if 'save' param set to true. * It can be used to clean up resources. */ default void close(boolean save) { // optional method } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java ================================================ package jadx.api.plugins.gui; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.KeyStroke; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import jadx.api.gui.tree.ITreeNode; import jadx.api.metadata.ICodeNodeRef; public interface JadxGuiContext { /** * Run code in UI Thread */ void uiRun(Runnable runnable); /** * Add global menu entry ('Plugins' section) */ void addMenuAction(String name, Runnable action); /** * Add code viewer popup menu entry * * @param name entry title * @param enabled check if entry should be enabled, called on popup creation * @param keyBinding optional assigned keybinding {@link KeyStroke#getKeyStroke(String)} */ void addPopupMenuAction(String name, @Nullable Function enabled, @Nullable String keyBinding, Consumer action); /** * Add popup menu entry for tree node * * @param name entry title * @param addPredicate check if entry should be added for provided node, called on popup creation */ @ApiStatus.Experimental void addTreePopupMenuEntry(String name, Predicate addPredicate, Consumer action); /** * Attach new key binding to main window * * @param id unique ID string * @param keyBinding keybinding string {@link KeyStroke#getKeyStroke(String)} * @param action runnable action * @return false if already registered */ boolean registerGlobalKeyBinding(String id, String keyBinding, Runnable action); void copyToClipboard(String str); /** * Access to GUI settings */ JadxGuiSettings settings(); /** * Main window component. * Can be used as a parent for creating new windows or dialogs. */ JFrame getMainFrame(); /** * Load SVG icon from jadx resources. * All available icons can be found in "jadx-gui/src/main/resources/icons". * Method is thread-safe. * * @param name short name in form: "category/iconName", example: "nodes/publicClass" * @return loaded and cached icon, if icon not found returns default icon: "ui/error" */ ImageIcon getSVGIcon(String name); ICodeNodeRef getNodeUnderCaret(); ICodeNodeRef getNodeUnderMouse(); ICodeNodeRef getEnclosingNodeUnderCaret(); ICodeNodeRef getEnclosingNodeUnderMouse(); /** * Jump to a code ref * * @return if successfully jumped to the code ref */ boolean open(ICodeNodeRef ref); /** * Open usage dialog for a node */ void openUsageDialog(ICodeNodeRef ref); /** * Reload code in active tab */ void reloadActiveTab(); /** * Reload code in all open tabs */ void reloadAllTabs(); /** * Save node rename in a project and run all needed UI updates */ void applyNodeRename(ICodeNodeRef node); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiSettings.java ================================================ package jadx.api.plugins.gui; import java.util.List; import jadx.api.plugins.options.OptionDescription; public interface JadxGuiSettings { /** * Set plugin custom settings page */ void setCustomSettingsGroup(ISettingsGroup group); /** * Helper method to build options group only for provided option list */ ISettingsGroup buildSettingsGroupForOptions(String title, List options); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/loader/JadxBasePluginLoader.java ================================================ package jadx.api.plugins.loader; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader; import jadx.api.plugins.JadxPlugin; /** * Loading plugins from current classpath */ public class JadxBasePluginLoader implements JadxPluginLoader { @Override public List load() { List list = new ArrayList<>(); ServiceLoader plugins = ServiceLoader.load(JadxPlugin.class); for (JadxPlugin plugin : plugins) { list.add(plugin); } return list; } @Override public void close() throws IOException { // nothing to close } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/loader/JadxPluginLoader.java ================================================ package jadx.api.plugins.loader; import java.io.Closeable; import java.util.List; import jadx.api.plugins.JadxPlugin; public interface JadxPluginLoader extends Closeable { List load(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/options/JadxPluginOptions.java ================================================ package jadx.api.plugins.options; import java.util.List; import java.util.Map; public interface JadxPluginOptions { void setOptions(Map options); List getOptionsDescriptions(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/options/OptionDescription.java ================================================ package jadx.api.plugins.options; import java.util.Collections; import java.util.List; import java.util.Set; import org.jetbrains.annotations.Nullable; public interface OptionDescription { String name(); String description(); /** * Possible values. * Empty if not a limited set */ List values(); /** * Default value. * Null if required */ @Nullable String defaultValue(); default OptionType getType() { return OptionType.STRING; } default Set getFlags() { return Collections.emptySet(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/options/OptionFlag.java ================================================ package jadx.api.plugins.options; public enum OptionFlag { /** * Store in project settings instead global (for jadx-gui) */ PER_PROJECT, /** * Do not show this option in jadx-gui (useful if option is configured with custom ui) */ HIDE_IN_GUI, /** * Option will be read-only in jadx-gui (can be used for calculated properties) */ DISABLE_IN_GUI, /** * Add this flag only if the option does not affect generated code. * If added, option value change will not cause code cache reset. */ NOT_CHANGING_CODE, } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/options/OptionType.java ================================================ package jadx.api.plugins.options; public enum OptionType { STRING, NUMBER, BOOLEAN } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/options/impl/BaseOptionsParser.java ================================================ package jadx.api.plugins.options.impl; import java.util.Locale; import java.util.Map; import java.util.function.Function; import jadx.api.plugins.options.JadxPluginOptions; /** * Prefer {@link BasePluginOptionsBuilder} as a better way to init and parse options */ @Deprecated public abstract class BaseOptionsParser implements JadxPluginOptions { protected Map options; @Override public void setOptions(Map options) { this.options = options; parseOptions(); } public abstract void parseOptions(); public boolean getBooleanOption(String key, boolean defValue) { String val = options.get(key); if (val == null) { return defValue; } String valLower = val.toLowerCase(Locale.ROOT); if (valLower.equals("yes") || valLower.equals("true")) { return true; } if (valLower.equals("no") || valLower.equals("false")) { return false; } throw new IllegalArgumentException("Unknown value '" + val + "' for option '" + key + "'" + ", expect: 'yes' or 'no'"); } public T getOption(String key, Function parse, T defValue) { String val = options.get(key); if (val == null) { return defValue; } return parse.apply(val); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/options/impl/BasePluginOptionsBuilder.java ================================================ package jadx.api.plugins.options.impl; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.options.OptionDescription; import jadx.api.plugins.options.OptionFlag; import jadx.api.plugins.options.OptionType; /** * Base class for {@link JadxPluginOptions} implementation *

* Override {@link BasePluginOptionsBuilder#registerOptions()} method * and use *option methods to add option info. */ @SuppressWarnings("unused") public abstract class BasePluginOptionsBuilder implements JadxPluginOptions { private final List> options = new ArrayList<>(); public abstract void registerOptions(); public BasePluginOptionsBuilder() { registerOptions(); for (OptionData option : options) { option.validate(); } } public OptionBuilder option(String name) { return addOption(new OptionData<>(name)); } public OptionBuilder option(String name, Class optionType) { return addOption(new OptionData<>(name)); } public OptionBuilder boolOption(String name) { return addOption( new OptionData(name) .type(OptionType.BOOLEAN) .values(Arrays.asList(Boolean.TRUE, Boolean.FALSE)) .formatter(b -> b ? "yes" : "no") .parser(val -> parseBoolOption(name, val))); } public OptionBuilder strOption(String name) { return addOption( new OptionData(name) .type(OptionType.STRING) .formatter(v -> v) .parser(v -> v)); } public OptionBuilder intOption(String name) { return addOption( new OptionData(name) .type(OptionType.NUMBER) .formatter(Object::toString) .parser(Integer::parseInt)); } public > OptionBuilder enumOption(String name, E[] values, Function valueOf) { return addOption( new OptionData(name) .type(OptionType.STRING) .values(Arrays.asList(values)) .formatter(v -> v.name().toLowerCase(Locale.ROOT)) .parser(v -> valueOf.apply(v.toUpperCase(Locale.ROOT)))); } @Override public void setOptions(Map map) { for (OptionData option : options) { parseOption(option, map.get(option.name)); } } @Override public List getOptionsDescriptions() { return Collections.unmodifiableList(options); } private static void parseOption(OptionData option, @Nullable String value) { T parsedValue; if (value == null) { parsedValue = option.defaultValue; } else { try { parsedValue = option.getParser().apply(value); } catch (Exception e) { throw new RuntimeException("Parse failed for option: " + option.name + ", value: " + value, e); } } try { option.getSetter().accept(parsedValue); } catch (Exception e) { throw new RuntimeException("Setter invoke failed for option: " + option.name + ", value: " + parsedValue, e); } } private static boolean parseBoolOption(String name, String val) { String valLower = val.trim().toLowerCase(Locale.ROOT); if (valLower.equals("yes") || valLower.equals("true")) { return true; } if (valLower.equals("no") || valLower.equals("false")) { return false; } throw new IllegalArgumentException("Unknown value '" + val + "' for option '" + name + "', expect: 'yes' or 'no'"); } private OptionBuilder addOption(OptionBuilder optionData) { this.options.add((OptionData) optionData); return optionData; } protected static class OptionData implements OptionDescription, OptionBuilder { private final String name; private String desc; private List values = Collections.emptyList(); private OptionType type = OptionType.STRING; private Set flags = EnumSet.noneOf(OptionFlag.class); private Function parser; private Function formatter; private Consumer setter; private T defaultValue; public OptionData(String name) { this.name = name; } @Override public String name() { return name; } @Override public String description() { return desc; } @Override public List values() { return values.stream().map(formatter).collect(Collectors.toList()); } @Override public @Nullable String defaultValue() { return formatter.apply(defaultValue); } @Override public OptionType getType() { return type; } @Override public Set getFlags() { return flags; } @Override public OptionBuilder description(String desc) { this.desc = desc; return this; } @Override public OptionBuilder defaultValue(@Nullable T defValue) { this.defaultValue = defValue; return this; } @Override public OptionBuilder parser(Function parser) { this.parser = parser; return this; } @Override public OptionBuilder formatter(Function formatter) { this.formatter = formatter; return this; } @Override public OptionBuilder setter(Consumer setter) { this.setter = setter; return this; } @Override public OptionBuilder type(OptionType optionType) { this.type = optionType; return this; } @Override public OptionBuilder flags(OptionFlag... flags) { this.flags = EnumSet.copyOf(Arrays.asList(flags)); return this; } @Override public OptionBuilder values(List values) { this.values = values; return this; } public Function getParser() { return parser; } public Function getFormatter() { return formatter; } public Consumer getSetter() { return setter; } public void validate() { if (desc == null || desc.isEmpty()) { throw new IllegalArgumentException("Description should be set for option: " + name); } if (parser == null) { throw new IllegalArgumentException("Parser should be set for option: " + name); } if (formatter == null) { throw new IllegalArgumentException("Formatter should be set for option: " + name); } if (setter == null) { throw new IllegalArgumentException("Setter should be set for option: " + name); } } } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java ================================================ package jadx.api.plugins.options.impl; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.options.OptionDescription; import jadx.api.plugins.options.OptionFlag; import jadx.api.plugins.options.OptionType; public class JadxOptionDescription implements OptionDescription { public static JadxOptionDescription booleanOption(String name, String desc, boolean defaultValue) { return new JadxOptionDescription(name, desc, defaultValue ? "yes" : "no", Arrays.asList("yes", "no"), OptionType.BOOLEAN); } private final String name; private final String desc; private final String defaultValue; private final List values; private final OptionType type; private final Set flags = EnumSet.noneOf(OptionFlag.class); public JadxOptionDescription(String name, String desc, @Nullable String defaultValue, List values) { this(name, desc, defaultValue, values, OptionType.STRING); } public JadxOptionDescription(String name, String desc, @Nullable String defaultValue, List values, OptionType type) { this.name = name; this.desc = desc; this.defaultValue = defaultValue; this.values = values; this.type = type; } @Override public String name() { return name; } @Override public String description() { return desc; } @Override public @Nullable String defaultValue() { return defaultValue; } @Override public List values() { return values; } @Override public OptionType getType() { return type; } @Override public Set getFlags() { return flags; } public JadxOptionDescription withFlag(OptionFlag flag) { this.flags.add(flag); return this; } public JadxOptionDescription withFlags(OptionFlag... flags) { Collections.addAll(this.flags, flags); return this; } @Override public String toString() { return "OptionDescription{" + desc + ", values=" + values + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/options/impl/OptionBuilder.java ================================================ package jadx.api.plugins.options.impl; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import jadx.api.plugins.options.OptionFlag; import jadx.api.plugins.options.OptionType; public interface OptionBuilder { /** * Option description (required) */ OptionBuilder description(String desc); OptionBuilder defaultValue(T defValue); /** * Function to parse input string into option value (required) */ OptionBuilder parser(Function parser); /** * Function to format option value into string for build help (required) */ OptionBuilder formatter(Function formatter); /** * Function to save/apply parsed option value (required) */ OptionBuilder setter(Consumer setter); /** * Possible option values */ OptionBuilder values(List values); OptionBuilder type(OptionType optionType); OptionBuilder flags(OptionFlag... flags); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/pass/JadxPass.java ================================================ package jadx.api.plugins.pass; import jadx.api.plugins.pass.types.JadxPassType; public interface JadxPass { JadxPassInfo getInfo(); JadxPassType getPassType(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/pass/JadxPassInfo.java ================================================ package jadx.api.plugins.pass; import java.util.List; public interface JadxPassInfo { /** * Add this to 'run after' list to place pass before others */ String START = "start"; /** * Add this to 'run before' list to place pass at end */ String END = "end"; /** * Pass short id, should be unique. */ String getName(); /** * Pass description */ String getDescription(); /** * This pass will be executed after these passes. * Passes names list. */ List runAfter(); /** * This pass will be executed before these passes. * Passes names list. */ List runBefore(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/pass/impl/OrderedJadxPassInfo.java ================================================ package jadx.api.plugins.pass.impl; import java.util.ArrayList; import java.util.List; import jadx.api.plugins.pass.JadxPassInfo; public class OrderedJadxPassInfo implements JadxPassInfo { private final String name; private final String desc; private final List runAfter; private final List runBefore; public OrderedJadxPassInfo(String name, String desc) { this(name, desc, new ArrayList<>(), new ArrayList<>()); } public OrderedJadxPassInfo(String name, String desc, List runAfter, List runBefore) { this.name = name; this.desc = desc; this.runAfter = runAfter; this.runBefore = runBefore; } public OrderedJadxPassInfo after(String pass) { runAfter.add(pass); return this; } public OrderedJadxPassInfo before(String pass) { runBefore.add(pass); return this; } @Override public String getName() { return name; } @Override public String getDescription() { return desc; } @Override public List runAfter() { return runAfter; } @Override public List runBefore() { return runBefore; } @Override public String toString() { return "PassInfo{'" + name + '\'' + ", desc='" + desc + '\'' + ", runAfter=" + runAfter + ", runBefore=" + runBefore + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/pass/impl/SimpleAfterLoadPass.java ================================================ package jadx.api.plugins.pass.impl; import java.util.function.Consumer; import jadx.api.JadxDecompiler; import jadx.api.plugins.pass.JadxPassInfo; import jadx.api.plugins.pass.types.JadxAfterLoadPass; public class SimpleAfterLoadPass implements JadxAfterLoadPass { private final JadxPassInfo info; private final Consumer init; public SimpleAfterLoadPass(String name, Consumer init) { this.info = new SimpleJadxPassInfo(name); this.init = init; } @Override public JadxPassInfo getInfo() { return info; } @Override public void init(JadxDecompiler decompiler) { init.accept(decompiler); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/pass/impl/SimpleJadxPassInfo.java ================================================ package jadx.api.plugins.pass.impl; import java.util.Collections; import java.util.List; import jadx.api.plugins.pass.JadxPassInfo; public class SimpleJadxPassInfo implements JadxPassInfo { private final String name; private final String desc; public SimpleJadxPassInfo(String name) { this(name, name); } public SimpleJadxPassInfo(String name, String desc) { this.name = name; this.desc = desc; } @Override public String getName() { return name; } @Override public String getDescription() { return desc; } @Override public List runAfter() { return Collections.emptyList(); } @Override public List runBefore() { return Collections.emptyList(); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxAfterLoadPass.java ================================================ package jadx.api.plugins.pass.types; import jadx.api.JadxDecompiler; import jadx.api.plugins.pass.JadxPass; public interface JadxAfterLoadPass extends JadxPass { JadxPassType TYPE = new JadxPassType("AfterLoadPass"); void init(JadxDecompiler decompiler); @Override default JadxPassType getPassType() { return TYPE; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxDecompilePass.java ================================================ package jadx.api.plugins.pass.types; import jadx.api.plugins.pass.JadxPass; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; public interface JadxDecompilePass extends JadxPass { JadxPassType TYPE = new JadxPassType("DecompilePass"); void init(RootNode root); /** * Visit class * * @return false for disable child methods and inner classes traversal */ boolean visit(ClassNode cls); /** * Visit method */ void visit(MethodNode mth); @Override default JadxPassType getPassType() { return TYPE; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxPassType.java ================================================ package jadx.api.plugins.pass.types; public class JadxPassType { private final String cls; public JadxPassType(String clsName) { this.cls = clsName; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof JadxPassType)) { return false; } return cls.equals(((JadxPassType) o).cls); } @Override public int hashCode() { return cls.hashCode(); } @Override public String toString() { return cls; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/pass/types/JadxPreparePass.java ================================================ package jadx.api.plugins.pass.types; import jadx.api.plugins.pass.JadxPass; import jadx.core.dex.nodes.RootNode; public interface JadxPreparePass extends JadxPass { JadxPassType TYPE = new JadxPassType("PreparePass"); void init(RootNode root); @Override default JadxPassType getPassType() { return TYPE; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/resources/IResContainerFactory.java ================================================ package jadx.api.plugins.resources; import java.io.IOException; import java.io.InputStream; import org.jetbrains.annotations.Nullable; import jadx.api.ResourceFile; import jadx.core.dex.nodes.RootNode; import jadx.core.xmlgen.ResContainer; /** * Factory for {@link ResContainer}. Can be used in plugins via * {@code context.getResourcesLoader().addResContainerFactory()} to implement content parsing in * files with * different formats. */ public interface IResContainerFactory { /** * Optional init method */ default void init(RootNode root) { } /** * Checks if resource file is of expected format and tries to parse its content. * * @return {@link ResContainer} if file is of expected format, {@code null} otherwise. */ @Nullable ResContainer create(ResourceFile resFile, InputStream inputStream) throws IOException; } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/resources/IResTableParserProvider.java ================================================ package jadx.api.plugins.resources; import org.jetbrains.annotations.Nullable; import jadx.api.ResourceFile; import jadx.core.dex.nodes.RootNode; import jadx.core.xmlgen.IResTableParser; /** * Provides the resource table parser instance for specific resource table file format. Can be used * in plugins via {@code context.getResourcesLoader().addResTableParserProvider()} to parse * resources from tables * in different formats. */ public interface IResTableParserProvider { /** * Optional init method */ default void init(RootNode root) { } /** * Checks a file format and provides the instance if the format is expected. * * @return {@link IResTableParser} if resource table is of expected format, {@code null} otherwise. */ @Nullable IResTableParser getParser(ResourceFile resFile); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/resources/IResourcesLoader.java ================================================ package jadx.api.plugins.resources; public interface IResourcesLoader { void addResContainerFactory(IResContainerFactory resContainerFactory); void addResTableParserProvider(IResTableParserProvider resTableParserProvider); } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/utils/CommonFileUtils.java ================================================ package jadx.api.plugins.utils; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CommonFileUtils { private static final Logger LOG = LoggerFactory.getLogger(CommonFileUtils.class); public static final File CWD = getCWD(); public static final Path CWD_PATH = CWD.toPath(); private static File getCWD() { try { return new File(".").getCanonicalFile(); } catch (IOException e) { throw new RuntimeException("Failed to init current working dir constant", e); } } public static Path saveToTempFile(InputStream in, String suffix) throws IOException { return saveToTempFile(null, in, suffix); } public static Path saveToTempFile(byte[] dataPrefix, InputStream in, String suffix) throws IOException { Path tempFile = Files.createTempFile("jadx-temp-", suffix); try (OutputStream out = Files.newOutputStream(tempFile)) { if (dataPrefix != null) { out.write(dataPrefix); } copyStream(in, out); } catch (Exception e) { throw new IOException("Failed to save temp file", e); } return tempFile; } public static boolean safeDeleteFile(File file) { try { return file.delete(); } catch (Exception e) { LOG.warn("Failed to delete file: {}", file, e); return false; } } public static byte[] loadBytes(InputStream input) throws IOException { return loadBytes(null, input); } public static byte[] loadBytes(byte[] dataPrefix, InputStream in) throws IOException { int estimateSize = dataPrefix == null ? in.available() : dataPrefix.length + in.available(); try (ByteArrayOutputStream out = new ByteArrayOutputStream(estimateSize)) { if (dataPrefix != null) { out.write(dataPrefix); } copyStream(in, out); return out.toByteArray(); } catch (Exception e) { throw new IOException("Failed to read input stream to bytes array", e); } } public static void copyStream(InputStream input, OutputStream output) throws IOException { byte[] buffer = new byte[8192]; while (true) { int count = input.read(buffer); if (count == -1) { break; } output.write(buffer, 0, count); } } @Nullable public static String getFileExtension(String fileName) { int dotIndex = fileName.lastIndexOf('.'); if (dotIndex == -1) { return null; } return fileName.substring(dotIndex + 1); } public static String removeFileExtension(String fileName) { int dotIndex = fileName.lastIndexOf('.'); if (dotIndex == -1) { return fileName; } return fileName.substring(0, dotIndex); } private static final Set ZIP_FILE_EXTS = Utils.constSet("zip", "jar", "apk"); public static boolean isZipFileExt(String fileName) { String ext = getFileExtension(fileName); if (ext == null) { return false; } return ZIP_FILE_EXTS.contains(ext); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/utils/Utils.java ================================================ package jadx.api.plugins.utils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.function.Function; import org.jetbrains.annotations.Nullable; public class Utils { public static void addToList(Collection list, @Nullable T item) { if (item != null) { list.add(item); } } public static void addToList(Collection list, @Nullable I item, Function map) { if (item != null) { T value = map.apply(item); if (value != null) { list.add(value); } } } public static List concat(List a, List b) { int aSize = a.size(); int bSize = b.size(); if (aSize == 0 && bSize == 0) { return Collections.emptyList(); } if (aSize == 0) { return b; } if (bSize == 0) { return a; } List list = new ArrayList<>(aSize + bSize); list.addAll(a); list.addAll(b); return list; } public static List concatDistinct(List a, List b) { int aSize = a.size(); int bSize = b.size(); if (aSize == 0 && bSize == 0) { return Collections.emptyList(); } if (aSize == 0) { return b; } if (bSize == 0) { return a; } Set set = new LinkedHashSet<>(aSize + bSize); set.addAll(a); set.addAll(b); return new ArrayList<>(set); } public static String listToStr(List list) { if (list == null) { return "null"; } if (list.isEmpty()) { return ""; } if (list.size() == 1) { return Objects.toString(list.get(0)); } StringBuilder sb = new StringBuilder(); Iterator it = list.iterator(); sb.append(it.next()); while (it.hasNext()) { sb.append(", ").append(it.next()); } return sb.toString(); } public static String formatOffset(int offset) { return String.format("0x%04x", offset); } @SafeVarargs public static Set constSet(T... arr) { return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(arr))); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/plugins/utils/ZipSecurity.java ================================================ package jadx.api.plugins.utils; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.jetbrains.annotations.Nullable; import jadx.api.JadxDecompiler; import jadx.api.plugins.JadxPluginContext; import jadx.core.utils.Utils; import jadx.zip.IZipEntry; import jadx.zip.ZipReader; import jadx.zip.io.LimitedInputStream; import jadx.zip.security.DisabledZipSecurity; import jadx.zip.security.IJadxZipSecurity; import jadx.zip.security.JadxZipSecurity; /** * Deprecated, migrate to {@link ZipReader}.
* Prefer already configured instance from {@link JadxDecompiler#getZipReader()} or * {@link JadxPluginContext#getZipReader()}. */ @Deprecated public class ZipSecurity { private static final boolean DISABLE_CHECKS = Utils.getEnvVarBool("JADX_DISABLE_ZIP_SECURITY", false); private static final int MAX_ENTRIES_COUNT = Utils.getEnvVarInt("JADX_ZIP_MAX_ENTRIES_COUNT", 100_000); private static final IJadxZipSecurity ZIP_SECURITY = buildZipSecurity(); private static final ZipReader ZIP_READER = new ZipReader(ZIP_SECURITY); private static IJadxZipSecurity buildZipSecurity() { if (DISABLE_CHECKS) { return DisabledZipSecurity.INSTANCE; } JadxZipSecurity jadxZipSecurity = new JadxZipSecurity(); jadxZipSecurity.setMaxEntriesCount(MAX_ENTRIES_COUNT); return jadxZipSecurity; } private ZipSecurity() { } public static boolean isInSubDirectory(File baseDir, File file) { return ZIP_SECURITY.isInSubDirectory(baseDir, file); } /** * Checks that entry name contains no any traversals and prevents cases like "../classes.dex", * to limit output only to the specified directory */ public static boolean isValidZipEntryName(String entryName) { return ZIP_SECURITY.isValidEntryName(entryName); } public static boolean isZipBomb(IZipEntry entry) { return !ZIP_SECURITY.isValidEntry(entry); } public static boolean isValidZipEntry(IZipEntry entry) { return ZIP_SECURITY.isValidEntry(entry); } public static InputStream getInputStreamForEntry(ZipFile zipFile, ZipEntry entry) throws IOException { if (DISABLE_CHECKS) { return new BufferedInputStream(zipFile.getInputStream(entry)); } InputStream in = zipFile.getInputStream(entry); LimitedInputStream limited = new LimitedInputStream(in, entry.getSize()); return new BufferedInputStream(limited); } /** * Visit valid entries in a zip file. * Return not null value from visitor to stop iteration. */ @Nullable public static R visitZipEntries(File file, Function visitor) { return ZIP_READER.visitEntries(file, visitor); } public static void readZipEntries(File file, BiConsumer visitor) { ZIP_READER.readEntries(file, visitor); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/resources/ResourceContentType.java ================================================ package jadx.api.resources; public enum ResourceContentType { CONTENT_TEXT, CONTENT_BINARY, CONTENT_NONE, CONTENT_UNKNOWN, } ================================================ FILE: jadx-core/src/main/java/jadx/api/security/IJadxSecurity.java ================================================ package jadx.api.security; import java.io.InputStream; import org.w3c.dom.Document; import jadx.zip.security.IJadxZipSecurity; public interface IJadxSecurity extends IJadxZipSecurity { /** * Check if application package is safe * * @return normalized/sanitized string or same string if safe */ String verifyAppPackage(String appPackage); /** * XML document parser */ Document parseXml(InputStream in); } ================================================ FILE: jadx-core/src/main/java/jadx/api/security/JadxSecurityFlag.java ================================================ package jadx.api.security; import java.util.EnumSet; import java.util.Set; public enum JadxSecurityFlag { VERIFY_APP_PACKAGE, SECURE_XML_PARSER, SECURE_ZIP_READER; public static Set all() { return EnumSet.allOf(JadxSecurityFlag.class); } public static Set none() { return EnumSet.noneOf(JadxSecurityFlag.class); } } ================================================ FILE: jadx-core/src/main/java/jadx/api/security/impl/JadxSecurity.java ================================================ package jadx.api.security.impl; import java.io.File; import java.io.InputStream; import java.util.Set; import javax.xml.parsers.DocumentBuilderFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import jadx.api.security.IJadxSecurity; import jadx.api.security.JadxSecurityFlag; import jadx.core.deobf.NameMapper; import jadx.zip.IZipEntry; import jadx.zip.security.DisabledZipSecurity; import jadx.zip.security.IJadxZipSecurity; import jadx.zip.security.JadxZipSecurity; import static jadx.api.security.JadxSecurityFlag.SECURE_ZIP_READER; public class JadxSecurity implements IJadxSecurity { private static final Logger LOG = LoggerFactory.getLogger(JadxSecurity.class); private final Set flags; private final IJadxZipSecurity zipSecurity; public JadxSecurity(Set flags) { this.flags = flags; this.zipSecurity = flags.contains(SECURE_ZIP_READER) ? new JadxZipSecurity() : DisabledZipSecurity.INSTANCE; } public JadxSecurity(Set flags, IJadxZipSecurity zipSecurity) { this.flags = flags; this.zipSecurity = zipSecurity; } @Override public boolean isValidEntry(IZipEntry entry) { return zipSecurity.isValidEntry(entry); } @Override public boolean isValidEntryName(String entryName) { return zipSecurity.isValidEntryName(entryName); } @Override public boolean isInSubDirectory(File baseDir, File file) { return zipSecurity.isInSubDirectory(baseDir, file); } @Override public boolean useLimitedDataStream() { return zipSecurity.useLimitedDataStream(); } @Override public int getMaxEntriesCount() { return zipSecurity.getMaxEntriesCount(); } @Override public String verifyAppPackage(String appPackage) { if (flags.contains(JadxSecurityFlag.VERIFY_APP_PACKAGE) && !NameMapper.isValidFullIdentifier(appPackage)) { LOG.warn("App package '{}' has invalid format and will be ignored", appPackage); return "INVALID_PACKAGE"; } return appPackage; } @Override public Document parseXml(InputStream in) { DocumentBuilderFactory dbf; if (flags.contains(JadxSecurityFlag.SECURE_XML_PARSER)) { dbf = SecureDBFHolder.INSTANCE; } else { dbf = SimpleDBFHolder.INSTANCE; } try { return dbf.newDocumentBuilder().parse(in); } catch (Exception e) { throw new RuntimeException("Failed to parse xml", e); } } private static final class SimpleDBFHolder { private static final DocumentBuilderFactory INSTANCE = DocumentBuilderFactory.newInstance(); } private static final class SecureDBFHolder { private static final DocumentBuilderFactory INSTANCE = buildSecureDBF(); private static DocumentBuilderFactory buildSecureDBF() { try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); dbf.setFeature("http://apache.org/xml/features/dom/create-entity-ref-nodes", false); dbf.setXIncludeAware(false); dbf.setExpandEntityReferences(false); return dbf; } catch (Exception e) { throw new RuntimeException("Fail to build secure XML DocumentBuilderFactory", e); } } } } ================================================ FILE: jadx-core/src/main/java/jadx/api/usage/IUsageInfoCache.java ================================================ package jadx.api.usage; import java.io.Closeable; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.RootNode; public interface IUsageInfoCache extends Closeable { @Nullable IUsageInfoData get(RootNode root); void set(RootNode root, IUsageInfoData data); } ================================================ FILE: jadx-core/src/main/java/jadx/api/usage/IUsageInfoData.java ================================================ package jadx.api.usage; import jadx.core.dex.nodes.ClassNode; public interface IUsageInfoData { void apply(); void applyForClass(ClassNode cls); void visitUsageData(IUsageInfoVisitor visitor); } ================================================ FILE: jadx-core/src/main/java/jadx/api/usage/IUsageInfoVisitor.java ================================================ package jadx.api.usage; import java.util.List; import jadx.api.plugins.input.data.IMethodRef; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; public interface IUsageInfoVisitor { void visitClassDeps(ClassNode cls, List deps); void visitClassUsage(ClassNode cls, List usage); void visitClassUseInMethods(ClassNode cls, List methods); void visitFieldsUsage(FieldNode fld, List methods); void visitMethodsUsage(MethodNode mth, List methods); void visitMethodsUses(MethodNode mth, List methods); void visitUnresolvedMethodsUsage(MethodNode mth, List methods); void visitIsSelfCall(MethodNode mth, boolean isSelfCall); void visitComplete(); } ================================================ FILE: jadx-core/src/main/java/jadx/api/usage/impl/EmptyUsageInfoCache.java ================================================ package jadx.api.usage.impl; import java.io.IOException; import org.jetbrains.annotations.Nullable; import jadx.api.usage.IUsageInfoCache; import jadx.api.usage.IUsageInfoData; import jadx.core.dex.nodes.RootNode; public class EmptyUsageInfoCache implements IUsageInfoCache { @Override public @Nullable IUsageInfoData get(RootNode root) { return null; } @Override public void set(RootNode root, IUsageInfoData data) { } @Override public void close() throws IOException { } } ================================================ FILE: jadx-core/src/main/java/jadx/api/usage/impl/InMemoryUsageInfoCache.java ================================================ package jadx.api.usage.impl; import org.jetbrains.annotations.Nullable; import jadx.api.usage.IUsageInfoCache; import jadx.api.usage.IUsageInfoData; import jadx.core.dex.nodes.RootNode; public class InMemoryUsageInfoCache implements IUsageInfoCache { private IUsageInfoData data; /** * `data` field tied to root node instance, keep hash to reset cache on change */ private int rootNodeHash; @Override public @Nullable IUsageInfoData get(RootNode root) { return rootNodeHash == root.hashCode() ? data : null; } @Override public void set(RootNode root, IUsageInfoData data) { this.rootNodeHash = root.hashCode(); this.data = data; } @Override public void close() { this.rootNodeHash = 0; this.data = null; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/utils/CodeUtils.java ================================================ package jadx.api.utils; import java.util.function.BiFunction; import jadx.api.ICodeInfo; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.core.dex.nodes.MethodNode; public class CodeUtils { public static String getLineForPos(String code, int pos) { int start = getLineStartForPos(code, pos); int end = getLineEndForPos(code, pos); return code.substring(start, end); } public static int getLineStartForPos(String code, int pos) { int start = getNewLinePosBefore(code, pos); return start == -1 ? 0 : start + 1; } public static int getLineEndForPos(String code, int pos) { int end = getNewLinePosAfter(code, pos); return end == -1 ? code.length() : end; } public static int getNewLinePosAfter(String code, int startPos) { int pos = code.indexOf('\n', startPos); if (pos != -1) { // check for '\r\n' int prev = pos - 1; if (code.charAt(prev) == '\r') { return prev; } } return pos; } public static int getNewLinePosBefore(String code, int startPos) { return code.lastIndexOf('\n', startPos); } public static int getLineNumForPos(String code, int pos, String newLine) { int newLineLen = newLine.length(); int line = 1; int prev = 0; while (true) { int next = code.indexOf(newLine, prev); if (next >= pos) { return line; } prev = next + newLineLen; line++; } } /** * Cut method code (including comments and annotations) from class code. * * @return method code or empty string if metadata is not available */ public static String extractMethodCode(MethodNode mth, ICodeInfo codeInfo) { int end = getMethodEnd(mth, codeInfo); if (end == -1) { return ""; } int start = getMethodStart(mth, codeInfo); if (end < start) { return ""; } return codeInfo.getCodeStr().substring(start, end); } /** * Search first empty line before method definition to include comments and annotations */ private static int getMethodStart(MethodNode mth, ICodeInfo codeInfo) { int pos = mth.getDefPosition(); String newLineStr = mth.root().getArgs().getCodeNewLineStr(); String emptyLine = newLineStr + newLineStr; int emptyLinePos = codeInfo.getCodeStr().lastIndexOf(emptyLine, pos); return emptyLinePos == -1 ? pos : emptyLinePos + emptyLine.length(); } /** * Search method end position in provided class code info. * * @return end pos or -1 if metadata not available */ public static int getMethodEnd(MethodNode mth, ICodeInfo codeInfo) { if (!codeInfo.hasMetadata()) { return -1; } // skip nested nodes DEF/END until first unpaired END annotation (end of this method) Integer end = codeInfo.getCodeMetadata().searchDown(mth.getDefPosition() + 1, new BiFunction<>() { int nested = 0; @Override public Integer apply(Integer pos, ICodeAnnotation ann) { switch (ann.getAnnType()) { case DECLARATION: ICodeNodeRef node = ((NodeDeclareRef) ann).getNode(); switch (node.getAnnType()) { case CLASS: case METHOD: nested++; break; } break; case END: if (nested == 0) { return pos; } nested--; break; } return null; } }); return end == null ? -1 : end; } } ================================================ FILE: jadx-core/src/main/java/jadx/api/utils/tasks/ITaskExecutor.java ================================================ package jadx.api.utils.tasks; import java.util.List; import java.util.concurrent.ExecutorService; import org.jetbrains.annotations.Nullable; /** * Schedule and execute tasks combined into stages * with parallel or sequential execution (similar to the fork-join pattern). */ public interface ITaskExecutor { /** * Add parallel stage with provided tasks */ void addParallelTasks(List parallelTasks); /** * Add sequential stage with provided tasks */ void addSequentialTasks(List seqTasks); /** * Add sequential stage with a single task */ void addSequentialTask(Runnable task); /** * Scheduled tasks count */ int getTasksCount(); /** * Set threads count for parallel stage. * Can be changed during execution. * Defaults to half of processors count. */ void setThreadsCount(int threadsCount); int getThreadsCount(); /** * Start tasks execution. */ void execute(); int getProgress(); /** * Not started tasks will be not executed after this method invocation. */ void terminate(); boolean isTerminating(); boolean isRunning(); /** * Block until execution is finished */ void awaitTermination(); /** * Return internal executor service. */ @Nullable ExecutorService getInternalExecutor(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/Consts.java ================================================ package jadx.core; public class Consts { public static final boolean DEBUG = false; public static final boolean DEBUG_WITH_ERRORS = false; // TODO: fix errors public static final boolean DEBUG_USAGE = false; public static final boolean DEBUG_TYPE_INFERENCE = false; public static final boolean DEBUG_OVERLOADED_CASTS = false; public static final boolean DEBUG_EXC_HANDLERS = false; public static final boolean DEBUG_FINALLY = false; public static final boolean DEBUG_ATTRIBUTES = false; public static final boolean DEBUG_RESTRUCTURE = false; public static final boolean DEBUG_EVENTS = Jadx.isDevVersion(); public static final String CLASS_OBJECT = "java.lang.Object"; public static final String CLASS_STRING = "java.lang.String"; public static final String CLASS_CLASS = "java.lang.Class"; public static final String CLASS_THROWABLE = "java.lang.Throwable"; public static final String CLASS_ERROR = "java.lang.Error"; public static final String CLASS_EXCEPTION = "java.lang.Exception"; public static final String CLASS_RUNTIME_EXCEPTION = "java.lang.RuntimeException"; public static final String CLASS_ENUM = "java.lang.Enum"; public static final String CLASS_STRING_BUILDER = "java.lang.StringBuilder"; public static final String OVERRIDE_ANNOTATION = "Ljava/lang/Override;"; public static final String DEFAULT_PACKAGE_NAME = "defpackage"; public static final String ANONYMOUS_CLASS_PREFIX = "AnonymousClass"; public static final String MTH_TOSTRING_SIGNATURE = "toString()Ljava/lang/String;"; private Consts() { } } ================================================ FILE: jadx-core/src/main/java/jadx/core/Jadx.java ================================================ package jadx.core; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.jar.Manifest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.CommentsLevel; import jadx.api.JadxArgs; import jadx.core.deobf.DeobfuscatorVisitor; import jadx.core.deobf.SaveDeobfMapping; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.visitors.AdjustForIfMergeVisitor; import jadx.core.dex.visitors.AnonymousClassVisitor; import jadx.core.dex.visitors.ApplyVariableNames; import jadx.core.dex.visitors.AttachCommentsVisitor; import jadx.core.dex.visitors.AttachMethodDetails; import jadx.core.dex.visitors.AttachTryCatchVisitor; import jadx.core.dex.visitors.CheckCode; import jadx.core.dex.visitors.ClassModifier; import jadx.core.dex.visitors.ConstInlineVisitor; import jadx.core.dex.visitors.ConstructorVisitor; import jadx.core.dex.visitors.DeboxingVisitor; import jadx.core.dex.visitors.DotGraphVisitor; import jadx.core.dex.visitors.EnumVisitor; import jadx.core.dex.visitors.ExtractFieldInit; import jadx.core.dex.visitors.FallbackModeVisitor; import jadx.core.dex.visitors.FixSwitchOverEnum; import jadx.core.dex.visitors.GenericTypesVisitor; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.InitCodeVariables; import jadx.core.dex.visitors.InlineMethods; import jadx.core.dex.visitors.MarkMethodsForInline; import jadx.core.dex.visitors.MethodInvokeVisitor; import jadx.core.dex.visitors.MethodThrowsVisitor; import jadx.core.dex.visitors.MethodVisitor; import jadx.core.dex.visitors.ModVisitor; import jadx.core.dex.visitors.MoveInlineVisitor; import jadx.core.dex.visitors.OverrideMethodVisitor; import jadx.core.dex.visitors.PrepareForCodeGen; import jadx.core.dex.visitors.ProcessAnonymous; import jadx.core.dex.visitors.ProcessInstructionsVisitor; import jadx.core.dex.visitors.ProcessMethodsForInline; import jadx.core.dex.visitors.ReplaceNewArray; import jadx.core.dex.visitors.ShadowFieldVisitor; import jadx.core.dex.visitors.SignatureProcessor; import jadx.core.dex.visitors.SimplifyVisitor; import jadx.core.dex.visitors.blocks.BlockFinisher; import jadx.core.dex.visitors.blocks.BlockProcessor; import jadx.core.dex.visitors.blocks.BlockSplitter; import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor; import jadx.core.dex.visitors.debuginfo.DebugInfoAttachVisitor; import jadx.core.dex.visitors.finaly.MarkFinallyVisitor; import jadx.core.dex.visitors.fixaccessmodifiers.FixAccessModifiers; import jadx.core.dex.visitors.gradle.NonFinalResIdsVisitor; import jadx.core.dex.visitors.kotlin.ProcessKotlinInternals; import jadx.core.dex.visitors.prepare.AddAndroidConstants; import jadx.core.dex.visitors.prepare.CollectConstValues; import jadx.core.dex.visitors.regions.CheckRegions; import jadx.core.dex.visitors.regions.CleanRegions; import jadx.core.dex.visitors.regions.IfRegionVisitor; import jadx.core.dex.visitors.regions.LoopRegionVisitor; import jadx.core.dex.visitors.regions.RegionMakerVisitor; import jadx.core.dex.visitors.regions.ReturnVisitor; import jadx.core.dex.visitors.regions.SwitchBreakVisitor; import jadx.core.dex.visitors.regions.SwitchOverStringVisitor; import jadx.core.dex.visitors.regions.variables.ProcessVariables; import jadx.core.dex.visitors.rename.CodeRenameVisitor; import jadx.core.dex.visitors.rename.RenameVisitor; import jadx.core.dex.visitors.rename.SourceFileRename; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.dex.visitors.ssa.SSATransform; import jadx.core.dex.visitors.typeinference.FinishTypeInference; import jadx.core.dex.visitors.typeinference.FixTypesVisitor; import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; import jadx.core.dex.visitors.usage.UsageInfoVisitor; import jadx.core.utils.exceptions.JadxRuntimeException; public class Jadx { private static final Logger LOG = LoggerFactory.getLogger(Jadx.class); private Jadx() { } public static List getPassesList(JadxArgs args) { switch (args.getDecompilationMode()) { case AUTO: case RESTRUCTURE: return getRegionsModePasses(args); case SIMPLE: return getSimpleModePasses(args); case FALLBACK: return getFallbackPassesList(); default: throw new JadxRuntimeException("Unknown decompilation mode: " + args.getDecompilationMode()); } } public static List getPreDecompilePassesList() { List passes = new ArrayList<>(); passes.add(new SignatureProcessor()); passes.add(new OverrideMethodVisitor()); passes.add(new AddAndroidConstants()); // rename and deobfuscation passes.add(new DeobfuscatorVisitor()); passes.add(new SourceFileRename()); passes.add(new RenameVisitor()); passes.add(new SaveDeobfMapping()); passes.add(new UsageInfoVisitor()); passes.add(new CollectConstValues()); passes.add(new ProcessAnonymous()); passes.add(new ProcessMethodsForInline()); return passes; } public static List getRegionsModePasses(JadxArgs args) { List passes = new ArrayList<>(); // instructions IR passes.add(new CheckCode()); if (args.isDebugInfo()) { passes.add(new DebugInfoAttachVisitor()); } passes.add(new AttachTryCatchVisitor()); if (args.getCommentsLevel() != CommentsLevel.NONE) { passes.add(new AttachCommentsVisitor()); } passes.add(new AttachMethodDetails()); passes.add(new ProcessInstructionsVisitor()); // blocks IR passes.add(new BlockSplitter()); passes.add(new BlockProcessor()); passes.add(new BlockFinisher()); if (args.isRawCFGOutput()) { passes.add(DotGraphVisitor.dumpRaw()); } passes.add(new SSATransform()); passes.add(new MoveInlineVisitor()); passes.add(new ConstructorVisitor()); passes.add(new InitCodeVariables()); if (args.isExtractFinally()) { passes.add(new MarkFinallyVisitor()); } passes.add(new ConstInlineVisitor()); passes.add(new TypeInferenceVisitor()); if (args.isDebugInfo()) { passes.add(new DebugInfoApplyVisitor()); } passes.add(new FixTypesVisitor()); passes.add(new FinishTypeInference()); passes.add(new AdjustForIfMergeVisitor()); if (args.getUseKotlinMethodsForVarNames() != JadxArgs.UseKotlinMethodsForVarNames.DISABLE) { passes.add(new ProcessKotlinInternals()); } passes.add(new CodeRenameVisitor()); if (args.isInlineMethods()) { passes.add(new InlineMethods()); } passes.add(new GenericTypesVisitor()); passes.add(new ShadowFieldVisitor()); passes.add(new DeboxingVisitor()); passes.add(new AnonymousClassVisitor()); passes.add(new ModVisitor()); passes.add(new CodeShrinkVisitor()); passes.add(new ReplaceNewArray()); if (args.isCfgOutput()) { passes.add(DotGraphVisitor.dump()); } // regions IR passes.add(new RegionMakerVisitor()); passes.add(new IfRegionVisitor()); if (args.isRestoreSwitchOverString()) { passes.add(new SwitchOverStringVisitor()); } passes.add(new ReturnVisitor()); passes.add(new CleanRegions()); passes.add(new MethodThrowsVisitor()); passes.add(new CodeShrinkVisitor()); passes.add(new MethodInvokeVisitor()); passes.add(new SimplifyVisitor()); passes.add(new CheckRegions()); passes.add(new EnumVisitor()); passes.add(new FixSwitchOverEnum()); passes.add(new NonFinalResIdsVisitor()); passes.add(new ExtractFieldInit()); passes.add(new FixAccessModifiers()); passes.add(new ClassModifier()); passes.add(new LoopRegionVisitor()); passes.add(new SwitchBreakVisitor()); if (args.isInlineMethods()) { passes.add(new MarkMethodsForInline()); } passes.add(new ProcessVariables()); passes.add(new ApplyVariableNames()); passes.add(new PrepareForCodeGen()); if (args.isCfgOutput()) { passes.add(DotGraphVisitor.dumpRegions()); } return passes; } public static List getSimpleModePasses(JadxArgs args) { List passes = new ArrayList<>(); if (args.isDebugInfo()) { passes.add(new DebugInfoAttachVisitor()); } passes.add(new AttachTryCatchVisitor()); if (args.getCommentsLevel() != CommentsLevel.NONE) { passes.add(new AttachCommentsVisitor()); } passes.add(new AttachMethodDetails()); passes.add(new ProcessInstructionsVisitor()); passes.add(new BlockSplitter()); if (args.isRawCFGOutput()) { passes.add(DotGraphVisitor.dumpRaw()); } passes.add(new MethodVisitor("DisableBlockLock", mth -> mth.add(AFlag.DISABLE_BLOCKS_LOCK))); passes.add(new BlockProcessor()); passes.add(new SSATransform()); passes.add(new MoveInlineVisitor()); passes.add(new ConstructorVisitor()); passes.add(new InitCodeVariables()); passes.add(new ConstInlineVisitor()); passes.add(new TypeInferenceVisitor()); if (args.isDebugInfo()) { passes.add(new DebugInfoApplyVisitor()); } passes.add(new FixTypesVisitor()); passes.add(new FinishTypeInference()); passes.add(new CodeRenameVisitor()); passes.add(new DeboxingVisitor()); passes.add(new ModVisitor()); passes.add(new CodeShrinkVisitor()); passes.add(new ReplaceNewArray()); passes.add(new SimplifyVisitor()); passes.add(new MethodVisitor("ForceGenerateAll", mth -> mth.remove(AFlag.DONT_GENERATE))); if (args.isCfgOutput()) { passes.add(DotGraphVisitor.dump()); } return passes; } public static List getFallbackPassesList() { List passes = new ArrayList<>(); passes.add(new AttachTryCatchVisitor()); passes.add(new AttachCommentsVisitor()); passes.add(new ProcessInstructionsVisitor()); passes.add(new FallbackModeVisitor()); return passes; } public static final String VERSION_DEV = "dev"; private static String version; public static String getVersion() { if (version == null) { version = searchJadxVersion(); } return version; } public static boolean isDevVersion() { return getVersion().equals(VERSION_DEV); } private static String searchJadxVersion() { try { ClassLoader classLoader = Jadx.class.getClassLoader(); if (classLoader != null) { Enumeration resources = classLoader.getResources("META-INF/MANIFEST.MF"); while (resources.hasMoreElements()) { try (InputStream is = resources.nextElement().openStream()) { Manifest manifest = new Manifest(is); String ver = manifest.getMainAttributes().getValue("jadx-version"); if (ver != null) { return ver; } } } } } catch (Exception e) { LOG.error("Can't get manifest file", e); } return VERSION_DEV; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/ProcessClass.java ================================================ package jadx.core; import java.util.EnumMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.DecompilationMode; import jadx.api.ICodeInfo; import jadx.api.JadxArgs; import jadx.api.impl.SimpleCodeInfo; import jadx.core.codegen.CodeGen; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.LoadStage; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.dex.nodes.ProcessState.GENERATED_AND_UNLOADED; import static jadx.core.dex.nodes.ProcessState.LOADED; import static jadx.core.dex.nodes.ProcessState.NOT_LOADED; import static jadx.core.dex.nodes.ProcessState.PROCESS_COMPLETE; import static jadx.core.dex.nodes.ProcessState.PROCESS_STARTED; public class ProcessClass { private static final Logger LOG = LoggerFactory.getLogger(ProcessClass.class); private static final ICodeInfo NOT_GENERATED = new SimpleCodeInfo(""); private final List passes; public ProcessClass(List passesList) { this.passes = passesList; } @Nullable private ICodeInfo process(ClassNode cls, boolean codegen) { if (!codegen && cls.getState() == PROCESS_COMPLETE) { // nothing to do return null; } Utils.checkThreadInterrupt(); synchronized (cls.getClassInfo()) { try { if (cls.contains(AFlag.CLASS_DEEP_RELOAD)) { cls.remove(AFlag.CLASS_DEEP_RELOAD); cls.deepUnload(); cls.add(AFlag.CLASS_UNLOADED); } if (cls.contains(AFlag.CLASS_UNLOADED)) { cls.root().runPreDecompileStageForClass(cls); cls.remove(AFlag.CLASS_UNLOADED); } if (cls.getState() == GENERATED_AND_UNLOADED) { // force loading code again cls.setState(NOT_LOADED); } if (codegen) { cls.setLoadStage(LoadStage.CODEGEN_STAGE); if (cls.contains(AFlag.RELOAD_AT_CODEGEN_STAGE)) { cls.remove(AFlag.RELOAD_AT_CODEGEN_STAGE); cls.unload(); } } else { cls.setLoadStage(LoadStage.PROCESS_STAGE); } if (cls.getState() == NOT_LOADED) { cls.load(); } if (cls.getState() == LOADED) { cls.setState(PROCESS_STARTED); for (IDexTreeVisitor visitor : passes) { DepthTraversal.visit(visitor, cls); } cls.setState(PROCESS_COMPLETE); } if (codegen) { Utils.checkThreadInterrupt(); ICodeInfo code = CodeGen.generate(cls); if (!cls.contains(AFlag.DONT_UNLOAD_CLASS)) { cls.unload(); cls.setState(GENERATED_AND_UNLOADED); } return code; } return null; } catch (StackOverflowError | Exception e) { if (codegen) { throw e; } cls.addError("Class process error: " + e.getClass().getSimpleName(), e); return null; } } } @NotNull public ICodeInfo generateCode(ClassNode cls) { ClassNode topParentClass = cls.getTopParentClass(); if (topParentClass != cls) { return generateCode(topParentClass); } try { if (cls.contains(AFlag.DONT_GENERATE)) { process(cls, false); return NOT_GENERATED; } for (ClassNode depCls : cls.getDependencies()) { process(depCls, false); } if (!cls.getCodegenDeps().isEmpty()) { process(cls, false); for (ClassNode codegenDep : cls.getCodegenDeps()) { process(codegenDep, false); } } ICodeInfo code = process(cls, true); if (code == null) { throw new JadxRuntimeException("Codegen failed"); } return code; } catch (StackOverflowError | Exception e) { throw new JadxRuntimeException("Failed to generate code for class: " + cls.getFullName(), e); } } /** * Load and process class without its deps */ public void forceProcess(ClassNode cls) { ClassNode topParentClass = cls.getTopParentClass(); if (topParentClass != cls) { forceProcess(topParentClass); return; } try { process(cls, false); } catch (StackOverflowError | Exception e) { throw new JadxRuntimeException("Failed to process class: " + cls.getFullName(), e); } } /** * Generate code for class without processing its deps */ public @Nullable ICodeInfo forceGenerateCode(ClassNode cls) { try { return process(cls, true); } catch (StackOverflowError | Exception e) { throw new JadxRuntimeException("Failed to generate code for class: " + cls.getFullName(), e); } } private final Map modesMap = new EnumMap<>(DecompilationMode.class); public @Nullable ICodeInfo forceGenerateCodeForMode(ClassNode cls, DecompilationMode mode) { synchronized (modesMap) { ProcessClass prCls = modesMap.computeIfAbsent(mode, m -> { RootNode root = cls.root(); ProcessClass newPrCls = new ProcessClass(getPassesForMode(root.getArgs(), m)); newPrCls.initPasses(root); return newPrCls; }); try { cls.addAttr(new DecompileModeOverrideAttr(mode)); return prCls.forceGenerateCode(cls); } finally { cls.remove(AType.DECOMPILE_MODE_OVERRIDE); } } } private static List getPassesForMode(JadxArgs baseArgs, DecompilationMode mode) { switch (mode) { case FALLBACK: return Jadx.getFallbackPassesList(); case SIMPLE: // copy properties into new args // keep in sync with properties usage in Jadx.getSimpleModePasses method JadxArgs args = new JadxArgs(); args.setDebugInfo(baseArgs.isDebugInfo()); args.setCommentsLevel(baseArgs.getCommentsLevel()); return Jadx.getSimpleModePasses(args); default: throw new JadxRuntimeException("Unexpected decompilation mode: " + mode); } } public void initPasses(RootNode root) { for (IDexTreeVisitor pass : passes) { try { pass.init(root); } catch (Exception e) { LOG.error("Visitor init failed: {}", pass.getClass().getSimpleName(), e); } } } // TODO: make passes list private and not visible public List getPasses() { return passes; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/clsp/ClsSet.java ================================================ package jadx.core.clsp; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Stream; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.DecodeException; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; /** * Classes list for import into classpath graph */ public class ClsSet { private static final Logger LOG = LoggerFactory.getLogger(ClsSet.class); private static final String CLST_EXTENSION = ".jcst"; private static final String CLST_FILENAME = "core" + CLST_EXTENSION; private static final String CLST_PATH = "/clst/" + CLST_FILENAME; private static final String JADX_CLS_SET_HEADER = "jadx-cst"; private static final int VERSION = 5; private static final String STRING_CHARSET = "US-ASCII"; private static final ArgType[] EMPTY_ARGTYPE_ARRAY = new ArgType[0]; private static final ArgType[] OBJECT_ARGTYPE_ARRAY = new ArgType[] { ArgType.OBJECT }; private final RootNode root; private int androidApiLevel; public ClsSet(RootNode root) { this.root = root; } private enum TypeEnum { WILDCARD, GENERIC, GENERIC_TYPE_VARIABLE, OUTER_GENERIC, OBJECT, ARRAY, PRIMITIVE } private ClspClass[] classes; public void loadFromClstFile() throws IOException, DecodeException { long startTime = System.currentTimeMillis(); try (InputStream input = ClsSet.class.getResourceAsStream(CLST_PATH)) { if (input == null) { throw new JadxRuntimeException("Can't load classpath file: " + CLST_PATH); } load(input); } if (LOG.isDebugEnabled()) { long time = System.currentTimeMillis() - startTime; int methodsCount = Stream.of(classes).mapToInt(clspClass -> clspClass.getMethodsMap().size()).sum(); LOG.debug("Clst file loaded in {}ms, android api: {}, classes: {}, methods: {}", time, androidApiLevel, classes.length, methodsCount); } } public void loadFrom(RootNode root) { List list = root.getClasses(true); Map names = new HashMap<>(list.size()); int k = 0; for (ClassNode cls : list) { ArgType clsType = cls.getClassInfo().getType(); String clsRawName = clsType.getObject(); cls.load(); ClspClassSource source = getClspClassSource(cls); ClspClass nClass = new ClspClass(clsType, k, cls.getAccessFlags().rawValue(), source); if (names.put(clsRawName, nClass) != null) { throw new JadxRuntimeException("Duplicate class: " + clsRawName); } k++; nClass.setTypeParameters(cls.getGenericTypeParameters()); nClass.setMethods(getMethodsDetails(cls)); } classes = new ClspClass[k]; k = 0; for (ClassNode cls : list) { ClspClass nClass = getCls(cls, names); if (nClass == null) { throw new JadxRuntimeException("Missing class: " + cls); } nClass.setParents(makeParentsArray(cls)); classes[k] = nClass; k++; } } private static ClspClassSource getClspClassSource(ClassNode cls) { String inputFileName = cls.getClsData().getInputFileName(); int idx = inputFileName.indexOf(':'); String sourceFile = inputFileName.substring(0, idx); ClspClassSource source = ClspClassSource.getClspClassSource(sourceFile); if (source == ClspClassSource.APP) { throw new JadxRuntimeException("Unexpected input file: " + inputFileName); } return source; } private List getMethodsDetails(ClassNode cls) { List methodsList = cls.getMethods(); List methods = new ArrayList<>(methodsList.size()); for (MethodNode mth : methodsList) { processMethodDetails(mth, methods); } return methods; } private void processMethodDetails(MethodNode mth, List methods) { AccessInfo accessFlags = mth.getAccessFlags(); if (accessFlags.isPrivate() || accessFlags.isSynthetic() || accessFlags.isBridge()) { return; } ClspMethod clspMethod = new ClspMethod(mth.getMethodInfo(), mth.getArgTypes(), mth.getReturnType(), mth.getTypeParameters(), mth.getThrows(), accessFlags.rawValue()); methods.add(clspMethod); } public static ArgType[] makeParentsArray(ClassNode cls) { ArgType superClass = cls.getSuperClass(); if (superClass == null) { // cls is java.lang.Object return EMPTY_ARGTYPE_ARRAY; } int interfacesCount = cls.getInterfaces().size(); if (interfacesCount == 0 && superClass == ArgType.OBJECT) { return OBJECT_ARGTYPE_ARRAY; } ArgType[] parents = new ArgType[1 + interfacesCount]; parents[0] = superClass; int k = 1; for (ArgType iface : cls.getInterfaces()) { parents[k] = iface; k++; } return parents; } private static ClspClass getCls(ClassNode cls, Map names) { return getCls(cls.getRawName(), names); } private static ClspClass getCls(ArgType clsType, Map names) { return getCls(clsType.getObject(), names); } private static ClspClass getCls(String fullName, Map names) { ClspClass cls = names.get(fullName); if (cls == null) { LOG.debug("Class not found: {}", fullName); } return cls; } public void save(Path path) throws IOException { FileUtils.makeDirsForFile(path); String outputName = path.getFileName().toString(); if (outputName.endsWith(CLST_EXTENSION)) { try (BufferedOutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(path))) { save(outputStream); } } else { throw new JadxRuntimeException("Unknown file format: " + outputName); } } private void save(OutputStream output) throws IOException { DataOutputStream out = new DataOutputStream(output); out.writeBytes(JADX_CLS_SET_HEADER); out.writeByte(VERSION); out.writeInt(androidApiLevel); Map names = new HashMap<>(classes.length); out.writeInt(classes.length); for (ClspClass cls : classes) { out.writeInt(cls.getAccFlags()); writeUnsignedByte(out, cls.getSource().ordinal()); String clsName = cls.getName(); writeString(out, clsName); names.put(clsName, cls); } for (ClspClass cls : classes) { writeArgTypesArray(out, cls.getParents(), names); writeArgTypesList(out, cls.getTypeParameters(), names); List methods = cls.getSortedMethodsList(); out.writeShort(methods.size()); for (ClspMethod method : methods) { writeMethod(out, method, names); } } int methodsCount = Stream.of(classes).mapToInt(c -> c.getMethodsMap().size()).sum(); LOG.info("Classes: {}, methods: {}, file size: {} bytes", classes.length, methodsCount, out.size()); } private static void writeMethod(DataOutputStream out, ClspMethod method, Map names) throws IOException { MethodInfo methodInfo = method.getMethodInfo(); writeString(out, methodInfo.getName()); writeArgTypesList(out, methodInfo.getArgumentsTypes(), names); writeArgType(out, methodInfo.getReturnType(), names); writeArgTypesList(out, method.containsGenericArgs() ? method.getArgTypes() : Collections.emptyList(), names); writeArgType(out, method.getReturnType(), names); writeArgTypesList(out, method.getTypeParameters(), names); out.writeInt(method.getRawAccessFlags()); writeArgTypesList(out, method.getThrows(), names); } private static void writeArgTypesList(DataOutputStream out, List list, Map names) throws IOException { int size = list.size(); writeUnsignedByte(out, size); if (size != 0) { for (ArgType type : list) { writeArgType(out, type, names); } } } private static void writeArgTypesArray(DataOutputStream out, @Nullable ArgType[] arr, Map names) throws IOException { if (arr == null) { out.writeByte(-1); return; } if (arr == OBJECT_ARGTYPE_ARRAY) { out.writeByte(-2); return; } int size = arr.length; out.writeByte(size); if (size != 0) { for (ArgType type : arr) { writeArgType(out, type, names); } } } private static void writeArgType(DataOutputStream out, ArgType argType, Map names) throws IOException { if (argType == null) { out.writeByte(-1); return; } if (argType.isPrimitive()) { out.writeByte(TypeEnum.PRIMITIVE.ordinal()); out.writeByte(argType.getPrimitiveType().getShortName().charAt(0)); } else if (argType.getOuterType() != null) { out.writeByte(TypeEnum.OUTER_GENERIC.ordinal()); writeArgType(out, argType.getOuterType(), names); writeArgType(out, argType.getInnerType(), names); } else if (argType.getWildcardType() != null) { out.writeByte(TypeEnum.WILDCARD.ordinal()); ArgType.WildcardBound bound = argType.getWildcardBound(); out.writeByte(bound.getNum()); if (bound != ArgType.WildcardBound.UNBOUND) { writeArgType(out, argType.getWildcardType(), names); } } else if (argType.isGeneric()) { out.writeByte(TypeEnum.GENERIC.ordinal()); out.writeInt(getCls(argType, names).getId()); writeArgTypesList(out, argType.getGenericTypes(), names); } else if (argType.isGenericType()) { out.writeByte(TypeEnum.GENERIC_TYPE_VARIABLE.ordinal()); writeString(out, argType.getObject()); writeArgTypesList(out, argType.getExtendTypes(), names); } else if (argType.isObject()) { out.writeByte(TypeEnum.OBJECT.ordinal()); out.writeInt(getCls(argType, names).getId()); } else if (argType.isArray()) { out.writeByte(TypeEnum.ARRAY.ordinal()); writeArgType(out, argType.getArrayElement(), names); } else { throw new JadxRuntimeException("Cannot save type: " + argType); } } private void load(InputStream input) throws IOException, DecodeException { try (DataInputStream in = new DataInputStream(new BufferedInputStream(input))) { byte[] header = new byte[JADX_CLS_SET_HEADER.length()]; int readHeaderLength = in.read(header); if (readHeaderLength != JADX_CLS_SET_HEADER.length() || !JADX_CLS_SET_HEADER.equals(new String(header, STRING_CHARSET))) { throw new DecodeException("Wrong jadx class set header"); } int version = in.readByte(); if (version != VERSION) { throw new DecodeException("Wrong jadx class set version, got: " + version + ", expect: " + VERSION); } androidApiLevel = in.readInt(); int clsCount = in.readInt(); classes = new ClspClass[clsCount]; for (int i = 0; i < clsCount; i++) { int accFlags = in.readInt(); ClspClassSource clsSource = readClsSource(in); String name = readString(in); classes[i] = new ClspClass(ArgType.object(name), i, accFlags, clsSource); } for (int i = 0; i < clsCount; i++) { ClspClass nClass = classes[i]; ClassInfo clsInfo = ClassInfo.fromType(root, nClass.getClsType()); nClass.setParents(readArgTypesArray(in)); nClass.setTypeParameters(readArgTypesList(in)); nClass.setMethods(readClsMethods(in, clsInfo)); } } } private static ClspClassSource readClsSource(DataInputStream in) throws IOException, DecodeException { int source = readUnsignedByte(in); ClspClassSource[] clspClassSources = ClspClassSource.values(); if (source < 0 || source > clspClassSources.length) { throw new DecodeException("Wrong jadx source identifier: " + source); } return clspClassSources[source]; } private List readClsMethods(DataInputStream in, ClassInfo clsInfo) throws IOException { int mCount = in.readShort(); List methods = new ArrayList<>(mCount); for (int j = 0; j < mCount; j++) { methods.add(readMethod(in, clsInfo)); } return methods; } private ClspMethod readMethod(DataInputStream in, ClassInfo clsInfo) throws IOException { String name = readString(in); List argTypes = readArgTypesList(in); ArgType retType = readArgType(in); List genericArgTypes = readArgTypesList(in); if (genericArgTypes.isEmpty() || Objects.equals(genericArgTypes, argTypes)) { genericArgTypes = argTypes; } ArgType genericRetType = readArgType(in); if (Objects.equals(genericRetType, retType)) { genericRetType = retType; } List typeParameters = readArgTypesList(in); int accFlags = in.readInt(); List throwList = readArgTypesList(in); MethodInfo methodInfo = MethodInfo.fromDetails(root, clsInfo, name, argTypes, retType); return new ClspMethod(methodInfo, genericArgTypes, genericRetType, typeParameters, throwList, accFlags); } private List readArgTypesList(DataInputStream in) throws IOException { int count = in.readByte(); if (count == 0) { return Collections.emptyList(); } List list = new ArrayList<>(count); for (int i = 0; i < count; i++) { list.add(readArgType(in)); } return list; } @Nullable private ArgType[] readArgTypesArray(DataInputStream in) throws IOException { int count = in.readByte(); switch (count) { case -1: return null; case -2: return OBJECT_ARGTYPE_ARRAY; case 0: return EMPTY_ARGTYPE_ARRAY; default: ArgType[] arr = new ArgType[count]; for (int i = 0; i < count; i++) { arr[i] = readArgType(in); } return arr; } } private ArgType readArgType(DataInputStream in) throws IOException { int ordinal = in.readByte(); if (ordinal == -1) { return null; } switch (TypeEnum.values()[ordinal]) { case WILDCARD: ArgType.WildcardBound bound = ArgType.WildcardBound.getByNum(in.readByte()); if (bound == ArgType.WildcardBound.UNBOUND) { return ArgType.WILDCARD; } ArgType objType = readArgType(in); return ArgType.wildcard(objType, bound); case OUTER_GENERIC: ArgType outerType = readArgType(in); ArgType innerType = readArgType(in); return ArgType.outerGeneric(outerType, innerType); case GENERIC: ArgType clsType = classes[in.readInt()].getClsType(); return ArgType.generic(clsType, readArgTypesList(in)); case GENERIC_TYPE_VARIABLE: String typeVar = readString(in); List extendTypes = readArgTypesList(in); return ArgType.genericType(typeVar, extendTypes); case OBJECT: return classes[in.readInt()].getClsType(); case ARRAY: return ArgType.array(Objects.requireNonNull(readArgType(in))); case PRIMITIVE: char shortName = (char) in.readByte(); return ArgType.parse(shortName); default: throw new JadxRuntimeException("Unsupported Arg Type: " + ordinal); } } private static void writeString(DataOutputStream out, String name) throws IOException { byte[] bytes = name.getBytes(STRING_CHARSET); int len = bytes.length; if (len >= 0xFF) { throw new JadxRuntimeException("String is too long: " + name); } writeUnsignedByte(out, bytes.length); out.write(bytes); } private static String readString(DataInputStream in) throws IOException { int len = readUnsignedByte(in); return readString(in, len); } private static String readString(DataInputStream in, int len) throws IOException { byte[] bytes = new byte[len]; int count = in.read(bytes); while (count != len) { int res = in.read(bytes, count, len - count); if (res == -1) { throw new IOException("String read error"); } else { count += res; } } return new String(bytes, STRING_CHARSET); } private static void writeUnsignedByte(DataOutputStream out, int value) throws IOException { if (value < 0 || value >= 0xFF) { throw new JadxRuntimeException("Unsigned byte value is too big: " + value); } out.writeByte(value); } private static int readUnsignedByte(DataInputStream in) throws IOException { return ((int) in.readByte()) & 0xFF; } public int getClassesCount() { return classes.length; } public void addToMap(Map nameMap) { for (ClspClass cls : classes) { nameMap.put(cls.getName(), cls); } } public int getAndroidApiLevel() { return androidApiLevel; } public void setAndroidApiLevel(int androidApiLevel) { this.androidApiLevel = androidApiLevel; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/clsp/ClspClass.java ================================================ package jadx.core.clsp; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import org.intellij.lang.annotations.MagicConstant; import jadx.api.plugins.input.data.AccessFlags; import jadx.core.dex.instructions.args.ArgType; /** * Class node in classpath graph */ public class ClspClass { private final ArgType clsType; private final int id; private final int accFlags; private ArgType[] parents; private Map methodsMap = Collections.emptyMap(); private List typeParameters = Collections.emptyList(); private final ClspClassSource source; public ClspClass(ArgType clsType, int id, int accFlags, ClspClassSource source) { this.clsType = clsType; this.id = id; this.accFlags = accFlags; this.source = source; } public String getName() { return clsType.getObject(); } public ArgType getClsType() { return clsType; } public int getId() { return id; } public int getAccFlags() { return accFlags; } public boolean isInterface() { return AccessFlags.hasFlag(accFlags, AccessFlags.INTERFACE); } public boolean hasAccFlag(@MagicConstant(flagsFromClass = AccessFlags.class) int flags) { return AccessFlags.hasFlag(accFlags, flags); } public ArgType[] getParents() { return parents; } public void setParents(ArgType[] parents) { this.parents = parents; } public Map getMethodsMap() { return methodsMap; } public List getSortedMethodsList() { List list = new ArrayList<>(methodsMap.size()); list.addAll(methodsMap.values()); Collections.sort(list); return list; } public void setMethodsMap(Map methodsMap) { this.methodsMap = Objects.requireNonNull(methodsMap); } public void setMethods(List methods) { Map map = new HashMap<>(methods.size()); for (ClspMethod mth : methods) { map.put(mth.getMethodInfo().getShortId(), mth); } setMethodsMap(map); } public List getTypeParameters() { return typeParameters; } public void setTypeParameters(List typeParameters) { this.typeParameters = typeParameters; } public ClspClassSource getSource() { return this.source; } @Override public int hashCode() { return clsType.hashCode(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ClspClass nClass = (ClspClass) o; return clsType.equals(nClass.clsType); } @Override public String toString() { return clsType.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/clsp/ClspClassSource.java ================================================ package jadx.core.clsp; public enum ClspClassSource { APP(""), CORE("android.jar"), ANDROID_CAR("android.car.jar"), APACHE_HTTP_LEGACY_CLIENT("org.apache.http.legacy.jar"); private final String jarFile; ClspClassSource(String jarFile) { this.jarFile = jarFile; } public String getJarFile() { return jarFile; } public static ClspClassSource getClspClassSource(String jarFile) { for (ClspClassSource classSource : ClspClassSource.values()) { if (classSource.getJarFile().equals(jarFile)) { return classSource; } } return APP; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java ================================================ package jadx.core.clsp; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.DecodeException; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Classes hierarchy graph with methods additional info */ public class ClspGraph { private static final Logger LOG = LoggerFactory.getLogger(ClspGraph.class); private final RootNode root; private Map nameMap; private Map> superTypesCache; private Map> implementsCache; private final Set missingClasses = new HashSet<>(); public ClspGraph(RootNode rootNode) { this.root = rootNode; } public void loadClsSetFile() throws IOException, DecodeException { ClsSet set = new ClsSet(root); set.loadFromClstFile(); addClasspath(set); } public void addClasspath(ClsSet set) { if (nameMap == null) { nameMap = new HashMap<>(set.getClassesCount()); set.addToMap(nameMap); } else { throw new JadxRuntimeException("Classpath already loaded"); } } public void addApp(List classes) { if (nameMap == null) { nameMap = new HashMap<>(classes.size()); } for (ClassNode cls : classes) { addClass(cls); } } public void initCache() { fillSuperTypesCache(); fillImplementsCache(); } public boolean isClsKnown(String fullName) { return nameMap.containsKey(fullName); } public ClspClass getClsDetails(ArgType type) { return nameMap.get(type.getObject()); } @Nullable public IMethodDetails getMethodDetails(MethodInfo methodInfo) { ClspClass cls = nameMap.get(methodInfo.getDeclClass().getRawName()); if (cls == null) { return null; } ClspMethod clspMethod = getMethodFromClass(cls, methodInfo); if (clspMethod != null) { return clspMethod; } // deep search for (ArgType parent : cls.getParents()) { ClspClass clspParent = getClspClass(parent); if (clspParent != null) { ClspMethod methodFromParent = getMethodFromClass(clspParent, methodInfo); if (methodFromParent != null) { return methodFromParent; } } } // unknown method return new SimpleMethodDetails(methodInfo); } private ClspMethod getMethodFromClass(ClspClass cls, MethodInfo methodInfo) { return cls.getMethodsMap().get(methodInfo.getShortId()); } private void addClass(ClassNode cls) { ArgType clsType = cls.getClassInfo().getType(); String rawName = clsType.getObject(); ClspClass clspClass = new ClspClass(clsType, -1, cls.getAccessFlags().rawValue(), ClspClassSource.APP); clspClass.setParents(ClsSet.makeParentsArray(cls)); nameMap.put(rawName, clspClass); } /** * @return {@code clsName} instanceof {@code implClsName} */ public boolean isImplements(String clsName, String implClsName) { Set anc = getSuperTypes(clsName); return anc.contains(implClsName); } public List getImplementations(String clsName) { List list = implementsCache.get(clsName); return list == null ? Collections.emptyList() : list; } private void fillImplementsCache() { Map> map = new HashMap<>(nameMap.size()); List classes = new ArrayList<>(nameMap.keySet()); Collections.sort(classes); for (String cls : classes) { for (String st : getSuperTypes(cls)) { map.computeIfAbsent(st, v -> new ArrayList<>()).add(cls); } } implementsCache = map; } public String getCommonAncestor(String clsName, String implClsName) { if (clsName.equals(implClsName)) { return clsName; } ClspClass cls = nameMap.get(implClsName); if (cls == null) { missingClasses.add(clsName); return null; } if (isImplements(clsName, implClsName)) { return implClsName; } Set anc = getSuperTypes(clsName); return searchCommonParent(anc, cls); } private String searchCommonParent(Set anc, ClspClass cls) { for (ArgType p : cls.getParents()) { String name = p.getObject(); if (anc.contains(name)) { return name; } ClspClass nCls = getClspClass(p); if (nCls != null) { String r = searchCommonParent(anc, nCls); if (r != null) { return r; } } } return null; } public Set getSuperTypes(String clsName) { Set result = superTypesCache.get(clsName); return result == null ? Collections.emptySet() : result; } private static final Set OBJECT_SINGLE_SET = Collections.singleton(Consts.CLASS_OBJECT); private void fillSuperTypesCache() { Map> map = new HashMap<>(nameMap.size()); Set tmpSet = new HashSet<>(); for (Map.Entry entry : nameMap.entrySet()) { ClspClass cls = entry.getValue(); tmpSet.clear(); addSuperTypes(cls, tmpSet); Set result; int size = tmpSet.size(); switch (size) { case 0: { result = Collections.emptySet(); break; } case 1: { String supCls = tmpSet.iterator().next(); if (supCls.equals(Consts.CLASS_OBJECT)) { result = OBJECT_SINGLE_SET; } else { result = Collections.singleton(supCls); } break; } default: { result = new HashSet<>(tmpSet); break; } } map.put(cls.getName(), result); } superTypesCache = map; } private void addSuperTypes(ClspClass cls, Set result) { for (ArgType parentType : cls.getParents()) { if (parentType == null) { continue; } ClspClass parentCls = getClspClass(parentType); if (parentCls != null) { boolean isNew = result.add(parentCls.getName()); if (isNew) { addSuperTypes(parentCls, result); } } else { // parent type is unknown result.add(parentType.getObject()); } } } @Nullable private ClspClass getClspClass(ArgType clsType) { ClspClass clspClass = nameMap.get(clsType.getObject()); if (clspClass == null) { missingClasses.add(clsType.getObject()); } return clspClass; } public void printMissingClasses() { int count = missingClasses.size(); if (count == 0) { return; } LOG.warn("Found {} references to unknown classes", count); if (LOG.isDebugEnabled()) { List clsNames = new ArrayList<>(missingClasses); Collections.sort(clsNames); for (String cls : clsNames) { LOG.debug(" {}", cls); } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/clsp/ClspMethod.java ================================================ package jadx.core.clsp; import java.util.List; import java.util.Objects; import org.jetbrains.annotations.NotNull; import jadx.api.plugins.input.data.AccessFlags; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.utils.Utils; /** * Method node in classpath graph. */ public class ClspMethod implements IMethodDetails, Comparable { private final MethodInfo methodInfo; private final List argTypes; private final ArgType returnType; private final List typeParameters; private final List throwList; private final int accFlags; public ClspMethod(MethodInfo methodInfo, List argTypes, ArgType returnType, List typeParameters, List throwList, int accFlags) { this.methodInfo = methodInfo; this.argTypes = argTypes; this.returnType = returnType; this.typeParameters = typeParameters; this.throwList = throwList; this.accFlags = accFlags; } @Override public MethodInfo getMethodInfo() { return methodInfo; } @Override public ArgType getReturnType() { return returnType; } @Override public List getArgTypes() { return argTypes; } public boolean containsGenericArgs() { return !Objects.equals(argTypes, methodInfo.getArgumentsTypes()); } public int getArgsCount() { return argTypes.size(); } @Override public List getTypeParameters() { return typeParameters; } @Override public List getThrows() { return throwList; } @Override public boolean isVarArg() { return (accFlags & AccessFlags.VARARGS) != 0; } @Override public int getRawAccessFlags() { return accFlags; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof ClspMethod)) { return false; } ClspMethod other = (ClspMethod) o; return methodInfo.equals(other.methodInfo); } @Override public int hashCode() { return methodInfo.hashCode(); } @Override public int compareTo(@NotNull ClspMethod other) { return this.methodInfo.compareTo(other.methodInfo); } @Override public String toAttrString() { return IMethodDetails.super.toAttrString() + " (c)"; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("ClspMth{"); if (Utils.notEmpty(getTypeParameters())) { sb.append('<'); sb.append(Utils.listToString(getTypeParameters())); sb.append("> "); } sb.append(getMethodInfo().getFullName()); sb.append('('); sb.append(Utils.listToString(getArgTypes())); sb.append("):"); sb.append(getReturnType()); if (isVarArg()) { sb.append(" VARARG"); } List throwsList = getThrows(); if (Utils.notEmpty(throwsList)) { sb.append(" throws ").append(Utils.listToString(throwsList)); } sb.append('}'); return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/clsp/SimpleMethodDetails.java ================================================ package jadx.core.clsp; import java.util.Collections; import java.util.List; import jadx.api.plugins.input.data.AccessFlags; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.IMethodDetails; /** * Method details build from MethodInfo. * Note: some fields have unknown values. */ public class SimpleMethodDetails implements IMethodDetails { private final MethodInfo methodInfo; public SimpleMethodDetails(MethodInfo methodInfo) { this.methodInfo = methodInfo; } @Override public MethodInfo getMethodInfo() { return methodInfo; } @Override public ArgType getReturnType() { return methodInfo.getReturnType(); } @Override public List getArgTypes() { return methodInfo.getArgumentsTypes(); } @Override public List getTypeParameters() { return Collections.emptyList(); } @Override public List getThrows() { return Collections.emptyList(); } @Override public boolean isVarArg() { return false; } @Override public int getRawAccessFlags() { return AccessFlags.PUBLIC; } @Override public String toAttrString() { return IMethodDetails.super.toAttrString() + " (s)"; } @Override public String toString() { return "SimpleMethodDetails{" + methodInfo + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/AnnotationGen.java ================================================ package jadx.core.codegen; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeWriter; import jadx.api.plugins.input.data.IFieldRef; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.AnnotationDefaultAttr; import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.core.Consts; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; public class AnnotationGen { private final ClassNode cls; private final ClassGen classGen; public AnnotationGen(ClassNode cls, ClassGen classGen) { this.cls = cls; this.classGen = classGen; } public void addForClass(ICodeWriter code) { add(cls, code); } public void addForMethod(ICodeWriter code, MethodNode mth) { add(mth, code); } public void addForField(ICodeWriter code, FieldNode field) { add(field, code); } public void addForParameter(ICodeWriter code, AnnotationMethodParamsAttr paramsAnnotations, int n) { List paramList = paramsAnnotations.getParamList(); if (n >= paramList.size()) { return; } AnnotationsAttr aList = paramList.get(n); if (aList == null || aList.isEmpty()) { return; } for (IAnnotation a : aList.getAll()) { formatAnnotation(code, a); code.add(' '); } } private void add(IAttributeNode node, ICodeWriter code) { AnnotationsAttr aList = node.get(JadxAttrType.ANNOTATION_LIST); if (aList == null || aList.isEmpty()) { return; } for (IAnnotation a : aList.getAll()) { String aCls = a.getAnnotationClass(); if (!aCls.equals(Consts.OVERRIDE_ANNOTATION)) { code.startLine(); formatAnnotation(code, a); } } } private void formatAnnotation(ICodeWriter code, IAnnotation a) { code.add('@'); ClassNode annCls = cls.root().resolveClass(a.getAnnotationClass()); if (annCls != null) { classGen.useClass(code, annCls); } else { classGen.useClass(code, a.getAnnotationClass()); } Map vl = a.getValues(); if (!vl.isEmpty()) { code.add('('); for (Iterator> it = vl.entrySet().iterator(); it.hasNext();) { Entry e = it.next(); String paramName = getParamName(annCls, e.getKey()); if (paramName.equals("value") && vl.size() == 1) { // don't add "value = " if no other parameters } else { code.add(paramName); code.add(" = "); } encodeValue(cls.root(), code, e.getValue()); if (it.hasNext()) { code.add(", "); } } code.add(')'); } } private String getParamName(@Nullable ClassNode annCls, String paramName) { if (annCls != null) { // TODO: save value type and search using signature MethodNode mth = annCls.searchMethodByShortName(paramName); if (mth != null) { return mth.getAlias(); } } return paramName; } public void addThrows(MethodNode mth, ICodeWriter code) { List throwList = mth.getThrows(); if (!throwList.isEmpty()) { code.add(" throws "); for (Iterator it = throwList.iterator(); it.hasNext();) { ArgType ex = it.next(); classGen.useType(code, ex); if (it.hasNext()) { code.add(", "); } } } } public EncodedValue getAnnotationDefaultValue(MethodNode mth) { AnnotationDefaultAttr defaultAttr = mth.get(JadxAttrType.ANNOTATION_DEFAULT); if (defaultAttr == null) { return null; } return defaultAttr.getValue(); } // TODO: refactor this boilerplate code public void encodeValue(RootNode root, ICodeWriter code, EncodedValue encodedValue) { if (encodedValue == null) { code.add("null"); return; } StringUtils stringUtils = getStringUtils(); Object value = encodedValue.getValue(); switch (encodedValue.getType()) { case ENCODED_NULL: code.add("null"); break; case ENCODED_BOOLEAN: code.add(Boolean.TRUE.equals(value) ? "true" : "false"); break; case ENCODED_BYTE: code.add(stringUtils.formatByte((Byte) value, false)); break; case ENCODED_SHORT: code.add(stringUtils.formatShort((Short) value, false)); break; case ENCODED_CHAR: code.add(stringUtils.unescapeChar((Character) value)); break; case ENCODED_INT: code.add(stringUtils.formatInteger((Integer) value, false)); break; case ENCODED_LONG: code.add(stringUtils.formatLong((Long) value, false)); break; case ENCODED_FLOAT: code.add(StringUtils.formatFloat((Float) value)); break; case ENCODED_DOUBLE: code.add(StringUtils.formatDouble((Double) value)); break; case ENCODED_STRING: code.add(stringUtils.unescapeString((String) value)); break; case ENCODED_TYPE: classGen.useType(code, ArgType.parse((String) value)); code.add(".class"); break; case ENCODED_ENUM: case ENCODED_FIELD: // must be a static field if (value instanceof IFieldRef) { FieldInfo fieldInfo = FieldInfo.fromRef(root, (IFieldRef) value); InsnGen.makeStaticFieldAccess(code, fieldInfo, classGen); } else if (value instanceof FieldInfo) { InsnGen.makeStaticFieldAccess(code, (FieldInfo) value, classGen); } else { throw new JadxRuntimeException("Unexpected field type class: " + value.getClass()); } break; case ENCODED_METHOD: // TODO break; case ENCODED_ARRAY: code.add('{'); Iterator it = ((Iterable) value).iterator(); while (it.hasNext()) { EncodedValue v = (EncodedValue) it.next(); encodeValue(cls.root(), code, v); if (it.hasNext()) { code.add(", "); } } code.add('}'); break; case ENCODED_ANNOTATION: formatAnnotation(code, (IAnnotation) value); break; default: throw new JadxRuntimeException("Can't decode value: " + encodedValue.getType() + " (" + encodedValue + ')'); } } private StringUtils getStringUtils() { return cls.root().getStringUtils(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/ClassGen.java ================================================ package jadx.core.codegen; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.CommentsLevel; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; import jadx.api.args.IntegerFormat; import jadx.api.metadata.annotations.NodeEnd; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.core.Consts; import jadx.core.codegen.utils.CodeGenUtils; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.FieldInitInsnAttr; import jadx.core.dex.attributes.nodes.EnumClassAttr; import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.attributes.nodes.MethodInlineAttr; import jadx.core.dex.attributes.nodes.NotificationAttrNode; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.EncodedValueUtils; import jadx.core.utils.Utils; import jadx.core.utils.android.AndroidResourcesUtils; import jadx.core.utils.exceptions.CodegenException; import jadx.core.utils.exceptions.JadxRuntimeException; public class ClassGen { private final ClassNode cls; private final ClassGen parentGen; private final AnnotationGen annotationGen; private final boolean fallback; private final boolean useImports; private final boolean showInconsistentCode; private final IntegerFormat integerFormat; private final Set imports = new HashSet<>(); private int clsDeclOffset; private boolean bodyGenStarted; @Nullable private NameGen outerNameGen; public ClassGen(ClassNode cls, JadxArgs jadxArgs) { this(cls, null, jadxArgs.isUseImports(), jadxArgs.isFallbackMode(), jadxArgs.isShowInconsistentCode(), jadxArgs.getIntegerFormat()); } public ClassGen(ClassNode cls, ClassGen parentClsGen) { this(cls, parentClsGen, parentClsGen.useImports, parentClsGen.fallback, parentClsGen.showInconsistentCode, parentClsGen.integerFormat); } public ClassGen(ClassNode cls, ClassGen parentClsGen, boolean useImports, boolean fallback, boolean showBadCode, IntegerFormat integerFormat) { this.cls = cls; this.parentGen = parentClsGen; this.fallback = fallback; this.useImports = useImports; this.showInconsistentCode = showBadCode; this.integerFormat = integerFormat; this.annotationGen = new AnnotationGen(cls, this); } public ClassNode getClassNode() { return cls; } public ICodeInfo makeClass() throws CodegenException { if (cls.contains(AFlag.PACKAGE_INFO)) { return makePackageInfo(); } ICodeWriter clsBody = cls.root().makeCodeWriter(); addClassCode(clsBody); ICodeWriter clsCode = cls.root().makeCodeWriter(); addPackage(clsCode); clsCode.newLine(); addImports(clsCode); clsCode.add(clsBody); return clsCode.finish(); } private void addPackage(ICodeWriter clsCode) { if (cls.getPackage().isEmpty()) { clsCode.add("// default package"); } else { clsCode.add("package ").add(cls.getPackage()).add(';'); } } private void addImports(ICodeWriter clsCode) { int importsCount = imports.size(); if (importsCount != 0) { List sortedImports = new ArrayList<>(imports); sortedImports.sort(Comparator.comparing(ClassInfo::getAliasFullName)); sortedImports.forEach(classInfo -> { clsCode.startLine("import "); ClassNode classNode = cls.root().resolveClass(classInfo); if (classNode != null) { clsCode.attachAnnotation(classNode); } clsCode.add(classInfo.getAliasFullName()); clsCode.add(';'); }); clsCode.newLine(); imports.clear(); } } private ICodeInfo makePackageInfo() { ICodeWriter code = cls.root().makeCodeWriter(); annotationGen.addForClass(code); code.newLine(); code.attachDefinition(cls); addPackage(code); code.newLine(); addImports(code); return code.finish(); } public void addClassCode(ICodeWriter code) throws CodegenException { if (cls.contains(AFlag.DONT_GENERATE)) { return; } if (Consts.DEBUG_USAGE) { addClassUsageInfo(code, cls); } addClassDeclaration(code); addClassBody(code); } public void addClassDeclaration(ICodeWriter clsCode) { AccessInfo af = cls.getAccessFlags(); if (af.isInterface()) { af = af.remove(AccessFlags.ABSTRACT) .remove(AccessFlags.STATIC); } // 'static' and 'private' modifier not allowed for top classes (not inner) if (!cls.getClassInfo().isInner()) { af = af.remove(AccessFlags.STATIC).remove(AccessFlags.PRIVATE); } CodeGenUtils.addComments(clsCode, cls); CodeGenUtils.addClassRenamedComment(clsCode, cls); CodeGenUtils.addErrors(clsCode, cls); CodeGenUtils.addSourceFileInfo(clsCode, cls); CodeGenUtils.addInputFileInfo(clsCode, cls); annotationGen.addForClass(clsCode); clsCode.startLineWithNum(cls.getSourceLine()).add(af.makeString(cls.checkCommentsLevel(CommentsLevel.INFO))); if (af.isInterface()) { if (af.isAnnotation()) { clsCode.add('@'); } clsCode.add("interface "); } else if (af.isEnum()) { clsCode.add("enum "); } else { clsCode.add("class "); } clsCode.attachDefinition(cls); clsCode.add(cls.getClassInfo().getAliasShortName()); addGenericTypeParameters(clsCode, cls.getGenericTypeParameters(), true); clsCode.add(' '); ArgType sup = cls.getSuperClass(); if (sup != null && !sup.equals(ArgType.OBJECT) && !cls.contains(AFlag.REMOVE_SUPER_CLASS)) { clsCode.add("extends "); useClass(clsCode, sup); clsCode.add(' '); } if (!cls.getInterfaces().isEmpty() && !af.isAnnotation()) { if (cls.getAccessFlags().isInterface()) { clsCode.add("extends "); } else { clsCode.add("implements "); } for (Iterator it = cls.getInterfaces().iterator(); it.hasNext();) { ArgType interf = it.next(); useClass(clsCode, interf); if (it.hasNext()) { clsCode.add(", "); } } if (!cls.getInterfaces().isEmpty()) { clsCode.add(' '); } } } public boolean addGenericTypeParameters(ICodeWriter code, List generics, boolean classDeclaration) { if (generics == null || generics.isEmpty()) { return false; } code.add('<'); int i = 0; for (ArgType genericInfo : generics) { if (i != 0) { code.add(", "); } if (genericInfo.isGenericType()) { code.add(genericInfo.getObject()); } else { useClass(code, genericInfo); } List list = genericInfo.getExtendTypes(); if (list != null && !list.isEmpty()) { code.add(" extends "); for (Iterator it = list.iterator(); it.hasNext();) { ArgType g = it.next(); if (g.isGenericType()) { code.add(g.getObject()); } else { useClass(code, g); if (classDeclaration && !cls.getClassInfo().isInner() && cls.root().getArgs().isUseImports()) { addImport(ClassInfo.fromType(cls.root(), g)); } } if (it.hasNext()) { code.add(" & "); } } } i++; } code.add('>'); return true; } public void addClassBody(ICodeWriter clsCode) throws CodegenException { addClassBody(clsCode, false); } /** * @param printClassName allows to print the original class name as comment (e.g. for inlined * classes) */ public void addClassBody(ICodeWriter clsCode, boolean printClassName) throws CodegenException { clsCode.add('{'); if (printClassName && cls.checkCommentsLevel(CommentsLevel.INFO)) { clsCode.add(" // from class: " + cls.getClassInfo().getFullName()); } setBodyGenStarted(true); clsDeclOffset = clsCode.getLength(); clsCode.incIndent(); addFields(clsCode); addInnerClsAndMethods(clsCode); clsCode.decIndent(); clsCode.startLine('}'); clsCode.attachAnnotation(NodeEnd.VALUE); } private void addInnerClsAndMethods(ICodeWriter clsCode) { Stream.of(cls.getInnerClasses(), cls.getMethods()) .flatMap(Collection::stream) .filter(node -> !skipNode(node)) .sorted(Comparator.comparingInt(LineAttrNode::getSourceLine)) .forEach(node -> { if (node instanceof ClassNode) { addInnerClass(clsCode, (ClassNode) node); } else { addMethod(clsCode, (MethodNode) node); } }); } private boolean skipNode(NotificationAttrNode node) { if (fallback) { return false; } if (Consts.DEBUG_ATTRIBUTES) { if (node.contains(AType.JADX_COMMENTS)) { return false; } } return node.contains(AFlag.DONT_GENERATE); } private void addInnerClass(ICodeWriter code, ClassNode innerCls) { try { ClassGen inClGen = new ClassGen(innerCls, getParentGen()); code.newLine(); inClGen.addClassCode(code); imports.addAll(inClGen.getImports()); } catch (Exception e) { innerCls.addError("Inner class code generation error", e); } } private boolean isInnerClassesPresents() { for (ClassNode innerCls : cls.getInnerClasses()) { if (!innerCls.contains(AType.ANONYMOUS_CLASS)) { return true; } } return false; } private void addMethod(ICodeWriter code, MethodNode mth) { if (skipMethod(mth)) { return; } if (code.getLength() != clsDeclOffset) { code.newLine(); } int savedIndent = code.getIndent(); try { addMethodCode(code, mth); } catch (Exception e) { if (mth.getParentClass().getTopParentClass().contains(AFlag.RESTART_CODEGEN)) { throw new JadxRuntimeException("Method generation error", e); } mth.addError("Method generation error", e); CodeGenUtils.addErrors(code, mth); code.setIndent(savedIndent); } } /** * Additional checks for inlined methods */ private boolean skipMethod(MethodNode mth) { if (cls.root().getArgs().getDecompilationMode().isSpecial()) { // show all methods for special decompilation modes return false; } MethodInlineAttr inlineAttr = mth.get(AType.METHOD_INLINE); if (inlineAttr == null || inlineAttr.notNeeded()) { return false; } try { if (mth.getUseIn().isEmpty()) { mth.add(AFlag.DONT_GENERATE); return true; } List useInCompleted = mth.getUseIn().stream() .filter(m -> m.getTopParentClass().getState().isProcessComplete()) .collect(Collectors.toList()); if (useInCompleted.isEmpty()) { mth.add(AFlag.DONT_GENERATE); return true; } mth.addDebugComment("Method not inlined, still used in: " + useInCompleted); return false; } catch (Exception e) { // check failed => keep method mth.addWarnComment("Failed to check method usage", e); return false; } } private boolean isMethodsPresents() { for (MethodNode mth : cls.getMethods()) { if (!mth.contains(AFlag.DONT_GENERATE)) { return true; } } return false; } public void addMethodCode(ICodeWriter code, MethodNode mth) throws CodegenException { CodeGenUtils.addErrorsAndComments(code, mth); if (mth.isNoCode()) { MethodGen mthGen = new MethodGen(this, mth); mthGen.addDefinition(code); code.add(';'); } else { boolean badCode = mth.contains(AFlag.INCONSISTENT_CODE); if (badCode && showInconsistentCode) { badCode = false; } MethodGen mthGen; if (badCode || fallback || mth.contains(AType.JADX_ERROR)) { mthGen = MethodGen.getFallbackMethodGen(mth); } else { mthGen = new MethodGen(this, mth); } if (mthGen.addDefinition(code)) { code.add(' '); } code.add('{'); code.incIndent(); mthGen.addInstructions(code); code.decIndent(); code.startLine('}'); code.attachAnnotation(NodeEnd.VALUE); } } private void addFields(ICodeWriter code) throws CodegenException { addEnumFields(code); for (FieldNode f : cls.getFields()) { addField(code, f); } } public void addField(ICodeWriter code, FieldNode f) { if (f.contains(AFlag.DONT_GENERATE)) { return; } if (f.contains(JadxAttrType.ANNOTATION_LIST) || f.contains(AType.JADX_COMMENTS) || f.contains(AType.CODE_COMMENTS) || f.getFieldInfo().hasAlias()) { code.newLine(); } if (Consts.DEBUG_USAGE) { addFieldUsageInfo(code, f); } CodeGenUtils.addComments(code, f); if (f.getFieldInfo().hasAlias()) { CodeGenUtils.addRenamedComment(code, f, f.getName()); } annotationGen.addForField(code, f); code.startLine(f.getAccessFlags().makeString(f.checkCommentsLevel(CommentsLevel.INFO))); useType(code, f.getType()); code.add(' '); code.attachDefinition(f); code.add(f.getAlias()); FieldInitInsnAttr initInsnAttr = f.get(AType.FIELD_INIT_INSN); if (initInsnAttr != null) { InsnGen insnGen = makeInsnGen(initInsnAttr.getInsnMth()); code.add(" = "); addInsnBody(insnGen, code, initInsnAttr.getInsn()); } else { EncodedValue constVal = f.get(JadxAttrType.CONSTANT_VALUE); if (constVal != null) { code.add(" = "); if (constVal.getType() == EncodedType.ENCODED_NULL) { code.add(TypeGen.literalToString(0, f.getType(), cls, fallback)); } else { Object val = EncodedValueUtils.convertToConstValue(constVal); if (val instanceof LiteralArg) { long lit = ((LiteralArg) val).getLiteral(); code.add(getIntegerString(lit, f.getType())); } else { annotationGen.encodeValue(cls.root(), code, constVal); } } } } code.add(';'); } private String getIntegerString(long lit, ArgType type) { if (integerFormat != IntegerFormat.DECIMAL && AndroidResourcesUtils.isResourceFieldValue(cls, type)) { return String.format("0x%08x", lit); } // force literal type to be same as field (java bytecode can use different type) return TypeGen.literalToString(lit, type, cls, fallback); } private boolean isFieldsPresents() { for (FieldNode field : cls.getFields()) { if (!field.contains(AFlag.DONT_GENERATE)) { return true; } } return false; } private void addEnumFields(ICodeWriter code) throws CodegenException { EnumClassAttr enumFields = cls.get(AType.ENUM_CLASS); if (enumFields == null) { return; } InsnGen igen = null; for (Iterator it = enumFields.getFields().iterator(); it.hasNext();) { EnumField f = it.next(); CodeGenUtils.addComments(code, f.getField()); code.startLine(f.getField().getAlias()); ConstructorInsn constrInsn = f.getConstrInsn(); MethodNode callMth = cls.root().resolveMethod(constrInsn.getCallMth()); int skipCount = getEnumCtrSkipArgsCount(callMth); if (constrInsn.getArgsCount() > skipCount) { if (igen == null) { igen = makeInsnGen(enumFields.getStaticMethod()); } igen.generateMethodArguments(code, constrInsn, 0, callMth); } if (f.getCls() != null) { code.add(' '); new ClassGen(f.getCls(), this).addClassBody(code, true); } if (it.hasNext()) { code.add(','); } } if (isMethodsPresents() || isFieldsPresents() || isInnerClassesPresents()) { if (enumFields.getFields().isEmpty()) { code.startLine(); } code.add(';'); if (isFieldsPresents()) { code.newLine(); } } } private int getEnumCtrSkipArgsCount(@Nullable MethodNode callMth) { if (callMth != null) { SkipMethodArgsAttr skipArgsAttr = callMth.get(AType.SKIP_MTH_ARGS); if (skipArgsAttr != null) { return skipArgsAttr.getSkipCount(); } } return 0; } private InsnGen makeInsnGen(MethodNode mth) { MethodGen mthGen = new MethodGen(this, mth); return new InsnGen(mthGen, false); } private void addInsnBody(InsnGen insnGen, ICodeWriter code, InsnNode insn) { try { insnGen.makeInsn(insn, code, InsnGen.Flags.BODY_ONLY_NOWRAP); } catch (Exception e) { cls.addError("Failed to generate init code", e); } } public void useType(ICodeWriter code, ArgType type) { PrimitiveType stype = type.getPrimitiveType(); if (stype == null) { code.add(type.toString()); } else if (stype == PrimitiveType.OBJECT) { if (type.isGenericType()) { code.add(type.getObject()); } else { useClass(code, type); } } else if (stype == PrimitiveType.ARRAY) { useType(code, type.getArrayElement()); code.add("[]"); } else { code.add(stype.getLongName()); } } public void useClass(ICodeWriter code, String rawCls) { useClass(code, ArgType.object(rawCls)); } public void useClass(ICodeWriter code, ArgType type) { ArgType outerType = type.getOuterType(); if (outerType != null) { useClass(code, outerType); code.add('.'); addInnerType(code, type); return; } useClass(code, ClassInfo.fromType(cls.root(), type)); addGenerics(code, type); } private void addInnerType(ICodeWriter code, ArgType baseType) { ArgType innerType = baseType.getInnerType(); ArgType outerType = innerType.getOuterType(); if (outerType != null) { useClassWithShortName(code, baseType, outerType); code.add('.'); addInnerType(code, innerType); return; } useClassWithShortName(code, baseType, innerType); } private void useClassWithShortName(ICodeWriter code, ArgType baseType, ArgType type) { String fullNameObj; if (type.getObject().contains(".")) { fullNameObj = type.getObject(); } else { fullNameObj = baseType.getObject(); } ClassInfo classInfo = ClassInfo.fromName(cls.root(), fullNameObj); ClassNode classNode = cls.root().resolveClass(classInfo); if (classNode != null) { code.attachAnnotation(classNode); } code.add(classInfo.getAliasShortName()); addGenerics(code, type); } private void addGenerics(ICodeWriter code, ArgType type) { List generics = type.getGenericTypes(); if (generics != null) { code.add('<'); int len = generics.size(); for (int i = 0; i < len; i++) { if (i != 0) { code.add(", "); } ArgType gt = generics.get(i); ArgType wt = gt.getWildcardType(); if (wt != null) { ArgType.WildcardBound bound = gt.getWildcardBound(); code.add(bound.getStr()); if (bound != ArgType.WildcardBound.UNBOUND) { useType(code, wt); } } else { useType(code, gt); } } code.add('>'); } } public void useClass(ICodeWriter code, ClassInfo classInfo) { ClassNode classNode = cls.root().resolveClass(classInfo); if (classNode != null) { useClass(code, classNode); } else { addClsName(code, classInfo); } } public void useClass(ICodeWriter code, ClassNode classNode) { code.attachAnnotation(classNode); addClsName(code, classNode.getClassInfo()); } public void addClsName(ICodeWriter code, ClassInfo classInfo) { String clsName = useClassInternal(cls.getClassInfo(), classInfo); code.add(clsName); } public void addClsShortNameForced(ICodeWriter code, ClassInfo classInfo) { code.add(classInfo.getAliasShortName()); if (!isBothClassesInOneTopClass(cls.getClassInfo(), classInfo)) { addImport(classInfo); } } private String useClassInternal(ClassInfo useCls, ClassInfo extClsInfo) { String fullName = extClsInfo.getAliasFullName(); if (fallback || !useImports) { return fullName; } String shortName = extClsInfo.getAliasShortName(); if (useCls.equals(extClsInfo)) { return shortName; } if (extClsInfo.getAliasPkg().isEmpty()) { // omit import for default package return shortName; } if (isClassInnerFor(useCls, extClsInfo)) { return shortName; } if (extClsInfo.isInner()) { return expandInnerClassName(useCls, extClsInfo); } if (checkInnerCollision(cls.root(), useCls, extClsInfo) || checkInPackageCollision(cls.root(), useCls, extClsInfo)) { return fullName; } if (isBothClassesInOneTopClass(useCls, extClsInfo)) { return shortName; } // don't add import for top classes from 'java.lang' package (subpackages excluded) if (extClsInfo.getPackage().equals("java.lang") && extClsInfo.getParentClass() == null) { return shortName; } // don't add import if this class from same package if (extClsInfo.getPackage().equals(useCls.getPackage()) && !extClsInfo.isInner()) { return shortName; } if (extClsInfo.getAliasPkg().equals(useCls.getAliasPkg())) { fullName = extClsInfo.getAliasNameWithoutPackage(); } for (ClassInfo importCls : getImports()) { if (!importCls.equals(extClsInfo) && importCls.getAliasShortName().equals(shortName)) { if (extClsInfo.isInner()) { String parent = useClassInternal(useCls, extClsInfo.getParentClass()); return parent + '.' + shortName; } else { return fullName; } } } addImport(extClsInfo); return shortName; } private String expandInnerClassName(ClassInfo useCls, ClassInfo extClsInfo) { List clsList = new ArrayList<>(); clsList.add(extClsInfo); ClassInfo parentCls = extClsInfo.getParentClass(); boolean addImport = true; while (parentCls != null) { if (parentCls == useCls || isClassInnerFor(useCls, parentCls)) { addImport = false; break; } clsList.add(parentCls); parentCls = parentCls.getParentClass(); } Collections.reverse(clsList); if (addImport) { addImport(clsList.get(0)); } return Utils.listToString(clsList, ".", ClassInfo::getAliasShortName); } private void addImport(ClassInfo classInfo) { if (parentGen != null) { parentGen.addImport(classInfo); } else { imports.add(classInfo); } } public Set getImports() { if (parentGen != null) { return parentGen.getImports(); } else { return imports; } } private static boolean isBothClassesInOneTopClass(ClassInfo useCls, ClassInfo extClsInfo) { ClassInfo a = useCls.getTopParentClass(); ClassInfo b = extClsInfo.getTopParentClass(); if (a != null) { return a.equals(b); } // useCls - is a top class return useCls.equals(b); } private static boolean isClassInnerFor(ClassInfo inner, ClassInfo parent) { if (inner.isInner()) { ClassInfo p = inner.getParentClass(); return Objects.equals(p, parent) || isClassInnerFor(p, parent); } return false; } private static boolean checkInnerCollision(RootNode root, @Nullable ClassInfo useCls, ClassInfo searchCls) { if (useCls == null) { return false; } String shortName = searchCls.getAliasShortName(); if (useCls.getAliasShortName().equals(shortName)) { return true; } ClassNode classNode = root.resolveClass(useCls); if (classNode != null) { for (ClassNode inner : classNode.getInnerClasses()) { if (inner.getShortName().equals(shortName) && !inner.getFullName().equals(searchCls.getAliasFullName())) { return true; } } } return checkInnerCollision(root, useCls.getParentClass(), searchCls); } /** * Check if class with same name exists in current package */ private static boolean checkInPackageCollision(RootNode root, ClassInfo useCls, ClassInfo searchCls) { String currentPkg = useCls.getAliasPkg(); if (currentPkg.equals(searchCls.getAliasPkg())) { // search class already from current package return false; } String shortName = searchCls.getAliasShortName(); return root.getClsp().isClsKnown(currentPkg + '.' + shortName); } private static void addClassUsageInfo(ICodeWriter code, ClassNode cls) { List deps = cls.getDependencies(); code.startLine("// deps - ").add(Integer.toString(deps.size())); for (ClassNode depCls : deps) { code.startLine("// ").add(depCls.getClassInfo().getFullName()); } List useIn = cls.getUseIn(); code.startLine("// use in - ").add(Integer.toString(useIn.size())); for (ClassNode useCls : useIn) { code.startLine("// ").add(useCls.getClassInfo().getFullName()); } List useInMths = cls.getUseInMth(); code.startLine("// use in methods - ").add(Integer.toString(useInMths.size())); for (MethodNode useMth : useInMths) { code.startLine("// ").add(useMth.toString()); } } static void addMthUsageInfo(ICodeWriter code, MethodNode mth) { List useInMths = mth.getUseIn(); code.startLine("// use in methods - ").add(Integer.toString(useInMths.size())); for (MethodNode useMth : useInMths) { code.startLine("// ").add(useMth.toString()); } } private static void addFieldUsageInfo(ICodeWriter code, FieldNode fieldNode) { List useInMths = fieldNode.getUseIn(); code.startLine("// use in methods - ").add(Integer.toString(useInMths.size())); for (MethodNode useMth : useInMths) { code.startLine("// ").add(useMth.toString()); } } public ClassGen getParentGen() { return parentGen == null ? this : parentGen; } public AnnotationGen getAnnotationGen() { return annotationGen; } public boolean isFallbackMode() { return fallback; } public boolean isBodyGenStarted() { return bodyGenStarted; } public void setBodyGenStarted(boolean bodyGenStarted) { this.bodyGenStarted = bodyGenStarted; } @Nullable public NameGen getOuterNameGen() { return outerNameGen; } public void setOuterNameGen(@NotNull NameGen outerNameGen) { this.outerNameGen = outerNameGen; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/CodeGen.java ================================================ package jadx.core.codegen; import java.util.concurrent.Callable; import jadx.api.ICodeInfo; import jadx.api.JadxArgs; import jadx.api.impl.SimpleCodeInfo; import jadx.core.codegen.json.JsonCodeGen; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; import jadx.core.utils.exceptions.JadxRuntimeException; public class CodeGen { public static ICodeInfo generate(ClassNode cls) { if (cls.contains(AFlag.DONT_GENERATE)) { return ICodeInfo.EMPTY; } JadxArgs args = cls.root().getArgs(); switch (args.getOutputFormat()) { case JAVA: return generateJavaCode(cls, args); case JSON: return generateJson(cls); default: throw new JadxRuntimeException("Unknown output format"); } } private static ICodeInfo generateJavaCode(ClassNode cls, JadxArgs args) { ClassGen clsGen = new ClassGen(cls, args); return wrapCodeGen(cls, clsGen::makeClass); } private static ICodeInfo generateJson(ClassNode cls) { JsonCodeGen codeGen = new JsonCodeGen(cls); String clsJson = wrapCodeGen(cls, codeGen::process); return new SimpleCodeInfo(clsJson); } private static R wrapCodeGen(ClassNode cls, Callable codeGenFunc) { try { return codeGenFunc.call(); } catch (Exception e) { if (cls.contains(AFlag.RESTART_CODEGEN)) { cls.remove(AFlag.RESTART_CODEGEN); try { return codeGenFunc.call(); } catch (Exception ex) { throw new JadxRuntimeException("Code generation error after restart", ex); } } else { throw new JadxRuntimeException("Code generation error", e); } } } private CodeGen() { } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/ConditionGen.java ================================================ package jadx.core.codegen; import java.util.ArrayDeque; import java.util.Iterator; import java.util.Queue; import jadx.api.ICodeWriter; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.ArithNode; import jadx.core.dex.instructions.IfOp; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.regions.conditions.Compare; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.regions.conditions.IfCondition.Mode; import jadx.core.utils.exceptions.CodegenException; import jadx.core.utils.exceptions.JadxRuntimeException; public class ConditionGen extends InsnGen { private static class CondStack { private final Queue stack = new ArrayDeque<>(); public Queue getStack() { return stack; } public void push(IfCondition cond) { stack.add(cond); } public IfCondition pop() { return stack.poll(); } } public ConditionGen(InsnGen insnGen) { super(insnGen.mgen, insnGen.fallback); } public void add(ICodeWriter code, IfCondition condition) throws CodegenException { add(code, new CondStack(), condition); } void wrap(ICodeWriter code, IfCondition condition) throws CodegenException { wrap(code, new CondStack(), condition); } private void add(ICodeWriter code, CondStack stack, IfCondition condition) throws CodegenException { stack.push(condition); switch (condition.getMode()) { case COMPARE: addCompare(code, stack, condition.getCompare()); break; case TERNARY: addTernary(code, stack, condition); break; case NOT: addNot(code, stack, condition); break; case AND: case OR: addAndOr(code, stack, condition); break; default: throw new JadxRuntimeException("Unknown condition mode: " + condition.getMode()); } stack.pop(); } private void wrap(ICodeWriter code, CondStack stack, IfCondition cond) throws CodegenException { boolean wrap = isWrapNeeded(cond); if (wrap) { code.add('('); } add(code, stack, cond); if (wrap) { code.add(')'); } } private void wrap(ICodeWriter code, InsnArg firstArg) throws CodegenException { boolean wrap = isArgWrapNeeded(firstArg); if (wrap) { code.add('('); } addArg(code, firstArg, false); if (wrap) { code.add(')'); } } private void addCompare(ICodeWriter code, CondStack stack, Compare compare) throws CodegenException { IfOp op = compare.getOp(); InsnArg firstArg = compare.getA(); InsnArg secondArg = compare.getB(); if (firstArg.getType().equals(ArgType.BOOLEAN) && secondArg.isLiteral() && secondArg.getType().equals(ArgType.BOOLEAN)) { LiteralArg lit = (LiteralArg) secondArg; if (lit.getLiteral() == 0) { op = op.invert(); } if (op == IfOp.EQ) { // == true if (stack.getStack().size() == 1) { addArg(code, firstArg, false); } else { wrap(code, firstArg); } return; } else if (op == IfOp.NE) { // != true code.add('!'); wrap(code, firstArg); return; } mth.addWarn("Unsupported boolean condition " + op.getSymbol()); } addArg(code, firstArg, isArgWrapNeeded(firstArg)); code.add(' ').add(op.getSymbol()).add(' '); addArg(code, secondArg, isArgWrapNeeded(secondArg)); } private void addTernary(ICodeWriter code, CondStack stack, IfCondition condition) throws CodegenException { add(code, stack, condition.first()); code.add(" ? "); add(code, stack, condition.second()); code.add(" : "); add(code, stack, condition.third()); } private void addNot(ICodeWriter code, CondStack stack, IfCondition condition) throws CodegenException { code.add('!'); wrap(code, stack, condition.getArgs().get(0)); } private void addAndOr(ICodeWriter code, CondStack stack, IfCondition condition) throws CodegenException { String mode = condition.getMode() == Mode.AND ? " && " : " || "; Iterator it = condition.getArgs().iterator(); while (it.hasNext()) { wrap(code, stack, it.next()); if (it.hasNext()) { code.add(mode); } } } private boolean isWrapNeeded(IfCondition condition) { if (condition.isCompare() || condition.contains(AFlag.DONT_WRAP)) { return false; } return condition.getMode() != Mode.NOT; } private static boolean isArgWrapNeeded(InsnArg arg) { if (!arg.isInsnWrap()) { return false; } InsnNode insn = ((InsnWrapArg) arg).getWrapInsn(); InsnType insnType = insn.getType(); if (insnType == InsnType.ARITH) { switch (((ArithNode) insn).getOp()) { case ADD: case SUB: case MUL: case DIV: case REM: return false; default: return true; } } else { switch (insnType) { case INVOKE: case SGET: case IGET: case AGET: case CONST: case ARRAY_LENGTH: return false; default: return true; } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/InsnGen.java ================================================ package jadx.core.codegen; import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.CommentsLevel; import jadx.api.ICodeWriter; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.VarNode; import jadx.api.plugins.input.data.MethodHandleType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.core.codegen.utils.CodeGenUtils; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.FieldInitInsnAttr; import jadx.core.dex.attributes.nodes.FieldReplaceAttr; import jadx.core.dex.attributes.nodes.GenericInfoAttr; import jadx.core.dex.attributes.nodes.LoopLabelAttr; import jadx.core.dex.attributes.nodes.MethodReplaceAttr; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.ArithNode; import jadx.core.dex.instructions.ArithOp; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.ConstClassNode; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.FillArrayInsn; import jadx.core.dex.instructions.FilledNewArrayNode; import jadx.core.dex.instructions.GotoNode; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeCustomNode; import jadx.core.dex.instructions.InvokeCustomRawNode; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.NewArrayNode; import jadx.core.dex.instructions.SwitchInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.Named; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.java.JsrNode; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.RegionUtils; import jadx.core.utils.exceptions.CodegenException; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.android.AndroidResourcesUtils.handleAppResField; public class InsnGen { private static final Logger LOG = LoggerFactory.getLogger(InsnGen.class); protected final MethodGen mgen; protected final MethodNode mth; protected final RootNode root; protected final boolean fallback; protected enum Flags { BODY_ONLY, BODY_ONLY_NOWRAP, INLINE } public InsnGen(MethodGen mgen, boolean fallback) { this.mgen = mgen; this.mth = mgen.getMethodNode(); this.root = mth.root(); this.fallback = fallback; } private boolean isFallback() { return fallback; } public void addArgDot(ICodeWriter code, InsnArg arg) throws CodegenException { int len = code.getLength(); addArg(code, arg, true); if (len != code.getLength()) { code.add('.'); } } public void addArg(ICodeWriter code, InsnArg arg) throws CodegenException { addArg(code, arg, true); } public void addArg(ICodeWriter code, InsnArg arg, boolean wrap) throws CodegenException { addArg(code, arg, wrap ? BODY_ONLY_FLAG : BODY_ONLY_NOWRAP_FLAGS); } public void addArg(ICodeWriter code, InsnArg arg, Set flags) throws CodegenException { if (arg.isRegister()) { RegisterArg reg = (RegisterArg) arg; if (code.isMetadataSupported()) { code.attachAnnotation(VarNode.getRef(mth, reg)); } code.add(mgen.getNameGen().useArg(reg)); } else if (arg.isLiteral()) { addLiteralArg(code, (LiteralArg) arg, flags); } else if (arg.isInsnWrap()) { addWrappedArg(code, (InsnWrapArg) arg, flags); } else if (arg.isNamed()) { code.add(((Named) arg).getName()); } else { throw new CodegenException("Unknown arg type " + arg); } } private void addLiteralArg(ICodeWriter code, LiteralArg litArg, Set flags) { String literalStr = lit(litArg); if (!flags.contains(Flags.BODY_ONLY_NOWRAP) && literalStr.startsWith("-")) { code.add('(').add(literalStr).add(')'); } else { code.add(literalStr); } } private void addWrappedArg(ICodeWriter code, InsnWrapArg arg, Set flags) throws CodegenException { InsnNode wrapInsn = arg.getWrapInsn(); if (wrapInsn.contains(AFlag.FORCE_ASSIGN_INLINE)) { code.add('('); makeInsn(wrapInsn, code, Flags.INLINE); code.add(')'); } else { makeInsnBody(code, wrapInsn, flags); } } public void assignVar(ICodeWriter code, InsnNode insn) throws CodegenException { RegisterArg arg = insn.getResult(); if (insn.contains(AFlag.DECLARE_VAR)) { declareVar(code, arg); } else { addArg(code, arg, false); } } public void declareVar(ICodeWriter code, RegisterArg arg) { declareVar(code, arg.getSVar().getCodeVar()); } public void declareVar(ICodeWriter code, CodeVar codeVar) { if (codeVar.isFinal()) { code.add("final "); } useType(code, codeVar.getType()); code.add(' '); defVar(code, codeVar); } /** * Variable definition without type, only var name */ private void defVar(ICodeWriter code, CodeVar codeVar) { String varName = mgen.getNameGen().assignArg(codeVar); if (code.isMetadataSupported()) { code.attachDefinition(VarNode.get(mth, codeVar)); } code.add(varName); } private String lit(LiteralArg arg) { return TypeGen.literalToString(arg, mth, fallback); } private void instanceField(ICodeWriter code, FieldInfo field, InsnArg arg) throws CodegenException { ClassNode pCls = mth.getParentClass(); FieldNode fieldNode = pCls.root().resolveField(field); if (fieldNode != null) { FieldReplaceAttr replace = fieldNode.get(AType.FIELD_REPLACE); if (replace != null) { switch (replace.getReplaceType()) { case CLASS_INSTANCE: useClass(code, replace.getClsRef()); code.add(".this"); break; case VAR: addArg(code, replace.getVarRef()); break; } return; } } addArgDot(code, arg); if (fieldNode != null) { code.attachAnnotation(fieldNode); } if (fieldNode == null) { code.add(field.getAlias()); } else { code.add(fieldNode.getAlias()); } } protected void staticField(ICodeWriter code, FieldInfo field) throws CodegenException { FieldNode fieldNode = root.resolveField(field); if (fieldNode != null && fieldNode.contains(AFlag.INLINE_INSTANCE_FIELD) && fieldNode.getParentClass().contains(AType.ANONYMOUS_CLASS)) { FieldInitInsnAttr initInsnAttr = fieldNode.get(AType.FIELD_INIT_INSN); if (initInsnAttr != null) { InsnNode insn = initInsnAttr.getInsn(); if (insn instanceof ConstructorInsn) { fieldNode.add(AFlag.DONT_GENERATE); inlineAnonymousConstructor(code, fieldNode.getParentClass(), (ConstructorInsn) insn); return; } } } makeStaticFieldAccess(code, field, fieldNode, mgen.getClassGen()); } public static void makeStaticFieldAccess(ICodeWriter code, FieldInfo field, ClassGen clsGen) { FieldNode fieldNode = clsGen.getClassNode().root().resolveField(field); makeStaticFieldAccess(code, field, fieldNode, clsGen); } private static void makeStaticFieldAccess(ICodeWriter code, FieldInfo field, @Nullable FieldNode fieldNode, ClassGen clsGen) { ClassInfo declClass = field.getDeclClass(); // TODO boolean fieldFromThisClass = clsGen.getClassNode().getClassInfo().equals(declClass); if (!fieldFromThisClass || !clsGen.isBodyGenStarted()) { // Android specific resources class handler if (!handleAppResField(code, clsGen, declClass)) { clsGen.useClass(code, declClass); } code.add('.'); } if (fieldNode != null) { code.attachAnnotation(fieldNode); } if (fieldNode == null) { code.add(field.getAlias()); } else { code.add(fieldNode.getAlias()); } } public void useClass(ICodeWriter code, ArgType type) { mgen.getClassGen().useClass(code, type); } public void useClass(ICodeWriter code, ClassInfo cls) { mgen.getClassGen().useClass(code, cls); } protected void useType(ICodeWriter code, ArgType type) { mgen.getClassGen().useType(code, type); } public void makeInsn(InsnNode insn, ICodeWriter code) throws CodegenException { makeInsn(insn, code, null); } private static final Set EMPTY_FLAGS = EnumSet.noneOf(Flags.class); private static final Set BODY_ONLY_FLAG = EnumSet.of(Flags.BODY_ONLY); private static final Set BODY_ONLY_NOWRAP_FLAGS = EnumSet.of(Flags.BODY_ONLY_NOWRAP); protected void makeInsn(InsnNode insn, ICodeWriter code, Flags flag) throws CodegenException { if (insn.getType() == InsnType.REGION_ARG) { return; } try { if (flag == Flags.BODY_ONLY || flag == Flags.BODY_ONLY_NOWRAP) { makeInsnBody(code, insn, flag == Flags.BODY_ONLY ? BODY_ONLY_FLAG : BODY_ONLY_NOWRAP_FLAGS); } else { if (flag != Flags.INLINE) { code.startLineWithNum(insn.getSourceLine()); InsnCodeOffset.attach(code, insn); if (insn.contains(AFlag.COMMENT_OUT)) { code.add("// "); } } RegisterArg resArg = insn.getResult(); if (resArg != null) { SSAVar var = resArg.getSVar(); if (var == null || var.getUseCount() != 0 || insn.getType() != InsnType.CONSTRUCTOR) { assignVar(code, insn); code.add(" = "); } } makeInsnBody(code, insn, EMPTY_FLAGS); if (flag != Flags.INLINE) { code.add(';'); CodeGenUtils.addCodeComments(code, mth, insn); } } } catch (Exception e) { throw new CodegenException(mth, "Error generate insn: " + insn, e); } } private void makeInsnBody(ICodeWriter code, InsnNode insn, Set state) throws CodegenException { switch (insn.getType()) { case CONST_STR: String str = ((ConstStringNode) insn).getString(); code.add(mth.root().getStringUtils().unescapeString(str)); break; case CONST_CLASS: ArgType clsType = ((ConstClassNode) insn).getClsType(); useType(code, clsType); code.add(".class"); break; case CONST: LiteralArg arg = (LiteralArg) insn.getArg(0); code.add(lit(arg)); break; case MOVE: addArg(code, insn.getArg(0), false); break; case CHECK_CAST: case CAST: { boolean wrap = state.contains(Flags.BODY_ONLY); if (wrap) { code.add('('); } code.add('('); useType(code, (ArgType) ((IndexInsnNode) insn).getIndex()); code.add(") "); addArg(code, insn.getArg(0), true); if (wrap) { code.add(')'); } break; } case ARITH: makeArith((ArithNode) insn, code, state); break; case NEG: oneArgInsn(code, insn, state, '-'); break; case NOT: char op = insn.getArg(0).getType() == ArgType.BOOLEAN ? '!' : '~'; oneArgInsn(code, insn, state, op); break; case RETURN: if (insn.getArgsCount() != 0) { code.add("return "); addArg(code, insn.getArg(0), false); } else { code.add("return"); } break; case BREAK: code.add("break"); LoopLabelAttr labelAttr = insn.get(AType.LOOP_LABEL); if (labelAttr != null) { code.add(' ').add(mgen.getNameGen().getLoopLabel(labelAttr)); } break; case CONTINUE: code.add("continue"); break; case THROW: code.add("throw "); addArg(code, insn.getArg(0), true); break; case CMP_L: case CMP_G: code.add('('); addArg(code, insn.getArg(0)); code.add(" > "); addArg(code, insn.getArg(1)); code.add(" ? 1 : ("); addArg(code, insn.getArg(0)); code.add(" == "); addArg(code, insn.getArg(1)); code.add(" ? 0 : -1))"); break; case INSTANCE_OF: { boolean wrap = state.contains(Flags.BODY_ONLY); if (wrap) { code.add('('); } addArg(code, insn.getArg(0)); code.add(" instanceof "); useType(code, (ArgType) ((IndexInsnNode) insn).getIndex()); if (wrap) { code.add(')'); } break; } case CONSTRUCTOR: makeConstructor((ConstructorInsn) insn, code); break; case INVOKE: makeInvoke((InvokeNode) insn, code); break; case NEW_ARRAY: { ArgType arrayType = ((NewArrayNode) insn).getArrayType(); code.add("new "); useType(code, arrayType.getArrayRootElement()); int k = 0; int argsCount = insn.getArgsCount(); for (; k < argsCount; k++) { code.add('['); addArg(code, insn.getArg(k), false); code.add(']'); } int dim = arrayType.getArrayDimension(); for (; k < dim; k++) { code.add("[]"); } break; } case ARRAY_LENGTH: addArg(code, insn.getArg(0)); code.add(".length"); break; case FILLED_NEW_ARRAY: filledNewArray((FilledNewArrayNode) insn, code); break; case FILL_ARRAY: FillArrayInsn arrayNode = (FillArrayInsn) insn; if (fallback) { String arrStr = arrayNode.dataToString(); addArg(code, insn.getArg(0)); code.add(" = {").add(arrStr.substring(1, arrStr.length() - 1)).add("} // fill-array"); } else { fillArray(code, arrayNode); } break; case AGET: addArg(code, insn.getArg(0)); code.add('['); addArg(code, insn.getArg(1), false); code.add(']'); break; case APUT: addArg(code, insn.getArg(0)); code.add('['); addArg(code, insn.getArg(1), false); code.add("] = "); addArg(code, insn.getArg(2), false); break; case IGET: { FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) insn).getIndex(); instanceField(code, fieldInfo, insn.getArg(0)); break; } case IPUT: { FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) insn).getIndex(); instanceField(code, fieldInfo, insn.getArg(1)); code.add(" = "); addArg(code, insn.getArg(0), false); break; } case SGET: staticField(code, (FieldInfo) ((IndexInsnNode) insn).getIndex()); break; case SPUT: FieldInfo field = (FieldInfo) ((IndexInsnNode) insn).getIndex(); staticField(code, field); code.add(" = "); addArg(code, insn.getArg(0), false); break; case STR_CONCAT: boolean wrap = state.contains(Flags.BODY_ONLY); if (wrap) { code.add('('); } for (Iterator it = insn.getArguments().iterator(); it.hasNext();) { addArg(code, it.next()); if (it.hasNext()) { code.add(" + "); } } if (wrap) { code.add(')'); } break; case MONITOR_ENTER: if (isFallback()) { code.add("monitor-enter("); addArg(code, insn.getArg(0)); code.add(')'); } break; case MONITOR_EXIT: if (isFallback()) { code.add("monitor-exit("); if (insn.getArgsCount() == 1) { addArg(code, insn.getArg(0)); } code.add(')'); } break; case TERNARY: makeTernary((TernaryInsn) insn, code, state); break; case ONE_ARG: addArg(code, insn.getArg(0), state); break; /* fallback mode instructions */ case IF: fallbackOnlyInsn(insn); IfNode ifInsn = (IfNode) insn; code.add("if ("); addArg(code, insn.getArg(0)); code.add(' '); code.add(ifInsn.getOp().getSymbol()).add(' '); addArg(code, insn.getArg(1)); code.add(") goto ").add(MethodGen.getLabelName(ifInsn)); break; case GOTO: fallbackOnlyInsn(insn); code.add("goto ").add(MethodGen.getLabelName(((GotoNode) insn).getTarget())); break; case MOVE_EXCEPTION: fallbackOnlyInsn(insn); code.add("move-exception"); break; case SWITCH: fallbackOnlyInsn(insn); SwitchInsn sw = (SwitchInsn) insn; code.add("switch("); addArg(code, insn.getArg(0)); code.add(") {"); code.incIndent(); int[] keys = sw.getKeys(); int size = keys.length; BlockNode[] targetBlocks = sw.getTargetBlocks(); if (targetBlocks != null) { for (int i = 0; i < size; i++) { code.startLine("case ").add(Integer.toString(keys[i])).add(": goto "); code.add(MethodGen.getLabelName(targetBlocks[i])).add(';'); } code.startLine("default: goto "); code.add(MethodGen.getLabelName(sw.getDefTargetBlock())).add(';'); } else { int[] targets = sw.getTargets(); for (int i = 0; i < size; i++) { code.startLine("case ").add(Integer.toString(keys[i])).add(": goto "); code.add(MethodGen.getLabelName(targets[i])).add(';'); } code.startLine("default: goto "); code.add(MethodGen.getLabelName(sw.getDefaultCaseOffset())).add(';'); } code.decIndent(); code.startLine('}'); break; case NEW_INSTANCE: // only fallback - make new instance in constructor invoke fallbackOnlyInsn(insn); code.add("new ").add(insn.getResult().getInitType().toString()); break; case PHI: fallbackOnlyInsn(insn); code.add(insn.getType().toString()).add('('); for (InsnArg insnArg : insn.getArguments()) { addArg(code, insnArg); code.add(' '); } code.add(')'); break; case MOVE_RESULT: fallbackOnlyInsn(insn); code.add("move-result"); break; case FILL_ARRAY_DATA: fallbackOnlyInsn(insn); code.add("fill-array " + insn); break; case SWITCH_DATA: fallbackOnlyInsn(insn); code.add(insn.toString()); break; case MOVE_MULTI: fallbackOnlyInsn(insn); int len = insn.getArgsCount(); for (int i = 0; i < len - 1; i += 2) { addArg(code, insn.getArg(i)); code.add(" = "); addArg(code, insn.getArg(i + 1)); code.add("; "); } break; case JAVA_JSR: fallbackOnlyInsn(insn); code.add("jsr -> ").add(MethodGen.getLabelName(((JsrNode) insn).getTarget())); break; case JAVA_RET: fallbackOnlyInsn(insn); code.add("ret "); addArg(code, insn.getArg(0)); break; default: throw new CodegenException(mth, "Unknown instruction: " + insn.getType()); } } /** * In most cases must be combined with new array instructions. * Use one by one array fill (can be replaced with System.arrayCopy) */ private void fillArray(ICodeWriter code, FillArrayInsn arrayNode) throws CodegenException { if (mth.checkCommentsLevel(CommentsLevel.INFO)) { code.add("// fill-array-data instruction"); } code.startLine(); InsnArg arrArg = arrayNode.getArg(0); ArgType arrayType = arrArg.getType(); ArgType elemType; if (arrayType.isTypeKnown() && arrayType.isArray()) { elemType = arrayType.getArrayElement(); } else { ArgType elementType = arrayNode.getElementType(); // unknown type elemType = elementType.selectFirst(); } List args = arrayNode.getLiteralArgs(elemType); int len = args.size(); for (int i = 0; i < len; i++) { if (i != 0) { code.add(';'); code.startLine(); } addArg(code, arrArg); code.add('[').add(Integer.toString(i)).add("] = ").add(lit(args.get(i))); } } private void oneArgInsn(ICodeWriter code, InsnNode insn, Set state, char op) throws CodegenException { boolean wrap = state.contains(Flags.BODY_ONLY); if (wrap) { code.add('('); } code.add(op); addArg(code, insn.getArg(0)); if (wrap) { code.add(')'); } } private void fallbackOnlyInsn(InsnNode insn) throws CodegenException { if (!fallback) { String msg = insn.getType() + " instruction can be used only in fallback mode"; CodegenException e = new CodegenException(msg); mth.addError(msg, e); mth.getParentClass().getTopParentClass().add(AFlag.RESTART_CODEGEN); throw e; } } private void filledNewArray(FilledNewArrayNode insn, ICodeWriter code) throws CodegenException { if (!insn.contains(AFlag.DECLARE_VAR)) { code.add("new "); useType(code, insn.getArrayType()); } code.add('{'); int c = insn.getArgsCount(); int wrap = 0; for (int i = 0; i < c; i++) { addArg(code, insn.getArg(i), false); if (i + 1 < c) { code.add(", "); } wrap++; if (wrap == 1000) { code.startLine(); wrap = 0; } } code.add('}'); } private void makeConstructor(ConstructorInsn insn, ICodeWriter code) throws CodegenException { ClassNode cls = mth.root().resolveClass(insn.getClassType()); if (cls != null && cls.isAnonymous() && !fallback) { inlineAnonymousConstructor(code, cls, insn); return; } if (insn.isSelf()) { throw new JadxRuntimeException("Constructor 'self' invoke must be removed!"); } MethodNode callMth = mth.root().resolveMethod(insn.getCallMth()); MethodNode refMth = callMth; if (callMth != null) { MethodReplaceAttr replaceAttr = callMth.get(AType.METHOD_REPLACE); if (replaceAttr != null) { refMth = replaceAttr.getReplaceMth(); } } if (insn.isSuper()) { code.attachAnnotation(refMth); code.add("super"); } else if (insn.isThis()) { code.attachAnnotation(refMth); code.add("this"); } else { boolean forceShortName = addOuterClassInstance(insn, code, callMth); code.add("new "); if (refMth == null || refMth.contains(AFlag.DONT_GENERATE)) { // use class reference if constructor method is missing (default constructor) code.attachAnnotation(mth.root().resolveClass(insn.getCallMth().getDeclClass())); } else { code.attachAnnotation(refMth); } if (forceShortName) { mgen.getClassGen().addClsShortNameForced(code, insn.getClassType()); } else { mgen.getClassGen().addClsName(code, insn.getClassType()); } GenericInfoAttr genericInfoAttr = insn.get(AType.GENERIC_INFO); if (genericInfoAttr != null) { code.add('<'); if (genericInfoAttr.isExplicit()) { boolean first = true; for (ArgType type : genericInfoAttr.getGenericTypes()) { if (!first) { code.add(','); } else { first = false; } mgen.getClassGen().useType(code, type); } } code.add('>'); } } generateMethodArguments(code, insn, 0, callMth); } private boolean addOuterClassInstance(ConstructorInsn insn, ICodeWriter code, MethodNode callMth) throws CodegenException { if (callMth == null || !callMth.contains(AFlag.SKIP_FIRST_ARG)) { return false; } ClassNode ctrCls = callMth.getDeclaringClass(); if (!ctrCls.isInner() || insn.getArgsCount() == 0) { return false; } InsnArg instArg = insn.getArg(0); if (instArg.isThis()) { return false; } // instance arg should be of an outer class type if (!instArg.getType().equals(ctrCls.getDeclaringClass().getType())) { return false; } addArgDot(code, instArg); // can't use another dot, force short name of class return true; } private void inlineAnonymousConstructor(ICodeWriter code, ClassNode cls, ConstructorInsn insn) throws CodegenException { cls.ensureProcessed(); if (this.mth.getParentClass() == cls) { cls.remove(AType.ANONYMOUS_CLASS); cls.remove(AFlag.DONT_GENERATE); mth.getParentClass().getTopParentClass().add(AFlag.RESTART_CODEGEN); throw new CodegenException("Anonymous inner class unlimited recursion detected." + " Convert class to inner: " + cls.getClassInfo().getFullName()); } ArgType parent = cls.get(AType.ANONYMOUS_CLASS).getBaseType(); // hide empty anonymous constructors for (MethodNode ctor : cls.getMethods()) { if (ctor.contains(AFlag.ANONYMOUS_CONSTRUCTOR) && RegionUtils.isEmpty(ctor.getRegion())) { ctor.add(AFlag.DONT_GENERATE); } } code.attachDefinition(cls); code.add("new "); useClass(code, parent); MethodNode callMth = mth.root().resolveMethod(insn.getCallMth()); if (callMth != null) { // copy var names List mthArgs = callMth.getArgRegs(); int argsCount = Math.min(insn.getArgsCount(), mthArgs.size()); for (int i = 0; i < argsCount; i++) { InsnArg arg = insn.getArg(i); if (arg.isRegister()) { RegisterArg mthArg = mthArgs.get(i); RegisterArg insnArg = (RegisterArg) arg; mthArg.getSVar().setCodeVar(insnArg.getSVar().getCodeVar()); } } } generateMethodArguments(code, insn, 0, callMth); code.add(' '); ClassGen classGen = new ClassGen(cls, mgen.getClassGen().getParentGen()); classGen.setOuterNameGen(mgen.getNameGen()); classGen.addClassBody(code, true); mth.getParentClass().addInlinedClass(cls); } private void makeInvoke(InvokeNode insn, ICodeWriter code) throws CodegenException { InvokeType type = insn.getInvokeType(); if (type == InvokeType.CUSTOM) { makeInvokeLambda(code, (InvokeCustomNode) insn); return; } MethodInfo callMth = insn.getCallMth(); MethodNode callMthNode = mth.root().resolveMethod(callMth); if (type == InvokeType.CUSTOM_RAW) { makeInvokeCustomRaw((InvokeCustomRawNode) insn, callMthNode, code); return; } if (insn.isPolymorphicCall()) { // add missing cast code.add('('); useType(code, callMth.getReturnType()); code.add(") "); } int k = 0; switch (type) { case DIRECT: case VIRTUAL: case INTERFACE: case POLYMORPHIC: InsnArg arg = insn.getArg(0); if (needInvokeArg(arg)) { addArgDot(code, arg); } k++; break; case SUPER: callSuper(code, callMth); k++; // use 'super' instead 'this' in 0 arg code.add('.'); break; case STATIC: ClassInfo insnCls = mth.getParentClass().getClassInfo(); ClassInfo declClass = callMth.getDeclClass(); if (!insnCls.equals(declClass)) { useClass(code, declClass); code.add('.'); } break; } if (callMthNode != null) { code.attachAnnotation(callMthNode); } if (insn.contains(AFlag.FORCE_RAW_NAME)) { code.add(callMth.getName()); } else { if (callMthNode != null) { code.add(callMthNode.getAlias()); } else { code.add(callMth.getAlias()); } } generateMethodArguments(code, insn, k, callMthNode); } private void makeInvokeCustomRaw(InvokeCustomRawNode insn, @Nullable MethodNode callMthNode, ICodeWriter code) throws CodegenException { if (isFallback()) { code.add("call_site("); code.incIndent(); for (EncodedValue value : insn.getCallSiteValues()) { code.startLine(value.toString()); } code.decIndent(); code.startLine(").invoke"); generateMethodArguments(code, insn, 0, callMthNode); } else { ArgType returnType = insn.getCallMth().getReturnType(); if (!returnType.isVoid()) { code.add('('); useType(code, returnType); code.add(") "); } makeInvoke(insn.getResolveInvoke(), code); code.add(".dynamicInvoker().invoke"); generateMethodArguments(code, insn, 0, callMthNode); code.add(" /* invoke-custom */"); } } // FIXME: add 'this' for equals methods in scope private boolean needInvokeArg(InsnArg arg) { if (arg.isAnyThis()) { if (arg.isThis()) { return false; } ClassNode clsNode = mth.root().resolveClass(arg.getType()); if (clsNode != null && clsNode.contains(AFlag.DONT_GENERATE)) { return false; } } return true; } private void makeInvokeLambda(ICodeWriter code, InvokeCustomNode customNode) throws CodegenException { if (customNode.isUseRef()) { makeRefLambda(code, customNode); return; } if (fallback || !customNode.isInlineInsn()) { makeSimpleLambda(code, customNode); return; } MethodNode callMth = (MethodNode) customNode.getCallInsn().get(AType.METHOD_DETAILS); makeInlinedLambdaMethod(code, customNode, callMth); } private void makeRefLambda(ICodeWriter code, InvokeCustomNode customNode) throws CodegenException { InsnNode callInsn = customNode.getCallInsn(); if (callInsn instanceof ConstructorInsn) { MethodInfo callMth = ((ConstructorInsn) callInsn).getCallMth(); useClass(code, callMth.getDeclClass()); code.add("::new"); return; } if (callInsn instanceof InvokeNode) { InvokeNode invokeInsn = (InvokeNode) callInsn; MethodInfo callMth = invokeInsn.getCallMth(); if (customNode.getHandleType() == MethodHandleType.INVOKE_STATIC) { useClass(code, callMth.getDeclClass()); } else { addArg(code, customNode.getArg(0)); } code.add("::").add(callMth.getAlias()); } } private void makeSimpleLambda(ICodeWriter code, InvokeCustomNode customNode) { try { InsnNode callInsn = customNode.getCallInsn(); MethodInfo implMthInfo = customNode.getImplMthInfo(); int implArgsCount = implMthInfo.getArgsCount(); if (implArgsCount == 0) { code.add("()"); } else { code.add('('); int callArgsCount = callInsn.getArgsCount(); int startArg = callArgsCount - implArgsCount; if (customNode.getHandleType() != MethodHandleType.INVOKE_STATIC && customNode.getArgsCount() > 0 && customNode.getArg(0).isThis()) { callInsn.getArg(0).add(AFlag.THIS); } if (startArg >= 0) { for (int i = startArg; i < callArgsCount; i++) { if (i != startArg) { code.add(", "); } addArg(code, callInsn.getArg(i)); } } else { code.add("/* ERROR: " + startArg + " */"); } code.add(')'); } code.add(" -> {"); if (fallback) { code.add(" // ").add(implMthInfo.toString()); } code.incIndent(); code.startLine(); if (!implMthInfo.getReturnType().isVoid()) { code.add("return "); } makeInsn(callInsn, code, Flags.INLINE); code.add(";"); code.decIndent(); code.startLine('}'); } catch (Exception e) { throw new JadxRuntimeException("Failed to generate 'invoke-custom' instruction: " + e.getMessage(), e); } } private void makeInlinedLambdaMethod(ICodeWriter code, InvokeCustomNode customNode, MethodNode callMth) throws CodegenException { MethodGen callMthGen = new MethodGen(mgen.getClassGen(), callMth); NameGen nameGen = callMthGen.getNameGen(); nameGen.inheritUsedNames(this.mgen.getNameGen()); List implArgs = customNode.getImplMthInfo().getArgumentsTypes(); List callArgs = callMth.getArgRegs(); if (implArgs.isEmpty()) { code.add("()"); } else { int callArgsCount = callArgs.size(); int startArg = callArgsCount - implArgs.size(); if (callArgsCount - startArg > 1) { code.add('('); } for (int i = startArg; i < callArgsCount; i++) { if (i != startArg) { code.add(", "); } CodeVar argCodeVar = callArgs.get(i).getSVar().getCodeVar(); defVar(code, argCodeVar); } if (callArgsCount - startArg > 1) { code.add(')'); } } // force set external arg names into call method args int extArgsCount = customNode.getArgsCount(); int startArg = customNode.getHandleType() == MethodHandleType.INVOKE_STATIC ? 0 : 1; // skip 'this' arg int callArg = 0; for (int i = startArg; i < extArgsCount; i++) { InsnArg arg = customNode.getArg(i); if (arg.isRegister()) { RegisterArg extArg = (RegisterArg) arg; RegisterArg callRegArg = callArgs.get(callArg++); callRegArg.getSVar().setCodeVar(extArg.getSVar().getCodeVar()); } else { throw new JadxRuntimeException("Unexpected argument type in lambda call: " + arg.getClass().getSimpleName()); } } code.add(" -> {"); code.incIndent(); callMthGen.addInstructions(code); code.decIndent(); code.startLine('}'); } private void callSuper(ICodeWriter code, MethodInfo callMth) { ClassInfo superCallCls = getClassForSuperCall(callMth); if (superCallCls == null) { // unknown class, add comment to keep that info code.add("super/*").add(callMth.getDeclClass().getFullName()).add("*/"); return; } ClassInfo curClass = mth.getParentClass().getClassInfo(); if (superCallCls.equals(curClass)) { code.add("super"); return; } // use custom class useClass(code, superCallCls); code.add(".super"); } /** * Search call class in super types of this * and all parent classes (needed for inlined synthetic calls) */ @Nullable private ClassInfo getClassForSuperCall(MethodInfo callMth) { ArgType declClsType = callMth.getDeclClass().getType(); ClassNode parentNode = mth.getParentClass(); while (true) { ClassInfo parentCls = parentNode.getClassInfo(); if (ArgType.isInstanceOf(root, parentCls.getType(), declClsType)) { return parentCls; } ClassNode nextParent = parentNode.getParentClass(); if (nextParent == parentNode) { // no parent, class not found return null; } parentNode = nextParent; } } void generateMethodArguments(ICodeWriter code, BaseInvokeNode insn, int startArgNum, @Nullable MethodNode mthNode) throws CodegenException { int k = startArgNum; if (mthNode != null && mthNode.contains(AFlag.SKIP_FIRST_ARG)) { k++; } int argsCount = insn.getArgsCount(); code.add('('); SkipMethodArgsAttr skipAttr = mthNode == null ? null : mthNode.get(AType.SKIP_MTH_ARGS); boolean firstArg = true; if (k < argsCount) { for (int i = k; i < argsCount; i++) { InsnArg arg = insn.getArg(i); if (arg.contains(AFlag.SKIP_ARG) || (skipAttr != null && skipAttr.isSkip(i - startArgNum))) { continue; } if (firstArg) { firstArg = false; } else { code.add(", "); } if (i == argsCount - 1 && processVarArg(code, insn, arg)) { continue; } addArg(code, arg, false); } } code.add(')'); } /** * Expand varArgs from filled array. */ private boolean processVarArg(ICodeWriter code, BaseInvokeNode invokeInsn, InsnArg lastArg) throws CodegenException { if (!invokeInsn.contains(AFlag.VARARG_CALL)) { return false; } if (!lastArg.getType().isArray() || !lastArg.isInsnWrap()) { return false; } InsnNode insn = ((InsnWrapArg) lastArg).getWrapInsn(); if (insn.getType() != InsnType.FILLED_NEW_ARRAY) { return false; } int count = insn.getArgsCount(); for (int i = 0; i < count; i++) { InsnArg elemArg = insn.getArg(i); addArg(code, elemArg, false); if (i < count - 1) { code.add(", "); } } return true; } private void makeTernary(TernaryInsn insn, ICodeWriter code, Set state) throws CodegenException { boolean wrap = state.contains(Flags.BODY_ONLY); if (wrap) { code.add('('); } InsnArg first = insn.getArg(0); InsnArg second = insn.getArg(1); ConditionGen condGen = new ConditionGen(this); if (first.isTrue() && second.isFalse()) { condGen.add(code, insn.getCondition()); } else { condGen.wrap(code, insn.getCondition()); code.add(" ? "); addArg(code, first, false); code.add(" : "); addArg(code, second, false); } if (wrap) { code.add(')'); } } private void makeArith(ArithNode insn, ICodeWriter code, Set state) throws CodegenException { if (insn.contains(AFlag.ARITH_ONEARG)) { makeArithOneArg(insn, code); return; } // wrap insn in brackets for save correct operation order boolean wrap = state.contains(Flags.BODY_ONLY) && !insn.contains(AFlag.DONT_WRAP); if (wrap) { code.add('('); } addArg(code, insn.getArg(0)); code.add(' '); code.add(insn.getOp().getSymbol()); code.add(' '); addArg(code, insn.getArg(1)); if (wrap) { code.add(')'); } } private void makeArithOneArg(ArithNode insn, ICodeWriter code) throws CodegenException { ArithOp op = insn.getOp(); InsnArg resArg = insn.getArg(0); InsnArg arg = insn.getArg(1); // "++" or "--" if (arg.isLiteral() && (op == ArithOp.ADD || op == ArithOp.SUB)) { LiteralArg lit = (LiteralArg) arg; if (lit.getLiteral() == 1 && lit.isInteger()) { addArg(code, resArg, false); String opSymbol = op.getSymbol(); code.add(opSymbol).add(opSymbol); return; } } // +=, -=, ... addArg(code, resArg, false); code.add(' ').add(op.getSymbol()).add("= "); addArg(code, arg, false); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/MethodGen.java ================================================ package jadx.core.codegen; import java.util.List; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.CommentsLevel; import jadx.api.DecompilationMode; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; import jadx.api.args.IntegerFormat; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.VarNode; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr; import jadx.core.Consts; import jadx.core.Jadx; import jadx.core.codegen.utils.CodeGenUtils; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr; import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.attributes.nodes.JumpInfo; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.MethodReplaceAttr; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.CodegenException; import jadx.core.utils.exceptions.JadxOverflowException; import static jadx.core.codegen.MethodGen.FallbackOption.BLOCK_DUMP; import static jadx.core.codegen.MethodGen.FallbackOption.COMMENTED_DUMP; import static jadx.core.codegen.MethodGen.FallbackOption.FALLBACK_MODE; public class MethodGen { private static final Logger LOG = LoggerFactory.getLogger(MethodGen.class); private final MethodNode mth; private final ClassGen classGen; private final AnnotationGen annotationGen; private final NameGen nameGen; public MethodGen(ClassGen classGen, MethodNode mth) { this.mth = mth; this.classGen = classGen; this.annotationGen = classGen.getAnnotationGen(); this.nameGen = new NameGen(mth, classGen); } public ClassGen getClassGen() { return classGen; } public NameGen getNameGen() { return nameGen; } public MethodNode getMethodNode() { return mth; } public boolean addDefinition(ICodeWriter code) { if (mth.getMethodInfo().isClassInit()) { code.startLine(); code.attachDefinition(mth); code.add("static"); return true; } if (mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) { // don't add method name and arguments code.startLine(); code.attachDefinition(mth); return false; } if (Consts.DEBUG_USAGE) { ClassGen.addMthUsageInfo(code, mth); } addOverrideAnnotation(code, mth); annotationGen.addForMethod(code, mth); AccessInfo clsAccFlags = mth.getParentClass().getAccessFlags(); AccessInfo ai = mth.getAccessFlags(); // don't add 'abstract' and 'public' to methods in interface if (clsAccFlags.isInterface()) { ai = ai.remove(AccessFlags.ABSTRACT); ai = ai.remove(AccessFlags.PUBLIC); } // don't add 'public' for annotations if (clsAccFlags.isAnnotation()) { ai = ai.remove(AccessFlags.PUBLIC); } if (mth.getMethodInfo().hasAlias() && !ai.isConstructor()) { CodeGenUtils.addRenamedComment(code, mth, mth.getName()); } if (mth.contains(AFlag.INCONSISTENT_CODE) && mth.checkCommentsLevel(CommentsLevel.ERROR)) { code.startLine("/*"); code.incIndent(); code.startLine("Code decompiled incorrectly, please refer to instructions dump."); if (!mth.root().getArgs().isShowInconsistentCode()) { if (code.isMetadataSupported()) { code.startLine("To view partially-correct code enable 'Show inconsistent code' option in preferences"); } else { code.startLine("To view partially-correct add '--show-bad-code' argument"); } } code.decIndent(); code.startLine("*/"); } code.startLineWithNum(mth.getSourceLine()); code.add(ai.makeString(mth.checkCommentsLevel(CommentsLevel.INFO))); if (clsAccFlags.isInterface() && !mth.isNoCode() && !mth.getAccessFlags().isStatic()) { // add 'default' for method with code in interface code.add("default "); } if (classGen.addGenericTypeParameters(code, mth.getTypeParameters(), false)) { code.add(' '); } if (ai.isConstructor()) { code.attachDefinition(mth); code.add(classGen.getClassNode().getShortName()); // constructor } else { classGen.useType(code, mth.getReturnType()); code.add(' '); MethodNode defMth = getMethodForDefinition(); code.attachDefinition(defMth); code.add(defMth.getAlias()); } code.add('('); addMethodArguments(code); code.add(')'); annotationGen.addThrows(mth, code); // add default value for annotation class if (mth.getParentClass().getAccessFlags().isAnnotation()) { EncodedValue def = annotationGen.getAnnotationDefaultValue(mth); if (def != null) { code.add(" default "); annotationGen.encodeValue(mth.root(), code, def); } } return true; } private MethodNode getMethodForDefinition() { MethodReplaceAttr replaceAttr = mth.get(AType.METHOD_REPLACE); if (replaceAttr != null) { return replaceAttr.getReplaceMth(); } return mth; } private void addOverrideAnnotation(ICodeWriter code, MethodNode mth) { MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE); if (overrideAttr == null) { return; } if (!overrideAttr.getBaseMethods().contains(mth)) { code.startLine("@Override"); if (mth.checkCommentsLevel(CommentsLevel.INFO)) { code.add(" // "); code.add(Utils.listToString(overrideAttr.getOverrideList(), ", ", md -> md.getMethodInfo().getDeclClass().getAliasFullName())); } } if (Consts.DEBUG) { code.startLine("// related by override: "); code.add(Utils.listToString(overrideAttr.getRelatedMthNodes(), ", ", m -> m.getParentClass().getFullName())); } } private void addMethodArguments(ICodeWriter code) { List args = mth.getArgRegs(); AnnotationMethodParamsAttr paramsAnnotation = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS); int argNum = -1; int lastArgNum = args.size() - 1; boolean first = true; for (RegisterArg mthArg : args) { argNum++; if (SkipMethodArgsAttr.isSkip(mth, argNum)) { continue; } if (first) { first = false; } else { code.add(", "); } SSAVar ssaVar = mthArg.getSVar(); CodeVar var; if (ssaVar == null) { // abstract or interface methods var = CodeVar.fromMthArg(mthArg, classGen.isFallbackMode()); } else { var = ssaVar.getCodeVar(); } // add argument annotation if (paramsAnnotation != null) { annotationGen.addForParameter(code, paramsAnnotation, argNum); } if (var.isFinal()) { code.add("final "); } ArgType argType; ArgType varType = var.getType(); if (varType == null || varType == ArgType.UNKNOWN) { // occur on decompilation errors argType = mthArg.getInitType(); } else { argType = varType; } if (argNum == lastArgNum && mth.getAccessFlags().isVarArgs()) { // change last array argument to varargs if (argType.isArray()) { ArgType elType = argType.getArrayElement(); classGen.useType(code, elType); code.add("..."); } else { mth.addWarnComment("Last argument in varargs method is not array: " + var); classGen.useType(code, argType); } } else { classGen.useType(code, argType); } code.add(' '); String varName = nameGen.assignArg(var); if (code.isMetadataSupported() && ssaVar != null /* for fallback mode */) { code.attachDefinition(VarNode.get(mth, var)); } code.add(varName); } } public void addInstructions(ICodeWriter code) throws CodegenException { JadxArgs args = mth.root().getArgs(); DecompileModeOverrideAttr modeOverrideAttr = mth.getTopParentClass().get(AType.DECOMPILE_MODE_OVERRIDE); DecompilationMode mode; if (modeOverrideAttr != null) { mode = modeOverrideAttr.getMode(); } else { mode = args.getDecompilationMode(); } switch (mode) { case AUTO: if (classGen.isFallbackMode() || mth.getRegion() == null) { // TODO: try simple mode first dumpInstructions(code); } else { addRegionInsns(code); } break; case RESTRUCTURE: addRegionInsns(code); break; case SIMPLE: addSimpleMethodCode(code); break; case FALLBACK: addFallbackMethodCode(code, FALLBACK_MODE); break; } } public void addRegionInsns(ICodeWriter code) throws CodegenException { try { RegionGen regionGen = new RegionGen(this); regionGen.makeRegion(code, mth.getRegion()); } catch (StackOverflowError | BootstrapMethodError e) { mth.addError("Method code generation error", new JadxOverflowException("StackOverflow")); CodeGenUtils.addErrors(code, mth); dumpInstructions(code); } catch (Exception e) { if (mth.getParentClass().getTopParentClass().contains(AFlag.RESTART_CODEGEN)) { throw e; } mth.addError("Method code generation error", e); CodeGenUtils.addErrors(code, mth); dumpInstructions(code); } } private void addSimpleMethodCode(ICodeWriter code) { if (mth.getBasicBlocks() == null) { code.startLine("// Blocks not ready for simple mode, using fallback"); addFallbackMethodCode(code, FALLBACK_MODE); return; } JadxArgs args = mth.root().getArgs(); ICodeWriter tmpCode = args.getCodeWriterProvider().apply(args); try { tmpCode.setIndent(code.getIndent()); generateSimpleCode(tmpCode); code.add(tmpCode); } catch (Exception e) { mth.addError("Simple mode code generation failed", e); CodeGenUtils.addError(code, "Simple mode code generation failed", e); dumpInstructions(code); } } private void generateSimpleCode(ICodeWriter code) throws CodegenException { SimpleModeHelper helper = new SimpleModeHelper(mth); List blocks = helper.prepareBlocks(); InsnGen insnGen = new InsnGen(this, true); for (BlockNode block : blocks) { if (block.contains(AFlag.DONT_GENERATE)) { continue; } if (helper.isNeedStartLabel(block)) { code.decIndent(); code.startLine(getLabelName(block)).add(':'); code.incIndent(); } for (InsnNode insn : block.getInstructions()) { if (!insn.contains(AFlag.DONT_GENERATE)) { if (insn.getResult() != null) { CodeVar codeVar = insn.getResult().getSVar().getCodeVar(); if (!codeVar.isDeclared()) { insn.add(AFlag.DECLARE_VAR); codeVar.setDeclared(true); } } InsnCodeOffset.attach(code, insn); insnGen.makeInsn(insn, code); addCatchComment(code, insn, false); CodeGenUtils.addCodeComments(code, mth, insn); } } if (helper.isNeedEndGoto(block)) { code.startLine("goto ").add(getLabelName(block.getSuccessors().get(0))); } } } public void dumpInstructions(ICodeWriter code) { if (mth.checkCommentsLevel(CommentsLevel.ERROR)) { code.startLine("/*"); addFallbackMethodCode(code, COMMENTED_DUMP); code.startLine("*/"); } code.startLine("throw new UnsupportedOperationException(\"Method not decompiled: ") .add(mth.getParentClass().getClassInfo().getAliasFullName()) .add('.') .add(mth.getAlias()) .add('(') .add(Utils.listToString(mth.getMethodInfo().getArgumentsTypes())) .add("):") .add(mth.getMethodInfo().getReturnType().toString()) .add("\");"); } public void addFallbackMethodCode(ICodeWriter code, FallbackOption fallbackOption) { if (fallbackOption == COMMENTED_DUMP && mth.getCommentsLevel() != CommentsLevel.DEBUG) { long insnCountEstimate = mth.getInsnsCount(); if (insnCountEstimate > 200) { code.incIndent(); code.startLine("Method dump skipped, instruction units count: " + insnCountEstimate); if (code.isMetadataSupported()) { code.startLine("To view this dump change 'Code comments level' option to 'DEBUG'"); } else { code.startLine("To view this dump add '--comments-level debug' option"); } code.decIndent(); return; } } if (fallbackOption != FALLBACK_MODE) { List errors = mth.getAll(AType.JADX_ERROR); // preserve error before unload try { // load original instructions mth.unload(); mth.load(); for (IDexTreeVisitor visitor : Jadx.getFallbackPassesList()) { DepthTraversal.visit(visitor, mth); } errors.forEach(err -> mth.addAttr(AType.JADX_ERROR, err)); } catch (Exception e) { LOG.error("Error reload instructions in fallback mode:", e); code.startLine("// Can't load method instructions: " + e.getMessage()); return; } finally { errors.forEach(err -> mth.addAttr(AType.JADX_ERROR, err)); } } InsnNode[] insnArr = mth.getInstructions(); if (insnArr == null) { code.startLine("// Can't load method instructions."); return; } code.incIndent(); if (mth.getThisArg() != null) { code.startLine(nameGen.useArg(mth.getThisArg())).add(" = this;"); } addFallbackInsns(code, mth, insnArr, fallbackOption); code.decIndent(); } public enum FallbackOption { FALLBACK_MODE, BLOCK_DUMP, COMMENTED_DUMP } public static void addFallbackInsns(ICodeWriter code, MethodNode mth, InsnNode[] insnArr, FallbackOption option) { int startIndent = code.getIndent(); MethodGen methodGen = getFallbackMethodGen(mth); InsnGen insnGen = new InsnGen(methodGen, true); InsnNode prevInsn = null; for (InsnNode insn : insnArr) { if (insn == null) { continue; } methodGen.dumpInsn(code, insnGen, option, startIndent, prevInsn, insn); prevInsn = insn; } } private boolean dumpInsn(ICodeWriter code, InsnGen insnGen, FallbackOption option, int startIndent, @Nullable InsnNode prevInsn, InsnNode insn) { if (insn.contains(AType.JADX_ERROR)) { for (JadxError error : insn.getAll(AType.JADX_ERROR)) { code.startLine("// ").add(error.getError()); } return true; } if (option != BLOCK_DUMP && needLabel(insn, prevInsn)) { code.decIndent(); code.startLine(getLabelName(insn.getOffset()) + ':'); code.incIndent(); } if (insn.getType() == InsnType.NOP) { return true; } try { boolean escapeComment = isCommentEscapeNeeded(insn, option); if (escapeComment) { code.decIndent(); code.startLine("*/"); code.startLine("// "); } else { code.startLineWithNum(insn.getSourceLine()); } InsnCodeOffset.attach(code, insn); RegisterArg resArg = insn.getResult(); if (resArg != null) { ArgType varType = resArg.getInitType(); if (varType.isTypeKnown()) { code.add(varType.toString()).add(' '); } } insnGen.makeInsn(insn, code, InsnGen.Flags.INLINE); if (escapeComment) { code.startLine("/*"); code.incIndent(); } addCatchComment(code, insn, true); CodeGenUtils.addCodeComments(code, mth, insn); } catch (Exception e) { LOG.debug("Error generate fallback instruction: ", e.getCause()); code.setIndent(startIndent); code.startLine("// error: " + insn); } return false; } private void addCatchComment(ICodeWriter code, InsnNode insn, boolean raw) { CatchAttr catchAttr = insn.get(AType.EXC_CATCH); if (catchAttr == null) { return; } code.add(" // Catch:"); for (ExceptionHandler handler : catchAttr.getHandlers()) { code.add(' '); classGen.useClass(code, handler.getArgType()); code.add(" -> "); if (raw) { code.add(getLabelName(handler.getHandlerOffset())); } else { code.add(getLabelName(handler.getHandlerBlock())); } } } private static boolean isCommentEscapeNeeded(InsnNode insn, FallbackOption option) { if (option == COMMENTED_DUMP) { if (insn.getType() == InsnType.CONST_STR) { String str = ((ConstStringNode) insn).getString(); return str.contains("*/"); } } return false; } private static boolean needLabel(InsnNode insn, InsnNode prevInsn) { if (insn.contains(AType.EXC_HANDLER)) { return true; } if (insn.contains(AType.JUMP)) { // don't add label for ifs else branch if (prevInsn != null && prevInsn.getType() == InsnType.IF) { List jumps = insn.getAll(AType.JUMP); if (jumps.size() == 1) { JumpInfo jump = jumps.get(0); if (jump.getSrc() == prevInsn.getOffset() && jump.getDest() == insn.getOffset()) { int target = ((IfNode) prevInsn).getTarget(); return insn.getOffset() == target; } } } return true; } return false; } /** * Return fallback variant of method codegen */ public static MethodGen getFallbackMethodGen(MethodNode mth) { ClassGen clsGen = new ClassGen(mth.getParentClass(), null, false, true, true, IntegerFormat.AUTO); return new MethodGen(clsGen, mth); } public static String getLabelName(BlockNode block) { return String.format("L%d", block.getCId()); } public static String getLabelName(IfNode insn) { BlockNode thenBlock = insn.getThenBlock(); if (thenBlock != null) { return getLabelName(thenBlock); } return getLabelName(insn.getTarget()); } public static String getLabelName(int offset) { if (offset < 0) { return String.format("LB_%x", -offset); } return String.format("L%x", offset); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/NameGen.java ================================================ package jadx.core.codegen; import java.util.HashSet; import java.util.List; import java.util.Set; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.nodes.LoopLabelAttr; import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.NamedArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; public class NameGen { private final MethodNode mth; private final boolean fallback; private final Set varNames = new HashSet<>(); public NameGen(MethodNode mth, ClassGen classGen) { this.mth = mth; this.fallback = classGen.isFallbackMode(); NameGen outerNameGen = classGen.getOuterNameGen(); if (outerNameGen != null) { inheritUsedNames(outerNameGen); } addNamesUsedInClass(); } public void inheritUsedNames(NameGen otherNameGen) { varNames.addAll(otherNameGen.varNames); } private void addNamesUsedInClass() { ClassNode parentClass = mth.getParentClass(); for (FieldNode field : parentClass.getFields()) { if (field.isStatic()) { varNames.add(field.getAlias()); } } for (ClassNode innerClass : parentClass.getInnerClasses()) { varNames.add(innerClass.getClassInfo().getAliasShortName()); } // add all root package names to avoid collisions with full class names varNames.addAll(mth.root().getCacheStorage().getRootPkgs()); } public String assignArg(CodeVar var) { if (fallback) { return getFallbackName(var); } if (var.isThis()) { return RegisterArg.THIS_ARG_NAME; } String name = getUniqueVarName(makeArgName(var)); var.setName(name); return name; } public String assignNamedArg(NamedArg arg) { String name = arg.getName(); if (fallback) { return name; } String uniqName = getUniqueVarName(name); arg.setName(uniqName); return uniqName; } public String useArg(RegisterArg arg) { String name = arg.getName(); if (name == null || fallback) { return getFallbackName(arg); } return name; } // TODO: avoid name collision with variables names public String getLoopLabel(LoopLabelAttr attr) { String name = "loop" + attr.getLoop().getId(); varNames.add(name); return name; } private String getUniqueVarName(String name) { String r = name; int i = 2; while (varNames.contains(r)) { r = name + i; i++; } varNames.add(r); return r; } private String makeArgName(CodeVar var) { String name = var.getName(); if (NameMapper.isValidAndPrintable(name)) { return name; } return getFallbackName(var); } private String getFallbackName(CodeVar var) { List ssaVars = var.getSsaVars(); if (ssaVars.isEmpty()) { return "v"; } return getFallbackName(ssaVars.get(0).getAssign()); } private String getFallbackName(RegisterArg arg) { return "r" + arg.getRegNum(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/RegionGen.java ================================================ package jadx.core.codegen; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.CommentsLevel; import jadx.api.ICodeWriter; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.VarNode; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.core.clsp.ClspClass; import jadx.core.codegen.utils.CodeGenUtils; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.DeclareVariablesAttr; import jadx.core.dex.attributes.nodes.ForceReturnAttr; import jadx.core.dex.attributes.nodes.LoopLabelAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.SwitchInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.NamedArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.regions.SwitchRegion.CaseInfo; import jadx.core.dex.regions.SynchronizedRegion; import jadx.core.dex.regions.TryCatchRegion; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.regions.conditions.IfRegion; import jadx.core.dex.regions.loops.ForEachLoop; import jadx.core.dex.regions.loops.ForLoop; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.regions.loops.LoopType; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.utils.BlockUtils; import jadx.core.utils.RegionUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.CodegenException; import jadx.core.utils.exceptions.JadxRuntimeException; public class RegionGen extends InsnGen { private static final Logger LOG = LoggerFactory.getLogger(RegionGen.class); public RegionGen(MethodGen mgen) { super(mgen, false); } public void makeRegion(ICodeWriter code, IContainer cont) throws CodegenException { declareVars(code, cont); cont.generate(this, code); } private void declareVars(ICodeWriter code, IContainer cont) { DeclareVariablesAttr declVars = cont.get(AType.DECLARE_VARIABLES); if (declVars != null) { for (CodeVar v : declVars.getVars()) { code.startLine(); declareVar(code, v); code.add(';'); CodeGenUtils.addCodeComments(code, mth, v.getAnySsaVar().getAssign()); } } } private void makeRegionIndent(ICodeWriter code, IContainer region) throws CodegenException { code.incIndent(); makeRegion(code, region); code.decIndent(); } public void makeSimpleBlock(IBlock block, ICodeWriter code) throws CodegenException { if (block.contains(AFlag.DONT_GENERATE)) { return; } for (InsnNode insn : block.getInstructions()) { if (!insn.contains(AFlag.DONT_GENERATE)) { makeInsn(insn, code); } } ForceReturnAttr retAttr = block.get(AType.FORCE_RETURN); if (retAttr != null) { makeInsn(retAttr.getReturnInsn(), code); } } public void makeIf(IfRegion region, ICodeWriter code, boolean newLine) throws CodegenException { if (newLine) { code.startLineWithNum(region.getSourceLine()); } else { code.attachSourceLine(region.getSourceLine()); } boolean comment = region.contains(AFlag.COMMENT_OUT); if (comment) { code.add("// "); } code.add("if ("); new ConditionGen(this).add(code, region.getCondition()); code.add(") {"); if (code.isMetadataSupported()) { List conditionBlocks = region.getConditionBlocks(); if (!conditionBlocks.isEmpty()) { BlockNode blockNode = conditionBlocks.get(0); InsnNode lastInsn = BlockUtils.getLastInsn(blockNode); InsnCodeOffset.attach(code, lastInsn); CodeGenUtils.addCodeComments(code, mth, lastInsn); } } makeRegionIndent(code, region.getThenRegion()); if (comment) { code.startLine("// }"); } else { code.startLine('}'); } IContainer els = region.getElseRegion(); if (RegionUtils.notEmpty(els)) { code.add(" else "); if (connectElseIf(code, els)) { return; } code.add('{'); makeRegionIndent(code, els); if (comment) { code.startLine("// }"); } else { code.startLine('}'); } } } /** * Connect if-else-if block */ private boolean connectElseIf(ICodeWriter code, IContainer els) throws CodegenException { if (els.contains(AFlag.ELSE_IF_CHAIN)) { IContainer elseBlock = RegionUtils.getSingleSubBlock(els); if (elseBlock instanceof IfRegion) { declareVars(code, elseBlock); makeIf((IfRegion) elseBlock, code, false); return true; } } return false; } public void makeLoop(LoopRegion region, ICodeWriter code) throws CodegenException { code.startLineWithNum(region.getSourceLine()); LoopLabelAttr labelAttr = region.getInfo().getStart().get(AType.LOOP_LABEL); if (labelAttr != null) { code.add(mgen.getNameGen().getLoopLabel(labelAttr)).add(": "); } IfCondition condition = region.getCondition(); if (condition == null) { // infinite loop code.add("while (true) {"); makeRegionIndent(code, region.getBody()); code.startLine('}'); return; } InsnNode condInsn = condition.getFirstInsn(); InsnCodeOffset.attach(code, condInsn); ConditionGen conditionGen = new ConditionGen(this); LoopType type = region.getType(); if (type != null) { if (type instanceof ForLoop) { ForLoop forLoop = (ForLoop) type; code.add("for ("); makeInsn(forLoop.getInitInsn(), code, Flags.INLINE); code.add("; "); conditionGen.add(code, condition); code.add("; "); makeInsn(forLoop.getIncrInsn(), code, Flags.INLINE); code.add(") {"); CodeGenUtils.addCodeComments(code, mth, condInsn); makeRegionIndent(code, region.getBody()); code.startLine('}'); return; } if (type instanceof ForEachLoop) { ForEachLoop forEachLoop = (ForEachLoop) type; code.add("for ("); declareVar(code, forEachLoop.getVarArg()); code.add(" : "); addArg(code, forEachLoop.getIterableArg(), false); code.add(") {"); CodeGenUtils.addCodeComments(code, mth, condInsn); makeRegionIndent(code, region.getBody()); code.startLine('}'); return; } throw new JadxRuntimeException("Unknown loop type: " + type.getClass()); } if (region.isConditionAtEnd()) { code.add("do {"); CodeGenUtils.addCodeComments(code, mth, condInsn); makeRegionIndent(code, region.getBody()); code.startLineWithNum(region.getSourceLine()); code.add("} while ("); conditionGen.add(code, condition); code.add(");"); } else { code.add("while ("); conditionGen.add(code, condition); code.add(") {"); CodeGenUtils.addCodeComments(code, mth, condInsn); makeRegionIndent(code, region.getBody()); code.startLine('}'); } } public void makeSynchronizedRegion(SynchronizedRegion cont, ICodeWriter code) throws CodegenException { code.startLine("synchronized ("); InsnNode monitorEnterInsn = cont.getEnterInsn(); addArg(code, monitorEnterInsn.getArg(0)); code.add(") {"); InsnCodeOffset.attach(code, monitorEnterInsn); CodeGenUtils.addCodeComments(code, mth, monitorEnterInsn); makeRegionIndent(code, cont.getRegion()); code.startLine('}'); } public void makeSwitch(SwitchRegion sw, ICodeWriter code) throws CodegenException { SwitchInsn insn = (SwitchInsn) BlockUtils.getLastInsn(sw.getHeader()); Objects.requireNonNull(insn, "Switch insn not found in header"); InsnArg arg = insn.getArg(0); code.startLine("switch ("); addArg(code, arg, false); code.add(") {"); InsnCodeOffset.attach(code, insn); CodeGenUtils.addCodeComments(code, mth, insn); code.incIndent(); for (CaseInfo caseInfo : sw.getCases()) { List keys = caseInfo.getKeys(); IContainer c = caseInfo.getContainer(); for (Object k : keys) { if (k == SwitchRegion.DEFAULT_CASE_KEY) { code.startLine("default:"); } else { code.startLine("case "); addCaseKey(code, arg, k); code.add(':'); } } makeRegionIndent(code, c); } code.decIndent(); code.startLine('}'); } private void addCaseKey(ICodeWriter code, InsnArg arg, Object k) throws CodegenException { if (k instanceof FieldNode) { FieldNode fld = (FieldNode) k; useField(code, fld.getFieldInfo(), fld); } else if (k instanceof FieldInfo) { useField(code, (FieldInfo) k, null); } else if (k instanceof Integer) { code.add(TypeGen.literalToString((Integer) k, arg.getType(), mth, fallback)); } else if (k instanceof String) { code.add('\"').add((String) k).add('\"'); } else { throw new JadxRuntimeException("Unexpected key in switch: " + (k != null ? k.getClass() : null)); } } private void useField(ICodeWriter code, FieldInfo fldInfo, @Nullable FieldNode fld) throws CodegenException { boolean isEnum; if (fld != null) { isEnum = fld.getParentClass().isEnum(); } else { ClspClass clsDetails = root.getClsp().getClsDetails(fldInfo.getDeclClass().getType()); isEnum = clsDetails != null && clsDetails.hasAccFlag(AccessFlags.ENUM); } if (isEnum) { if (fld != null) { code.attachAnnotation(fld); } code.add(fldInfo.getAlias()); return; } staticField(code, fldInfo); if (fld != null && mth.checkCommentsLevel(CommentsLevel.INFO)) { // print original value, sometimes replaced with incorrect field EncodedValue constVal = fld.get(JadxAttrType.CONSTANT_VALUE); if (constVal != null && constVal.getValue() != null) { code.add(" /* ").add(constVal.getValue().toString()).add(" */"); } } } public void makeTryCatch(TryCatchRegion region, ICodeWriter code) throws CodegenException { code.startLine("try {"); InsnNode insn = BlockUtils.getFirstInsn(Utils.first(region.getTryCatchBlock().getBlocks())); InsnCodeOffset.attach(code, insn); CodeGenUtils.addCodeComments(code, mth, insn); makeRegionIndent(code, region.getTryRegion()); // TODO: move search of 'allHandler' to 'TryCatchRegion' ExceptionHandler allHandler = null; for (Map.Entry entry : region.getCatchRegions().entrySet()) { ExceptionHandler handler = entry.getKey(); if (handler.isCatchAll()) { if (allHandler != null) { LOG.warn("Several 'all' handlers in try/catch block in {}", mth); } allHandler = handler; } else { makeCatchBlock(code, handler); } } if (allHandler != null) { makeCatchBlock(code, allHandler); } IContainer finallyRegion = region.getFinallyRegion(); if (finallyRegion != null) { code.startLine("} finally {"); makeRegionIndent(code, finallyRegion); } code.startLine('}'); } private void makeCatchBlock(ICodeWriter code, ExceptionHandler handler) throws CodegenException { IContainer region = handler.getHandlerRegion(); if (region == null) { return; } code.startLine("} catch ("); if (handler.isCatchAll()) { useClass(code, ArgType.THROWABLE); } else { Iterator it = handler.getCatchTypes().iterator(); if (it.hasNext()) { useClass(code, it.next()); } while (it.hasNext()) { code.add(" | "); useClass(code, it.next()); } } code.add(' '); InsnArg arg = handler.getArg(); if (arg == null) { code.add("unknown"); // throwing exception is too late at this point } else if (arg instanceof RegisterArg) { SSAVar ssaVar = ((RegisterArg) arg).getSVar(); if (code.isMetadataSupported()) { code.attachDefinition(VarNode.get(mth, ssaVar)); } code.add(mgen.getNameGen().assignArg(ssaVar.getCodeVar())); } else if (arg instanceof NamedArg) { code.add(mgen.getNameGen().assignNamedArg((NamedArg) arg)); } else { throw new JadxRuntimeException("Unexpected arg type in catch block: " + arg + ", class: " + arg.getClass().getSimpleName()); } code.add(") {"); InsnCodeOffset.attach(code, handler.getHandlerOffset()); CodeGenUtils.addCodeComments(code, mth, handler.getHandlerBlock()); makeRegionIndent(code, region); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/SimpleModeHelper.java ================================================ package jadx.core.codegen; import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; import java.util.List; import java.util.Objects; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.TargetInsnNode; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.visitors.blocks.BlockProcessor; import jadx.core.dex.visitors.blocks.BlockSplitter; import jadx.core.utils.BlockUtils; public class SimpleModeHelper { private final MethodNode mth; private final BitSet startLabel; private final BitSet endGoto; public SimpleModeHelper(MethodNode mth) { this.mth = mth; this.startLabel = BlockUtils.newBlocksBitSet(mth); this.endGoto = BlockUtils.newBlocksBitSet(mth); } public List prepareBlocks() { removeEmptyBlocks(); List blocksList = getSortedBlocks(); blocksList.removeIf(b -> b.equals(mth.getEnterBlock()) || b.equals(mth.getExitBlock())); unbindExceptionHandlers(); if (blocksList.isEmpty()) { return Collections.emptyList(); } @Nullable BlockNode prev = null; int blocksCount = blocksList.size(); for (int i = 0; i < blocksCount; i++) { BlockNode block = blocksList.get(i); BlockNode nextBlock = i + 1 == blocksCount ? null : blocksList.get(i + 1); List preds = block.getPredecessors(); int predsCount = preds.size(); if (predsCount > 1) { startLabel.set(block.getId()); } else if (predsCount == 1 && prev != null) { if (!prev.equals(preds.get(0))) { if (!block.contains(AFlag.EXC_BOTTOM_SPLITTER)) { startLabel.set(block.getId()); } if (prev.getSuccessors().size() == 1 && !mth.isPreExitBlock(prev)) { endGoto.set(prev.getId()); } } } InsnNode lastInsn = BlockUtils.getLastInsn(block); if (lastInsn instanceof TargetInsnNode) { processTargetInsn(block, lastInsn, nextBlock); } if (block.contains(AType.EXC_HANDLER)) { startLabel.set(block.getId()); } if (nextBlock == null && !mth.isPreExitBlock(block)) { endGoto.set(block.getId()); } prev = block; } if (mth.isVoidReturn()) { int last = blocksList.size() - 1; if (blocksList.get(last).contains(AFlag.RETURN)) { // remove trailing return blocksList.remove(last); } } return blocksList; } private void removeEmptyBlocks() { for (BlockNode block : mth.getBasicBlocks()) { if (block.getInstructions().isEmpty() && block.getPredecessors().size() > 0 && block.getSuccessors().size() == 1) { BlockNode successor = block.getSuccessors().get(0); List predecessors = block.getPredecessors(); BlockSplitter.removeConnection(block, successor); if (predecessors.size() == 1) { BlockSplitter.replaceConnection(predecessors.get(0), block, successor); } else { for (BlockNode pred : new ArrayList<>(predecessors)) { BlockSplitter.replaceConnection(pred, block, successor); } } block.add(AFlag.REMOVE); } } BlockProcessor.removeMarkedBlocks(mth); } private void unbindExceptionHandlers() { if (mth.isNoExceptionHandlers()) { return; } for (ExceptionHandler handler : mth.getExceptionHandlers()) { BlockNode handlerBlock = handler.getHandlerBlock(); if (handlerBlock != null) { BlockSplitter.removePredecessors(handlerBlock); } } } private void processTargetInsn(BlockNode block, InsnNode lastInsn, @Nullable BlockNode next) { if (lastInsn instanceof IfNode) { IfNode ifInsn = (IfNode) lastInsn; BlockNode thenBlock = ifInsn.getThenBlock(); if (Objects.equals(next, thenBlock)) { ifInsn.invertCondition(); startLabel.set(ifInsn.getThenBlock().getId()); } else { startLabel.set(thenBlock.getId()); } ifInsn.normalize(); } else { for (BlockNode successor : block.getSuccessors()) { startLabel.set(successor.getId()); } } } public boolean isNeedStartLabel(BlockNode block) { return startLabel.get(block.getId()); } public boolean isNeedEndGoto(BlockNode block) { return endGoto.get(block.getId()); } // DFS sort blocks to reduce goto count private List getSortedBlocks() { List list = new ArrayList<>(mth.getBasicBlocks().size()); BlockUtils.visitDFS(mth, list::add); return list; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/TypeGen.java ================================================ package jadx.core.codegen; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.nodes.IDexNode; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; public class TypeGen { private static final Logger LOG = LoggerFactory.getLogger(TypeGen.class); private TypeGen() { } public static String signature(ArgType type) { PrimitiveType stype = type.getPrimitiveType(); if (stype == PrimitiveType.OBJECT) { return Utils.makeQualifiedObjectName(type.getObject()); } if (stype == PrimitiveType.ARRAY) { return '[' + signature(type.getArrayElement()); } return stype.getShortName(); } /** * Convert literal arg to string (preferred method) */ public static String literalToString(LiteralArg arg, IDexNode dexNode, boolean fallback) { return literalToString(arg.getLiteral(), arg.getType(), dexNode.root().getStringUtils(), fallback, arg.contains(AFlag.EXPLICIT_PRIMITIVE_TYPE)); } /** * Convert literal value to string according to value type * * @throws JadxRuntimeException for incorrect type or literal value */ public static String literalToString(long lit, ArgType type, IDexNode dexNode, boolean fallback) { return literalToString(lit, type, dexNode.root().getStringUtils(), fallback, false); } public static String literalToString(long lit, ArgType type, StringUtils stringUtils, boolean fallback, boolean cast) { if (type == null || !type.isTypeKnown()) { String n = Long.toString(lit); if (fallback && Math.abs(lit) > 100) { StringBuilder sb = new StringBuilder(); sb.append(n).append("(0x").append(Long.toHexString(lit)); if (type == null || type.contains(PrimitiveType.FLOAT)) { sb.append(", float:").append(Float.intBitsToFloat((int) lit)); } if (type == null || type.contains(PrimitiveType.DOUBLE)) { sb.append(", double:").append(Double.longBitsToDouble(lit)); } sb.append(')'); return sb.toString(); } return n; } switch (type.getPrimitiveType()) { case BOOLEAN: return lit == 0 ? "false" : "true"; case CHAR: return stringUtils.unescapeChar((char) lit, cast); case BYTE: return stringUtils.formatByte(lit, cast); case SHORT: return stringUtils.formatShort(lit, cast); case INT: return stringUtils.formatInteger(lit, cast); case LONG: return stringUtils.formatLong(lit, cast); case FLOAT: return StringUtils.formatFloat(Float.intBitsToFloat((int) lit)); case DOUBLE: return StringUtils.formatDouble(Double.longBitsToDouble(lit)); case OBJECT: case ARRAY: if (lit != 0) { LOG.warn("Wrong object literal: {} for type: {}", lit, type); return Long.toString(lit); } return "null"; default: throw new JadxRuntimeException("Unknown type in literalToString: " + type); } } @Nullable public static String literalToRawString(LiteralArg arg) { ArgType type = arg.getType(); if (type == null) { return null; } long lit = arg.getLiteral(); switch (type.getPrimitiveType()) { case BOOLEAN: return lit == 0 ? "false" : "true"; case CHAR: return String.valueOf((char) lit); case BYTE: case SHORT: case INT: case LONG: return Long.toString(lit); case FLOAT: return Float.toString(Float.intBitsToFloat((int) lit)); case DOUBLE: return Double.toString(Double.longBitsToDouble(lit)); case OBJECT: case ARRAY: if (lit != 0) { LOG.warn("Wrong object literal: {} for type: {}", lit, type); return Long.toString(lit); } return "null"; default: return null; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java ================================================ package jadx.core.codegen.json; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.SimpleCodeWriter; import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.core.codegen.ClassGen; import jadx.core.codegen.MethodGen; import jadx.core.codegen.json.cls.JsonClass; import jadx.core.codegen.json.cls.JsonCodeLine; import jadx.core.codegen.json.cls.JsonField; import jadx.core.codegen.json.cls.JsonMethod; import jadx.core.codegen.utils.CodeGenUtils; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.GsonUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; public class JsonCodeGen { private static final Gson GSON = GsonUtils.defaultGsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES) .disableHtmlEscaping() .create(); private final ClassNode cls; private final JadxArgs args; private final RootNode root; public JsonCodeGen(ClassNode cls) { this.cls = cls; this.root = cls.root(); this.args = root.getArgs(); } public String process() { JsonClass jsonCls = processCls(cls, null); return GSON.toJson(jsonCls); } private JsonClass processCls(ClassNode cls, @Nullable ClassGen parentCodeGen) { ClassGen classGen; if (parentCodeGen == null) { classGen = new ClassGen(cls, args); } else { classGen = new ClassGen(cls, parentCodeGen); } ClassInfo classInfo = cls.getClassInfo(); JsonClass jsonCls = new JsonClass(); jsonCls.setPkg(classInfo.getAliasPkg()); jsonCls.setDex(cls.getInputFileName()); jsonCls.setName(classInfo.getFullName()); if (classInfo.hasAlias()) { jsonCls.setAlias(classInfo.getAliasFullName()); } jsonCls.setType(getClassTypeStr(cls)); jsonCls.setAccessFlags(cls.getAccessFlags().rawValue()); ArgType superClass = cls.getSuperClass(); if (superClass != null && !superClass.equals(ArgType.OBJECT) && !cls.contains(AFlag.REMOVE_SUPER_CLASS)) { jsonCls.setSuperClass(getTypeAlias(classGen, superClass)); } if (!cls.getInterfaces().isEmpty()) { jsonCls.setInterfaces(Utils.collectionMap(cls.getInterfaces(), clsType -> getTypeAlias(classGen, clsType))); } ICodeWriter cw = new SimpleCodeWriter(args); CodeGenUtils.addErrorsAndComments(cw, cls); classGen.addClassDeclaration(cw); jsonCls.setDeclaration(cw.getCodeStr()); addFields(cls, jsonCls, classGen); addMethods(cls, jsonCls, classGen); addInnerClasses(cls, jsonCls, classGen); if (!cls.getClassInfo().isInner()) { List imports = Utils.collectionMap(classGen.getImports(), ClassInfo::getAliasFullName); Collections.sort(imports); jsonCls.setImports(imports); } return jsonCls; } private void addInnerClasses(ClassNode cls, JsonClass jsonCls, ClassGen classGen) { List innerClasses = cls.getInnerClasses(); if (innerClasses.isEmpty()) { return; } jsonCls.setInnerClasses(new ArrayList<>(innerClasses.size())); for (ClassNode innerCls : innerClasses) { if (innerCls.contains(AFlag.DONT_GENERATE)) { continue; } JsonClass innerJsonCls = processCls(innerCls, classGen); jsonCls.getInnerClasses().add(innerJsonCls); } } private void addFields(ClassNode cls, JsonClass jsonCls, ClassGen classGen) { jsonCls.setFields(new ArrayList<>()); for (FieldNode field : cls.getFields()) { if (field.contains(AFlag.DONT_GENERATE)) { continue; } JsonField jsonField = new JsonField(); jsonField.setName(field.getName()); if (field.getFieldInfo().hasAlias()) { jsonField.setAlias(field.getAlias()); } ICodeWriter cw = new SimpleCodeWriter(args); classGen.addField(cw, field); jsonField.setDeclaration(cw.getCodeStr()); jsonField.setAccessFlags(field.getAccessFlags().rawValue()); jsonCls.getFields().add(jsonField); } } private void addMethods(ClassNode cls, JsonClass jsonCls, ClassGen classGen) { jsonCls.setMethods(new ArrayList<>()); for (MethodNode mth : cls.getMethods()) { if (mth.contains(AFlag.DONT_GENERATE)) { continue; } JsonMethod jsonMth = new JsonMethod(); jsonMth.setName(mth.getName()); if (mth.getMethodInfo().hasAlias()) { jsonMth.setAlias(mth.getAlias()); } jsonMth.setSignature(mth.getMethodInfo().getShortId()); jsonMth.setReturnType(getTypeAlias(classGen, mth.getReturnType())); jsonMth.setArguments(Utils.collectionMap(mth.getMethodInfo().getArgumentsTypes(), clsType -> getTypeAlias(classGen, clsType))); MethodGen mthGen = new MethodGen(classGen, mth); ICodeWriter cw = new AnnotatedCodeWriter(args); mthGen.addDefinition(cw); jsonMth.setDeclaration(cw.getCodeStr()); jsonMth.setAccessFlags(mth.getAccessFlags().rawValue()); jsonMth.setLines(fillMthCode(mth, mthGen)); jsonMth.setOffset("0x" + Long.toHexString(mth.getMethodCodeOffset())); jsonCls.getMethods().add(jsonMth); } } private List fillMthCode(MethodNode mth, MethodGen mthGen) { if (mth.isNoCode()) { return Collections.emptyList(); } ICodeWriter cw = mth.root().makeCodeWriter(); try { mthGen.addInstructions(cw); } catch (Exception e) { throw new JadxRuntimeException("Method generation error", e); } ICodeInfo code = cw.finish(); String codeStr = code.getCodeStr(); if (codeStr.isEmpty()) { return Collections.emptyList(); } String[] lines = codeStr.split(args.getCodeNewLineStr()); Map lineMapping = code.getCodeMetadata().getLineMapping(); ICodeMetadata metadata = code.getCodeMetadata(); long mthCodeOffset = mth.getMethodCodeOffset() + 16; int linesCount = lines.length; List codeLines = new ArrayList<>(linesCount); int lineStartPos = 0; int newLineLen = args.getCodeNewLineStr().length(); for (int i = 0; i < linesCount; i++) { String codeLine = lines[i]; int line = i + 2; JsonCodeLine jsonCodeLine = new JsonCodeLine(); jsonCodeLine.setCode(codeLine); jsonCodeLine.setSourceLine(lineMapping.get(line)); Object obj = metadata.getAt(lineStartPos); if (obj instanceof InsnCodeOffset) { long offset = ((InsnCodeOffset) obj).getOffset(); jsonCodeLine.setOffset("0x" + Long.toHexString(mthCodeOffset + offset * 2)); } codeLines.add(jsonCodeLine); lineStartPos += codeLine.length() + newLineLen; } return codeLines; } private String getTypeAlias(ClassGen classGen, ArgType clsType) { ICodeWriter code = new SimpleCodeWriter(args); classGen.useType(code, clsType); return code.getCodeStr(); } private String getClassTypeStr(ClassNode cls) { if (cls.isEnum()) { return "enum"; } if (cls.getAccessFlags().isInterface()) { return "interface"; } return "class"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/json/JsonMappingGen.java ================================================ package jadx.core.codegen.json; import java.io.File; import java.io.FileWriter; import java.io.Writer; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import jadx.api.JadxArgs; import jadx.core.codegen.json.mapping.JsonClsMapping; import jadx.core.codegen.json.mapping.JsonFieldMapping; import jadx.core.codegen.json.mapping.JsonMapping; import jadx.core.codegen.json.mapping.JsonMthMapping; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.GsonUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; public class JsonMappingGen { private static final Logger LOG = LoggerFactory.getLogger(JsonMappingGen.class); private static final Gson GSON = GsonUtils.defaultGsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES) .disableHtmlEscaping() .create(); public static void dump(RootNode root) { JsonMapping mapping = new JsonMapping(); fillMapping(mapping, root); JadxArgs args = root.getArgs(); File outDirSrc = args.getOutDirSrc().getAbsoluteFile(); File mappingFile = new File(outDirSrc, "mapping.json"); FileUtils.makeDirsForFile(mappingFile); try (Writer writer = new FileWriter(mappingFile)) { GSON.toJson(mapping, writer); LOG.info("Save mappings to {}", mappingFile.getAbsolutePath()); } catch (Exception e) { throw new JadxRuntimeException("Failed to save mapping json", e); } } private static void fillMapping(JsonMapping mapping, RootNode root) { List classes = root.getClasses(true); mapping.setClasses(new ArrayList<>(classes.size())); for (ClassNode cls : classes) { ClassInfo classInfo = cls.getClassInfo(); JsonClsMapping jsonCls = new JsonClsMapping(); jsonCls.setName(classInfo.getRawName()); jsonCls.setAlias(classInfo.getAliasFullName()); jsonCls.setInner(classInfo.isInner()); jsonCls.setJson(cls.getTopParentClass().getClassInfo().getAliasFullPath() + ".json"); if (classInfo.isInner()) { jsonCls.setTopClass(cls.getTopParentClass().getClassInfo().getFullName()); } addFields(cls, jsonCls); addMethods(cls, jsonCls); mapping.getClasses().add(jsonCls); } } private static void addMethods(ClassNode cls, JsonClsMapping jsonCls) { List methods = cls.getMethods(); if (methods.isEmpty()) { return; } jsonCls.setMethods(new ArrayList<>(methods.size())); for (MethodNode method : methods) { JsonMthMapping jsonMethod = new JsonMthMapping(); MethodInfo methodInfo = method.getMethodInfo(); jsonMethod.setSignature(methodInfo.getShortId()); jsonMethod.setName(methodInfo.getName()); jsonMethod.setAlias(methodInfo.getAlias()); jsonMethod.setOffset("0x" + Long.toHexString(method.getMethodCodeOffset())); jsonCls.getMethods().add(jsonMethod); } } private static void addFields(ClassNode cls, JsonClsMapping jsonCls) { List fields = cls.getFields(); if (fields.isEmpty()) { return; } jsonCls.setFields(new ArrayList<>(fields.size())); for (FieldNode field : fields) { JsonFieldMapping jsonField = new JsonFieldMapping(); jsonField.setName(field.getName()); jsonField.setAlias(field.getAlias()); jsonCls.getFields().add(jsonField); } } private JsonMappingGen() { } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonClass.java ================================================ package jadx.core.codegen.json.cls; import java.util.List; import com.google.gson.annotations.SerializedName; public class JsonClass extends JsonNode { @SerializedName("package") private String pkg; private String type; // class, interface, enum @SerializedName("extends") private String superClass; @SerializedName("implements") private List interfaces; private String dex; private List fields; private List methods; private List innerClasses; private List imports; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getSuperClass() { return superClass; } public void setSuperClass(String superClass) { this.superClass = superClass; } public List getInterfaces() { return interfaces; } public void setInterfaces(List interfaces) { this.interfaces = interfaces; } public List getFields() { return fields; } public void setFields(List fields) { this.fields = fields; } public List getMethods() { return methods; } public void setMethods(List methods) { this.methods = methods; } public List getInnerClasses() { return innerClasses; } public void setInnerClasses(List innerClasses) { this.innerClasses = innerClasses; } public String getPkg() { return pkg; } public void setPkg(String pkg) { this.pkg = pkg; } public String getDex() { return dex; } public void setDex(String dex) { this.dex = dex; } public List getImports() { return imports; } public void setImports(List imports) { this.imports = imports; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonCodeLine.java ================================================ package jadx.core.codegen.json.cls; import org.jetbrains.annotations.Nullable; public class JsonCodeLine { private String code; private String offset; private Integer sourceLine; public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getOffset() { return offset; } public void setOffset(String offset) { this.offset = offset; } public Integer getSourceLine() { return sourceLine; } public void setSourceLine(@Nullable Integer sourceLine) { this.sourceLine = sourceLine; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonField.java ================================================ package jadx.core.codegen.json.cls; public class JsonField extends JsonNode { String type; } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonMethod.java ================================================ package jadx.core.codegen.json.cls; import java.util.List; public class JsonMethod extends JsonNode { private String signature; private String returnType; private List arguments; private List lines; private String offset; public String getSignature() { return signature; } public void setSignature(String signature) { this.signature = signature; } public String getReturnType() { return returnType; } public void setReturnType(String returnType) { this.returnType = returnType; } public List getArguments() { return arguments; } public void setArguments(List arguments) { this.arguments = arguments; } public List getLines() { return lines; } public void setLines(List lines) { this.lines = lines; } public String getOffset() { return offset; } public void setOffset(String offset) { this.offset = offset; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonNode.java ================================================ package jadx.core.codegen.json.cls; public class JsonNode { private String name; private String alias; private String declaration; private int accessFlags; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAlias() { return alias; } public void setAlias(String alias) { this.alias = alias; } public String getDeclaration() { return declaration; } public void setDeclaration(String declaration) { this.declaration = declaration; } public int getAccessFlags() { return accessFlags; } public void setAccessFlags(int accessFlags) { this.accessFlags = accessFlags; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonClsMapping.java ================================================ package jadx.core.codegen.json.mapping; import java.util.List; public class JsonClsMapping { private String name; private String alias; private String json; private boolean inner; private String topClass; private List fields; private List methods; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAlias() { return alias; } public void setAlias(String alias) { this.alias = alias; } public String getJson() { return json; } public void setJson(String json) { this.json = json; } public boolean isInner() { return inner; } public void setInner(boolean inner) { this.inner = inner; } public String getTopClass() { return topClass; } public void setTopClass(String topClass) { this.topClass = topClass; } public List getFields() { return fields; } public void setFields(List fields) { this.fields = fields; } public List getMethods() { return methods; } public void setMethods(List methods) { this.methods = methods; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonFieldMapping.java ================================================ package jadx.core.codegen.json.mapping; public class JsonFieldMapping { private String name; private String alias; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAlias() { return alias; } public void setAlias(String alias) { this.alias = alias; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonMapping.java ================================================ package jadx.core.codegen.json.mapping; import java.util.List; public class JsonMapping { private List classes; public List getClasses() { return classes; } public void setClasses(List classes) { this.classes = classes; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonMthMapping.java ================================================ package jadx.core.codegen.json.mapping; public class JsonMthMapping { private String signature; private String name; private String alias; private String offset; public String getSignature() { return signature; } public void setSignature(String signature) { this.signature = signature; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAlias() { return alias; } public void setAlias(String alias) { this.alias = alias; } public String getOffset() { return offset; } public void setOffset(String offset) { this.offset = offset; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/utils/CodeComment.java ================================================ package jadx.core.codegen.utils; import jadx.api.data.CommentStyle; import jadx.api.data.ICodeComment; public class CodeComment { private final String comment; private final CommentStyle style; public CodeComment(String comment, CommentStyle style) { this.comment = comment; this.style = style; } public CodeComment(ICodeComment comment) { this(comment.getComment(), comment.getStyle()); } public String getComment() { return comment; } public CommentStyle getStyle() { return style; } @Override public String toString() { return "CodeComment{" + style + ": '" + comment + "'}"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/codegen/utils/CodeGenUtils.java ================================================ package jadx.core.codegen.utils; import java.util.List; import java.util.function.BiConsumer; import java.util.regex.Pattern; import org.jetbrains.annotations.Nullable; import jadx.api.CommentsLevel; import jadx.api.ICodeWriter; import jadx.api.data.CommentStyle; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.SourceFileAttr; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.attributes.nodes.JadxCommentsAttr; import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.attributes.nodes.NotificationAttrNode; import jadx.core.dex.attributes.nodes.RenameReasonAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ICodeNode; import jadx.core.utils.Utils; public class CodeGenUtils { public static void addErrorsAndComments(ICodeWriter code, NotificationAttrNode node) { addComments(code, node); addErrors(code, node); } public static void addErrors(ICodeWriter code, NotificationAttrNode node) { if (!node.checkCommentsLevel(CommentsLevel.ERROR)) { return; } List errors = node.getAll(AType.JADX_ERROR); if (!errors.isEmpty()) { errors.stream().distinct().sorted().forEach(err -> { addError(code, err.getError(), err.getCause()); }); } } public static void addError(ICodeWriter code, String errMsg, Throwable cause) { code.startLine("/* JADX ERROR: ").add(errMsg); if (cause != null) { code.incIndent(); Utils.appendStackTrace(code, cause); code.decIndent(); } code.add("*/"); } public static void addComments(ICodeWriter code, NotificationAttrNode node) { JadxCommentsAttr commentsAttr = node.get(AType.JADX_COMMENTS); if (commentsAttr != null) { commentsAttr.formatAndFilter(node.getCommentsLevel()) .forEach(comment -> code.startLine("/* ").addMultiLine(comment).add(" */")); } addCodeComments(code, node, node); } public static void addCodeComments(ICodeWriter code, NotificationAttrNode parent, @Nullable IAttributeNode node) { if (node == null) { return; } if (parent.checkCommentsLevel(CommentsLevel.USER_ONLY)) { addCodeComments(code, node); } } private static void addCodeComments(ICodeWriter code, @Nullable IAttributeNode node) { if (node == null) { return; } boolean startNewLine = node instanceof ICodeNode; // add on same line for instructions for (CodeComment comment : node.getAll(AType.CODE_COMMENTS)) { addCodeComment(code, comment, startNewLine); } } private static void addCodeComment(ICodeWriter code, CodeComment comment, boolean startNewLine) { if (startNewLine) { code.startLine(); } else { code.add(' '); } addCommentWithStyle(code, comment.getStyle(), comment.getComment()); } public static void addJadxNodeComment(ICodeWriter code, NotificationAttrNode node, CommentsLevel level, BiConsumer commentFunc) { if (node.checkCommentsLevel(level)) { code.startLine(); addCommentWithStyle(code, CommentStyle.BLOCK_CONDENSED, (commentCode, newLinePrefix) -> { commentCode.add("JADX ").add(level.name()).add(": "); commentFunc.accept(commentCode, newLinePrefix); }); } } public static void addJadxComment(ICodeWriter code, CommentsLevel level, String commentStr) { code.startLine(); addCommentWithStyle(code, CommentStyle.BLOCK_CONDENSED, "JADX " + level.name() + ": " + commentStr); } private static void addCommentWithStyle(ICodeWriter code, CommentStyle style, String commentStr) { appendMultiLineString(code, "", style.getStart()); appendMultiLineString(code, style.getOnNewLine(), commentStr); appendMultiLineString(code, "", style.getEnd()); } /** * Insert comment with function, use second arg as new line prefix */ private static void addCommentWithStyle(ICodeWriter code, CommentStyle style, BiConsumer commentFunc) { appendMultiLineString(code, "", style.getStart()); commentFunc.accept(code, style.getOnNewLine()); appendMultiLineString(code, "", style.getEnd()); } private static final Pattern NEW_LINE_PATTERN = Pattern.compile("\\R"); private static void appendMultiLineString(ICodeWriter code, String onNewLine, String str) { String[] lines = NEW_LINE_PATTERN.split(str); int linesCount = lines.length; if (linesCount == 0) { return; } code.add(lines[0]); for (int i = 1; i < linesCount; i++) { code.startLine(onNewLine); code.add(lines[i]); } } public static void addClassRenamedComment(ICodeWriter code, ClassNode cls) { ClassInfo classInfo = cls.getClassInfo(); if (classInfo.hasAlias()) { addRenamedComment(code, cls, classInfo.getType().getObject()); } } public static void addRenamedComment(ICodeWriter code, NotificationAttrNode node, String origName) { addJadxNodeComment(code, node, CommentsLevel.INFO, (commentCode, newLinePrefix) -> { commentCode.add("renamed from: ").add(origName); RenameReasonAttr renameReasonAttr = node.get(AType.RENAME_REASON); if (renameReasonAttr != null) { commentCode.add(", reason: ").add(renameReasonAttr.getDescription()); } }); } public static void addSourceFileInfo(ICodeWriter code, ClassNode node) { if (!node.checkCommentsLevel(CommentsLevel.INFO)) { return; } SourceFileAttr sourceFileAttr = node.get(JadxAttrType.SOURCE_FILE); if (sourceFileAttr != null) { String fileName = sourceFileAttr.getFileName(); String topClsName = node.getTopParentClass().getClassInfo().getShortName(); if (topClsName.contains(fileName)) { // ignore similar name return; } addJadxComment(code, CommentsLevel.INFO, "compiled from: " + fileName); } } public static void addInputFileInfo(ICodeWriter code, ClassNode cls) { if (cls.checkCommentsLevel(CommentsLevel.INFO) && cls.getClsData() != null) { String inputFileName = cls.getClsData().getInputFileName(); if (inputFileName != null) { ClassNode declCls = cls.getDeclaringClass(); if (declCls != null && declCls.getClsData() != null && inputFileName.equals(declCls.getClsData().getInputFileName())) { // don't add same comment for inner classes return; } addJadxComment(code, CommentsLevel.INFO, "loaded from: " + inputFileName); } } } public static CodeVar getCodeVar(RegisterArg arg) { SSAVar svar = arg.getSVar(); if (svar != null) { return svar.getCodeVar(); } return null; } private CodeGenUtils() { } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/DeobfAliasProvider.java ================================================ package jadx.core.deobf; import jadx.api.deobf.IAliasProvider; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.StringUtils; public class DeobfAliasProvider implements IAliasProvider { private int pkgIndex = 0; private int clsIndex = 0; private int fldIndex = 0; private int mthIndex = 0; private int maxLength; @Override public void init(RootNode root) { this.maxLength = root.getArgs().getDeobfuscationMaxLength(); } @Override public void initIndexes(int pkg, int cls, int fld, int mth) { pkgIndex = pkg; clsIndex = cls; fldIndex = fld; mthIndex = mth; } @Override public String forPackage(PackageNode pkg) { return String.format("p%03d%s", pkgIndex++, prepareNamePart(pkg.getPkgInfo().getName())); } @Override public String forClass(ClassNode cls) { String prefix = makeClsPrefix(cls); return String.format("%sC%04d%s", prefix, clsIndex++, prepareNamePart(cls.getName())); } @Override public String forField(FieldNode fld) { return String.format("f%d%s", fldIndex++, prepareNamePart(fld.getName())); } @Override public String forMethod(MethodNode mth) { String prefix = mth.contains(AType.METHOD_OVERRIDE) ? "mo" : "m"; return String.format("%s%d%s", prefix, mthIndex++, prepareNamePart(mth.getName())); } private String prepareNamePart(String name) { if (name.length() > maxLength) { return 'x' + Integer.toHexString(name.hashCode()); } return NameMapper.removeInvalidCharsMiddle(name); } /** * Generate a prefix for a class name that bases on certain class properties, certain * extended superclasses or implemented interfaces. */ private String makeClsPrefix(ClassNode cls) { if (cls.isEnum()) { return "Enum"; } StringBuilder result = new StringBuilder(); if (cls.getAccessFlags().isInterface()) { result.append("Interface"); } else if (cls.getAccessFlags().isAbstract()) { result.append("Abstract"); } result.append(getBaseName(cls)); return result.toString(); } /** * Process current class and all super classes to get meaningful parent name */ private static String getBaseName(ClassNode cls) { ClassNode currentCls = cls; while (currentCls != null) { ArgType superCls = currentCls.getSuperClass(); if (superCls != null) { String superClsName = superCls.getObject(); if (superClsName.startsWith("android.app.") // e.g. Activity or Fragment || superClsName.startsWith("android.os.") // e.g. AsyncTask ) { return getClsName(superClsName); } } for (ArgType interfaceType : cls.getInterfaces()) { String name = interfaceType.getObject(); if (name.equals("java.lang.Runnable")) { return "Runnable"; } if (name.startsWith("java.util.concurrent.") // e.g. Callable || name.startsWith("android.view.") // e.g. View.OnClickListener || name.startsWith("android.content.") // e.g. DialogInterface.OnClickListener ) { return getClsName(name); } } if (superCls == null) { break; } currentCls = cls.root().resolveClass(superCls); } return ""; } private static String getClsName(String name) { int pgkEnd = name.lastIndexOf('.'); String clsName = name.substring(pgkEnd + 1); return StringUtils.removeChar(clsName, '$'); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/DeobfPresets.java ================================================ package jadx.core.deobf; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.api.args.GeneratedRenamesMappingFileMode; import jadx.api.deobf.IAliasProvider; import jadx.api.deobf.impl.AlwaysRename; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.files.FileUtils; import static java.nio.charset.StandardCharsets.UTF_8; public class DeobfPresets { private static final Logger LOG = LoggerFactory.getLogger(DeobfPresets.class); private static final Charset MAP_FILE_CHARSET = UTF_8; private final Path deobfMapFile; private final Map pkgPresetMap = new HashMap<>(); private final Map clsPresetMap = new HashMap<>(); private final Map fldPresetMap = new HashMap<>(); private final Map mthPresetMap = new HashMap<>(); public static DeobfPresets build(RootNode root) { Path deobfMapPath = getPathDeobfMapPath(root); if (root.getArgs().getGeneratedRenamesMappingFileMode() != GeneratedRenamesMappingFileMode.IGNORE) { LOG.debug("Deobfuscation map file set to: {}", deobfMapPath); } return new DeobfPresets(deobfMapPath); } private static Path getPathDeobfMapPath(RootNode root) { JadxArgs jadxArgs = root.getArgs(); File deobfMapFile = jadxArgs.getGeneratedRenamesMappingFile(); if (deobfMapFile != null) { return deobfMapFile.toPath(); } Path inputFilePath = jadxArgs.getInputFiles().get(0).toPath().toAbsolutePath(); String baseName = FileUtils.getPathBaseName(inputFilePath); return inputFilePath.getParent().resolve(baseName + ".jobf"); } private DeobfPresets(Path deobfMapFile) { this.deobfMapFile = deobfMapFile; } /** * Loads deobfuscator presets */ public boolean load() { if (!Files.exists(deobfMapFile)) { return false; } LOG.info("Loading obfuscation map from: {}", deobfMapFile.toAbsolutePath()); try { List lines = Files.readAllLines(deobfMapFile, MAP_FILE_CHARSET); for (String l : lines) { l = l.trim(); if (l.isEmpty() || l.startsWith("#")) { continue; } String[] va = splitAndTrim(l); if (va.length != 2) { continue; } String origName = va[0]; String alias = va[1]; switch (l.charAt(0)) { case 'p': pkgPresetMap.put(origName, alias); break; case 'c': clsPresetMap.put(origName, alias); break; case 'f': fldPresetMap.put(origName, alias); break; case 'm': mthPresetMap.put(origName, alias); break; case 'v': // deprecated break; } } return true; } catch (Exception e) { LOG.error("Failed to load deobfuscation map file '{}'", deobfMapFile.toAbsolutePath(), e); return false; } } private static String[] splitAndTrim(String str) { String[] v = str.substring(2).split("="); for (int i = 0; i < v.length; i++) { v[i] = v[i].trim(); } return v; } public void save() throws IOException { List list = new ArrayList<>(); for (Map.Entry pkgEntry : pkgPresetMap.entrySet()) { list.add(String.format("p %s = %s", pkgEntry.getKey(), pkgEntry.getValue())); } for (Map.Entry clsEntry : clsPresetMap.entrySet()) { list.add(String.format("c %s = %s", clsEntry.getKey(), clsEntry.getValue())); } for (Map.Entry fldEntry : fldPresetMap.entrySet()) { list.add(String.format("f %s = %s", fldEntry.getKey(), fldEntry.getValue())); } for (Map.Entry mthEntry : mthPresetMap.entrySet()) { list.add(String.format("m %s = %s", mthEntry.getKey(), mthEntry.getValue())); } Collections.sort(list); if (list.isEmpty()) { if (LOG.isDebugEnabled()) { LOG.debug("Deobfuscation map is empty, not saving it"); } return; } Files.write(deobfMapFile, list, MAP_FILE_CHARSET, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); LOG.info("Deobfuscation map file saved as: {}", deobfMapFile); } public void fill(RootNode root) { for (PackageNode pkg : root.getPackages()) { if (pkg.isLeaf()) { // ignore middle packages if (pkg.hasParentAlias()) { pkgPresetMap.put(pkg.getPkgInfo().getFullName(), pkg.getAliasPkgInfo().getFullName()); } else if (pkg.hasAlias()) { pkgPresetMap.put(pkg.getPkgInfo().getFullName(), pkg.getAliasPkgInfo().getName()); } } } for (ClassNode cls : root.getClasses()) { ClassInfo classInfo = cls.getClassInfo(); if (classInfo.hasAlias()) { clsPresetMap.put(classInfo.makeRawFullName(), classInfo.getAliasShortName()); } for (FieldNode fld : cls.getFields()) { FieldInfo fieldInfo = fld.getFieldInfo(); if (fieldInfo.hasAlias()) { fldPresetMap.put(fieldInfo.getRawFullId(), fld.getAlias()); } } for (MethodNode mth : cls.getMethods()) { MethodInfo methodInfo = mth.getMethodInfo(); if (methodInfo.hasAlias()) { mthPresetMap.put(methodInfo.getRawFullId(), methodInfo.getAlias()); } } } } public void apply(RootNode root) { DeobfuscatorVisitor.process(root, AlwaysRename.INSTANCE, new IAliasProvider() { @Override public String forPackage(PackageNode pkg) { return pkgPresetMap.get(pkg.getPkgInfo().getFullName()); } @Override public String forClass(ClassNode cls) { return getForCls(cls.getClassInfo()); } @Override public String forField(FieldNode fld) { return getForFld(fld.getFieldInfo()); } @Override public String forMethod(MethodNode mth) { return getForMth(mth.getMethodInfo()); } }); } public void initIndexes(IAliasProvider aliasProvider) { aliasProvider.initIndexes(pkgPresetMap.size(), clsPresetMap.size(), fldPresetMap.size(), mthPresetMap.size()); } public String getForCls(ClassInfo cls) { if (clsPresetMap.isEmpty()) { return null; } return clsPresetMap.get(cls.makeRawFullName()); } public String getForFld(FieldInfo fld) { if (fldPresetMap.isEmpty()) { return null; } return fldPresetMap.get(fld.getRawFullId()); } public String getForMth(MethodInfo mth) { if (mthPresetMap.isEmpty()) { return null; } return mthPresetMap.get(mth.getRawFullId()); } public void clear() { pkgPresetMap.clear(); clsPresetMap.clear(); fldPresetMap.clear(); mthPresetMap.clear(); } public Path getDeobfMapFile() { return deobfMapFile; } public Map getPkgPresetMap() { return pkgPresetMap; } public Map getClsPresetMap() { return clsPresetMap; } public Map getFldPresetMap() { return fldPresetMap; } public Map getMthPresetMap() { return mthPresetMap; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/DeobfuscatorVisitor.java ================================================ package jadx.core.deobf; import jadx.api.JadxArgs; import jadx.api.deobf.IAliasProvider; import jadx.api.deobf.IRenameCondition; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.exceptions.JadxException; public class DeobfuscatorVisitor extends AbstractVisitor { @Override public void init(RootNode root) throws JadxException { JadxArgs args = root.getArgs(); if (!args.isDeobfuscationOn()) { return; } DeobfPresets mapping = DeobfPresets.build(root); if (args.getGeneratedRenamesMappingFileMode().shouldRead()) { if (mapping.load()) { mapping.apply(root); } } IAliasProvider aliasProvider = args.getAliasProvider(); IRenameCondition renameCondition = args.getRenameCondition(); mapping.initIndexes(aliasProvider); process(root, renameCondition, aliasProvider); } public static void process(RootNode root, IRenameCondition renameCondition, IAliasProvider aliasProvider) { boolean pkgUpdated = false; for (PackageNode pkg : root.getPackages()) { if (renameCondition.shouldRename(pkg)) { String alias = aliasProvider.forPackage(pkg); if (alias != null) { pkg.rename(alias, false); pkgUpdated = true; } } } if (pkgUpdated) { root.runPackagesUpdate(); } for (ClassNode cls : root.getClasses()) { if (renameCondition.shouldRename(cls)) { String clsAlias = aliasProvider.forClass(cls); if (clsAlias != null) { cls.rename(clsAlias); } } for (FieldNode fld : cls.getFields()) { if (renameCondition.shouldRename(fld)) { String fldAlias = aliasProvider.forField(fld); if (fldAlias != null) { fld.rename(fldAlias); } } } for (MethodNode mth : cls.getMethods()) { if (renameCondition.shouldRename(mth)) { String mthAlias = aliasProvider.forMethod(mth); if (mthAlias != null) { mth.rename(mthAlias); } } } } } @Override public String getName() { return "DeobfuscatorVisitor"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/FileTypeDetector.java ================================================ package jadx.core.deobf; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import jadx.core.utils.FileSignature; import jadx.core.utils.StringUtils; public class FileTypeDetector { private static final Pattern DOCTYPE_PATTERN = Pattern.compile("\\s*]", Pattern.CASE_INSENSITIVE); private static final List FILE_SIGNATURES = new ArrayList<>(); static { register("png", "89 50 4E 47"); register("jpg", "FF D8 FF"); register("gif", "47 49 46 38"); register("webp", "52 49 46 46 ?? ?? ?? ?? 57 45 42 50 56 50 38"); register("bmp", "42 4D"); register("bmp", "42 41"); register("bmp", "43 49"); register("bmp", "43 50"); register("bmp", "49 43"); register("bmp", "50 54"); register("mp4", "00 00 00 ?? 66 74 79 70 69 73 6F 36"); register("mp4", "00 00 00 ?? 66 74 79 70 6D 70 34 32"); register("m4a", "00 00 00 ?? 66 74 79 70 4D 34 41 20"); register("mp3", "49 44 33"); register("ogg", "4F 67 67 53"); register("wav", "52 49 46 46 ?? ?? ?? ?? 57 41 56 45"); register("ttf", "00 01 00 00"); register("ttc", "74 74 63 66"); register("otf", "4F 54 54 4F"); register("xml", "03 00 08 00"); } public static void register(String fileType, String signature) { FILE_SIGNATURES.add(new FileSignature(fileType, signature)); } private static String detectByHeaders(byte[] data) { for (FileSignature sig : FILE_SIGNATURES) { if (FileSignature.matches(sig, data)) { if (sig.getFileType().equals("png") && isNinePatch(data)) { return ".9.png"; } return "." + sig.getFileType(); } } return null; } public static String detectFileExtension(byte[] data) { // detect ext by headers String extByHeaders = detectByHeaders(data); if (!StringUtils.isEmpty(extByHeaders)) { return extByHeaders; } // detect ext by readable text String text = new String(data, StandardCharsets.UTF_8); if (text.startsWith("-----BEGIN CERTIFICATE-----")) { return ".cer"; } if (text.startsWith("-----BEGIN PRIVATE KEY-----")) { return ".key"; } if (text.contains("")) { return ".html"; } Matcher m = DOCTYPE_PATTERN.matcher(text); if (m.lookingAt()) { return "." + m.group(1).toLowerCase(); } try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); factory.setFeature("http://xml.org/sax/features/external-general-entities", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.parse(new java.io.ByteArrayInputStream(data)); String rootTag = doc.getDocumentElement().getNodeName(); if ("svg".equalsIgnoreCase(rootTag)) { return ".svg"; } if ("plist".equalsIgnoreCase(rootTag)) { return ".plist"; } if ("kml".equalsIgnoreCase(rootTag)) { return ".kml"; } return ".xml"; } catch (Exception ignored) { } return null; } private static int readInt(byte[] data, int offset) { return (data[offset] & 0xFF) << 24 | (data[offset + 1] & 0xFF) << 16 | (data[offset + 2] & 0xFF) << 8 | (data[offset + 3] & 0xFF); } private static boolean isNinePatch(byte[] data) { int offset = 8; while (offset + 8 < data.length) { int chunkLength = readInt(data, offset); int chunkType = readInt(data, offset + 4); if (chunkType == 0x6e705463) { // 'npTc' return true; } offset += 8 + chunkLength + 4; // chunk + data + CRC } return false; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/NameMapper.java ================================================ package jadx.core.deobf; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.regex.Pattern; import jadx.core.utils.StringUtils; import static jadx.core.utils.StringUtils.notEmpty; public class NameMapper { public static final Pattern VALID_JAVA_IDENTIFIER = Pattern.compile( "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); private static final Pattern VALID_JAVA_FULL_IDENTIFIER = Pattern.compile( "(" + VALID_JAVA_IDENTIFIER + "\\.)*" + VALID_JAVA_IDENTIFIER); private static final Set RESERVED_NAMES = new HashSet<>( Arrays.asList( "_", "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "false", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "void", "volatile", "while")); public static boolean isReserved(String str) { return RESERVED_NAMES.contains(str); } public static boolean isValidIdentifier(String str) { return notEmpty(str) && !isReserved(str) && VALID_JAVA_IDENTIFIER.matcher(str).matches(); } public static boolean isValidFullIdentifier(String str) { return notEmpty(str) && !isReserved(str) && VALID_JAVA_FULL_IDENTIFIER.matcher(str).matches(); } public static boolean isValidAndPrintable(String str) { return isValidIdentifier(str) && isAllCharsPrintable(str); } public static boolean isValidIdentifierStart(int codePoint) { return Character.isJavaIdentifierStart(codePoint); } public static boolean isValidIdentifierPart(int codePoint) { return Character.isJavaIdentifierPart(codePoint); } public static boolean isPrintableChar(char c) { return 32 <= c && c <= 126; } public static boolean isPrintableAsciiCodePoint(int c) { return 32 <= c && c <= 126; } public static boolean isPrintableCodePoint(int codePoint) { if (Character.isISOControl(codePoint)) { return false; } if (Character.isWhitespace(codePoint)) { // don't print whitespaces other than standard one return codePoint == ' '; } switch (Character.getType(codePoint)) { case Character.CONTROL: case Character.FORMAT: case Character.PRIVATE_USE: case Character.SURROGATE: case Character.UNASSIGNED: return false; } return true; } public static boolean isAllCharsPrintable(String str) { int len = str.length(); int offset = 0; while (offset < len) { int codePoint = str.codePointAt(offset); if (!isPrintableAsciiCodePoint(codePoint)) { return false; } offset += Character.charCount(codePoint); } return true; } /** * Return modified string with removed: *
    *
  • not printable chars (including unicode) *
  • chars not valid for java identifier part *
* Note: this 'middle' method must be used with prefixed string: *
    *
  • can leave invalid chars for java identifier start (i.e numbers) *
  • result not checked for reserved words *
*/ public static String removeInvalidCharsMiddle(String name) { if (isValidIdentifier(name) && isAllCharsPrintable(name)) { return name; } int len = name.length(); StringBuilder sb = new StringBuilder(len); StringUtils.visitCodePoints(name, codePoint -> { if (isPrintableAsciiCodePoint(codePoint) && isValidIdentifierPart(codePoint)) { sb.appendCodePoint(codePoint); } }); return sb.toString(); } /** * Return string with removed invalid chars, see {@link #removeInvalidCharsMiddle} *

* Prepend prefix if first char is not valid as java identifier start char. */ public static String removeInvalidChars(String name, String prefix) { String result = removeInvalidCharsMiddle(name); if (!result.isEmpty()) { int codePoint = result.codePointAt(0); if (!isValidIdentifierStart(codePoint)) { return prefix + result; } } return result; } public static String removeNonPrintableCharacters(String name) { StringBuilder sb = new StringBuilder(name.length()); StringUtils.visitCodePoints(name, codePoint -> { if (isPrintableAsciiCodePoint(codePoint)) { sb.appendCodePoint(codePoint); } }); return sb.toString(); } private NameMapper() { } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/SaveDeobfMapping.java ================================================ package jadx.core.deobf; import java.nio.file.Files; import java.nio.file.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.api.args.GeneratedRenamesMappingFileMode; import jadx.core.codegen.json.JsonMappingGen; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.exceptions.JadxException; public class SaveDeobfMapping extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(SaveDeobfMapping.class); @Override public void init(RootNode root) throws JadxException { JadxArgs args = root.getArgs(); if (args.isDeobfuscationOn() || !args.isJsonOutput()) { saveMappings(root); } if (args.isJsonOutput()) { JsonMappingGen.dump(root); } } private void saveMappings(RootNode root) { GeneratedRenamesMappingFileMode mode = root.getArgs().getGeneratedRenamesMappingFileMode(); if (!mode.shouldWrite()) { return; } DeobfPresets mapping = DeobfPresets.build(root); Path deobfMapFile = mapping.getDeobfMapFile(); if (mode == GeneratedRenamesMappingFileMode.READ_OR_SAVE && Files.exists(deobfMapFile)) { return; } try { mapping.clear(); mapping.fill(root); mapping.save(); } catch (Exception e) { LOG.error("Failed to save deobfuscation map file '{}'", deobfMapFile.toAbsolutePath(), e); } } @Override public String getName() { return "SaveDeobfMapping"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/conditions/AbstractDeobfCondition.java ================================================ package jadx.core.deobf.conditions; import jadx.api.deobf.IDeobfCondition; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; public abstract class AbstractDeobfCondition implements IDeobfCondition { @Override public void init(RootNode root) { } @Override public Action check(PackageNode pkg) { return Action.NO_ACTION; } @Override public Action check(ClassNode cls) { return Action.NO_ACTION; } @Override public Action check(FieldNode fld) { return Action.NO_ACTION; } @Override public Action check(MethodNode mth) { return Action.NO_ACTION; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/conditions/AvoidClsAndPkgNamesCollision.java ================================================ package jadx.core.deobf.conditions; import java.util.HashSet; import java.util.Set; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; public class AvoidClsAndPkgNamesCollision extends AbstractDeobfCondition { private final Set avoidClsNames = new HashSet<>(); @Override public void init(RootNode root) { avoidClsNames.clear(); for (PackageNode pkg : root.getPackages()) { avoidClsNames.add(pkg.getName()); } } @Override public Action check(ClassNode cls) { if (avoidClsNames.contains(cls.getAlias())) { return Action.FORCE_RENAME; } return Action.NO_ACTION; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/conditions/BaseDeobfCondition.java ================================================ package jadx.core.deobf.conditions; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; /** * Disable deobfuscation for nodes: * - with 'DONT_RENAME' flag * - already renamed */ public class BaseDeobfCondition extends AbstractDeobfCondition { @Override public Action check(PackageNode pkg) { if (pkg.contains(AFlag.DONT_RENAME) || pkg.hasAlias()) { return Action.FORBID_RENAME; } return Action.NO_ACTION; } @Override public Action check(ClassNode cls) { if (cls.contains(AFlag.DONT_RENAME) || cls.getClassInfo().hasAlias()) { return Action.FORBID_RENAME; } return Action.NO_ACTION; } @Override public Action check(MethodNode mth) { if (mth.contains(AFlag.DONT_RENAME) || mth.getMethodInfo().hasAlias() || mth.isConstructor()) { return Action.FORBID_RENAME; } return Action.NO_ACTION; } @Override public Action check(FieldNode fld) { if (fld.contains(AFlag.DONT_RENAME) || fld.getFieldInfo().hasAlias()) { return Action.FORBID_RENAME; } return Action.NO_ACTION; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/conditions/DeobfLengthCondition.java ================================================ package jadx.core.deobf.conditions; import jadx.api.JadxArgs; import jadx.api.deobf.IDeobfCondition; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; public class DeobfLengthCondition implements IDeobfCondition { private int minLength; private int maxLength; @Override public void init(RootNode root) { JadxArgs args = root.getArgs(); this.minLength = args.getDeobfuscationMinLength(); this.maxLength = args.getDeobfuscationMaxLength(); } private Action checkName(String s) { int len = s.length(); if (len < minLength || len > maxLength) { return Action.FORCE_RENAME; } return Action.NO_ACTION; } @Override public Action check(PackageNode pkg) { return checkName(pkg.getName()); } @Override public Action check(ClassNode cls) { return checkName(cls.getName()); } @Override public Action check(FieldNode fld) { return checkName(fld.getName()); } @Override public Action check(MethodNode mth) { return checkName(mth.getName()); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/conditions/DeobfWhitelist.java ================================================ package jadx.core.deobf.conditions; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; public class DeobfWhitelist extends AbstractDeobfCondition { public static final List DEFAULT_LIST = Arrays.asList( "android.support.v4.*", "android.support.v7.*", "android.support.v4.os.*", "android.support.annotation.Px", "androidx.core.os.*", "androidx.annotation.Px"); public static final String DEFAULT_STR = Utils.listToString(DEFAULT_LIST, " "); private final Set packages = new HashSet<>(); private final Set classes = new HashSet<>(); @Override public void init(RootNode root) { packages.clear(); classes.clear(); for (String whitelistItem : root.getArgs().getDeobfuscationWhitelist()) { if (!whitelistItem.isEmpty()) { if (whitelistItem.endsWith(".*")) { packages.add(whitelistItem.substring(0, whitelistItem.length() - 2)); } else { classes.add(whitelistItem); } } } } @Override public Action check(PackageNode pkg) { if (packages.contains(pkg.getPkgInfo().getFullName())) { return Action.FORBID_RENAME; } return Action.NO_ACTION; } @Override public Action check(ClassNode cls) { if (classes.contains(cls.getClassInfo().getFullName())) { return Action.FORBID_RENAME; } return Action.NO_ACTION; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/conditions/ExcludeAndroidRClass.java ================================================ package jadx.core.deobf.conditions; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; public class ExcludeAndroidRClass extends AbstractDeobfCondition { @Override public Action check(ClassNode cls) { if (isR(cls.getTopParentClass())) { return Action.FORBID_RENAME; } return Action.NO_ACTION; } private static boolean isR(ClassNode cls) { if (cls.contains(AFlag.ANDROID_R_CLASS)) { return true; } if (!cls.getClassInfo().getShortName().equals("R")) { return false; } if (!cls.getMethods().isEmpty() || !cls.getFields().isEmpty()) { return false; } for (ClassNode inner : cls.getInnerClasses()) { for (MethodNode m : inner.getMethods()) { if (!m.getMethodInfo().isConstructor() && !m.getMethodInfo().isClassInit()) { return false; } } for (FieldNode field : cls.getFields()) { ArgType type = field.getType(); if (type != ArgType.INT && (!type.isArray() || type.getArrayElement() != ArgType.INT)) { return false; } } } cls.add(AFlag.ANDROID_R_CLASS); return true; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/conditions/ExcludePackageWithTLDNames.java ================================================ package jadx.core.deobf.conditions; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.Set; import java.util.stream.Collectors; import jadx.core.dex.nodes.PackageNode; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Provides a list of all top level domains, so we can exclude them from deobfuscation. */ public class ExcludePackageWithTLDNames extends AbstractDeobfCondition { /** * Lazy load TLD set */ private static class TldHolder { private static final Set TLD_SET = loadTldSet(); } private static Set loadTldSet() { try (BufferedReader reader = new BufferedReader(new InputStreamReader(TldHolder.class.getResourceAsStream("tlds.txt")))) { return reader.lines() .filter(line -> !line.startsWith("#") && !line.isEmpty()) .collect(Collectors.toSet()); } catch (Exception e) { throw new JadxRuntimeException("Failed to load top level domain list file: tlds.txt", e); } } @Override public Action check(PackageNode pkg) { if (pkg.isRoot() && TldHolder.TLD_SET.contains(pkg.getName())) { return Action.FORBID_RENAME; } return Action.NO_ACTION; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/deobf/conditions/JadxRenameConditions.java ================================================ package jadx.core.deobf.conditions; import java.util.ArrayList; import java.util.List; import jadx.api.deobf.IDeobfCondition; import jadx.api.deobf.IRenameCondition; import jadx.api.deobf.impl.CombineDeobfConditions; public class JadxRenameConditions { /** * This method provides a mutable list of default deobfuscation conditions used by jadx. * To build {@link IRenameCondition} use {@link CombineDeobfConditions#combine(List)} method. */ public static List buildDefaultDeobfConditions() { List list = new ArrayList<>(); list.add(new BaseDeobfCondition()); list.add(new DeobfWhitelist()); list.add(new ExcludePackageWithTLDNames()); list.add(new ExcludeAndroidRClass()); list.add(new AvoidClsAndPkgNamesCollision()); list.add(new DeobfLengthCondition()); return list; } public static IRenameCondition buildDefault() { return CombineDeobfConditions.combine(buildDefaultDeobfConditions()); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java ================================================ package jadx.core.dex.attributes; public enum AFlag { MTH_ENTER_BLOCK, MTH_EXIT_BLOCK, TRY_ENTER, TRY_LEAVE, LOOP_START, LOOP_END, SYNTHETIC, RETURN, // block contains only return instruction ORIG_RETURN, DONT_WRAP, DONT_INLINE, DONT_INLINE_CONST, DONT_INVERT, // don't invert this if statement DONT_GENERATE, // process as usual, but don't output to generated code COMMENT_OUT, // process as usual, but comment insn in generated code REMOVE, // can be completely removed REMOVE_SUPER_CLASS, // don't add super class HIDDEN, // instruction used inside other instruction but not listed in args CONVERTED_ENUM, // enum class successfully restored to original form DONT_RENAME, // do not rename during deobfuscation FORCE_RAW_NAME, // force use of raw name instead alias ADDED_TO_REGION, // this loop condition has been merged or otherwise shouldn't be subject to the 1 instruction limit ALLOW_MULTIPLE_INSNS_LOOP_COND, EXC_TOP_SPLITTER, EXC_BOTTOM_SPLITTER, FINALLY_INSNS, IGNORE_THROW_SPLIT, SKIP_FIRST_ARG, SKIP_ARG, // skip argument in invoke call NO_SKIP_ARGS, ANONYMOUS_CONSTRUCTOR, INLINE_INSTANCE_FIELD, THIS, SUPER, PACKAGE_INFO, /** * Mark Android resources class */ ANDROID_R_CLASS, /** * RegisterArg attribute for method arguments */ METHOD_ARGUMENT, /** * Type of RegisterArg or SSAVar can't be changed */ IMMUTABLE_TYPE, /** * Force inline instruction with inline assign */ FORCE_ASSIGN_INLINE, /** * A MOVE instruction has been inlined */ MOVE_INLINED, CUSTOM_DECLARE, // variable for this register don't need declaration DECLARE_VAR, ELSE_IF_CHAIN, WRAPPED, ARITH_ONEARG, FALL_THROUGH, VARARG_CALL, /** * Use constants with explicit type: cast '(byte) 1' or type letter '7L' */ EXPLICIT_PRIMITIVE_TYPE, EXPLICIT_CAST, SOFT_CAST, // synthetic cast to help type inference (allow unchecked casts for generics) INCONSISTENT_CODE, // warning about incorrect decompilation REQUEST_IF_REGION_OPTIMIZE, // run if region visitor again REQUEST_CODE_SHRINK, METHOD_CANDIDATE_FOR_INLINE, USE_LINES_HINTS, // source lines info in methods can be trusted DISABLE_BLOCKS_LOCK, // Class processing flags RESTART_CODEGEN, // codegen must be executed again RELOAD_AT_CODEGEN_STAGE, // class can't be analyzed at 'process' stage => unload before 'codegen' stage CLASS_DEEP_RELOAD, // perform deep class unload (reload) before process CLASS_UNLOADED, // class was completely unloaded DONT_UNLOAD_CLASS, // don't unload class after code generation (only for tests and debug!) RESOLVE_JAVA_JSR, COMPUTE_POST_DOM, } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/AType.java ================================================ package jadx.core.dex.attributes; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.codegen.utils.CodeComment; import jadx.core.dex.attributes.nodes.AnonymousClassAttr; import jadx.core.dex.attributes.nodes.ClassTypeVarsAttr; import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; import jadx.core.dex.attributes.nodes.DeclareVariablesAttr; import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr; import jadx.core.dex.attributes.nodes.EdgeInsnAttr; import jadx.core.dex.attributes.nodes.EnumClassAttr; import jadx.core.dex.attributes.nodes.EnumMapAttr; import jadx.core.dex.attributes.nodes.ExcSplitCrossAttr; import jadx.core.dex.attributes.nodes.FieldReplaceAttr; import jadx.core.dex.attributes.nodes.ForceReturnAttr; import jadx.core.dex.attributes.nodes.GenericInfoAttr; import jadx.core.dex.attributes.nodes.InlinedAttr; import jadx.core.dex.attributes.nodes.JadxCommentsAttr; import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.attributes.nodes.JumpInfo; import jadx.core.dex.attributes.nodes.LocalVarsDebugInfoAttr; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.attributes.nodes.LoopLabelAttr; import jadx.core.dex.attributes.nodes.MethodBridgeAttr; import jadx.core.dex.attributes.nodes.MethodInlineAttr; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.MethodReplaceAttr; import jadx.core.dex.attributes.nodes.MethodThrowsAttr; import jadx.core.dex.attributes.nodes.MethodTypeVarsAttr; import jadx.core.dex.attributes.nodes.PhiListAttr; import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; import jadx.core.dex.attributes.nodes.RegionRefAttr; import jadx.core.dex.attributes.nodes.RenameReasonAttr; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.attributes.nodes.SpecialEdgeAttr; import jadx.core.dex.attributes.nodes.TmpEdgeAttr; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.dex.trycatch.TryCatchBlockAttr; /** * Attribute types enumeration, * uses generic type for omit cast after 'AttributeStorage.get' method * * @param attribute class implementation */ public final class AType implements IJadxAttrType { // class, method, field, insn public static final AType> CODE_COMMENTS = new AType<>(); // class, method, field public static final AType RENAME_REASON = new AType<>(); // class, method public static final AType> JADX_ERROR = new AType<>(); // code failed to decompile public static final AType JADX_COMMENTS = new AType<>(); // additional info about decompilation // class public static final AType ENUM_CLASS = new AType<>(); public static final AType ENUM_MAP = new AType<>(); public static final AType CLASS_TYPE_VARS = new AType<>(); public static final AType ANONYMOUS_CLASS = new AType<>(); public static final AType INLINED = new AType<>(); public static final AType DECOMPILE_MODE_OVERRIDE = new AType<>(); // field public static final AType FIELD_INIT_INSN = new AType<>(); public static final AType FIELD_REPLACE = new AType<>(); // method public static final AType LOCAL_VARS_DEBUG_INFO = new AType<>(); public static final AType METHOD_INLINE = new AType<>(); public static final AType METHOD_REPLACE = new AType<>(); public static final AType BRIDGED_BY = new AType<>(); public static final AType SKIP_MTH_ARGS = new AType<>(); public static final AType METHOD_OVERRIDE = new AType<>(); public static final AType METHOD_TYPE_VARS = new AType<>(); public static final AType> TRY_BLOCKS_LIST = new AType<>(); public static final AType METHOD_CODE_FEATURES = new AType<>(); public static final AType METHOD_THROWS = new AType<>(); // region public static final AType DECLARE_VARIABLES = new AType<>(); // block public static final AType PHI_LIST = new AType<>(); public static final AType FORCE_RETURN = new AType<>(); public static final AType> LOOP = new AType<>(); public static final AType> EDGE_INSN = new AType<>(); public static final AType> SPECIAL_EDGE = new AType<>(); public static final AType TMP_EDGE = new AType<>(); public static final AType TRY_BLOCK = new AType<>(); public static final AType EXC_SPLIT_CROSS = new AType<>(); // block or insn public static final AType EXC_HANDLER = new AType<>(); public static final AType EXC_CATCH = new AType<>(); // instruction public static final AType LOOP_LABEL = new AType<>(); public static final AType> JUMP = new AType<>(); public static final AType METHOD_DETAILS = new AType<>(); public static final AType GENERIC_INFO = new AType<>(); public static final AType REGION_REF = new AType<>(); // register public static final AType REG_DEBUG_INFO = new AType<>(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/AttrList.java ================================================ package jadx.core.dex.attributes; import java.util.ArrayList; import java.util.List; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.utils.Utils; public class AttrList implements IJadxAttribute { private static final int MAX_ATTRLIST_LENGTH = 300; private final IJadxAttrType> type; private final List list = new ArrayList<>(); public AttrList(IJadxAttrType> type) { this.type = type; } public List getList() { return list; } @Override public IJadxAttrType> getAttrType() { return type; } @Override public String toString() { String commaDelimited = Utils.listToString(list, ", "); // if the comma delimited list is too long, use newlines instead to maintain readability if (commaDelimited.length() > MAX_ATTRLIST_LENGTH) { return Utils.listToString(list, "\n "); } return commaDelimited; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/AttrNode.java ================================================ package jadx.core.dex.attributes; import java.util.List; import jadx.api.CommentsLevel; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.Consts; import jadx.core.dex.attributes.nodes.JadxCommentsAttr; import jadx.core.utils.Utils; public abstract class AttrNode implements IAttributeNode { private static final AttributeStorage EMPTY_ATTR_STORAGE = EmptyAttrStorage.INSTANCE; private AttributeStorage storage = EMPTY_ATTR_STORAGE; @Override public void add(AFlag flag) { initStorage().add(flag); if (Consts.DEBUG_ATTRIBUTES) { addDebugComment("Add flag " + flag + " at " + Utils.currentStackTrace(2)); } } @Override public void addAttr(IJadxAttribute attr) { initStorage().add(attr); if (Consts.DEBUG_ATTRIBUTES) { addDebugComment("Add attribute " + attr.getClass().getSimpleName() + ": " + attr + " at " + Utils.currentStackTrace(2)); } } @Override public void addAttrs(List list) { if (list.isEmpty()) { return; } initStorage().add(list); } @Override public void addAttr(IJadxAttrType> type, T obj) { initStorage().add(type, obj); if (Consts.DEBUG_ATTRIBUTES) { addDebugComment("Add attribute " + obj + " at " + Utils.currentStackTrace(2)); } } public void addAttr(IJadxAttrType> type, List list) { AttributeStorage strg = initStorage(); list.forEach(attr -> strg.add(type, attr)); } @Override public void copyAttributesFrom(AttrNode attrNode) { AttributeStorage copyFrom = attrNode.storage; if (!copyFrom.isEmpty()) { initStorage().addAll(copyFrom); } } @Override public void copyAttributeFrom(AttrNode attrNode, AType attrType) { IJadxAttribute attr = attrNode.get(attrType); if (attr != null) { this.addAttr(attr); } } /** * Remove attribute in this node, add copy from other if exists */ @Override public void rewriteAttributeFrom(AttrNode attrNode, AType attrType) { remove(attrType); copyAttributeFrom(attrNode, attrType); } private AttributeStorage initStorage() { AttributeStorage store = storage; if (store == EMPTY_ATTR_STORAGE) { store = new AttributeStorage(); storage = store; } return store; } private void unloadIfEmpty() { if (storage.isEmpty() && storage != EMPTY_ATTR_STORAGE) { storage = EMPTY_ATTR_STORAGE; } } @Override public boolean contains(AFlag flag) { return storage.contains(flag); } @Override public boolean contains(IJadxAttrType type) { return storage.contains(type); } @Override public T get(IJadxAttrType type) { return storage.get(type); } @Override public IAnnotation getAnnotation(String cls) { return storage.getAnnotation(cls); } @Override public List getAll(IJadxAttrType> type) { return storage.getAll(type); } @Override public void remove(AFlag flag) { storage.remove(flag); unloadIfEmpty(); } @Override public void remove(IJadxAttrType type) { storage.remove(type); unloadIfEmpty(); } @Override public void removeAttr(IJadxAttribute attr) { storage.remove(attr); unloadIfEmpty(); } @Override public void clearAttributes() { storage = EMPTY_ATTR_STORAGE; } public void unloadAttributes() { if (storage == EMPTY_ATTR_STORAGE) { return; } storage.unloadAttributes(); storage.clearFlags(); unloadIfEmpty(); } @Override public List getAttributesStringsList() { return storage.getAttributeStrings(); } @Override public String getAttributesString() { return storage.toString(); } @Override public boolean isAttrStorageEmpty() { return storage.isEmpty(); } private void addDebugComment(String msg) { JadxCommentsAttr commentsAttr = get(AType.JADX_COMMENTS); if (commentsAttr == null) { commentsAttr = new JadxCommentsAttr(); initStorage().add(commentsAttr); } commentsAttr.add(CommentsLevel.DEBUG, msg); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/AttributeStorage.java ================================================ package jadx.core.dex.attributes; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Storage for different attribute types:
* 1. Flags - boolean attribute (set or not)
* 2. Attributes - class instance ({@link IJadxAttribute}) associated with an attribute type * ({@link IJadxAttrType})
*/ public class AttributeStorage { public static AttributeStorage fromList(List list) { AttributeStorage storage = new AttributeStorage(); storage.add(list); return storage; } static { int flagsCount = AFlag.values().length; if (flagsCount >= 64) { throw new JadxRuntimeException("Try to reduce flags count to 64 for use one long in EnumSet, now " + flagsCount); } } private static final Map, IJadxAttribute> EMPTY_ATTRIBUTES = Collections.emptyMap(); private final Set flags; private Map, IJadxAttribute> attributes; public AttributeStorage() { flags = EnumSet.noneOf(AFlag.class); attributes = EMPTY_ATTRIBUTES; } public void add(AFlag flag) { flags.add(flag); } public void add(IJadxAttribute attr) { writeAttributes(map -> map.put(attr.getAttrType(), attr)); } public void add(List list) { writeAttributes(map -> list.forEach(attr -> map.put(attr.getAttrType(), attr))); } public void add(IJadxAttrType> type, T obj) { AttrList list = get(type); if (list == null) { list = new AttrList<>(type); add(list); } list.getList().add(obj); } public void addAll(AttributeStorage otherList) { flags.addAll(otherList.flags); if (!otherList.attributes.isEmpty()) { writeAttributes(m -> m.putAll(otherList.attributes)); } } public boolean contains(AFlag flag) { return flags.contains(flag); } public boolean contains(IJadxAttrType type) { return attributes.containsKey(type); } @SuppressWarnings("unchecked") public T get(IJadxAttrType type) { return (T) attributes.get(type); } public IAnnotation getAnnotation(String cls) { AnnotationsAttr aList = get(JadxAttrType.ANNOTATION_LIST); return aList == null ? null : aList.get(cls); } public List getAll(IJadxAttrType> type) { AttrList attrList = get(type); if (attrList == null) { return Collections.emptyList(); } return Collections.unmodifiableList(attrList.getList()); } public void remove(AFlag flag) { flags.remove(flag); } public void clearFlags() { flags.clear(); } public void remove(IJadxAttrType type) { if (!attributes.isEmpty()) { writeAttributes(map -> map.remove(type)); } } public void remove(IJadxAttribute attr) { if (!attributes.isEmpty()) { writeAttributes(map -> { IJadxAttrType type = attr.getAttrType(); IJadxAttribute a = map.get(type); if (a == attr) { map.remove(type); } }); } } private void writeAttributes(Consumer, IJadxAttribute>> mapConsumer) { synchronized (this) { if (attributes == EMPTY_ATTRIBUTES) { attributes = new IdentityHashMap<>(2); // only 1 or 2 attributes added in most cases } mapConsumer.accept(attributes); if (attributes.isEmpty()) { attributes = EMPTY_ATTRIBUTES; } } } public void unloadAttributes() { if (attributes.isEmpty()) { return; } writeAttributes(map -> map.entrySet().removeIf(entry -> !entry.getValue().keepLoaded())); } public List getAttributeStrings() { int size = flags.size() + attributes.size(); if (size == 0) { return Collections.emptyList(); } List list = new ArrayList<>(size); for (AFlag a : flags) { list.add(a.toString()); } for (IJadxAttribute a : attributes.values()) { list.add(a.toAttrString()); } return list; } public boolean isEmpty() { return flags.isEmpty() && attributes.isEmpty(); } @Override public String toString() { List list = getAttributeStrings(); if (list.isEmpty()) { return ""; } list.sort(String::compareTo); return "A[" + Utils.listToString(list) + ']'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/EmptyAttrStorage.java ================================================ package jadx.core.dex.attributes; import java.util.Collections; import java.util.List; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; public final class EmptyAttrStorage extends AttributeStorage { public static final AttributeStorage INSTANCE = new EmptyAttrStorage(); private EmptyAttrStorage() { // singleton } @Override public boolean contains(AFlag flag) { return false; } @Override public boolean contains(IJadxAttrType type) { return false; } @Override public T get(IJadxAttrType type) { return null; } @Override public IAnnotation getAnnotation(String cls) { return null; } @Override public List getAll(IJadxAttrType> type) { return Collections.emptyList(); } @Override public void remove(AFlag flag) { // ignore } @Override public void remove(IJadxAttrType type) { // ignore } @Override public void remove(IJadxAttribute attr) { // ignore } @Override public List getAttributeStrings() { return Collections.emptyList(); } @Override public boolean isEmpty() { return true; } @Override public String toString() { return ""; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/FieldInitInsnAttr.java ================================================ package jadx.core.dex.attributes; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.PinnedAttribute; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import static java.util.Objects.requireNonNull; public final class FieldInitInsnAttr extends PinnedAttribute { private final MethodNode mth; private final InsnNode insn; public FieldInitInsnAttr(MethodNode mth, InsnNode insn) { this.mth = requireNonNull(mth); this.insn = requireNonNull(insn); } public InsnNode getInsn() { return insn; } public MethodNode getInsnMth() { return mth; } @Override public IJadxAttrType getAttrType() { return AType.FIELD_INIT_INSN; } @Override public String toString() { return "INIT{" + insn + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/IAttributeNode.java ================================================ package jadx.core.dex.attributes; import java.util.List; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; public interface IAttributeNode { void add(AFlag flag); void addAttr(IJadxAttribute attr); void addAttrs(List list); void addAttr(IJadxAttrType> type, T obj); void copyAttributesFrom(AttrNode attrNode); void copyAttributeFrom(AttrNode attrNode, AType attrType); void rewriteAttributeFrom(AttrNode attrNode, AType attrType); boolean contains(AFlag flag); boolean contains(IJadxAttrType type); T get(IJadxAttrType type); IAnnotation getAnnotation(String cls); List getAll(IJadxAttrType> type); void remove(AFlag flag); void remove(IJadxAttrType type); void removeAttr(IJadxAttribute attr); void clearAttributes(); List getAttributesStringsList(); String getAttributesString(); boolean isAttrStorageEmpty(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/ILineAttributeNode.java ================================================ package jadx.core.dex.attributes; public interface ILineAttributeNode { int getSourceLine(); void setSourceLine(int sourceLine); int getDefPosition(); void setDefPosition(int pos); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/AnonymousClassAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.PinnedAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; public class AnonymousClassAttr extends PinnedAttribute { public enum InlineType { CONSTRUCTOR, INSTANCE_FIELD, } private final ClassNode outerCls; private final ArgType baseType; private final InlineType inlineType; public AnonymousClassAttr(ClassNode outerCls, ArgType baseType, InlineType inlineType) { this.outerCls = outerCls; this.baseType = baseType; this.inlineType = inlineType; } public ClassNode getOuterCls() { return outerCls; } public ArgType getBaseType() { return baseType; } public InlineType getInlineType() { return inlineType; } @Override public AType getAttrType() { return AType.ANONYMOUS_CLASS; } @Override public String toString() { return "AnonymousClass{" + outerCls + ", base: " + baseType + ", inline type: " + inlineType + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/ClassTypeVarsAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.Collections; import java.util.List; import java.util.Map; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.args.ArgType; public class ClassTypeVarsAttr implements IJadxAttribute { public static final ClassTypeVarsAttr EMPTY = new ClassTypeVarsAttr(Collections.emptyList(), Collections.emptyMap()); /** * Type vars defined in current class */ private final List typeVars; /** * Type vars mapping in current and super types: * TypeRawObj -> (TypeVarInSuperType -> TypeVarFromThisClass) */ private final Map> superTypeMaps; public ClassTypeVarsAttr(List typeVars, Map> superTypeMaps) { this.typeVars = typeVars; this.superTypeMaps = superTypeMaps; } public List getTypeVars() { return typeVars; } public Map getTypeVarsMapFor(ArgType type) { Map typeMap = superTypeMaps.get(type.getObject()); if (typeMap == null) { return Collections.emptyMap(); } return typeMap; } @Override public AType getAttrType() { return AType.CLASS_TYPE_VARS; } @Override public String toString() { return "ClassTypeVarsAttr{" + typeVars + ", super maps: " + superTypeMaps + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/CodeFeaturesAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.EnumSet; import java.util.Set; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.MethodNode; public class CodeFeaturesAttr implements IJadxAttribute { public enum CodeFeature { /** * Code contains switch instruction */ SWITCH, /** * Code contains new-array instruction */ NEW_ARRAY, } public static boolean contains(MethodNode mth, CodeFeature feature) { CodeFeaturesAttr codeFeaturesAttr = mth.get(AType.METHOD_CODE_FEATURES); if (codeFeaturesAttr == null) { return false; } return codeFeaturesAttr.getCodeFeatures().contains(feature); } public static void add(MethodNode mth, CodeFeature feature) { CodeFeaturesAttr codeFeaturesAttr = mth.get(AType.METHOD_CODE_FEATURES); if (codeFeaturesAttr == null) { codeFeaturesAttr = new CodeFeaturesAttr(); mth.addAttr(codeFeaturesAttr); } codeFeaturesAttr.getCodeFeatures().add(feature); } private final Set codeFeatures = EnumSet.noneOf(CodeFeature.class); public Set getCodeFeatures() { return codeFeatures; } @Override public AType getAttrType() { return AType.METHOD_CODE_FEATURES; } @Override public String toAttrString() { return "CodeFeatures{" + codeFeatures + '}'; } @Override public String toString() { return toAttrString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/DeclareVariablesAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.ArrayList; import java.util.List; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.args.CodeVar; import jadx.core.utils.Utils; /** * List of variables to be declared at region start. */ public class DeclareVariablesAttr implements IJadxAttribute { private final List vars = new ArrayList<>(); public Iterable getVars() { return vars; } public void addVar(CodeVar arg) { vars.add(arg); } @Override public AType getAttrType() { return AType.DECLARE_VARIABLES; } @Override public String toString() { return "DECL_VAR: " + Utils.listToString(vars); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/DecompileModeOverrideAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.DecompilationMode; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; public class DecompileModeOverrideAttr implements IJadxAttribute { private final DecompilationMode mode; public DecompileModeOverrideAttr(DecompilationMode mode) { this.mode = mode; } public DecompilationMode getMode() { return mode; } @Override public IJadxAttrType getAttrType() { return AType.DECOMPILE_MODE_OVERRIDE; } @Override public String toString() { return "DECOMPILE_MODE_OVERRIDE: " + mode; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/EdgeInsnAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.Objects; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttrList; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.Edge; import jadx.core.dex.nodes.InsnNode; public class EdgeInsnAttr implements IJadxAttribute { private final BlockNode start; private final BlockNode end; private final InsnNode insn; public static void addEdgeInsn(Edge edge, InsnNode insn) { addEdgeInsn(edge.getSource(), edge.getTarget(), insn); } public static void addEdgeInsn(BlockNode start, BlockNode end, InsnNode insn) { EdgeInsnAttr edgeInsnAttr = new EdgeInsnAttr(start, end, insn); if (!start.getAll(AType.EDGE_INSN).contains(edgeInsnAttr)) { start.addAttr(AType.EDGE_INSN, edgeInsnAttr); } if (!end.getAll(AType.EDGE_INSN).contains(edgeInsnAttr)) { end.addAttr(AType.EDGE_INSN, edgeInsnAttr); } } private EdgeInsnAttr(BlockNode start, BlockNode end, InsnNode insn) { this.start = start; this.end = end; this.insn = insn; } @Override public AType> getAttrType() { return AType.EDGE_INSN; } public BlockNode getStart() { return start; } public BlockNode getEnd() { return end; } public InsnNode getInsn() { return insn; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } EdgeInsnAttr that = (EdgeInsnAttr) o; return start.equals(that.start) && end.equals(that.end) && insn.isDeepEquals(that.insn); } @Override public int hashCode() { return Objects.hash(start, end, insn); } @Override public String toString() { return "EDGE_INSN: " + start + "->" + end + ' ' + insn; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/EnumClassAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; public class EnumClassAttr implements IJadxAttribute { public static class EnumField { private final FieldNode field; private final ConstructorInsn constrInsn; private final @Nullable String nameStr; private ClassNode cls; public EnumField(FieldNode field, ConstructorInsn co, @Nullable String nameStr) { this.field = field; this.constrInsn = co; this.nameStr = nameStr; } public FieldNode getField() { return field; } public ConstructorInsn getConstrInsn() { return constrInsn; } public ClassNode getCls() { return cls; } public void setCls(ClassNode cls) { this.cls = cls; } public @Nullable String getNameStr() { return nameStr; } @Override public String toString() { return field + "(" + constrInsn + ") " + cls; } } private final List fields; private MethodNode staticMethod; public EnumClassAttr(List fields) { this.fields = fields; } public List getFields() { return fields; } public MethodNode getStaticMethod() { return staticMethod; } public void setStaticMethod(MethodNode staticMethod) { this.staticMethod = staticMethod; } @Override public AType getAttrType() { return AType.ENUM_CLASS; } @Override public String toString() { return "Enum fields: " + fields; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/EnumMapAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.HashMap; import java.util.Map; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.FieldNode; public class EnumMapAttr implements IJadxAttribute { public static class KeyValueMap { private final Map map = new HashMap<>(); public Object get(Object key) { return map.get(key); } void put(Object key, Object value) { map.put(key, value); } } @Nullable private Map fieldsMap; @Nullable public KeyValueMap getMap(FieldNode field) { if (fieldsMap == null) { return null; } return fieldsMap.get(field); } public void add(FieldNode field, Object key, Object value) { KeyValueMap map = getMap(field); if (map == null) { map = new KeyValueMap(); if (fieldsMap == null) { fieldsMap = new HashMap<>(); } fieldsMap.put(field, map); } map.put(key, value); } public boolean isEmpty() { return fieldsMap == null || fieldsMap.isEmpty(); } @Override public AType getAttrType() { return AType.ENUM_MAP; } @Override public String toString() { return "Enum fields map: " + fieldsMap; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/ExcSplitCrossAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.BlockNode; /** * This attribute is set on the new synthetic node that BlockExceptionHandler creates at the bottom * of certain try regions. It stores a reference to the original path cross of the bottom of the try * region, so that blocks can be restructured to not pass through it when that would create an * erroneous loop. */ public class ExcSplitCrossAttr implements IJadxAttribute { private final BlockNode originalPathCross; public ExcSplitCrossAttr(BlockNode originalPathCross) { this.originalPathCross = originalPathCross; } public BlockNode getOriginalPathCross() { return this.originalPathCross; } @Override public IJadxAttrType getAttrType() { return AType.EXC_SPLIT_CROSS; } @Override public String toString() { return "ExcSplitCross -> " + originalPathCross.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/FieldReplaceAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.PinnedAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.args.InsnArg; public class FieldReplaceAttr extends PinnedAttribute { public enum ReplaceWith { CLASS_INSTANCE, VAR } private final ReplaceWith replaceType; private final Object replaceObj; public FieldReplaceAttr(ClassInfo cls) { this.replaceType = ReplaceWith.CLASS_INSTANCE; this.replaceObj = cls; } public FieldReplaceAttr(InsnArg reg) { this.replaceType = ReplaceWith.VAR; this.replaceObj = reg; } public ReplaceWith getReplaceType() { return replaceType; } public ClassInfo getClsRef() { return (ClassInfo) replaceObj; } public InsnArg getVarRef() { return (InsnArg) replaceObj; } @Override public AType getAttrType() { return AType.FIELD_REPLACE; } @Override public String toString() { return "REPLACE: " + replaceType + ' ' + replaceObj; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/ForceReturnAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.Utils; public class ForceReturnAttr implements IJadxAttribute { private final InsnNode returnInsn; public ForceReturnAttr(InsnNode retInsn) { this.returnInsn = retInsn; } public InsnNode getReturnInsn() { return returnInsn; } @Override public AType getAttrType() { return AType.FORCE_RETURN; } @Override public String toString() { return "FORCE_RETURN " + Utils.listToString(returnInsn.getArguments()); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/GenericInfoAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.List; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.args.ArgType; public class GenericInfoAttr implements IJadxAttribute { private final List genericTypes; private boolean explicit; public GenericInfoAttr(List genericTypes) { this.genericTypes = genericTypes; } public List getGenericTypes() { return genericTypes; } public boolean isExplicit() { return explicit; } public void setExplicit(boolean explicit) { this.explicit = explicit; } @Override public AType getAttrType() { return AType.GENERIC_INFO; } @Override public String toString() { return "GenericInfoAttr{" + genericTypes + ", explicit=" + explicit + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/InlinedAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.ClassNode; public class InlinedAttr implements IJadxAttribute { private final ClassNode inlineCls; public InlinedAttr(ClassNode inlineCls) { this.inlineCls = inlineCls; } public ClassNode getInlineCls() { return inlineCls; } @Override public IJadxAttrType getAttrType() { return AType.INLINED; } @Override public String toString() { return "INLINED: " + inlineCls; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxCommentsAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.Collections; import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import jadx.api.CommentsLevel; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.utils.Utils; public class JadxCommentsAttr implements IJadxAttribute { public static void add(IAttributeNode node, CommentsLevel level, String comment) { initFor(node).add(level, comment); } private static JadxCommentsAttr initFor(IAttributeNode node) { JadxCommentsAttr currentAttr = node.get(AType.JADX_COMMENTS); if (currentAttr != null) { return currentAttr; } JadxCommentsAttr newAttr = new JadxCommentsAttr(); node.addAttr(newAttr); return newAttr; } private final Map> comments = new EnumMap<>(CommentsLevel.class); public void add(CommentsLevel level, String comment) { comments.computeIfAbsent(level, l -> new HashSet<>()).add(comment); } public List formatAndFilter(CommentsLevel level) { if (level == CommentsLevel.NONE || level == CommentsLevel.USER_ONLY) { return Collections.emptyList(); } return comments.entrySet().stream() .filter(e -> e.getKey().filter(level)) .flatMap(e -> { String levelName = e.getKey().name(); return e.getValue().stream() .map(v -> "JADX " + levelName + ": " + v); }) .sorted() .collect(Collectors.toList()); } public Map> getComments() { return comments; } @Override public IJadxAttrType getAttrType() { return AType.JADX_COMMENTS; } @Override public String toString() { return "JadxCommentsAttr{\n " + Utils.listToString(comments.entrySet(), "\n ", e -> e.getKey() + ": \n -> " + Utils.listToString(e.getValue(), "\n -> ")) + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxError.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.Objects; import org.jetbrains.annotations.NotNull; import jadx.core.utils.Utils; public class JadxError implements Comparable { private final String error; private final Throwable cause; public JadxError(String error, Throwable cause) { this.error = Objects.requireNonNull(error); this.cause = cause; } public String getError() { return error; } public Throwable getCause() { return cause; } @Override public int compareTo(@NotNull JadxError o) { return this.error.compareTo(o.getError()); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } JadxError other = (JadxError) o; return error.equals(other.error); } @Override public int hashCode() { return error.hashCode(); } @Override public String toString() { StringBuilder str = new StringBuilder(); str.append("JadxError: ").append(error).append(' '); if (cause != null) { str.append(cause.getClass()); str.append(':'); str.append(cause.getMessage()); str.append('\n'); str.append(Utils.getStackTrace(cause)); } return str.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JumpInfo.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.core.utils.InsnUtils; public class JumpInfo { private final int src; private final int dest; public JumpInfo(int src, int dest) { this.src = src; this.dest = dest; } public int getSrc() { return src; } public int getDest() { return dest; } @Override public int hashCode() { return 31 * dest + src; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } JumpInfo other = (JumpInfo) obj; return dest == other.dest && src == other.src; } @Override public String toString() { return "JUMP: " + InsnUtils.formatOffset(src) + " -> " + InsnUtils.formatOffset(dest); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LineAttrNode.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.core.dex.attributes.AttrNode; import jadx.core.dex.attributes.ILineAttributeNode; public abstract class LineAttrNode extends AttrNode implements ILineAttributeNode { private int sourceLine; /** * Position where a node declared at in decompiled code */ private int defPosition; @Override public int getSourceLine() { return sourceLine; } @Override public void setSourceLine(int sourceLine) { this.sourceLine = sourceLine; } @Override public int getDefPosition() { return this.defPosition; } @Override public void setDefPosition(int defPosition) { this.defPosition = defPosition; } public void addSourceLineFrom(LineAttrNode lineAttrNode) { if (this.getSourceLine() == 0) { this.setSourceLine(lineAttrNode.getSourceLine()); } } public void copyLines(LineAttrNode lineAttrNode) { setSourceLine(lineAttrNode.getSourceLine()); setDefPosition(lineAttrNode.getDefPosition()); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LocalVarsDebugInfoAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.List; import jadx.api.plugins.input.data.ILocalVar; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.utils.Utils; public class LocalVarsDebugInfoAttr implements IJadxAttribute { private final List localVars; public LocalVarsDebugInfoAttr(List localVars) { this.localVars = localVars; } public List getLocalVars() { return localVars; } @Override public AType getAttrType() { return AType.LOCAL_VARS_DEBUG_INFO; } @Override public String toString() { return "Debug Info:\n " + Utils.listToString(localVars, "\n "); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LoopInfo.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.Edge; import jadx.core.utils.BlockUtils; public class LoopInfo { private final BlockNode start; private final BlockNode end; private final Set loopBlocks; private int id; private LoopInfo parentLoop; public LoopInfo(BlockNode start, BlockNode end, Set loopBlocks) { this.start = start; this.end = end; this.loopBlocks = loopBlocks; } public BlockNode getStart() { return start; } public BlockNode getEnd() { return end; } public Set getLoopBlocks() { return loopBlocks; } /** * Return source blocks of exit edges.
* Exit nodes belongs to loop (contains in {@code loopBlocks}) */ public Set getExitNodes() { Set nodes = new HashSet<>(); Set blocks = getLoopBlocks(); for (BlockNode block : blocks) { // exit: successor node not from this loop, (don't change to getCleanSuccessors) for (BlockNode s : block.getSuccessors()) { if (!blocks.contains(s) && !s.contains(AType.EXC_HANDLER)) { nodes.add(block); } } } return nodes; } /** * Return loop exit edges. */ public List getExitEdges() { List edges = new ArrayList<>(); Set blocks = getLoopBlocks(); for (BlockNode block : blocks) { for (BlockNode s : block.getSuccessors()) { // don't use clean successors to include loop back edges if (!blocks.contains(s) && !BlockUtils.isExceptionHandlerPath(s)) { edges.add(new Edge(block, s)); } } } return edges; } public BlockNode getPreHeader() { return BlockUtils.selectOther(end, start.getPredecessors()); } public int getId() { return id; } public void setId(int id) { this.id = id; } public LoopInfo getParentLoop() { return parentLoop; } public void setParentLoop(LoopInfo parentLoop) { this.parentLoop = parentLoop; } public boolean hasParent(LoopInfo searchLoop) { LoopInfo parent = parentLoop; while (true) { if (parent == null) { return false; } if (parent == searchLoop) { return true; } parent = parent.getParentLoop(); } } @Override public String toString() { return "LOOP:" + id + ": " + start + "->" + end; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LoopLabelAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; public class LoopLabelAttr implements IJadxAttribute { private final LoopInfo loop; public LoopLabelAttr(LoopInfo loop) { this.loop = loop; } public LoopInfo getLoop() { return loop; } @Override public AType getAttrType() { return AType.LOOP_LABEL; } @Override public String toString() { return "LOOP_LABEL: " + loop; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodBridgeAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.PinnedAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.MethodNode; public class MethodBridgeAttr extends PinnedAttribute { private final MethodNode bridgeMth; public MethodBridgeAttr(MethodNode bridgeMth) { this.bridgeMth = bridgeMth; } public MethodNode getBridgeMth() { return bridgeMth; } @Override public AType getAttrType() { return AType.BRIDGED_BY; } @Override public String toString() { return "BRIDGED_BY: " + bridgeMth; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodInlineAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.List; import java.util.Objects; import jadx.api.plugins.input.data.attributes.PinnedAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; public class MethodInlineAttr extends PinnedAttribute { private static final MethodInlineAttr INLINE_NOT_NEEDED = new MethodInlineAttr(null, null); public static MethodInlineAttr markForInline(MethodNode mth, InsnNode replaceInsn) { Objects.requireNonNull(replaceInsn); List allArgRegs = mth.getAllArgRegs(); int argsCount = allArgRegs.size(); int[] regNums = new int[argsCount]; for (int i = 0; i < argsCount; i++) { RegisterArg reg = allArgRegs.get(i); regNums[i] = reg.getRegNum(); } MethodInlineAttr mia = new MethodInlineAttr(replaceInsn, regNums); mth.addAttr(mia); mth.addDebugComment("Marked for inline"); return mia; } public static MethodInlineAttr inlineNotNeeded(MethodNode mth) { mth.addAttr(INLINE_NOT_NEEDED); return INLINE_NOT_NEEDED; } private final InsnNode insn; /** * Store method arguments register numbers to allow remap registers */ private final int[] argsRegNums; private MethodInlineAttr(InsnNode insn, int[] argsRegNums) { this.insn = insn; this.argsRegNums = argsRegNums; } public boolean notNeeded() { return insn == null; } public InsnNode getInsn() { return insn; } public int[] getArgsRegNums() { return argsRegNums; } @Override public AType getAttrType() { return AType.METHOD_INLINE; } @Override public String toString() { if (notNeeded()) { return "INLINE_NOT_NEEDED"; } return "INLINE: " + insn; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodOverrideAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.List; import java.util.Set; import java.util.SortedSet; import jadx.api.plugins.input.data.attributes.PinnedAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.MethodNode; public class MethodOverrideAttr extends PinnedAttribute { /** * All methods overridden by current method. Current method excluded, empty for base method. */ private final List overrideList; /** * All method nodes from override hierarchy. Current method included. */ private SortedSet relatedMthNodes; private final Set baseMethods; public MethodOverrideAttr(List overrideList, SortedSet relatedMthNodes, Set baseMethods) { this.overrideList = overrideList; this.relatedMthNodes = relatedMthNodes; this.baseMethods = baseMethods; } public List getOverrideList() { return overrideList; } public SortedSet getRelatedMthNodes() { return relatedMthNodes; } public Set getBaseMethods() { return baseMethods; } public void setRelatedMthNodes(SortedSet relatedMthNodes) { this.relatedMthNodes = relatedMthNodes; } @Override public AType getAttrType() { return AType.METHOD_OVERRIDE; } @Override public String toString() { return "METHOD_OVERRIDE: " + getBaseMethods(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodReplaceAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.PinnedAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.MethodNode; /** * Calls of method should be replaced by provided method (used for synthetic methods redirect) */ public class MethodReplaceAttr extends PinnedAttribute { private final MethodNode replaceMth; public MethodReplaceAttr(MethodNode replaceMth) { this.replaceMth = replaceMth; } public MethodNode getReplaceMth() { return replaceMth; } @Override public AType getAttrType() { return AType.METHOD_REPLACE; } @Override public String toString() { return "REPLACED_BY: " + replaceMth; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodThrowsAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.Set; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.PinnedAttribute; import jadx.core.dex.attributes.AType; public class MethodThrowsAttr extends PinnedAttribute { private final Set list; private boolean visited; public MethodThrowsAttr(Set list) { this.list = list; } public boolean isVisited() { return visited; } public void setVisited(boolean visited) { this.visited = visited; } public Set getList() { return list; } @Override public IJadxAttrType getAttrType() { return AType.METHOD_THROWS; } @Override public String toString() { return "THROWS:" + list; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodTypeVarsAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.Collections; import java.util.Set; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.args.ArgType; import static jadx.core.utils.Utils.isEmpty; /** * Set of known type variables at current method */ public class MethodTypeVarsAttr implements IJadxAttribute { private static final MethodTypeVarsAttr EMPTY = new MethodTypeVarsAttr(Collections.emptySet()); public static MethodTypeVarsAttr build(Set typeVars) { if (isEmpty(typeVars)) { return EMPTY; } return new MethodTypeVarsAttr(typeVars); } private final Set typeVars; private MethodTypeVarsAttr(Set typeVars) { this.typeVars = typeVars; } public Set getTypeVars() { return typeVars; } @Override public AType getAttrType() { return AType.METHOD_TYPE_VARS; } @Override public String toString() { if (this == EMPTY) { return "TYPE_VARS: EMPTY"; } return "TYPE_VARS: " + typeVars; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/NotificationAttrNode.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.CommentsLevel; import jadx.api.data.CommentStyle; import jadx.core.codegen.utils.CodeComment; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.ICodeNode; import jadx.core.utils.ErrorsCounter; import jadx.core.utils.Utils; public abstract class NotificationAttrNode extends LineAttrNode implements ICodeNode { public boolean checkCommentsLevel(CommentsLevel required) { return required.filter(this.root().getArgs().getCommentsLevel()); } public void addError(String errStr, Throwable e) { ErrorsCounter.error(this, errStr, e); } public void addWarn(String warn) { ErrorsCounter.warning(this, warn); JadxCommentsAttr.add(this, CommentsLevel.WARN, warn); this.add(AFlag.INCONSISTENT_CODE); } public void addCodeComment(String comment) { addAttr(AType.CODE_COMMENTS, new CodeComment(comment, CommentStyle.LINE)); } public void addCodeComment(String comment, CommentStyle style) { addAttr(AType.CODE_COMMENTS, new CodeComment(comment, style)); } public void addWarnComment(String warn) { JadxCommentsAttr.add(this, CommentsLevel.WARN, warn); } public void addWarnComment(String warn, Throwable exc) { String commentStr = warn + root().getArgs().getCodeNewLineStr() + Utils.getStackTrace(exc); JadxCommentsAttr.add(this, CommentsLevel.WARN, commentStr); } public void addInfoComment(String commentStr) { JadxCommentsAttr.add(this, CommentsLevel.INFO, commentStr); } public void addDebugComment(String commentStr) { JadxCommentsAttr.add(this, CommentsLevel.DEBUG, commentStr); } public CommentsLevel getCommentsLevel() { return this.root().getArgs().getCommentsLevel(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/PhiListAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.ArrayList; import java.util.List; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.RegisterArg; public class PhiListAttr implements IJadxAttribute { private final List list = new ArrayList<>(); @Override public AType getAttrType() { return AType.PHI_LIST; } public List getList() { return list; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("PHI:"); for (PhiInsn phiInsn : list) { RegisterArg resArg = phiInsn.getResult(); if (resArg != null) { sb.append(" r").append(resArg.getRegNum()); } } for (PhiInsn phiInsn : list) { sb.append('\n').append(" ").append(phiInsn); } return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/RegDebugInfoAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.Objects; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.args.ArgType; public class RegDebugInfoAttr implements IJadxAttribute { private final ArgType type; private final String name; public RegDebugInfoAttr(ArgType type, String name) { this.type = type; this.name = name; } public String getName() { return name; } public ArgType getRegType() { return type; } @Override public AType getAttrType() { return AType.REG_DEBUG_INFO; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } RegDebugInfoAttr that = (RegDebugInfoAttr) o; return Objects.equals(type, that.type) && Objects.equals(name, that.name); } @Override public int hashCode() { return Objects.hash(type, name); } @Override public String toString() { return "D('" + name + "' " + type + ')'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/RegionRefAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.IRegion; /** * Region created based on parent instruction */ public class RegionRefAttr implements IJadxAttribute { private final IRegion region; public RegionRefAttr(IRegion region) { this.region = region; } public IRegion getRegion() { return region; } @Override public AType getAttrType() { return AType.REGION_REF; } @Override public String toString() { return "RegionRef:" + region.baseString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/RenameReasonAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttrNode; public class RenameReasonAttr implements IJadxAttribute { public static RenameReasonAttr forNode(AttrNode node) { RenameReasonAttr renameReasonAttr = node.get(AType.RENAME_REASON); if (renameReasonAttr != null) { return renameReasonAttr; } RenameReasonAttr newAttr = new RenameReasonAttr(); node.addAttr(newAttr); return newAttr; } private String description; public RenameReasonAttr() { this.description = ""; } public RenameReasonAttr(String description) { this.description = description; } public RenameReasonAttr(AttrNode node) { RenameReasonAttr renameReasonAttr = node.get(AType.RENAME_REASON); if (renameReasonAttr != null) { this.description = renameReasonAttr.description; } else { this.description = ""; } } public RenameReasonAttr(AttrNode node, boolean notValid, boolean notPrintable) { this(node); if (notValid) { notValid(); } if (notPrintable) { notPrintable(); } } public RenameReasonAttr notValid() { return append("not valid java name"); } public RenameReasonAttr notPrintable() { return append("contains not printable characters"); } public RenameReasonAttr append(String reason) { if (description.isEmpty()) { description += reason; } else { description += " and " + reason; } return this; } public String getDescription() { return description; } @Override public AType getAttrType() { return AType.RENAME_REASON; } @Override public String toString() { return "RENAME_REASON:" + description; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/SkipMethodArgsAttr.java ================================================ package jadx.core.dex.attributes.nodes; import java.util.BitSet; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.attributes.PinnedAttribute; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; public class SkipMethodArgsAttr extends PinnedAttribute { public static void skipArg(MethodNode mth, RegisterArg arg) { int argNum = Utils.indexInListByRef(mth.getArgRegs(), arg); if (argNum == -1) { throw new JadxRuntimeException("Arg not found: " + arg); } skipArg(mth, argNum); } public static void skipArg(MethodNode mth, int argNum) { SkipMethodArgsAttr attr = mth.get(AType.SKIP_MTH_ARGS); if (attr == null) { attr = new SkipMethodArgsAttr(mth); mth.addAttr(attr); } attr.skip(argNum); } public static boolean isSkip(@Nullable MethodNode mth, int argNum) { if (mth == null) { return false; } if (argNum == 0 && mth.contains(AFlag.SKIP_FIRST_ARG)) { return true; } SkipMethodArgsAttr attr = mth.get(AType.SKIP_MTH_ARGS); if (attr == null) { return false; } return attr.isSkip(argNum); } private final BitSet skipArgs; private SkipMethodArgsAttr(MethodNode mth) { this.skipArgs = new BitSet(mth.getMethodInfo().getArgsCount()); } public void skip(int argNum) { skipArgs.set(argNum); } public boolean isSkip(int argNum) { return skipArgs.get(argNum); } public int getSkipCount() { return skipArgs.cardinality(); } @Override public AType getAttrType() { return AType.SKIP_MTH_ARGS; } @Override public String toString() { return "SKIP_MTH_ARGS: " + skipArgs; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/SpecialEdgeAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttrList; import jadx.core.dex.nodes.BlockNode; public class SpecialEdgeAttr implements IJadxAttribute { public enum SpecialEdgeType { BACK_EDGE, CROSS_EDGE } private final SpecialEdgeType type; private final BlockNode start; private final BlockNode end; public SpecialEdgeAttr(SpecialEdgeType type, BlockNode start, BlockNode end) { this.type = type; this.start = start; this.end = end; } public SpecialEdgeType getType() { return type; } public BlockNode getStart() { return start; } public BlockNode getEnd() { return end; } @Override public AType> getAttrType() { return AType.SPECIAL_EDGE; } @Override public String toString() { return type + ": " + start + " -> " + end; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/attributes/nodes/TmpEdgeAttr.java ================================================ package jadx.core.dex.attributes.nodes; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.BlockNode; public class TmpEdgeAttr implements IJadxAttribute { private final BlockNode block; public TmpEdgeAttr(BlockNode block) { this.block = block; } public BlockNode getBlock() { return block; } @Override public IJadxAttrType getAttrType() { return AType.TMP_EDGE; } @Override public String toString() { return "TMP_EDGE: " + block; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java ================================================ package jadx.core.dex.info; import org.intellij.lang.annotations.MagicConstant; import jadx.api.plugins.input.data.AccessFlags; import jadx.core.Consts; import jadx.core.utils.exceptions.JadxRuntimeException; public class AccessInfo { public static final int VISIBILITY_FLAGS = AccessFlags.PUBLIC | AccessFlags.PROTECTED | AccessFlags.PRIVATE; private final int accFlags; public enum AFType { CLASS, FIELD, METHOD } private final AFType type; public AccessInfo(int accessFlags, AFType type) { this.accFlags = accessFlags; this.type = type; } @MagicConstant(valuesFromClass = AccessFlags.class) public boolean containsFlag(int flag) { return (accFlags & flag) != 0; } @MagicConstant(valuesFromClass = AccessFlags.class) public boolean containsFlags(int... flags) { for (int flag : flags) { if ((accFlags & flag) == 0) { return false; } } return true; } public AccessInfo remove(int flag) { if (containsFlag(flag)) { return new AccessInfo(accFlags & ~flag, type); } return this; } public AccessInfo add(int flag) { if (!containsFlag(flag)) { return new AccessInfo(accFlags | flag, type); } return this; } public AccessInfo changeVisibility(int flag) { int currentVisFlags = accFlags & VISIBILITY_FLAGS; if (currentVisFlags == flag) { return this; } int unsetAllVisFlags = accFlags & ~VISIBILITY_FLAGS; return new AccessInfo(unsetAllVisFlags | flag, type); } public AccessInfo getVisibility() { return new AccessInfo(accFlags & VISIBILITY_FLAGS, type); } public boolean isVisibilityWeakerThan(AccessInfo otherAccInfo) { int thisVis = accFlags & VISIBILITY_FLAGS; int otherVis = otherAccInfo.accFlags & VISIBILITY_FLAGS; if (thisVis == otherVis) { return false; } return orderedVisibility(thisVis) < orderedVisibility(otherVis); } private static int orderedVisibility(int flag) { switch (flag) { case AccessFlags.PRIVATE: return 1; case 0: // package-private return 2; case AccessFlags.PROTECTED: return 3; case AccessFlags.PUBLIC: return 4; default: throw new JadxRuntimeException("Unexpected visibility flag: " + flag); } } public boolean isPublic() { return (accFlags & AccessFlags.PUBLIC) != 0; } public boolean isProtected() { return (accFlags & AccessFlags.PROTECTED) != 0; } public boolean isPrivate() { return (accFlags & AccessFlags.PRIVATE) != 0; } public boolean isPackagePrivate() { return (accFlags & VISIBILITY_FLAGS) == 0; } public boolean isAbstract() { return (accFlags & AccessFlags.ABSTRACT) != 0; } public boolean isInterface() { return (accFlags & AccessFlags.INTERFACE) != 0; } public boolean isAnnotation() { return (accFlags & AccessFlags.ANNOTATION) != 0; } public boolean isNative() { return (accFlags & AccessFlags.NATIVE) != 0; } public boolean isStatic() { return (accFlags & AccessFlags.STATIC) != 0; } public boolean isFinal() { return (accFlags & AccessFlags.FINAL) != 0; } public boolean isConstructor() { return (accFlags & AccessFlags.CONSTRUCTOR) != 0; } public boolean isEnum() { return (accFlags & AccessFlags.ENUM) != 0; } public boolean isSynthetic() { return (accFlags & AccessFlags.SYNTHETIC) != 0; } public boolean isBridge() { return (accFlags & AccessFlags.BRIDGE) != 0; } public boolean isVarArgs() { return (accFlags & AccessFlags.VARARGS) != 0; } public boolean isSynchronized() { return (accFlags & (AccessFlags.SYNCHRONIZED | AccessFlags.DECLARED_SYNCHRONIZED)) != 0; } public boolean isTransient() { return (accFlags & AccessFlags.TRANSIENT) != 0; } public boolean isVolatile() { return (accFlags & AccessFlags.VOLATILE) != 0; } public boolean isModuleInfo() { return (accFlags & AccessFlags.MODULE) != 0; } public boolean isData() { return (accFlags & AccessFlags.DATA) != 0; } public AFType getType() { return type; } public String makeString(boolean showHidden) { StringBuilder code = new StringBuilder(); if (isPublic()) { code.append("public "); } if (isPrivate()) { code.append("private "); } if (isProtected()) { code.append("protected "); } if (isStatic()) { code.append("static "); } if (isFinal()) { code.append("final "); } if (isAbstract()) { code.append("abstract "); } if (isNative()) { code.append("native "); } switch (type) { case METHOD: if (isSynchronized()) { code.append("synchronized "); } if (showHidden) { if (isBridge()) { code.append("/* bridge */ "); } if (Consts.DEBUG && isVarArgs()) { code.append("/* varargs */ "); } } break; case FIELD: if (isVolatile()) { code.append("volatile "); } if (isTransient()) { code.append("transient "); } break; case CLASS: if ((accFlags & AccessFlags.STRICT) != 0) { code.append("strict "); } if (showHidden) { if (isData()) { code.append("/* data */ "); } if (isModuleInfo()) { code.append("/* module-info */ "); } if (Consts.DEBUG) { if ((accFlags & AccessFlags.SUPER) != 0) { code.append("/* super */ "); } if ((accFlags & AccessFlags.ENUM) != 0) { code.append("/* enum */ "); } } } break; } if (isSynthetic() && showHidden) { code.append("/* synthetic */ "); } return code.toString(); } public String visibilityName() { if (isPackagePrivate()) { return "package-private"; } if (isPublic()) { return "public"; } if (isPrivate()) { return "private"; } if (isProtected()) { return "protected"; } throw new JadxRuntimeException("Unknown visibility flags: " + getVisibility()); } public int rawValue() { return accFlags; } @Override public String toString() { return "AccessInfo: " + type + " 0x" + Integer.toHexString(accFlags) + " (" + makeString(true) + ')'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/info/ClassAliasInfo.java ================================================ package jadx.core.dex.info; import org.jetbrains.annotations.Nullable; import jadx.core.utils.StringUtils; class ClassAliasInfo { private final String shortName; @Nullable private final String pkg; @Nullable private String fullName; ClassAliasInfo(@Nullable String pkg, String shortName) { if (StringUtils.isEmpty(shortName)) { throw new IllegalArgumentException("Class alias can't be empty"); } this.pkg = pkg; this.shortName = shortName; } @Nullable public String getPkg() { return pkg; } public String getShortName() { return shortName; } @Nullable public String getFullName() { return fullName; } public void setFullName(@Nullable String fullName) { this.fullName = fullName; } @Override public String toString() { return "Alias{" + shortName + ", pkg=" + pkg + ", fullName=" + fullName + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/info/ClassInfo.java ================================================ package jadx.core.dex.info; import java.io.File; import java.util.Objects; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; public final class ClassInfo implements Comparable { private final ArgType type; private String name; @Nullable("for inner classes") private String pkg; private String fullName; @Nullable private ClassInfo parentClass; @Nullable private ClassAliasInfo alias; private ClassInfo(RootNode root, ArgType type, boolean canBeInner) { this.type = type; splitAndApplyNames(root, type, canBeInner); } public static ClassInfo fromType(RootNode root, ArgType type) { ArgType clsType = checkClassType(type); ClassInfo cls = root.getInfoStorage().getCls(clsType); if (cls != null) { return cls; } boolean canBeInner = root.getArgs().isMoveInnerClasses(); ClassInfo newClsInfo = new ClassInfo(root, clsType, canBeInner); return root.getInfoStorage().putCls(newClsInfo); } public static ClassInfo fromName(RootNode root, String clsName) { return fromType(root, ArgType.object(clsName)); } public static ClassInfo fromNameWithoutCache(RootNode root, String fullClsName, boolean canBeInner) { return new ClassInfo(root, ArgType.object(fullClsName), canBeInner); } private static ArgType checkClassType(ArgType type) { if (type == null) { throw new JadxRuntimeException("Null class type"); } if (type.isArray()) { // TODO: check case with method declared in array class like ( clone in int[]) return ArgType.OBJECT; } if (!type.isObject() || type.isGenericType()) { throw new JadxRuntimeException("Not class type: " + type); } if (type.isGeneric()) { return ArgType.object(type.getObject()); } return type; } public void changeShortName(String aliasName) { ClassAliasInfo newAlias; String aliasPkg = getAliasPkg(); if (Objects.equals(name, aliasName) || StringUtils.isEmpty(aliasName)) { if (Objects.equals(getPackage(), aliasPkg)) { newAlias = null; } else { newAlias = new ClassAliasInfo(aliasPkg, name); } } else { newAlias = new ClassAliasInfo(aliasPkg, aliasName); } if (newAlias != null) { fillAliasFullName(newAlias); } this.alias = newAlias; } public void changePkg(String aliasPkg) { if (isInner()) { throw new JadxRuntimeException("Can't change package for inner class: " + this); } if (!Objects.equals(getAliasPkg(), aliasPkg)) { ClassAliasInfo newAlias = new ClassAliasInfo(aliasPkg, getAliasShortName()); fillAliasFullName(newAlias); this.alias = newAlias; } } public void changePkgAndName(String aliasPkg, String aliasShortName) { if (isInner()) { throw new JadxRuntimeException("Can't change package for inner class"); } ClassAliasInfo newAlias = new ClassAliasInfo(aliasPkg, aliasShortName); fillAliasFullName(newAlias); this.alias = newAlias; } private void fillAliasFullName(ClassAliasInfo alias) { if (parentClass == null) { alias.setFullName(makeFullClsName(alias.getPkg(), alias.getShortName(), null, true, false)); } } public String getAliasPkg() { if (isInner()) { return parentClass.getAliasPkg(); } return alias == null ? getPackage() : alias.getPkg(); } public String getAliasShortName() { return alias == null ? getShortName() : alias.getShortName(); } public String getAliasFullName() { if (alias != null) { String aliasFullName = alias.getFullName(); if (aliasFullName == null) { return makeAliasFullName(); } return aliasFullName; } if (parentClass != null && parentClass.hasAlias()) { return makeAliasFullName(); } return getFullName(); } public boolean hasAlias() { if (alias != null && !alias.getShortName().equals(getShortName())) { return true; } return parentClass != null && parentClass.hasAlias(); } public boolean hasAliasPkg() { return !getPackage().equals(getAliasPkg()); } public void removeAlias() { this.alias = null; } private void splitAndApplyNames(RootNode root, ArgType type, boolean canBeInner) { String fullObjectName = type.getObject(); String clsPkg; String clsName; int dot = fullObjectName.lastIndexOf('.'); if (dot == -1) { clsPkg = ""; clsName = fullObjectName; } else { clsPkg = fullObjectName.substring(0, dot); clsName = fullObjectName.substring(dot + 1); } boolean innerCls = false; if (canBeInner) { int sep = clsName.lastIndexOf('$'); if (sep > 0 && sep != clsName.length() - 1) { String parClsName = clsPkg + '.' + clsName.substring(0, sep); if (clsPkg.isEmpty()) { parClsName = clsName.substring(0, sep); } pkg = null; parentClass = fromName(root, parClsName); clsName = clsName.substring(sep + 1); innerCls = true; } } if (!innerCls) { pkg = clsPkg; parentClass = null; } this.name = clsName; this.fullName = makeFullName(); } private static String makeFullClsName(String pkg, String shortName, ClassInfo parentClass, boolean alias, boolean raw) { if (parentClass != null) { String parentFullName; char innerSep = raw ? '$' : '.'; if (alias) { parentFullName = raw ? parentClass.makeAliasRawFullName() : parentClass.getAliasFullName(); } else { parentFullName = raw ? parentClass.makeRawFullName() : parentClass.getFullName(); } return parentFullName + innerSep + shortName; } return pkg.isEmpty() ? shortName : pkg + '.' + shortName; } private String makeFullName() { return makeFullClsName(pkg, name, parentClass, false, false); } public String makeRawFullName() { return makeFullClsName(pkg, name, parentClass, false, true); } public String makeAliasFullName() { return makeFullClsName(getAliasPkg(), getAliasShortName(), parentClass, true, false); } public String makeAliasRawFullName() { return makeFullClsName(getAliasPkg(), getAliasShortName(), parentClass, true, true); } public String getAliasFullPath() { return getAliasPkg().replace('.', File.separatorChar) + File.separatorChar + getAliasNameWithoutPackage().replace('.', '_'); } public String getFullName() { return fullName; } public String getShortName() { return name; } @NotNull public String getPackage() { if (parentClass != null) { return parentClass.getPackage(); } if (pkg == null) { throw new JadxRuntimeException("Package is null for not inner class"); } return pkg; } public boolean isDefaultPackage() { return getPackage().isEmpty(); } public String getRawName() { return type.getObject(); } public String getAliasNameWithoutPackage() { if (parentClass == null) { return getAliasShortName(); } return parentClass.getAliasNameWithoutPackage() + '.' + getAliasShortName(); } @Nullable public ClassInfo getParentClass() { return parentClass; } public ClassInfo getTopParentClass() { if (parentClass != null) { ClassInfo topCls = parentClass.getTopParentClass(); return topCls != null ? topCls : parentClass; } return null; } public boolean isInner() { return parentClass != null; } public void notInner(RootNode root) { splitAndApplyNames(root, type, false); this.parentClass = null; } public void convertToInner(ClassNode parent) { splitAndApplyNames(parent.root(), type, true); this.parentClass = parent.getClassInfo(); } public void updateNames(RootNode root) { splitAndApplyNames(root, type, isInner()); } public ArgType getType() { return type; } @Override public String toString() { return getFullName(); } @Override public int hashCode() { return type.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof ClassInfo) { return type.equals(((ClassInfo) obj).type); } return false; } @Override public int compareTo(@NotNull ClassInfo other) { return getRawName().compareTo(other.getRawName()); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/info/ConstStorage.java ================================================ package jadx.core.dex.info; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.Nullable; import jadx.api.JadxArgs; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.IFieldInfoRef; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.prepare.CollectConstValues; public class ConstStorage { private static final class ValueStorage { private final Map values = new ConcurrentHashMap<>(); private final Set duplicates = new HashSet<>(); public Map getValues() { return values; } public IFieldInfoRef get(Object key) { return values.get(key); } /** * @return true if this value is duplicated */ public boolean put(Object value, IFieldInfoRef fld) { if (duplicates.contains(value)) { values.remove(value); return true; } IFieldInfoRef prev = values.put(value, fld); if (prev != null) { values.remove(value); duplicates.add(value); return true; } return false; } public boolean contains(Object value) { return duplicates.contains(value) || values.containsKey(value); } void removeForCls(ClassNode cls) { values.entrySet().removeIf(entry -> { IFieldInfoRef field = entry.getValue(); if (field instanceof FieldNode) { return ((FieldNode) field).getParentClass().equals(cls); } return false; }); } } private final boolean replaceEnabled; private final ValueStorage globalValues = new ValueStorage(); private final Map classes = new HashMap<>(); private Map resourcesNames = new HashMap<>(); public ConstStorage(JadxArgs args) { this.replaceEnabled = args.isReplaceConsts(); } public void addConstField(FieldNode fld, Object value, boolean isPublic) { if (isPublic) { addGlobalConstField(fld, value); } else { getClsValues(fld.getParentClass()).put(value, fld); } } public void addGlobalConstField(IFieldInfoRef fld, Object value) { globalValues.put(value, fld); } /** * Use method from CollectConstValues class */ @Deprecated public static @Nullable Object getFieldConstValue(FieldNode fld) { return CollectConstValues.getFieldConstValue(fld); } public void removeForClass(ClassNode cls) { classes.remove(cls); globalValues.removeForCls(cls); } private ValueStorage getClsValues(ClassNode cls) { return classes.computeIfAbsent(cls, c -> new ValueStorage()); } public @Nullable IFieldInfoRef getConstField(ClassNode cls, Object value, boolean searchGlobal) { if (!replaceEnabled) { return null; } RootNode root = cls.root(); if (value instanceof Integer) { FieldNode rField = getResourceField((Integer) value, root); if (rField != null) { return rField; } } boolean foundInGlobal = globalValues.contains(value); if (foundInGlobal && !searchGlobal) { return null; } ClassNode current = cls; while (current != null) { ValueStorage classValues = classes.get(current); if (classValues != null) { IFieldInfoRef field = classValues.get(value); if (field != null) { if (foundInGlobal) { return null; } return field; } } ClassInfo parentClass = current.getClassInfo().getParentClass(); if (parentClass == null) { break; } current = root.resolveClass(parentClass); } if (searchGlobal) { return globalValues.get(value); } return null; } @Nullable private FieldNode getResourceField(Integer value, RootNode root) { String str = resourcesNames.get(value); if (str == null) { return null; } ClassNode appResClass = root.getAppResClass(); if (appResClass == null) { return null; } String[] parts = str.split("/", 2); if (parts.length != 2) { return null; } String typeName = parts[0]; String fieldName = parts[1]; for (ClassNode innerClass : appResClass.getInnerClasses()) { if (innerClass.getClassInfo().getShortName().equals(typeName)) { return innerClass.searchFieldByName(fieldName); } } appResClass.addWarn("Not found resource field with id: " + value + ", name: " + str.replace('/', '.')); return null; } public @Nullable IFieldInfoRef getConstFieldByLiteralArg(ClassNode cls, LiteralArg arg) { if (!replaceEnabled) { return null; } PrimitiveType type = arg.getType().getPrimitiveType(); if (type == null) { return null; } long literal = arg.getLiteral(); switch (type) { case BOOLEAN: return getConstField(cls, literal == 1, false); case CHAR: return getConstField(cls, (char) literal, Math.abs(literal) > 10); case BYTE: return getConstField(cls, (byte) literal, Math.abs(literal) > 10); case SHORT: return getConstField(cls, (short) literal, Math.abs(literal) > 100); case INT: return getConstField(cls, (int) literal, Math.abs(literal) > 100); case LONG: return getConstField(cls, literal, Math.abs(literal) > 1000); case FLOAT: float f = Float.intBitsToFloat((int) literal); return getConstField(cls, f, Float.compare(f, 0) == 0); case DOUBLE: double d = Double.longBitsToDouble(literal); return getConstField(cls, d, Double.compare(d, 0) == 0); default: return null; } } public void setResourcesNames(Map resourcesNames) { this.resourcesNames = resourcesNames; } public Map getResourcesNames() { return resourcesNames; } public Map getGlobalConstFields() { return globalValues.getValues(); } public boolean isReplaceEnabled() { return replaceEnabled; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java ================================================ package jadx.core.dex.info; import java.util.Objects; import jadx.api.plugins.input.data.IFieldRef; import jadx.core.codegen.TypeGen; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.IFieldInfoRef; import jadx.core.dex.nodes.RootNode; public final class FieldInfo implements IFieldInfoRef { private final ClassInfo declClass; private final String name; private final ArgType type; private String alias; private FieldInfo(ClassInfo declClass, String name, ArgType type) { this.declClass = declClass; this.name = name; this.type = type; this.alias = name; } public static FieldInfo from(RootNode root, ClassInfo declClass, String name, ArgType type) { FieldInfo field = new FieldInfo(declClass, name, type); return root.getInfoStorage().getField(field); } public static FieldInfo fromRef(RootNode root, IFieldRef fieldRef) { ClassInfo declClass = ClassInfo.fromName(root, fieldRef.getParentClassType()); FieldInfo field = new FieldInfo(declClass, fieldRef.getName(), ArgType.parse(fieldRef.getType())); return root.getInfoStorage().getField(field); } public String getName() { return name; } public ArgType getType() { return type; } public ClassInfo getDeclClass() { return declClass; } public String getAlias() { return alias; } public void setAlias(String alias) { this.alias = alias; } public void removeAlias() { this.alias = name; } public boolean hasAlias() { return !Objects.equals(name, alias); } public String getFullId() { return declClass.getFullName() + '.' + name + ':' + TypeGen.signature(type); } public String getShortId() { return name + ':' + TypeGen.signature(type); } public String getRawFullId() { return declClass.makeRawFullName() + '.' + name + ':' + TypeGen.signature(type); } public boolean equalsNameAndType(FieldInfo other) { return name.equals(other.name) && type.equals(other.type); } @Override public FieldInfo getFieldInfo() { return this; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } FieldInfo fieldInfo = (FieldInfo) o; return name.equals(fieldInfo.name) && type.equals(fieldInfo.type) && declClass.equals(fieldInfo.declClass); } @Override public int hashCode() { int result = name.hashCode(); result = 31 * result + type.hashCode(); result = 31 * result + declClass.hashCode(); return result; } @Override public String toString() { return declClass + "." + name + ' ' + type; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/info/InfoStorage.java ================================================ package jadx.core.dex.info; import java.util.HashMap; import java.util.Map; import org.jetbrains.annotations.Nullable; import jadx.core.dex.instructions.args.ArgType; public class InfoStorage { private final Map classes = new HashMap<>(); private final Map fields = new HashMap<>(); // use only one MethodInfo instance private final Map uniqueMethods = new HashMap<>(); // can contain same method with different ids (from different files) private final Map methods = new HashMap<>(); private final Map packages = new HashMap<>(); public ClassInfo getCls(ArgType type) { return classes.get(type); } public ClassInfo putCls(ClassInfo cls) { synchronized (classes) { ClassInfo prev = classes.put(cls.getType(), cls); return prev == null ? cls : prev; } } public MethodInfo getByUniqId(int id) { synchronized (methods) { return methods.get(id); } } public void putByUniqId(int id, MethodInfo mth) { synchronized (methods) { methods.put(id, mth); } } public MethodInfo putMethod(MethodInfo newMth) { synchronized (uniqueMethods) { MethodInfo prev = uniqueMethods.get(newMth); if (prev != null) { return prev; } uniqueMethods.put(newMth, newMth); return newMth; } } public FieldInfo getField(FieldInfo field) { synchronized (fields) { FieldInfo f = fields.get(field); if (f != null) { return f; } fields.put(field, field); return field; } } public @Nullable PackageInfo getPkg(String fullName) { return packages.get(fullName); } public void putPkg(PackageInfo pkg) { packages.put(pkg.getFullName(), pkg); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/info/MethodInfo.java ================================================ package jadx.core.dex.info; import java.util.List; import java.util.Objects; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.input.data.IMethodRef; import jadx.core.codegen.TypeGen; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; public final class MethodInfo implements Comparable { private final String name; private final ArgType retType; private final List argTypes; private final ClassInfo declClass; private final String shortId; private final String rawFullId; private final int hash; private String alias; private MethodInfo(ClassInfo declClass, String name, List args, ArgType retType) { this.name = name; this.alias = name; this.declClass = declClass; this.argTypes = args; this.retType = retType; this.shortId = makeShortId(name, argTypes, retType); this.rawFullId = declClass.makeRawFullName() + '.' + shortId; this.hash = calcHashCode(); } public static MethodInfo fromRef(RootNode root, IMethodRef methodRef) { InfoStorage infoStorage = root.getInfoStorage(); int uniqId = methodRef.getUniqId(); if (uniqId != 0) { MethodInfo prevMth = infoStorage.getByUniqId(uniqId); if (prevMth != null) { return prevMth; } } methodRef.load(); ArgType parentClsType = ArgType.parse(methodRef.getParentClassType()); ClassInfo parentClass = ClassInfo.fromType(root, parentClsType); ArgType returnType = ArgType.parse(methodRef.getReturnType()); List args = Utils.collectionMap(methodRef.getArgTypes(), ArgType::parse); MethodInfo newMth = new MethodInfo(parentClass, methodRef.getName(), args, returnType); MethodInfo uniqMth = infoStorage.putMethod(newMth); if (uniqId != 0) { infoStorage.putByUniqId(uniqId, uniqMth); } return uniqMth; } public static MethodInfo fromDetails(RootNode root, ClassInfo declClass, String name, List args, ArgType retType) { MethodInfo newMth = new MethodInfo(declClass, name, args, retType); return root.getInfoStorage().putMethod(newMth); } public static MethodInfo fromMethodProto(RootNode root, ClassInfo declClass, String name, IMethodProto proto) { List args = Utils.collectionMap(proto.getArgTypes(), ArgType::parse); ArgType returnType = ArgType.parse(proto.getReturnType()); return fromDetails(root, declClass, name, args, returnType); } public String makeSignature(boolean includeRetType) { return makeSignature(false, includeRetType); } public String makeSignature(boolean useAlias, boolean includeRetType) { return makeShortId(useAlias ? alias : name, argTypes, includeRetType ? retType : null); } public static String makeShortId(String name, List argTypes, @Nullable ArgType retType) { StringBuilder sb = new StringBuilder(); sb.append(name); sb.append('('); for (ArgType arg : argTypes) { sb.append(TypeGen.signature(arg)); } sb.append(')'); if (retType != null) { sb.append(TypeGen.signature(retType)); } return sb.toString(); } public boolean isOverloadedBy(MethodInfo otherMthInfo) { return argTypes.size() == otherMthInfo.argTypes.size() && name.equals(otherMthInfo.name) && !Objects.equals(this.shortId, otherMthInfo.shortId); } public String getName() { return name; } public String getFullName() { return declClass.getFullName() + '.' + name; } public String getAliasFullName() { return declClass.getAliasFullName() + '.' + alias; } public String getFullId() { return declClass.getFullName() + '.' + shortId; } public String getRawFullId() { return rawFullId; } /** * Method name and signature */ public String getShortId() { return shortId; } public ClassInfo getDeclClass() { return declClass; } public ArgType getReturnType() { return retType; } public List getArgumentsTypes() { return argTypes; } public int getArgsCount() { return argTypes.size(); } public boolean isConstructor() { return name.equals(""); } public boolean isClassInit() { return name.equals(""); } public String getAlias() { return alias; } public void setAlias(String alias) { this.alias = alias; } public void removeAlias() { this.alias = name; } public boolean hasAlias() { return !name.equals(alias); } public int calcHashCode() { return shortId.hashCode() + 31 * declClass.hashCode(); } @Override public int hashCode() { return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof MethodInfo)) { return false; } MethodInfo other = (MethodInfo) obj; return shortId.equals(other.shortId) && declClass.equals(other.declClass); } @Override public int compareTo(MethodInfo other) { int clsCmp = declClass.compareTo(other.declClass); if (clsCmp != 0) { return clsCmp; } return shortId.compareTo(other.shortId); } @Override public String toString() { return declClass.getFullName() + '.' + name + '(' + Utils.listToString(argTypes) + "):" + retType; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/info/PackageInfo.java ================================================ package jadx.core.dex.info; import java.util.Objects; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.RootNode; public class PackageInfo { private final @Nullable PackageInfo parentPkg; private final String fullName; private final String name; public static synchronized PackageInfo fromFullPkg(RootNode root, String fullPkg) { PackageInfo existPkg = root.getInfoStorage().getPkg(fullPkg); if (existPkg != null) { return existPkg; } PackageInfo newPkg; int lastDot = fullPkg.lastIndexOf('.'); if (lastDot == -1) { // unknown root pkg newPkg = new PackageInfo(fullPkg, null, fullPkg); } else { PackageInfo parentPkg = fromFullPkg(root, fullPkg.substring(0, lastDot)); newPkg = new PackageInfo(fullPkg, parentPkg, fullPkg.substring(lastDot + 1)); } root.getInfoStorage().putPkg(newPkg); return newPkg; } public static synchronized PackageInfo fromShortName(RootNode root, @Nullable PackageInfo parent, String shortName) { String fullPkg = parent == null ? shortName : parent.getFullName() + '.' + shortName; PackageInfo existPkg = root.getInfoStorage().getPkg(fullPkg); if (existPkg != null) { return existPkg; } PackageInfo newPkg = new PackageInfo(fullPkg, parent, shortName); root.getInfoStorage().putPkg(newPkg); return newPkg; } private PackageInfo(String fullName, @Nullable PackageInfo parentPkg, String name) { this.fullName = fullName; this.parentPkg = parentPkg; this.name = name; } public boolean isRoot() { return parentPkg == null; } public boolean isDefaultPkg() { return fullName.isEmpty(); } public String getFullName() { return fullName; } public @Nullable PackageInfo getParentPkg() { return parentPkg; } public String getName() { return name; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof PackageInfo)) { return false; } return Objects.equals(fullName, ((PackageInfo) o).getFullName()); } @Override public int hashCode() { return fullName.hashCode(); } @Override public String toString() { return fullName; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/ArithNode.java ================================================ package jadx.core.dex.instructions; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; import jadx.core.utils.exceptions.JadxRuntimeException; public class ArithNode extends InsnNode { public static ArithNode build(InsnData insn, ArithOp op, ArgType type) { RegisterArg resArg = InsnArg.reg(insn, 0, fixResultType(op, type)); ArgType argType = fixArgType(op, type); switch (insn.getRegsCount()) { case 2: return new ArithNode(op, resArg, InsnArg.reg(insn, 0, argType), InsnArg.reg(insn, 1, argType)); case 3: return new ArithNode(op, resArg, InsnArg.reg(insn, 1, argType), InsnArg.reg(insn, 2, argType)); default: throw new JadxRuntimeException("Unexpected registers count in " + insn); } } public static ArithNode buildLit(InsnData insn, ArithOp op, ArgType type) { RegisterArg resArg = InsnArg.reg(insn, 0, fixResultType(op, type)); ArgType argType = fixArgType(op, type); LiteralArg litArg = InsnArg.lit(insn, argType); switch (insn.getRegsCount()) { case 1: return new ArithNode(op, resArg, InsnArg.reg(insn, 0, argType), litArg); case 2: return new ArithNode(op, resArg, InsnArg.reg(insn, 1, argType), litArg); default: throw new JadxRuntimeException("Unexpected registers count in " + insn); } } private static ArgType fixResultType(ArithOp op, ArgType type) { if (type == ArgType.INT && op.isBitOp()) { return ArgType.INT_BOOLEAN; } return type; } private static ArgType fixArgType(ArithOp op, ArgType type) { if (type == ArgType.INT && op.isBitOp()) { return ArgType.NARROW_NUMBERS_NO_FLOAT; } return type; } private final ArithOp op; public ArithNode(ArithOp op, @Nullable RegisterArg res, InsnArg a, InsnArg b) { super(InsnType.ARITH, 2); this.op = op; setResult(res); addArg(a); addArg(b); } /** * Create one argument arithmetic instructions (a+=2). * Result is not set (null). * * @param res argument to change */ public static ArithNode oneArgOp(ArithOp op, InsnArg res, InsnArg a) { ArithNode insn = new ArithNode(op, null, res, a); insn.add(AFlag.ARITH_ONEARG); return insn; } public ArithOp getOp() { return op; } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof ArithNode) || !super.isSame(obj)) { return false; } ArithNode other = (ArithNode) obj; return op == other.op && isSameLiteral(other); } private boolean isSameLiteral(ArithNode other) { InsnArg thisSecond = getArg(1); InsnArg otherSecond = other.getArg(1); if (thisSecond.isLiteral() != otherSecond.isLiteral()) { return false; } if (!thisSecond.isLiteral()) { // both not literals return true; } // both literals long thisLit = ((LiteralArg) thisSecond).getLiteral(); long otherLit = ((LiteralArg) otherSecond).getLiteral(); return thisLit == otherLit; } @Override public InsnNode copy() { ArithNode copy = new ArithNode(op, null, getArg(0).duplicate(), getArg(1).duplicate()); return copyCommonParams(copy); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(InsnUtils.formatOffset(offset)); sb.append(": ARITH "); if (contains(AFlag.ARITH_ONEARG)) { sb.append(getArg(0)).append(' ').append(op.getSymbol()).append("= ").append(getArg(1)); } else { RegisterArg result = getResult(); if (result != null) { sb.append(result).append(" = "); } sb.append(getArg(0)).append(' ').append(op.getSymbol()).append(' ').append(getArg(1)); } appendAttributes(sb); return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/ArithOp.java ================================================ package jadx.core.dex.instructions; public enum ArithOp { ADD("+"), SUB("-"), MUL("*"), DIV("/"), REM("%"), AND("&"), OR("|"), XOR("^"), SHL("<<"), SHR(">>"), USHR(">>>"); private final String symbol; ArithOp(String symbol) { this.symbol = symbol; } public String getSymbol() { return this.symbol; } public boolean isBitOp() { switch (this) { case AND: case OR: case XOR: return true; default: return false; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/BaseInvokeNode.java ================================================ package jadx.core.dex.instructions; import org.jetbrains.annotations.Nullable; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.InsnNode; public abstract class BaseInvokeNode extends InsnNode { public BaseInvokeNode(InsnType type, int argsCount) { super(type, argsCount); } public abstract MethodInfo getCallMth(); @Nullable public abstract InsnArg getInstanceArg(); public abstract boolean isStaticCall(); /** * Return offset to match method args from {@link #getCallMth()} */ public abstract int getFirstArgOffset(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/ConstClassNode.java ================================================ package jadx.core.dex.instructions; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.InsnNode; public final class ConstClassNode extends InsnNode { private final ArgType clsType; public ConstClassNode(ArgType clsType) { super(InsnType.CONST_CLASS, 0); this.clsType = clsType; } public ArgType getClsType() { return clsType; } @Override public InsnNode copy() { return copyCommonParams(new ConstClassNode(clsType)); } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof ConstClassNode) || !super.isSame(obj)) { return false; } ConstClassNode other = (ConstClassNode) obj; return clsType.equals(other.clsType); } @Override public String toString() { return super.toString() + ' ' + clsType + ".class"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/ConstStringNode.java ================================================ package jadx.core.dex.instructions; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.StringUtils; public final class ConstStringNode extends InsnNode { private final String str; public ConstStringNode(String str) { super(InsnType.CONST_STR, 0); this.str = str; } public String getString() { return str; } @Override public InsnNode copy() { return copyCommonParams(new ConstStringNode(str)); } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof ConstStringNode) || !super.isSame(obj)) { return false; } ConstStringNode other = (ConstStringNode) obj; return str.equals(other.str); } @Override public String toString() { return super.toString() + ' ' + StringUtils.getInstance().unescapeString(str); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/FillArrayData.java ================================================ package jadx.core.dex.instructions; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import jadx.api.plugins.input.insns.custom.IArrayPayload; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.exceptions.JadxRuntimeException; public final class FillArrayData extends InsnNode { private static final ArgType ONE_BYTE_TYPE = ArgType.unknown(PrimitiveType.BYTE, PrimitiveType.BOOLEAN); private static final ArgType TWO_BYTES_TYPE = ArgType.unknown(PrimitiveType.SHORT, PrimitiveType.CHAR); private static final ArgType FOUR_BYTES_TYPE = ArgType.unknown(PrimitiveType.INT, PrimitiveType.FLOAT); private static final ArgType EIGHT_BYTES_TYPE = ArgType.unknown(PrimitiveType.LONG, PrimitiveType.DOUBLE); private final Object data; private final int size; private final int elemSize; private ArgType elemType; public FillArrayData(IArrayPayload payload) { this(payload.getData(), payload.getSize(), payload.getElementSize()); } private FillArrayData(Object data, int size, int elemSize) { super(InsnType.FILL_ARRAY_DATA, 0); this.data = data; this.size = size; this.elemSize = elemSize; this.elemType = getElementType(elemSize); } private static ArgType getElementType(int elementWidthUnit) { switch (elementWidthUnit) { case 1: case 0: return ONE_BYTE_TYPE; case 2: return TWO_BYTES_TYPE; case 4: return FOUR_BYTES_TYPE; case 8: return EIGHT_BYTES_TYPE; default: throw new JadxRuntimeException("Unknown array element width: " + elementWidthUnit); } } public Object getData() { return data; } public int getSize() { return size; } public ArgType getElementType() { return elemType; } public List getLiteralArgs(ArgType type) { List list = new ArrayList<>(size); Object array = data; switch (elemSize) { case 1: for (byte b : (byte[]) array) { list.add(InsnArg.lit(b, type)); } break; case 2: for (short b : (short[]) array) { list.add(InsnArg.lit(b, type)); } break; case 4: for (int b : (int[]) array) { list.add(InsnArg.lit(b, type)); } break; case 8: for (long b : (long[]) array) { list.add(InsnArg.lit(b, type)); } break; default: throw new JadxRuntimeException("Unknown type: " + data.getClass() + ", expected: " + type); } return list; } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof FillArrayData) || !super.isSame(obj)) { return false; } FillArrayData other = (FillArrayData) obj; return elemType.equals(other.elemType) && data == other.data; } @Override public InsnNode copy() { FillArrayData copy = new FillArrayData(data, size, elemSize); copy.elemType = this.elemType; return copyCommonParams(copy); } public String dataToString() { switch (elemSize) { case 1: return Arrays.toString((byte[]) data); case 2: return Arrays.toString((short[]) data); case 4: return Arrays.toString((int[]) data); case 8: return Arrays.toString((long[]) data); default: return "?"; } } @Override public String toString() { return super.toString() + ", data: " + dataToString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/FillArrayInsn.java ================================================ package jadx.core.dex.instructions; import java.util.List; import java.util.Objects; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.nodes.InsnNode; public final class FillArrayInsn extends InsnNode { private final int target; private FillArrayData arrayData; public FillArrayInsn(InsnArg arg, int target) { super(InsnType.FILL_ARRAY, 1); this.target = target; addArg(arg); } public int getTarget() { return target; } public void setArrayData(FillArrayData arrayData) { this.arrayData = arrayData; } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof FillArrayInsn) || !super.isSame(obj)) { return false; } FillArrayInsn other = (FillArrayInsn) obj; return Objects.equals(arrayData, other.arrayData); } @Override public InsnNode copy() { FillArrayInsn copy = new FillArrayInsn(getArg(0), target); return copyCommonParams(copy); } @Override public String toString() { return super.toString() + ", data: " + arrayData; } public int getSize() { return arrayData.getSize(); } public ArgType getElementType() { return arrayData.getElementType(); } public List getLiteralArgs(ArgType elType) { return arrayData.getLiteralArgs(elType); } public String dataToString() { return Objects.toString(arrayData); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/FilledNewArrayNode.java ================================================ package jadx.core.dex.instructions; import org.jetbrains.annotations.NotNull; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.InsnNode; public class FilledNewArrayNode extends InsnNode { private final ArgType elemType; public FilledNewArrayNode(@NotNull ArgType elemType, int size) { super(InsnType.FILLED_NEW_ARRAY, size); this.elemType = elemType; } public ArgType getElemType() { return elemType; } public ArgType getArrayType() { return ArgType.array(elemType); } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof FilledNewArrayNode) || !super.isSame(obj)) { return false; } FilledNewArrayNode other = (FilledNewArrayNode) obj; return elemType == other.elemType; } @Override public InsnNode copy() { return copyCommonParams(new FilledNewArrayNode(elemType, getArgsCount())); } @Override public String toString() { return super.toString() + " elemType: " + elemType; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/GotoNode.java ================================================ package jadx.core.dex.instructions; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; public class GotoNode extends TargetInsnNode { protected final int target; public GotoNode(int target) { this(InsnType.GOTO, target, 0); } protected GotoNode(InsnType type, int target, int argsCount) { super(type, argsCount); this.target = target; } public int getTarget() { return target; } @Override public InsnNode copy() { return copyCommonParams(new GotoNode(target)); } @Override public String toString() { return super.toString() + "-> " + InsnUtils.formatOffset(target); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/IfNode.java ================================================ package jadx.core.dex.instructions; import java.util.List; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; import static jadx.core.utils.BlockUtils.getBlockByOffset; import static jadx.core.utils.BlockUtils.selectOther; public class IfNode extends GotoNode { protected IfOp op; private BlockNode thenBlock; private BlockNode elseBlock; public IfNode(InsnData insn, IfOp op) { super(InsnType.IF, insn.getTarget(), 2); this.op = op; ArgType argType = narrowTypeByOp(op); addArg(InsnArg.reg(insn, 0, argType)); if (insn.getRegsCount() == 1) { addArg(InsnArg.lit(0, argType)); } else { addArg(InsnArg.reg(insn, 1, argType)); } } public IfNode(IfOp op, int targetOffset, InsnArg arg1, InsnArg arg2) { this(op, targetOffset); addArg(arg1); addArg(arg2); } private IfNode(IfOp op, int targetOffset) { super(InsnType.IF, targetOffset, 2); this.op = op; } // change default types priority private static final ArgType WIDE_TYPE = ArgType.unknown( PrimitiveType.INT, PrimitiveType.BOOLEAN, PrimitiveType.OBJECT, PrimitiveType.ARRAY, PrimitiveType.BYTE, PrimitiveType.SHORT, PrimitiveType.CHAR); private static final ArgType NUMBERS_TYPE = ArgType.unknown( PrimitiveType.INT, PrimitiveType.BYTE, PrimitiveType.SHORT, PrimitiveType.CHAR); private static ArgType narrowTypeByOp(IfOp op) { if (op == IfOp.EQ || op == IfOp.NE) { return WIDE_TYPE; } return NUMBERS_TYPE; } public IfOp getOp() { return op; } public void invertCondition() { op = op.invert(); BlockNode tmp = thenBlock; thenBlock = elseBlock; elseBlock = tmp; } /** * Change 'a != false' to 'a == true' */ public void normalize() { if (getOp() == IfOp.NE && getArg(1).isFalse()) { changeCondition(IfOp.EQ, getArg(0), LiteralArg.litTrue()); } } public void changeCondition(IfOp op, InsnArg arg1, InsnArg arg2) { this.op = op; setArg(0, arg1); setArg(1, arg2); } @Override public void initBlocks(BlockNode curBlock) { List successors = curBlock.getSuccessors(); thenBlock = getBlockByOffset(target, successors); if (successors.size() == 1) { elseBlock = thenBlock; } else { elseBlock = selectOther(thenBlock, successors); } } @Override public boolean replaceTargetBlock(BlockNode origin, BlockNode replace) { boolean replaced = false; if (thenBlock == origin) { thenBlock = replace; replaced = true; } if (elseBlock == origin) { elseBlock = replace; replaced = true; } return replaced; } public BlockNode getThenBlock() { return thenBlock; } public BlockNode getElseBlock() { return elseBlock; } @Override public int getTarget() { return thenBlock == null ? target : thenBlock.getStartOffset(); } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof IfNode) || !super.isSame(obj)) { return false; } IfNode other = (IfNode) obj; return op == other.op; } @Override public InsnNode copy() { IfNode copy = new IfNode(op, target); copy.thenBlock = thenBlock; copy.elseBlock = elseBlock; return copyCommonParams(copy); } @Override public String toString() { return InsnUtils.formatOffset(offset) + ": " + InsnUtils.insnTypeToString(insnType) + getArg(0) + ' ' + op.getSymbol() + ' ' + getArg(1) + " -> " + (thenBlock != null ? thenBlock : InsnUtils.formatOffset(target)) + attributesString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/IfOp.java ================================================ package jadx.core.dex.instructions; import jadx.core.utils.exceptions.JadxRuntimeException; public enum IfOp { EQ("=="), NE("!="), LT("<"), LE("<="), GT(">"), GE(">="); private final String symbol; IfOp(String symbol) { this.symbol = symbol; } public String getSymbol() { return symbol; } public IfOp invert() { switch (this) { case EQ: return NE; case NE: return EQ; case LT: return GE; case LE: return GT; case GT: return LE; case GE: return LT; default: throw new JadxRuntimeException("Unknown if operations type: " + this); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/IndexInsnNode.java ================================================ package jadx.core.dex.instructions; import java.util.Objects; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; import jadx.core.utils.Utils; public class IndexInsnNode extends InsnNode { private Object index; public IndexInsnNode(InsnType type, Object index, int argCount) { super(type, argCount); this.index = index; } public Object getIndex() { return index; } public void updateIndex(Object index) { this.index = index; } public ArgType getIndexAsType() { return (ArgType) index; } @Override public IndexInsnNode copy() { return copyCommonParams(new IndexInsnNode(insnType, index, getArgsCount())); } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof IndexInsnNode) || !super.isSame(obj)) { return false; } IndexInsnNode other = (IndexInsnNode) obj; return Objects.equals(index, other.index); } @Override public String toString() { switch (insnType) { case CAST: case CHECK_CAST: StringBuilder sb = new StringBuilder(); sb.append(InsnUtils.formatOffset(offset)).append(": "); sb.append(insnType).append(' '); if (getResult() != null) { sb.append(getResult()).append(" = "); } sb.append('(').append(InsnUtils.indexToString(index)).append(") "); sb.append(Utils.listToString(getArguments())); return sb.toString(); default: return super.toString() + ' ' + InsnUtils.indexToString(index); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java ================================================ package jadx.core.dex.instructions; import java.util.List; import java.util.Objects; import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.insns.InsnData; import jadx.api.plugins.input.insns.custom.IArrayPayload; import jadx.api.plugins.input.insns.custom.ICustomPayload; import jadx.api.plugins.input.insns.custom.ISwitchPayload; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; import jadx.core.dex.attributes.nodes.CodeFeaturesAttr.CodeFeature; import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.java.JsrNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.DecodeException; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.input.InsnDataUtils; public class InsnDecoder { private final MethodNode method; private final RootNode root; public InsnDecoder(MethodNode mthNode) { this.method = mthNode; this.root = method.root(); } public InsnNode[] process(ICodeReader codeReader) { InsnNode[] instructions = new InsnNode[codeReader.getUnitsCount()]; codeReader.visitInstructions(rawInsn -> { int offset = rawInsn.getOffset(); InsnNode insn; try { rawInsn.decode(); insn = decode(rawInsn); } catch (Exception e) { boolean mthWithErrors = method.contains(AType.JADX_ERROR); method.addError("Failed to decode insn: " + rawInsn, e); if (mthWithErrors) { // second error in this method => abort processing throw new JadxRuntimeException("Failed to decode insn: " + rawInsn, e); } insn = new InsnNode(InsnType.NOP, 0); insn.addAttr(AType.JADX_ERROR, new JadxError("decode failed: " + e.getMessage(), e)); } insn.setOffset(offset); instructions[offset] = insn; }); return instructions; } protected InsnNode decode(InsnData insn) throws DecodeException { switch (insn.getOpcode()) { case NOP: return new InsnNode(InsnType.NOP, 0); // move-result will be process in invoke and filled-new-array instructions case MOVE_RESULT: return insn(InsnType.MOVE_RESULT, InsnArg.reg(insn, 0, ArgType.UNKNOWN)); case CONST: LiteralArg narrowLitArg = InsnArg.lit(insn, ArgType.NARROW); return insn(InsnType.CONST, InsnArg.reg(insn, 0, narrowLitArg.getType()), narrowLitArg); case CONST_WIDE: LiteralArg wideLitArg = InsnArg.lit(insn, ArgType.WIDE); return insn(InsnType.CONST, InsnArg.reg(insn, 0, wideLitArg.getType()), wideLitArg); case CONST_STRING: InsnNode constStrInsn = new ConstStringNode(insn.getIndexAsString()); constStrInsn.setResult(InsnArg.reg(insn, 0, ArgType.STRING)); return constStrInsn; case CONST_CLASS: { ArgType clsType = ArgType.parse(insn.getIndexAsType()); InsnNode constClsInsn = new ConstClassNode(clsType); constClsInsn.setResult(InsnArg.reg(insn, 0, ArgType.generic(Consts.CLASS_CLASS, clsType))); return constClsInsn; } case MOVE: return insn(InsnType.MOVE, InsnArg.reg(insn, 0, ArgType.NARROW), InsnArg.reg(insn, 1, ArgType.NARROW)); case MOVE_MULTI: int len = insn.getRegsCount(); InsnNode mmv = new InsnNode(InsnType.MOVE_MULTI, len); for (int i = 0; i < len; i++) { mmv.addArg(InsnArg.reg(insn, i, ArgType.UNKNOWN)); } return mmv; case MOVE_WIDE: return insn(InsnType.MOVE, InsnArg.reg(insn, 0, ArgType.WIDE), InsnArg.reg(insn, 1, ArgType.WIDE)); case MOVE_OBJECT: return insn(InsnType.MOVE, InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT), InsnArg.reg(insn, 1, ArgType.UNKNOWN_OBJECT)); case ADD_INT: return arith(insn, ArithOp.ADD, ArgType.INT); case ADD_DOUBLE: return arith(insn, ArithOp.ADD, ArgType.DOUBLE); case ADD_FLOAT: return arith(insn, ArithOp.ADD, ArgType.FLOAT); case ADD_LONG: return arith(insn, ArithOp.ADD, ArgType.LONG); case ADD_INT_LIT: return arithLit(insn, ArithOp.ADD, ArgType.INT); case SUB_INT: return arith(insn, ArithOp.SUB, ArgType.INT); case RSUB_INT: return new ArithNode(ArithOp.SUB, InsnArg.reg(insn, 0, ArgType.INT), InsnArg.lit(insn, ArgType.INT), InsnArg.reg(insn, 1, ArgType.INT)); case SUB_LONG: return arith(insn, ArithOp.SUB, ArgType.LONG); case SUB_FLOAT: return arith(insn, ArithOp.SUB, ArgType.FLOAT); case SUB_DOUBLE: return arith(insn, ArithOp.SUB, ArgType.DOUBLE); case MUL_INT: return arith(insn, ArithOp.MUL, ArgType.INT); case MUL_DOUBLE: return arith(insn, ArithOp.MUL, ArgType.DOUBLE); case MUL_FLOAT: return arith(insn, ArithOp.MUL, ArgType.FLOAT); case MUL_LONG: return arith(insn, ArithOp.MUL, ArgType.LONG); case MUL_INT_LIT: return arithLit(insn, ArithOp.MUL, ArgType.INT); case DIV_INT: return arith(insn, ArithOp.DIV, ArgType.INT); case REM_INT: return arith(insn, ArithOp.REM, ArgType.INT); case REM_LONG: return arith(insn, ArithOp.REM, ArgType.LONG); case REM_FLOAT: return arith(insn, ArithOp.REM, ArgType.FLOAT); case REM_DOUBLE: return arith(insn, ArithOp.REM, ArgType.DOUBLE); case DIV_DOUBLE: return arith(insn, ArithOp.DIV, ArgType.DOUBLE); case DIV_FLOAT: return arith(insn, ArithOp.DIV, ArgType.FLOAT); case DIV_LONG: return arith(insn, ArithOp.DIV, ArgType.LONG); case DIV_INT_LIT: return arithLit(insn, ArithOp.DIV, ArgType.INT); case REM_INT_LIT: return arithLit(insn, ArithOp.REM, ArgType.INT); case AND_INT: return arith(insn, ArithOp.AND, ArgType.INT); case AND_INT_LIT: return arithLit(insn, ArithOp.AND, ArgType.INT); case XOR_INT_LIT: return arithLit(insn, ArithOp.XOR, ArgType.INT); case AND_LONG: return arith(insn, ArithOp.AND, ArgType.LONG); case OR_INT: return arith(insn, ArithOp.OR, ArgType.INT); case OR_INT_LIT: return arithLit(insn, ArithOp.OR, ArgType.INT); case XOR_INT: return arith(insn, ArithOp.XOR, ArgType.INT); case OR_LONG: return arith(insn, ArithOp.OR, ArgType.LONG); case XOR_LONG: return arith(insn, ArithOp.XOR, ArgType.LONG); case USHR_INT: return arith(insn, ArithOp.USHR, ArgType.INT); case USHR_LONG: return arith(insn, ArithOp.USHR, ArgType.LONG); case SHL_INT: return arith(insn, ArithOp.SHL, ArgType.INT); case SHL_LONG: return arith(insn, ArithOp.SHL, ArgType.LONG); case SHR_INT: return arith(insn, ArithOp.SHR, ArgType.INT); case SHR_LONG: return arith(insn, ArithOp.SHR, ArgType.LONG); case SHL_INT_LIT: return arithLit(insn, ArithOp.SHL, ArgType.INT); case SHR_INT_LIT: return arithLit(insn, ArithOp.SHR, ArgType.INT); case USHR_INT_LIT: return arithLit(insn, ArithOp.USHR, ArgType.INT); case NEG_INT: return neg(insn, ArgType.INT); case NEG_LONG: return neg(insn, ArgType.LONG); case NEG_FLOAT: return neg(insn, ArgType.FLOAT); case NEG_DOUBLE: return neg(insn, ArgType.DOUBLE); case NOT_INT: return not(insn, ArgType.INT); case NOT_LONG: return not(insn, ArgType.LONG); case INT_TO_BYTE: return cast(insn, ArgType.INT, ArgType.BYTE); case INT_TO_CHAR: return cast(insn, ArgType.INT, ArgType.CHAR); case INT_TO_SHORT: return cast(insn, ArgType.INT, ArgType.SHORT); case INT_TO_FLOAT: return cast(insn, ArgType.INT, ArgType.FLOAT); case INT_TO_DOUBLE: return cast(insn, ArgType.INT, ArgType.DOUBLE); case INT_TO_LONG: return cast(insn, ArgType.INT, ArgType.LONG); case FLOAT_TO_INT: return cast(insn, ArgType.FLOAT, ArgType.INT); case FLOAT_TO_DOUBLE: return cast(insn, ArgType.FLOAT, ArgType.DOUBLE); case FLOAT_TO_LONG: return cast(insn, ArgType.FLOAT, ArgType.LONG); case DOUBLE_TO_INT: return cast(insn, ArgType.DOUBLE, ArgType.INT); case DOUBLE_TO_FLOAT: return cast(insn, ArgType.DOUBLE, ArgType.FLOAT); case DOUBLE_TO_LONG: return cast(insn, ArgType.DOUBLE, ArgType.LONG); case LONG_TO_INT: return cast(insn, ArgType.LONG, ArgType.INT); case LONG_TO_FLOAT: return cast(insn, ArgType.LONG, ArgType.FLOAT); case LONG_TO_DOUBLE: return cast(insn, ArgType.LONG, ArgType.DOUBLE); case IF_EQ: case IF_EQZ: return new IfNode(insn, IfOp.EQ); case IF_NE: case IF_NEZ: return new IfNode(insn, IfOp.NE); case IF_GT: case IF_GTZ: return new IfNode(insn, IfOp.GT); case IF_GE: case IF_GEZ: return new IfNode(insn, IfOp.GE); case IF_LT: case IF_LTZ: return new IfNode(insn, IfOp.LT); case IF_LE: case IF_LEZ: return new IfNode(insn, IfOp.LE); case CMP_LONG: return cmp(insn, InsnType.CMP_L, ArgType.LONG); case CMPL_FLOAT: return cmp(insn, InsnType.CMP_L, ArgType.FLOAT); case CMPL_DOUBLE: return cmp(insn, InsnType.CMP_L, ArgType.DOUBLE); case CMPG_FLOAT: return cmp(insn, InsnType.CMP_G, ArgType.FLOAT); case CMPG_DOUBLE: return cmp(insn, InsnType.CMP_G, ArgType.DOUBLE); case GOTO: return new GotoNode(insn.getTarget()); case JAVA_JSR: method.add(AFlag.RESOLVE_JAVA_JSR); JsrNode jsr = new JsrNode(insn.getTarget()); jsr.setResult(InsnArg.reg(insn, 0, ArgType.UNKNOWN_INT)); return jsr; case JAVA_RET: method.add(AFlag.RESOLVE_JAVA_JSR); return insn(InsnType.JAVA_RET, null, InsnArg.reg(insn, 0, ArgType.UNKNOWN_INT)); case THROW: return insn(InsnType.THROW, null, InsnArg.reg(insn, 0, ArgType.THROWABLE)); case MOVE_EXCEPTION: method.add(AFlag.COMPUTE_POST_DOM); // Post dominators required for try/catch block processing return insn(InsnType.MOVE_EXCEPTION, InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT_NO_ARRAY)); case RETURN_VOID: return new InsnNode(InsnType.RETURN, 0); case RETURN: return insn(InsnType.RETURN, null, InsnArg.reg(insn, 0, method.getReturnType())); case INSTANCE_OF: InsnNode instInsn = new IndexInsnNode(InsnType.INSTANCE_OF, ArgType.parse(insn.getIndexAsType()), 1); instInsn.setResult(InsnArg.reg(insn, 0, ArgType.BOOLEAN)); instInsn.addArg(InsnArg.reg(insn, 1, ArgType.UNKNOWN_OBJECT)); return instInsn; case CHECK_CAST: ArgType castType = ArgType.parse(insn.getIndexAsType()); InsnNode checkCastInsn = new IndexInsnNode(InsnType.CHECK_CAST, castType, 1); checkCastInsn.setResult(InsnArg.reg(insn, 0, castType)); checkCastInsn.addArg(InsnArg.reg(insn, insn.getRegsCount() == 2 ? 1 : 0, ArgType.UNKNOWN_OBJECT)); return checkCastInsn; case IGET: FieldInfo igetFld = FieldInfo.fromRef(root, insn.getIndexAsField()); InsnNode igetInsn = new IndexInsnNode(InsnType.IGET, igetFld, 1); igetInsn.setResult(InsnArg.reg(insn, 0, tryResolveFieldType(igetFld))); igetInsn.addArg(InsnArg.reg(insn, 1, igetFld.getDeclClass().getType())); return igetInsn; case IPUT: FieldInfo iputFld = FieldInfo.fromRef(root, insn.getIndexAsField()); InsnNode iputInsn = new IndexInsnNode(InsnType.IPUT, iputFld, 2); iputInsn.addArg(InsnArg.reg(insn, 0, tryResolveFieldType(iputFld))); iputInsn.addArg(InsnArg.reg(insn, 1, iputFld.getDeclClass().getType())); return iputInsn; case SGET: FieldInfo sgetFld = FieldInfo.fromRef(root, insn.getIndexAsField()); InsnNode sgetInsn = new IndexInsnNode(InsnType.SGET, sgetFld, 0); sgetInsn.setResult(InsnArg.reg(insn, 0, tryResolveFieldType(sgetFld))); return sgetInsn; case SPUT: FieldInfo sputFld = FieldInfo.fromRef(root, insn.getIndexAsField()); InsnNode sputInsn = new IndexInsnNode(InsnType.SPUT, sputFld, 1); sputInsn.addArg(InsnArg.reg(insn, 0, tryResolveFieldType(sputFld))); return sputInsn; case ARRAY_LENGTH: InsnNode arrLenInsn = new InsnNode(InsnType.ARRAY_LENGTH, 1); arrLenInsn.setResult(InsnArg.reg(insn, 0, ArgType.INT)); arrLenInsn.addArg(InsnArg.reg(insn, 1, ArgType.array(ArgType.UNKNOWN))); return arrLenInsn; case AGET: return arrayGet(insn, ArgType.INT_FLOAT, ArgType.NARROW_NUMBERS_NO_BOOL); case AGET_BOOLEAN: return arrayGet(insn, ArgType.BOOLEAN); case AGET_BYTE: return arrayGet(insn, ArgType.BYTE, ArgType.NARROW_INTEGRAL); case AGET_BYTE_BOOLEAN: return arrayGet(insn, ArgType.BYTE_BOOLEAN); case AGET_CHAR: return arrayGet(insn, ArgType.CHAR); case AGET_SHORT: return arrayGet(insn, ArgType.SHORT); case AGET_WIDE: return arrayGet(insn, ArgType.WIDE); case AGET_OBJECT: return arrayGet(insn, ArgType.UNKNOWN_OBJECT); case APUT: return arrayPut(insn, ArgType.INT_FLOAT, ArgType.NARROW_NUMBERS_NO_BOOL); case APUT_BOOLEAN: return arrayPut(insn, ArgType.BOOLEAN); case APUT_BYTE: return arrayPut(insn, ArgType.BYTE); case APUT_BYTE_BOOLEAN: return arrayPut(insn, ArgType.BYTE_BOOLEAN); case APUT_CHAR: return arrayPut(insn, ArgType.CHAR); case APUT_SHORT: return arrayPut(insn, ArgType.SHORT); case APUT_WIDE: return arrayPut(insn, ArgType.WIDE); case APUT_OBJECT: return arrayPut(insn, ArgType.UNKNOWN_OBJECT); case INVOKE_STATIC: return invoke(insn, InvokeType.STATIC, false); case INVOKE_STATIC_RANGE: return invoke(insn, InvokeType.STATIC, true); case INVOKE_DIRECT: return invoke(insn, InvokeType.DIRECT, false); case INVOKE_INTERFACE: return invoke(insn, InvokeType.INTERFACE, false); case INVOKE_SUPER: return invoke(insn, InvokeType.SUPER, false); case INVOKE_VIRTUAL: return invoke(insn, InvokeType.VIRTUAL, false); case INVOKE_CUSTOM: return invokeCustom(insn, false); case INVOKE_SPECIAL: return invokeSpecial(insn); case INVOKE_POLYMORPHIC: return invokePolymorphic(insn, false); case INVOKE_DIRECT_RANGE: return invoke(insn, InvokeType.DIRECT, true); case INVOKE_INTERFACE_RANGE: return invoke(insn, InvokeType.INTERFACE, true); case INVOKE_SUPER_RANGE: return invoke(insn, InvokeType.SUPER, true); case INVOKE_VIRTUAL_RANGE: return invoke(insn, InvokeType.VIRTUAL, true); case INVOKE_CUSTOM_RANGE: return invokeCustom(insn, true); case INVOKE_POLYMORPHIC_RANGE: return invokePolymorphic(insn, true); case NEW_INSTANCE: ArgType clsType = ArgType.parse(insn.getIndexAsType()); IndexInsnNode newInstInsn = new IndexInsnNode(InsnType.NEW_INSTANCE, clsType, 0); newInstInsn.setResult(InsnArg.reg(insn, 0, clsType)); return newInstInsn; case NEW_ARRAY: return makeNewArray(insn); case FILL_ARRAY_DATA: return new FillArrayInsn(InsnArg.reg(insn, 0, ArgType.UNKNOWN_ARRAY), insn.getTarget()); case FILL_ARRAY_DATA_PAYLOAD: return new FillArrayData((IArrayPayload) Objects.requireNonNull(insn.getPayload())); case FILLED_NEW_ARRAY: return filledNewArray(insn, false); case FILLED_NEW_ARRAY_RANGE: return filledNewArray(insn, true); case PACKED_SWITCH: return makeSwitch(insn, true); case SPARSE_SWITCH: return makeSwitch(insn, false); case PACKED_SWITCH_PAYLOAD: case SPARSE_SWITCH_PAYLOAD: return new SwitchData((ISwitchPayload) insn.getPayload()); case MONITOR_ENTER: return insn(InsnType.MONITOR_ENTER, null, InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT)); case MONITOR_EXIT: return insn(InsnType.MONITOR_EXIT, null, InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT)); default: throw new DecodeException("Unknown instruction: '" + insn + '\''); } } private SwitchInsn makeSwitch(InsnData insn, boolean packed) { SwitchInsn swInsn = new SwitchInsn(InsnArg.reg(insn, 0, ArgType.NARROW_INTEGRAL), insn.getTarget(), packed); ICustomPayload payload = insn.getPayload(); if (payload != null) { swInsn.attachSwitchData(new SwitchData((ISwitchPayload) payload), insn.getTarget()); } method.add(AFlag.COMPUTE_POST_DOM); CodeFeaturesAttr.add(method, CodeFeature.SWITCH); return swInsn; } private InsnNode makeNewArray(InsnData insn) { ArgType indexType = ArgType.parse(insn.getIndexAsType()); int dim = (int) insn.getLiteral(); ArgType arrType; if (dim == 0) { arrType = indexType; } else { if (indexType.isArray()) { // java bytecode can pass array as a base type arrType = indexType; } else { arrType = ArgType.array(indexType, dim); } } int regsCount = insn.getRegsCount(); NewArrayNode newArr = new NewArrayNode(arrType, regsCount - 1); newArr.setResult(InsnArg.reg(insn, 0, arrType)); for (int i = 1; i < regsCount; i++) { newArr.addArg(InsnArg.typeImmutableReg(insn, i, ArgType.INT)); } CodeFeaturesAttr.add(method, CodeFeature.NEW_ARRAY); return newArr; } private ArgType tryResolveFieldType(FieldInfo igetFld) { FieldNode fieldNode = root.resolveField(igetFld); if (fieldNode != null) { return fieldNode.getType(); } return igetFld.getType(); } private InsnNode filledNewArray(InsnData insn, boolean isRange) { ArgType arrType = ArgType.parse(insn.getIndexAsType()); ArgType elType = arrType.getArrayElement(); boolean typeImmutable = elType.isPrimitive(); int regsCount = insn.getRegsCount(); InsnArg[] regs = new InsnArg[regsCount]; if (isRange) { int r = insn.getReg(0); for (int i = 0; i < regsCount; i++) { regs[i] = InsnArg.reg(r, elType, typeImmutable); r++; } } else { for (int i = 0; i < regsCount; i++) { int regNum = insn.getReg(i); regs[i] = InsnArg.reg(regNum, elType, typeImmutable); } } InsnNode node = new FilledNewArrayNode(elType, regs.length); // node.setResult(resReg == -1 ? null : InsnArg.reg(resReg, arrType)); for (InsnArg arg : regs) { node.addArg(arg); } return node; } private InsnNode cmp(InsnData insn, InsnType itype, ArgType argType) { InsnNode inode = new InsnNode(itype, 2); inode.setResult(InsnArg.reg(insn, 0, ArgType.INT)); inode.addArg(InsnArg.reg(insn, 1, argType)); inode.addArg(InsnArg.reg(insn, 2, argType)); return inode; } private InsnNode cast(InsnData insn, ArgType from, ArgType to) { InsnNode inode = new IndexInsnNode(InsnType.CAST, to, 1); inode.setResult(InsnArg.reg(insn, 0, to)); inode.addArg(InsnArg.reg(insn, 1, from)); return inode; } private InsnNode invokeCustom(InsnData insn, boolean isRange) { return InvokeCustomBuilder.build(method, insn, isRange); } private InsnNode invokePolymorphic(InsnData insn, boolean isRange) { IMethodRef mthRef = InsnDataUtils.getMethodRef(insn); if (mthRef == null) { throw new JadxRuntimeException("Failed to load method reference for insn: " + insn); } MethodInfo callMth = MethodInfo.fromRef(root, mthRef); IMethodProto proto = insn.getIndexAsProto(insn.getTarget()); // expand call args List args = Utils.collectionMap(proto.getArgTypes(), ArgType::parse); ArgType returnType = ArgType.parse(proto.getReturnType()); MethodInfo effectiveCallMth = MethodInfo.fromDetails(root, callMth.getDeclClass(), callMth.getName(), args, returnType); return new InvokePolymorphicNode(effectiveCallMth, insn, proto, callMth, isRange); } private InsnNode invokeSpecial(InsnData insn) { IMethodRef mthRef = InsnDataUtils.getMethodRef(insn); if (mthRef == null) { throw new JadxRuntimeException("Failed to load method reference for insn: " + insn); } MethodInfo mthInfo = MethodInfo.fromRef(root, mthRef); // convert 'special' to 'direct/super' same as dx InvokeType type; if (mthInfo.isConstructor() || Objects.equals(mthInfo.getDeclClass(), method.getParentClass().getClassInfo())) { type = InvokeType.DIRECT; } else { type = InvokeType.SUPER; } return new InvokeNode(mthInfo, insn, type, false); } private InsnNode invoke(InsnData insn, InvokeType type, boolean isRange) { IMethodRef mthRef = InsnDataUtils.getMethodRef(insn); if (mthRef == null) { throw new JadxRuntimeException("Failed to load method reference for insn: " + insn); } MethodInfo mthInfo = MethodInfo.fromRef(root, mthRef); return new InvokeNode(mthInfo, insn, type, isRange); } private InsnNode arrayGet(InsnData insn, ArgType argType) { return arrayGet(insn, argType, argType); } private InsnNode arrayGet(InsnData insn, ArgType arrElemType, ArgType resType) { InsnNode inode = new InsnNode(InsnType.AGET, 2); inode.setResult(InsnArg.typeImmutableIfKnownReg(insn, 0, resType)); inode.addArg(InsnArg.typeImmutableIfKnownReg(insn, 1, ArgType.array(arrElemType))); inode.addArg(InsnArg.reg(insn, 2, ArgType.NARROW_INTEGRAL)); return inode; } private InsnNode arrayPut(InsnData insn, ArgType argType) { return arrayPut(insn, argType, argType); } private InsnNode arrayPut(InsnData insn, ArgType arrElemType, ArgType argType) { InsnNode inode = new InsnNode(InsnType.APUT, 3); inode.addArg(InsnArg.typeImmutableIfKnownReg(insn, 1, ArgType.array(arrElemType))); inode.addArg(InsnArg.reg(insn, 2, ArgType.NARROW_INTEGRAL)); inode.addArg(InsnArg.typeImmutableIfKnownReg(insn, 0, argType)); return inode; } private InsnNode arith(InsnData insn, ArithOp op, ArgType type) { return ArithNode.build(insn, op, type); } private InsnNode arithLit(InsnData insn, ArithOp op, ArgType type) { return ArithNode.buildLit(insn, op, type); } private InsnNode neg(InsnData insn, ArgType type) { InsnNode inode = new InsnNode(InsnType.NEG, 1); inode.setResult(InsnArg.reg(insn, 0, type)); inode.addArg(InsnArg.reg(insn, 1, type)); return inode; } private InsnNode not(InsnData insn, ArgType type) { InsnNode inode = new InsnNode(InsnType.NOT, 1); inode.setResult(InsnArg.reg(insn, 0, type)); inode.addArg(InsnArg.reg(insn, 1, type)); return inode; } private InsnNode insn(InsnType type, RegisterArg res) { InsnNode node = new InsnNode(type, 0); node.setResult(res); return node; } private InsnNode insn(InsnType type, RegisterArg res, InsnArg arg) { InsnNode node = new InsnNode(type, 1); node.setResult(res); node.addArg(arg); return node; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/InsnType.java ================================================ package jadx.core.dex.instructions; public enum InsnType { CONST, CONST_STR, CONST_CLASS, ARITH, NEG, NOT, MOVE, MOVE_MULTI, CAST, RETURN, GOTO, THROW, MOVE_EXCEPTION, CMP_L, CMP_G, IF, SWITCH, SWITCH_DATA, MONITOR_ENTER, MONITOR_EXIT, CHECK_CAST, INSTANCE_OF, ARRAY_LENGTH, FILL_ARRAY, FILL_ARRAY_DATA, FILLED_NEW_ARRAY, AGET, APUT, NEW_ARRAY, NEW_INSTANCE, IGET, IPUT, SGET, SPUT, INVOKE, MOVE_RESULT, // *** Additional instructions *** // replacement for removed instructions NOP, TERNARY, CONSTRUCTOR, BREAK, CONTINUE, // strings concatenation STR_CONCAT, // just generate one argument ONE_ARG, PHI, // fake insn to keep arguments which will be used in regions codegen REGION_ARG, // Java specific dynamic jump instructions JAVA_JSR, JAVA_RET, } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/InvokeCustomBuilder.java ================================================ package jadx.core.dex.instructions; import java.util.List; import jadx.api.plugins.input.data.ICallSite; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.instructions.invokedynamic.CustomLambdaCall; import jadx.core.dex.instructions.invokedynamic.CustomRawCall; import jadx.core.dex.instructions.invokedynamic.CustomStringConcat; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.input.InsnDataUtils; public class InvokeCustomBuilder { public static InsnNode build(MethodNode mth, InsnData insn, boolean isRange) { try { ICallSite callSite = InsnDataUtils.getCallSite(insn); if (callSite == null) { throw new JadxRuntimeException("Failed to get call site for insn: " + insn); } callSite.load(); List values = callSite.getValues(); if (CustomLambdaCall.isLambdaInvoke(values)) { return CustomLambdaCall.buildLambdaMethodCall(mth, insn, isRange, values); } if (CustomStringConcat.isStringConcat(values)) { return CustomStringConcat.buildStringConcat(insn, isRange, values); } try { return CustomRawCall.build(mth, insn, isRange, values); } catch (Exception e) { mth.addWarn("Failed to decode invoke-custom: \n" + Utils.listToString(values, "\n") + ",\n exception: " + Utils.getStackTrace(e)); InsnNode nop = new InsnNode(InsnType.NOP, 0); nop.add(AFlag.SYNTHETIC); nop.addAttr(AType.JADX_ERROR, new JadxError("Failed to decode invoke-custom: " + values, e)); return nop; } } catch (Exception e) { throw new JadxRuntimeException("'invoke-custom' instruction processing error: " + e.getMessage(), e); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/InvokeCustomNode.java ================================================ package jadx.core.dex.instructions; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.MethodHandleType; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; public class InvokeCustomNode extends InvokeNode { private MethodInfo implMthInfo; private MethodHandleType handleType; private InsnNode callInsn; private boolean inlineInsn; private boolean useRef; public InvokeCustomNode(MethodInfo lambdaInfo, InsnData insn, boolean instanceCall, boolean isRange) { super(lambdaInfo, insn, InvokeType.CUSTOM, instanceCall, isRange); } private InvokeCustomNode(MethodInfo mth, InvokeType invokeType, int argsCount) { super(mth, invokeType, argsCount); } @Override public InsnNode copy() { InvokeCustomNode copy = new InvokeCustomNode(getCallMth(), getInvokeType(), getArgsCount()); copyCommonParams(copy); copy.setImplMthInfo(implMthInfo); copy.setHandleType(handleType); copy.setCallInsn(callInsn); copy.setInlineInsn(inlineInsn); copy.setUseRef(useRef); return copy; } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof InvokeCustomNode) || !super.isSame(obj)) { return false; } InvokeCustomNode other = (InvokeCustomNode) obj; return handleType == other.handleType && implMthInfo.equals(other.implMthInfo) && callInsn.isSame(other.callInsn) && inlineInsn == other.inlineInsn && useRef == other.useRef; } public MethodInfo getImplMthInfo() { return implMthInfo; } public void setImplMthInfo(MethodInfo implMthInfo) { this.implMthInfo = implMthInfo; } public MethodHandleType getHandleType() { return handleType; } public void setHandleType(MethodHandleType handleType) { this.handleType = handleType; } public InsnNode getCallInsn() { return callInsn; } public void setCallInsn(InsnNode callInsn) { this.callInsn = callInsn; } public boolean isInlineInsn() { return inlineInsn; } public void setInlineInsn(boolean inlineInsn) { this.inlineInsn = inlineInsn; } public boolean isUseRef() { return useRef; } public void setUseRef(boolean useRef) { this.useRef = useRef; } @Nullable public BaseInvokeNode getInvokeCall() { if (callInsn.getType() == InsnType.INVOKE) { return (BaseInvokeNode) callInsn; } return null; } @Override public @Nullable InsnArg getInstanceArg() { return null; } @Override public boolean isStaticCall() { return true; } @Override public int getFirstArgOffset() { return 0; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(InsnUtils.formatOffset(offset)).append(": INVOKE_CUSTOM "); if (getResult() != null) { sb.append(getResult()).append(" = "); } appendArgs(sb); appendAttributes(sb); sb.append("\n handle type: ").append(handleType); sb.append("\n lambda: ").append(implMthInfo); sb.append("\n call insn: ").append(callInsn); return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/InvokeCustomRawNode.java ================================================ package jadx.core.dex.instructions; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.invokedynamic.CustomRawCall; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; import jadx.core.utils.Utils; /** * Information for raw invoke-custom instruction.
* Output will be formatted as polymorphic call with equivalent semantic * Contains two parts: * - resolve: treated as additional invoke insn (uses only constant args) * - invoke: call of resolved method (base for this invoke) *
* See {@link CustomRawCall} class for build details */ public class InvokeCustomRawNode extends InvokeNode { private final InvokeNode resolve; private List callSiteValues; public InvokeCustomRawNode(InvokeNode resolve, MethodInfo mthInfo, InsnData insn, boolean isRange) { super(mthInfo, insn, InvokeType.CUSTOM_RAW, false, isRange); this.resolve = resolve; } public InvokeCustomRawNode(InvokeNode resolve, MethodInfo mthInfo, InvokeType invokeType, int argsCount) { super(mthInfo, invokeType, argsCount); this.resolve = resolve; } public InvokeNode getResolveInvoke() { return resolve; } public void setCallSiteValues(List callSiteValues) { this.callSiteValues = callSiteValues; } public List getCallSiteValues() { return callSiteValues; } @Override public InsnNode copy() { InvokeCustomRawNode copy = new InvokeCustomRawNode(resolve, getCallMth(), getInvokeType(), getArgsCount()); copyCommonParams(copy); copy.setCallSiteValues(callSiteValues); return copy; } @Override public boolean isStaticCall() { return true; } @Override public int getFirstArgOffset() { return 0; } @Override public @Nullable InsnArg getInstanceArg() { return null; } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (obj instanceof InvokeCustomRawNode) { return super.isSame(obj) && resolve.isSame(((InvokeCustomRawNode) obj).resolve); } return false; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(InsnUtils.formatOffset(offset)).append(": INVOKE_CUSTOM "); if (getResult() != null) { sb.append(getResult()).append(" = "); } if (!appendArgs(sb)) { sb.append('\n'); } appendAttributes(sb); sb.append(" call-site: \n ").append(Utils.listToString(callSiteValues, "\n ")).append('\n'); return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/InvokeNode.java ================================================ package jadx.core.dex.instructions; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.InsnNode; public class InvokeNode extends BaseInvokeNode { private final InvokeType type; private final MethodInfo mth; public InvokeNode(MethodInfo mthInfo, InsnData insn, InvokeType invokeType, boolean isRange) { this(mthInfo, insn, invokeType, invokeType != InvokeType.STATIC, isRange); } public InvokeNode(MethodInfo mth, InsnData insn, InvokeType type, boolean instanceCall, boolean isRange) { super(InsnType.INVOKE, mth.getArgsCount() + (instanceCall ? 1 : 0)); this.mth = mth; this.type = type; int k = isRange ? insn.getReg(0) : 0; if (instanceCall) { int r = isRange ? k : insn.getReg(k); addReg(r, mth.getDeclClass().getType()); k++; } for (ArgType arg : mth.getArgumentsTypes()) { addReg(isRange ? k : insn.getReg(k), arg); k += arg.getRegCount(); } int resReg = insn.getResultReg(); if (resReg != -1) { setResult(InsnArg.reg(resReg, mth.getReturnType())); } } public InvokeNode(MethodInfo mth, InvokeType invokeType, int argsCount) { super(InsnType.INVOKE, argsCount); this.mth = mth; this.type = invokeType; } public InvokeType getInvokeType() { return type; } @Override public MethodInfo getCallMth() { return mth; } @Override @Nullable public InsnArg getInstanceArg() { if (type != InvokeType.STATIC && getArgsCount() > 0) { return getArg(0); } return null; } @Override public boolean isStaticCall() { return type == InvokeType.STATIC; } public boolean isPolymorphicCall() { if (type == InvokeType.POLYMORPHIC) { return true; } // java bytecode uses virtual call with modified method info if (type == InvokeType.VIRTUAL && mth.getDeclClass().getFullName().equals("java.lang.invoke.MethodHandle") && (mth.getName().equals("invoke") || mth.getName().equals("invokeExact"))) { return true; } return false; } public int getFirstArgOffset() { return type == InvokeType.STATIC ? 0 : 1; } @Override public InsnNode copy() { return copyCommonParams(new InvokeNode(mth, type, getArgsCount())); } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof InvokeNode) || !super.isSame(obj)) { return false; } InvokeNode other = (InvokeNode) obj; return type == other.type && mth.equals(other.mth); } @Override public String toString() { return baseString() + " " + type + " call: " + mth + attributesString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/InvokePolymorphicNode.java ================================================ package jadx.core.dex.instructions; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; public class InvokePolymorphicNode extends InvokeNode { private final IMethodProto proto; private final MethodInfo baseCallRef; public InvokePolymorphicNode(MethodInfo callMth, InsnData insn, IMethodProto proto, MethodInfo baseRef, boolean isRange) { super(callMth, insn, InvokeType.POLYMORPHIC, true, isRange); this.proto = proto; this.baseCallRef = baseRef; } public InvokePolymorphicNode(MethodInfo callMth, int argsCount, IMethodProto proto, MethodInfo baseRef) { super(callMth, InvokeType.POLYMORPHIC, argsCount); this.proto = proto; this.baseCallRef = baseRef; } public IMethodProto getProto() { return proto; } public MethodInfo getBaseCallRef() { return baseCallRef; } @Override public InsnNode copy() { InvokePolymorphicNode copy = new InvokePolymorphicNode(getCallMth(), getArgsCount(), proto, baseCallRef); copyCommonParams(copy); return copy; } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof InvokePolymorphicNode) || !super.isSame(obj)) { return false; } InvokePolymorphicNode other = (InvokePolymorphicNode) obj; return proto.equals(other.proto); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(InsnUtils.formatOffset(offset)).append(": INVOKE_POLYMORPHIC "); if (getResult() != null) { sb.append(getResult()).append(" = "); } if (!appendArgs(sb)) { sb.append('\n'); } appendAttributes(sb); sb.append(" base: ").append(baseCallRef).append('\n'); sb.append(" proto: ").append(proto).append('\n'); return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/InvokeType.java ================================================ package jadx.core.dex.instructions; public enum InvokeType { STATIC, DIRECT, VIRTUAL, INTERFACE, SUPER, POLYMORPHIC, CUSTOM, CUSTOM_RAW, } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/NewArrayNode.java ================================================ package jadx.core.dex.instructions; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.InsnNode; public class NewArrayNode extends InsnNode { private final ArgType arrType; public NewArrayNode(ArgType arrType, int argsCount) { super(InsnType.NEW_ARRAY, argsCount); this.arrType = arrType; } public ArgType getArrayType() { return arrType; } public int getDimension() { return arrType.getArrayDimension(); } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof NewArrayNode) || !super.isSame(obj)) { return false; } NewArrayNode other = (NewArrayNode) obj; return arrType == other.arrType; } @Override public InsnNode copy() { return copyCommonParams(new NewArrayNode(arrType, getArgsCount())); } @Override public String toString() { return super.toString() + " type: " + arrType; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/PhiInsn.java ================================================ package jadx.core.dex.instructions; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnRemover; import jadx.core.utils.exceptions.JadxRuntimeException; public final class PhiInsn extends InsnNode { // map arguments to blocks (in same order as in arguments list) private final List blockBinds; public PhiInsn(int regNum, int predecessors) { this(predecessors); setResult(InsnArg.reg(regNum, ArgType.UNKNOWN)); add(AFlag.DONT_INLINE); add(AFlag.DONT_GENERATE); } private PhiInsn(int argsCount) { super(InsnType.PHI, argsCount); this.blockBinds = new ArrayList<>(argsCount); } public RegisterArg bindArg(BlockNode pred) { RegisterArg arg = InsnArg.reg(getResult().getRegNum(), getResult().getInitType()); bindArg(arg, pred); return arg; } public void bindArg(RegisterArg arg, BlockNode pred) { if (blockBinds.contains(pred)) { throw new JadxRuntimeException("Duplicate predecessors in PHI insn: " + pred + ", " + this); } if (pred == null) { throw new JadxRuntimeException("Null bind block in PHI insn: " + this); } super.addArg(arg); blockBinds.add(pred); } @Nullable public BlockNode getBlockByArg(RegisterArg arg) { int index = getArgIndex(arg); if (index == -1) { return null; } return blockBinds.get(index); } public BlockNode getBlockByArgIndex(int argIndex) { return blockBinds.get(argIndex); } @Override @NotNull public RegisterArg getArg(int n) { return (RegisterArg) super.getArg(n); } public @Nullable RegisterArg getArgByBlock(BlockNode block) { for (int i = 0; i < blockBinds.size(); i++) { if (blockBinds.get(i) == block) { return getArg(i); } } return null; } @Override public boolean removeArg(InsnArg arg) { int index = getArgIndex(arg); if (index == -1) { return false; } removeArg(index); return true; } @Override public RegisterArg removeArg(int index) { RegisterArg reg = (RegisterArg) super.removeArg(index); blockBinds.remove(index); reg.getSVar().updateUsedInPhiList(); return reg; } @Nullable public RegisterArg getArgBySsaVar(SSAVar ssaVar) { if (getArgsCount() == 0) { return null; } for (InsnArg insnArg : getArguments()) { RegisterArg reg = (RegisterArg) insnArg; if (reg.getSVar() == ssaVar) { return reg; } } return null; } @Nullable public RegisterArg getArgByBlock(IBlock block) { if (getArgsCount() == 0) { return null; } int index = blockBinds.indexOf(block); if (index == -1) { return null; } return getArg(index); } @Override public boolean replaceArg(InsnArg from, InsnArg to) { if (!(from instanceof RegisterArg) || !(to instanceof RegisterArg)) { return false; } int argIndex = getArgIndex(from); if (argIndex == -1) { return false; } ((RegisterArg) to).getSVar().addUsedInPhi(this); super.setArg(argIndex, to); InsnRemover.unbindArgUsage(null, from); ((RegisterArg) from).getSVar().updateUsedInPhiList(); return true; } @Override public void addArg(InsnArg arg) { throw new JadxRuntimeException("Direct addArg is forbidden for PHI insn, bindArg must be used"); } @Override public void setArg(int n, InsnArg arg) { throw new JadxRuntimeException("Direct setArg is forbidden for PHI insn, bindArg must be used"); } @Override public InsnNode copy() { return copyCommonParams(new PhiInsn(getArgsCount())); } @Override public String toString() { return baseString() + " binds: " + blockBinds + attributesString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/SwitchData.java ================================================ package jadx.core.dex.instructions; import jadx.api.plugins.input.insns.custom.ISwitchPayload; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; public class SwitchData extends InsnNode { private final int size; private final int[] keys; private final int[] targets; public SwitchData(ISwitchPayload payload) { super(InsnType.SWITCH_DATA, 0); this.size = payload.getSize(); this.keys = payload.getKeys(); this.targets = payload.getTargets(); } public void fixTargets(int switchOffset) { int size = this.size; int[] targets = this.targets; for (int i = 0; i < size; i++) { targets[i] += switchOffset; } } public int getSize() { return size; } public int[] getKeys() { return keys; } public int[] getTargets() { return targets; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("switch-data {"); for (int i = 0; i < size; i++) { sb.append(keys[i]).append("->").append(InsnUtils.formatOffset(targets[i])).append(", "); } sb.append('}'); appendAttributes(sb); return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/SwitchInsn.java ================================================ package jadx.core.dex.instructions; import java.util.List; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.BlockUtils.getBlockByOffset; public class SwitchInsn extends TargetInsnNode { private final int dataTarget; private final boolean packed; // type of switch insn, if true can contain filler keys @Nullable private SwitchData switchData; private int def; // next instruction private Object[] modifiedKeys; private BlockNode[] targetBlocks; private BlockNode defTargetBlock; public SwitchInsn(InsnArg arg, int dataTarget, boolean packed) { super(InsnType.SWITCH, 1); addArg(arg); this.dataTarget = dataTarget; this.packed = packed; } public boolean needData() { return this.switchData == null; } public void attachSwitchData(SwitchData data, int def) { this.switchData = data; this.def = def; } @Override public void initBlocks(BlockNode curBlock) { if (switchData == null) { throw new JadxRuntimeException("Switch data not yet attached"); } List successors = curBlock.getSuccessors(); int[] targets = switchData.getTargets(); int len = targets.length; targetBlocks = new BlockNode[len]; for (int i = 0; i < len; i++) { targetBlocks[i] = getBlockByOffset(targets[i], successors); } defTargetBlock = getBlockByOffset(def, successors); } @Override public boolean replaceTargetBlock(BlockNode origin, BlockNode replace) { if (targetBlocks == null) { return false; } int count = 0; int len = targetBlocks.length; for (int i = 0; i < len; i++) { if (targetBlocks[i] == origin) { targetBlocks[i] = replace; count++; } } if (defTargetBlock == origin) { defTargetBlock = replace; count++; } return count > 0; } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof SwitchInsn) || !super.isSame(obj)) { return false; } SwitchInsn other = (SwitchInsn) obj; return dataTarget == other.dataTarget && packed == other.packed; } @Override public InsnNode copy() { SwitchInsn copy = new SwitchInsn(getArg(0), dataTarget, packed); copy.switchData = switchData; copy.def = def; copy.targetBlocks = targetBlocks; copy.defTargetBlock = defTargetBlock; return copyCommonParams(copy); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(baseString()); if (switchData == null) { sb.append("no payload"); } else { int size = switchData.getSize(); int[] keys = switchData.getKeys(); if (targetBlocks != null) { for (int i = 0; i < size; i++) { sb.append('\n'); sb.append(" case ").append(keys[i]).append(": goto ").append(targetBlocks[i]); } if (def != -1) { sb.append('\n').append(" default: goto ").append(defTargetBlock); } } else { int[] targets = switchData.getTargets(); for (int i = 0; i < size; i++) { sb.append('\n'); sb.append(" case ").append(keys[i]).append(": goto ").append(InsnUtils.formatOffset(targets[i])); } if (def != -1) { sb.append('\n'); sb.append(" default: goto ").append(InsnUtils.formatOffset(def)); } } } appendAttributes(sb); return sb.toString(); } public int getDataTarget() { return dataTarget; } public boolean isPacked() { return packed; } public int getDefaultCaseOffset() { return def; } @NotNull private SwitchData getSwitchData() { if (switchData == null) { throw new JadxRuntimeException("Switch data not yet attached"); } return switchData; } public int[] getTargets() { return getSwitchData().getTargets(); } public int[] getKeys() { return getSwitchData().getKeys(); } public Object getKey(int i) { if (modifiedKeys != null) { return modifiedKeys[i]; } return getSwitchData().getKeys()[i]; } public void modifyKey(int i, Object newKey) { if (modifiedKeys == null) { int[] keys = getKeys(); int caseCount = keys.length; Object[] newKeys = new Object[caseCount]; for (int j = 0; j < caseCount; j++) { newKeys[j] = keys[j]; } modifiedKeys = newKeys; } modifiedKeys[i] = newKey; } public BlockNode[] getTargetBlocks() { return targetBlocks; } public BlockNode getDefTargetBlock() { return defTargetBlock; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/TargetInsnNode.java ================================================ package jadx.core.dex.instructions; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; public abstract class TargetInsnNode extends InsnNode { public TargetInsnNode(InsnType type, int argsCount) { super(type, argsCount); } public void initBlocks(BlockNode curBlock) { } public boolean replaceTargetBlock(BlockNode origin, BlockNode replace) { return false; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java ================================================ package jadx.core.dex.instructions.args; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Function; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import jadx.core.Consts; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.typeinference.TypeCompareEnum; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; public abstract class ArgType { public static final ArgType INT = primitive(PrimitiveType.INT); public static final ArgType BOOLEAN = primitive(PrimitiveType.BOOLEAN); public static final ArgType BYTE = primitive(PrimitiveType.BYTE); public static final ArgType SHORT = primitive(PrimitiveType.SHORT); public static final ArgType CHAR = primitive(PrimitiveType.CHAR); public static final ArgType FLOAT = primitive(PrimitiveType.FLOAT); public static final ArgType DOUBLE = primitive(PrimitiveType.DOUBLE); public static final ArgType LONG = primitive(PrimitiveType.LONG); public static final ArgType VOID = primitive(PrimitiveType.VOID); public static final ArgType OBJECT = objectNoCache(Consts.CLASS_OBJECT); public static final ArgType CLASS = objectNoCache(Consts.CLASS_CLASS); public static final ArgType STRING = objectNoCache(Consts.CLASS_STRING); public static final ArgType ENUM = objectNoCache(Consts.CLASS_ENUM); public static final ArgType THROWABLE = objectNoCache(Consts.CLASS_THROWABLE); public static final ArgType ERROR = objectNoCache(Consts.CLASS_ERROR); public static final ArgType EXCEPTION = objectNoCache(Consts.CLASS_EXCEPTION); public static final ArgType RUNTIME_EXCEPTION = objectNoCache(Consts.CLASS_RUNTIME_EXCEPTION); public static final ArgType OBJECT_ARRAY = array(OBJECT); public static final ArgType WILDCARD = wildcard(); public static final ArgType UNKNOWN = unknown(PrimitiveType.values()); public static final ArgType UNKNOWN_OBJECT = unknown(PrimitiveType.OBJECT, PrimitiveType.ARRAY); public static final ArgType UNKNOWN_OBJECT_NO_ARRAY = unknown(PrimitiveType.OBJECT); public static final ArgType UNKNOWN_ARRAY = array(UNKNOWN); public static final ArgType NARROW = unknown( PrimitiveType.INT, PrimitiveType.FLOAT, PrimitiveType.BOOLEAN, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR, PrimitiveType.OBJECT, PrimitiveType.ARRAY); public static final ArgType NARROW_NUMBERS = unknown( PrimitiveType.BOOLEAN, PrimitiveType.INT, PrimitiveType.FLOAT, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); public static final ArgType NARROW_INTEGRAL = unknown( PrimitiveType.INT, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); public static final ArgType NARROW_NUMBERS_NO_BOOL = unknown( PrimitiveType.INT, PrimitiveType.FLOAT, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); public static final ArgType NARROW_NEG_NUMBERS = unknown( PrimitiveType.INT, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.FLOAT); public static final ArgType NARROW_NUMBERS_NO_FLOAT = unknown( PrimitiveType.INT, PrimitiveType.BOOLEAN, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); public static final ArgType WIDE = unknown(PrimitiveType.LONG, PrimitiveType.DOUBLE); public static final ArgType INT_FLOAT = unknown(PrimitiveType.INT, PrimitiveType.FLOAT); public static final ArgType INT_BOOLEAN = unknown(PrimitiveType.INT, PrimitiveType.BOOLEAN); public static final ArgType BYTE_BOOLEAN = unknown(PrimitiveType.BYTE, PrimitiveType.BOOLEAN); public static final ArgType UNKNOWN_INT = unknown(PrimitiveType.INT); protected int hash; private static ArgType primitive(PrimitiveType stype) { return new PrimitiveArg(stype); } private static ArgType objectNoCache(String obj) { return new ObjectType(obj); } public static ArgType object(String obj) { // TODO: add caching String cleanObjectName = Utils.cleanObjectName(obj); switch (cleanObjectName) { case Consts.CLASS_OBJECT: return OBJECT; case Consts.CLASS_STRING: return STRING; case Consts.CLASS_CLASS: return CLASS; case Consts.CLASS_THROWABLE: return THROWABLE; case Consts.CLASS_EXCEPTION: return EXCEPTION; default: return new ObjectType(cleanObjectName); } } public static ArgType genericType(String type) { return new GenericType(type); } public static ArgType genericType(String type, ArgType extendType) { return new GenericType(type, extendType); } public static ArgType genericType(String type, List extendTypes) { return new GenericType(type, extendTypes); } public static ArgType wildcard() { return new WildcardType(OBJECT, WildcardBound.UNBOUND); } public static ArgType wildcard(ArgType obj, WildcardBound bound) { return new WildcardType(obj, bound); } public static ArgType generic(ArgType obj, List generics) { if (!obj.isObject()) { throw new IllegalArgumentException("Expected Object as ArgType, got: " + obj); } return new GenericObject(obj.getObject(), generics); } public static ArgType generic(ArgType obj, ArgType... generics) { return generic(obj, Arrays.asList(generics)); } public static ArgType generic(String obj, List generics) { return new GenericObject(Utils.cleanObjectName(obj), generics); } public static ArgType generic(String obj, ArgType generic) { return generic(obj, Collections.singletonList(generic)); } @TestOnly public static ArgType generic(String obj, ArgType... generics) { return generic(obj, Arrays.asList(generics)); } public static ArgType outerGeneric(ArgType genericOuterType, ArgType innerType) { return new OuterGenericObject((ObjectType) genericOuterType, (ObjectType) innerType); } public static ArgType array(@NotNull ArgType vtype) { return new ArrayArg(vtype); } public static ArgType array(@NotNull ArgType type, int dimension) { if (dimension == 1) { return new ArrayArg(type); } ArgType arrType = type; for (int i = 0; i < dimension; i++) { arrType = new ArrayArg(arrType); } return arrType; } public static ArgType unknown(PrimitiveType... types) { return new UnknownArg(types); } private abstract static class KnownType extends ArgType { private static final PrimitiveType[] EMPTY_POSSIBLES = new PrimitiveType[0]; @Override public boolean isTypeKnown() { return true; } @Override public boolean contains(PrimitiveType type) { return getPrimitiveType() == type; } @Override public ArgType selectFirst() { return null; } @Override public PrimitiveType[] getPossibleTypes() { return EMPTY_POSSIBLES; } } private static final class PrimitiveArg extends KnownType { private final PrimitiveType type; public PrimitiveArg(PrimitiveType type) { this.type = type; this.hash = type.hashCode(); } @Override public PrimitiveType getPrimitiveType() { return type; } @Override public boolean isPrimitive() { return true; } @Override boolean internalEquals(Object obj) { return type == ((PrimitiveArg) obj).type; } @Override public String toString() { return type.toString(); } } private static class ObjectType extends KnownType { protected final String objName; public ObjectType(String obj) { this.objName = obj; this.hash = objName.hashCode(); } @Override public String getObject() { return objName; } @Override public boolean isObject() { return true; } @Override public PrimitiveType getPrimitiveType() { return PrimitiveType.OBJECT; } @Override boolean internalEquals(Object obj) { return objName.equals(((ObjectType) obj).objName); } @Override public String toString() { return objName; } } private static final class GenericType extends ObjectType { private List extendTypes; public GenericType(String obj) { this(obj, Collections.emptyList()); } public GenericType(String obj, ArgType extendType) { this(obj, Collections.singletonList(extendType)); } public GenericType(String obj, List extendTypes) { super(obj); this.extendTypes = extendTypes; } @Override public boolean isGenericType() { return true; } @Override public List getExtendTypes() { return extendTypes; } @Override public void setExtendTypes(List extendTypes) { this.extendTypes = extendTypes; } @Override boolean internalEquals(Object obj) { return super.internalEquals(obj) && extendTypes.equals(((GenericType) obj).extendTypes); } @Override public String toString() { List extTypes = this.extendTypes; if (extTypes.isEmpty()) { return objName; } return objName + " extends " + Utils.listToString(extTypes, " & "); } } public enum WildcardBound { EXTENDS(1, "? extends "), // upper bound (? extends A) UNBOUND(0, "?"), // no bounds (?) SUPER(-1, "? super "); // lower bound (? super A) private final int num; private final String str; WildcardBound(int val, String str) { this.num = val; this.str = str; } public int getNum() { return num; } public String getStr() { return str; } public static WildcardBound getByNum(int num) { return num == 0 ? UNBOUND : (num == 1 ? EXTENDS : SUPER); } } private static final class WildcardType extends ObjectType { private final ArgType type; private final WildcardBound bound; public WildcardType(ArgType obj, WildcardBound bound) { super(OBJECT.getObject()); this.type = Objects.requireNonNull(obj); this.bound = Objects.requireNonNull(bound); } @Override public boolean isWildcard() { return true; } @Override public boolean isGeneric() { return true; } @Override public ArgType getWildcardType() { return type; } @Override public WildcardBound getWildcardBound() { return bound; } @Override boolean internalEquals(Object obj) { return super.internalEquals(obj) && bound == ((WildcardType) obj).bound && type.equals(((WildcardType) obj).type); } @Override public String toString() { if (bound == WildcardBound.UNBOUND) { return bound.getStr(); } return bound.getStr() + type; } } private static class GenericObject extends ObjectType { private final List generics; public GenericObject(String obj, List generics) { super(obj); this.generics = Objects.requireNonNull(generics); this.hash = calcHash(); } private int calcHash() { return objName.hashCode() + 31 * generics.hashCode(); } @Override public boolean isGeneric() { return true; } @Override public List getGenericTypes() { return generics; } @Override boolean internalEquals(Object obj) { return super.internalEquals(obj) && Objects.equals(generics, ((GenericObject) obj).generics); } @Override public String toString() { return super.toString() + '<' + Utils.listToString(generics) + '>'; } } private static class OuterGenericObject extends ObjectType { private final ObjectType outerType; private final ObjectType innerType; public OuterGenericObject(ObjectType outerType, ObjectType innerType) { super(outerType.getObject() + '$' + innerType.getObject()); this.outerType = outerType; this.innerType = innerType; this.hash = calcHash(); } private int calcHash() { return objName.hashCode() + 31 * (outerType.hashCode() + 31 * innerType.hashCode()); } @Override public boolean isGeneric() { return true; } @Override public List getGenericTypes() { return innerType.getGenericTypes(); } @Override public ArgType getOuterType() { return outerType; } @Override public ArgType getInnerType() { return innerType; } @Override boolean internalEquals(Object obj) { return super.internalEquals(obj) && Objects.equals(outerType, ((OuterGenericObject) obj).outerType) && Objects.equals(innerType, ((OuterGenericObject) obj).innerType); } @Override public String toString() { return outerType.toString() + '$' + innerType.toString(); } } private static final class ArrayArg extends KnownType { private static final PrimitiveType[] ARRAY_POSSIBLES = new PrimitiveType[] { PrimitiveType.ARRAY }; private final ArgType arrayElement; public ArrayArg(ArgType arrayElement) { this.arrayElement = arrayElement; this.hash = arrayElement.hashCode(); } @Override public ArgType getArrayElement() { return arrayElement; } @Override public boolean isArray() { return true; } @Override public PrimitiveType getPrimitiveType() { return PrimitiveType.ARRAY; } @Override public boolean isTypeKnown() { return arrayElement.isTypeKnown(); } @Override public ArgType selectFirst() { return array(arrayElement.selectFirst()); } @Override public PrimitiveType[] getPossibleTypes() { return ARRAY_POSSIBLES; } @Override public int getArrayDimension() { return 1 + arrayElement.getArrayDimension(); } @Override public ArgType getArrayRootElement() { return arrayElement.getArrayRootElement(); } @Override boolean internalEquals(Object other) { ArrayArg otherArr = (ArrayArg) other; return this.arrayElement.equals(otherArr.getArrayElement()); } @Override public String toString() { return arrayElement + "[]"; } } private static final class UnknownArg extends ArgType { private final PrimitiveType[] possibleTypes; public UnknownArg(PrimitiveType[] types) { this.possibleTypes = types; this.hash = Arrays.hashCode(possibleTypes); } @Override public PrimitiveType[] getPossibleTypes() { return possibleTypes; } @Override public boolean isTypeKnown() { return false; } @Override public boolean contains(PrimitiveType type) { for (PrimitiveType t : possibleTypes) { if (t == type) { return true; } } return false; } @Override public ArgType selectFirst() { if (contains(PrimitiveType.OBJECT)) { return OBJECT; } if (contains(PrimitiveType.ARRAY)) { return array(OBJECT); } return primitive(possibleTypes[0]); } @Override boolean internalEquals(Object obj) { return Arrays.equals(possibleTypes, ((UnknownArg) obj).possibleTypes); } @Override public String toString() { if (possibleTypes.length == PrimitiveType.values().length) { return "??"; } else { return "??[" + Utils.arrayToStr(possibleTypes) + ']'; } } } public boolean isTypeKnown() { return false; } public PrimitiveType getPrimitiveType() { return null; } public boolean isPrimitive() { return false; } public String getObject() { throw new UnsupportedOperationException("ArgType.getObject(), call class: " + this.getClass()); } public boolean isObject() { return false; } public boolean isGeneric() { return false; } public boolean isGenericType() { return false; } public List getGenericTypes() { return null; } public List getExtendTypes() { return Collections.emptyList(); } public void setExtendTypes(List extendTypes) { } public ArgType getWildcardType() { return null; } public WildcardBound getWildcardBound() { return null; } public boolean isWildcard() { return false; } public ArgType getOuterType() { return null; } public ArgType getInnerType() { return null; } public boolean isArray() { return false; } public int getArrayDimension() { return 0; } public ArgType getArrayElement() { return null; } public ArgType getArrayRootElement() { return this; } public abstract boolean contains(PrimitiveType type); public abstract ArgType selectFirst(); public abstract PrimitiveType[] getPossibleTypes(); public static boolean isCastNeeded(RootNode root, ArgType from, ArgType to) { if (from.equals(to)) { return false; } TypeCompareEnum result = root.getTypeCompare().compareTypes(from, to); return !result.isNarrow(); } public static boolean isInstanceOf(RootNode root, ArgType type, ArgType of) { if (type.equals(of)) { return true; } if (!type.isObject() || !of.isObject()) { return false; } return root.getClsp().isImplements(type.getObject(), of.getObject()); } public static boolean isClsKnown(RootNode root, ArgType cls) { if (cls.isObject()) { return root.getClsp().isClsKnown(cls.getObject()); } return false; } public boolean canBeObject() { return isObject() || (!isTypeKnown() && contains(PrimitiveType.OBJECT)); } public boolean canBeArray() { return isArray() || (!isTypeKnown() && contains(PrimitiveType.ARRAY)); } public boolean canBePrimitive(PrimitiveType primitiveType) { return (isPrimitive() && getPrimitiveType() == primitiveType) || (!isTypeKnown() && contains(primitiveType)); } public boolean canBeAnyNumber() { if (isPrimitive()) { return !getPrimitiveType().isObjectOrArray(); } for (PrimitiveType primitiveType : getPossibleTypes()) { if (!primitiveType.isObjectOrArray()) { return true; } } return false; } public static ArgType convertFromPrimitiveType(PrimitiveType primitiveType) { switch (primitiveType) { case BOOLEAN: return BOOLEAN; case CHAR: return CHAR; case BYTE: return BYTE; case SHORT: return SHORT; case INT: return INT; case FLOAT: return FLOAT; case LONG: return LONG; case DOUBLE: return DOUBLE; case OBJECT: return OBJECT; case ARRAY: return OBJECT_ARRAY; case VOID: return ArgType.VOID; } return OBJECT; } public static ArgType parse(String type) { if (type == null || type.isEmpty()) { throw new JadxRuntimeException("Failed to parse type string: " + type); } char f = type.charAt(0); switch (f) { case 'L': return object(type); case 'T': return genericType(type.substring(1, type.length() - 1)); case '[': return array(parse(type.substring(1))); default: if (type.length() != 1) { throw new JadxRuntimeException("Unknown type string: \"" + type + '"'); } return parse(f); } } public static ArgType parse(char f) { switch (f) { case 'Z': return BOOLEAN; case 'B': return BYTE; case 'C': return CHAR; case 'S': return SHORT; case 'I': return INT; case 'J': return LONG; case 'F': return FLOAT; case 'D': return DOUBLE; case 'V': return VOID; default: throw new JadxRuntimeException("Unknown type char: '" + f + "' (0x" + Integer.toHexString(f) + ')'); } } public int getRegCount() { if (isPrimitive()) { PrimitiveType type = getPrimitiveType(); if (type == PrimitiveType.LONG || type == PrimitiveType.DOUBLE) { return 2; } else { return 1; } } if (!isTypeKnown()) { return 0; } return 1; } public boolean containsGeneric() { if (isGeneric() || isGenericType()) { return true; } if (isArray()) { ArgType arrayElement = getArrayElement(); if (arrayElement != null) { return arrayElement.containsGeneric(); } } return false; } public boolean containsTypeVariable() { if (isGenericType()) { return true; } ArgType wildcardType = getWildcardType(); if (wildcardType != null) { return wildcardType.containsTypeVariable(); } if (isGeneric()) { List genericTypes = getGenericTypes(); if (genericTypes != null) { for (ArgType genericType : genericTypes) { if (genericType.containsTypeVariable()) { return true; } } } ArgType outerType = getOuterType(); if (outerType != null) { return outerType.containsTypeVariable(); } return false; } if (isArray()) { ArgType arrayElement = getArrayElement(); if (arrayElement != null) { return arrayElement.containsTypeVariable(); } } return false; } public boolean isVoid() { return isPrimitive() && getPrimitiveType() == PrimitiveType.VOID; } /** * Recursively visit all subtypes of this type. * To exit return non-null value. */ @Nullable public R visitTypes(Function visitor) { R r = visitor.apply(this); if (r != null) { return r; } if (isArray()) { ArgType arrayElement = getArrayElement(); if (arrayElement != null) { return arrayElement.visitTypes(visitor); } } ArgType wildcardType = getWildcardType(); if (wildcardType != null) { R res = wildcardType.visitTypes(visitor); if (res != null) { return res; } } if (isGeneric()) { List genericTypes = getGenericTypes(); if (genericTypes != null) { for (ArgType genericType : genericTypes) { R res = genericType.visitTypes(visitor); if (res != null) { return res; } } } } return null; } public static ArgType tryToResolveClassAlias(RootNode root, ArgType type) { if (type.isGenericType()) { return type; } if (type.isArray()) { ArgType rootType = type.getArrayRootElement(); ArgType aliasType = tryToResolveClassAlias(root, rootType); if (aliasType == rootType) { return type; } return ArgType.array(aliasType, type.getArrayDimension()); } if (type.isObject()) { ArgType wildcardType = type.getWildcardType(); if (wildcardType != null) { return new WildcardType(tryToResolveClassAlias(root, wildcardType), type.getWildcardBound()); } ClassInfo clsInfo = ClassInfo.fromName(root, type.getObject()); ArgType baseType = clsInfo.hasAlias() ? ArgType.object(clsInfo.getAliasFullName()) : type; if (!type.isGeneric()) { return baseType; } List genericTypes = type.getGenericTypes(); if (genericTypes != null) { return new GenericObject(baseType.getObject(), tryToResolveClassAlias(root, genericTypes)); } } return type; } public static List tryToResolveClassAlias(RootNode root, List types) { return ListUtils.map(types, t -> tryToResolveClassAlias(root, t)); } @Override public String toString() { return "ARG_TYPE"; } @Override public int hashCode() { return hash; } abstract boolean internalEquals(Object obj); @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (hash != obj.hashCode()) { return false; } if (getClass() != obj.getClass()) { return false; } return internalEquals(obj); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/CodeVar.java ================================================ package jadx.core.dex.instructions.args; import java.util.ArrayList; import java.util.Collections; import java.util.List; import jadx.api.metadata.annotations.VarNode; public class CodeVar { private String name; private ArgType type; // before type inference can be null and set only for immutable types private List ssaVars = Collections.emptyList(); private boolean isFinal; private boolean isThis; private boolean isDeclared; private VarNode cachedVarNode; // set and used at codegen stage public static CodeVar fromMthArg(RegisterArg mthArg, boolean linkRegister) { CodeVar var = new CodeVar(); var.setType(mthArg.getInitType()); var.setName(mthArg.getName()); var.setThis(mthArg.isThis()); var.setDeclared(true); var.setThis(mthArg.isThis()); if (linkRegister) { var.setSsaVars(Collections.singletonList(new SSAVar(mthArg.getRegNum(), 0, mthArg))); } return var; } public String getName() { return name; } public void setName(String name) { this.name = name; } public ArgType getType() { return type; } public void setType(ArgType type) { this.type = type; } public List getSsaVars() { return ssaVars; } public void addSsaVar(SSAVar ssaVar) { if (ssaVars.isEmpty()) { ssaVars = new ArrayList<>(3); } if (!ssaVars.contains(ssaVar)) { ssaVars.add(ssaVar); } } public void setSsaVars(List ssaVars) { this.ssaVars = ssaVars; } public SSAVar getAnySsaVar() { if (ssaVars.isEmpty()) { throw new IllegalStateException("CodeVar without SSA variables attached: " + this); } return ssaVars.get(0); } public boolean isFinal() { return isFinal; } public void setFinal(boolean aFinal) { isFinal = aFinal; } public boolean isThis() { return isThis; } public void setThis(boolean aThis) { isThis = aThis; } public boolean isDeclared() { return isDeclared; } public void setDeclared(boolean declared) { isDeclared = declared; } public VarNode getCachedVarNode() { return cachedVarNode; } public void setCachedVarNode(VarNode varNode) { this.cachedVarNode = varNode; } /** * Merge flags with OR operator */ public void mergeFlagsFrom(CodeVar other) { if (other.isDeclared()) { setDeclared(true); } if (other.isThis()) { setThis(true); } if (other.isFinal()) { setFinal(true); } } @Override public String toString() { return (isFinal ? "final " : "") + type + ' ' + name; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java ================================================ package jadx.core.dex.instructions.args; import java.util.Objects; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnUtils; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Instruction argument. * Can be: register, literal, instruction or name */ public abstract class InsnArg extends Typed { private static final Logger LOG = LoggerFactory.getLogger(InsnArg.class); @Nullable("Null for method arguments") protected InsnNode parentInsn; public static RegisterArg reg(int regNum, ArgType type) { return new RegisterArg(regNum, type); } public static RegisterArg reg(InsnData insn, int argNum, ArgType type) { return reg(insn.getReg(argNum), type); } public static RegisterArg typeImmutableIfKnownReg(InsnData insn, int argNum, ArgType type) { if (type.isTypeKnown()) { return typeImmutableReg(insn.getReg(argNum), type); } return reg(insn.getReg(argNum), type); } public static RegisterArg typeImmutableReg(InsnData insn, int argNum, ArgType type) { return typeImmutableReg(insn.getReg(argNum), type); } public static RegisterArg typeImmutableReg(int regNum, ArgType type) { return reg(regNum, type, true); } public static RegisterArg reg(int regNum, ArgType type, boolean typeImmutable) { RegisterArg reg = new RegisterArg(regNum, type); if (typeImmutable) { reg.add(AFlag.IMMUTABLE_TYPE); } return reg; } public static LiteralArg lit(long literal, ArgType type) { return LiteralArg.makeWithFixedType(literal, type); } public static LiteralArg lit(InsnData insn, ArgType type) { return lit(insn.getLiteral(), type); } private static InsnWrapArg wrap(InsnNode insn) { insn.add(AFlag.WRAPPED); return new InsnWrapArg(insn); } public boolean isRegister() { return false; } public boolean isLiteral() { return false; } public boolean isInsnWrap() { return false; } public boolean isNamed() { return false; } @Nullable public InsnNode getParentInsn() { return parentInsn; } public void setParentInsn(@Nullable InsnNode parentInsn) { this.parentInsn = parentInsn; } @Nullable("if wrap failed") public InsnArg wrapInstruction(MethodNode mth, InsnNode insn) { return wrapInstruction(mth, insn, true); } @Nullable("if wrap failed") public InsnArg wrapInstruction(MethodNode mth, InsnNode insn, boolean unbind) { InsnNode parent = parentInsn; if (parent == null) { return null; } if (parent == insn) { LOG.debug("Can't wrap instruction info itself: {}", insn); return null; } int i = getArgIndex(parent, this); if (i == -1) { return null; } if (insn.getType() == InsnType.MOVE && this.isRegister()) { // preserve variable name for move insn (needed in `for-each` loop for iteration variable) String name = ((RegisterArg) this).getName(); if (name != null) { InsnArg arg = insn.getArg(0); if (arg.isRegister()) { ((RegisterArg) arg).setNameIfUnknown(name); } else if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); RegisterArg registerArg = wrapInsn.getResult(); if (registerArg != null) { registerArg.setNameIfUnknown(name); } } } } InsnArg arg = wrapInsnIntoArg(insn); InsnArg oldArg = parent.getArg(i); if (arg.getType() == ArgType.UNKNOWN) { // restore arg type if wrapped insn missing result arg.setType(oldArg.getType()); } parent.setArg(i, arg); InsnRemover.unbindArgUsage(mth, oldArg); if (unbind) { InsnRemover.unbindArgUsage(mth, this); // result not needed in wrapped insn InsnRemover.unbindResult(mth, insn); insn.setResult(null); } return arg; } private static int getArgIndex(InsnNode parent, InsnArg arg) { int count = parent.getArgsCount(); for (int i = 0; i < count; i++) { if (parent.getArg(i) == arg) { return i; } } return -1; } @NotNull public static InsnArg wrapInsnIntoArg(InsnNode insn) { InsnType type = insn.getType(); if (type == InsnType.CONST || type == InsnType.MOVE) { if (insn.contains(AFlag.FORCE_ASSIGN_INLINE)) { RegisterArg resArg = insn.getResult(); InsnArg arg = wrap(insn); if (resArg != null) { arg.setType(resArg.getType()); } return arg; } else { InsnArg arg = insn.getArg(0); insn.add(AFlag.DONT_GENERATE); return arg; } } return wrapArg(insn); } /** * Prefer {@link InsnArg#wrapInsnIntoArg(InsnNode)}. *

* This method don't support MOVE and CONST insns! */ public static InsnArg wrapArg(InsnNode insn) { RegisterArg resArg = insn.getResult(); InsnArg arg = wrap(insn); switch (insn.getType()) { case CONST: case MOVE: throw new JadxRuntimeException("Don't wrap MOVE or CONST insns: " + insn); case CONST_STR: arg.setType(ArgType.STRING); if (resArg != null) { resArg.setType(ArgType.STRING); } break; case CONST_CLASS: arg.setType(ArgType.CLASS); if (resArg != null) { resArg.setType(ArgType.CLASS); } break; default: if (resArg != null) { arg.setType(resArg.getType()); } break; } return arg; } public boolean isZeroLiteral() { return false; } public boolean isZeroConst() { if (isZeroLiteral()) { return true; } if (isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) this).getWrapInsn(); if (wrapInsn.getType() == InsnType.CONST) { return wrapInsn.getArg(0).isZeroLiteral(); } } return false; } public boolean isFalse() { if (isLiteral()) { LiteralArg litArg = (LiteralArg) this; return litArg.getLiteral() == 0 && Objects.equals(litArg.getType(), ArgType.BOOLEAN); } return false; } public boolean isTrue() { if (isLiteral()) { LiteralArg litArg = (LiteralArg) this; return litArg.getLiteral() == 1 && Objects.equals(litArg.getType(), ArgType.BOOLEAN); } return false; } public boolean isThis() { return contains(AFlag.THIS); } /** * Return true for 'this' from other classes (often occur in anonymous classes) */ public boolean isAnyThis() { if (contains(AFlag.THIS)) { return true; } InsnNode wrappedInsn = unwrap(); if (wrappedInsn != null && wrappedInsn.getType() == InsnType.IGET) { return wrappedInsn.getArg(0).isAnyThis(); } return false; } public InsnNode unwrap() { if (isInsnWrap()) { return ((InsnWrapArg) this).getWrapInsn(); } return null; } public boolean isConst() { return isLiteral() || (isInsnWrap() && ((InsnWrapArg) this).getWrapInsn().isConstInsn()); } public boolean isSameConst(InsnArg other) { if (isConst() && other.isConst()) { return this.equals(other); } return false; } public boolean isSameVar(RegisterArg arg) { if (arg == null) { return false; } if (isRegister()) { return ((RegisterArg) this).sameRegAndSVar(arg); } return false; } public boolean isSameVar(SSAVar ssaVar) { if (ssaVar == null) { return false; } if (isRegister()) { SSAVar thisSsaVar = ((RegisterArg) this).getSVar(); return Objects.equals(thisSsaVar, ssaVar); } return false; } public boolean isSameCodeVar(RegisterArg arg) { if (arg == null) { return false; } if (isRegister()) { return ((RegisterArg) this).sameCodeVar(arg); } return false; } public boolean isUseVar(RegisterArg arg) { return InsnUtils.containsVar(this, arg); } protected final T copyCommonParams(T copy) { copy.copyAttributesFrom(this); copy.setParentInsn(parentInsn); return copy; } public InsnArg duplicate() { return this; } public String toShortString() { return this.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnWrapArg.java ================================================ package jadx.core.dex.instructions.args; import org.jetbrains.annotations.NotNull; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.exceptions.JadxRuntimeException; public final class InsnWrapArg extends InsnArg { private final InsnNode wrappedInsn; /** * Use {@link InsnArg#wrapInsnIntoArg(InsnNode)} instead this constructor */ InsnWrapArg(@NotNull InsnNode insn) { RegisterArg result = insn.getResult(); this.type = result != null ? result.getType() : ArgType.UNKNOWN; this.wrappedInsn = insn; } public InsnNode getWrapInsn() { return wrappedInsn; } public InsnNode unWrapWithCopy() { InsnNode copy = wrappedInsn.copyWithoutResult(); copy.remove(AFlag.WRAPPED); return copy; } @Override public void setParentInsn(InsnNode parentInsn) { if (parentInsn == wrappedInsn) { throw new JadxRuntimeException("Can't wrap instruction info itself: " + parentInsn); } this.parentInsn = parentInsn; } @Override public InsnArg duplicate() { InsnWrapArg copy = new InsnWrapArg(wrappedInsn.copyWithoutResult()); copy.setType(type); return copyCommonParams(copy); } @Override public boolean isInsnWrap() { return true; } @Override public int hashCode() { return wrappedInsn.hashCode(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof InsnWrapArg)) { return false; } InsnWrapArg that = (InsnWrapArg) o; InsnNode thisInsn = wrappedInsn; InsnNode thatInsn = that.wrappedInsn; if (!thisInsn.isSame(thatInsn)) { return false; } int count = thisInsn.getArgsCount(); for (int i = 0; i < count; i++) { if (!thisInsn.getArg(i).equals(thatInsn.getArg(i))) { return false; } } return true; } @Override public String toShortString() { if (wrappedInsn.getType() == InsnType.CONST_STR) { return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")"; } return "(wrap:" + type + ":" + wrappedInsn.getType() + ')'; } @Override public String toString() { if (wrappedInsn.getType() == InsnType.CONST_STR) { return "(\"" + ((ConstStringNode) wrappedInsn).getString() + "\")"; } return "(wrap:" + type + ":" + wrappedInsn + ')'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/LiteralArg.java ================================================ package jadx.core.dex.instructions.args; import org.jetbrains.annotations.Nullable; import jadx.core.codegen.TypeGen; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; public final class LiteralArg extends InsnArg { public static LiteralArg make(long value, ArgType type) { return new LiteralArg(value, type); } public static LiteralArg makeWithFixedType(long value, ArgType type) { return new LiteralArg(value, fixLiteralType(value, type)); } private static ArgType fixLiteralType(long value, ArgType type) { if (value == 0 || type.isTypeKnown() || type.contains(PrimitiveType.LONG) || type.contains(PrimitiveType.DOUBLE)) { return type; } if (value == 1) { return ArgType.NARROW_NUMBERS; } if (value < 0) { return ArgType.NARROW_NEG_NUMBERS; } return ArgType.NARROW_NUMBERS_NO_BOOL; } public static LiteralArg litFalse() { return new LiteralArg(0, ArgType.BOOLEAN); } public static LiteralArg litTrue() { return new LiteralArg(1, ArgType.BOOLEAN); } private final long literal; private LiteralArg(long value, ArgType type) { if (value != 0 && type.isObject()) { throw new JadxRuntimeException("Wrong literal type: " + type + " for value: " + value); } this.literal = value; this.type = type; } public long getLiteral() { return literal; } @Override public void setType(ArgType type) { super.setType(type); } @Override public boolean isLiteral() { return true; } @Override public boolean isZeroLiteral() { return literal == 0; } public boolean isInteger() { switch (type.getPrimitiveType()) { case INT: case BYTE: case CHAR: case SHORT: case LONG: return true; default: return false; } } public boolean isNegative() { if (isInteger()) { return literal < 0; } if (type == ArgType.FLOAT) { float val = Float.intBitsToFloat(((int) literal)); return val < 0 && Float.isFinite(val); } if (type == ArgType.DOUBLE) { double val = Double.longBitsToDouble(literal); return val < 0 && Double.isFinite(val); } return false; } @Nullable public LiteralArg negate() { long neg; if (isInteger()) { neg = -literal; } else if (type == ArgType.FLOAT) { float val = Float.intBitsToFloat(((int) literal)); neg = Float.floatToIntBits(-val); } else if (type == ArgType.DOUBLE) { double val = Double.longBitsToDouble(literal); neg = Double.doubleToLongBits(-val); } else { return null; } return new LiteralArg(neg, type); } @Override public InsnArg duplicate() { return copyCommonParams(new LiteralArg(literal, type)); } @Override public int hashCode() { return (int) (literal ^ literal >>> 32) + 31 * getType().hashCode(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } LiteralArg that = (LiteralArg) o; return literal == that.literal && getType().equals(that.getType()); } @Override public String toShortString() { return Long.toString(literal); } @Override public String toString() { try { String value = TypeGen.literalToString(literal, getType(), StringUtils.getInstance(), true, false); if (getType().equals(ArgType.BOOLEAN) && (value.equals("true") || value.equals("false"))) { return value; } return '(' + value + ' ' + type + ')'; } catch (JadxRuntimeException ex) { // can't convert literal to string return "(" + literal + ' ' + type + ')'; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/Named.java ================================================ package jadx.core.dex.instructions.args; public interface Named { String getName(); void setName(String name); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/NamedArg.java ================================================ package jadx.core.dex.instructions.args; import org.jetbrains.annotations.NotNull; public final class NamedArg extends InsnArg implements Named { @NotNull private String name; public NamedArg(@NotNull String name, @NotNull ArgType type) { this.name = name; this.type = type; } @NotNull public String getName() { return name; } @Override public boolean isNamed() { return true; } @Override public void setName(@NotNull String name) { this.name = name; } @Override public InsnArg duplicate() { return copyCommonParams(new NamedArg(name, type)); } @Override public int hashCode() { return name.hashCode(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof NamedArg)) { return false; } return name.equals(((NamedArg) o).name); } @Override public String toShortString() { return name; } @Override public String toString() { return '(' + name + ' ' + type + ')'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/PrimitiveType.java ================================================ package jadx.core.dex.instructions.args; public enum PrimitiveType { BOOLEAN("Z", "boolean", ArgType.object("java.lang.Boolean")), CHAR("C", "char", ArgType.object("java.lang.Character")), BYTE("B", "byte", ArgType.object("java.lang.Byte")), SHORT("S", "short", ArgType.object("java.lang.Short")), INT("I", "int", ArgType.object("java.lang.Integer")), FLOAT("F", "float", ArgType.object("java.lang.Float")), LONG("J", "long", ArgType.object("java.lang.Long")), DOUBLE("D", "double", ArgType.object("java.lang.Double")), OBJECT("L", "OBJECT", ArgType.OBJECT), ARRAY("[", "ARRAY", ArgType.OBJECT_ARRAY), VOID("V", "void", ArgType.object("java.lang.Void")); private final String shortName; private final String longName; private final ArgType boxType; PrimitiveType(String shortName, String longName, ArgType boxType) { this.shortName = shortName; this.longName = longName; this.boxType = boxType; } public String getShortName() { return shortName; } public String getLongName() { return longName; } public ArgType getBoxType() { return boxType; } @Override public String toString() { return longName; } public boolean isObjectOrArray() { return this == OBJECT || this == ARRAY; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java ================================================ package jadx.core.dex.instructions.args; import java.util.Objects; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.exceptions.JadxRuntimeException; public class RegisterArg extends InsnArg implements Named { public static final String THIS_ARG_NAME = "this"; public static final String SUPER_ARG_NAME = "super"; protected final int regNum; // not null after SSATransform pass private SSAVar sVar; public RegisterArg(int rn, ArgType type) { this.type = type; // initial type, not changing, can be unknown this.regNum = rn; } public int getRegNum() { return regNum; } @Override public boolean isRegister() { return true; } public ArgType getInitType() { return type; } @Override public ArgType getType() { if (sVar != null) { return sVar.getTypeInfo().getType(); } return ArgType.UNKNOWN; } @Override public void setType(ArgType newType) { if (sVar == null) { throw new JadxRuntimeException("Can't change type for register without SSA variable: " + this); } sVar.setType(newType); } public void forceSetInitType(ArgType type) { this.type = type; } @Nullable public ArgType getImmutableType() { if (sVar != null) { return sVar.getImmutableType(); } if (contains(AFlag.IMMUTABLE_TYPE)) { return type; } return null; } @Override public boolean isTypeImmutable() { if (sVar != null) { return sVar.isTypeImmutable(); } return contains(AFlag.IMMUTABLE_TYPE); } public SSAVar getSVar() { return sVar; } void setSVar(@NotNull SSAVar sVar) { this.sVar = sVar; } public void resetSSAVar() { this.sVar = null; } @Override public String getName() { if (isSuper()) { return SUPER_ARG_NAME; } if (isThis()) { return THIS_ARG_NAME; } if (sVar == null) { return null; } return sVar.getName(); } private boolean isSuper() { return contains(AFlag.SUPER); } @Override public void setName(String name) { if (sVar != null && name != null) { sVar.setName(name); } } public void setNameIfUnknown(String name) { if (getName() == null) { setName(name); } } public boolean isNameEquals(InsnArg arg) { String n = getName(); if (n == null || !(arg instanceof Named)) { return false; } return n.equals(((Named) arg).getName()); } @Override public RegisterArg duplicate() { return duplicate(getRegNum(), getInitType(), sVar); } public RegisterArg duplicate(ArgType initType) { return duplicate(getRegNum(), initType, sVar); } public RegisterArg duplicateWithNewSSAVar(MethodNode mth) { RegisterArg duplicate = duplicate(regNum, getInitType(), null); mth.makeNewSVar(duplicate); return duplicate; } public RegisterArg duplicate(int regNum, @Nullable SSAVar sVar) { return duplicate(regNum, getInitType(), sVar); } public RegisterArg duplicate(int regNum, ArgType initType, @Nullable SSAVar sVar) { RegisterArg dup = new RegisterArg(regNum, initType); if (sVar != null) { // only 'set' here, 'assign' or 'use' will binds later dup.setSVar(sVar); } return copyCommonParams(dup); } @Nullable public InsnNode getAssignInsn() { if (sVar == null) { return null; } return sVar.getAssign().getParentInsn(); } public boolean equalRegisterAndType(RegisterArg arg) { return regNum == arg.regNum && type.equals(arg.type); } public boolean sameRegAndSVar(InsnArg arg) { if (this == arg) { return true; } if (!arg.isRegister()) { return false; } RegisterArg reg = (RegisterArg) arg; return regNum == reg.getRegNum() && Objects.equals(sVar, reg.getSVar()); } public boolean sameReg(InsnArg arg) { if (!arg.isRegister()) { return false; } return regNum == ((RegisterArg) arg).getRegNum(); } public boolean sameType(InsnArg arg) { return this.getType().equals(arg.getType()); } public boolean sameCodeVar(RegisterArg arg) { return this.getSVar().getCodeVar() == arg.getSVar().getCodeVar(); } public boolean isLinkedToOtherSsaVars() { return getSVar().getCodeVar().getSsaVars().size() > 1; } @Override public int hashCode() { return regNum; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof RegisterArg)) { return false; } RegisterArg other = (RegisterArg) obj; return regNum == other.regNum && Objects.equals(sVar, other.getSVar()); } @Override public String toShortString() { StringBuilder sb = new StringBuilder(); sb.append("r").append(regNum); if (sVar != null) { sb.append('v').append(sVar.getVersion()); } return sb.toString(); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("(r").append(regNum); if (sVar != null) { sb.append('v').append(sVar.getVersion()); } if (getName() != null) { sb.append(" '").append(getName()).append('\''); } ArgType type = sVar != null ? getType() : null; if (type != null) { sb.append(' ').append(type); } ArgType initType = getInitType(); if (type == null || (!type.equals(initType) && !type.isTypeKnown())) { sb.append(" I:").append(initType); } if (!isAttrStorageEmpty()) { sb.append(' ').append(getAttributesString()); } sb.append(')'); return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/SSAVar.java ================================================ package jadx.core.dex.instructions.args; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.typeinference.TypeInfo; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; public class SSAVar implements Comparable { private static final Logger LOG = LoggerFactory.getLogger(SSAVar.class); private static final Comparator SSA_VAR_COMPARATOR = Comparator.comparingInt(SSAVar::getRegNum).thenComparingInt(SSAVar::getVersion); private final int regNum; private final int version; private RegisterArg assign; private final List useList = new ArrayList<>(2); private List usedInPhi = null; private final TypeInfo typeInfo = new TypeInfo(); @Nullable("Set in InitCodeVariables pass") private CodeVar codeVar; public SSAVar(int regNum, int v, @NotNull RegisterArg assign) { this.regNum = regNum; this.version = v; this.assign = assign; assign.setSVar(this); } public int getRegNum() { return regNum; } public int getVersion() { return version; } public @NotNull RegisterArg getAssign() { return assign; } public @Nullable InsnNode getAssignInsn() { return assign.getParentInsn(); } public void setAssign(@NotNull RegisterArg assign) { RegisterArg oldAssign = this.assign; if (oldAssign == null) { this.assign = assign; } else if (oldAssign != assign) { oldAssign.resetSSAVar(); this.assign = assign; } } public List getUseList() { return useList; } public int getUseCount() { return useList.size(); } @Nullable public ArgType getImmutableType() { if (isTypeImmutable()) { return assign.getInitType(); } return null; } public boolean isTypeImmutable() { return assign.contains(AFlag.IMMUTABLE_TYPE); } public void markAsImmutable(ArgType type) { assign.add(AFlag.IMMUTABLE_TYPE); ArgType initType = assign.getInitType(); if (!initType.equals(type)) { assign.forceSetInitType(type); if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Update immutable type at var {} assign with type: {} previous type: {}", this.toShortString(), type, initType); } } } public void setType(ArgType type) { ArgType imType = getImmutableType(); if (imType != null && !imType.equals(type)) { throw new JadxRuntimeException("Can't change immutable type " + imType + " to " + type + " for " + this); } updateType(type); } public void forceSetType(ArgType type) { updateType(type); } private void updateType(ArgType type) { typeInfo.setType(type); if (codeVar != null) { codeVar.setType(type); } } public void use(RegisterArg arg) { if (arg.getSVar() != null) { arg.getSVar().removeUse(arg); } arg.setSVar(this); useList.add(arg); } public void removeUse(RegisterArg arg) { useList.removeIf(registerArg -> registerArg == arg); } public void addUsedInPhi(PhiInsn phiInsn) { if (usedInPhi == null) { usedInPhi = new ArrayList<>(1); } usedInPhi.add(phiInsn); } public void removeUsedInPhi(PhiInsn phiInsn) { if (usedInPhi != null) { usedInPhi.removeIf(insn -> insn == phiInsn); if (usedInPhi.isEmpty()) { usedInPhi = null; } } } public void updateUsedInPhiList() { this.usedInPhi = null; for (RegisterArg reg : useList) { InsnNode parentInsn = reg.getParentInsn(); if (parentInsn != null && parentInsn.getType() == InsnType.PHI) { addUsedInPhi((PhiInsn) parentInsn); } } } @Nullable public PhiInsn getOnlyOneUseInPhi() { if (usedInPhi != null && usedInPhi.size() == 1) { return usedInPhi.get(0); } return null; } public List getUsedInPhi() { if (usedInPhi == null) { return Collections.emptyList(); } return usedInPhi; } /** * Concat assign PHI insn and usedInPhi */ public List getPhiList() { InsnNode assignInsn = getAssign().getParentInsn(); if (assignInsn != null && assignInsn.getType() == InsnType.PHI) { PhiInsn assignPhi = (PhiInsn) assignInsn; if (usedInPhi == null) { return Collections.singletonList(assignPhi); } List list = new ArrayList<>(1 + usedInPhi.size()); list.add(assignPhi); list.addAll(usedInPhi); return list; } if (usedInPhi == null) { return Collections.emptyList(); } return usedInPhi; } public boolean isAssignInPhi() { InsnNode assignInsn = getAssignInsn(); return assignInsn != null && assignInsn.getType() == InsnType.PHI; } public boolean isUsedInPhi() { return usedInPhi != null && !usedInPhi.isEmpty(); } public void setName(String name) { if (name != null) { if (codeVar == null) { throw new JadxRuntimeException("CodeVar not initialized for name set in SSAVar: " + this); } codeVar.setName(name); } } public String getName() { if (codeVar == null) { return null; } return codeVar.getName(); } public TypeInfo getTypeInfo() { return typeInfo; } @NotNull public CodeVar getCodeVar() { if (codeVar == null) { throw new JadxRuntimeException("Code variable not set in " + this); } return codeVar; } public void setCodeVar(@NotNull CodeVar codeVar) { this.codeVar = codeVar; codeVar.addSsaVar(this); ArgType imType = getImmutableType(); if (imType != null) { codeVar.setType(imType); } } public void resetTypeAndCodeVar() { if (!isTypeImmutable()) { updateType(ArgType.UNKNOWN); } this.typeInfo.getBounds().clear(); this.codeVar = null; } public boolean isCodeVarSet() { return codeVar != null; } public String getDetailedVarInfo(MethodNode mth) { Set types = new HashSet<>(); Set names = Collections.emptySet(); List useArgs = new ArrayList<>(1 + useList.size()); useArgs.add(assign); useArgs.addAll(useList); if (mth.contains(AType.LOCAL_VARS_DEBUG_INFO)) { names = new HashSet<>(); for (RegisterArg arg : useArgs) { RegDebugInfoAttr debugInfoAttr = arg.get(AType.REG_DEBUG_INFO); if (debugInfoAttr != null) { names.add(debugInfoAttr.getName()); types.add(debugInfoAttr.getRegType()); } } } for (RegisterArg arg : useArgs) { ArgType initType = arg.getInitType(); if (initType.isTypeKnown()) { types.add(initType); } ArgType type = arg.getType(); if (type.isTypeKnown()) { types.add(type); } } StringBuilder sb = new StringBuilder(); sb.append('r').append(regNum).append('v').append(version); if (!names.isEmpty()) { String orderedNames = names.stream() .sorted() .collect(Collectors.joining(", ", "[", "]")); sb.append(", names: ").append(orderedNames); } if (!types.isEmpty()) { String orderedTypes = types.stream() .map(String::valueOf) .sorted() .collect(Collectors.joining(", ", "[", "]")); sb.append(", types: ").append(orderedTypes); } return sb.toString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof SSAVar)) { return false; } SSAVar ssaVar = (SSAVar) o; return regNum == ssaVar.regNum && version == ssaVar.version; } @Override public int hashCode() { return 31 * regNum + version; } @Override public int compareTo(@NotNull SSAVar o) { return SSA_VAR_COMPARATOR.compare(this, o); } public String toShortString() { return "r" + regNum + 'v' + version; } @Override public String toString() { return toShortString() + (StringUtils.notEmpty(getName()) ? " '" + getName() + "' " : "") + ' ' + typeInfo.getType(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/Typed.java ================================================ package jadx.core.dex.instructions.args; import jadx.core.dex.attributes.AttrNode; public abstract class Typed extends AttrNode { protected ArgType type; public ArgType getType() { return type; } public void setType(ArgType type) { this.type = type; } public boolean isTypeImmutable() { return false; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/args/VarName.java ================================================ package jadx.core.dex.instructions.args; public class VarName { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return name; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/CustomLambdaCall.java ================================================ package jadx.core.dex.instructions.invokedynamic; import java.util.List; import org.jetbrains.annotations.NotNull; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.MethodHandleType; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.InvokeCustomNode; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.NamedArg; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; public class CustomLambdaCall { /** * Expect LambdaMetafactory.metafactory method */ public static boolean isLambdaInvoke(List values) { if (values.size() < 6) { return false; } EncodedValue mthRef = values.get(0); if (mthRef.getType() != EncodedType.ENCODED_METHOD_HANDLE) { return false; } IMethodHandle methodHandle = (IMethodHandle) mthRef.getValue(); if (methodHandle.getType() != MethodHandleType.INVOKE_STATIC) { return false; } IMethodRef methodRef = methodHandle.getMethodRef(); if (!methodRef.getParentClassType().equals("Ljava/lang/invoke/LambdaMetafactory;")) { return false; } String mthName = methodRef.getName(); return mthName.equals("metafactory") || mthName.equals("altMetafactory"); } public static InvokeCustomNode buildLambdaMethodCall(MethodNode mth, InsnData insn, boolean isRange, List values) { IMethodHandle callMthHandle = (IMethodHandle) values.get(4).getValue(); if (callMthHandle.getType().isField()) { throw new JadxRuntimeException("Not yet supported"); } InvokeCustomNode resNode = buildMethodCall(mth, insn, isRange, values, callMthHandle); int resReg = insn.getResultReg(); if (resReg != -1) { resNode.setResult(InsnArg.reg(resReg, mth.getReturnType())); } return resNode; } @NotNull private static InvokeCustomNode buildMethodCall(MethodNode mth, InsnData insn, boolean isRange, List values, IMethodHandle callMthHandle) { RootNode root = mth.root(); IMethodProto lambdaProto = (IMethodProto) values.get(2).getValue(); MethodInfo lambdaInfo = MethodInfo.fromMethodProto(root, mth.getParentClass().getClassInfo(), "", lambdaProto); MethodHandleType methodHandleType = callMthHandle.getType(); InvokeCustomNode invokeCustomNode = new InvokeCustomNode(lambdaInfo, insn, false, isRange); invokeCustomNode.setHandleType(methodHandleType); ClassInfo implCls = ClassInfo.fromType(root, lambdaInfo.getReturnType()); String implName = (String) values.get(1).getValue(); IMethodProto implProto = (IMethodProto) values.get(3).getValue(); MethodInfo implMthInfo = MethodInfo.fromMethodProto(root, implCls, implName, implProto); invokeCustomNode.setImplMthInfo(implMthInfo); MethodInfo callMthInfo = MethodInfo.fromRef(root, callMthHandle.getMethodRef()); InvokeNode invokeNode = buildInvokeNode(methodHandleType, invokeCustomNode, callMthInfo); if (methodHandleType == MethodHandleType.INVOKE_CONSTRUCTOR) { ConstructorInsn ctrInsn = new ConstructorInsn(mth, invokeNode); invokeCustomNode.setCallInsn(ctrInsn); } else { invokeCustomNode.setCallInsn(invokeNode); } MethodNode callMth = root.resolveMethod(callMthInfo); if (callMth != null) { invokeCustomNode.getCallInsn().addAttr(callMth); if (callMth.getAccessFlags().isSynthetic() && callMth.getParentClass().equals(mth.getParentClass())) { // inline only synthetic methods from same class callMth.add(AFlag.DONT_GENERATE); invokeCustomNode.setInlineInsn(true); } } if (!invokeCustomNode.isInlineInsn()) { IMethodProto effectiveMthProto = (IMethodProto) values.get(5).getValue(); List args = Utils.collectionMap(effectiveMthProto.getArgTypes(), ArgType::parse); boolean sameArgs = args.equals(callMthInfo.getArgumentsTypes()); invokeCustomNode.setUseRef(sameArgs); } // prevent args inlining into not generated invoke custom node for (InsnArg arg : invokeCustomNode.getArguments()) { arg.add(AFlag.DONT_INLINE); } return invokeCustomNode; } @NotNull private static InvokeNode buildInvokeNode(MethodHandleType methodHandleType, InvokeCustomNode invokeCustomNode, MethodInfo callMthInfo) { InvokeType invokeType = InvokeCustomUtils.convertInvokeType(methodHandleType); int callArgsCount = callMthInfo.getArgsCount(); boolean instanceCall = invokeType != InvokeType.STATIC; if (instanceCall) { callArgsCount++; } InvokeNode invokeNode = new InvokeNode(callMthInfo, invokeType, callArgsCount); // copy insn args int argsCount = invokeCustomNode.getArgsCount(); for (int i = 0; i < argsCount; i++) { InsnArg arg = invokeCustomNode.getArg(i); invokeNode.addArg(arg.duplicate()); } if (callArgsCount > argsCount) { // fill remaining args with NamedArg int callArgNum = argsCount; if (instanceCall) { callArgNum--; // start from instance type } List callArgTypes = callMthInfo.getArgumentsTypes(); for (int i = argsCount; i < callArgsCount; i++) { ArgType argType; if (callArgNum < 0) { // instance arg type argType = callMthInfo.getDeclClass().getType(); } else { argType = callArgTypes.get(callArgNum++); } invokeNode.addArg(new NamedArg("v" + i, argType)); } } return invokeNode; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/CustomRawCall.java ================================================ package jadx.core.dex.instructions.invokedynamic; import java.util.ArrayList; import java.util.List; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.InvokeCustomRawNode; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.EncodedValueUtils.buildLookupArg; import static jadx.core.utils.EncodedValueUtils.convertToInsnArg; /** * Show `invoke-custom` similar to polymorphic call */ public class CustomRawCall { public static InsnNode build(MethodNode mth, InsnData insn, boolean isRange, List values) { IMethodHandle resolveHandle = (IMethodHandle) values.get(0).getValue(); String invokeName = (String) values.get(1).getValue(); IMethodProto invokeProto = (IMethodProto) values.get(2).getValue(); List resolveArgs = buildArgs(mth, values); if (resolveHandle.getType().isField()) { throw new JadxRuntimeException("Field handle not yet supported"); } RootNode root = mth.root(); MethodInfo resolveMth = MethodInfo.fromRef(root, resolveHandle.getMethodRef()); InvokeType resolveInvokeType = InvokeCustomUtils.convertInvokeType(resolveHandle.getType()); InvokeNode resolve = new InvokeNode(resolveMth, resolveInvokeType, resolveArgs.size()); resolveArgs.forEach(resolve::addArg); ClassInfo invokeCls = ClassInfo.fromType(root, ArgType.OBJECT); // type will be known at runtime MethodInfo invokeMth = MethodInfo.fromMethodProto(root, invokeCls, invokeName, invokeProto); InvokeCustomRawNode customRawNode = new InvokeCustomRawNode(resolve, invokeMth, insn, isRange); customRawNode.setCallSiteValues(values); return customRawNode; } private static List buildArgs(MethodNode mth, List values) { int valuesCount = values.size(); List list = new ArrayList<>(valuesCount); RootNode root = mth.root(); list.add(buildLookupArg(root)); // use `java.lang.invoke.MethodHandles.lookup()` as first arg for (int i = 1; i < valuesCount; i++) { EncodedValue value = values.get(i); try { list.add(convertToInsnArg(root, value)); } catch (Exception e) { mth.addWarnComment("Failed to build arg in invoke-custom insn: " + value, e); list.add(InsnArg.wrapArg(new ConstStringNode(value.toString()))); } } return list; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/CustomStringConcat.java ================================================ package jadx.core.dex.instructions.invokedynamic; import java.util.List; import java.util.Objects; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.MethodHandleType; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.instructions.ConstClassNode; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.EncodedValueUtils; import jadx.core.utils.exceptions.JadxRuntimeException; public class CustomStringConcat { public static boolean isStringConcat(List values) { if (values.size() < 4) { return false; } IMethodHandle methodHandle = (IMethodHandle) values.get(0).getValue(); if (methodHandle.getType() != MethodHandleType.INVOKE_STATIC) { return false; } IMethodRef methodRef = methodHandle.getMethodRef(); if (!methodRef.getName().equals("makeConcatWithConstants")) { return false; } if (!methodRef.getParentClassType().equals("Ljava/lang/invoke/StringConcatFactory;")) { return false; } if (!Objects.equals(values.get(1).getValue(), "makeConcatWithConstants")) { return false; } if (values.get(3).getType() != EncodedType.ENCODED_STRING) { return false; } return true; } public static InsnNode buildStringConcat(InsnData insn, boolean isRange, List values) { try { int argsCount = values.size() - 3 + insn.getRegsCount(); InsnNode concat = new InsnNode(InsnType.STR_CONCAT, argsCount); String recipe = (String) values.get(3).getValue(); processRecipe(recipe, concat, values, insn); int resReg = insn.getResultReg(); if (resReg != -1) { concat.setResult(InsnArg.reg(resReg, ArgType.STRING)); } return concat; } catch (Exception e) { InsnNode nop = new InsnNode(InsnType.NOP, 0); nop.add(AFlag.SYNTHETIC); nop.addAttr(AType.JADX_ERROR, new JadxError("Failed to process dynamic string concat: " + e.getMessage(), e)); return nop; } } private static void processRecipe(String recipe, InsnNode concat, List values, InsnData insn) { int len = recipe.length(); int offset = 0; int argNum = 0; int constNum = 4; StringBuilder sb = new StringBuilder(len); while (offset < len) { int cp = recipe.codePointAt(offset); offset += Character.charCount(cp); boolean argTag = cp == 1; boolean constTag = cp == 2; if (argTag || constTag) { if (sb.length() != 0) { concat.addArg(InsnArg.wrapArg(new ConstStringNode(sb.toString()))); sb.setLength(0); } if (argTag) { concat.addArg(InsnArg.reg(insn, argNum++, ArgType.UNKNOWN)); } else { InsnArg constArg = buildInsnArgFromEncodedValue(values.get(constNum++)); concat.addArg(constArg); } } else { sb.appendCodePoint(cp); } } if (sb.length() != 0) { concat.addArg(InsnArg.wrapArg(new ConstStringNode(sb.toString()))); } } private static InsnArg buildInsnArgFromEncodedValue(EncodedValue encodedValue) { Object value = EncodedValueUtils.convertToConstValue(encodedValue); if (value == null) { return InsnArg.lit(0, ArgType.UNKNOWN); } if (value instanceof LiteralArg) { return ((LiteralArg) value); } if (value instanceof ArgType) { return InsnArg.wrapArg(new ConstClassNode((ArgType) value)); } if (value instanceof String) { return InsnArg.wrapArg(new ConstStringNode(((String) value))); } throw new JadxRuntimeException("Can't build insn arg from encoded value: " + encodedValue); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/invokedynamic/InvokeCustomUtils.java ================================================ package jadx.core.dex.instructions.invokedynamic; import jadx.api.plugins.input.data.MethodHandleType; import jadx.core.dex.instructions.InvokeType; import jadx.core.utils.exceptions.JadxRuntimeException; public class InvokeCustomUtils { public static InvokeType convertInvokeType(MethodHandleType type) { switch (type) { case INVOKE_STATIC: return InvokeType.STATIC; case INVOKE_INSTANCE: return InvokeType.VIRTUAL; case INVOKE_DIRECT: case INVOKE_CONSTRUCTOR: return InvokeType.DIRECT; case INVOKE_INTERFACE: return InvokeType.INTERFACE; default: throw new JadxRuntimeException("Unsupported method handle type: " + type); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/java/JsrNode.java ================================================ package jadx.core.dex.instructions.java; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.TargetInsnNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; public class JsrNode extends TargetInsnNode { protected final int target; public JsrNode(int target) { this(InsnType.JAVA_JSR, target, 0); } protected JsrNode(InsnType type, int target, int argsCount) { super(type, argsCount); this.target = target; } public int getTarget() { return target; } @Override public InsnNode copy() { return copyCommonParams(new JsrNode(target)); } @Override public String toString() { return baseString() + " -> " + InsnUtils.formatOffset(target) + attributesString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/mods/ConstructorInsn.java ================================================ package jadx.core.dex.instructions.mods; import org.jetbrains.annotations.Nullable; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; public final class ConstructorInsn extends BaseInvokeNode { private final MethodInfo callMth; private final CallType callType; public enum CallType { CONSTRUCTOR, // just new instance SUPER, // super call THIS, // call constructor from other constructor SELF // call itself } public ConstructorInsn(MethodNode mth, InvokeNode invoke) { this(mth, invoke, invoke.getCallMth()); } public ConstructorInsn(MethodNode mth, InvokeNode invoke, MethodInfo callMth) { super(InsnType.CONSTRUCTOR, invoke.getArgsCount() - 1); this.callMth = callMth; this.callType = getCallType(mth, callMth.getDeclClass(), invoke.getArg(0)); int argsCount = invoke.getArgsCount(); for (int i = 1; i < argsCount; i++) { addArg(invoke.getArg(i)); } } private CallType getCallType(MethodNode mth, ClassInfo classType, InsnArg instanceArg) { if (!instanceArg.isThis()) { return CallType.CONSTRUCTOR; } if (!classType.equals(mth.getParentClass().getClassInfo())) { return CallType.SUPER; } if (callMth.getShortId().equals(mth.getMethodInfo().getShortId())) { // self constructor return CallType.SELF; } return CallType.THIS; } public ConstructorInsn(MethodInfo callMth, CallType callType) { super(InsnType.CONSTRUCTOR, callMth.getArgsCount()); this.callMth = callMth; this.callType = callType; } @Override public MethodInfo getCallMth() { return callMth; } @Override @Nullable public RegisterArg getInstanceArg() { return null; } public ClassInfo getClassType() { return callMth.getDeclClass(); } public CallType getCallType() { return callType; } public boolean isNewInstance() { return callType == CallType.CONSTRUCTOR; } public boolean isSuper() { return callType == CallType.SUPER; } public boolean isThis() { return callType == CallType.THIS; } public boolean isSelf() { return callType == CallType.SELF; } @Override public boolean isStaticCall() { return false; } @Override public int getFirstArgOffset() { return 0; } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof ConstructorInsn) || !super.isSame(obj)) { return false; } ConstructorInsn other = (ConstructorInsn) obj; return callMth.equals(other.callMth) && callType == other.callType; } @Override public InsnNode copy() { return copyCommonParams(new ConstructorInsn(callMth, callType)); } @Override public String toString() { return super.toString() + " call: " + callMth + " type: " + callType; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/instructions/mods/TernaryInsn.java ================================================ package jadx.core.dex.instructions.mods; import java.util.Collection; import java.util.function.Consumer; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.utils.InsnUtils; public final class TernaryInsn extends InsnNode { private IfCondition condition; public TernaryInsn(IfCondition condition, RegisterArg result, InsnArg th, InsnArg els) { this(); setResult(result); if (th.isFalse() && els.isTrue()) { // inverted this.condition = IfCondition.invert(condition); addArg(els); addArg(th); } else { this.condition = condition; addArg(th); addArg(els); } visitInsns(this::inheritMetadata); } private TernaryInsn() { super(InsnType.TERNARY, 2); } public IfCondition getCondition() { return condition; } public void simplifyCondition() { condition = IfCondition.simplify(condition); if (condition.getMode() == IfCondition.Mode.NOT) { invert(); } } private void invert() { condition = IfCondition.invert(condition); InsnArg tmp = getArg(0); setArg(0, getArg(1)); setArg(1, tmp); } @Override public void getRegisterArgs(Collection list) { super.getRegisterArgs(list); list.addAll(condition.getRegisterArgs()); } @Override public boolean replaceArg(InsnArg from, InsnArg to) { if (super.replaceArg(from, to)) { return true; } return condition.replaceArg(from, to); } public void visitInsns(Consumer visitor) { super.visitInsns(visitor); condition.visitInsns(visitor); } @Override public boolean isSame(InsnNode obj) { if (this == obj) { return true; } if (!(obj instanceof TernaryInsn) || !super.isSame(obj)) { return false; } TernaryInsn that = (TernaryInsn) obj; return condition.equals(that.condition); } @Override public InsnNode copy() { TernaryInsn copy = new TernaryInsn(); copy.condition = condition; return copyCommonParams(copy); } @Override public void rebindArgs() { super.rebindArgs(); for (RegisterArg reg : condition.getRegisterArgs()) { InsnNode parentInsn = reg.getParentInsn(); if (parentInsn != null) { parentInsn.rebindArgs(); } } } @Override public String toString() { return InsnUtils.formatOffset(offset) + ": TERNARY " + getResult() + " = (" + condition + ") ? " + getArg(0) + " : " + getArg(1); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/BlockNode.java ================================================ package jadx.core.dex.nodes; import java.util.ArrayList; import java.util.BitSet; import java.util.List; import org.jetbrains.annotations.NotNull; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttrNode; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.utils.BlockUtils; import jadx.core.utils.EmptyBitSet; import jadx.core.utils.InsnUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.Utils.lockList; public final class BlockNode extends AttrNode implements IBlock, Comparable { /** * Const ID */ private final int cid; /** * Position in blocks list (easier to use BitSet) */ private int pos; /** * Offset in methods bytecode */ private final int startOffset; private final List instructions = new ArrayList<>(2); private List predecessors = new ArrayList<>(1); private List successors = new ArrayList<>(1); private List cleanSuccessors; /** * All dominators, excluding self */ private BitSet doms = EmptyBitSet.EMPTY; /** * Post dominators, excluding self */ private BitSet postDoms = EmptyBitSet.EMPTY; /** * Dominance frontier */ private BitSet domFrontier; /** * Immediate dominator */ private BlockNode idom; /** * Immediate post dominator */ private BlockNode iPostDom; /** * Blocks on which dominates this block */ private List dominatesOn = new ArrayList<>(3); public BlockNode(int cid, int pos, int offset) { this.cid = cid; this.pos = pos; this.startOffset = offset; } public int getCId() { return cid; } void setPos(int id) { this.pos = id; } /** * Deprecated. Use {@link #getPos()}. */ @Deprecated public int getId() { return pos; } public int getPos() { return pos; } public List getPredecessors() { return predecessors; } public List getSuccessors() { return successors; } public List getCleanSuccessors() { return this.cleanSuccessors; } public void updateCleanSuccessors() { cleanSuccessors = cleanSuccessors(this); } public static void updateBlockPositions(List blocks) { int count = blocks.size(); for (int i = 0; i < count; i++) { blocks.get(i).setPos(i); } } public void lock() { try { List successorsList = successors; successors = lockList(successorsList); cleanSuccessors = successorsList == cleanSuccessors ? this.successors : lockList(cleanSuccessors); predecessors = lockList(predecessors); dominatesOn = lockList(dominatesOn); if (domFrontier == null) { throw new JadxRuntimeException("Dominance frontier not set for block: " + this); } } catch (Exception e) { throw new JadxRuntimeException("Failed to lock block: " + this, e); } } /** * Return all successor which are not exception handler or followed by loop back edge */ private static List cleanSuccessors(BlockNode block) { List sucList = block.getSuccessors(); if (sucList.isEmpty()) { return sucList; } List toRemove = new ArrayList<>(sucList.size()); for (BlockNode b : sucList) { if (BlockUtils.isExceptionHandlerPath(b)) { toRemove.add(b); } } if (block.contains(AFlag.LOOP_END)) { List loops = block.getAll(AType.LOOP); for (LoopInfo loop : loops) { toRemove.add(loop.getStart()); } } if (toRemove.isEmpty()) { return sucList; } List result = new ArrayList<>(sucList); result.removeAll(toRemove); return result; } @Override public List getInstructions() { return instructions; } public int getStartOffset() { return startOffset; } /** * Check if 'block' dominated on this node */ public boolean isDominator(BlockNode block) { return doms.get(block.getPos()); } /** * Dominators of this node (exclude itself) */ public BitSet getDoms() { return doms; } public void setDoms(BitSet doms) { this.doms = doms; } public BitSet getPostDoms() { return postDoms; } public void setPostDoms(BitSet postDoms) { this.postDoms = postDoms; } public BitSet getDomFrontier() { return domFrontier; } public void setDomFrontier(BitSet domFrontier) { this.domFrontier = domFrontier; } /** * Immediate dominator */ public BlockNode getIDom() { return idom; } public void setIDom(BlockNode idom) { this.idom = idom; } public BlockNode getIPostDom() { return iPostDom; } public void setIPostDom(BlockNode iPostDom) { this.iPostDom = iPostDom; } public List getDominatesOn() { return dominatesOn; } public void addDominatesOn(BlockNode block) { dominatesOn.add(block); } public boolean isSynthetic() { return contains(AFlag.SYNTHETIC); } public boolean isReturnBlock() { return contains(AFlag.RETURN); } public boolean isMthExitBlock() { return contains(AFlag.MTH_EXIT_BLOCK); } public boolean isEmpty() { return instructions.isEmpty(); } @Override public int hashCode() { return cid; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof BlockNode)) { return false; } BlockNode other = (BlockNode) obj; return cid == other.cid; } @Override public int compareTo(@NotNull BlockNode o) { return Integer.compare(cid, o.cid); } @Override public String baseString() { return Integer.toString(cid); } @Override public String toString() { return "B:" + cid + ':' + InsnUtils.formatOffset(startOffset); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java ================================================ package jadx.core.dex.nodes; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.DecompilationMode; import jadx.api.ICodeCache; import jadx.api.ICodeInfo; import jadx.api.JavaClass; import jadx.api.impl.SimpleCodeInfo; import jadx.api.impl.SimpleCodeWriter; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.api.metadata.annotations.VarRef; import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.input.data.IFieldData; import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.AnnotationDefaultAttr; import jadx.api.plugins.input.data.attributes.types.AnnotationDefaultClassAttr; import jadx.api.plugins.input.data.attributes.types.InnerClassesAttr; import jadx.api.plugins.input.data.attributes.types.InnerClsInfo; import jadx.api.plugins.input.data.attributes.types.SourceFileAttr; import jadx.api.plugins.input.data.impl.ListConsumer; import jadx.api.usage.IUsageInfoData; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.InlinedAttr; import jadx.core.dex.attributes.nodes.NotificationAttrNode; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.AccessInfo.AFType; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.nodes.utils.TypeUtils; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.dex.nodes.ProcessState.LOADED; import static jadx.core.dex.nodes.ProcessState.NOT_LOADED; public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeNode, IPackageUpdate, Comparable { private static final Logger LOG = LoggerFactory.getLogger(ClassNode.class); private final RootNode root; private final IClassData clsData; private final ClassInfo clsInfo; private PackageNode packageNode; private AccessInfo accessFlags; private ArgType superClass; private List interfaces; private List generics = Collections.emptyList(); private String inputFileName; private List methods; private List fields; private List innerClasses = Collections.emptyList(); private List inlinedClasses = Collections.emptyList(); // store smali private String smali; // store parent for inner classes or 'this' otherwise private ClassNode parentClass = this; private volatile ProcessState state = ProcessState.NOT_LOADED; private LoadStage loadStage = LoadStage.NONE; /** * Top level classes used in this class (only for top level classes, empty for inners) */ private List dependencies = Collections.emptyList(); /** * Top level classes needed for code generation stage */ private List codegenDeps = Collections.emptyList(); /** * Classes which uses this class */ private List useIn = Collections.emptyList(); /** * Methods which uses this class (by instructions only, definition is excluded) */ private List useInMth = Collections.emptyList(); // cache maps private Map mthInfoMap = Collections.emptyMap(); private JavaClass javaNode; public ClassNode(RootNode root, IClassData cls) { this.root = root; this.clsInfo = ClassInfo.fromType(root, ArgType.object(cls.getType())); this.packageNode = PackageNode.getForClass(root, clsInfo.getPackage(), this); this.clsData = cls.copy(); load(clsData, false); } private void load(IClassData cls, boolean reloading) { try { addAttrs(cls.getAttributes()); this.accessFlags = new AccessInfo(getAccessFlags(cls), AFType.CLASS); this.superClass = checkSuperType(cls); this.interfaces = Utils.collectionMap(cls.getInterfacesTypes(), ArgType::object); setInputFileName(cls.getInputFileName()); ListConsumer fieldsConsumer = new ListConsumer<>(fld -> FieldNode.build(this, fld)); ListConsumer methodsConsumer = new ListConsumer<>(mth -> MethodNode.build(this, mth)); cls.visitFieldsAndMethods(fieldsConsumer, methodsConsumer); this.fields = fieldsConsumer.getResult(); this.methods = methodsConsumer.getResult(); if (reloading) { restoreUsageData(); } initStaticValues(fields); processAttributes(this); processSpecialClasses(this); buildCache(); // TODO: implement module attribute parsing if (this.accessFlags.isModuleInfo()) { this.addWarnComment("Modules not supported yet"); } } catch (Exception e) { throw new JadxRuntimeException("Error decode class: " + clsInfo, e); } } private void restoreUsageData() { IUsageInfoData usageInfoData = root.getArgs().getUsageInfoCache().get(root); if (usageInfoData != null) { usageInfoData.applyForClass(this); } else { LOG.warn("Can't restore usage data for class: {}", this); } } private ArgType checkSuperType(IClassData cls) { String superType = cls.getSuperType(); if (superType == null) { if (clsInfo.getType().getObject().equals(Consts.CLASS_OBJECT)) { // java.lang.Object don't have super class return null; } if (this.accessFlags.isModuleInfo()) { // module-info also don't have super class return null; } throw new JadxRuntimeException("No super class in " + clsInfo.getType()); } return ArgType.object(superType); } public void updateGenericClsData(List generics, ArgType superClass, List interfaces) { this.generics = generics; this.superClass = superClass; this.interfaces = interfaces; } private static void processSpecialClasses(ClassNode cls) { if (cls.getName().equals("package-info") && cls.getFields().isEmpty() && cls.getMethods().isEmpty()) { cls.add(AFlag.PACKAGE_INFO); cls.add(AFlag.DONT_RENAME); } } private static void processAttributes(ClassNode cls) { // move AnnotationDefault from cls to methods (dex specific) AnnotationDefaultClassAttr defAttr = cls.get(JadxAttrType.ANNOTATION_DEFAULT_CLASS); if (defAttr != null) { cls.remove(JadxAttrType.ANNOTATION_DEFAULT_CLASS); for (Map.Entry entry : defAttr.getValues().entrySet()) { MethodNode mth = cls.searchMethodByShortName(entry.getKey()); if (mth != null) { mth.addAttr(new AnnotationDefaultAttr(entry.getValue())); } else { cls.addWarnComment("Method from annotation default annotation not found: " + entry.getKey()); } } } // check source file attribute if (!cls.checkSourceFilenameAttr()) { cls.remove(JadxAttrType.SOURCE_FILE); } } private int getAccessFlags(IClassData cls) { InnerClassesAttr innerClassesAttr = get(JadxAttrType.INNER_CLASSES); if (innerClassesAttr != null) { InnerClsInfo innerClsInfo = innerClassesAttr.getMap().get(cls.getType()); if (innerClsInfo != null) { return innerClsInfo.getAccessFlags(); } } return cls.getAccessFlags(); } public static ClassNode addSyntheticClass(RootNode root, String name, int accessFlags) { ClassInfo clsInfo = ClassInfo.fromName(root, name); ClassNode existCls = root.resolveClass(clsInfo); if (existCls != null) { throw new JadxRuntimeException("Class already exist: " + name); } return addSyntheticClass(root, clsInfo, accessFlags); } public static ClassNode addSyntheticClass(RootNode root, ClassInfo clsInfo, int accessFlags) { ClassNode cls = new ClassNode(root, clsInfo, accessFlags); cls.add(AFlag.SYNTHETIC); cls.setInputFileName("synthetic"); cls.setState(ProcessState.PROCESS_COMPLETE); root.addClassNode(cls); return cls; } // Create empty class private ClassNode(RootNode root, ClassInfo clsInfo, int accessFlags) { this.root = root; this.clsData = null; this.clsInfo = clsInfo; this.interfaces = new ArrayList<>(); this.methods = new ArrayList<>(); this.fields = new ArrayList<>(); this.accessFlags = new AccessInfo(accessFlags, AFType.CLASS); this.packageNode = PackageNode.getForClass(root, clsInfo.getPackage(), this); } private void initStaticValues(List fields) { if (fields.isEmpty()) { return; } // bytecode can omit field initialization to 0 (of any type) // add explicit init to all static final fields // incorrect initializations will be removed if assign found in class init for (FieldNode fld : fields) { AccessInfo accFlags = fld.getAccessFlags(); if (accFlags.isStatic() && accFlags.isFinal() && fld.get(JadxAttrType.CONSTANT_VALUE) == null) { fld.addAttr(EncodedValue.NULL); } } } private boolean checkSourceFilenameAttr() { SourceFileAttr sourceFileAttr = get(JadxAttrType.SOURCE_FILE); if (sourceFileAttr == null) { return true; } String fileName = sourceFileAttr.getFileName(); if (fileName.endsWith(".java")) { fileName = fileName.substring(0, fileName.length() - 5); } if (fileName.isEmpty() || fileName.equals("SourceFile")) { return false; } if (clsInfo != null) { String name = clsInfo.getShortName(); if (fileName.equals(name)) { return false; } ClassInfo parentCls = clsInfo.getParentClass(); while (parentCls != null) { String parentName = parentCls.getShortName(); if (parentName.equals(fileName) || parentName.startsWith(fileName + '$')) { return false; } parentCls = parentCls.getParentClass(); } if (fileName.contains("$") && fileName.endsWith('$' + name)) { return false; } if (name.contains("$") && name.startsWith(fileName)) { return false; } } return true; } public boolean checkProcessed() { return getTopParentClass().getState().isProcessComplete(); } public void ensureProcessed() { if (!checkProcessed()) { ClassNode topParentClass = getTopParentClass(); throw new JadxRuntimeException("Expected class to be processed at this point," + " class: " + topParentClass + ", state: " + topParentClass.getState()); } } public ICodeInfo decompile() { return decompile(true); } private static final Object DECOMPILE_WITH_MODE_SYNC = new Object(); /** * WARNING: Slow operation! Use with caution! */ public ICodeInfo decompileWithMode(DecompilationMode mode) { switch (mode) { case AUTO: case RESTRUCTURE: return decompile(true); case SIMPLE: case FALLBACK: synchronized (DECOMPILE_WITH_MODE_SYNC) { try { unload(); ICodeInfo code = root.getProcessClasses().forceGenerateCodeForMode(this, mode); return Utils.getOrElse(code, ICodeInfo.EMPTY); } finally { unload(); } } default: throw new JadxRuntimeException("Unknown mode: " + mode); } } public ICodeInfo getCode() { return decompile(true); } public ICodeInfo reloadCode() { add(AFlag.CLASS_DEEP_RELOAD); return decompile(false); } public void unloadCode() { if (state == NOT_LOADED) { return; } add(AFlag.CLASS_UNLOADED); unloadFromCache(); deepUnload(); } public void deepUnload() { if (clsData == null) { // manually added class return; } clearAttributes(); unload(); root().getConstValues().removeForClass(this); load(clsData, true); innerClasses.forEach(ClassNode::deepUnload); } public void unloadFromCache() { if (isInner()) { return; } ICodeCache codeCache = root().getCodeCache(); codeCache.remove(getRawName()); } private synchronized ICodeInfo decompile(boolean searchInCache) { if (isInner()) { return ICodeInfo.EMPTY; } ICodeCache codeCache = root().getCodeCache(); String clsRawName = getRawName(); if (searchInCache) { ICodeInfo code = codeCache.get(clsRawName); if (code != ICodeInfo.EMPTY) { return code; } } ICodeInfo codeInfo = generateClassCode(); if (codeInfo != ICodeInfo.EMPTY) { codeCache.add(clsRawName, codeInfo); } return codeInfo; } private ICodeInfo generateClassCode() { try { if (Consts.DEBUG) { LOG.debug("Decompiling class: {}", this); } ICodeInfo codeInfo = root.getProcessClasses().generateCode(this); processDefinitionAnnotations(codeInfo); return codeInfo; } catch (StackOverflowError | Exception e) { addError("Code generation failed", e); return new SimpleCodeInfo(Utils.getStackTrace(e)); } } /** * Save node definition positions found in code */ private static void processDefinitionAnnotations(ICodeInfo codeInfo) { Map annotations = codeInfo.getCodeMetadata().getAsMap(); if (annotations.isEmpty()) { return; } for (Map.Entry entry : annotations.entrySet()) { ICodeAnnotation ann = entry.getValue(); if (ann.getAnnType() == AnnType.DECLARATION) { NodeDeclareRef declareRef = (NodeDeclareRef) ann; int pos = entry.getKey(); declareRef.setDefPos(pos); declareRef.getNode().setDefPosition(pos); } } // validate var refs annotations.values().removeIf(v -> { if (v.getAnnType() == ICodeAnnotation.AnnType.VAR_REF) { VarRef varRef = (VarRef) v; if (varRef.getRefPos() == 0) { if (LOG.isDebugEnabled()) { LOG.debug("Var reference '{}' incorrect (ref pos is zero) and was removed from metadata", varRef); } return true; } return false; } return false; }); } @Nullable public ICodeInfo getCodeFromCache() { ICodeCache codeCache = root().getCodeCache(); String clsRawName = getRawName(); ICodeInfo codeInfo = codeCache.get(clsRawName); if (codeInfo == ICodeInfo.EMPTY) { return null; } return codeInfo; } @Override public void load() { for (MethodNode mth : getMethods()) { try { mth.load(); } catch (Exception e) { mth.addError("Method load error", e); } } for (ClassNode innerCls : getInnerClasses()) { innerCls.load(); } setState(LOADED); } @Override public void unload() { if (state == NOT_LOADED) { return; } synchronized (clsInfo) { // decompilation sync methods.forEach(MethodNode::unload); innerClasses.forEach(ClassNode::unload); fields.forEach(FieldNode::unload); unloadAttributes(); setState(NOT_LOADED); this.loadStage = LoadStage.NONE; this.smali = null; } } private void buildCache() { mthInfoMap = new HashMap<>(methods.size()); for (MethodNode mth : methods) { mthInfoMap.put(mth.getMethodInfo(), mth); } } @Nullable public ArgType getSuperClass() { return superClass; } public List getInterfaces() { return interfaces; } public List getGenericTypeParameters() { return generics; } public ArgType getType() { ArgType clsType = clsInfo.getType(); if (Utils.notEmpty(generics)) { return ArgType.generic(clsType, generics); } return clsType; } public List getMethods() { return methods; } public List getFields() { return fields; } public void addField(FieldNode fld) { if (fields == null || fields.isEmpty()) { fields = new ArrayList<>(1); } fields.add(fld); } public @Nullable IFieldInfoRef getConstField(Object obj) { return getConstField(obj, true); } public @Nullable IFieldInfoRef getConstField(Object obj, boolean searchGlobal) { return root().getConstValues().getConstField(this, obj, searchGlobal); } public @Nullable IFieldInfoRef getConstFieldByLiteralArg(LiteralArg arg) { return root().getConstValues().getConstFieldByLiteralArg(this, arg); } public FieldNode searchField(FieldInfo field) { for (FieldNode f : fields) { if (f.getFieldInfo().equals(field)) { return f; } } return null; } public FieldNode searchFieldByNameAndType(FieldInfo field) { for (FieldNode f : fields) { if (f.getFieldInfo().equalsNameAndType(field)) { return f; } } return null; } public FieldNode searchFieldByName(String name) { for (FieldNode f : fields) { if (f.getName().equals(name)) { return f; } } return null; } public FieldNode searchFieldByShortId(String shortId) { for (FieldNode f : fields) { if (f.getFieldInfo().getShortId().equals(shortId)) { return f; } } return null; } public MethodNode searchMethod(MethodInfo mth) { return mthInfoMap.get(mth); } public MethodNode searchMethodByShortId(String shortId) { for (MethodNode m : methods) { if (m.getMethodInfo().getShortId().equals(shortId)) { return m; } } return null; } /** * Return first method by original short name * Note: methods are not unique by name (class can have several methods with same name but different * signature) */ @Nullable public MethodNode searchMethodByShortName(String name) { for (MethodNode m : methods) { if (m.getMethodInfo().getName().equals(name)) { return m; } } return null; } @Override public ClassNode getDeclaringClass() { return isInner() ? parentClass : null; } public ClassNode getParentClass() { return parentClass; } public void notInner() { this.clsInfo.notInner(root); this.parentClass = this; } /** * Change class name and package (if full name provided) * Leading dot can be used to move to default package. * Package for inner classes can't be changed. */ @Override public void rename(String newName) { if (newName.indexOf('.') == -1) { clsInfo.changeShortName(newName); return; } // full name provided ClassInfo newClsInfo = ClassInfo.fromNameWithoutCache(root, newName, clsInfo.isInner()); // change class package String newPkg = newClsInfo.getPackage(); String newShortName = newClsInfo.getShortName(); if (clsInfo.isInner()) { if (!newPkg.equals(clsInfo.getPackage())) { addWarn("Can't change package for inner class: " + this + " to " + newName); } clsInfo.changeShortName(newShortName); } else { if (changeClassNodePackage(newPkg)) { clsInfo.changePkgAndName(newPkg, newShortName); } else { clsInfo.changeShortName(newShortName); } } } private boolean changeClassNodePackage(String fullPkg) { if (fullPkg.equals(clsInfo.getAliasPkg())) { return false; } if (clsInfo.isInner()) { throw new JadxRuntimeException("Can't change package for inner class: " + clsInfo); } root.removeClsFromPackage(packageNode, this); packageNode = PackageNode.getForClass(root, fullPkg, this); root.sortPackages(); return true; } public void removeAlias() { if (!clsInfo.isInner()) { changeClassNodePackage(clsInfo.getPackage()); } clsInfo.removeAlias(); } @Override public void onParentPackageUpdate(PackageNode updatedPkg) { if (clsInfo.isInner()) { return; } clsInfo.changePkg(packageNode.getAliasPkgInfo().getFullName()); } public PackageNode getPackageNode() { return packageNode; } public ClassNode getTopParentClass() { ClassNode parent = getParentClass(); return parent == this ? this : parent.getTopParentClass(); } public void visitParentClasses(Consumer consumer) { ClassNode currentCls = this; ClassNode parentCls = currentCls.getParentClass(); while (parentCls != currentCls) { consumer.accept(parentCls); currentCls = parentCls; parentCls = currentCls.getParentClass(); } } public void visitSuperTypes(BiConsumer consumer) { TypeUtils typeUtils = root.getTypeUtils(); ArgType thisType = this.getType(); if (!superClass.equals(ArgType.OBJECT)) { consumer.accept(thisType, superClass); typeUtils.visitSuperTypes(superClass, consumer); } for (ArgType iface : interfaces) { consumer.accept(thisType, iface); typeUtils.visitSuperTypes(iface, consumer); } } public boolean hasNotGeneratedParent() { if (contains(AFlag.DONT_GENERATE)) { return true; } ClassNode parent = getParentClass(); if (parent == this) { return false; } return parent.hasNotGeneratedParent(); } public List getInnerClasses() { return innerClasses; } public List getInlinedClasses() { return inlinedClasses; } /** * Get all inner and inlined classes recursively * * @param resultClassesSet all identified inner and inlined classes are added to this set */ public void getInnerAndInlinedClassesRecursive(Set resultClassesSet) { for (ClassNode innerCls : innerClasses) { if (resultClassesSet.add(innerCls)) { innerCls.getInnerAndInlinedClassesRecursive(resultClassesSet); } } for (ClassNode inlinedCls : inlinedClasses) { if (resultClassesSet.add(inlinedCls)) { inlinedCls.getInnerAndInlinedClassesRecursive(resultClassesSet); } } } public void addInnerClass(ClassNode cls) { if (innerClasses.isEmpty()) { innerClasses = new ArrayList<>(5); } innerClasses.add(cls); cls.parentClass = this; } public void addInlinedClass(ClassNode cls) { if (inlinedClasses.isEmpty()) { inlinedClasses = new ArrayList<>(5); } cls.addAttr(new InlinedAttr(this)); inlinedClasses.add(cls); } public boolean isEnum() { return getAccessFlags().isEnum() && getSuperClass() != null && getSuperClass().getObject().equals(ArgType.ENUM.getObject()); } public boolean isAnonymous() { return contains(AType.ANONYMOUS_CLASS); } public boolean isSynthetic() { return contains(AFlag.SYNTHETIC); } public boolean isInner() { return parentClass != this; } public boolean isTopClass() { return parentClass == this; } @Nullable public MethodNode getClassInitMth() { return searchMethodByShortId("()V"); } @Nullable public MethodNode getDefaultConstructor() { for (MethodNode mth : methods) { if (mth.isDefaultConstructor()) { return mth; } } return null; } @Override public AccessInfo getAccessFlags() { return accessFlags; } @Override public void setAccessFlags(AccessInfo accessFlags) { this.accessFlags = accessFlags; } @Override public RootNode root() { return root; } @Override public String typeName() { return "class"; } public String getRawName() { return clsInfo.getRawName(); } /** * Internal class info (don't use in code generation and external api). */ public ClassInfo getClassInfo() { return clsInfo; } public String getName() { return clsInfo.getShortName(); } public String getAlias() { return clsInfo.getAliasShortName(); } /** * Deprecated. Use {@link #getAlias()} */ @Deprecated public String getShortName() { return clsInfo.getAliasShortName(); } public String getFullName() { return clsInfo.getAliasFullName(); } public String getPackage() { return clsInfo.getAliasPkg(); } public String getDisassembledCode() { if (smali == null) { SimpleCodeWriter code = new SimpleCodeWriter(root.getArgs()); getDisassembledCode(code); Set allInlinedClasses = new LinkedHashSet<>(); getInnerAndInlinedClassesRecursive(allInlinedClasses); for (ClassNode innerClass : allInlinedClasses) { innerClass.getDisassembledCode(code); } smali = code.finish().getCodeStr(); } return smali; } protected void getDisassembledCode(SimpleCodeWriter code) { if (clsData == null) { code.startLine(String.format("###### Class %s is created by jadx", getFullName())); return; } code.startLine(String.format("###### Class %s (%s)", getFullName(), getRawName())); try { code.startLine(clsData.getDisassembledCode()); } catch (Exception e) { code.startLine("Failed to disassemble class:"); code.startLine(Utils.getStackTrace(e)); } } /** * Low level class data access. * * @return null for classes generated by jadx */ public @Nullable IClassData getClsData() { return clsData; } public ProcessState getState() { return state; } public void setState(ProcessState state) { this.state = state; } public LoadStage getLoadStage() { return loadStage; } public void setLoadStage(LoadStage loadStage) { this.loadStage = loadStage; } public void reloadAtCodegenStage() { ClassNode topCls = this.getTopParentClass(); if (topCls.getLoadStage() == LoadStage.CODEGEN_STAGE) { throw new JadxRuntimeException("Class not yet loaded at codegen stage: " + topCls); } topCls.add(AFlag.RELOAD_AT_CODEGEN_STAGE); } public List getDependencies() { return dependencies; } public void setDependencies(List dependencies) { this.dependencies = dependencies; } public void removeDependency(ClassNode dep) { this.dependencies = ListUtils.safeRemoveAndTrim(this.dependencies, dep); } public List getCodegenDeps() { return codegenDeps; } public void setCodegenDeps(List codegenDeps) { this.codegenDeps = codegenDeps; } public void addCodegenDep(ClassNode dep) { if (!codegenDeps.contains(dep)) { this.codegenDeps = ListUtils.safeAdd(this.codegenDeps, dep); } } public int getTotalDepsCount() { return dependencies.size() + codegenDeps.size(); } public List getUseIn() { return useIn; } public void setUseIn(List useIn) { this.useIn = useIn; } public List getUseInMth() { return useInMth; } public void setUseInMth(List useInMth) { this.useInMth = useInMth; } @Override public String getInputFileName() { return inputFileName; } public void setInputFileName(String inputFileName) { this.inputFileName = inputFileName; } public JavaClass getJavaNode() { return javaNode; } public void setJavaNode(JavaClass javaNode) { this.javaNode = javaNode; } @Override public AnnType getAnnType() { return AnnType.CLASS; } @Override public int hashCode() { return clsInfo.hashCode(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o instanceof ClassNode) { ClassNode other = (ClassNode) o; return clsInfo.equals(other.clsInfo); } return false; } @Override public int compareTo(@NotNull ClassNode o) { return this.clsInfo.compareTo(o.clsInfo); } @Override public String toString() { return clsInfo.getFullName(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/Edge.java ================================================ package jadx.core.dex.nodes; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AttrNode; public class Edge extends AttrNode { private final BlockNode source; private final BlockNode target; public Edge(BlockNode source, BlockNode target) { this(source, target, false); } public Edge(BlockNode source, BlockNode target, boolean isSynthetic) { if (isSynthetic) { this.add(AFlag.SYNTHETIC); } this.source = source; this.target = target; } public BlockNode getSource() { return source; } public BlockNode getTarget() { return target; } public boolean isSynthetic() { return this.contains(AFlag.SYNTHETIC); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Edge edge = (Edge) o; return source.equals(edge.source) && target.equals(edge.target); } @Override public int hashCode() { return source.hashCode() + 31 * target.hashCode(); } @Override public String toString() { return "Edge: " + source + " -> " + target; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java ================================================ package jadx.core.dex.nodes; import java.util.Collections; import java.util.List; import jadx.api.JavaField; import jadx.api.plugins.input.data.IFieldData; import jadx.core.dex.attributes.nodes.NotificationAttrNode; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.AccessInfo.AFType; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.utils.ListUtils; public class FieldNode extends NotificationAttrNode implements ICodeNode, IFieldInfoRef { private final ClassNode parentClass; private final FieldInfo fieldInfo; private AccessInfo accFlags; private ArgType type; private List useIn = Collections.emptyList(); private JavaField javaNode; public static FieldNode build(ClassNode cls, IFieldData fieldData) { FieldInfo fieldInfo = FieldInfo.fromRef(cls.root(), fieldData); FieldNode fieldNode = new FieldNode(cls, fieldInfo, fieldData.getAccessFlags()); fieldNode.addAttrs(fieldData.getAttributes()); return fieldNode; } public FieldNode(ClassNode cls, FieldInfo fieldInfo, int accessFlags) { this.parentClass = cls; this.fieldInfo = fieldInfo; this.type = fieldInfo.getType(); this.accFlags = new AccessInfo(accessFlags, AFType.FIELD); } public void unload() { unloadAttributes(); } public void updateType(ArgType type) { this.type = type; } @Override public FieldInfo getFieldInfo() { return fieldInfo; } @Override public AccessInfo getAccessFlags() { return accFlags; } @Override public void setAccessFlags(AccessInfo accFlags) { this.accFlags = accFlags; } public boolean isStatic() { return accFlags.isStatic(); } public boolean isInstance() { return !accFlags.isStatic(); } public String getName() { return fieldInfo.getName(); } public String getAlias() { return fieldInfo.getAlias(); } @Override public void rename(String alias) { fieldInfo.setAlias(alias); } public ArgType getType() { return type; } @Override public ClassNode getDeclaringClass() { return parentClass; } public ClassNode getParentClass() { return parentClass; } public ClassNode getTopParentClass() { return parentClass.getTopParentClass(); } public List getUseIn() { return useIn; } public void setUseIn(List useIn) { this.useIn = useIn; } public synchronized void addUseIn(MethodNode mth) { useIn = ListUtils.safeAdd(useIn, mth); } @Override public String typeName() { return "field"; } @Override public String getInputFileName() { return parentClass.getInputFileName(); } @Override public RootNode root() { return parentClass.root(); } public JavaField getJavaNode() { return javaNode; } public void setJavaNode(JavaField javaNode) { this.javaNode = javaNode; } @Override public AnnType getAnnType() { return AnnType.FIELD; } @Override public int hashCode() { return fieldInfo.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } FieldNode other = (FieldNode) obj; return fieldInfo.equals(other.fieldInfo); } @Override public String toString() { return fieldInfo.getDeclClass() + "." + fieldInfo.getName() + " :" + type; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/IBlock.java ================================================ package jadx.core.dex.nodes; import java.util.List; import jadx.api.ICodeWriter; import jadx.core.codegen.RegionGen; import jadx.core.utils.exceptions.CodegenException; public interface IBlock extends IContainer { List getInstructions(); @Override default void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException { regionGen.makeSimpleBlock(this, code); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/IBranchRegion.java ================================================ package jadx.core.dex.nodes; import java.util.List; public interface IBranchRegion extends IRegion { /** * Return list of branches in this region. * NOTE: Contains 'null' elements for indicate empty branches. */ List getBranches(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/ICodeDataUpdateListener.java ================================================ package jadx.core.dex.nodes; import jadx.api.data.ICodeData; public interface ICodeDataUpdateListener { void updated(ICodeData codeData); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/ICodeNode.java ================================================ package jadx.core.dex.nodes; import jadx.api.metadata.ICodeNodeRef; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.info.AccessInfo; public interface ICodeNode extends IDexNode, IAttributeNode, IUsageInfoNode, ICodeNodeRef { ClassNode getDeclaringClass(); AccessInfo getAccessFlags(); void setAccessFlags(AccessInfo newAccessFlags); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/IConditionRegion.java ================================================ package jadx.core.dex.nodes; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.core.dex.regions.conditions.IfCondition; public interface IConditionRegion extends IRegion { @Nullable IfCondition getCondition(); /** * Blocks merged into condition * Needed for backtracking * TODO: merge into condition object ??? */ List getConditionBlocks(); void invertCondition(); boolean simplifyCondition(); int getConditionSourceLine(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/IContainer.java ================================================ package jadx.core.dex.nodes; import jadx.api.ICodeWriter; import jadx.core.codegen.RegionGen; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.utils.exceptions.CodegenException; public interface IContainer extends IAttributeNode { /** * Unique id for use in 'toString()' method */ String baseString(); /** * Dispatch to needed generate method in RegionGen */ default void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException { throw new CodegenException("Code generate not implemented for container: " + getClass().getSimpleName()); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/IDexNode.java ================================================ package jadx.core.dex.nodes; import jadx.api.data.IRenameNode; public interface IDexNode extends IRenameNode { String typeName(); RootNode root(); String getInputFileName(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/IFieldInfoRef.java ================================================ package jadx.core.dex.nodes; import jadx.core.dex.info.FieldInfo; /** * Common interface for FieldInfo and FieldNode */ public interface IFieldInfoRef { FieldInfo getFieldInfo(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/ILoadable.java ================================================ package jadx.core.dex.nodes; import jadx.core.utils.exceptions.DecodeException; public interface ILoadable { /** * On demand loading */ void load() throws DecodeException; /** * Free resources */ void unload(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/IMethodDetails.java ================================================ package jadx.core.dex.nodes; import java.util.List; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.utils.Utils; public interface IMethodDetails extends IJadxAttribute { MethodInfo getMethodInfo(); ArgType getReturnType(); List getArgTypes(); List getTypeParameters(); List getThrows(); boolean isVarArg(); int getRawAccessFlags(); @Override default AType getAttrType() { return AType.METHOD_DETAILS; } @Override default String toAttrString() { StringBuilder sb = new StringBuilder(); sb.append("MD:"); if (Utils.notEmpty(getTypeParameters())) { sb.append('<'); sb.append(Utils.listToString(getTypeParameters())); sb.append(">:"); } sb.append('('); sb.append(Utils.listToString(getArgTypes())); sb.append("):"); sb.append(getReturnType()); if (isVarArg()) { sb.append(" VARARG"); } List throwsList = getThrows(); if (Utils.notEmpty(throwsList)) { sb.append(" throws ").append(Utils.listToString(throwsList)); } return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/IPackageUpdate.java ================================================ package jadx.core.dex.nodes; public interface IPackageUpdate { void onParentPackageUpdate(PackageNode updatedPkg); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/IRegion.java ================================================ package jadx.core.dex.nodes; import java.util.List; public interface IRegion extends IContainer { IRegion getParent(); void setParent(IRegion parent); List getSubBlocks(); boolean replaceSubBlock(IContainer oldBlock, IContainer newBlock); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/IUsageInfoNode.java ================================================ package jadx.core.dex.nodes; import java.util.List; public interface IUsageInfoNode { List getUseIn(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/InsnContainer.java ================================================ package jadx.core.dex.nodes; import java.util.ArrayList; import java.util.List; import jadx.core.dex.attributes.AttrNode; /** * Lightweight replacement for BlockNode in regions. * Use with caution! Some passes still expect BlockNode in method blocks list (mth.getBlockNodes()) */ public final class InsnContainer extends AttrNode implements IBlock { private final List insns; public InsnContainer(InsnNode insn) { List list = new ArrayList<>(1); list.add(insn); this.insns = list; } public InsnContainer(List insns) { this.insns = insns; } @Override public List getInstructions() { return insns; } @Override public String baseString() { return "IC"; } @Override public String toString() { return "InsnContainer"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/InsnNode.java ================================================ package jadx.core.dex.nodes; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.insns.InsnData; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; public class InsnNode extends LineAttrNode { protected final InsnType insnType; private RegisterArg result; private final List arguments; protected int offset; public InsnNode(InsnType type, int argsCount) { this(type, argsCount == 0 ? Collections.emptyList() : new ArrayList<>(argsCount)); } public InsnNode(InsnType type, List args) { this.insnType = type; this.arguments = args; this.offset = -1; for (InsnArg arg : args) { attachArg(arg); } } public static InsnNode wrapArg(InsnArg arg) { InsnNode insn = new InsnNode(InsnType.ONE_ARG, 1); insn.addArg(arg); return insn; } public void setResult(@Nullable RegisterArg res) { this.result = res; if (res != null) { res.setParentInsn(this); SSAVar ssaVar = res.getSVar(); if (ssaVar != null) { ssaVar.setAssign(res); } } } public void addArg(InsnArg arg) { arguments.add(arg); attachArg(arg); } public void setArg(int n, InsnArg arg) { arguments.set(n, arg); attachArg(arg); } protected void attachArg(InsnArg arg) { arg.setParentInsn(this); if (arg.isRegister()) { RegisterArg reg = (RegisterArg) arg; SSAVar ssaVar = reg.getSVar(); if (ssaVar != null) { ssaVar.use(reg); } } } public InsnType getType() { return insnType; } public RegisterArg getResult() { return result; } public Iterable getArguments() { return arguments; } public List getArgList() { return arguments; } public int getArgsCount() { return arguments.size(); } public InsnArg getArg(int n) { return arguments.get(n); } public boolean containsArg(InsnArg arg) { if (getArgsCount() == 0) { return false; } for (InsnArg a : arguments) { if (a == arg) { return true; } } return false; } public boolean containsVar(RegisterArg arg) { if (getArgsCount() == 0) { return false; } return InsnUtils.containsVar(arguments, arg); } /** * Replace instruction arg with another using recursive search. */ public boolean replaceArg(InsnArg from, InsnArg to) { int count = getArgsCount(); for (int i = 0; i < count; i++) { InsnArg arg = arguments.get(i); if (arg == from) { InsnRemover.unbindArgUsage(null, arg); setArg(i, to); return true; } if (arg.isInsnWrap() && ((InsnWrapArg) arg).getWrapInsn().replaceArg(from, to)) { return true; } } return false; } protected boolean removeArg(InsnArg arg) { int index = getArgIndex(arg); if (index == -1) { return false; } removeArg(index); return true; } public InsnArg removeArg(int index) { InsnArg arg = arguments.get(index); arguments.remove(index); InsnRemover.unbindArgUsage(null, arg); return arg; } public int getArgIndex(InsnArg arg) { int count = getArgsCount(); for (int i = 0; i < count; i++) { if (arg == arguments.get(i)) { return i; } } return -1; } protected void addReg(InsnData insn, int i, ArgType type) { addArg(InsnArg.reg(insn, i, type)); } protected void addReg(int regNum, ArgType type) { addArg(InsnArg.reg(regNum, type)); } protected void addLit(long literal, ArgType type) { addArg(InsnArg.lit(literal, type)); } protected void addLit(InsnData insn, ArgType type) { addArg(InsnArg.lit(insn, type)); } public int getOffset() { return offset; } public void setOffset(int offset) { this.offset = offset; } public void getRegisterArgs(Collection collection) { for (InsnArg arg : this.getArguments()) { if (arg.isRegister()) { collection.add((RegisterArg) arg); } else if (arg.isInsnWrap()) { ((InsnWrapArg) arg).getWrapInsn().getRegisterArgs(collection); } } } public boolean isConstInsn() { switch (getType()) { case CONST: case CONST_STR: case CONST_CLASS: return true; default: return false; } } public boolean isExitEdgeInsn() { switch (getType()) { case RETURN: case THROW: case CONTINUE: case BREAK: return true; default: return false; } } public boolean canRemoveResult() { switch (getType()) { case INVOKE: case CONSTRUCTOR: return true; default: return false; } } public boolean canReorder() { if (contains(AFlag.DONT_GENERATE)) { if (getType() == InsnType.MONITOR_EXIT) { return false; } return true; } for (InsnArg arg : getArguments()) { if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); if (!wrapInsn.canReorder()) { return false; } } } switch (getType()) { case CONST: case CONST_STR: case CONST_CLASS: case CAST: case MOVE: case ARITH: case NEG: case CMP_L: case CMP_G: case CHECK_CAST: case INSTANCE_OF: case FILL_ARRAY: case FILLED_NEW_ARRAY: case NEW_ARRAY: case STR_CONCAT: return true; case SGET: case IGET: // TODO: allow to move final fields return false; default: return false; } } public boolean containsWrappedInsn() { for (InsnArg arg : this.getArguments()) { if (arg.isInsnWrap()) { return true; } } return false; } /** * Visit this instruction and all inner (wrapped) instructions */ public void visitInsns(Consumer visitor) { visitor.accept(this); for (InsnArg arg : this.getArguments()) { if (arg.isInsnWrap()) { ((InsnWrapArg) arg).getWrapInsn().visitInsns(visitor); } } } /** * Visit this instruction and all inner (wrapped) instructions * To terminate visiting return non-null value */ @Nullable public R visitInsns(Function visitor) { R result = visitor.apply(this); if (result != null) { return result; } for (InsnArg arg : this.getArguments()) { if (arg.isInsnWrap()) { InsnNode innerInsn = ((InsnWrapArg) arg).getWrapInsn(); R res = innerInsn.visitInsns(visitor); if (res != null) { return res; } } } return null; } /** * Visit all args recursively (including inner instructions), but excluding wrapped args */ public void visitArgs(Consumer visitor) { for (InsnArg arg : getArguments()) { if (arg.isInsnWrap()) { ((InsnWrapArg) arg).getWrapInsn().visitArgs(visitor); } else { visitor.accept(arg); } } } /** * Visit all args recursively (including inner instructions), but excluding wrapped args. * To terminate visiting return non-null value */ @Nullable public R visitArgs(Function visitor) { for (InsnArg arg : getArguments()) { R result; if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); result = wrapInsn.visitArgs(visitor); } else { result = visitor.apply(arg); } if (result != null) { return result; } } return null; } /** * 'Soft' equals, don't compare arguments, only instruction specific parameters. */ public boolean isSame(InsnNode other) { if (this == other) { return true; } if (insnType != other.insnType) { return false; } int size = arguments.size(); if (size != other.arguments.size()) { return false; } // check wrapped instructions for (int i = 0; i < size; i++) { InsnArg arg = arguments.get(i); InsnArg otherArg = other.arguments.get(i); if (arg.isInsnWrap()) { if (!otherArg.isInsnWrap()) { return false; } InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); InsnNode otherWrapInsn = ((InsnWrapArg) otherArg).getWrapInsn(); if (!wrapInsn.isSame(otherWrapInsn)) { return false; } } } return true; } /** * 'Hard' equals, compare all arguments */ public boolean isDeepEquals(InsnNode other) { if (this == other) { return true; } return isSame(other) && Objects.equals(result, other.result) && Objects.equals(arguments, other.arguments); } protected final T copyCommonParams(T copy) { if (copy.getArgsCount() == 0) { for (InsnArg arg : this.getArguments()) { copy.addArg(arg.duplicate()); } } copy.copyAttributesFrom(this); copy.copyLines(this); copy.setOffset(this.getOffset()); return copy; } public void copyAttributesFrom(InsnNode attrNode) { super.copyAttributesFrom(attrNode); this.addSourceLineFrom(attrNode); } /** * Make copy of InsnNode object. *
* NOTE: can't copy instruction with result argument * (SSA variable can't be used in two different assigns). *
* Prefer use next methods: *

    *
  • {@link #copyWithoutResult()} to explicitly state that result not needed *
  • {@link #copy(RegisterArg)} to provide new result arg *
  • {@link #copyWithNewSsaVar(MethodNode)} to make new SSA variable for result arg *
*/ public InsnNode copy() { if (this.getClass() != InsnNode.class) { throw new JadxRuntimeException("Copy method not implemented in insn class " + this.getClass().getSimpleName()); } return copyCommonParams(new InsnNode(insnType, getArgsCount())); } /** * See {@link #copy()} */ @SuppressWarnings("unchecked") public T copyWithoutResult() { return (T) copy(); } public InsnNode copyWithoutSsa() { InsnNode copy = copyWithoutResult(); if (result != null) { if (result.getSVar() == null) { copy.setResult(result.duplicate()); } else { throw new JadxRuntimeException("Can't copy if SSA var is set"); } } return copy; } /** * See {@link #copy()} */ public InsnNode copy(RegisterArg newReturnArg) { InsnNode copy = copy(); copy.setResult(newReturnArg); return copy; } /** * See {@link #copy()} */ public InsnNode copyWithNewSsaVar(MethodNode mth) { RegisterArg result = getResult(); if (result == null) { throw new JadxRuntimeException("Result in null"); } int regNum = result.getRegNum(); RegisterArg resDupArg = result.duplicate(regNum, null); mth.makeNewSVar(resDupArg); return copy(resDupArg); } /** * Fix SSAVar info in register arguments. * Must be used after altering instructions. */ public void rebindArgs() { RegisterArg resArg = getResult(); if (resArg != null) { SSAVar ssaVar = resArg.getSVar(); if (ssaVar == null) { throw new JadxRuntimeException("No SSA var for result arg: " + resArg + " from " + resArg.getParentInsn()); } ssaVar.setAssign(resArg); } for (InsnArg arg : getArguments()) { if (arg instanceof RegisterArg) { RegisterArg reg = (RegisterArg) arg; SSAVar ssaVar = reg.getSVar(); ssaVar.use(reg); ssaVar.updateUsedInPhiList(); } else if (arg instanceof InsnWrapArg) { ((InsnWrapArg) arg).getWrapInsn().rebindArgs(); } } } public boolean canThrowException() { switch (getType()) { case RETURN: case IF: case GOTO: case MOVE: case MOVE_EXCEPTION: case NEG: case CONST: case CONST_STR: case CONST_CLASS: case CMP_L: case CMP_G: case NOP: return false; default: return true; } } public void inheritMetadata(InsnNode sourceInsn) { if (insnType == InsnType.RETURN) { this.copyLines(sourceInsn); if (this.contains(AFlag.SYNTHETIC)) { this.setOffset(sourceInsn.getOffset()); this.rewriteAttributeFrom(sourceInsn, AType.CODE_COMMENTS); } else { this.copyAttributeFrom(sourceInsn, AType.CODE_COMMENTS); } } else { this.copyAttributeFrom(sourceInsn, AType.CODE_COMMENTS); this.addSourceLineFrom(sourceInsn); } } /** * Compare instruction only by identity. */ @SuppressWarnings("EmptyMethod") @Override public final int hashCode() { return super.hashCode(); } /** * Compare instruction only by identity. */ @Override public final boolean equals(Object obj) { return super.equals(obj); } /** * Append arguments type, wrap line if too long * * @return true if args wrapped */ protected boolean appendArgs(StringBuilder sb) { if (arguments.isEmpty()) { return false; } String argsStr = Utils.listToString(arguments); if (argsStr.length() < 120) { sb.append(argsStr); return false; } // wrap args String separator = "\n "; sb.append(separator).append(Utils.listToString(arguments, separator)); sb.append('\n'); return true; } protected String attributesString() { StringBuilder sb = new StringBuilder(); appendAttributes(sb); return sb.toString(); } protected void appendAttributes(StringBuilder sb) { if (!isAttrStorageEmpty()) { sb.append(' ').append(getAttributesString()); } if (getSourceLine() != 0) { sb.append(" (LINE:").append(getSourceLine()).append(')'); } } protected String baseString() { StringBuilder sb = new StringBuilder(); if (offset != -1) { sb.append(InsnUtils.formatOffset(offset)).append(": "); } sb.append(insnType).append(' '); if (result != null) { sb.append(result).append(" = "); } appendArgs(sb); return sb.toString(); } @Override public String toString() { return baseString() + attributesString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/LoadStage.java ================================================ package jadx.core.dex.nodes; public enum LoadStage { NONE, PROCESS_STAGE, // dependencies not yet loaded CODEGEN_STAGE, // all dependencies loaded } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java ================================================ package jadx.core.dex.nodes; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.JavaMethod; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.api.metadata.annotations.VarNode; import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IDebugInfo; import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.ExceptionsAttr; import jadx.api.utils.CodeUtils; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.MethodThrowsAttr; import jadx.core.dex.attributes.nodes.NotificationAttrNode; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.AccessInfo.AFType; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.InsnDecoder; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.utils.TypeUtils; import jadx.core.dex.regions.Region; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.visitors.InitCodeVariables; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.DecodeException; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.Utils.lockList; public class MethodNode extends NotificationAttrNode implements IMethodDetails, ILoadable, ICodeNode, Comparable { private static final Logger LOG = LoggerFactory.getLogger(MethodNode.class); private static final InsnNode[] EMPTY_INSN_ARRAY = new InsnNode[0]; private final MethodInfo mthInfo; private final ClassNode parentClass; private AccessInfo accFlags; private final ICodeReader codeReader; private final int insnsCount; private boolean noCode; private int regsCount; private int argsStartReg; private boolean loaded; // additional info available after load, keep on unload private ArgType retType; private List argTypes; private List typeParameters; // decompilation data, reset on unload private RegisterArg thisArg; private List argsList; private InsnNode[] instructions; private List blocks; private int blocksMaxCId; private BlockNode enterBlock; private BlockNode exitBlock; private List sVars; private List exceptionHandlers; private List loops; private Region region; // Methods that use this method private List useIn = Collections.emptyList(); // Unresolved methods that use this method private List unresolvedUsed = Collections.emptyList(); // Methods that this method uses private Set methodsUsed = new HashSet<>(); // True if this method contains a self call private boolean callsSelf = false; private JavaMethod javaNode; public static MethodNode build(ClassNode classNode, IMethodData methodData) { MethodNode methodNode = new MethodNode(classNode, methodData); methodNode.addAttrs(methodData.getAttributes()); return methodNode; } private MethodNode(ClassNode classNode, IMethodData mthData) { this.mthInfo = MethodInfo.fromRef(classNode.root(), mthData.getMethodRef()); this.parentClass = classNode; this.accFlags = new AccessInfo(mthData.getAccessFlags(), AFType.METHOD); ICodeReader codeReader = mthData.getCodeReader(); this.noCode = codeReader == null; if (noCode) { this.codeReader = null; this.insnsCount = 0; } else { this.codeReader = codeReader.copy(); this.insnsCount = codeReader.getUnitsCount(); } this.retType = mthInfo.getReturnType(); this.argTypes = mthInfo.getArgumentsTypes(); this.typeParameters = Collections.emptyList(); unload(); } @Override public void unload() { loaded = false; // don't unload retType, argTypes, typeParameters thisArg = null; argsList = null; sVars = Collections.emptyList(); instructions = null; blocks = null; enterBlock = null; exitBlock = null; region = null; exceptionHandlers = Collections.emptyList(); loops = Collections.emptyList(); unloadAttributes(); } public void updateTypes(List argTypes, ArgType retType) { this.argTypes = argTypes; this.retType = retType; } public void updateTypeParameters(List typeParameters) { this.typeParameters = typeParameters; } @Override public void load() throws DecodeException { if (loaded) { // method already loaded return; } try { loaded = true; if (noCode) { regsCount = 0; // TODO: registers not needed without code initArguments(this.argTypes); return; } this.regsCount = codeReader.getRegistersCount(); this.argsStartReg = codeReader.getArgsStartReg(); initArguments(this.argTypes); if (contains(AType.JADX_ERROR)) { // don't load instructions for method with errors this.instructions = EMPTY_INSN_ARRAY; } else { InsnDecoder decoder = new InsnDecoder(this); this.instructions = decoder.process(codeReader); } } catch (Exception e) { if (!noCode) { unload(); noCode = true; // load without code load(); noCode = false; } throw new DecodeException(this, "Load method exception: " + e.getClass().getSimpleName() + ": " + e.getMessage(), e); } } public void reload() { unload(); try { load(); } catch (DecodeException e) { throw new JadxRuntimeException("Failed to reload method " + getClass().getName() + "." + getName()); } } private void initArguments(List args) { int pos = getArgsStartPos(args); TypeUtils typeUtils = root().getTypeUtils(); if (accFlags.isStatic()) { thisArg = null; } else { ArgType thisClsType = typeUtils.expandTypeVariables(this, parentClass.getType()); RegisterArg arg = InsnArg.reg(pos++, thisClsType); arg.add(AFlag.THIS); arg.add(AFlag.IMMUTABLE_TYPE); thisArg = arg; } if (args.isEmpty()) { argsList = Collections.emptyList(); return; } argsList = new ArrayList<>(args.size()); for (ArgType argType : args) { ArgType expandedType = typeUtils.expandTypeVariables(this, argType); RegisterArg regArg = InsnArg.reg(pos, expandedType); regArg.add(AFlag.METHOD_ARGUMENT); regArg.add(AFlag.IMMUTABLE_TYPE); argsList.add(regArg); pos += argType.getRegCount(); } } private int getArgsStartPos(List args) { if (noCode) { return 0; } if (argsStartReg != -1) { return argsStartReg; } int pos = regsCount; for (ArgType arg : args) { pos -= arg.getRegCount(); } if (!accFlags.isStatic()) { pos--; } return pos; } @Override @NotNull public List getArgTypes() { if (argTypes == null) { throw new JadxRuntimeException("Method generic types not initialized: " + this); } return argTypes; } public void updateArgTypes(List newArgTypes, String comment) { this.addDebugComment(comment + ", original types: " + getArgTypes()); this.argTypes = Collections.unmodifiableList(newArgTypes); initArguments(newArgTypes); } public boolean containsGenericArgs() { return !Objects.equals(mthInfo.getArgumentsTypes(), getArgTypes()); } @Override @NotNull public ArgType getReturnType() { return retType; } public void updateReturnType(ArgType type) { this.retType = type; } public boolean isVoidReturn() { return mthInfo.getReturnType().equals(ArgType.VOID); } public List collectArgNodes() { ICodeInfo codeInfo = getTopParentClass().getCode(); int mthDefPos = getDefPosition(); int lineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos); int argsCount = mthInfo.getArgsCount(); List args = new ArrayList<>(argsCount); codeInfo.getCodeMetadata().searchDown(mthDefPos, (pos, ann) -> { if (pos > lineEndPos) { // Stop at line end return Boolean.TRUE; } if (ann instanceof NodeDeclareRef) { ICodeNodeRef declRef = ((NodeDeclareRef) ann).getNode(); if (declRef instanceof VarNode) { VarNode varNode = (VarNode) declRef; if (!varNode.getMth().equals(this)) { // Stop if we've gone too far and have entered a different method return Boolean.TRUE; } args.add(varNode); } } return null; }); if (args.size() != argsCount) { LOG.warn("Incorrect args count, expected: {}, got: {}", argsCount, args.size()); } return args; } public List getArgRegs() { if (argsList == null) { throw new JadxRuntimeException("Method arg registers not loaded: " + this + ", class status: " + parentClass.getTopParentClass().getState()); } return argsList; } public List getAllArgRegs() { List argRegs = getArgRegs(); if (thisArg != null) { List list = new ArrayList<>(argRegs.size() + 1); list.add(thisArg); list.addAll(argRegs); return list; } return argRegs; } @Nullable public RegisterArg getThisArg() { return thisArg; } public void skipFirstArgument() { this.add(AFlag.SKIP_FIRST_ARG); } @Override public List getTypeParameters() { return typeParameters; } public String getName() { return mthInfo.getName(); } public String getAlias() { return mthInfo.getAlias(); } @Override public ClassNode getDeclaringClass() { return parentClass; } public ClassNode getParentClass() { return parentClass; } public ClassNode getTopParentClass() { return parentClass.getTopParentClass(); } public boolean isNoCode() { return noCode; } public InsnNode[] getInstructions() { return instructions; } public void unloadInsnArr() { this.instructions = null; } public void initBasicBlocks() { blocks = new ArrayList<>(); } public void finishBasicBlocks() { blocks = lockList(blocks); loops = lockList(loops); blocks.forEach(BlockNode::lock); } public List getBasicBlocks() { return blocks; } public void setBasicBlocks(List blocks) { this.blocks = blocks; updateBlockPositions(); } public void updateBlockPositions() { BlockNode.updateBlockPositions(blocks); } public int getNextBlockCId() { return blocksMaxCId++; } public BlockNode getEnterBlock() { return enterBlock; } public void setEnterBlock(BlockNode enterBlock) { this.enterBlock = enterBlock; } public BlockNode getExitBlock() { return exitBlock; } public void setExitBlock(BlockNode exitBlock) { this.exitBlock = exitBlock; } public List getPreExitBlocks() { return exitBlock.getPredecessors(); } public boolean isPreExitBlock(BlockNode block) { List successors = block.getSuccessors(); if (successors.size() == 1) { return successors.get(0).equals(exitBlock); } return exitBlock.getPredecessors().contains(block); } public void resetLoops() { this.loops = new ArrayList<>(); } public void registerLoop(LoopInfo loop) { if (loops.isEmpty()) { loops = new ArrayList<>(5); } loop.setId(loops.size()); loops.add(loop); } @Nullable public LoopInfo getLoopForBlock(BlockNode block) { if (loops.isEmpty()) { return null; } for (LoopInfo loop : loops) { if (loop.getLoopBlocks().contains(block)) { return loop; } } return null; } public List getAllLoopsForBlock(BlockNode block) { if (loops.isEmpty()) { return Collections.emptyList(); } List list = new ArrayList<>(loops.size()); for (LoopInfo loop : loops) { if (loop.getLoopBlocks().contains(block)) { list.add(loop); } } return list; } public int getLoopsCount() { return loops.size(); } public Iterable getLoops() { return loops; } public ExceptionHandler addExceptionHandler(ExceptionHandler handler) { if (exceptionHandlers.isEmpty()) { exceptionHandlers = new ArrayList<>(2); } exceptionHandlers.add(handler); return handler; } public boolean clearExceptionHandlers() { return exceptionHandlers.removeIf(ExceptionHandler::isRemoved); } public Iterable getExceptionHandlers() { return exceptionHandlers; } public boolean isNoExceptionHandlers() { return exceptionHandlers.isEmpty(); } public int getExceptionHandlersCount() { return exceptionHandlers.size(); } @Override public List getThrows() { MethodThrowsAttr throwsAttr = get(AType.METHOD_THROWS); if (throwsAttr != null) { return Utils.collectionMap(throwsAttr.getList(), ArgType::object); } ExceptionsAttr exceptionsAttr = get(JadxAttrType.EXCEPTIONS); if (exceptionsAttr != null) { return Utils.collectionMap(exceptionsAttr.getList(), ArgType::object); } return Collections.emptyList(); } /** * Return true if exists method with same name and arguments count */ public boolean isArgsOverloaded() { MethodInfo thisMthInfo = this.mthInfo; // quick check in current class for (MethodNode method : parentClass.getMethods()) { if (method == this) { continue; } if (method.getMethodInfo().isOverloadedBy(thisMthInfo)) { return true; } } return root().getMethodUtils().isMethodArgsOverloaded(parentClass.getClassInfo().getType(), thisMthInfo); } public boolean isConstructor() { return accFlags.isConstructor() && mthInfo.isConstructor(); } public boolean isDefaultConstructor() { if (isConstructor()) { int defaultArgCount = 0; // workaround for non-static inner class constructor, that has synthetic argument if (parentClass.getClassInfo().isInner() && !parentClass.getAccessFlags().isStatic()) { ClassNode outerCls = parentClass.getParentClass(); if (argsList != null && !argsList.isEmpty() && argsList.get(0).getInitType().equals(outerCls.getClassInfo().getType())) { defaultArgCount = 1; } } return argsList == null || argsList.size() == defaultArgCount; } return false; } public int getRegsCount() { return regsCount; } public int getArgsStartReg() { return argsStartReg; } /** * Create new fake register arg. */ public RegisterArg makeSyntheticRegArg(ArgType type) { RegisterArg arg = InsnArg.reg(0, type); arg.add(AFlag.SYNTHETIC); SSAVar ssaVar = makeNewSVar(arg); InitCodeVariables.initCodeVar(ssaVar); ssaVar.setType(type); return arg; } public RegisterArg makeSyntheticRegArg(ArgType type, String name) { RegisterArg arg = makeSyntheticRegArg(type); arg.setName(name); return arg; } public SSAVar makeNewSVar(@NotNull RegisterArg assignArg) { int regNum = assignArg.getRegNum(); return makeNewSVar(regNum, getNextSVarVersion(regNum), assignArg); } public SSAVar makeNewSVar(int regNum, int version, @NotNull RegisterArg assignArg) { SSAVar var = new SSAVar(regNum, version, assignArg); if (sVars.isEmpty()) { sVars = new ArrayList<>(); } sVars.add(var); return var; } private int getNextSVarVersion(int regNum) { int v = -1; for (SSAVar sVar : sVars) { if (sVar.getRegNum() == regNum) { v = Math.max(v, sVar.getVersion()); } } v++; return v; } public void removeSVar(SSAVar var) { sVars.remove(var); } public List getSVars() { return sVars; } @Override public int getRawAccessFlags() { return accFlags.rawValue(); } @Override public AccessInfo getAccessFlags() { return accFlags; } @Override public void setAccessFlags(AccessInfo newAccessFlags) { this.accFlags = newAccessFlags; } public Region getRegion() { return region; } public void setRegion(Region region) { this.region = region; } @Override public RootNode root() { return parentClass.root(); } @Override public String typeName() { return "method"; } @Override public String getInputFileName() { return parentClass.getInputFileName(); } @Override public MethodInfo getMethodInfo() { return mthInfo; } public long getMethodCodeOffset() { return noCode ? 0 : codeReader.getCodeOffset(); } @Nullable public IDebugInfo getDebugInfo() { return noCode ? null : codeReader.getDebugInfo(); } public void ignoreMethod() { add(AFlag.DONT_GENERATE); noCode = true; } @Override public void rename(String newName) { MethodOverrideAttr overrideAttr = get(AType.METHOD_OVERRIDE); if (overrideAttr != null) { for (MethodNode relatedMth : overrideAttr.getRelatedMthNodes()) { relatedMth.getMethodInfo().setAlias(newName); } } else { mthInfo.setAlias(newName); } } /** * Calculate instructions count at current stage */ public long countInsns() { if (instructions != null) { return instructions.length; } if (blocks != null) { return blocks.stream().mapToLong(block -> block.getInstructions().size()).sum(); } return -1; } /** * Raw instructions count in method bytecode */ public int getInsnsCount() { return insnsCount; } /** * Returns method code with comments and annotations */ public String getCodeStr() { return CodeUtils.extractMethodCode(this, getTopParentClass().getCode()); } @Override public boolean isVarArg() { return accFlags.isVarArgs(); } public boolean isLoaded() { return loaded; } public @Nullable ICodeReader getCodeReader() { return codeReader; } // Cannot modify through get, use setUseIn public List getUseIn() { return Collections.unmodifiableList(useIn); } // Do not modify passed list after setting public void setUseIn(List useIn) { this.useIn = useIn; // Notify all methods (callers) this method (calee) is used in for (MethodNode methodUsedIn : useIn) { methodUsedIn.addUsed(this); } } public void addUsed(MethodNode used) { if (used != null) { this.methodsUsed.add(used); } } public void setUsed(List methodsUsed) { this.methodsUsed = new HashSet<>(methodsUsed); } public Set getUsed() { this.removeInavlidMethodsUsed(); return methodsUsed; } public List getUnresolvedUsed() { return unresolvedUsed; } public void setUnresolvedUsed(List unresolvedUsed) { this.unresolvedUsed = unresolvedUsed; } public void setCallsSelf(boolean callsSelf) { this.callsSelf = callsSelf; } public boolean callsSelf() { return this.callsSelf; } // Remove any methods from the list of used methods (calees) if this method (caller) has been // removed from the calee's list of callers private void removeInavlidMethodsUsed() { for (MethodNode methodUsed : new ArrayList<>(methodsUsed)) { if (!methodUsed.getUseIn().contains(this)) { methodsUsed.remove(methodUsed); } } } public JavaMethod getJavaNode() { return javaNode; } @ApiStatus.Internal public void setJavaNode(JavaMethod javaNode) { this.javaNode = javaNode; } @Override public AnnType getAnnType() { return AnnType.METHOD; } @Override public int hashCode() { return mthInfo.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } MethodNode other = (MethodNode) obj; return mthInfo.equals(other.mthInfo); } @Override public int compareTo(@NotNull MethodNode o) { return mthInfo.compareTo(o.mthInfo); } @Override public String toAttrString() { return IMethodDetails.super.toAttrString() + " (m)"; } @Override public String toString() { return parentClass + "." + mthInfo.getName() + '(' + Utils.listToString(argTypes) + "):" + retType; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/PackageNode.java ================================================ package jadx.core.dex.nodes; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.JavaPackage; import jadx.api.metadata.ICodeNodeRef; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.info.PackageInfo; import static jadx.core.utils.StringUtils.containsChar; public class PackageNode extends LineAttrNode implements IPackageUpdate, IDexNode, ICodeNodeRef, Comparable { private final RootNode root; private final PackageInfo pkgInfo; private final @Nullable PackageNode parentPkg; private final List subPackages = new ArrayList<>(); private final List classes = new ArrayList<>(); private PackageInfo aliasPkgInfo; private JavaPackage javaNode; public static PackageNode getForClass(RootNode root, String fullPkg, ClassNode cls) { PackageNode pkg = getOrBuild(root, fullPkg); pkg.getClasses().add(cls); return pkg; } public static PackageNode getOrBuild(RootNode root, String fullPkg) { PackageNode existPkg = root.resolvePackage(fullPkg); if (existPkg != null) { return existPkg; } PackageInfo pgkInfo = PackageInfo.fromFullPkg(root, fullPkg); PackageNode parentPkg = getParentPkg(root, pgkInfo); PackageNode pkgNode = new PackageNode(root, parentPkg, pgkInfo); if (parentPkg != null) { parentPkg.getSubPackages().add(pkgNode); } root.addPackage(pkgNode); return pkgNode; } private static @Nullable PackageNode getParentPkg(RootNode root, PackageInfo pgkInfo) { PackageInfo parentPkg = pgkInfo.getParentPkg(); if (parentPkg == null) { return null; } return getOrBuild(root, parentPkg.getFullName()); } private PackageNode(RootNode root, @Nullable PackageNode parentPkg, PackageInfo pkgInfo) { this.root = root; this.parentPkg = parentPkg; this.pkgInfo = pkgInfo; this.aliasPkgInfo = pkgInfo; } @Override public void rename(String newName) { rename(newName, true); } public void rename(String newName, boolean runUpdates) { String alias; boolean isFullAlias; if (containsChar(newName, '/')) { alias = newName.replace('/', '.'); isFullAlias = true; } else if (newName.startsWith(".")) { // treat as full pkg, remove start dot alias = newName.substring(1); isFullAlias = true; } else { alias = newName; isFullAlias = containsChar(newName, '.'); } if (isFullAlias) { setFullAlias(alias, runUpdates); } else { setLeafAlias(alias, runUpdates); } } public void setLeafAlias(String alias, boolean runUpdates) { if (pkgInfo.getName().equals(alias)) { aliasPkgInfo = pkgInfo; } else { aliasPkgInfo = PackageInfo.fromShortName(root, getParentAliasPkgInfo(), alias); } if (runUpdates) { updatePackages(this); } } public void setFullAlias(String fullAlias, boolean runUpdates) { if (pkgInfo.getFullName().equals(fullAlias)) { aliasPkgInfo = pkgInfo; } else { aliasPkgInfo = PackageInfo.fromFullPkg(root, fullAlias); } if (runUpdates) { updatePackages(this); } } @Override public void onParentPackageUpdate(PackageNode updatedPkg) { aliasPkgInfo = PackageInfo.fromShortName(root, getParentAliasPkgInfo(), aliasPkgInfo.getName()); updatePackages(updatedPkg); } public void updatePackages() { updatePackages(this); } private void updatePackages(PackageNode updatedPkg) { for (PackageNode subPackage : subPackages) { subPackage.onParentPackageUpdate(updatedPkg); } for (ClassNode cls : classes) { cls.onParentPackageUpdate(updatedPkg); } } public String getName() { return pkgInfo.getName(); } public String getFullName() { return pkgInfo.getFullName(); } public PackageInfo getPkgInfo() { return pkgInfo; } public PackageInfo getAliasPkgInfo() { return aliasPkgInfo; } public boolean hasAlias() { if (pkgInfo == aliasPkgInfo) { return false; } return !pkgInfo.getName().equals(aliasPkgInfo.getName()); } public boolean hasParentAlias() { if (pkgInfo == aliasPkgInfo) { return false; } return !Objects.equals(pkgInfo.getParentPkg(), aliasPkgInfo.getParentPkg()); } public void removeAlias() { aliasPkgInfo = pkgInfo; } public @Nullable PackageNode getParentPkg() { return parentPkg; } public @Nullable PackageInfo getParentAliasPkgInfo() { return parentPkg == null ? null : parentPkg.aliasPkgInfo; } public boolean isRoot() { return parentPkg == null; } public boolean isLeaf() { return subPackages.isEmpty(); } public List getSubPackages() { return subPackages; } public List getClasses() { return classes; } public List getClassesNoDup() { return classes.stream() .map(ClassNode::getClassInfo) .collect(Collectors.toSet()) .stream() .map(e -> root.resolveClass(e)).collect(Collectors.toList()); } public JavaPackage getJavaNode() { return javaNode; } public void setJavaNode(JavaPackage javaNode) { this.javaNode = javaNode; } public boolean isEmpty() { return classes.isEmpty() && subPackages.isEmpty(); } @Override public String typeName() { return "package"; } @Override public AnnType getAnnType() { return AnnType.PKG; } @Override public RootNode root() { return root; } @Override public String getInputFileName() { return ""; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof PackageNode)) { return false; } return pkgInfo.equals(((PackageNode) o).pkgInfo); } @Override public int hashCode() { return pkgInfo.hashCode(); } @Override public int compareTo(@NotNull PackageNode other) { return getPkgInfo().getFullName().compareTo(other.getPkgInfo().getFullName()); } @Override public String toString() { return getPkgInfo().getFullName(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/ProcessState.java ================================================ package jadx.core.dex.nodes; public enum ProcessState { NOT_LOADED, LOADED, PROCESS_STARTED, PROCESS_COMPLETE, GENERATED_AND_UNLOADED; public boolean isProcessComplete() { return this == PROCESS_COMPLETE || this == GENERATED_AND_UNLOADED; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java ================================================ package jadx.core.dex.nodes; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.DecompilationMode; import jadx.api.ICodeCache; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.api.ResourcesLoader; import jadx.api.data.ICodeData; import jadx.api.impl.passes.DecompilePassWrapper; import jadx.api.impl.passes.PreparePassWrapper; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.pass.JadxPass; import jadx.api.plugins.pass.types.JadxDecompilePass; import jadx.api.plugins.pass.types.JadxPassType; import jadx.api.plugins.pass.types.JadxPreparePass; import jadx.core.Jadx; import jadx.core.ProcessClass; import jadx.core.clsp.ClspGraph; import jadx.core.dex.attributes.AttributeStorage; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.ConstStorage; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.InfoStorage; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.info.PackageInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.utils.MethodUtils; import jadx.core.dex.nodes.utils.SelectFromDuplicates; import jadx.core.dex.nodes.utils.TypeUtils; import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.typeinference.TypeCompare; import jadx.core.dex.visitors.typeinference.TypeUpdate; import jadx.core.export.GradleInfoStorage; import jadx.core.utils.CacheStorage; import jadx.core.utils.DebugChecks; import jadx.core.utils.ErrorsCounter; import jadx.core.utils.PassMerge; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.core.utils.android.AndroidResourcesUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.IResTableParser; import jadx.core.xmlgen.ManifestAttributes; import jadx.core.xmlgen.ResourceStorage; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ValuesParser; public class RootNode { private static final Logger LOG = LoggerFactory.getLogger(RootNode.class); private final JadxArgs args; private final ErrorsCounter errorsCounter = new ErrorsCounter(); private final StringUtils stringUtils; private final ConstStorage constValues; private final InfoStorage infoStorage = new InfoStorage(); private final CacheStorage cacheStorage = new CacheStorage(); private final TypeUpdate typeUpdate; private final MethodUtils methodUtils; private final TypeUtils typeUtils; private final AttributeStorage attributes = new AttributeStorage(); private final List codeDataUpdateListeners = new ArrayList<>(); private final GradleInfoStorage gradleInfoStorage = new GradleInfoStorage(); private final Map clsMap = new HashMap<>(); private final Map rawClsMap = new HashMap<>(); private List classes = new ArrayList<>(); private final Map pkgMap = new HashMap<>(); private final List packages = new ArrayList<>(); private List preDecompilePasses; private ProcessClass processClasses; private ClspGraph clsp; private @Nullable String appPackage; private @Nullable ClassNode appResClass; /** * Optional decompiler reference */ private @Nullable JadxDecompiler decompiler; private @Nullable ManifestAttributes manifestAttributes; public RootNode(JadxDecompiler decompiler) { this(decompiler, decompiler.getArgs()); } /** * Deprecated. Prefer {@link #RootNode(JadxDecompiler)} */ @Deprecated public RootNode(JadxArgs args) { this(null, args); } private RootNode(@Nullable JadxDecompiler decompiler, JadxArgs args) { this.decompiler = decompiler; this.args = args; this.preDecompilePasses = Jadx.getPreDecompilePassesList(); this.processClasses = new ProcessClass(Jadx.getPassesList(args)); this.stringUtils = new StringUtils(args); this.constValues = new ConstStorage(args); this.typeUpdate = new TypeUpdate(this); this.methodUtils = new MethodUtils(this); this.typeUtils = new TypeUtils(this); } public void init() { if (args.isDeobfuscationOn() || !args.getRenameFlags().isEmpty()) { args.getAliasProvider().init(this); } if (args.isDeobfuscationOn()) { args.getRenameCondition().init(this); } } public void loadClasses(List loadedInputs) { for (ICodeLoader codeLoader : loadedInputs) { codeLoader.visitClasses(cls -> { try { addClassNode(new ClassNode(RootNode.this, cls)); } catch (Exception e) { addDummyClass(cls, e); } Utils.checkThreadInterrupt(); }); } } public void finishClassLoad() { if (classes.size() != clsMap.size()) { // class name duplication detected fixDuplicatedClasses(); } classes = new ArrayList<>(clsMap.values()); // print stats for loaded classes int mthCount = classes.stream().mapToInt(c -> c.getMethods().size()).sum(); int insnsCount = classes.stream().flatMap(c -> c.getMethods().stream()).mapToInt(MethodNode::getInsnsCount).sum(); LOG.info("Loaded classes: {}, methods: {}, instructions: {}", classes.size(), mthCount, insnsCount); // sort classes by name, expect top classes before inner classes.sort(Comparator.comparing(ClassNode::getRawName)); if (args.isMoveInnerClasses()) { // detect and move inner classes initInnerClasses(); } // sort packages Collections.sort(packages); } private void addDummyClass(IClassData classData, Exception exc) { try { String typeStr = classData.getType(); String name = null; try { ClassInfo clsInfo = ClassInfo.fromName(this, typeStr); if (clsInfo != null) { name = clsInfo.getShortName(); } } catch (Exception e) { LOG.error("Failed to get name for class with type {}", typeStr, e); } if (name == null || name.isEmpty()) { name = "CLASS_" + typeStr; } ClassNode clsNode = ClassNode.addSyntheticClass(this, name, classData.getAccessFlags()); ErrorsCounter.error(clsNode, "Load error", exc); } catch (Exception innerExc) { LOG.error("Failed to load class from file: {}", classData.getInputFileName(), exc); } } private void fixDuplicatedClasses() { classes.stream() .collect(Collectors.groupingBy(ClassNode::getClassInfo)) .entrySet() .stream() .filter(entry -> entry.getValue().size() > 1) .forEach(entry -> { ClassInfo clsInfo = entry.getKey(); List dupClsList = entry.getValue(); ClassNode selectedCls = SelectFromDuplicates.process(dupClsList); // keep only selected class in classes maps clsMap.put(clsInfo, selectedCls); rawClsMap.put(selectedCls.getRawName(), selectedCls); String selectedSource = selectedCls.getInputFileName(); String sources = dupClsList.stream() .map(ClassNode::getInputFileName) .sorted() .collect(Collectors.joining("\n ")); LOG.warn("Found duplicated class: {}, count: {}, sources:" + "\n {}\n Keep class with source: {}, others will be removed.", clsInfo, dupClsList.size(), sources, selectedSource); selectedCls.addWarnComment("Classes with same name are omitted, all sources:\n " + sources + '\n'); }); } public void addClassNode(ClassNode clsNode) { classes.add(clsNode); clsMap.put(clsNode.getClassInfo(), clsNode); rawClsMap.put(clsNode.getRawName(), clsNode); } public void loadResources(ResourcesLoader resLoader, List resources) { ResourceFile arsc = getResourceFile(resources); if (arsc == null) { LOG.debug("'resources.arsc' or 'resources.pb' file not found"); return; } try { IResTableParser parser = ResourcesLoader.decodeStream(arsc, (size, is) -> resLoader.decodeTable(arsc, is)); if (parser != null) { processResources(parser.getResStorage()); updateObfuscatedFiles(parser, resources); initManifestAttributes().updateAttributes(parser); } } catch (Exception e) { LOG.error("Failed to parse 'resources.pb'/'.arsc' file", e); } } private @Nullable ResourceFile getResourceFile(List resources) { for (ResourceFile rf : resources) { if (rf.getType() == ResourceType.ARSC) { return rf; } } return null; } public void processResources(ResourceStorage resStorage) { constValues.setResourcesNames(resStorage.getResourcesNames()); appPackage = resStorage.getAppPackage(); appResClass = AndroidResourcesUtils.searchAppResClass(this, resStorage); } public void initClassPath() { try { if (this.clsp == null) { ClspGraph newClsp = new ClspGraph(this); if (args.isLoadJadxClsSetFile()) { newClsp.loadClsSetFile(); } newClsp.addApp(classes); newClsp.initCache(); this.clsp = newClsp; } } catch (Exception e) { throw new JadxRuntimeException("Error loading jadx class set", e); } } private void updateObfuscatedFiles(IResTableParser parser, List resources) { if (args.isSkipResources()) { return; } boolean useHeaders = args.isUseHeadersForDetectResourceExtensions(); long start = System.currentTimeMillis(); int renamedCount = 0; ResourceStorage resStorage = parser.getResStorage(); ValuesParser valuesParser = new ValuesParser(parser.getStrings(), resStorage.getResourcesNames()); Map entryNames = new HashMap<>(); for (ResourceEntry resEntry : resStorage.getResources()) { String val = valuesParser.getSimpleValueString(resEntry); if (val != null) { entryNames.put(val, resEntry); } } for (ResourceFile resource : resources) { ResourceEntry resEntry = entryNames.get(resource.getOriginalName()); if (resEntry != null) { if (resource.setAlias(resEntry, useHeaders)) { renamedCount++; } } } if (LOG.isDebugEnabled()) { LOG.debug("Renamed obfuscated resources: {}, duration: {}ms", renamedCount, System.currentTimeMillis() - start); } } private void initInnerClasses() { // move inner classes List inner = new ArrayList<>(); for (ClassNode cls : classes) { if (cls.getClassInfo().isInner()) { inner.add(cls); } } List updated = new ArrayList<>(); for (ClassNode cls : inner) { ClassInfo clsInfo = cls.getClassInfo(); ClassNode parent = resolveParentClass(clsInfo); if (parent == null) { updated.add(cls); cls.notInner(); } else { parent.addInnerClass(cls); } } // reload names for inner classes of updated parents for (ClassNode updCls : updated) { for (ClassNode innerCls : updCls.getInnerClasses()) { innerCls.getClassInfo().updateNames(this); } } for (PackageNode pkg : packages) { pkg.getClasses().removeIf(cls -> cls.getClassInfo().isInner()); } } public void mergePasses(Map> customPasses) { DecompilationMode mode = args.getDecompilationMode(); if (mode == DecompilationMode.FALLBACK || mode == DecompilationMode.SIMPLE) { // for predefined modes ignore custom (and plugin) passes return; } new PassMerge(preDecompilePasses) .merge(customPasses.get(JadxPreparePass.TYPE), p -> new PreparePassWrapper((JadxPreparePass) p)); new PassMerge(processClasses.getPasses()) .merge(customPasses.get(JadxDecompilePass.TYPE), p -> new DecompilePassWrapper((JadxDecompilePass) p)); if (args.isRunDebugChecks()) { preDecompilePasses = DebugChecks.insertPasses(preDecompilePasses); processClasses = new ProcessClass(DebugChecks.insertPasses(processClasses.getPasses())); } List disabledPasses = args.getDisabledPasses(); if (!disabledPasses.isEmpty()) { Set disabledSet = new HashSet<>(disabledPasses); Predicate filter = p -> { if (disabledSet.contains(p.getName())) { LOG.debug("Disable pass: {}", p.getName()); return true; } return false; }; preDecompilePasses.removeIf(filter); processClasses.getPasses().removeIf(filter); } } public void runPreDecompileStage() { boolean debugEnabled = LOG.isDebugEnabled(); for (IDexTreeVisitor pass : preDecompilePasses) { Utils.checkThreadInterrupt(); long start = debugEnabled ? System.currentTimeMillis() : 0; try { pass.init(this); } catch (Exception e) { LOG.error("Visitor init failed: {}", pass.getClass().getSimpleName(), e); } for (ClassNode cls : classes) { if (cls.isInner()) { continue; } DepthTraversal.visit(pass, cls); } if (debugEnabled) { LOG.debug("Prepare pass: '{}' - {}ms", pass, System.currentTimeMillis() - start); } } } public void runPreDecompileStageForClass(ClassNode cls) { for (IDexTreeVisitor pass : preDecompilePasses) { DepthTraversal.visit(pass, cls); } } // TODO: make better API for reload passes lists public void resetPasses() { preDecompilePasses.clear(); preDecompilePasses.addAll(Jadx.getPreDecompilePassesList()); processClasses.getPasses().clear(); processClasses.getPasses().addAll(Jadx.getPassesList(args)); } public void restartVisitors() { for (ClassNode cls : classes) { cls.unload(); cls.clearAttributes(); cls.unloadFromCache(); } runPreDecompileStage(); } public List getClasses() { return classes; } public List getClassesWithoutInner() { return getClasses(false); } public List getClasses(boolean includeInner) { if (includeInner) { return classes; } List notInnerClasses = new ArrayList<>(); for (ClassNode cls : classes) { if (!cls.getClassInfo().isInner()) { notInnerClasses.add(cls); } } return notInnerClasses; } public List getPackages() { return packages; } public @Nullable PackageNode resolvePackage(String fullPkg) { return pkgMap.get(fullPkg); } public @Nullable PackageNode resolvePackage(@Nullable PackageInfo pkgInfo) { return pkgInfo == null ? null : pkgMap.get(pkgInfo.getFullName()); } public void addPackage(PackageNode pkg) { pkgMap.put(pkg.getPkgInfo().getFullName(), pkg); packages.add(pkg); } public void removePackage(PackageNode pkg) { if (pkgMap.remove(pkg.getPkgInfo().getFullName()) != null) { packages.remove(pkg); PackageNode parentPkg = pkg.getParentPkg(); if (parentPkg != null) { parentPkg.getSubPackages().remove(pkg); if (parentPkg.isEmpty()) { removePackage(parentPkg); } } for (PackageNode subPkg : pkg.getSubPackages()) { removePackage(subPkg); } } } public void sortPackages() { Collections.sort(packages); } public void removeClsFromPackage(PackageNode pkg, ClassNode cls) { boolean removed = pkg.getClasses().remove(cls); if (removed && pkg.isEmpty()) { removePackage(pkg); } } /** * Update sub packages */ public void runPackagesUpdate() { for (PackageNode pkg : getPackages()) { if (pkg.isRoot()) { pkg.updatePackages(); } } } @Nullable public ClassNode resolveClass(ClassInfo clsInfo) { return clsMap.get(clsInfo); } @Nullable public ClassNode resolveClass(ArgType clsType) { if (!clsType.isTypeKnown() || clsType.isGenericType()) { return null; } if (clsType.getWildcardBound() == ArgType.WildcardBound.UNBOUND) { return null; } if (clsType.isGeneric()) { clsType = ArgType.object(clsType.getObject()); } return resolveClass(ClassInfo.fromType(this, clsType)); } @Nullable public ClassNode resolveClass(String fullName) { ClassInfo clsInfo = ClassInfo.fromName(this, fullName); return resolveClass(clsInfo); } @Nullable public ClassNode resolveRawClass(String rawFullName) { return rawClsMap.get(rawFullName); } /** * Find and correct the parent of an inner class. *
* Sometimes inner ClassInfo generated wrong parent info. * e.g. inner is `Cls$mth$1`, current parent = `Cls$mth`, real parent = `Cls` */ @Nullable public ClassNode resolveParentClass(ClassInfo clsInfo) { ClassInfo parentInfo = clsInfo.getParentClass(); ClassNode parentNode = resolveClass(parentInfo); if (parentNode == null && parentInfo != null) { String parClsName = parentInfo.getFullName(); // strip last part as method name int sep = parClsName.lastIndexOf('.'); if (sep > 0 && sep != parClsName.length() - 1) { String mthName = parClsName.substring(sep + 1); String upperParClsName = parClsName.substring(0, sep); ClassNode tmpParent = resolveClass(upperParClsName); if (tmpParent != null && tmpParent.searchMethodByShortName(mthName) != null) { parentNode = tmpParent; clsInfo.convertToInner(parentNode); } } } return parentNode; } /** * Searches for ClassNode by its full name (original or alias name) *
* Warning: This method has a runtime of O(n) (n = number of classes). * If you need to call it more than once consider {@link #buildFullAliasClassCache()} instead */ @Nullable public ClassNode searchClassByFullAlias(String fullName) { for (ClassNode cls : classes) { ClassInfo classInfo = cls.getClassInfo(); if (classInfo.getFullName().equals(fullName) || classInfo.getAliasFullName().equals(fullName)) { return cls; } } return null; } public Map buildFullAliasClassCache() { Map classNameCache = new HashMap<>(classes.size()); for (ClassNode cls : classes) { ClassInfo classInfo = cls.getClassInfo(); String fullName = classInfo.getFullName(); String alias = classInfo.getAliasFullName(); classNameCache.put(fullName, cls); if (alias != null && !fullName.equals(alias)) { classNameCache.put(alias, cls); } } return classNameCache; } public List searchClassByShortName(String shortName) { List list = new ArrayList<>(); for (ClassNode cls : classes) { if (cls.getClassInfo().getShortName().equals(shortName)) { list.add(cls); } } return list; } @Nullable public MethodNode resolveMethod(@NotNull MethodInfo mth) { ClassNode cls = resolveClass(mth.getDeclClass()); if (cls == null) { return null; } MethodNode methodNode = cls.searchMethod(mth); if (methodNode != null) { return methodNode; } return deepResolveMethod(cls, mth.makeSignature(false)); } public @NotNull MethodNode resolveDirectMethod(String rawClsName, String mthShortId) { ClassNode clsNode = resolveRawClass(rawClsName); if (clsNode == null) { throw new RuntimeException("Class not found: " + rawClsName); } MethodNode methodNode = clsNode.searchMethodByShortId(mthShortId); if (methodNode == null) { throw new RuntimeException("Method not found: " + rawClsName + "." + mthShortId); } return methodNode; } @Nullable private MethodNode deepResolveMethod(@NotNull ClassNode cls, String signature) { for (MethodNode m : cls.getMethods()) { if (m.getMethodInfo().getShortId().startsWith(signature)) { return m; } } MethodNode found; ArgType superClass = cls.getSuperClass(); if (superClass != null) { ClassNode superNode = resolveClass(superClass); if (superNode != null) { found = deepResolveMethod(superNode, signature); if (found != null) { return found; } } } for (ArgType iFaceType : cls.getInterfaces()) { ClassNode iFaceNode = resolveClass(iFaceType); if (iFaceNode != null) { found = deepResolveMethod(iFaceNode, signature); if (found != null) { return found; } } } return null; } @Nullable public FieldNode resolveField(FieldInfo field) { ClassNode cls = resolveClass(field.getDeclClass()); if (cls == null) { return null; } FieldNode fieldNode = cls.searchField(field); if (fieldNode != null) { return fieldNode; } return deepResolveField(cls, field); } @Nullable private FieldNode deepResolveField(@NotNull ClassNode cls, FieldInfo fieldInfo) { FieldNode field = cls.searchFieldByNameAndType(fieldInfo); if (field != null) { return field; } ArgType superClass = cls.getSuperClass(); if (superClass != null) { ClassNode superNode = resolveClass(superClass); if (superNode != null) { FieldNode found = deepResolveField(superNode, fieldInfo); if (found != null) { return found; } } } for (ArgType iFaceType : cls.getInterfaces()) { ClassNode iFaceNode = resolveClass(iFaceType); if (iFaceNode != null) { FieldNode found = deepResolveField(iFaceNode, fieldInfo); if (found != null) { return found; } } } return null; } public ProcessClass getProcessClasses() { return processClasses; } public List getPasses() { return processClasses.getPasses(); } public List getPreDecompilePasses() { return preDecompilePasses; } public void initPasses() { processClasses.initPasses(this); } public ICodeWriter makeCodeWriter() { JadxArgs jadxArgs = this.args; return jadxArgs.getCodeWriterProvider().apply(jadxArgs); } public void registerCodeDataUpdateListener(ICodeDataUpdateListener listener) { this.codeDataUpdateListeners.add(listener); } public void notifyCodeDataListeners() { ICodeData codeData = args.getCodeData(); codeDataUpdateListeners.forEach(l -> l.updated(codeData)); } public ClspGraph getClsp() { return clsp; } public ErrorsCounter getErrorsCounter() { return errorsCounter; } @Nullable public String getAppPackage() { return appPackage; } @Nullable public ClassNode getAppResClass() { return appResClass; } public StringUtils getStringUtils() { return stringUtils; } public ConstStorage getConstValues() { return constValues; } public InfoStorage getInfoStorage() { return infoStorage; } public CacheStorage getCacheStorage() { return cacheStorage; } public JadxArgs getArgs() { return args; } public @Nullable JadxDecompiler getDecompiler() { return decompiler; } public TypeUpdate getTypeUpdate() { return typeUpdate; } public TypeCompare getTypeCompare() { return typeUpdate.getTypeCompare(); } public ICodeCache getCodeCache() { return args.getCodeCache(); } public MethodUtils getMethodUtils() { return methodUtils; } public TypeUtils getTypeUtils() { return typeUtils; } public AttributeStorage getAttributes() { return attributes; } public GradleInfoStorage getGradleInfoStorage() { return gradleInfoStorage; } public synchronized ManifestAttributes initManifestAttributes() { ManifestAttributes attrs = manifestAttributes; if (attrs == null) { attrs = new ManifestAttributes(args.getSecurity()); manifestAttributes = attrs; } return attrs; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java ================================================ package jadx.core.dex.nodes.parser; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.SignatureAttr; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.utils.exceptions.JadxRuntimeException; public class SignatureParser { private static final Logger LOG = LoggerFactory.getLogger(SignatureParser.class); private static final char STOP_CHAR = 0; private final String sign; private final int end; private int pos; private int mark; public SignatureParser(String signature) { sign = signature; end = sign.length(); pos = -1; mark = 0; } @Nullable public static SignatureParser fromNode(IAttributeNode node) { String signature = getSignature(node); if (signature == null) { return null; } return new SignatureParser(signature); } @Nullable public static String getSignature(IAttributeNode node) { SignatureAttr attr = node.get(JadxAttrType.SIGNATURE); if (attr == null) { return null; } return attr.getSignature(); } private char next() { pos++; if (pos >= end) { return STOP_CHAR; } return sign.charAt(pos); } private boolean lookAhead(char ch) { int next = pos + 1; return next < end && sign.charAt(next) == ch; } private void mark() { mark = pos; } /** * Exclusive slice. * * @return string from 'mark' to current position (not including current character) */ private String slice() { int start = mark == -1 ? 0 : mark; if (start >= pos) { return ""; } return sign.substring(start, pos); } /** * Inclusive slice (includes current character) */ private String inclusiveSlice() { int start = mark; if (start == -1) { start = 0; } int last = pos + 1; if (start >= last) { return ""; } return sign.substring(start, last); } private boolean skipUntil(char untilChar) { int startPos = pos; while (true) { if (lookAhead(untilChar)) { return true; } char ch = next(); if (ch == STOP_CHAR) { pos = startPos; return false; } } } private void consume(char exp) { char c = next(); if (exp != c) { throw new JadxRuntimeException("Consume wrong char: '" + c + "' != '" + exp + "', sign: " + debugString()); } } private boolean tryConsume(char exp) { if (lookAhead(exp)) { next(); return true; } return false; } @Nullable public String consumeUntil(char lastChar) { mark(); return skipUntil(lastChar) ? inclusiveSlice() : null; } public ArgType consumeType() { char ch = next(); switch (ch) { case 'L': ArgType obj = consumeObjectType(false); if (obj != null) { return obj; } break; case 'T': next(); mark(); String typeVarName = consumeUntil(';'); if (typeVarName != null) { consume(';'); if (typeVarName.contains(")")) { throw new JadxRuntimeException("Bad name for type variable: " + typeVarName); } return ArgType.genericType(typeVarName); } break; case '[': return ArgType.array(consumeType()); case STOP_CHAR: return null; default: // primitive type (one char) ArgType type = ArgType.parse(ch); if (type != null) { return type; } break; } throw new JadxRuntimeException("Can't parse type: " + debugString() + ", unexpected: " + ch); } public List consumeTypeList() { List list = null; while (true) { ArgType type = consumeType(); if (type == null) { break; } if (list == null) { list = new ArrayList<>(); } list.add(type); } if (list == null) { return Collections.emptyList(); } return list; } private ArgType consumeObjectType(boolean innerType) { mark(); int ch; do { if (innerType && lookAhead('.')) { // stop before next nested inner class return ArgType.object(inclusiveSlice()); } ch = next(); if (ch == STOP_CHAR) { return null; } } while (ch != '<' && ch != ';'); if (ch == ';') { String obj; if (innerType) { obj = slice().replace('/', '.'); } else { obj = inclusiveSlice(); } return ArgType.object(obj); } // generic type start ('<') String obj = slice(); if (!innerType) { obj += ';'; } else { obj = obj.replace('/', '.'); } List typeVars = consumeGenericArgs(); consume('>'); ArgType genericType = ArgType.generic(obj, typeVars); if (!lookAhead('.')) { consume(';'); return genericType; } consume('.'); next(); // type parsing not completed, proceed to inner class ArgType inner = consumeObjectType(true); if (inner == null) { throw new JadxRuntimeException("No inner type found: " + debugString()); } // for every nested inner type create nested type object while (lookAhead('.')) { genericType = ArgType.outerGeneric(genericType, inner); consume('.'); next(); inner = consumeObjectType(true); if (inner == null) { throw new JadxRuntimeException("Unexpected inner type found: " + debugString()); } } return ArgType.outerGeneric(genericType, inner); } private List consumeGenericArgs() { List list = new ArrayList<>(); ArgType type; do { if (lookAhead('*')) { next(); type = ArgType.wildcard(); } else if (lookAhead('+')) { next(); type = ArgType.wildcard(consumeType(), ArgType.WildcardBound.EXTENDS); } else if (lookAhead('-')) { next(); type = ArgType.wildcard(consumeType(), ArgType.WildcardBound.SUPER); } else { type = consumeType(); } if (type != null) { list.add(type); } } while (type != null && !lookAhead('>')); return list; } /** * Map of generic types names to extends classes. *

* Example: "<T:Ljava/lang/Exception;:Ljava/lang/Object;>" */ @SuppressWarnings("ConditionalBreakInInfiniteLoop") public List consumeGenericTypeParameters() { if (!lookAhead('<')) { return Collections.emptyList(); } List list = new ArrayList<>(); consume('<'); while (true) { if (lookAhead('>') || next() == STOP_CHAR) { break; } String id = consumeUntil(':'); if (id == null) { throw new JadxRuntimeException("Failed to parse generic types map"); } consume(':'); tryConsume(':'); List types = consumeExtendsTypesList(); list.add(ArgType.genericType(id, types)); } consume('>'); return list; } /** * List of types separated by ':' last type is 'java.lang.Object'. *

* Example: "Ljava/lang/Exception;:Ljava/lang/Object;" */ private List consumeExtendsTypesList() { List types = Collections.emptyList(); boolean next; do { ArgType argType = consumeType(); if (argType == null) { throw new JadxRuntimeException("Unexpected end of signature"); } if (!argType.equals(ArgType.OBJECT)) { if (types.isEmpty()) { types = new ArrayList<>(); } types.add(argType); } next = lookAhead(':'); if (next) { consume(':'); } } while (next); return types; } public List consumeMethodArgs(int argsCount) { consume('('); if (lookAhead(')')) { consume(')'); return Collections.emptyList(); } List args = new ArrayList<>(argsCount); int limit = argsCount + 10; // just prevent endless loop, args count can be different for synthetic methods do { ArgType type = consumeType(); if (type == null) { throw new JadxRuntimeException("Unexpected end of signature"); } args.add(type); if (args.size() > limit) { throw new JadxRuntimeException("Arguments count limit reached: " + args.size()); } } while (!lookAhead(')')); consume(')'); return args; } private static String mergeSignature(List list) { if (list.size() == 1) { return list.get(0); } StringBuilder sb = new StringBuilder(); for (String s : list) { sb.append(s); } return sb.toString(); } public String getSignature() { return sign; } private String debugString() { if (pos >= sign.length()) { return sign; } return sign + " at position " + pos + " ('" + sign.charAt(pos) + "')"; } @Override public String toString() { if (pos == -1) { return sign; } return sign.substring(0, mark) + '{' + sign.substring(mark, pos) + '}' + sign.substring(pos); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/utils/MethodUtils.java ================================================ package jadx.core.dex.nodes.utils; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.core.clsp.ClspClass; import jadx.core.clsp.ClspMethod; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodBridgeAttr; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; public class MethodUtils { private final RootNode root; public MethodUtils(RootNode rootNode) { this.root = rootNode; } @Nullable public IMethodDetails getMethodDetails(BaseInvokeNode invokeNode) { IMethodDetails methodDetails = invokeNode.get(AType.METHOD_DETAILS); if (methodDetails != null) { return methodDetails; } return getMethodDetails(invokeNode.getCallMth()); } @Nullable public IMethodDetails getMethodDetails(MethodInfo callMth) { MethodNode mthNode = root.resolveMethod(callMth); if (mthNode != null) { return mthNode; } return root.getClsp().getMethodDetails(callMth); } @Nullable public MethodNode resolveMethod(BaseInvokeNode invokeNode) { IMethodDetails methodDetails = getMethodDetails(invokeNode); if (methodDetails instanceof MethodNode) { return (MethodNode) methodDetails; } return null; } public boolean isSkipArg(BaseInvokeNode invokeNode, InsnArg arg) { MethodNode mth = resolveMethod(invokeNode); if (mth == null) { return false; } SkipMethodArgsAttr skipArgsAttr = mth.get(AType.SKIP_MTH_ARGS); if (skipArgsAttr == null) { return false; } int argIndex = invokeNode.getArgIndex(arg); return skipArgsAttr.isSkip(argIndex); } /** * Search methods with same name and args count in class hierarchy starting from {@code startCls} * Beware {@code startCls} can be different from {@code mthInfo.getDeclClass()} */ public boolean isMethodArgsOverloaded(ArgType startCls, MethodInfo mthInfo) { return processMethodArgsOverloaded(startCls, mthInfo, null); } public List collectOverloadedMethods(ArgType startCls, MethodInfo mthInfo) { List list = new ArrayList<>(); processMethodArgsOverloaded(startCls, mthInfo, list); return list; } @Nullable public ArgType getMethodGenericReturnType(BaseInvokeNode invokeNode) { IMethodDetails methodDetails = getMethodDetails(invokeNode); if (methodDetails != null) { ArgType returnType = methodDetails.getReturnType(); if (returnType != null && returnType.containsGeneric()) { return returnType; } } return null; } private boolean processMethodArgsOverloaded(ArgType startCls, MethodInfo mthInfo, @Nullable List collectedMths) { if (startCls == null || !startCls.isObject()) { return false; } boolean isMthConstructor = mthInfo.isConstructor() || mthInfo.isClassInit(); ClassNode classNode = root.resolveClass(startCls); if (classNode != null) { for (MethodNode mth : classNode.getMethods()) { if (mthInfo.isOverloadedBy(mth.getMethodInfo())) { if (collectedMths == null) { return true; } collectedMths.add(mth); } } if (!isMthConstructor) { if (processMethodArgsOverloaded(classNode.getSuperClass(), mthInfo, collectedMths)) { if (collectedMths == null) { return true; } } for (ArgType parentInterface : classNode.getInterfaces()) { if (processMethodArgsOverloaded(parentInterface, mthInfo, collectedMths)) { if (collectedMths == null) { return true; } } } } } else { ClspClass clsDetails = root.getClsp().getClsDetails(startCls); if (clsDetails == null) { // class info not available return false; } for (ClspMethod clspMth : clsDetails.getMethodsMap().values()) { if (mthInfo.isOverloadedBy(clspMth.getMethodInfo())) { if (collectedMths == null) { return true; } collectedMths.add(clspMth); } } if (!isMthConstructor) { for (ArgType parent : clsDetails.getParents()) { if (processMethodArgsOverloaded(parent, mthInfo, collectedMths)) { if (collectedMths == null) { return true; } } } } } return false; } @Nullable public IMethodDetails getOverrideBaseMth(MethodNode mth) { MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE); if (overrideAttr == null) { return null; } return Utils.getOne(overrideAttr.getBaseMethods()); } public ClassInfo getMethodOriginDeclClass(MethodNode mth) { IMethodDetails baseMth = getOverrideBaseMth(mth); if (baseMth != null) { return baseMth.getMethodInfo().getDeclClass(); } MethodBridgeAttr bridgeAttr = mth.get(AType.BRIDGED_BY); if (bridgeAttr != null) { return getMethodOriginDeclClass(bridgeAttr.getBridgeMth()); } return mth.getMethodInfo().getDeclClass(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/utils/SelectFromDuplicates.java ================================================ package jadx.core.dex.nodes.utils; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.nodes.ClassNode; /** * Select best class from list of classes with same full name * Current implementation: use class with source file as 'classesN.dex' where N is minimal */ public class SelectFromDuplicates { private static final Logger LOG = LoggerFactory.getLogger(SelectFromDuplicates.class); private static final Pattern CLASSES_DEX_PATTERN = Pattern.compile("classes([1-9]\\d*)\\.dex"); public static ClassNode process(List dupClsList) { ClassNode bestCls = null; int bestClsIndex = -1; for (ClassNode clsNode : dupClsList) { boolean selectCurrent = false; if (bestCls == null) { selectCurrent = true; } else { int clsIndex = getClassesIndex(clsNode.getInputFileName()); if (clsIndex != -1) { if (bestClsIndex != -1) { // if both are valid, the lower index has precedence if (clsIndex < bestClsIndex) { selectCurrent = true; } } else { // valid dex names have precedence selectCurrent = true; } } } if (selectCurrent) { bestCls = clsNode; bestClsIndex = getClassesIndex(clsNode.getInputFileName()); } } return bestCls; } /** * Get N from classesN.dex * * @return -1 if source is not valid dex name */ private static int getClassesIndex(String source) { if ("classes.dex".equals(source)) { return 1; } try { Matcher matcher = CLASSES_DEX_PATTERN.matcher(source); if (!matcher.matches()) { return -1; } String num = matcher.group(1); if (num.equals("1")) { return -1; } return Integer.parseInt(num); } catch (Exception e) { LOG.debug("Failed to parse source classes index", e); return -1; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/nodes/utils/TypeUtils.java ================================================ package jadx.core.dex.nodes.utils; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; import org.jetbrains.annotations.Nullable; import jadx.core.clsp.ClspClass; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.ClassTypeVarsAttr; import jadx.core.dex.attributes.nodes.MethodTypeVarsAttr; import jadx.core.dex.attributes.nodes.NotificationAttrNode; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; import static jadx.core.utils.Utils.isEmpty; import static jadx.core.utils.Utils.notEmpty; public class TypeUtils { private final RootNode root; public TypeUtils(RootNode rootNode) { this.root = rootNode; } public List getClassGenerics(ArgType type) { ClassNode classNode = root.resolveClass(type); if (classNode != null) { return classNode.getGenericTypeParameters(); } ClspClass clsDetails = root.getClsp().getClsDetails(type); if (clsDetails == null || clsDetails.getTypeParameters().isEmpty()) { return Collections.emptyList(); } List generics = clsDetails.getTypeParameters(); return generics == null ? Collections.emptyList() : generics; } @Nullable public ClassTypeVarsAttr getClassTypeVars(ArgType type) { ClassNode classNode = root.resolveClass(type); if (classNode == null) { return null; } ClassTypeVarsAttr typeVarsAttr = classNode.get(AType.CLASS_TYPE_VARS); if (typeVarsAttr != null) { return typeVarsAttr; } return buildClassTypeVarsAttr(classNode); } public ArgType expandTypeVariables(ClassNode cls, ArgType type) { if (type.containsTypeVariable()) { expandTypeVar(cls, type, getKnownTypeVarsAtClass(cls)); } return type; } public ArgType expandTypeVariables(MethodNode mth, ArgType type) { if (type.containsTypeVariable()) { expandTypeVar(mth, type, getKnownTypeVarsAtMethod(mth)); } return type; } private void expandTypeVar(NotificationAttrNode node, ArgType type, Collection typeVars) { if (typeVars.isEmpty()) { return; } boolean allExtendsEmpty = true; for (ArgType argType : typeVars) { if (notEmpty(argType.getExtendTypes())) { allExtendsEmpty = false; break; } } if (allExtendsEmpty) { return; } type.visitTypes(t -> { if (t.isGenericType()) { String typeVarName = t.getObject(); for (ArgType typeVar : typeVars) { if (typeVar.getObject().equals(typeVarName)) { t.setExtendTypes(typeVar.getExtendTypes()); return null; } } node.addWarnComment("Unknown type variable: " + typeVarName + " in type: " + type); } return null; }); } public Set getKnownTypeVarsAtMethod(MethodNode mth) { MethodTypeVarsAttr typeVarsAttr = mth.get(AType.METHOD_TYPE_VARS); if (typeVarsAttr != null) { return typeVarsAttr.getTypeVars(); } Set typeVars = collectKnownTypeVarsAtMethod(mth); MethodTypeVarsAttr varsAttr = MethodTypeVarsAttr.build(typeVars); mth.addAttr(varsAttr); return varsAttr.getTypeVars(); } private static Collection getKnownTypeVarsAtClass(ClassNode cls) { if (cls.isInner()) { Set typeVars = new HashSet<>(cls.getGenericTypeParameters()); cls.visitParentClasses(parent -> typeVars.addAll(parent.getGenericTypeParameters())); return typeVars; } return cls.getGenericTypeParameters(); } private static Set collectKnownTypeVarsAtMethod(MethodNode mth) { Set typeVars = new HashSet<>(); typeVars.addAll(getKnownTypeVarsAtClass(mth.getParentClass())); typeVars.addAll(mth.getTypeParameters()); return typeVars.isEmpty() ? Collections.emptySet() : typeVars; } /** * Search for unknown type vars at current method. Return only first. * * @return unknown type var, null if not found */ @Nullable public ArgType checkForUnknownTypeVars(MethodNode mth, ArgType checkType) { Set knownTypeVars = getKnownTypeVarsAtMethod(mth); return checkType.visitTypes(type -> { if (type.isGenericType() && !knownTypeVars.contains(type)) { return type; } return null; }); } public boolean containsUnknownTypeVar(MethodNode mth, ArgType type) { return checkForUnknownTypeVars(mth, type) != null; } /** * Replace generic types in {@code typeWithGeneric} using instance types *
* Example: *

    *
  • {@code instanceType: Set} *
  • {@code typeWithGeneric: Iterator} *
  • {@code return: Iterator} *
*/ @Nullable public ArgType replaceClassGenerics(ArgType instanceType, ArgType typeWithGeneric) { return replaceClassGenerics(instanceType, instanceType, typeWithGeneric); } @Nullable public ArgType replaceClassGenerics(ArgType instanceType, ArgType genericSourceType, ArgType typeWithGeneric) { if (typeWithGeneric == null || genericSourceType == null) { return null; } Map typeVarsMap = Collections.emptyMap(); ClassTypeVarsAttr typeVars = getClassTypeVars(instanceType); if (typeVars != null) { typeVarsMap = mergeTypeMaps(typeVarsMap, typeVars.getTypeVarsMapFor(genericSourceType)); } typeVarsMap = mergeTypeMaps(typeVarsMap, getTypeVariablesMapping(instanceType)); ArgType outerType = instanceType.getOuterType(); while (outerType != null) { typeVarsMap = mergeTypeMaps(typeVarsMap, getTypeVariablesMapping(outerType)); outerType = outerType.getOuterType(); } return replaceTypeVariablesUsingMap(typeWithGeneric, typeVarsMap); } private static Map mergeTypeMaps(Map base, Map addition) { if (base.isEmpty()) { return addition; } if (addition.isEmpty()) { return base; } Map map = new HashMap<>(base.size() + addition.size()); for (Map.Entry entry : base.entrySet()) { ArgType value = entry.getValue(); ArgType type = addition.remove(value); if (type != null) { map.put(entry.getKey(), type); } else { map.put(entry.getKey(), entry.getValue()); } } map.putAll(addition); return map; } public Map getTypeVariablesMapping(ArgType clsType) { if (!clsType.isGeneric()) { return Collections.emptyMap(); } List typeParameters = root.getTypeUtils().getClassGenerics(clsType); if (typeParameters.isEmpty()) { return Collections.emptyMap(); } List actualTypes = clsType.getGenericTypes(); if (isEmpty(actualTypes)) { return Collections.emptyMap(); } int genericParamsCount = actualTypes.size(); if (genericParamsCount != typeParameters.size()) { return Collections.emptyMap(); } Map replaceMap = new HashMap<>(genericParamsCount); for (int i = 0; i < genericParamsCount; i++) { ArgType actualType = actualTypes.get(i); ArgType typeVar = typeParameters.get(i); if (typeVar.getExtendTypes() != null) { // force short form (only type var name) typeVar = ArgType.genericType(typeVar.getObject()); } replaceMap.put(typeVar, actualType); } return replaceMap; } public Map getTypeVarMappingForInvoke(BaseInvokeNode invokeInsn) { IMethodDetails mthDetails = root.getMethodUtils().getMethodDetails(invokeInsn); if (mthDetails == null) { return Collections.emptyMap(); } Map map = new HashMap<>(1 + invokeInsn.getArgsCount()); addTypeVarMapping(map, mthDetails.getReturnType(), invokeInsn.getResult()); int argCount = Math.min(mthDetails.getArgTypes().size(), invokeInsn.getArgsCount() - invokeInsn.getFirstArgOffset()); for (int i = 0; i < argCount; i++) { addTypeVarMapping(map, mthDetails.getArgTypes().get(i), invokeInsn.getArg(i + invokeInsn.getFirstArgOffset())); } return map; } private static void addTypeVarMapping(Map map, ArgType typeVar, InsnArg arg) { if (arg == null || typeVar == null || !typeVar.isTypeKnown()) { return; } if (typeVar.isGenericType()) { map.put(typeVar, arg.getType()); } // TODO: resolve inner type vars: 'List -> List' to 'T -> String' } @Nullable public ArgType replaceMethodGenerics(BaseInvokeNode invokeInsn, IMethodDetails details, ArgType typeWithGeneric) { if (typeWithGeneric == null) { return null; } List methodArgTypes = details.getArgTypes(); if (methodArgTypes.isEmpty()) { return null; } int firstArgOffset = invokeInsn.getFirstArgOffset(); int argsCount = methodArgTypes.size(); for (int i = 0; i < argsCount; i++) { ArgType methodArgType = methodArgTypes.get(i); InsnArg insnArg = invokeInsn.getArg(i + firstArgOffset); ArgType insnType = insnArg.getType(); if (methodArgType.equals(typeWithGeneric)) { return insnType; } } // TODO build complete map for type variables return null; } @Nullable public ArgType replaceTypeVariablesUsingMap(ArgType replaceType, Map replaceMap) { if (replaceMap.isEmpty()) { return null; } if (replaceType.isGenericType()) { return replaceMap.get(replaceType); } if (replaceType.isArray()) { ArgType replaced = replaceTypeVariablesUsingMap(replaceType.getArrayElement(), replaceMap); if (replaced == null) { return null; } return ArgType.array(replaced); } ArgType wildcardType = replaceType.getWildcardType(); if (wildcardType != null && wildcardType.containsTypeVariable()) { ArgType newWildcardType = replaceTypeVariablesUsingMap(wildcardType, replaceMap); if (newWildcardType == null) { return null; } return ArgType.wildcard(newWildcardType, replaceType.getWildcardBound()); } if (replaceType.isGeneric()) { ArgType outerType = replaceType.getOuterType(); if (outerType != null) { ArgType replacedOuter = replaceTypeVariablesUsingMap(outerType, replaceMap); if (replacedOuter == null) { return null; } ArgType innerType = replaceType.getInnerType(); ArgType replacedInner = replaceTypeVariablesUsingMap(innerType, replaceMap); return ArgType.outerGeneric(replacedOuter, replacedInner == null ? innerType : replacedInner); } List genericTypes = replaceType.getGenericTypes(); if (notEmpty(genericTypes)) { List newTypes = Utils.collectionMap(genericTypes, t -> { ArgType type = replaceTypeVariablesUsingMap(t, replaceMap); return type == null ? t : type; }); return ArgType.generic(replaceType, newTypes); } } return null; } private ClassTypeVarsAttr buildClassTypeVarsAttr(ClassNode cls) { Map> map = new HashMap<>(); ArgType currentClsType = cls.getClassInfo().getType(); map.put(currentClsType.getObject(), getTypeVariablesMapping(currentClsType)); cls.visitSuperTypes((parent, type) -> { List currentVars = type.getGenericTypes(); if (Utils.isEmpty(currentVars)) { return; } int varsCount = currentVars.size(); List sourceTypeVars = getClassGenerics(type); if (varsCount == sourceTypeVars.size()) { Map parentTypeMap = map.get(parent.getObject()); Map varsMap = new HashMap<>(varsCount); for (int i = 0; i < varsCount; i++) { ArgType currentTypeVar = currentVars.get(i); ArgType resultType = parentTypeMap != null ? parentTypeMap.get(currentTypeVar) : null; varsMap.put(sourceTypeVars.get(i), resultType != null ? resultType : currentTypeVar); } map.put(type.getObject(), varsMap); } }); List currentTypeVars = cls.getGenericTypeParameters(); ClassTypeVarsAttr typeVarsAttr = new ClassTypeVarsAttr(currentTypeVars, map); cls.addAttr(typeVarsAttr); return typeVarsAttr; } public void visitSuperTypes(ArgType type, BiConsumer consumer) { ClassNode cls = root.resolveClass(type); if (cls != null) { cls.visitSuperTypes(consumer); } else { ClspClass clspClass = root.getClsp().getClsDetails(type); if (clspClass != null) { for (ArgType superType : clspClass.getParents()) { if (!superType.equals(ArgType.OBJECT)) { consumer.accept(type, superType); visitSuperTypes(superType, consumer); } } } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/AbstractRegion.java ================================================ package jadx.core.dex.regions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.attributes.AttrNode; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; public abstract class AbstractRegion extends AttrNode implements IRegion { private static final Logger LOG = LoggerFactory.getLogger(AbstractRegion.class); private IRegion parent; public AbstractRegion(IRegion parent) { this.parent = parent; } @Override public IRegion getParent() { return parent; } @Override public void setParent(IRegion parent) { this.parent = parent; } @Override public boolean replaceSubBlock(IContainer oldBlock, IContainer newBlock) { LOG.warn("Replace sub block not supported for class \"{}\"", this.getClass()); return false; } public void updateParent(IContainer container, IRegion newParent) { if (container instanceof IRegion) { ((IRegion) container).setParent(newParent); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/Region.java ================================================ package jadx.core.dex.regions; import java.util.ArrayList; import java.util.List; import jadx.api.ICodeWriter; import jadx.core.codegen.RegionGen; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.CodegenException; public final class Region extends AbstractRegion { private final List blocks; public Region(IRegion parent) { super(parent); this.blocks = new ArrayList<>(1); } @Override public List getSubBlocks() { return blocks; } public void add(IContainer region) { updateParent(region, this); blocks.add(region); } @Override public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException { for (IContainer c : blocks) { regionGen.makeRegion(code, c); } } @Override public boolean replaceSubBlock(IContainer oldBlock, IContainer newBlock) { int i = blocks.indexOf(oldBlock); if (i != -1) { blocks.set(i, newBlock); updateParent(newBlock, this); return true; } return false; } @Override public String baseString() { StringBuilder sb = new StringBuilder(); int size = blocks.size(); sb.append('('); sb.append(size); if (size > 0) { sb.append(':'); Utils.listToString(sb, blocks, "|", IContainer::baseString); } sb.append(')'); return sb.toString(); } @Override public String toString() { return 'R' + baseString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java ================================================ package jadx.core.dex.regions; import java.util.ArrayList; import java.util.Collections; import java.util.List; import jadx.api.ICodeWriter; import jadx.core.codegen.RegionGen; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBranchRegion; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.CodegenException; public final class SwitchRegion extends AbstractRegion implements IBranchRegion { public static final Object DEFAULT_CASE_KEY = new Object() { @Override public String toString() { return "default"; } }; private final BlockNode header; private final List cases; public SwitchRegion(IRegion parent, BlockNode header) { super(parent); this.header = header; this.cases = new ArrayList<>(); } public static final class CaseInfo { private final List keys; private final IContainer container; public CaseInfo(List keys, IContainer container) { this.keys = keys; this.container = container; } public boolean isDefaultCase() { return keys.size() == 1 && keys.get(0) == DEFAULT_CASE_KEY; } public List getKeys() { return keys; } public IContainer getContainer() { return container; } } public BlockNode getHeader() { return header; } public void addCase(List keysList, IContainer c) { cases.add(new CaseInfo(keysList, c)); } public List getCases() { return cases; } public List getCaseContainers() { return Utils.collectionMap(cases, caseInfo -> caseInfo.container); } @Override public List getSubBlocks() { List all = new ArrayList<>(cases.size() + 1); all.add(header); all.addAll(getCaseContainers()); return Collections.unmodifiableList(all); } @Override public List getBranches() { return Collections.unmodifiableList(getCaseContainers()); } @Override public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException { regionGen.makeSwitch(this, code); } @Override public String baseString() { return "SW:" + header.baseString(); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Switch: ").append(header.baseString()); for (CaseInfo caseInfo : cases) { List keyStrings = Utils.collectionMap(caseInfo.getKeys(), k -> k == DEFAULT_CASE_KEY ? "default" : k.toString()); sb.append("\n case ") .append(Utils.listToString(keyStrings)) .append(" -> ").append(caseInfo.getContainer()); } return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/SynchronizedRegion.java ================================================ package jadx.core.dex.regions; import java.util.ArrayList; import java.util.List; import jadx.api.ICodeWriter; import jadx.core.codegen.RegionGen; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.exceptions.CodegenException; public final class SynchronizedRegion extends AbstractRegion { private final InsnNode enterInsn; private final List exitInsns = new ArrayList<>(); private final Region region; public SynchronizedRegion(IRegion parent, InsnNode insn) { super(parent); this.enterInsn = insn; this.region = new Region(this); } public InsnNode getEnterInsn() { return enterInsn; } public List getExitInsns() { return exitInsns; } public Region getRegion() { return region; } @Override public List getSubBlocks() { return region.getSubBlocks(); } @Override public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException { regionGen.makeSynchronizedRegion(this, code); } @Override public String baseString() { return Integer.toHexString(enterInsn.getOffset()); } @Override public String toString() { return "Synchronized:" + region; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/TryCatchRegion.java ================================================ package jadx.core.dex.regions; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import jadx.api.ICodeWriter; import jadx.core.codegen.RegionGen; import jadx.core.dex.nodes.IBranchRegion; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.trycatch.TryCatchBlockAttr; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.CodegenException; public final class TryCatchRegion extends AbstractRegion implements IBranchRegion { private final IContainer tryRegion; private Map catchRegions = Collections.emptyMap(); private IContainer finallyRegion; private TryCatchBlockAttr tryCatchBlock; public TryCatchRegion(IRegion parent, IContainer tryRegion) { super(parent); this.tryRegion = tryRegion; } public void setTryCatchBlock(TryCatchBlockAttr tryCatchBlock) { this.tryCatchBlock = tryCatchBlock; int count = tryCatchBlock.getHandlersCount(); this.catchRegions = new LinkedHashMap<>(count); for (ExceptionHandler handler : tryCatchBlock.getHandlers()) { IContainer handlerRegion = handler.getHandlerRegion(); if (handlerRegion != null) { if (handler.isFinally()) { finallyRegion = handlerRegion; } else { catchRegions.put(handler, handlerRegion); } } } } public IContainer getTryRegion() { return tryRegion; } public Map getCatchRegions() { return catchRegions; } public TryCatchBlockAttr getTryCatchBlock() { return tryCatchBlock; } public IContainer getFinallyRegion() { return finallyRegion; } public void setFinallyRegion(IContainer finallyRegion) { this.finallyRegion = finallyRegion; } @Override public List getSubBlocks() { List all = new ArrayList<>(2 + catchRegions.size()); all.add(tryRegion); all.addAll(catchRegions.values()); if (finallyRegion != null) { all.add(finallyRegion); } return Collections.unmodifiableList(all); } @Override public List getBranches() { return getSubBlocks(); } @Override public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException { regionGen.makeTryCatch(this, code); } @Override public String baseString() { return tryRegion.baseString(); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Try: ").append(tryRegion); if (!catchRegions.isEmpty()) { sb.append(" catches: ").append(Utils.listToString(catchRegions.values())); } if (finallyRegion != null) { sb.append(" finally: ").append(finallyRegion); } return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/conditions/Compare.java ================================================ package jadx.core.dex.regions.conditions; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.IfOp; import jadx.core.dex.instructions.args.InsnArg; public final class Compare { private final IfNode insn; public Compare(IfNode insn) { insn.add(AFlag.HIDDEN); this.insn = insn; } public IfOp getOp() { return insn.getOp(); } public InsnArg getA() { return insn.getArg(0); } public InsnArg getB() { return insn.getArg(1); } public IfNode getInsn() { return insn; } public Compare invert() { insn.invertCondition(); return this; } public void normalize() { insn.normalize(); } @Override public String toString() { return getA() + " " + getOp().getSymbol() + ' ' + getB(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/conditions/ConditionRegion.java ================================================ package jadx.core.dex.regions.conditions; import java.util.Collections; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IConditionRegion; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.regions.AbstractRegion; import jadx.core.utils.BlockUtils; public abstract class ConditionRegion extends AbstractRegion implements IConditionRegion { @Nullable private IfCondition condition; private List conditionBlocks = Collections.emptyList(); public ConditionRegion(IRegion parent) { super(parent); } @Override @Nullable public IfCondition getCondition() { return condition; } @Override public List getConditionBlocks() { return conditionBlocks; } @Override public void invertCondition() { if (condition != null) { condition = IfCondition.invert(condition); } } @Override public boolean simplifyCondition() { if (condition == null) { return false; } IfCondition updated = IfCondition.simplify(condition); if (updated != condition) { condition = updated; return true; } return false; } @Override public int getConditionSourceLine() { for (BlockNode block : conditionBlocks) { InsnNode lastInsn = BlockUtils.getLastInsn(block); if (lastInsn != null) { int sourceLine = lastInsn.getSourceLine(); if (sourceLine != 0) { return sourceLine; } } } return 0; } /** * Preferred way to update condition info */ public void updateCondition(IfInfo info) { this.condition = info.getCondition(); this.conditionBlocks = info.getMergedBlocks().toList(); } public void updateCondition(IfCondition condition, List conditionBlocks) { this.condition = condition; this.conditionBlocks = conditionBlocks; } public void updateCondition(BlockNode block) { this.condition = IfCondition.fromIfBlock(block); this.conditionBlocks = Collections.singletonList(block); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfCondition.java ================================================ package jadx.core.dex.regions.conditions; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AttrNode; import jadx.core.dex.instructions.ArithNode; import jadx.core.dex.instructions.ArithOp; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.IfOp; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.BlockUtils; import jadx.core.utils.exceptions.JadxRuntimeException; public final class IfCondition extends AttrNode { public enum Mode { COMPARE, TERNARY, NOT, AND, OR } private final Mode mode; private final List args; private final Compare compare; private IfCondition(Compare compare) { this.mode = Mode.COMPARE; this.compare = compare; this.args = Collections.emptyList(); } private IfCondition(Mode mode, List args) { this.mode = mode; this.args = args; this.compare = null; } private IfCondition(IfCondition c) { this.mode = c.mode; this.compare = c.compare; if (c.mode == Mode.COMPARE) { this.args = Collections.emptyList(); } else { this.args = new ArrayList<>(c.args); } } public static IfCondition fromIfBlock(BlockNode header) { InsnNode lastInsn = BlockUtils.getLastInsn(header); if (lastInsn == null) { return null; } return fromIfNode((IfNode) lastInsn); } public static IfCondition fromIfNode(IfNode insn) { return new IfCondition(new Compare(insn)); } public static IfCondition ternary(IfCondition a, IfCondition b, IfCondition c) { return new IfCondition(Mode.TERNARY, Arrays.asList(a, b, c)); } public static IfCondition merge(Mode mode, IfCondition a, IfCondition b) { if (a.getMode() == mode) { IfCondition n = new IfCondition(a); n.addArg(b); return n; } return new IfCondition(mode, Arrays.asList(a, b)); } public Mode getMode() { return mode; } public List getArgs() { return args; } public IfCondition first() { return args.get(0); } public IfCondition second() { return args.get(1); } public IfCondition third() { return args.get(2); } public void addArg(IfCondition c) { args.add(c); } public boolean isCompare() { return mode == Mode.COMPARE; } public Compare getCompare() { return compare; } public static IfCondition invert(IfCondition cond) { Mode mode = cond.getMode(); switch (mode) { case COMPARE: return new IfCondition(cond.getCompare().invert()); case TERNARY: return ternary(cond.first(), not(cond.second()), not(cond.third())); case NOT: return cond.first(); case AND: case OR: List args = cond.getArgs(); List newArgs = new ArrayList<>(args.size()); for (IfCondition arg : args) { newArgs.add(invert(arg)); } return new IfCondition(mode == Mode.AND ? Mode.OR : Mode.AND, newArgs); } throw new JadxRuntimeException("Unknown mode for invert: " + mode); } public static IfCondition not(IfCondition cond) { if (cond.getMode() == Mode.NOT) { return cond.first(); } if (cond.getCompare() != null) { return new IfCondition(cond.compare.invert()); } return new IfCondition(Mode.NOT, Collections.singletonList(cond)); } public static IfCondition simplify(IfCondition cond) { if (cond.isCompare()) { Compare c = cond.getCompare(); IfCondition i = simplifyCmpOp(c); if (i != null) { return i; } if (c.getOp() == IfOp.EQ && c.getB().isFalse()) { cond = new IfCondition(Mode.NOT, Collections.singletonList(new IfCondition(c.invert()))); } else { c.normalize(); } } List args = null; for (int i = 0; i < cond.getArgs().size(); i++) { IfCondition arg = cond.getArgs().get(i); IfCondition simpl = simplify(arg); if (simpl != arg) { if (args == null) { args = new ArrayList<>(cond.getArgs()); } args.set(i, simpl); } } if (args != null) { // arguments was changed cond = new IfCondition(cond.getMode(), args); } if (cond.getMode() == Mode.NOT && cond.first().getMode() == Mode.NOT) { cond = invert(cond.first()); } if (cond.getMode() == Mode.TERNARY && cond.first().getMode() == Mode.NOT) { cond = invert(cond); } // for condition with a lot of negations => make invert if (cond.getMode() == Mode.OR || cond.getMode() == Mode.AND) { int count = cond.getArgs().size(); if (count > 1) { int negCount = 0; for (IfCondition arg : cond.getArgs()) { if (arg.getMode() == Mode.NOT || (arg.isCompare() && arg.getCompare().getOp() == IfOp.NE)) { negCount++; } } if (negCount > count / 2) { return not(invert(cond)); } } } return cond; } private static IfCondition simplifyCmpOp(Compare c) { if (!c.getA().isInsnWrap()) { return null; } if (!c.getB().isLiteral()) { return null; } long lit = ((LiteralArg) c.getB()).getLiteral(); if (lit != 0 && lit != 1) { return null; } InsnNode wrapInsn = ((InsnWrapArg) c.getA()).getWrapInsn(); switch (wrapInsn.getType()) { case CMP_L: case CMP_G: if (lit == 0) { IfNode insn = c.getInsn(); insn.changeCondition(insn.getOp(), wrapInsn.getArg(0), wrapInsn.getArg(1)); } break; case ARITH: if (c.getB().getType() == ArgType.BOOLEAN) { ArithOp arithOp = ((ArithNode) wrapInsn).getOp(); if (arithOp == ArithOp.OR || arithOp == ArithOp.AND) { IfOp ifOp = c.getInsn().getOp(); boolean isTrue = ifOp == IfOp.NE && lit == 0 || ifOp == IfOp.EQ && lit == 1; IfOp op = isTrue ? IfOp.NE : IfOp.EQ; Mode mode = isTrue && arithOp == ArithOp.OR || !isTrue && arithOp == ArithOp.AND ? Mode.OR : Mode.AND; IfNode if1 = new IfNode(op, -1, wrapInsn.getArg(0), LiteralArg.litFalse()); IfNode if2 = new IfNode(op, -1, wrapInsn.getArg(1), LiteralArg.litFalse()); return new IfCondition(mode, Arrays.asList(new IfCondition(new Compare(if1)), new IfCondition(new Compare(if2)))); } } break; default: break; } return null; } public List getRegisterArgs() { List list = new ArrayList<>(); if (mode == Mode.COMPARE) { compare.getInsn().getRegisterArgs(list); } else { for (IfCondition arg : args) { list.addAll(arg.getRegisterArgs()); } } return list; } public boolean replaceArg(InsnArg from, InsnArg to) { if (mode == Mode.COMPARE) { return compare.getInsn().replaceArg(from, to); } for (IfCondition arg : args) { if (arg.replaceArg(from, to)) { return true; } } return false; } public void visitInsns(Consumer visitor) { if (mode == Mode.COMPARE) { compare.getInsn().visitInsns(visitor); } else { args.forEach(arg -> arg.visitInsns(visitor)); } } public List collectInsns() { List list = new ArrayList<>(); visitInsns(list::add); return list; } public int getSourceLine() { for (InsnNode insn : collectInsns()) { int line = insn.getSourceLine(); if (line != 0) { return line; } } return 0; } @Nullable public InsnNode getFirstInsn() { if (mode == Mode.COMPARE) { return compare.getInsn(); } return args.get(0).getFirstInsn(); } @Override public String toString() { switch (mode) { case COMPARE: return compare.toString(); case TERNARY: return first() + " ? " + second() + " : " + third(); case NOT: return "!(" + first() + ')'; case AND: case OR: String op = mode == Mode.OR ? " || " : " && "; StringBuilder sb = new StringBuilder(); sb.append('('); for (Iterator it = args.iterator(); it.hasNext();) { IfCondition arg = it.next(); sb.append(arg); if (it.hasNext()) { sb.append(op); } } sb.append(')'); return sb.toString(); } return "??"; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof IfCondition)) { return false; } IfCondition other = (IfCondition) obj; if (mode != other.mode) { return false; } return Objects.equals(args, other.args) && Objects.equals(compare, other.compare); } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + mode.hashCode(); result = 31 * result + args.hashCode(); result = 31 * result + (compare != null ? compare.hashCode() : 0); return result; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfInfo.java ================================================ package jadx.core.dex.regions.conditions; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.blocks.BlockSet; public final class IfInfo { private final MethodNode mth; private final IfCondition condition; private final BlockSet mergedBlocks; private final BlockNode thenBlock; private final BlockNode elseBlock; private final Set skipBlocks; private final List forceInlineInsns; private BlockNode outBlock; public IfInfo(MethodNode mth, IfCondition condition, BlockNode thenBlock, BlockNode elseBlock) { this(mth, condition, thenBlock, elseBlock, BlockSet.empty(mth), new HashSet<>(), new ArrayList<>()); } public IfInfo(IfInfo info, BlockNode thenBlock, BlockNode elseBlock) { this(info.getMth(), info.getCondition(), thenBlock, elseBlock, info.getMergedBlocks(), info.getSkipBlocks(), info.getForceInlineInsns()); } private IfInfo(MethodNode mth, IfCondition condition, BlockNode thenBlock, BlockNode elseBlock, BlockSet mergedBlocks, Set skipBlocks, List forceInlineInsns) { this.mth = mth; this.condition = condition; this.thenBlock = thenBlock; this.elseBlock = elseBlock; this.mergedBlocks = mergedBlocks; this.skipBlocks = skipBlocks; this.forceInlineInsns = forceInlineInsns; } public static IfInfo invert(IfInfo info) { return new IfInfo(info.getMth(), IfCondition.invert(info.getCondition()), info.getElseBlock(), info.getThenBlock(), info.getMergedBlocks(), info.getSkipBlocks(), info.getForceInlineInsns()); } public void merge(IfInfo... arr) { for (IfInfo info : arr) { mergedBlocks.addAll(info.getMergedBlocks()); skipBlocks.addAll(info.getSkipBlocks()); addInsnsForForcedInline(info.getForceInlineInsns()); } } @Deprecated public BlockNode getFirstIfBlock() { return mergedBlocks.getFirst(); } public BlockSet getMergedBlocks() { return mergedBlocks; } public MethodNode getMth() { return mth; } public IfCondition getCondition() { return condition; } public Set getSkipBlocks() { return skipBlocks; } public BlockNode getThenBlock() { return thenBlock; } public BlockNode getElseBlock() { return elseBlock; } public BlockNode getOutBlock() { return outBlock; } public void setOutBlock(BlockNode outBlock) { this.outBlock = outBlock; } public List getForceInlineInsns() { return forceInlineInsns; } public void resetForceInlineInsns() { forceInlineInsns.clear(); } public void addInsnsForForcedInline(List insns) { forceInlineInsns.addAll(insns); } @Override public String toString() { return "IfInfo: then: " + thenBlock + ", else: " + elseBlock; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfRegion.java ================================================ package jadx.core.dex.regions.conditions; import java.util.ArrayList; import java.util.Collections; import java.util.List; import jadx.api.ICodeWriter; import jadx.core.codegen.RegionGen; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBranchRegion; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.utils.exceptions.CodegenException; public final class IfRegion extends ConditionRegion implements IBranchRegion { private IContainer thenRegion; private IContainer elseRegion; public IfRegion(IRegion parent) { super(parent); } public IContainer getThenRegion() { return thenRegion; } public void setThenRegion(IContainer thenRegion) { this.thenRegion = thenRegion; } public IContainer getElseRegion() { return elseRegion; } public void setElseRegion(IContainer elseRegion) { this.elseRegion = elseRegion; } public void invert() { invertCondition(); // swap regions IContainer tmp = thenRegion; thenRegion = elseRegion; elseRegion = tmp; } public int getSourceLine() { return getConditionSourceLine(); } @Override public List getSubBlocks() { List conditionBlocks = getConditionBlocks(); List all = new ArrayList<>(conditionBlocks.size() + 2); all.addAll(conditionBlocks); if (thenRegion != null) { all.add(thenRegion); } if (elseRegion != null) { all.add(elseRegion); } return Collections.unmodifiableList(all); } @Override public List getBranches() { List branches = new ArrayList<>(2); branches.add(thenRegion); branches.add(elseRegion); return Collections.unmodifiableList(branches); } @Override public boolean replaceSubBlock(IContainer oldBlock, IContainer newBlock) { if (oldBlock == thenRegion) { thenRegion = newBlock; updateParent(thenRegion, this); return true; } if (oldBlock == elseRegion) { elseRegion = newBlock; updateParent(elseRegion, this); return true; } return false; } @Override public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException { regionGen.makeIf(this, code, true); } @Override public String baseString() { StringBuilder sb = new StringBuilder(); if (thenRegion != null) { sb.append(thenRegion.baseString()); } if (elseRegion != null) { sb.append(elseRegion.baseString()); } return sb.toString(); } @Override public String toString() { return "IF " + getConditionBlocks() + " THEN: " + thenRegion + " ELSE: " + elseRegion; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/loops/ForEachLoop.java ================================================ package jadx.core.dex.regions.loops; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; public final class ForEachLoop extends LoopType { private final InsnNode varArgInsn; private final InsnNode iterableArgInsn; public ForEachLoop(RegisterArg varArg, InsnArg iterableArg) { // store for-each args in fake instructions to // save code semantics and allow args manipulations like args inlining varArgInsn = new InsnNode(InsnType.REGION_ARG, 0); varArgInsn.add(AFlag.DONT_INLINE); varArgInsn.setResult(varArg.duplicate()); iterableArgInsn = new InsnNode(InsnType.REGION_ARG, 1); iterableArgInsn.add(AFlag.DONT_INLINE); iterableArgInsn.addArg(iterableArg.duplicate()); // will be declared at codegen getVarArg().getSVar().getCodeVar().setDeclared(true); } public void injectFakeInsns(LoopRegion loopRegion) { loopRegion.getInfo().getPreHeader().getInstructions().add(iterableArgInsn); loopRegion.getHeader().getInstructions().add(0, varArgInsn); } public RegisterArg getVarArg() { return varArgInsn.getResult(); } public InsnArg getIterableArg() { return iterableArgInsn.getArg(0); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/loops/ForLoop.java ================================================ package jadx.core.dex.regions.loops; import jadx.core.dex.nodes.InsnNode; public final class ForLoop extends LoopType { private final InsnNode initInsn; private final InsnNode incrInsn; public ForLoop(InsnNode initInsn, InsnNode incrInsn) { this.initInsn = initInsn; this.incrInsn = incrInsn; } public InsnNode getInitInsn() { return initInsn; } public InsnNode getIncrInsn() { return incrInsn; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/loops/LoopRegion.java ================================================ package jadx.core.dex.regions.loops; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeWriter; import jadx.core.codegen.RegionGen; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.regions.conditions.ConditionRegion; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnUtils; import jadx.core.utils.exceptions.CodegenException; public final class LoopRegion extends ConditionRegion { private final LoopInfo info; private final boolean conditionAtEnd; private final @Nullable BlockNode header; // instruction which must be executed before condition in every loop private @Nullable BlockNode preCondition; private IRegion body; private LoopType type; public LoopRegion(IRegion parent, LoopInfo info, @Nullable BlockNode header, boolean reversed) { super(parent); this.info = info; this.header = header; this.conditionAtEnd = reversed; if (header != null) { updateCondition(header); } } public LoopInfo getInfo() { return info; } @Nullable public BlockNode getHeader() { return header; } public boolean isEndless() { return header == null; } public IRegion getBody() { return body; } public void setBody(IRegion body) { this.body = body; } public boolean isConditionAtEnd() { return conditionAtEnd; } /** * Set instructions which must be executed before condition in every loop */ public void setPreCondition(BlockNode preCondition) { this.preCondition = preCondition; } /** * Check if pre-conditions can be inlined into loop condition */ public boolean checkPreCondition() { List insns = preCondition.getInstructions(); if (insns.isEmpty()) { return true; } IfCondition condition = getCondition(); if (condition == null) { return false; } List conditionArgs = condition.getRegisterArgs(); if (conditionArgs.isEmpty()) { return false; } int size = insns.size(); for (int i = 0; i < size; i++) { InsnNode insn = insns.get(i); if (insn.getResult() == null) { return false; } RegisterArg res = insn.getResult(); if (res.getSVar().getUseCount() > 1) { return false; } boolean found = false; // search result arg in other insns for (int j = i + 1; j < size; j++) { if (insns.get(i).containsVar(res)) { found = true; } } // or in if insn if (!found && InsnUtils.containsVar(conditionArgs, res)) { found = true; } if (!found) { return false; } } return true; } /** * Move all preCondition block instructions before conditionBlock instructions */ public void mergePreCondition() { if (preCondition != null && header != null) { List condInsns = header.getInstructions(); List preCondInsns = preCondition.getInstructions(); preCondInsns.addAll(condInsns); condInsns.clear(); condInsns.addAll(preCondInsns); header.add(AFlag.ALLOW_MULTIPLE_INSNS_LOOP_COND); preCondInsns.clear(); preCondition = null; } } public int getSourceLine() { InsnNode lastInsn = BlockUtils.getLastInsn(header); int headerLine = lastInsn == null ? 0 : lastInsn.getSourceLine(); if (headerLine != 0) { return headerLine; } return getConditionSourceLine(); } public LoopType getType() { return type; } public void setType(LoopType type) { this.type = type; } @Override public List getSubBlocks() { List all = new ArrayList<>(2 + getConditionBlocks().size()); if (preCondition != null) { all.add(preCondition); } all.addAll(getConditionBlocks()); if (body != null) { all.add(body); } return all; } @Override public boolean replaceSubBlock(IContainer oldBlock, IContainer newBlock) { return false; } @Override public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException { regionGen.makeLoop(this, code); } @Override public String baseString() { return body == null ? "-" : body.baseString(); } @Override public String toString() { return "LOOP:" + info.getId() + ": " + baseString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/regions/loops/LoopType.java ================================================ package jadx.core.dex.regions.loops; public abstract class LoopType { } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/trycatch/CatchAttr.java ================================================ package jadx.core.dex.trycatch; import java.util.Comparator; import java.util.List; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.utils.Utils; public class CatchAttr implements IJadxAttribute { public static CatchAttr build(List handlers) { handlers.sort(Comparator.comparingInt(ExceptionHandler::getHandlerOffset)); return new CatchAttr(handlers); } private final List handlers; private CatchAttr(List handlers) { this.handlers = handlers; } public List getHandlers() { return handlers; } @Override public AType getAttrType() { return AType.EXC_CATCH; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof CatchAttr)) { return false; } CatchAttr catchAttr = (CatchAttr) o; return getHandlers().equals(catchAttr.getHandlers()); } @Override public int hashCode() { return getHandlers().hashCode(); } @Override public String toString() { return "Catch: " + Utils.listToString(getHandlers()); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/trycatch/ExcHandlerAttr.java ================================================ package jadx.core.dex.trycatch; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; public class ExcHandlerAttr implements IJadxAttribute { private final ExceptionHandler handler; public ExcHandlerAttr(ExceptionHandler handler) { this.handler = handler; } @Override public AType getAttrType() { return AType.EXC_HANDLER; } public TryCatchBlockAttr getTryBlock() { return handler.getTryBlock(); } public ExceptionHandler getHandler() { return handler; } @Override public String toString() { return "ExcHandler: " + handler; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/trycatch/ExceptionHandler.java ================================================ package jadx.core.dex.trycatch; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.InsnUtils; import jadx.core.utils.Utils; public class ExceptionHandler { private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandler.class); private final List catchTypes = new ArrayList<>(1); private final int handlerOffset; private BlockNode handlerBlock; private final List blocks = new ArrayList<>(); private IContainer handlerRegion; private InsnArg arg; private TryCatchBlockAttr tryBlock; private boolean isFinally; private boolean removed = false; public static ExceptionHandler build(MethodNode mth, int addr, @Nullable ClassInfo type) { ExceptionHandler eh = new ExceptionHandler(addr); eh.addCatchType(mth, type); return eh; } private ExceptionHandler(int addr) { this.handlerOffset = addr; } /** * Add exception type to catch block * * @param type - null for 'all' or 'Throwable' handler */ public boolean addCatchType(MethodNode mth, @Nullable ClassInfo type) { if (type != null) { if (catchTypes.contains(type)) { return false; } return catchTypes.add(type); } if (!this.catchTypes.isEmpty()) { mth.addDebugComment("Throwable added to exception handler: '" + catchTypeStr() + "', keep only Throwable"); catchTypes.clear(); return true; } return false; } public void addCatchTypes(MethodNode mth, Collection types) { for (ClassInfo type : types) { addCatchType(mth, type); } } public List getCatchTypes() { return catchTypes; } public ArgType getArgType() { if (isCatchAll()) { return ArgType.THROWABLE; } List types = getCatchTypes(); if (types.size() == 1) { return types.iterator().next().getType(); } else { return ArgType.THROWABLE; } } public boolean isCatchAll() { if (catchTypes.isEmpty()) { return true; } for (ClassInfo classInfo : catchTypes) { if (classInfo.getFullName().equals(Consts.CLASS_THROWABLE)) { return true; } } return false; } public int getHandlerOffset() { return handlerOffset; } public BlockNode getHandlerBlock() { return handlerBlock; } public void setHandlerBlock(BlockNode handlerBlock) { this.handlerBlock = handlerBlock; } public List getBlocks() { return blocks; } public void addBlock(BlockNode node) { blocks.add(node); } public IContainer getHandlerRegion() { return handlerRegion; } public void setHandlerRegion(IContainer handlerRegion) { this.handlerRegion = handlerRegion; } public InsnArg getArg() { return arg; } public void setArg(InsnArg arg) { this.arg = arg; } public void setTryBlock(TryCatchBlockAttr tryBlock) { this.tryBlock = tryBlock; } public TryCatchBlockAttr getTryBlock() { return tryBlock; } public boolean isFinally() { return isFinally; } public void setFinally(boolean isFinally) { this.isFinally = isFinally; } public boolean isRemoved() { return removed; } @Nullable public BlockNode getBottomSplitter() { TryCatchBlockAttr handlerTryBlock = getTryBlock(); // TODO: Implement support for finding bottom splitter of catch with inner tries if (handlerTryBlock.getInnerTryBlocks().size() > 1) { LOG.warn("No support yet for finding bottom block of try body with multipe inner trys"); return null; } final TryCatchBlockAttr searchForTryBody; if (handlerTryBlock.getInnerTryBlocks().isEmpty()) { searchForTryBody = handlerTryBlock; } else { searchForTryBody = Utils.getOne(handlerTryBlock.getInnerTryBlocks()); } BlockNode splitter = null; for (BlockNode handlerPredecessor : getHandlerBlock().getPredecessors()) { if (!handlerPredecessor.contains(AFlag.EXC_BOTTOM_SPLITTER)) { continue; } for (BlockNode splitterPredecessor : handlerPredecessor.getPredecessors()) { TryCatchBlockAttr tryBody = splitterPredecessor.get(AType.TRY_BLOCK); if (tryBody == searchForTryBody) { splitter = handlerPredecessor; break; } } if (splitter != null) { break; } } return splitter; } public void markForRemove() { this.removed = true; this.blocks.forEach(b -> b.add(AFlag.REMOVE)); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ExceptionHandler that = (ExceptionHandler) o; return handlerOffset == that.handlerOffset && catchTypes.equals(that.catchTypes) && Objects.equals(tryBlock, that.tryBlock); } @Override public int hashCode() { return Objects.hash(catchTypes, handlerOffset /* , tryBlock */); } public String catchTypeStr() { return catchTypes.isEmpty() ? "all" : Utils.listToString(catchTypes, " | ", ClassInfo::getShortName); } @Override public String toString() { return catchTypeStr() + " -> " + InsnUtils.formatOffset(handlerOffset); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlockAttr.java ================================================ package jadx.core.dex.trycatch; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Stream; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.Edge; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.BlockUtils; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; public class TryCatchBlockAttr implements IJadxAttribute { public static boolean isImplicitOrMerged(TryCatchBlockAttr tryBlock) { return tryBlock.isMerged() || tryBlock.getHandlers().isEmpty(); } private final int id; private final List handlers; private List blocks; private TryCatchBlockAttr outerTryBlock; private List innerTryBlocks = Collections.emptyList(); private boolean merged = false; private BlockNode topSplitter; public TryCatchBlockAttr(int id, List handlers, List blocks) { this.id = id; this.handlers = handlers; this.blocks = blocks; handlers.forEach(h -> h.setTryBlock(this)); } public boolean isAllHandler() { return handlers.size() == 1 && handlers.get(0).isCatchAll(); } public boolean isThrowOnly() { boolean throwFound = false; for (BlockNode block : blocks) { List insns = block.getInstructions(); if (insns.size() != 1) { return false; } InsnNode insn = insns.get(0); switch (insn.getType()) { case MOVE_EXCEPTION: case MONITOR_EXIT: // allowed instructions break; case THROW: throwFound = true; break; default: return false; } } return throwFound; } public int getId() { return id; } public List getHandlers() { return handlers; } public int getHandlersCount() { return handlers.size(); } public List getBlocks() { return blocks; } public void setBlocks(List blocks) { this.blocks = blocks; } public void clear() { blocks.clear(); handlers.forEach(ExceptionHandler::markForRemove); handlers.clear(); } public void removeBlock(BlockNode block) { blocks.remove(block); } public void removeHandler(ExceptionHandler handler) { handlers.remove(handler); handler.markForRemove(); } public List getInnerTryBlocks() { return innerTryBlocks; } public void addInnerTryBlock(TryCatchBlockAttr inner) { if (this.innerTryBlocks.isEmpty()) { this.innerTryBlocks = new ArrayList<>(); } this.innerTryBlocks.add(inner); } public TryCatchBlockAttr getOuterTryBlock() { return outerTryBlock; } public void setOuterTryBlock(TryCatchBlockAttr outerTryBlock) { this.outerTryBlock = outerTryBlock; } public BlockNode getTopSplitter() { return topSplitter; } public void setTopSplitter(BlockNode topSplitter) { this.topSplitter = topSplitter; } public boolean isMerged() { return merged; } public void setMerged(boolean merged) { this.merged = merged; } public int id() { return id; } public List getHandlerTryEdges() { List mergedHandlers = getMergedHandlers(); List edges = new ArrayList<>(mergedHandlers.size()); for (ExceptionHandler handler : mergedHandlers) { BlockNode handlerBlock = handler.getHandlerBlock(); BlockNode handlerSplitter = handler.getBottomSplitter(); if (handlerSplitter == null) { // If we cannot find a bottom splitter, there might be none. In this case, assume that the top // splitter of this try catch is the source of the exit. List allChildren = ListUtils.filter(handlerBlock.getPredecessors(), blk -> getBlocks().contains(blk)); handlerSplitter = BlockUtils.getBottomBlock(allChildren); if (handlerSplitter == null) { handlerSplitter = getTopSplitter(); } } TryEdge edge = new TryEdge(handlerSplitter, handlerBlock, handler); edges.add(edge); } return edges; } public List getFallthroughTryEdges() { List edges = new LinkedList<>(); List exploredBlocks = new ArrayList<>(); List exploredTrys = new LinkedList<>(); getFallthroughTryEdges(edges, exploredBlocks, exploredTrys); return edges; } public void getFallthroughTryEdges(List edges, List exploredBlocks, List exploredTrys) { List mergedHandlers = getMergedHandlers(); Set searchBlocks = new HashSet<>(); searchBlocks.addAll(getBlocks()); for (ExceptionHandler handler : mergedHandlers) { searchBlocks.removeAll(handler.getBlocks()); } BlockNode sourceBlock = BlockUtils.getTopBlock(new ArrayList<>(searchBlocks)); exploredTrys.add(this); exploreTryPath(edges, sourceBlock, searchBlocks, exploredBlocks, exploredTrys); } public List getTryEdges() { List handlerEdges = getHandlerTryEdges(); List fallthroughEdges = getFallthroughTryEdges(); List edges = new ArrayList<>(handlerEdges.size() + fallthroughEdges.size()); edges.addAll(handlerEdges); edges.addAll(fallthroughEdges); return Collections.unmodifiableList(edges); } private void exploreTryPath(List edges, BlockNode blk, Set searchBlocks, List exploredBlocks, List exploredTrys) { for (BlockNode successor : blk.getSuccessors()) { // If a separate branch has already explored this block, we don't need to recalculate its exits. if (exploredBlocks.contains(successor)) { continue; } // If this is a bottom splitter, ignore - we only care about non-handler edges. if (successor.contains(AFlag.EXC_BOTTOM_SPLITTER)) { continue; } exploredBlocks.add(successor); if (successor.contains(AFlag.LOOP_END)) { final var loopsAttrList = successor.get(AType.LOOP); final List loops = loopsAttrList.getList(); final List loopStartBlocks = new LinkedList<>(); for (final LoopInfo loop : loops) { loopStartBlocks.add(loop.getStart()); final List loopEdges = loop.getExitEdges(); for (final Edge loopEdge : loopEdges) { if (loopEdge.getTarget() == successor) { loopStartBlocks.add(loopEdge.getSource()); } } } final boolean includesAllLoopStart = ListUtils.allMatch(loopStartBlocks, exploredBlocks::contains); if (!includesAllLoopStart) { edges.add(new TryEdge(blk, successor, TryEdgeType.LOOP_EXIT)); continue; } } boolean isPathToAnySearchBlock = false; for (final BlockNode searchBlock : searchBlocks) { if (BlockUtils.isPathExists(successor, searchBlock)) { isPathToAnySearchBlock = true; break; } } if (!searchBlocks.contains(successor) && !isPathToAnySearchBlock) { // This block is not contained within this try's block list. This can either be since it is an exit // to the try or it is a block which leads to an exit (for example, an exception handler). // If this block (successor) leads to an exit, then the "bottom block" of all try blocks and this // block will be // equal to the bottom block of all try blocks. If this block is an exit, then either: // - a path does not exist from all try blocks to this block, thus making the bottom block null. // - a path does exist from all try blocks to this block but no more try blocks follow, thus making // the bottom block this block. List allBlocksWithCurrent = new ArrayList<>(getBlocks().size() + 1); allBlocksWithCurrent.addAll(getBlocks()); allBlocksWithCurrent.add(successor); BlockNode bottomBlock = BlockUtils.getBottomBlock(allBlocksWithCurrent); if (!(bottomBlock == null || bottomBlock == successor)) { // This block leads to an exit. exploreTryPath(edges, successor, searchBlocks, exploredBlocks, exploredTrys); continue; } BlockNode emptyPathEndOfSuccessor = BlockUtils.followEmptyPath(successor, false, false); if (emptyPathEndOfSuccessor.contains(AFlag.EXC_TOP_SPLITTER)) { // This block is an exit which enters another try catch. In this case, the next try catch is within // the same scope. Thus, we will take all of the edges out of that try and add them to the list of // edges of this try. Set nestedTrys = new HashSet<>(); List allSuccessorsOnTryBody = ListUtils.filter(emptyPathEndOfSuccessor.getSuccessors(), potentialTryBlock -> potentialTryBlock.contains(AFlag.TRY_ENTER)); for (BlockNode tryBodyEnter : allSuccessorsOnTryBody) { TryCatchBlockAttr nestedTry = tryBodyEnter.get(AType.TRY_BLOCK); if (nestedTry == null) { continue; } // If we have already added a try's edges, skip over it to avoid infinite recursion. if (exploredTrys.contains(nestedTry)) { continue; } // Unsure of why these top splitters have to be the same for them to be "nested" trys, but this // seems to work (?) if (nestedTry.getTopSplitter() != getTopSplitter()) { continue; } nestedTrys.add(nestedTry); } // Only will we attempt to add nested inners if there exists any. If none exist, perform normal // handling of the edge. if (!nestedTrys.isEmpty()) { for (TryCatchBlockAttr nestedTry : nestedTrys) { nestedTry.getFallthroughTryEdges(edges, exploredBlocks, exploredTrys); } continue; } } if (bottomBlock == null) { // This block is an exit which occurs before all try blocks are logically executed. edges.add(new TryEdge(blk, successor, TryEdgeType.PREMATURE_EXIT)); } else if (bottomBlock == successor) { // This block is an exit which occurs after all try blocks are logically executed. edges.add(new TryEdge(blk, successor, TryEdgeType.TRUE_FALLTHROUGH)); } else { // All possible cases should have been caught by the above if / else and the preceeding if. // If this is hit, any changes made to this algorithm must aptly handle all possible code paths // before executing this. throw new JadxRuntimeException( "Unexpected code execution branch taken during try edge resolution: blk=" + blk + ",successor=" + successor); } } else { exploreTryPath(edges, successor, searchBlocks, exploredBlocks, exploredTrys); } } } public List getMergedHandlers() { boolean hasInnerBlocks = !getInnerTryBlocks().isEmpty(); final List mergedHandlers; if (hasInnerBlocks) { // collect handlers from this and all inner blocks // (intentionally not using recursive collect for now) mergedHandlers = new ArrayList<>(getHandlers()); for (TryCatchBlockAttr innerTryBlock : getInnerTryBlocks()) { mergedHandlers.addAll(innerTryBlock.getHandlers()); } } else { mergedHandlers = getHandlers(); } return Collections.unmodifiableList(mergedHandlers); } public Map getEdgeBlockMap(MethodNode mth) { List edges = getTryEdges(); Map blockMap = new HashMap<>(); for (TryEdge edge : edges) { blockMap.put(edge, edge.getTarget()); } return blockMap; } public TryEdgeScopeGroupMap getExecutionScopeGroups(MethodNode mth) { Map handlerBlocks = getEdgeBlockMap(mth); TryEdgeScopeGroupMap scopeGroups = new TryEdgeScopeGroupMap(mth, this, handlerBlocks.size()); scopeGroups.populateFromEdges(handlerBlocks); return scopeGroups; } public Map> getHandlerFallthroughGroups(MethodNode mth, TryEdgeScopeGroupMap scopeGroups) { return scopeGroups.getScopeEnds(mth); } public List getSearchBlocksFromFallthroughGroups(MethodNode mth, ExceptionHandler finallyHandler, Map> fallthroughGroups) { List searchBlocks = new LinkedList<>(); for (Map.Entry> entry : fallthroughGroups.entrySet()) { BlockNode scopeEndBlock = entry.getKey(); List sourceHandlers = entry.getValue(); for (BlockNode scopeEndPredecessor : scopeEndBlock.getPredecessors()) { // Add all predecessors to the scope end which are connected to some handler's scope start try (Stream stream = sourceHandlers.stream()) { Object[] matchedHandlerPaths = stream.filter(handler -> !(handler.isHandlerExit() && handler.getExceptionHandler() == finallyHandler)) .map(handler -> handler.getTarget()) .filter(scopeStart -> BlockUtils.isPathExists(scopeStart, scopeEndPredecessor)) .toArray(); if (matchedHandlerPaths.length != 0) { searchBlocks.add(scopeEndPredecessor); } } } } return searchBlocks; } @Override public IJadxAttrType getAttrType() { return AType.TRY_BLOCK; } @Override public int hashCode() { return handlers.hashCode() + 31 * blocks.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } TryCatchBlockAttr other = (TryCatchBlockAttr) obj; return id == other.id && handlers.equals(other.handlers) && blocks.equals(other.blocks); } @Override public String toString() { if (merged) { return "Merged into " + outerTryBlock; } StringBuilder sb = new StringBuilder(); sb.append("TryCatch #").append(id).append(" {").append(Utils.listToString(handlers)); sb.append(", blocks: (").append(Utils.listToString(blocks)).append(')'); if (topSplitter != null) { sb.append(", top: ").append(topSplitter); } if (outerTryBlock != null) { sb.append(", outer: #").append(outerTryBlock.id); } if (!innerTryBlocks.isEmpty()) { sb.append(", inners: ").append(Utils.listToString(innerTryBlocks, inner -> "#" + inner.id)); } sb.append(" }"); return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdge.java ================================================ package jadx.core.dex.trycatch; import java.util.Objects; import java.util.Optional; import org.jetbrains.annotations.NotNull; import jadx.core.dex.nodes.BlockNode; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Represents an edge between two blocks representing an exit out of a try body. * The source block will be within the try body. */ public final class TryEdge { private final BlockNode source; private final BlockNode target; private final Optional handler; private final TryEdgeType type; public TryEdge(final BlockNode source, final BlockNode target, final TryEdgeType type) { this(source, target, type, Optional.empty()); } public TryEdge(final BlockNode source, final BlockNode target, final @NotNull ExceptionHandler handler) { this(source, target, TryEdgeType.HANDLER, Optional.of(handler)); } public TryEdge(final BlockNode source, final BlockNode target, final TryEdgeType type, final Optional handler) { this.source = source; this.target = target; this.handler = handler; this.type = type; if (isHandlerExit() && handler.isEmpty()) { throw new JadxRuntimeException("Attempted to add a null exception handler as an edge of \"" + type.toString() + "\" type"); } else if (isNotHandlerExit() && handler.isPresent()) { throw new JadxRuntimeException("Attempted to add an exception handler as an edge of \"" + type.toString() + "\" type"); } } @Override public final String toString() { StringBuilder sb = new StringBuilder("TryEdge: ["); sb.append(type); sb.append(' '); sb.append(source.toString()); sb.append(" -> "); sb.append(target.toString()); sb.append("] - Handler: "); if (handler.isEmpty()) { sb.append("None"); } else { sb.append(handler.get().toString()); } return sb.toString(); } @Override public final boolean equals(Object obj) { if (!(obj instanceof TryEdge)) { return false; } final TryEdge other = (TryEdge) obj; return source.equals(other.source) && target.equals(other.target) && handler.equals(other.handler) && type.equals(other.type); } @Override public final int hashCode() { return Objects.hash(source, target, type, handler); } public final BlockNode getSource() { return source; } public final BlockNode getTarget() { return target; } public final TryEdgeType getType() { return type; } public final boolean isHandlerExit() { return type == TryEdgeType.HANDLER; } public final boolean isNotHandlerExit() { return !isHandlerExit(); } public final ExceptionHandler getExceptionHandler() { if (!isHandlerExit()) { throw new JadxRuntimeException("Attempted to get the exception handler of a non-handler edge type"); } if (handler.isEmpty()) { throw new JadxRuntimeException("Attempted to get the exception handler of a handler edge type, however none was present"); } return handler.get(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdgeScopeGroupMap.java ================================================ package jadx.core.dex.trycatch; import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.BlockUtils; import jadx.core.utils.Pair; import jadx.core.utils.exceptions.JadxRuntimeException; /** * A map which stores the information of how try edges correlate with each other. * K is a try edge and V contains all other try edges whose who share the same logical scope. */ public final class TryEdgeScopeGroupMap implements Map> { private static final class TryEdgeScope { private final TryEdge edge; private final BlockNode block; public TryEdgeScope(final TryEdge edge, final BlockNode block) { this.edge = edge; this.block = block; } } private final List> mergedEdges = new ArrayList<>(); private final TryCatchBlockAttr tryCatch; private final Map> underlyingMap; public TryEdgeScopeGroupMap(final MethodNode mth, final TryCatchBlockAttr tryCatch, final int initialCapacity) { this.tryCatch = tryCatch; underlyingMap = new HashMap<>(initialCapacity); } @Override public final void clear() { underlyingMap.clear(); } @Override public final boolean containsKey(Object key) { return underlyingMap.containsKey(key); } @Override public final boolean containsValue(Object value) { if (!(value instanceof TryEdge)) { return false; } final TryEdge edge = (TryEdge) value; return underlyingMap.containsKey(edge); } @Override public final Set>> entrySet() { return underlyingMap.entrySet(); } @Override public final Map get(Object key) { return underlyingMap.get(key); } @Override public final boolean isEmpty() { return underlyingMap.isEmpty(); } @Override public final Set keySet() { return underlyingMap.keySet(); } @Override public final Map put(TryEdge key, Map value) { return underlyingMap.put(key, value); } @Override public final void putAll(Map> otherMap) { underlyingMap.putAll(otherMap); } @Override public final Map remove(Object key) { return underlyingMap.remove(key); } @Override public final int size() { return underlyingMap.size(); } @Override public final Collection> values() { return underlyingMap.values(); } public final boolean hasMergedEdges() { return !mergedEdges.isEmpty(); } public final List> getMergedScopes() { return mergedEdges; } public final void populateFromEdges(final Map edges) { mergeSameScopes(edges); for (final TryEdge edge : edges.keySet()) { final BlockNode edgeBlock = edges.get(edge); final Map handlerFallthroughMap = createEdgeTerminusMap(edges, edge, edgeBlock); put(edge, handlerFallthroughMap); } } /** * Returns a map of all points where edges meet with each other, dictating the end of that * edge's scope. * * @param mth * @return */ public Map> getScopeEnds(final MethodNode mth) { final Map> groups = new HashMap<>(); // A list containing pairs of edges where there are no shared common clean successors between the // two handlers. This usually indicates that these edge pairs must be processed differently. final List isolatedEdgePairs = new LinkedList<>(); for (final TryEdge mergeEdgeA : keySet()) { final Pair edgeMergedPair = getMergedNodeFromEdge(mergeEdgeA); if (edgeMergedPair != null) { continue; } final Map handlerRelations = get(mergeEdgeA); final List scopeEnds = new ArrayList<>(handlerRelations.size()); for (final TryEdge mergeEdgeB : handlerRelations.keySet()) { final Pair mergedPairFromRelation = getMergedNodeFromEdge(mergeEdgeB); if (mergedPairFromRelation != null && mergedPairFromRelation.getFirst() == mergeEdgeA) { continue; } final BlockNode sharedTerminator = handlerRelations.get(mergeEdgeB); if (sharedTerminator == null) { // There are no common clean succesors between the two handlers. isolatedEdgePairs.add(mergeEdgeB); } else { scopeEnds.add(sharedTerminator); } } if (scopeEnds.isEmpty()) { // Isolated edge pairs found - we will deal with them later continue; } final BlockNode topGrouping = BlockUtils.getTopBlock(scopeEnds); if (groups.containsKey(topGrouping)) { groups.get(topGrouping).add(mergeEdgeA); } else { final List groupingHandlers = new LinkedList<>(); groupingHandlers.add(mergeEdgeA); groups.put(topGrouping, groupingHandlers); } } for (final TryEdge isolatedEdge : isolatedEdgePairs) { boolean isInList = false; for (final List foundEdges : groups.values()) { if (foundEdges.contains(isolatedEdge)) { isInList = true; break; } } if (isInList) { // The isolated edge is not isolated with another handler - we can ignore this edge. break; } // If an isolated edge has not been added to the groupings, we will add it now. // This will be added by locating the point where the search for a common successor stops. // Since a common successor of all blocks which do have some clean path can be found in the method // exit node, the mentioned point will be the farthest successor of the edge target which has no // clean successors. final BlockNode target = isolatedEdge.getTarget(); final List successorBlocks = BlockUtils.collectAllSuccessors(mth, target, true); final BlockNode cleanSuccessorEnd = BlockUtils.getBottomBlock(successorBlocks); if (cleanSuccessorEnd == null) { throw new JadxRuntimeException("Could not find bottom clean successor for isolated try edge"); } final List scopeTerminusList; if (groups.containsKey(cleanSuccessorEnd)) { scopeTerminusList = groups.get(cleanSuccessorEnd); } else { scopeTerminusList = new LinkedList<>(); groups.put(cleanSuccessorEnd, scopeTerminusList); } scopeTerminusList.add(isolatedEdge); } if (groups.size() == 1) { for (final Pair pair : mergedEdges) { final TryEdge keptEdge = pair.getFirst(); final TryEdge removedEdge = pair.getSecond(); if (keptEdge.isHandlerExit() && !tryCatch.getHandlers().contains(keptEdge.getExceptionHandler())) { continue; } if (removedEdge.isHandlerExit() && !tryCatch.getHandlers().contains(removedEdge.getExceptionHandler())) { continue; } // If both handlers are not handler exits, we can assume that the code paths merge at some Phi node // which begins the finally duplicated code. if (keptEdge.isNotHandlerExit() && removedEdge.isNotHandlerExit()) { continue; } for (final List edgesWithTerminus : groups.values()) { if (edgesWithTerminus.contains(keptEdge)) { edgesWithTerminus.remove(keptEdge); } } final BlockNode terminus = get(keptEdge).get(removedEdge); final List terminusEdges; if (!groups.containsKey(terminus)) { terminusEdges = new LinkedList<>(); terminusEdges.add(keptEdge); groups.put(terminus, terminusEdges); } else { terminusEdges = groups.get(terminus); } terminusEdges.add(removedEdge); } } return groups; } @Nullable private Pair getMergedNodeFromEdge(final TryEdge edge) { for (Pair pair : mergedEdges) { if (pair.getSecond() == edge) { return pair; } } return null; } private Map createEdgeTerminusMap(final Map edgeStartMap, final TryEdge edge, final BlockNode edgeStart) { final Map scopeRelations = new HashMap<>(edgeStartMap.size() - 1); for (final TryEdge otherEdge : edgeStartMap.keySet()) { if (edge == otherEdge) { continue; } final BlockNode otherEdgeStart = edgeStartMap.get(otherEdge); final boolean eitherEdgeIsHandler = edge.isHandlerExit() || otherEdge.isHandlerExit(); if (otherEdgeStart == edgeStart && eitherEdgeIsHandler) { continue; } if (otherEdgeStart.isMthExitBlock()) { scopeRelations.put(otherEdge, otherEdgeStart); // Everything leads to the exit node so merged edges are no longer needed mergedEdges.clear(); continue; } if (edgeStart.isMthExitBlock()) { scopeRelations.put(otherEdge, edgeStart); // Everything leads to the exit node so merged edges are no longer needed mergedEdges.clear(); continue; } final BitSet sharedPostDominators = (BitSet) edgeStart.getPostDoms().clone(); final BitSet otherPostDoms = otherEdgeStart.getPostDoms(); if (sharedPostDominators.isEmpty() || otherPostDoms.isEmpty()) { continue; } sharedPostDominators.and(otherPostDoms); final List postDomHandler = new LinkedList<>(); BlockNode currentBlock = edgeStart; while (currentBlock != null) { postDomHandler.add(currentBlock); currentBlock = currentBlock.getIPostDom(); } BlockNode commonPostDom = null; currentBlock = otherEdgeStart; while (currentBlock != null) { if (postDomHandler.contains(currentBlock)) { commonPostDom = currentBlock; break; } currentBlock = currentBlock.getIPostDom(); } final BlockNode scopeEnd = commonPostDom; scopeRelations.put(otherEdge, scopeEnd); } return scopeRelations; } /** * If two scopes ever merge, as in if one edge leads to the same execution point as the target of * another edge, this function will record it. * * @param handlers * @return */ private Map mergeSameScopes(final Map handlers) { final List> exceptionHandlers = new ArrayList<>(handlers.entrySet()); final List> handlerPairs = new LinkedList<>(); for (int i = 0; i < exceptionHandlers.size(); i++) { for (int j = i + 1; j < exceptionHandlers.size(); j++) { TryEdgeScope a = new TryEdgeScope(exceptionHandlers.get(i).getKey(), exceptionHandlers.get(i).getValue()); TryEdgeScope b = new TryEdgeScope(exceptionHandlers.get(j).getKey(), exceptionHandlers.get(j).getValue()); handlerPairs.add(new Pair<>(a, b)); } } final Map simplifiedScopes = new HashMap<>(handlers); int i = 0; while (i < handlerPairs.size()) { final Pair handlerPair = handlerPairs.get(i); final TryEdgeScope edgeScopeA = handlerPair.getFirst(); final TryEdgeScope edgeScopeB = handlerPair.getSecond(); final BlockNode edgeBlockA = edgeScopeA.block; final BlockNode edgeBlockB = edgeScopeB.block; final boolean pathExists = BlockUtils.isPathExists(edgeBlockA, edgeBlockB) || BlockUtils.isPathExists(edgeBlockB, edgeBlockA); if (pathExists) { BlockNode bottomBlock = BlockUtils.getBottomBlock(List.of(edgeBlockA, edgeBlockB)); // The two blocks are within the same scope - remove these from the matrix final TryEdge removeHandler = edgeBlockA != bottomBlock ? edgeScopeA.edge : edgeScopeB.edge; final TryEdge keepHandler = edgeBlockA == bottomBlock ? edgeScopeA.edge : edgeScopeB.edge; simplifiedScopes.remove(removeHandler); handlerPairs.remove(i); mergedEdges.add(new Pair<>(keepHandler, removeHandler)); } else { i++; } } return simplifiedScopes; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdgeType.java ================================================ package jadx.core.dex.trycatch; public enum TryEdgeType { TRUE_FALLTHROUGH, PREMATURE_EXIT, LOOP_EXIT, HANDLER } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/AbstractVisitor.java ================================================ package jadx.core.dex.visitors; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxException; public abstract class AbstractVisitor implements IDexTreeVisitor { @Override public void init(RootNode root) throws JadxException { // no op implementation } @Override public boolean visit(ClassNode cls) throws JadxException { // no op implementation return true; } @Override public void visit(MethodNode mth) throws JadxException { // no op implementation } @Override public String getName() { return this.getClass().getSimpleName(); } @Override public String toString() { return getName(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/AdjustForIfMergeVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.Collections; import java.util.List; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.SpecialEdgeAttr; import jadx.core.dex.attributes.nodes.SpecialEdgeAttr.SpecialEdgeType; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.regions.RegionMakerVisitor; import jadx.core.dex.visitors.typeinference.FinishTypeInference; import jadx.core.utils.BlockUtils; @JadxVisitor( name = "AdjustForIfMergeVisitor", desc = "Move instructions between if blocks that can't be inlined but are safe to push through the if to allow the ifs to merge", runBefore = { RegionMakerVisitor.class }, runAfter = { FinishTypeInference.class } ) public class AdjustForIfMergeVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) { if (mth.isNoCode() || mth.getBasicBlocks() == null) { return; } // Find candidates for adjustment by selecting blocks between two if statements List blocks = mth.getBasicBlocks(); for (BlockNode blk : blocks) { if (areSurroundingsCorrectShape(blk)) { BlockNode pred = blk.getPredecessors().get(0); BlockNode succ = blk.getCleanSuccessors().get(0); if (isSimpleIf(pred) && isSimpleIf(succ)) { List movableInstructions = getMovableInstructions(blk, succ); if (!movableInstructions.isEmpty() && couldMerge(mth, pred, blk, succ)) { doMove(mth, blk, succ, movableInstructions); } } } } } private boolean areSurroundingsCorrectShape(BlockNode blk) { return (blk.getPredecessors().size() == 1 && blk.getCleanSuccessors().size() == 1); } private boolean isSimpleIf(BlockNode blk) { return blk.getInstructions().size() == 1 && blk.getInstructions().get(0).getType() == InsnType.IF; } private boolean couldMerge(MethodNode mth, BlockNode pred, BlockNode blk, BlockNode succ) { // we cannot merge if the edge from blk to succ is a back edge // there's a function in BlockUtils that purports to check if something is a back edge but it // doesn't so do it by hand here List specialEdges = mth.getAll(AType.SPECIAL_EDGE); for (SpecialEdgeAttr edge : specialEdges) { if (edge.getStart() == blk && edge.getEnd() == succ && edge.getType() == SpecialEdgeType.BACK_EDGE) { mth.addDebugComment("Refusing to push insns through at block " + blk.toString() + " : edge to successor is a back edge."); return false; } } return true; } private List getMovableInstructions(BlockNode blk, BlockNode succ) { // A 'movable instruction' is one that does not impact either codegen or the semantics of the // following block, so it can be pushed through into the new synthetics. // For now, we just look for nop moves along the same register such that the target variable is not // used in the succ block. List movableInstructions = new ArrayList<>(); for (InsnNode insn : blk.getInstructions()) { if (insn.getType() == InsnType.MOVE) { if (!(insn.getArg(0) instanceof RegisterArg)) { // could be a LiteralArg continue; } RegisterArg source = (RegisterArg) insn.getArg(0); RegisterArg target = insn.getResult(); List uses = target.getSVar().getUseList(); for (RegisterArg use : uses) { if (BlockUtils.blockContains(succ, use.getParentInsn())) { // the target is used inside the successor, so we can't cleanly do the assignment afterwards continue; } } // we don't want to just push everything through, e.g. // if (condition) { return; } // x = 123456 // if (condition) { return; } // would be a less clean result if the assignment was pushed into the block of the 2nd if. if (source.getRegNum() == target.getRegNum()) { movableInstructions.add(insn); } } } return movableInstructions; } private void doMove(MethodNode mth, BlockNode target, BlockNode bottomIf, List movableInstructions) { // Move instructions from the list out of blk and into new synthetics on each edge out of succ // preserving instruction ordering, although it's unlikely that it would ever matter here Collections.reverse(movableInstructions); for (InsnNode insn : movableInstructions) { target.getInstructions().remove(insn); for (BlockNode succ : bottomIf.getCleanSuccessors()) { succ.getInstructions().add(0, insn); // add at start if (succ.contains(AFlag.LOOP_START)) { // if we're merging into a loop condition, silence the warning when there's more than one // instruction in the loop header succ.add(AFlag.ALLOW_MULTIPLE_INSNS_LOOP_COND); } } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/AnonymousClassVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.FieldReplaceAttr; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "AnonymousClassVisitor", desc = "Prepare anonymous class for inline", runBefore = { ModVisitor.class, CodeShrinkVisitor.class } ) public class AnonymousClassVisitor extends AbstractVisitor { @Override public boolean visit(ClassNode cls) throws JadxException { if (cls.contains(AType.ANONYMOUS_CLASS)) { for (MethodNode mth : cls.getMethods()) { if (mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) { processAnonymousConstructor(mth); break; } } } return true; } private static void processAnonymousConstructor(MethodNode mth) { List usedInsns = new ArrayList<>(); Map argsMap = getArgsToFieldsMapping(mth, usedInsns); if (argsMap.isEmpty()) { mth.add(AFlag.NO_SKIP_ARGS); } else { for (Map.Entry entry : argsMap.entrySet()) { FieldNode field = entry.getValue(); if (field == null) { continue; } InsnArg arg = entry.getKey(); field.addAttr(new FieldReplaceAttr(arg)); field.add(AFlag.DONT_GENERATE); if (arg.isRegister()) { arg.add(AFlag.SKIP_ARG); SkipMethodArgsAttr.skipArg(mth, ((RegisterArg) arg)); } } } for (InsnNode usedInsn : usedInsns) { usedInsn.add(AFlag.DONT_GENERATE); } } private static Map getArgsToFieldsMapping(MethodNode mth, List usedInsns) { MethodInfo callMth = mth.getMethodInfo(); ClassNode cls = mth.getParentClass(); List argList = mth.getArgRegs(); ClassNode outerCls = mth.getUseIn().get(0).getParentClass(); int startArg = 0; if (callMth.getArgsCount() != 0 && callMth.getArgumentsTypes().get(0).equals(outerCls.getClassInfo().getType())) { startArg = 1; } Map map = new LinkedHashMap<>(); int argsCount = argList.size(); for (int i = startArg; i < argsCount; i++) { RegisterArg arg = argList.get(i); InsnNode useInsn = getParentInsnSkipMove(arg); if (useInsn == null) { return Collections.emptyMap(); } switch (useInsn.getType()) { case IPUT: FieldNode fieldNode = cls.searchField((FieldInfo) ((IndexInsnNode) useInsn).getIndex()); if (fieldNode == null || !fieldNode.getAccessFlags().isSynthetic()) { return Collections.emptyMap(); } map.put(arg, fieldNode); usedInsns.add(useInsn); break; case CONSTRUCTOR: ConstructorInsn superConstr = (ConstructorInsn) useInsn; if (!superConstr.isSuper()) { return Collections.emptyMap(); } usedInsns.add(useInsn); break; default: return Collections.emptyMap(); } } return map; } private static InsnNode getParentInsnSkipMove(RegisterArg arg) { SSAVar sVar = arg.getSVar(); if (sVar.getUseCount() != 1) { return null; } RegisterArg useArg = sVar.getUseList().get(0); InsnNode parentInsn = useArg.getParentInsn(); if (parentInsn == null) { return null; } if (parentInsn.getType() == InsnType.MOVE) { return getParentInsnSkipMove(parentInsn.getResult()); } return parentInsn; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ApplyVariableNames.java ================================================ package jadx.core.dex.visitors; import java.util.List; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.core.Consts; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.regions.variables.ProcessVariables; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "ApplyVariableNames", desc = "Try to guess variable name from usage", runAfter = { ProcessVariables.class } ) public class ApplyVariableNames extends AbstractVisitor { private static final Map OBJ_ALIAS = Utils.newConstStringMap( Consts.CLASS_STRING, "str", Consts.CLASS_CLASS, "cls", Consts.CLASS_THROWABLE, "th", Consts.CLASS_OBJECT, "obj", "java.util.Iterator", "it", "java.util.HashMap", "map", "java.lang.Boolean", "bool", "java.lang.Short", "sh", "java.lang.Integer", "num", "java.lang.Character", "ch", "java.lang.Byte", "b", "java.lang.Float", "f", "java.lang.Long", "l", "java.lang.Double", "d", "java.lang.StringBuilder", "sb", "java.lang.Exception", "exc"); private static final Set GOOD_VAR_NAMES = Set.of( "size", "length", "list", "map", "next"); private static final List INVOKE_PREFIXES = List.of( "get", "set", "to", "parse", "read", "format"); private RootNode root; @Override public void init(RootNode root) throws JadxException { this.root = root; } @Override public void visit(MethodNode mth) throws JadxException { for (SSAVar ssaVar : mth.getSVars()) { CodeVar codeVar = ssaVar.getCodeVar(); String newName = guessName(codeVar); if (newName != null) { codeVar.setName(newName); } } } private @Nullable String guessName(CodeVar var) { if (var.isThis()) { return RegisterArg.THIS_ARG_NAME; } if (!var.isDeclared()) { // name is not used in code return null; } if (NameMapper.isValidAndPrintable(var.getName())) { // the current name is valid, keep it return null; } List ssaVars = var.getSsaVars(); if (Utils.notEmpty(ssaVars)) { boolean mthArg = ssaVars.stream().anyMatch(ssaVar -> ssaVar.getAssign().contains(AFlag.METHOD_ARGUMENT)); if (mthArg) { // for method args use defined type and ignore usage return makeNameForType(var.getType()); } for (SSAVar ssaVar : ssaVars) { String name = makeNameForSSAVar(ssaVar); if (name != null) { return name; } } } return makeNameForType(var.getType()); } private @Nullable String makeNameForSSAVar(SSAVar ssaVar) { String ssaVarName = ssaVar.getName(); if (ssaVarName != null) { return ssaVarName; } InsnNode assignInsn = ssaVar.getAssignInsn(); if (assignInsn != null) { String name = makeNameFromInsn(ssaVar, assignInsn); if (NameMapper.isValidAndPrintable(name)) { return name; } } return null; } private String makeNameFromInsn(SSAVar ssaVar, InsnNode insn) { switch (insn.getType()) { case INVOKE: return makeNameFromInvoke(ssaVar, (InvokeNode) insn); case CONSTRUCTOR: ConstructorInsn co = (ConstructorInsn) insn; MethodNode callMth = root.getMethodUtils().resolveMethod(co); if (callMth != null && callMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) { // don't use name of anonymous class return null; } return makeNameForClass(co.getClassType()); case ARRAY_LENGTH: return "length"; case ARITH: case TERNARY: case CAST: for (InsnArg arg : insn.getArguments()) { if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); String wName = makeNameFromInsn(ssaVar, wrapInsn); if (wName != null) { return wName; } } } break; default: break; } return null; } private String makeNameForType(ArgType type) { if (type.isPrimitive()) { return type.getPrimitiveType().getShortName().toLowerCase(); } if (type.isArray()) { return makeNameForType(type.getArrayRootElement()) + "Arr"; } return makeNameForObject(type); } private String makeNameForObject(ArgType type) { if (type.isGenericType()) { return StringUtils.escape(type.getObject().toLowerCase()); } if (type.isObject()) { String alias = getAliasForObject(type.getObject()); if (alias != null) { return alias; } return makeNameForCheckedClass(ClassInfo.fromType(root, type)); } return StringUtils.escape(type.toString()); } private String makeNameForCheckedClass(ClassInfo classInfo) { String shortName = classInfo.getAliasShortName(); String vName = fromName(shortName); if (vName != null) { return vName; } String lower = StringUtils.escape(shortName.toLowerCase()); if (shortName.equals(lower)) { return lower + "Var"; } return lower; } private String makeNameForClass(ClassInfo classInfo) { String alias = getAliasForObject(classInfo.getFullName()); if (alias != null) { return alias; } return makeNameForCheckedClass(classInfo); } private static String fromName(String name) { if (name == null || name.isEmpty()) { return null; } if (name.toUpperCase().equals(name)) { // all characters are upper case return name.toLowerCase(); } String v1 = Character.toLowerCase(name.charAt(0)) + name.substring(1); if (!v1.equals(name)) { return v1; } if (name.length() < 3) { return name + "Var"; } return null; } private static String getAliasForObject(String name) { return OBJ_ALIAS.get(name); } private String makeNameFromInvoke(SSAVar ssaVar, InvokeNode inv) { MethodInfo callMth = inv.getCallMth(); String name = callMth.getAlias(); ClassInfo declClass = callMth.getDeclClass(); if ("getInstance".equals(name)) { // e.g. Cipher.getInstance return makeNameForClass(declClass); } String shortName = cutPrefix(name); if (shortName != null) { return fromName(shortName); } if ("iterator".equals(name)) { return "it"; } if ("toString".equals(name)) { return makeNameForClass(declClass); } if ("forName".equals(name) && declClass.getType().equals(ArgType.CLASS)) { return OBJ_ALIAS.get(Consts.CLASS_CLASS); } // use method name as a variable name not the best idea in most cases if (!GOOD_VAR_NAMES.contains(name)) { String typeName = makeNameForType(ssaVar.getCodeVar().getType()); if (!typeName.equalsIgnoreCase(name)) { return typeName + StringUtils.capitalizeFirstChar(name); } } return name; } private @Nullable String cutPrefix(String name) { for (String prefix : INVOKE_PREFIXES) { if (name.startsWith(prefix)) { return name.substring(prefix.length()); } } return null; } @Override public String getName() { return "ApplyVariableNames"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/AttachCommentsVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.data.CodeRefType; import jadx.api.data.ICodeComment; import jadx.api.data.ICodeData; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef; import jadx.core.codegen.utils.CodeComment; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; @JadxVisitor( name = "AttachComments", desc = "Attach user code comments", runBefore = { ProcessInstructionsVisitor.class } ) public class AttachCommentsVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(AttachCommentsVisitor.class); private Map> clsCommentsMap; @Override public void init(RootNode root) throws JadxException { updateCommentsData(root.getArgs().getCodeData()); root.registerCodeDataUpdateListener(this::updateCommentsData); } @Override public boolean visit(ClassNode cls) { List clsComments = getCommentsData(cls); if (!clsComments.isEmpty()) { applyComments(cls, clsComments); } cls.getInnerClasses().forEach(this::visit); return false; } private static void applyComments(ClassNode cls, List clsComments) { for (ICodeComment comment : clsComments) { IJavaNodeRef nodeRef = comment.getNodeRef(); switch (nodeRef.getType()) { case CLASS: addComment(cls, comment); break; case FIELD: FieldNode fieldNode = cls.searchFieldByShortId(nodeRef.getShortId()); if (fieldNode == null) { LOG.warn("Field reference not found: {}", nodeRef); } else { addComment(fieldNode, comment); } break; case METHOD: MethodNode methodNode = cls.searchMethodByShortId(nodeRef.getShortId()); if (methodNode == null) { LOG.warn("Method reference not found: {}", nodeRef); } else { IJavaCodeRef codeRef = comment.getCodeRef(); if (codeRef == null) { addComment(methodNode, comment); } else { processCustomAttach(methodNode, codeRef, comment); } } break; } } } private static InsnNode getInsnByOffset(MethodNode mth, int offset) { try { return mth.getInstructions()[offset]; } catch (Exception e) { LOG.warn("Insn reference not found in: {} with offset: {}", mth, offset); return null; } } private static void processCustomAttach(MethodNode mth, IJavaCodeRef codeRef, ICodeComment comment) { CodeRefType attachType = codeRef.getAttachType(); switch (attachType) { case INSN: { InsnNode insn = getInsnByOffset(mth, codeRef.getIndex()); addComment(insn, comment); break; } default: throw new JadxRuntimeException("Unexpected attach type: " + attachType); } } private static void addComment(@Nullable IAttributeNode node, ICodeComment comment) { if (node == null) { return; } node.addAttr(AType.CODE_COMMENTS, new CodeComment(comment)); } private List getCommentsData(ClassNode cls) { if (clsCommentsMap == null) { return Collections.emptyList(); } List clsComments = clsCommentsMap.get(cls.getClassInfo().getRawName()); if (clsComments == null) { return Collections.emptyList(); } return clsComments; } private void updateCommentsData(@Nullable ICodeData data) { if (data == null) { this.clsCommentsMap = Collections.emptyMap(); } else { this.clsCommentsMap = data.getComments().stream() .collect(Collectors.groupingBy(c -> c.getNodeRef().getDeclaringClass())); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/AttachMethodDetails.java ================================================ package jadx.core.dex.visitors; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.utils.MethodUtils; import jadx.core.dex.visitors.blocks.BlockSplitter; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "Attach Method Details", desc = "Attach method details for invoke instructions", runBefore = { BlockSplitter.class, MethodInvokeVisitor.class } ) public class AttachMethodDetails extends AbstractVisitor { private MethodUtils methodUtils; @Override public void init(RootNode root) { methodUtils = root.getMethodUtils(); } @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } for (InsnNode insn : mth.getInstructions()) { if (insn instanceof BaseInvokeNode) { attachMethodDetails((BaseInvokeNode) insn); } } } private void attachMethodDetails(BaseInvokeNode insn) { IMethodDetails methodDetails = methodUtils.getMethodDetails(insn.getCallMth()); if (methodDetails != null) { insn.addAttr(methodDetails); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/AttachTryCatchVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.ICatch; import jadx.api.plugins.input.data.ITry; import jadx.api.plugins.utils.Utils; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.utils.exceptions.JadxException; import static jadx.core.dex.visitors.ProcessInstructionsVisitor.getNextInsnOffset; @JadxVisitor( name = "Attach Try/Catch Visitor", desc = "Attach try/catch info to instructions", runBefore = { ProcessInstructionsVisitor.class } ) public class AttachTryCatchVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(AttachTryCatchVisitor.class); @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } initTryCatches(mth, mth.getInstructions(), mth.getCodeReader().getTries()); } private static void initTryCatches(MethodNode mth, InsnNode[] insnByOffset, List tries) { if (tries.isEmpty()) { return; } if (Consts.DEBUG_EXC_HANDLERS) { LOG.debug("Raw try blocks in {}", mth); tries.forEach(tryData -> LOG.debug(" - {}", tryData)); } for (ITry tryData : tries) { List handlers = convertToHandlers(mth, tryData.getCatch(), insnByOffset); if (handlers.isEmpty()) { continue; } markTryBounds(insnByOffset, tryData, CatchAttr.build(handlers)); } } private static void markTryBounds(InsnNode[] insnByOffset, ITry aTry, CatchAttr catchAttr) { int offset = aTry.getStartOffset(); int end = aTry.getEndOffset(); boolean tryBlockStarted = false; InsnNode insn = null; while (offset <= end) { InsnNode insnAtOffset = insnByOffset[offset]; if (insnAtOffset != null) { insn = insnAtOffset; attachCatchAttr(catchAttr, insn); if (!tryBlockStarted) { insn.add(AFlag.TRY_ENTER); tryBlockStarted = true; } } offset = getNextInsnOffset(insnByOffset, offset); if (offset == -1) { break; } } if (tryBlockStarted) { insn.add(AFlag.TRY_LEAVE); } else { // no instructions found in range -> add nop at start offset InsnNode nop = insertNOP(insnByOffset, aTry.getStartOffset()); nop.add(AFlag.TRY_ENTER); nop.add(AFlag.TRY_LEAVE); nop.addAttr(catchAttr); } } private static void attachCatchAttr(CatchAttr catchAttr, InsnNode insn) { CatchAttr existAttr = insn.get(AType.EXC_CATCH); if (existAttr != null) { // merge handlers List handlers = Utils.concat(existAttr.getHandlers(), catchAttr.getHandlers()); insn.addAttr(CatchAttr.build(handlers)); } else { insn.addAttr(catchAttr); } } private static List convertToHandlers(MethodNode mth, ICatch catchBlock, InsnNode[] insnByOffset) { int[] handlerOffsetArr = catchBlock.getHandlers(); String[] handlerTypes = catchBlock.getTypes(); int handlersCount = handlerOffsetArr.length; List list = new ArrayList<>(handlersCount); for (int i = 0; i < handlersCount; i++) { int handlerOffset = handlerOffsetArr[i]; ClassInfo type = ClassInfo.fromName(mth.root(), handlerTypes[i]); Utils.addToList(list, createHandler(mth, insnByOffset, handlerOffset, type)); } int allHandlerOffset = catchBlock.getCatchAllHandler(); if (allHandlerOffset >= 0) { Utils.addToList(list, createHandler(mth, insnByOffset, allHandlerOffset, null)); } return list; } @Nullable private static ExceptionHandler createHandler(MethodNode mth, InsnNode[] insnByOffset, int handlerOffset, @Nullable ClassInfo type) { InsnNode insn = insnByOffset[handlerOffset]; if (insn != null) { ExcHandlerAttr excHandlerAttr = insn.get(AType.EXC_HANDLER); if (excHandlerAttr != null) { ExceptionHandler handler = excHandlerAttr.getHandler(); if (handler.addCatchType(mth, type)) { // exist handler updated (assume from same try block) - don't add again return null; } // same handler (can be used in different try blocks) return handler; } } else { insn = insertNOP(insnByOffset, handlerOffset); } ExceptionHandler handler = ExceptionHandler.build(mth, handlerOffset, type); mth.addExceptionHandler(handler); insn.addAttr(new ExcHandlerAttr(handler)); return handler; } private static InsnNode insertNOP(InsnNode[] insnByOffset, int offset) { InsnNode nop = new InsnNode(InsnType.NOP, 0); nop.setOffset(offset); nop.add(AFlag.SYNTHETIC); insnByOffset[offset] = nop; return nop; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/CheckCode.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.List; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.Utils.isEmpty; @JadxVisitor( name = "CheckCode", desc = "Check and remove bad or incorrect code" ) public class CheckCode extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { MethodInfo mthInfo = mth.getMethodInfo(); if (mthInfo.getArgumentsTypes().size() > 255) { // java spec don't allow more than 255 args if (canRemoveMethod(mth)) { mth.ignoreMethod(); } else { // TODO: convert args to array } } checkInstructions(mth); } private boolean canRemoveMethod(MethodNode mth) { if (mth.getUseIn().isEmpty()) { return true; } InsnNode[] insns = mth.getInstructions(); if (insns.length == 0) { return true; } for (InsnNode insn : insns) { if (insn != null && insn.getType() != InsnType.NOP) { if (insn.getType() == InsnType.RETURN && insn.getArgsCount() == 0) { // ignore void return } else { // found useful instruction return false; } } } return true; } public void checkInstructions(MethodNode mth) { if (isEmpty(mth.getInstructions())) { return; } int regsCount = mth.getRegsCount(); List list = new ArrayList<>(); for (InsnNode insnNode : mth.getInstructions()) { if (insnNode == null) { continue; } list.clear(); RegisterArg resultArg = insnNode.getResult(); if (resultArg != null) { list.add(resultArg); } insnNode.getRegisterArgs(list); for (RegisterArg arg : list) { int regNum = arg.getRegNum(); if (regNum < 0) { throw new JadxRuntimeException("Incorrect negative register number in instruction: " + insnNode); } if (regNum >= regsCount) { throw new JadxRuntimeException("Incorrect register number in instruction: " + insnNode + ", expected to be less than " + regsCount); } } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.List; import java.util.Objects; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.FieldReplaceAttr; import jadx.core.dex.attributes.nodes.MethodReplaceAttr; import jadx.core.dex.attributes.nodes.RenameReasonAttr; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.fixaccessmodifiers.FixAccessModifiers; import jadx.core.dex.visitors.usage.UsageInfoVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnRemover; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "ClassModifier", desc = "Remove synthetic classes, methods and fields", runAfter = { ModVisitor.class, FixAccessModifiers.class, ProcessAnonymous.class, ExtractFieldInit.class } ) public class ClassModifier extends AbstractVisitor { @Override public boolean visit(ClassNode cls) throws JadxException { if (cls.contains(AFlag.PACKAGE_INFO)) { return false; } for (ClassNode inner : cls.getInnerClasses()) { visit(inner); } if (isEmptySyntheticClass(cls)) { cls.add(AFlag.DONT_GENERATE); return false; } removeSyntheticFields(cls); cls.getMethods().forEach(ClassModifier::removeSyntheticMethods); cls.getMethods().forEach(ClassModifier::removeEmptyMethods); return false; } private static boolean isEmptySyntheticClass(ClassNode cls) { return cls.getAccessFlags().isSynthetic() && cls.getFields().isEmpty() && cls.getMethods().isEmpty() && cls.getInnerClasses().isEmpty(); } /** * Remove synthetic fields if type is outer class or class will be inlined (anonymous) */ private static void removeSyntheticFields(ClassNode cls) { boolean inline = cls.isAnonymous(); if (inline || cls.getClassInfo().isInner()) { for (FieldNode field : cls.getFields()) { ArgType fldType = field.getType(); if (field.getAccessFlags().isSynthetic() && fldType.isObject() && !fldType.isGenericType()) { ClassInfo clsInfo = ClassInfo.fromType(cls.root(), fldType); ClassNode fieldsCls = cls.root().resolveClass(clsInfo); ClassInfo parentClass = cls.getClassInfo().getParentClass(); if (fieldsCls == null) { continue; } boolean isParentInst = Objects.equals(parentClass, fieldsCls.getClassInfo()); if (inline || isParentInst) { int found = 0; for (MethodNode mth : cls.getMethods()) { if (removeFieldUsageFromConstructor(mth, field, fieldsCls)) { found++; } } if (found != 0) { if (isParentInst) { field.addAttr(new FieldReplaceAttr(fieldsCls.getClassInfo())); } field.add(AFlag.DONT_GENERATE); } } } } } } private static boolean removeFieldUsageFromConstructor(MethodNode mth, FieldNode field, ClassNode fieldsCls) { if (mth.isNoCode() || !mth.getAccessFlags().isConstructor()) { return false; } List args = mth.getArgRegs(); if (args.isEmpty() || mth.contains(AFlag.SKIP_FIRST_ARG)) { return false; } RegisterArg arg = args.get(0); if (!arg.getType().equals(fieldsCls.getClassInfo().getType())) { return false; } BlockNode block = mth.getEnterBlock().getCleanSuccessors().get(0); List instructions = block.getInstructions(); if (instructions.isEmpty()) { return false; } InsnNode insn = instructions.get(0); if (insn.getType() != InsnType.IPUT) { return false; } IndexInsnNode putInsn = (IndexInsnNode) insn; FieldInfo fieldInfo = (FieldInfo) putInsn.getIndex(); if (!fieldInfo.equals(field.getFieldInfo()) || !putInsn.getArg(0).equals(arg)) { return false; } mth.skipFirstArgument(); InsnRemover.remove(mth, block, insn); // other arg usage -> wrap with IGET insn if (arg.getSVar().getUseCount() != 0) { InsnNode iget = new IndexInsnNode(InsnType.IGET, fieldInfo, 1); iget.addArg(insn.getArg(1)); for (InsnArg insnArg : new ArrayList<>(arg.getSVar().getUseList())) { insnArg.wrapInstruction(mth, iget); } } return true; } private static void removeSyntheticMethods(MethodNode mth) { if (mth.isNoCode() || mth.contains(AFlag.DONT_GENERATE)) { return; } AccessInfo af = mth.getAccessFlags(); if (!af.isSynthetic()) { return; } ClassNode cls = mth.getParentClass(); if (removeBridgeMethod(cls, mth)) { if (Consts.DEBUG) { mth.addDebugComment("Removed as synthetic bridge method"); } else { mth.add(AFlag.DONT_GENERATE); } return; } // remove synthetic constructor for inner classes if (mth.isConstructor() && (mth.contains(AFlag.METHOD_CANDIDATE_FOR_INLINE) || mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR))) { InsnNode insn = BlockUtils.getOnlyOneInsnFromMth(mth); if (insn != null) { List args = mth.getArgRegs(); if (isRemovedClassInArgs(cls, args)) { modifySyntheticMethod(cls, mth, insn, args); } } } } private static boolean isRemovedClassInArgs(ClassNode cls, List mthArgs) { boolean removedFound = false; for (RegisterArg arg : mthArgs) { ArgType argType = arg.getType(); if (!argType.isObject()) { continue; } boolean remove = false; ClassNode argCls = cls.root().resolveClass(argType); if (argCls == null) { // check if missing class from current top class ClassInfo argClsInfo = ClassInfo.fromType(cls.root(), argType); if (argClsInfo.getParentClass() != null && cls.getFullName().startsWith(argClsInfo.getParentClass().getFullName())) { remove = true; } } else { if (argCls.contains(AFlag.DONT_GENERATE) || isEmptySyntheticClass(argCls)) { remove = true; } } if (remove) { arg.add(AFlag.REMOVE); removedFound = true; } } return removedFound; } /** * Remove synthetic constructor and redirect calls to existing constructor */ private static void modifySyntheticMethod(ClassNode cls, MethodNode mth, InsnNode insn, List args) { if (insn.getType() == InsnType.CONSTRUCTOR) { ConstructorInsn constr = (ConstructorInsn) insn; if (constr.isThis() && !args.isEmpty()) { // remove first arg for non-static class (references to outer class) RegisterArg firstArg = args.get(0); if (firstArg.getType().equals(cls.getParentClass().getClassInfo().getType())) { SkipMethodArgsAttr.skipArg(mth, 0); } // remove unused args int argsCount = args.size(); for (int i = 0; i < argsCount; i++) { RegisterArg arg = args.get(i); SSAVar sVar = arg.getSVar(); if (sVar != null && sVar.getUseCount() == 0) { SkipMethodArgsAttr.skipArg(mth, i); } } MethodInfo callMth = constr.getCallMth(); MethodNode callMthNode = cls.root().resolveMethod(callMth); if (callMthNode != null) { mth.addAttr(new MethodReplaceAttr(callMthNode)); mth.add(AFlag.DONT_GENERATE); // code generation order should be already fixed for marked methods UsageInfoVisitor.replaceMethodUsage(callMthNode, mth); } } } } private static boolean removeBridgeMethod(ClassNode cls, MethodNode mth) { if (cls.root().getArgs().isInlineMethods()) { // simple wrapper remove is same as inline List allInsns = BlockUtils.collectAllInsns(mth.getBasicBlocks()); if (allInsns.size() == 1) { InsnNode wrappedInsn = allInsns.get(0); if (wrappedInsn.getType() == InsnType.RETURN) { InsnArg arg = wrappedInsn.getArg(0); if (arg.isInsnWrap()) { wrappedInsn = ((InsnWrapArg) arg).getWrapInsn(); } } return checkSyntheticWrapper(mth, wrappedInsn); } } return false; } private static boolean checkSyntheticWrapper(MethodNode mth, InsnNode insn) { InsnType insnType = insn.getType(); if (insnType != InsnType.INVOKE) { return false; } InvokeNode invokeInsn = (InvokeNode) insn; if (invokeInsn.getInvokeType() == InvokeType.SUPER) { return false; } MethodInfo callMth = invokeInsn.getCallMth(); MethodNode wrappedMth = mth.root().resolveMethod(callMth); if (wrappedMth == null) { return false; } AccessInfo wrappedAccFlags = wrappedMth.getAccessFlags(); if (wrappedAccFlags.isStatic()) { return false; } if (callMth.getArgsCount() != mth.getMethodInfo().getArgsCount()) { return false; } // rename method only from current class if (!mth.getParentClass().equals(wrappedMth.getParentClass())) { return false; } // all args must be registers passed from method args (allow only casts insns) for (InsnArg arg : insn.getArguments()) { if (!registersAndCastsOnly(arg)) { return false; } } // remove confirmed, change visibility and name if needed if (!wrappedAccFlags.isPublic() && !mth.root().getArgs().isRespectBytecodeAccModifiers()) { // must be public FixAccessModifiers.changeVisibility(wrappedMth, AccessFlags.PUBLIC); } String alias = mth.getAlias(); if (!Objects.equals(wrappedMth.getAlias(), alias)) { wrappedMth.rename(alias); RenameReasonAttr.forNode(wrappedMth).append("merged with bridge method [inline-methods]"); } wrappedMth.addAttr(new MethodReplaceAttr(mth)); wrappedMth.copyAttributeFrom(mth, AType.METHOD_OVERRIDE); wrappedMth.addDebugComment("Method merged with bridge method: " + mth.getMethodInfo().getShortId()); return true; } private static boolean registersAndCastsOnly(InsnArg arg) { if (arg.isRegister()) { return true; } if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); if (wrapInsn.getType() == InsnType.CHECK_CAST) { return registersAndCastsOnly(wrapInsn.getArg(0)); } } return false; } /** * Remove public empty constructors (static or default) */ private static void removeEmptyMethods(MethodNode mth) { if (!mth.getArgRegs().isEmpty()) { return; } AccessInfo af = mth.getAccessFlags(); boolean publicConstructor = mth.isConstructor() && af.isPublic(); boolean enumDefConstructor = mth.isConstructor() && mth.getParentClass().contains(AFlag.CONVERTED_ENUM); boolean clsInit = mth.getMethodInfo().isClassInit() && af.isStatic(); if (publicConstructor || enumDefConstructor || clsInit) { if (!BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) { return; } if (clsInit) { mth.add(AFlag.DONT_GENERATE); } else { // don't remove default constructor if other constructors exists or constructor has annotations if (mth.isDefaultConstructor() && !isNonDefaultConstructorExists(mth) && !mth.contains(JadxAttrType.ANNOTATION_LIST)) { mth.add(AFlag.DONT_GENERATE); } } } } private static boolean isNonDefaultConstructorExists(MethodNode defCtor) { ClassNode parentClass = defCtor.getParentClass(); for (MethodNode mth : parentClass.getMethods()) { if (mth != defCtor && mth.isConstructor() && !mth.isDefaultConstructor()) { return true; } } return false; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.List; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IFieldInfoRef; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.finaly.MarkFinallyVisitor; import jadx.core.dex.visitors.ssa.SSATransform; import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; import jadx.core.utils.InsnRemover; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; @JadxVisitor( name = "Constants Inline", desc = "Inline constant registers into instructions", runAfter = { SSATransform.class, MarkFinallyVisitor.class }, runBefore = TypeInferenceVisitor.class ) public class ConstInlineVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } process(mth); } public static void process(MethodNode mth) { List toRemove = new ArrayList<>(); for (BlockNode block : mth.getBasicBlocks()) { toRemove.clear(); for (InsnNode insn : block.getInstructions()) { checkInsn(mth, insn, toRemove); } InsnRemover.removeAllAndUnbind(mth, block, toRemove); } } private static void checkInsn(MethodNode mth, InsnNode insn, List toRemove) { if (insn.contains(AFlag.DONT_INLINE) || insn.contains(AFlag.DONT_GENERATE) || insn.getResult() == null) { return; } SSAVar sVar = insn.getResult().getSVar(); InsnArg constArg; Runnable onSuccess = null; switch (insn.getType()) { case CONST: case MOVE: { constArg = insn.getArg(0); if (!constArg.isLiteral()) { return; } if (constArg.isZeroLiteral() && forbidNullInlines(sVar)) { // all usages forbids inlining return; } break; } case CONST_STR: { String s = ((ConstStringNode) insn).getString(); IFieldInfoRef f = mth.getParentClass().getConstField(s); if (f == null) { InsnNode copy = insn.copyWithoutResult(); constArg = InsnArg.wrapArg(copy); } else { InsnNode constGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0); constArg = InsnArg.wrapArg(constGet); constArg.setType(ArgType.STRING); onSuccess = () -> ModVisitor.addFieldUsage(f, mth); } break; } case CONST_CLASS: { if (sVar.isUsedInPhi()) { return; } constArg = InsnArg.wrapArg(insn.copyWithoutResult()); constArg.setType(ArgType.CLASS); break; } default: return; } // all check passed, run replace if (replaceConst(mth, insn, constArg)) { toRemove.add(insn); if (onSuccess != null) { onSuccess.run(); } } } /** * Don't inline null object */ private static boolean forbidNullInlines(SSAVar sVar) { List useList = sVar.getUseList(); if (useList.isEmpty()) { return false; } int k = 0; for (RegisterArg useArg : useList) { InsnNode insn = useArg.getParentInsn(); if (insn != null && forbidNullArgInline(insn, useArg)) { k++; } } return k == useList.size(); } private static boolean forbidNullArgInline(InsnNode insn, RegisterArg useArg) { if (insn.getType() == InsnType.MOVE) { // result is null, chain checks return forbidNullInlines(insn.getResult().getSVar()); } if (!canUseNull(insn, useArg)) { useArg.add(AFlag.DONT_INLINE_CONST); return true; } return false; } private static boolean canUseNull(InsnNode insn, RegisterArg useArg) { switch (insn.getType()) { case INVOKE: return ((InvokeNode) insn).getInstanceArg() != useArg; case ARRAY_LENGTH: case AGET: case APUT: case IGET: case SWITCH: case MONITOR_ENTER: case MONITOR_EXIT: case INSTANCE_OF: return insn.getArg(0) != useArg; case IPUT: return insn.getArg(1) != useArg; } return true; } private static boolean replaceConst(MethodNode mth, InsnNode constInsn, InsnArg constArg) { SSAVar ssaVar = constInsn.getResult().getSVar(); if (ssaVar.getUseCount() == 0) { return true; } List useList = new ArrayList<>(ssaVar.getUseList()); int replaceCount = 0; for (RegisterArg arg : useList) { if (canInline(mth, arg) && replaceArg(mth, arg, constArg, constInsn)) { replaceCount++; } } if (replaceCount == useList.size()) { return true; } // hide insn if used only in not generated insns if (ssaVar.getUseList().stream().allMatch(ConstInlineVisitor::canIgnoreInsn)) { constInsn.add(AFlag.DONT_GENERATE); } return false; } private static boolean canIgnoreInsn(RegisterArg reg) { InsnNode parentInsn = reg.getParentInsn(); if (parentInsn == null || parentInsn.getType() == InsnType.PHI) { return false; } if (reg.isLinkedToOtherSsaVars()) { return false; } return parentInsn.contains(AFlag.DONT_GENERATE); } @SuppressWarnings("RedundantIfStatement") private static boolean canInline(MethodNode mth, RegisterArg arg) { if (arg.contains(AFlag.DONT_INLINE_CONST) || arg.contains(AFlag.DONT_INLINE)) { return false; } InsnNode parentInsn = arg.getParentInsn(); if (parentInsn == null) { return false; } if (parentInsn.contains(AFlag.DONT_GENERATE)) { return false; } if (arg.isLinkedToOtherSsaVars() && !arg.getSVar().isUsedInPhi()) { // don't inline vars used in finally block return false; } if (parentInsn.getType() == InsnType.CONSTRUCTOR) { // don't inline into anonymous call if it can be inlined later MethodNode ctrMth = mth.root().getMethodUtils().resolveMethod((ConstructorInsn) parentInsn); if (ctrMth != null && (ctrMth.contains(AFlag.METHOD_CANDIDATE_FOR_INLINE) || ctrMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR))) { return false; } } return true; } private static boolean replaceArg(MethodNode mth, RegisterArg arg, InsnArg constArg, InsnNode constInsn) { InsnNode useInsn = arg.getParentInsn(); if (useInsn == null) { return false; } InsnType insnType = useInsn.getType(); if (insnType == InsnType.PHI) { return false; } if (constArg.isLiteral()) { long literal = ((LiteralArg) constArg).getLiteral(); ArgType argType = arg.getType(); if (argType == ArgType.UNKNOWN) { argType = arg.getInitType(); } if (argType.isObject() && literal != 0) { argType = ArgType.NARROW_NUMBERS; } LiteralArg litArg = InsnArg.lit(literal, argType); litArg.copyAttributesFrom(constArg); if (!useInsn.replaceArg(arg, litArg)) { return false; } // arg replaced, made some optimizations IFieldInfoRef fieldNode = null; ArgType litArgType = litArg.getType(); if (litArgType.isTypeKnown()) { fieldNode = mth.getParentClass().getConstFieldByLiteralArg(litArg); } else if (litArgType.contains(PrimitiveType.INT)) { fieldNode = mth.getParentClass().getConstField((int) literal, false); } if (fieldNode != null) { IndexInsnNode sgetInsn = new IndexInsnNode(InsnType.SGET, fieldNode.getFieldInfo(), 0); if (litArg.wrapInstruction(mth, sgetInsn) != null) { ModVisitor.addFieldUsage(fieldNode, mth); } } else { addExplicitCast(useInsn, litArg); } } else { if (!useInsn.replaceArg(arg, constArg.duplicate())) { return false; } } useInsn.inheritMetadata(constInsn); return true; } private static void addExplicitCast(InsnNode insn, LiteralArg arg) { if (insn instanceof BaseInvokeNode) { BaseInvokeNode callInsn = (BaseInvokeNode) insn; MethodInfo callMth = callInsn.getCallMth(); if (callInsn.getInstanceArg() == arg) { // instance arg is null, force cast if (!arg.isZeroLiteral()) { throw new JadxRuntimeException("Unexpected instance arg in invoke"); } ArgType castType = callMth.getDeclClass().getType(); InsnNode castInsn = new IndexInsnNode(InsnType.CAST, castType, 1); castInsn.addArg(arg); castInsn.add(AFlag.EXPLICIT_CAST); InsnArg wrapCast = InsnArg.wrapArg(castInsn); wrapCast.setType(castType); insn.replaceArg(arg, wrapCast); } else { int offset = callInsn.getFirstArgOffset(); int argIndex = insn.getArgIndex(arg); ArgType argType = callMth.getArgumentsTypes().get(argIndex - offset); if (argType.isPrimitive()) { arg.setType(argType); if (argType.equals(ArgType.BYTE)) { arg.add(AFlag.EXPLICIT_PRIMITIVE_TYPE); } } } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ConstructorVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.core.codegen.TypeGen; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.ssa.SSATransform; import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnRemover; import jadx.core.utils.exceptions.JadxRuntimeException; @JadxVisitor( name = "ConstructorVisitor", desc = "Replace invoke with constructor call", runAfter = { SSATransform.class, MoveInlineVisitor.class }, runBefore = TypeInferenceVisitor.class ) public class ConstructorVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) { if (mth.isNoCode()) { return; } if (replaceInvoke(mth)) { MoveInlineVisitor.moveInline(mth); } } private static boolean replaceInvoke(MethodNode mth) { boolean replaced = false; InsnRemover remover = new InsnRemover(mth); for (BlockNode block : mth.getBasicBlocks()) { remover.setBlock(block); int size = block.getInstructions().size(); for (int i = 0; i < size; i++) { InsnNode insn = block.getInstructions().get(i); if (insn.getType() == InsnType.INVOKE) { replaced |= processInvoke(mth, block, i, remover); } } remover.perform(); } return replaced; } private static boolean processInvoke(MethodNode mth, BlockNode block, int indexInBlock, InsnRemover remover) { InvokeNode inv = (InvokeNode) block.getInstructions().get(indexInBlock); MethodInfo callMth = inv.getCallMth(); if (!callMth.isConstructor()) { return false; } ArgType instType = searchInstanceType(inv); if (instType != null && !instType.equals(callMth.getDeclClass().getType())) { ClassInfo instCls = ClassInfo.fromType(mth.root(), instType); callMth = MethodInfo.fromDetails(mth.root(), instCls, callMth.getName(), callMth.getArgumentsTypes(), callMth.getReturnType()); } ConstructorInsn co = new ConstructorInsn(mth, inv, callMth); if (canRemoveConstructor(mth, co)) { remover.addAndUnbind(inv); return false; } co.inheritMetadata(inv); RegisterArg instanceArg = (RegisterArg) inv.getArg(0); instanceArg.getSVar().removeUse(instanceArg); if (co.isNewInstance()) { InsnNode assignInsn = instanceArg.getAssignInsn(); if (assignInsn != null) { if (assignInsn.getType() == InsnType.CONSTRUCTOR) { // arg already used in another constructor instruction // insert new PHI insn to merge these branched constructors results instanceArg = insertPhiInsn(mth, block, instanceArg, ((ConstructorInsn) assignInsn)); } else { InsnNode newInstInsn = removeAssignChain(mth, assignInsn, remover, InsnType.NEW_INSTANCE); if (newInstInsn != null) { co.inheritMetadata(newInstInsn); newInstInsn.add(AFlag.REMOVE); remover.addWithoutUnbind(newInstInsn); } } } // convert instance arg from 'use' to 'assign' co.setResult(instanceArg.duplicate()); } co.rebindArgs(); ConstructorInsn replace = processConstructor(mth, co); if (replace != null) { remover.addAndUnbind(co); BlockUtils.replaceInsn(mth, block, indexInBlock, replace); } else { BlockUtils.replaceInsn(mth, block, indexInBlock, co); } return true; } private static @Nullable ArgType searchInstanceType(InvokeNode inv) { InsnArg instanceArg = inv.getInstanceArg(); if (instanceArg == null || !instanceArg.isRegister()) { return null; } InsnNode assignInsn = ((RegisterArg) instanceArg).getSVar().getAssignInsn(); if (assignInsn == null || assignInsn.getType() != InsnType.NEW_INSTANCE) { return null; } return ((IndexInsnNode) assignInsn).getIndexAsType(); } private static RegisterArg insertPhiInsn(MethodNode mth, BlockNode curBlock, RegisterArg instArg, ConstructorInsn otherCtr) { BlockNode otherBlock = BlockUtils.getBlockByInsn(mth, otherCtr); if (otherBlock == null) { throw new JadxRuntimeException("Block not found by insn: " + otherCtr); } BlockNode crossBlock = BlockUtils.getPathCross(mth, curBlock, otherBlock); if (crossBlock == null) { // no path cross => PHI insn not needed // use new SSA var on usage from current path RegisterArg newResArg = instArg.duplicateWithNewSSAVar(mth); List pathBlocks = BlockUtils.collectAllSuccessors(mth, curBlock, true); for (RegisterArg useReg : instArg.getSVar().getUseList()) { InsnNode parentInsn = useReg.getParentInsn(); if (parentInsn != null) { BlockNode useBlock = BlockUtils.getBlockByInsn(mth, parentInsn, pathBlocks); if (useBlock != null) { parentInsn.replaceArg(useReg, newResArg.duplicate()); } } } return newResArg; } RegisterArg newResArg = instArg.duplicateWithNewSSAVar(mth); RegisterArg useArg = otherCtr.getResult(); RegisterArg otherResArg = useArg.duplicateWithNewSSAVar(mth); PhiInsn phiInsn = SSATransform.addPhi(mth, crossBlock, useArg.getRegNum()); phiInsn.setResult(useArg.duplicate()); phiInsn.bindArg(newResArg.duplicate(), BlockUtils.getPrevBlockOnPath(mth, crossBlock, curBlock)); phiInsn.bindArg(otherResArg.duplicate(), BlockUtils.getPrevBlockOnPath(mth, crossBlock, otherBlock)); phiInsn.rebindArgs(); otherCtr.setResult(otherResArg.duplicate()); otherCtr.rebindArgs(); return newResArg; } private static boolean canRemoveConstructor(MethodNode mth, ConstructorInsn co) { ClassNode parentClass = mth.getParentClass(); if (co.isSuper() && co.getArgsCount() == 0) { return true; } if (co.isThis() && co.getArgsCount() == 0) { MethodNode defCo = parentClass.searchMethodByShortId(co.getCallMth().getShortId()); if (defCo == null || defCo.isNoCode()) { // default constructor not implemented return true; } } // remove super() call in instance initializer return parentClass.isAnonymous() && mth.isDefaultConstructor() && co.isSuper(); } /** * Replace call of synthetic constructor with all 'null' args * to a non-synthetic or default constructor if possible. * * @return insn for replacement or null if replace not needed or not possible. */ @Nullable private static ConstructorInsn processConstructor(MethodNode mth, ConstructorInsn co) { MethodNode callMth = mth.root().resolveMethod(co.getCallMth()); if (callMth == null || !callMth.getAccessFlags().isSynthetic() || !allArgsNull(co)) { return null; } ClassNode classNode = mth.root().resolveClass(callMth.getParentClass().getClassInfo()); if (classNode == null) { return null; } RegisterArg instanceArg = co.getResult(); if (instanceArg == null) { return null; } boolean passThis = instanceArg.isThis(); String ctrId = "(" + (passThis ? TypeGen.signature(instanceArg.getInitType()) : "") + ")V"; MethodNode defCtr = classNode.searchMethodByShortId(ctrId); if (defCtr == null || defCtr.equals(callMth) || defCtr.getAccessFlags().isSynthetic()) { return null; } ConstructorInsn newInsn = new ConstructorInsn(defCtr.getMethodInfo(), co.getCallType()); newInsn.setResult(co.getResult().duplicate()); newInsn.inheritMetadata(co); return newInsn; } private static boolean allArgsNull(ConstructorInsn insn) { for (InsnArg insnArg : insn.getArguments()) { if (insnArg.isLiteral()) { LiteralArg lit = (LiteralArg) insnArg; if (lit.getLiteral() != 0) { return false; } } else { return false; } } return true; } /** * Remove instructions on 'move' chain until instruction with type 'insnType' */ private static InsnNode removeAssignChain(MethodNode mth, InsnNode insn, InsnRemover remover, InsnType insnType) { if (insn == null) { return null; } InsnType type = insn.getType(); if (type == insnType) { return insn; } if (insn.isAttrStorageEmpty()) { remover.addWithoutUnbind(insn); } else { BlockUtils.replaceInsn(mth, insn, new InsnNode(InsnType.NOP, 0)); } if (type == InsnType.MOVE) { RegisterArg arg = (RegisterArg) insn.getArg(0); return removeAssignChain(mth, arg.getAssignInsn(), remover, insnType); } return null; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/DeboxingVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.regions.variables.ProcessVariables; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.exceptions.JadxException; /** * Remove primitives boxing * i.e convert 'Integer.valueOf(1)' to '1' */ @JadxVisitor( name = "DeboxingVisitor", desc = "Remove primitives boxing", runBefore = { CodeShrinkVisitor.class, ProcessVariables.class } ) public class DeboxingVisitor extends AbstractVisitor { private Set valueOfMths; @Override public void init(RootNode root) { valueOfMths = new HashSet<>(); valueOfMths.add(valueOfMth(root, ArgType.INT, "java.lang.Integer")); valueOfMths.add(valueOfMth(root, ArgType.BOOLEAN, "java.lang.Boolean")); valueOfMths.add(valueOfMth(root, ArgType.BYTE, "java.lang.Byte")); valueOfMths.add(valueOfMth(root, ArgType.SHORT, "java.lang.Short")); valueOfMths.add(valueOfMth(root, ArgType.CHAR, "java.lang.Character")); valueOfMths.add(valueOfMth(root, ArgType.LONG, "java.lang.Long")); } private static MethodInfo valueOfMth(RootNode root, ArgType argType, String clsName) { ArgType boxType = ArgType.object(clsName); ClassInfo boxCls = ClassInfo.fromType(root, boxType); return MethodInfo.fromDetails(root, boxCls, "valueOf", Collections.singletonList(argType), boxType); } @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } boolean replaced = false; for (BlockNode blockNode : mth.getBasicBlocks()) { List insnList = blockNode.getInstructions(); int count = insnList.size(); for (int i = 0; i < count; i++) { InsnNode insnNode = insnList.get(i); if (insnNode.getType() == InsnType.INVOKE) { InsnNode replaceInsn = checkForReplace(((InvokeNode) insnNode)); if (replaceInsn != null) { BlockUtils.replaceInsn(mth, blockNode, i, replaceInsn); replaced = true; } } } } if (replaced) { ConstInlineVisitor.process(mth); } } private InsnNode checkForReplace(InvokeNode insnNode) { if (insnNode.getInvokeType() != InvokeType.STATIC || insnNode.getResult() == null) { return null; } MethodInfo callMth = insnNode.getCallMth(); if (valueOfMths.contains(callMth)) { RegisterArg resArg = insnNode.getResult(); InsnArg arg = insnNode.getArg(0); if (arg.isLiteral()) { ArgType primitiveType = callMth.getArgumentsTypes().get(0); ArgType boxType = callMth.getReturnType(); if (isNeedExplicitCast(resArg, primitiveType, boxType)) { arg.add(AFlag.EXPLICIT_PRIMITIVE_TYPE); } arg.setType(primitiveType); boolean forbidInline; if (canChangeTypeToPrimitive(resArg, boxType)) { resArg.setType(primitiveType); forbidInline = false; } else { forbidInline = true; } InsnNode constInsn = new InsnNode(InsnType.CONST, 1); constInsn.addArg(arg); constInsn.setResult(resArg); if (forbidInline) { constInsn.add(AFlag.DONT_INLINE); } return constInsn; } } return null; } private boolean isNeedExplicitCast(RegisterArg resArg, ArgType primitiveType, ArgType boxType) { if (primitiveType == ArgType.LONG) { return true; } if (primitiveType != ArgType.INT) { Set useTypes = collectUseTypes(resArg); useTypes.add(resArg.getType()); useTypes.remove(boxType); useTypes.remove(primitiveType); return !useTypes.isEmpty(); } return false; } private boolean canChangeTypeToPrimitive(RegisterArg arg, ArgType boxType) { for (SSAVar ssaVar : arg.getSVar().getCodeVar().getSsaVars()) { if (ssaVar.isTypeImmutable()) { return false; } InsnNode assignInsn = ssaVar.getAssignInsn(); if (assignInsn == null) { // method arg return false; } InsnType assignInsnType = assignInsn.getType(); if (assignInsnType == InsnType.CONST || assignInsnType == InsnType.MOVE) { if (assignInsn.getArg(0).getType().isObject()) { return false; } } ArgType initType = assignInsn.getResult().getInitType(); if (initType.isObject() && !initType.equals(boxType)) { // some of related vars have another object type return false; } for (RegisterArg useArg : ssaVar.getUseList()) { InsnNode parentInsn = useArg.getParentInsn(); if (parentInsn == null) { return false; } if (parentInsn.getType() == InsnType.INVOKE) { InvokeNode invokeNode = (InvokeNode) parentInsn; if (useArg.equals(invokeNode.getInstanceArg())) { return false; } } } } return true; } private Set collectUseTypes(RegisterArg arg) { Set types = new HashSet<>(); for (RegisterArg useArg : arg.getSVar().getUseList()) { types.add(useArg.getType()); types.add(useArg.getInitType()); } return types; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/DepthTraversal.java ================================================ package jadx.core.dex.visitors; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; public class DepthTraversal { public static void visit(IDexTreeVisitor visitor, ClassNode cls) { try { if (visitor.visit(cls)) { cls.getInnerClasses().forEach(inCls -> visit(visitor, inCls)); cls.getMethods().forEach(mth -> visit(visitor, mth)); } } catch (StackOverflowError | BootstrapMethodError | Exception e) { cls.addError(e.getClass().getSimpleName() + " in pass: " + visitor.getClass().getSimpleName(), e); } } public static void visit(IDexTreeVisitor visitor, MethodNode mth) { try { if (mth.contains(AType.JADX_ERROR)) { return; } visitor.visit(mth); } catch (StackOverflowError | BootstrapMethodError | Exception e) { mth.addError(e.getClass().getSimpleName() + " in pass: " + visitor.getClass().getSimpleName(), e); } } private DepthTraversal() { } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java ================================================ package jadx.core.dex.visitors; import java.io.File; import java.util.Optional; import java.util.regex.Matcher; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.DotGraphUtils; public class DotGraphVisitor extends AbstractVisitor { private static final String NL = "\\l"; private static final String NLQR = Matcher.quoteReplacement(NL); private static final boolean PRINT_DOMINATORS = false; private static final boolean PRINT_DOMINATORS_INFO = false; private final boolean useRegions; private final boolean rawInsn; // if present, this region and it's children will still be drawn when not in regions mode. private Optional highlightRegion; public static DotGraphVisitor dump() { return new DotGraphVisitor(false, false); } public static DotGraphVisitor dumpRaw() { return new DotGraphVisitor(false, true); } public static DotGraphVisitor dumpRegions() { return new DotGraphVisitor(true, false); } public static DotGraphVisitor dumpRawRegions() { return new DotGraphVisitor(true, true); } /** * Helper function to generate a cfg at a given point showing only one of the regions in the graph. * Intended to be called during a debugging session to produce a CFG with only a region of interest, * with DotGraphVisitor.debugDumpWithRegionHiglight(region).visit(mth); * * @param region the region to show * @return the visitor, to be invoked with `.visit(mth);` */ public static DotGraphVisitor debugDumpWithRegionHighlight(IRegion region) { return new DotGraphVisitor(false, false, Optional.of(region)); } private DotGraphVisitor(boolean useRegions, boolean rawInsn) { this(useRegions, rawInsn, Optional.empty()); } private DotGraphVisitor(boolean useRegions, boolean rawInsn, Optional highlightRegion) { this.useRegions = useRegions; this.rawInsn = rawInsn; this.highlightRegion = highlightRegion; } @Override public String getName() { return "DotGraphVisitor"; } @Override public void visit(MethodNode mth) { if (mth.isNoCode()) { return; } new DotGraphUtils(useRegions, rawInsn, highlightRegion).dumpToFile(mth); } public void save(File dir, MethodNode mth) { if (mth.isNoCode()) { return; } new DotGraphUtils(useRegions, rawInsn, highlightRegion).dumpToFile(mth, dir); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.AccessFlags; import jadx.core.codegen.TypeGen; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.EnumClassAttr; import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField; import jadx.core.dex.attributes.nodes.RenameReasonAttr; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.regions.Region; import jadx.core.dex.visitors.regions.CheckRegions; import jadx.core.dex.visitors.regions.IfRegionVisitor; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.BlockInsnPair; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnUtils; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.InsnUtils.checkInsnType; import static jadx.core.utils.InsnUtils.getSingleArg; import static jadx.core.utils.InsnUtils.getWrappedInsn; @JadxVisitor( name = "EnumVisitor", desc = "Restore enum classes", runAfter = { CodeShrinkVisitor.class, // all possible instructions already inlined ModVisitor.class, ReplaceNewArray.class, // values array normalized IfRegionVisitor.class, // ternary operator inlined CheckRegions.class // regions processing finished }, runBefore = { ExtractFieldInit.class } ) public class EnumVisitor extends AbstractVisitor { private static final String ENUM_SUPER_CONSTRUCTOR_ID = "java.lang.Enum.(Ljava/lang/String;I)V"; private MethodInfo enumValueOfMth; private MethodInfo cloneMth; @Override public void init(RootNode root) { enumValueOfMth = MethodInfo.fromDetails( root, ClassInfo.fromType(root, ArgType.ENUM), "valueOf", Arrays.asList(ArgType.CLASS, ArgType.STRING), ArgType.ENUM); cloneMth = MethodInfo.fromDetails(root, ClassInfo.fromType(root, ArgType.OBJECT), "clone", Collections.emptyList(), ArgType.OBJECT); } @Override public boolean visit(ClassNode cls) throws JadxException { if (cls.isEnum()) { boolean converted; try { converted = convertToEnum(cls); } catch (Exception e) { cls.addWarnComment("Enum visitor error", e); converted = false; } if (!converted) { AccessInfo accessFlags = cls.getAccessFlags(); if (accessFlags.isEnum()) { cls.setAccessFlags(accessFlags.remove(AccessFlags.ENUM)); cls.addWarnComment("Failed to restore enum class, 'enum' modifier and super class removed"); } } } return true; } private boolean convertToEnum(ClassNode cls) { ArgType superType = cls.getSuperClass(); if (superType != null && superType.getObject().equals(ArgType.ENUM.getObject())) { cls.add(AFlag.REMOVE_SUPER_CLASS); } MethodNode classInitMth = cls.getClassInitMth(); if (classInitMth == null) { cls.addWarnComment("Enum class init method not found"); return false; } Region staticRegion = classInitMth.getRegion(); if (staticRegion == null || classInitMth.getBasicBlocks().isEmpty()) { return false; } // collect blocks on linear part of static method (ignore branching on method end) List staticBlocks = new ArrayList<>(); for (IContainer subBlock : staticRegion.getSubBlocks()) { if (subBlock instanceof BlockNode) { staticBlocks.add((BlockNode) subBlock); } else { break; } } if (staticBlocks.isEmpty()) { cls.addWarnComment("Unexpected branching in enum static init block"); return false; } EnumData data = new EnumData(cls, classInitMth, staticBlocks); if (!searchValuesField(data)) { return false; } List enumFields = null; InsnArg arrArg = data.valuesInitInsn.getArg(0); if (arrArg.isInsnWrap()) { InsnNode wrappedInsn = ((InsnWrapArg) arrArg).getWrapInsn(); enumFields = extractEnumFieldsFromInsn(data, wrappedInsn); } else if (arrArg.isRegister()) { // Kotlin 1.9+ $ENTRIES pattern: array register has multiple uses, // preventing CodeShrinkVisitor from inlining into the SPUT RegisterArg regArg = (RegisterArg) arrArg; InsnNode assignInsn = regArg.getAssignInsn(); if (assignInsn != null) { enumFields = extractEnumFieldsFromInsn(data, assignInsn); } } if (enumFields == null) { cls.addWarnComment("Unknown enum class pattern. Please report as an issue!"); return false; } data.toRemove.add(data.valuesInitInsn); // all checks complete, perform transform EnumClassAttr attr = new EnumClassAttr(enumFields); attr.setStaticMethod(classInitMth); cls.addAttr(attr); for (EnumField enumField : attr.getFields()) { FieldNode fieldNode = enumField.getField(); String name = enumField.getNameStr(); if (name != null && !fieldNode.getAlias().equals(name) && NameMapper.isValidAndPrintable(name) && cls.root().getArgs().isRenameValid()) { fieldNode.getFieldInfo().setAlias(name); } fieldNode.add(AFlag.DONT_GENERATE); processConstructorInsn(data, enumField, classInitMth); } data.valuesField.add(AFlag.DONT_GENERATE); InsnRemover.removeAllAndUnbind(classInitMth, data.toRemove); if (classInitMth.countInsns() == 0) { classInitMth.add(AFlag.DONT_GENERATE); } else if (!data.toRemove.isEmpty()) { CodeShrinkVisitor.shrinkMethod(classInitMth); } removeEnumMethods(cls, data.valuesField); fixAccessFlags(cls); cls.add(AFlag.CONVERTED_ENUM); return true; } private static void fixAccessFlags(ClassNode cls) { // remove invalid access flags cls.setAccessFlags(cls.getAccessFlags() .remove(AccessFlags.FINAL) .remove(AccessFlags.ABSTRACT) .remove(AccessFlags.STATIC)); for (MethodNode mth : cls.getMethods()) { if (mth.getMethodInfo().isConstructor()) { mth.setAccessFlags(mth.getAccessFlags().remove(AccessInfo.VISIBILITY_FLAGS)); } } } /** * Search "$VALUES" field (holds all enum values) */ private boolean searchValuesField(EnumData data) { ArgType clsType = data.cls.getClassInfo().getType(); List valuesCandidates = data.cls.getFields().stream() .filter(f -> f.getAccessFlags().isStatic()) .filter(f -> f.getType().isArray()) .filter(f -> Objects.equals(f.getType().getArrayRootElement(), clsType)) .collect(Collectors.toList()); if (valuesCandidates.isEmpty()) { data.cls.addWarnComment("$VALUES field not found"); return false; } if (valuesCandidates.size() > 1) { valuesCandidates.removeIf(f -> !f.getAccessFlags().isSynthetic()); } if (valuesCandidates.size() > 1) { Optional valuesOpt = valuesCandidates.stream().filter(f -> f.getName().equals("$VALUES")).findAny(); if (valuesOpt.isPresent()) { valuesCandidates.clear(); valuesCandidates.add(valuesOpt.get()); } } if (valuesCandidates.size() != 1) { data.cls.addWarnComment("Found several \"values\" enum fields: " + valuesCandidates); return false; } data.valuesField = valuesCandidates.get(0); // search "$VALUES" array init and collect enum fields BlockInsnPair valuesInitPair = getValuesInitInsn(data); if (valuesInitPair == null) { return false; } data.valuesInitInsn = valuesInitPair.getInsn(); return true; } private void processConstructorInsn(EnumData data, EnumField enumField, MethodNode classInitMth) { ConstructorInsn co = enumField.getConstrInsn(); ClassInfo enumClsInfo = co.getClassType(); if (!enumClsInfo.equals(data.cls.getClassInfo())) { ClassNode enumCls = data.cls.root().resolveClass(enumClsInfo); if (enumCls != null) { processEnumCls(data.cls, enumField, enumCls); } } MethodNode ctrMth = data.cls.root().resolveMethod(co.getCallMth()); if (ctrMth != null) { markArgsForSkip(ctrMth); } RegisterArg coResArg = co.getResult(); if (coResArg == null || coResArg.getSVar().getUseList().size() <= 2) { data.toRemove.add(co); } else { boolean varUseFound = coResArg.getSVar().getUseList().stream() .anyMatch(useArg -> !data.toRemove.contains(useArg.getParentInsn())); if (varUseFound) { // constructor result used in other places -> replace constructor with enum field get (SGET) IndexInsnNode enumGet = new IndexInsnNode(InsnType.SGET, enumField.getField().getFieldInfo(), 0); enumGet.setResult(coResArg.duplicate()); BlockUtils.replaceInsn(classInitMth, co, enumGet); } } } @Nullable private List extractEnumFieldsFromInsn(EnumData enumData, InsnNode wrappedInsn) { switch (wrappedInsn.getType()) { case FILLED_NEW_ARRAY: return extractEnumFieldsFromFilledArray(enumData, wrappedInsn); case INVOKE: // handle redirection of values array fill (added in java 15) return extractEnumFieldsFromInvoke(enumData, (InvokeNode) wrappedInsn); case NEW_ARRAY: InsnArg arg = wrappedInsn.getArg(0); if (arg.isZeroLiteral()) { // empty enum return Collections.emptyList(); } return null; default: return null; } } private List extractEnumFieldsFromInvoke(EnumData enumData, InvokeNode invokeNode) { MethodInfo callMth = invokeNode.getCallMth(); MethodNode valuesMth = enumData.cls.root().resolveMethod(callMth); if (valuesMth == null || valuesMth.isVoidReturn()) { return null; } BlockNode returnBlock = Utils.getOne(valuesMth.getPreExitBlocks()); InsnNode returnInsn = BlockUtils.getLastInsn(returnBlock); InsnNode wrappedInsn = getWrappedInsn(getSingleArg(returnInsn)); if (wrappedInsn == null) { return null; } List enumFields = extractEnumFieldsFromInsn(enumData, wrappedInsn); if (enumFields != null && ListUtils.isSingleElement(valuesMth.getUseIn(), enumData.classInitMth)) { valuesMth.add(AFlag.DONT_GENERATE); if (valuesMth.getName().equals("$values")) { // Kotlin synthetic method used for init values // rename to actual values method to use in $ENTRIES init code valuesMth.getMethodInfo().setAlias("values"); } } return enumFields; } private BlockInsnPair getValuesInitInsn(EnumData data) { FieldInfo searchField = data.valuesField.getFieldInfo(); for (BlockNode blockNode : data.staticBlocks) { for (InsnNode insn : blockNode.getInstructions()) { if (insn.getType() == InsnType.SPUT) { IndexInsnNode indexInsnNode = (IndexInsnNode) insn; FieldInfo f = (FieldInfo) indexInsnNode.getIndex(); if (f.equals(searchField)) { return new BlockInsnPair(blockNode, indexInsnNode); } } } } return null; } private List extractEnumFieldsFromFilledArray(EnumData enumData, InsnNode arrFillInsn) { List enumFields = new ArrayList<>(); for (InsnArg arg : arrFillInsn.getArguments()) { EnumField field = null; if (arg.isInsnWrap()) { InsnNode wrappedInsn = ((InsnWrapArg) arg).getWrapInsn(); field = processEnumFieldByWrappedInsn(enumData, wrappedInsn); } else if (arg.isRegister()) { field = processEnumFieldByRegister(enumData, (RegisterArg) arg); } if (field == null) { return null; } enumFields.add(field); } enumData.toRemove.add(arrFillInsn); return enumFields; } private EnumField processEnumFieldByWrappedInsn(EnumData data, InsnNode wrappedInsn) { if (wrappedInsn.getType() == InsnType.SGET) { return processEnumFieldByField(data, wrappedInsn); } ConstructorInsn constructorInsn = castConstructorInsn(wrappedInsn); if (constructorInsn != null) { FieldNode enumFieldNode = createFakeField(data.cls, "EF" + constructorInsn.getOffset()); data.cls.addField(enumFieldNode); return createEnumFieldByConstructor(data, enumFieldNode, constructorInsn); } return null; } @Nullable private EnumField processEnumFieldByField(EnumData data, InsnNode sgetInsn) { if (sgetInsn.getType() != InsnType.SGET) { return null; } FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) sgetInsn).getIndex(); FieldNode enumFieldNode = data.cls.searchField(fieldInfo); if (enumFieldNode == null) { return null; } InsnNode sputInsn = searchFieldPutInsn(data, enumFieldNode); if (sputInsn == null) { return null; } ConstructorInsn co = getConstructorInsn(sputInsn); if (co == null) { return null; } RegisterArg sgetResult = sgetInsn.getResult(); if (sgetResult == null || sgetResult.getSVar().getUseCount() == 1) { data.toRemove.add(sgetInsn); } data.toRemove.add(sputInsn); return createEnumFieldByConstructor(data, enumFieldNode, co); } @Nullable private EnumField processEnumFieldByRegister(EnumData data, RegisterArg arg) { InsnNode assignInsn = arg.getAssignInsn(); if (assignInsn != null && assignInsn.getType() == InsnType.SGET) { return processEnumFieldByField(data, assignInsn); } SSAVar ssaVar = arg.getSVar(); if (ssaVar.getUseCount() == 0) { return null; } InsnNode constrInsn = ssaVar.getAssign().getParentInsn(); if (constrInsn == null || constrInsn.getType() != InsnType.CONSTRUCTOR) { return null; } FieldNode enumFieldNode = searchEnumField(data, ssaVar); if (enumFieldNode == null) { enumFieldNode = createFakeField(data.cls, "EF" + arg.getRegNum()); data.cls.addField(enumFieldNode); } return createEnumFieldByConstructor(data, enumFieldNode, (ConstructorInsn) constrInsn); } private FieldNode createFakeField(ClassNode cls, String name) { FieldNode enumFieldNode; FieldInfo fldInfo = FieldInfo.from(cls.root(), cls.getClassInfo(), name, cls.getType()); enumFieldNode = new FieldNode(cls, fldInfo, 0); enumFieldNode.add(AFlag.SYNTHETIC); enumFieldNode.addInfoComment("Fake field, exist only in values array"); return enumFieldNode; } @Nullable private FieldNode searchEnumField(EnumData data, SSAVar ssaVar) { InsnNode sputInsn = ssaVar.getUseList().get(0).getParentInsn(); if (sputInsn == null || sputInsn.getType() != InsnType.SPUT) { return null; } FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) sputInsn).getIndex(); FieldNode enumFieldNode = data.cls.searchField(fieldInfo); if (enumFieldNode == null) { return null; } data.toRemove.add(sputInsn); return enumFieldNode; } private EnumField createEnumFieldByConstructor(EnumData data, FieldNode enumFieldNode, ConstructorInsn co) { // usually constructor signature is '(Ljava/lang/String;I)V', sometimes one or both args can // be omitted ClassNode cls = data.cls; ClassInfo clsInfo = co.getClassType(); ClassNode constrCls = cls.root().resolveClass(clsInfo); if (constrCls == null) { return null; } if (constrCls.equals(cls)) { // allow same class } else if (constrCls.contains(AType.ANONYMOUS_CLASS)) { // allow external class already marked as anonymous } else { return null; } MethodNode ctrMth = cls.root().resolveMethod(co.getCallMth()); if (ctrMth == null) { return null; } // usually constructor signature is '(Ljava/lang/String;I)V' // sometimes one or both args can be inlined or omitted String nameStr = null; if (co.getArgsCount() == 0) { ConstructorInsn ctrInsn = searchEnumSuperCtrInsn(ctrMth); if (ctrInsn != null && ctrInsn.getArgsCount() != 0) { nameStr = getConstString(ctrMth.root(), ctrInsn.getArg(0)); } } else { nameStr = getConstString(cls.root(), co.getArg(0)); // verify and try to inline additional constructor args List regs = new ArrayList<>(); co.getRegisterArgs(regs); if (!regs.isEmpty()) { ConstructorInsn replacedCo = inlineExternalRegs(data, co); if (replacedCo == null) { throw new JadxRuntimeException("Init of enum field '" + enumFieldNode.getName() + "' uses external variables"); } data.toRemove.add(co); co = replacedCo; } } return new EnumField(enumFieldNode, co, nameStr); } private @Nullable ConstructorInsn searchEnumSuperCtrInsn(MethodNode ctrMth) { for (BlockNode block : ctrMth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { if (insn.getType() == InsnType.CONSTRUCTOR) { ConstructorInsn ctrCall = (ConstructorInsn) insn; if (ctrCall.isSuper() && ctrCall.getArgsCount() != 0 && ctrCall.getCallMth().getRawFullId().equals(ENUM_SUPER_CONSTRUCTOR_ID)) { return ctrCall; } } } } return null; } private ConstructorInsn inlineExternalRegs(EnumData data, ConstructorInsn co) { ConstructorInsn resCo = co.copyWithoutResult(); List regs = new ArrayList<>(); resCo.getRegisterArgs(regs); for (RegisterArg reg : regs) { FieldInfo enumField = checkExternalRegUsage(data, reg); if (enumField == null) { return null; } InsnNode enumUse = new IndexInsnNode(InsnType.SGET, enumField, 0); boolean replaced = resCo.replaceArg(reg, InsnArg.wrapArg(enumUse)); if (!replaced) { return null; } } return resCo; } private static FieldInfo checkExternalRegUsage(EnumData data, RegisterArg reg) { ClassNode cls = data.cls; SSAVar ssaVar = reg.getSVar(); InsnNode assignInsn = checkInsnType(ssaVar.getAssignInsn(), InsnType.CONSTRUCTOR); if (assignInsn == null || !((ConstructorInsn) assignInsn).getClassType().equals(cls.getClassInfo())) { return null; } FieldInfo enumField = null; for (RegisterArg useArg : ssaVar.getUseList()) { InsnNode useInsn = useArg.getParentInsn(); if (useInsn == null) { return null; } switch (useInsn.getType()) { case SPUT: { FieldInfo field = (FieldInfo) ((IndexInsnNode) useInsn).getIndex(); if (!field.getDeclClass().equals(cls.getClassInfo()) || !field.getType().equals(cls.getType())) { return null; } enumField = field; break; } case CONSTRUCTOR: { ConstructorInsn useCo = (ConstructorInsn) useInsn; if (!useCo.getClassType().equals(cls.getClassInfo())) { return null; } break; } case FILLED_NEW_ARRAY: { // allow usage in values init instruction InsnArg valuesArg = data.valuesInitInsn.getArg(0); InsnNode unwrapped = valuesArg.unwrap(); if (unwrapped != null) { if (unwrapped != useInsn) { return null; } } else if (valuesArg.isRegister()) { InsnNode valuesAssign = ((RegisterArg) valuesArg).getAssignInsn(); if (valuesAssign != useInsn) { return null; } } else { return null; } break; } default: return null; } } if (enumField != null) { data.toRemove.add(assignInsn); } return enumField; } @Nullable private InsnNode searchFieldPutInsn(EnumData data, FieldNode enumFieldNode) { for (BlockNode block : data.staticBlocks) { for (InsnNode sputInsn : block.getInstructions()) { if (sputInsn != null && sputInsn.getType() == InsnType.SPUT) { FieldInfo f = (FieldInfo) ((IndexInsnNode) sputInsn).getIndex(); FieldNode fieldNode = data.cls.searchField(f); if (Objects.equals(fieldNode, enumFieldNode)) { return sputInsn; } } } } return null; } private void removeEnumMethods(ClassNode cls, FieldNode valuesField) { ArgType clsType = cls.getClassInfo().getType(); String valuesMethodShortId = "values()" + TypeGen.signature(ArgType.array(clsType)); MethodNode valuesMethod = null; // remove compiler generated methods for (MethodNode mth : cls.getMethods()) { MethodInfo mi = mth.getMethodInfo(); if (mi.isClassInit() || mth.isNoCode()) { continue; } String shortId = mi.getShortId(); if (mi.isConstructor()) { markArgsForSkip(mth); // remove super constructor call ConstructorInsn superCtrInsn = searchEnumSuperCtrInsn(mth); if (superCtrInsn != null) { superCtrInsn.add(AFlag.DONT_GENERATE); InsnRemover.remove(mth, superCtrInsn); } if (isDefaultConstructor(mth, shortId)) { mth.add(AFlag.DONT_GENERATE); } } else if (mi.getShortId().equals(valuesMethodShortId)) { if (isValuesMethod(mth, clsType)) { valuesMethod = mth; mth.add(AFlag.DONT_GENERATE); } else { // custom values method => rename to resolve conflict with enum method mth.getMethodInfo().setAlias("valuesCustom"); mth.addAttr(new RenameReasonAttr(mth).append("to resolve conflict with enum method")); } } else if (isValuesMethod(mth, clsType)) { if (!mth.getMethodInfo().getAlias().equals("values") && !mth.getUseIn().isEmpty()) { // rename to use default values method mth.getMethodInfo().setAlias("values"); mth.addAttr(new RenameReasonAttr(mth).append("to match enum method name")); mth.add(AFlag.DONT_RENAME); } valuesMethod = mth; mth.add(AFlag.DONT_GENERATE); } else if (simpleValueOfMth(mth, clsType)) { mth.add(AFlag.DONT_GENERATE); } } FieldInfo valuesFieldInfo = valuesField.getFieldInfo(); for (MethodNode mth : cls.getMethods()) { // fix access to 'values' field and 'values()' method fixValuesAccess(mth, valuesFieldInfo, clsType, valuesMethod); } } private void markArgsForSkip(MethodNode mth) { // skip first and second args SkipMethodArgsAttr.skipArg(mth, 0); if (mth.getMethodInfo().getArgsCount() > 1) { SkipMethodArgsAttr.skipArg(mth, 1); } } private boolean isDefaultConstructor(MethodNode mth, String shortId) { boolean defaultId = shortId.equals("(Ljava/lang/String;I)V") || shortId.equals("(Ljava/lang/String;)V"); if (defaultId) { // check content return mth.countInsns() == 0; } return false; } // TODO: support other method patterns ??? private boolean isValuesMethod(MethodNode mth, ArgType clsType) { ArgType retType = mth.getReturnType(); if (!retType.isArray() || !retType.getArrayElement().equals(clsType)) { return false; } InsnNode returnInsn = BlockUtils.getOnlyOneInsnFromMth(mth); if (returnInsn == null || returnInsn.getType() != InsnType.RETURN || returnInsn.getArgsCount() != 1) { return false; } InsnNode wrappedInsn = getWrappedInsn(getSingleArg(returnInsn)); IndexInsnNode castInsn = (IndexInsnNode) checkInsnType(wrappedInsn, InsnType.CHECK_CAST); if (castInsn != null && Objects.equals(castInsn.getIndex(), ArgType.array(clsType))) { InvokeNode invokeInsn = (InvokeNode) checkInsnType(getWrappedInsn(getSingleArg(castInsn)), InsnType.INVOKE); return invokeInsn != null && invokeInsn.getCallMth().equals(cloneMth); } return false; } private boolean simpleValueOfMth(MethodNode mth, ArgType clsType) { InsnNode returnInsn = InsnUtils.searchSingleReturnInsn(mth, insn -> insn.getArgsCount() == 1); if (returnInsn == null) { return false; } InsnNode wrappedInsn = getWrappedInsn(getSingleArg(returnInsn)); IndexInsnNode castInsn = (IndexInsnNode) checkInsnType(wrappedInsn, InsnType.CHECK_CAST); if (castInsn != null && Objects.equals(castInsn.getIndex(), clsType)) { InvokeNode invokeInsn = (InvokeNode) checkInsnType(getWrappedInsn(getSingleArg(castInsn)), InsnType.INVOKE); return invokeInsn != null && invokeInsn.getCallMth().equals(enumValueOfMth); } return false; } private void fixValuesAccess(MethodNode mth, FieldInfo valuesFieldInfo, ArgType clsType, @Nullable MethodNode valuesMethod) { MethodInfo mi = mth.getMethodInfo(); if (mi.isConstructor() || mi.isClassInit() || mth.isNoCode() || mth == valuesMethod) { return; } // search value field usage Predicate insnTest = insn -> Objects.equals(((IndexInsnNode) insn).getIndex(), valuesFieldInfo); InsnNode useInsn = InsnUtils.searchInsn(mth, InsnType.SGET, insnTest); if (useInsn == null) { return; } // replace 'values' field with 'values()' method InsnUtils.replaceInsns(mth, insn -> { if (insn.getType() == InsnType.SGET && insnTest.test(insn)) { MethodInfo valueMth = valuesMethod == null ? getValueMthInfo(mth.root(), clsType) : valuesMethod.getMethodInfo(); InvokeNode invokeNode = new InvokeNode(valueMth, InvokeType.STATIC, 0); invokeNode.setResult(insn.getResult()); if (valuesMethod == null) { // forcing enum method (can overlap and get renamed by custom method) invokeNode.add(AFlag.FORCE_RAW_NAME); } mth.addDebugComment("Replace access to removed values field (" + valuesFieldInfo.getName() + ") with 'values()' method"); return invokeNode; } return null; }); } private MethodInfo getValueMthInfo(RootNode root, ArgType clsType) { return MethodInfo.fromDetails(root, ClassInfo.fromType(root, clsType), "values", Collections.emptyList(), ArgType.array(clsType)); } private static void processEnumCls(ClassNode cls, EnumField field, ClassNode innerCls) { // remove constructor, because it is anonymous class for (MethodNode innerMth : innerCls.getMethods()) { if (innerMth.getAccessFlags().isConstructor()) { innerMth.add(AFlag.DONT_GENERATE); } } field.setCls(innerCls); if (!innerCls.getParentClass().equals(cls)) { // not inner cls.addInlinedClass(innerCls); innerCls.add(AFlag.DONT_GENERATE); } } private ConstructorInsn getConstructorInsn(InsnNode insn) { if (insn.getArgsCount() != 1) { return null; } InsnArg arg = insn.getArg(0); if (arg.isInsnWrap()) { return castConstructorInsn(((InsnWrapArg) arg).getWrapInsn()); } if (arg.isRegister()) { return castConstructorInsn(((RegisterArg) arg).getAssignInsn()); } return null; } @Nullable private ConstructorInsn castConstructorInsn(InsnNode coCandidate) { if (coCandidate != null && coCandidate.getType() == InsnType.CONSTRUCTOR) { return (ConstructorInsn) coCandidate; } return null; } private String getConstString(RootNode root, InsnArg arg) { if (arg.isInsnWrap()) { InsnNode constInsn = ((InsnWrapArg) arg).getWrapInsn(); Object constValue = InsnUtils.getConstValueByInsn(root, constInsn); if (constValue instanceof String) { return (String) constValue; } } return null; } private static class EnumData { final ClassNode cls; final MethodNode classInitMth; final List staticBlocks; final List toRemove = new ArrayList<>(); FieldNode valuesField; InsnNode valuesInitInsn; public EnumData(ClassNode cls, MethodNode classInitMth, List staticBlocks) { this.cls = cls; this.classInitMth = classInitMth; this.staticBlocks = staticBlocks; } } @Override public String getName() { return "EnumVisitor"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ExtractFieldInit.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.FieldInitInsnAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnRemover; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "ExtractFieldInit", desc = "Move duplicated field initialization from constructors", runAfter = ModVisitor.class, runBefore = ClassModifier.class ) public class ExtractFieldInit extends AbstractVisitor { @Override public boolean visit(ClassNode cls) throws JadxException { for (ClassNode inner : cls.getInnerClasses()) { visit(inner); } if (!cls.getFields().isEmpty()) { moveStaticFieldsInit(cls); moveCommonFieldsInit(cls); } return false; } private static final class FieldInitInfo { final FieldNode fieldNode; final IndexInsnNode putInsn; final boolean canMove; public FieldInitInfo(FieldNode fieldNode, IndexInsnNode putInsn, boolean canMove) { this.fieldNode = fieldNode; this.putInsn = putInsn; this.canMove = canMove; } } private static final class ConstructorInitInfo { final MethodNode constructorMth; final List fieldInits; private ConstructorInitInfo(MethodNode constructorMth, List fieldInits) { this.constructorMth = constructorMth; this.fieldInits = fieldInits; } } private static void moveStaticFieldsInit(ClassNode cls) { MethodNode classInitMth = cls.getClassInitMth(); if (classInitMth == null || !classInitMth.getAccessFlags().isStatic() || classInitMth.isNoCode() || classInitMth.getBasicBlocks() == null) { return; } if (ListUtils.noneMatch(cls.getFields(), FieldNode::isStatic)) { return; } while (processStaticFields(cls, classInitMth)) { // sometimes instructions moved to field init prevent from vars inline -> inline and try again CodeShrinkVisitor.shrinkMethod(classInitMth); } } private static boolean processStaticFields(ClassNode cls, MethodNode classInitMth) { List inits = collectFieldsInit(cls, classInitMth, InsnType.SPUT); if (inits.isEmpty()) { return false; } // ignore field init constant if field initialized in class init method for (FieldInitInfo fieldInit : inits) { FieldNode field = fieldInit.fieldNode; if (field.getAccessFlags().isFinal()) { field.remove(JadxAttrType.CONSTANT_VALUE); } } filterFieldsInit(inits); if (inits.isEmpty()) { return false; } for (FieldInitInfo fieldInit : inits) { IndexInsnNode insn = fieldInit.putInsn; InsnArg arg = insn.getArg(0); if (arg instanceof InsnWrapArg) { ((InsnWrapArg) arg).getWrapInsn().add(AFlag.DECLARE_VAR); } InsnRemover.remove(classInitMth, insn); addFieldInitAttr(classInitMth, fieldInit.fieldNode, insn); } fixFieldsOrder(cls, inits); return true; } private static void moveCommonFieldsInit(ClassNode cls) { if (ListUtils.noneMatch(cls.getFields(), FieldNode::isInstance)) { return; } List constructors = getConstructorsList(cls); if (constructors.isEmpty()) { return; } List infoList = new ArrayList<>(constructors.size()); for (MethodNode constructorMth : constructors) { List inits = collectFieldsInit(cls, constructorMth, InsnType.IPUT); filterFieldsInit(inits); if (inits.isEmpty()) { return; } infoList.add(new ConstructorInitInfo(constructorMth, inits)); } // compare collected instructions ConstructorInitInfo common = null; for (ConstructorInitInfo info : infoList) { if (common == null) { common = info; continue; } if (!compareFieldInits(common.fieldInits, info.fieldInits)) { return; } } if (common == null) { return; } // all checks passed for (ConstructorInitInfo info : infoList) { for (FieldInitInfo fieldInit : info.fieldInits) { IndexInsnNode putInsn = fieldInit.putInsn; InsnArg arg = putInsn.getArg(0); if (arg instanceof InsnWrapArg) { ((InsnWrapArg) arg).getWrapInsn().add(AFlag.DECLARE_VAR); } InsnRemover.remove(info.constructorMth, putInsn); } } for (FieldInitInfo fieldInit : common.fieldInits) { addFieldInitAttr(common.constructorMth, fieldInit.fieldNode, fieldInit.putInsn); } fixFieldsOrder(cls, common.fieldInits); } private static List collectFieldsInit(ClassNode cls, MethodNode mth, InsnType putType) { List fieldsInit = new ArrayList<>(); Set singlePathBlocks = new HashSet<>(); BlockUtils.visitSinglePath(mth.getEnterBlock(), singlePathBlocks::add); boolean canReorder = true; for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { boolean fieldInsn = false; if (insn.getType() == putType) { IndexInsnNode putInsn = (IndexInsnNode) insn; FieldInfo field = (FieldInfo) putInsn.getIndex(); if (field.getDeclClass().equals(cls.getClassInfo())) { FieldNode fn = cls.searchField(field); if (fn != null) { boolean canMove = canReorder && singlePathBlocks.contains(block); fieldsInit.add(new FieldInitInfo(fn, putInsn, canMove)); fieldInsn = true; } } } if (!fieldInsn && canReorder && !insn.canReorder()) { canReorder = false; } } } return fieldsInit; } private static void filterFieldsInit(List inits) { // exclude fields initialized several times Set excludedFields = inits .stream() .collect(Collectors.toMap(fi -> fi.fieldNode, fi -> 1, Integer::sum)) .entrySet() .stream() .filter(v -> v.getValue() > 1) .map(v -> v.getKey().getFieldInfo()) .collect(Collectors.toSet()); for (FieldInitInfo initInfo : inits) { if (!checkInsn(initInfo)) { excludedFields.add(initInfo.fieldNode.getFieldInfo()); } } if (!excludedFields.isEmpty()) { boolean changed; do { changed = false; for (FieldInitInfo initInfo : inits) { FieldInfo fieldInfo = initInfo.fieldNode.getFieldInfo(); if (excludedFields.contains(fieldInfo)) { continue; } if (insnUseExcludedField(initInfo, excludedFields)) { excludedFields.add(fieldInfo); changed = true; } } } while (changed); } // apply if (!excludedFields.isEmpty()) { inits.removeIf(fi -> excludedFields.contains(fi.fieldNode.getFieldInfo())); } } private static boolean checkInsn(FieldInitInfo initInfo) { if (!initInfo.canMove) { return false; } IndexInsnNode insn = initInfo.putInsn; InsnArg arg = insn.getArg(0); if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); if (!wrapInsn.canReorder() && insn.contains(AType.EXC_CATCH)) { return false; } } else { return arg.isLiteral() || arg.isThis(); } Set regs = new HashSet<>(); insn.getRegisterArgs(regs); if (!regs.isEmpty()) { for (RegisterArg reg : regs) { if (!reg.isThis()) { return false; } } } return true; } private static boolean insnUseExcludedField(FieldInitInfo initInfo, Set excludedFields) { if (excludedFields.isEmpty()) { return false; } IndexInsnNode insn = initInfo.putInsn; boolean staticField = insn.getType() == InsnType.SPUT; InsnType useType = staticField ? InsnType.SGET : InsnType.IGET; // exclude if init code use any excluded field Boolean exclude = insn.visitInsns(innerInsn -> { if (innerInsn.getType() == useType) { FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) innerInsn).getIndex(); if (excludedFields.contains(fieldInfo)) { return true; } } return null; }); return Objects.equals(exclude, Boolean.TRUE); } private static void fixFieldsOrder(ClassNode cls, List inits) { List orderedFields = processFieldsDependencies(cls, inits); applyFieldsOrder(cls, orderedFields); } private static List processFieldsDependencies(ClassNode cls, List inits) { List orderedFields = Utils.collectionMap(inits, v -> v.fieldNode); // collect dependant fields Map> deps = new HashMap<>(inits.size()); for (FieldInitInfo initInfo : inits) { IndexInsnNode insn = initInfo.putInsn; boolean staticField = insn.getType() == InsnType.SPUT; InsnType useType = staticField ? InsnType.SGET : InsnType.IGET; insn.visitInsns(subInsn -> { if (subInsn.getType() == useType) { FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) subInsn).getIndex(); if (fieldInfo.getDeclClass().equals(cls.getClassInfo())) { FieldNode depField = cls.searchField(fieldInfo); if (depField != null) { deps.computeIfAbsent(initInfo.fieldNode, k -> new ArrayList<>()) .add(depField); } } } }); } if (deps.isEmpty()) { return orderedFields; } // build new list with deps fields before usage field List result = new ArrayList<>(); for (FieldNode field : orderedFields) { int idx = result.indexOf(field); List fieldDeps = deps.get(field); if (fieldDeps == null) { if (idx == -1) { result.add(field); } continue; } if (idx == -1) { for (FieldNode depField : fieldDeps) { if (!result.contains(depField)) { result.add(depField); } } result.add(field); continue; } for (FieldNode depField : fieldDeps) { int depIdx = result.indexOf(depField); if (depIdx == -1) { result.add(idx, depField); } else if (depIdx > idx) { result.remove(depIdx); result.add(idx, depField); } } } return result; } private static void applyFieldsOrder(ClassNode cls, List orderedFields) { List clsFields = cls.getFields(); // check if already ordered boolean ordered = Collections.indexOfSubList(clsFields, orderedFields) != -1; if (!ordered) { clsFields.removeAll(orderedFields); clsFields.addAll(orderedFields); } } private static boolean compareFieldInits(List base, List other) { if (base.size() != other.size()) { return false; } int count = base.size(); for (int i = 0; i < count; i++) { InsnNode baseInsn = base.get(i).putInsn; InsnNode otherInsn = other.get(i).putInsn; if (!baseInsn.isSame(otherInsn)) { return false; } } return true; } private static List getConstructorsList(ClassNode cls) { List list = new ArrayList<>(); for (MethodNode mth : cls.getMethods()) { AccessInfo accFlags = mth.getAccessFlags(); if (!accFlags.isStatic() && accFlags.isConstructor()) { list.add(mth); if (mth.isNoCode() || BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) { return Collections.emptyList(); } } } return list; } private static void addFieldInitAttr(MethodNode mth, FieldNode field, IndexInsnNode putInsn) { InsnNode assignInsn; InsnArg fldArg = putInsn.getArg(0); if (fldArg.isInsnWrap()) { assignInsn = ((InsnWrapArg) fldArg).getWrapInsn(); } else { assignInsn = InsnNode.wrapArg(fldArg); } field.addAttr(new FieldInitInsnAttr(mth, assignInsn)); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/FallbackModeVisitor.java ================================================ package jadx.core.dex.visitors; import jadx.core.codegen.json.JsonMappingGen; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.utils.exceptions.JadxException; public class FallbackModeVisitor extends AbstractVisitor { @Override public void init(RootNode root) { if (root.getArgs().isJsonOutput()) { JsonMappingGen.dump(root); } } @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } for (InsnNode insn : mth.getInstructions()) { if (insn == null) { continue; } // remove 'exception catch' for instruction which don't throw any exceptions CatchAttr catchAttr = insn.get(AType.EXC_CATCH); if (catchAttr != null) { switch (insn.getType()) { case RETURN: case IF: case GOTO: case JAVA_JSR: case MOVE: case MOVE_EXCEPTION: case ARITH: // ?? case NEG: case CONST: case CONST_STR: case CONST_CLASS: case CMP_L: case CMP_G: insn.remove(AType.EXC_CATCH); break; default: break; } } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/FixSwitchOverEnum.java ================================================ package jadx.core.dex.visitors; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.IntFunction; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.EnumClassAttr; import jadx.core.dex.attributes.nodes.EnumMapAttr; import jadx.core.dex.attributes.nodes.RegionRefAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.SwitchInsn; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "FixSwitchOverEnum", desc = "Simplify synthetic code in switch over enum", runAfter = { CodeShrinkVisitor.class, EnumVisitor.class } ) public class FixSwitchOverEnum extends AbstractVisitor { @Override public boolean visit(ClassNode cls) throws JadxException { initClsEnumMap(cls); return true; } @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } boolean changed = false; for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { if (insn.getType() == InsnType.SWITCH && !insn.contains(AFlag.REMOVE)) { changed |= processEnumSwitch(mth, (SwitchInsn) insn); } } } if (changed) { CodeShrinkVisitor.shrinkMethod(mth); } } private static boolean processEnumSwitch(MethodNode mth, SwitchInsn insn) { InsnArg arg = insn.getArg(0); if (!arg.isInsnWrap()) { return false; } InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); switch (wrapInsn.getType()) { case AGET: return processRemappedEnumSwitch(mth, insn, wrapInsn, arg); case INVOKE: return processDirectEnumSwitch(mth, insn, (InvokeNode) wrapInsn, arg); } return false; } private static boolean executeReplace(SwitchInsn swInsn, InsnArg arg, InsnArg invVar, IntFunction caseReplace) { RegionRefAttr regionRefAttr = swInsn.get(AType.REGION_REF); if (regionRefAttr == null) { return false; } if (!swInsn.replaceArg(arg, invVar)) { return false; } Map replaceMap = new HashMap<>(); int caseCount = swInsn.getKeys().length; for (int i = 0; i < caseCount; i++) { Object key = swInsn.getKey(i); Object replaceObj = caseReplace.apply(i); swInsn.modifyKey(i, replaceObj); replaceMap.put(key, replaceObj); } SwitchRegion region = (SwitchRegion) regionRefAttr.getRegion(); for (SwitchRegion.CaseInfo caseInfo : region.getCases()) { caseInfo.getKeys().replaceAll(key -> Utils.getOrElse(replaceMap.get(key), key)); } return true; } private static boolean processDirectEnumSwitch(MethodNode mth, SwitchInsn swInsn, InvokeNode invInsn, InsnArg arg) { MethodInfo callMth = invInsn.getCallMth(); if (!callMth.getShortId().equals("ordinal()I")) { return false; } InsnArg invVar = invInsn.getArg(0); ClassNode enumCls = mth.root().resolveClass(invVar.getType()); if (enumCls == null) { return false; } EnumClassAttr enumClassAttr = enumCls.get(AType.ENUM_CLASS); if (enumClassAttr == null) { return false; } FieldNode[] casesReplaceArr = mapToCases(swInsn, enumClassAttr.getFields()); if (casesReplaceArr == null) { return false; } return executeReplace(swInsn, arg, invVar, i -> casesReplaceArr[i]); } private static @Nullable FieldNode[] mapToCases(SwitchInsn swInsn, List fields) { int caseCount = swInsn.getKeys().length; if (fields.size() < caseCount) { return null; } FieldNode[] casesMap = new FieldNode[caseCount]; for (int i = 0; i < caseCount; i++) { Object key = swInsn.getKey(i); if (key instanceof Integer) { int ordinal = (Integer) key; try { casesMap[ordinal] = fields.get(ordinal).getField(); } catch (Exception e) { return null; } } else { return null; } } return casesMap; } private static boolean processRemappedEnumSwitch(MethodNode mth, SwitchInsn insn, InsnNode wrapInsn, InsnArg arg) { EnumMapInfo enumMapInfo = checkEnumMapAccess(mth.root(), wrapInsn); if (enumMapInfo == null) { return false; } FieldNode enumMapField = enumMapInfo.getMapField(); InsnArg invArg = enumMapInfo.getArg(); EnumMapAttr.KeyValueMap valueMap = getEnumMap(enumMapField); if (valueMap == null) { return false; } int caseCount = insn.getKeys().length; for (int i = 0; i < caseCount; i++) { Object key = insn.getKey(i); Object newKey = valueMap.get(key); if (newKey == null) { return false; } } if (executeReplace(insn, arg, invArg, i -> valueMap.get(insn.getKey(i)))) { enumMapField.add(AFlag.DONT_GENERATE); checkAndHideClass(enumMapField.getParentClass()); return true; } return false; } private static void initClsEnumMap(ClassNode enumCls) { MethodNode clsInitMth = enumCls.getClassInitMth(); if (clsInitMth == null || clsInitMth.isNoCode() || clsInitMth.getBasicBlocks() == null) { return; } EnumMapAttr mapAttr = new EnumMapAttr(); for (BlockNode block : clsInitMth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { if (insn.getType() == InsnType.APUT) { addToEnumMap(enumCls.root(), mapAttr, insn); } } } if (!mapAttr.isEmpty()) { enumCls.addAttr(mapAttr); } } private static @Nullable EnumMapAttr.KeyValueMap getEnumMap(FieldNode field) { ClassNode syntheticClass = field.getParentClass(); EnumMapAttr mapAttr = syntheticClass.get(AType.ENUM_MAP); if (mapAttr == null) { return null; } return mapAttr.getMap(field); } private static void addToEnumMap(RootNode root, EnumMapAttr mapAttr, InsnNode aputInsn) { InsnArg litArg = aputInsn.getArg(2); if (!litArg.isLiteral()) { return; } EnumMapInfo mapInfo = checkEnumMapAccess(root, aputInsn); if (mapInfo == null) { return; } InsnArg enumArg = mapInfo.getArg(); FieldNode field = mapInfo.getMapField(); if (field == null || !enumArg.isInsnWrap()) { return; } InsnNode sget = ((InsnWrapArg) enumArg).getWrapInsn(); if (!(sget instanceof IndexInsnNode)) { return; } Object index = ((IndexInsnNode) sget).getIndex(); if (!(index instanceof FieldInfo)) { return; } FieldNode fieldNode = root.resolveField((FieldInfo) index); if (fieldNode == null) { return; } int literal = (int) ((LiteralArg) litArg).getLiteral(); mapAttr.add(field, literal, fieldNode); } private static @Nullable EnumMapInfo checkEnumMapAccess(RootNode root, InsnNode checkInsn) { InsnArg sgetArg = checkInsn.getArg(0); InsnArg invArg = checkInsn.getArg(1); if (!sgetArg.isInsnWrap() || !invArg.isInsnWrap()) { return null; } InsnNode invInsn = ((InsnWrapArg) invArg).getWrapInsn(); InsnNode sgetInsn = ((InsnWrapArg) sgetArg).getWrapInsn(); if (invInsn.getType() != InsnType.INVOKE || sgetInsn.getType() != InsnType.SGET) { return null; } InvokeNode inv = (InvokeNode) invInsn; if (!inv.getCallMth().getShortId().equals("ordinal()I")) { return null; } ClassNode enumCls = root.resolveClass(inv.getCallMth().getDeclClass()); if (enumCls == null || !enumCls.isEnum()) { return null; } Object index = ((IndexInsnNode) sgetInsn).getIndex(); if (!(index instanceof FieldInfo)) { return null; } FieldNode enumMapField = root.resolveField((FieldInfo) index); if (enumMapField == null || !enumMapField.getAccessFlags().isSynthetic()) { return null; } return new EnumMapInfo(inv.getArg(0), enumMapField); } /** * If all static final synthetic fields have DONT_GENERATE => hide whole class */ private static void checkAndHideClass(ClassNode cls) { for (FieldNode field : cls.getFields()) { AccessInfo af = field.getAccessFlags(); if (af.isSynthetic() && af.isStatic() && af.isFinal() && !field.contains(AFlag.DONT_GENERATE)) { return; } } cls.add(AFlag.DONT_GENERATE); } private static class EnumMapInfo { private final InsnArg arg; private final FieldNode mapField; public EnumMapInfo(InsnArg arg, FieldNode mapField) { this.arg = arg; this.mapField = mapField; } public InsnArg getArg() { return arg; } public FieldNode getMapField() { return mapField; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/GenericTypesVisitor.java ================================================ package jadx.core.dex.visitors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.attributes.nodes.GenericInfoAttr; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "GenericTypesVisitor", desc = "Fix and apply generic type info", runAfter = TypeInferenceVisitor.class, runBefore = { CodeShrinkVisitor.class, MethodInvokeVisitor.class } ) public class GenericTypesVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(GenericTypesVisitor.class); @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { if (insn.getType() == InsnType.CONSTRUCTOR) { attachGenericTypesInfo(mth, (ConstructorInsn) insn); } } } } private void attachGenericTypesInfo(MethodNode mth, ConstructorInsn insn) { try { RegisterArg resultArg = insn.getResult(); if (resultArg == null) { return; } ArgType argType = resultArg.getSVar().getCodeVar().getType(); if (argType == null || argType.getGenericTypes() == null) { return; } ClassNode cls = mth.root().resolveClass(insn.getClassType()); if (cls != null && cls.getGenericTypeParameters().isEmpty()) { return; } insn.addAttr(new GenericInfoAttr(argType.getGenericTypes())); } catch (Exception e) { LOG.error("Failed to attach constructor generic info", e); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/IDexTreeVisitor.java ================================================ package jadx.core.dex.visitors; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxException; /** * Visitor interface for traverse dex tree */ public interface IDexTreeVisitor { /** * Visitor short id */ String getName(); /** * Called after loading dex tree, but before visitor traversal. */ void init(RootNode root) throws JadxException; /** * Visit class * * @return false for disable child methods and inner classes traversal */ boolean visit(ClassNode cls) throws JadxException; /** * Visit method */ void visit(MethodNode mth) throws JadxException; } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/InitCodeVariables.java ================================================ package jadx.core.dex.visitors; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.ssa.SSATransform; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; @JadxVisitor( name = "InitCodeVariables", desc = "Initialize code variables", runAfter = SSATransform.class ) public class InitCodeVariables extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { initCodeVars(mth); } public static void rerun(MethodNode mth) { for (SSAVar sVar : mth.getSVars()) { sVar.resetTypeAndCodeVar(); } initCodeVars(mth); } private static void initCodeVars(MethodNode mth) { RegisterArg thisArg = mth.getThisArg(); if (thisArg != null) { initCodeVar(mth, thisArg); } for (RegisterArg mthArg : mth.getArgRegs()) { initCodeVar(mth, mthArg); } for (SSAVar ssaVar : mth.getSVars()) { initCodeVar(ssaVar); } } public static void initCodeVar(MethodNode mth, RegisterArg regArg) { SSAVar ssaVar = regArg.getSVar(); if (ssaVar == null) { ssaVar = mth.makeNewSVar(regArg); } initCodeVar(ssaVar); } public static void initCodeVar(SSAVar ssaVar) { if (ssaVar.isCodeVarSet()) { return; } CodeVar codeVar = new CodeVar(); RegisterArg assignArg = ssaVar.getAssign(); if (assignArg.contains(AFlag.THIS)) { codeVar.setName(RegisterArg.THIS_ARG_NAME); codeVar.setThis(true); } if (assignArg.contains(AFlag.METHOD_ARGUMENT) || assignArg.contains(AFlag.CUSTOM_DECLARE)) { codeVar.setDeclared(true); } setCodeVar(ssaVar, codeVar); } private static void setCodeVar(SSAVar ssaVar, CodeVar codeVar) { List phiList = ssaVar.getPhiList(); if (!phiList.isEmpty()) { Set vars = new LinkedHashSet<>(); vars.add(ssaVar); collectConnectedVars(phiList, vars); setCodeVarType(codeVar, vars); vars.forEach(var -> { if (var.isCodeVarSet()) { codeVar.mergeFlagsFrom(var.getCodeVar()); } var.setCodeVar(codeVar); }); } else { ssaVar.setCodeVar(codeVar); } } private static void setCodeVarType(CodeVar codeVar, Set vars) { if (vars.size() > 1) { List imTypes = vars.stream() .map(SSAVar::getImmutableType) .filter(Objects::nonNull) .filter(ArgType::isTypeKnown) .distinct() .collect(Collectors.toList()); int imCount = imTypes.size(); if (imCount == 1) { codeVar.setType(imTypes.get(0)); } else if (imCount > 1) { throw new JadxRuntimeException("Several immutable types in one variable: " + imTypes + ", vars: " + vars); } } } private static void collectConnectedVars(List phiInsnList, Set vars) { if (phiInsnList.isEmpty()) { return; } for (PhiInsn phiInsn : phiInsnList) { SSAVar resultVar = phiInsn.getResult().getSVar(); if (vars.add(resultVar)) { collectConnectedVars(resultVar.getPhiList(), vars); } phiInsn.getArguments().forEach(arg -> { SSAVar sVar = ((RegisterArg) arg).getSVar(); if (vars.add(sVar)) { collectConnectedVars(sVar.getPhiList(), vars); } }); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/InlineMethods.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodInlineAttr; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnRemover; import jadx.core.utils.ListUtils; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; @JadxVisitor( name = "InlineMethods", desc = "Inline methods (previously marked in MarkMethodsForInline)", runAfter = TypeInferenceVisitor.class, runBefore = ModVisitor.class ) public class InlineMethods extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(InlineMethods.class); @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { if (insn.getType() == InsnType.INVOKE) { processInvokeInsn(mth, block, (InvokeNode) insn); } } } } private void processInvokeInsn(MethodNode mth, BlockNode block, InvokeNode insn) { IMethodDetails callMthDetails = insn.get(AType.METHOD_DETAILS); if (!(callMthDetails instanceof MethodNode)) { return; } MethodNode callMth = (MethodNode) callMthDetails; try { MethodInlineAttr mia = MarkMethodsForInline.process(callMth); if (mia == null) { // method is not yet loaded => force process mth.addDebugComment("Class process forced to load method for inline: " + callMth); mth.root().getProcessClasses().forceProcess(callMth.getParentClass()); // run check again mia = MarkMethodsForInline.process(callMth); if (mia == null) { mth.addWarnComment("Failed to check method for inline after forced process" + callMth); return; } } if (mia.notNeeded()) { return; } inlineMethod(mth, callMth, mia, block, insn); } catch (Exception e) { throw new JadxRuntimeException("Failed to process method for inline: " + callMth, e); } } private void inlineMethod(MethodNode mth, MethodNode callMth, MethodInlineAttr mia, BlockNode block, InvokeNode insn) { InsnNode inlCopy = mia.getInsn().copyWithoutResult(); if (replaceRegs(mth, callMth, mia, insn, inlCopy)) { IMethodDetails methodDetailsAttr = inlCopy.get(AType.METHOD_DETAILS); // replaceInsn replaces the attributes as well, make sure to preserve METHOD_DETAILS if (BlockUtils.replaceInsn(mth, block, insn, inlCopy)) { if (methodDetailsAttr != null) { inlCopy.addAttr(methodDetailsAttr); } updateUsageInfo(mth, callMth, mia.getInsn()); return; } } mth.addWarnComment("Failed to inline method: " + callMth); // undo changes to insn InsnRemover.unbindInsn(mth, inlCopy); insn.rebindArgs(); } private boolean replaceRegs(MethodNode mth, MethodNode callMth, MethodInlineAttr mia, InvokeNode insn, InsnNode inlCopy) { try { if (!callMth.getMethodInfo().getArgumentsTypes().isEmpty()) { // remap args InsnArg[] regs = new InsnArg[callMth.getRegsCount()]; int[] regNums = mia.getArgsRegNums(); for (int i = 0; i < regNums.length; i++) { InsnArg arg = insn.getArg(i); regs[regNums[i]] = arg; } // replace args List inlArgs = new ArrayList<>(); inlCopy.getRegisterArgs(inlArgs); for (RegisterArg r : inlArgs) { int regNum = r.getRegNum(); if (regNum >= regs.length) { mth.addWarnComment("Unknown register number '" + r + "' in method call: " + callMth); return false; } InsnArg repl = regs[regNum]; if (repl == null) { mth.addWarnComment("Not passed register '" + r + "' in method call: " + callMth); return false; } if (!inlCopy.replaceArg(r, repl.duplicate())) { mth.addWarnComment("Failed to replace arg " + r + " for method inline: " + callMth); return false; } } } RegisterArg resultArg = insn.getResult(); if (resultArg != null) { inlCopy.setResult(resultArg.duplicate()); } else if (isAssignNeeded(mia.getInsn(), insn, callMth)) { // add a fake result to make correct java expression (see test TestGetterInlineNegative) inlCopy.setResult(mth.makeSyntheticRegArg(callMth.getReturnType(), "unused")); } return true; } catch (Exception e) { mth.addWarnComment("Method inline failed with exception", e); return false; } } private boolean isAssignNeeded(InsnNode inlineInsn, InvokeNode parentInsn, MethodNode callMthNode) { if (parentInsn.getResult() != null) { return false; } if (parentInsn.contains(AFlag.WRAPPED)) { return false; } if (inlineInsn.getType() == InsnType.IPUT) { return false; } return !callMthNode.isVoidReturn(); } private void updateUsageInfo(MethodNode mth, MethodNode inlinedMth, InsnNode insn) { List newUseIn = new ArrayList<>(inlinedMth.getUseIn()); newUseIn.remove(mth); inlinedMth.setUseIn(newUseIn); insn.visitInsns(innerInsn -> { // TODO: share code with UsageInfoVisitor switch (innerInsn.getType()) { case INVOKE: case CONSTRUCTOR: MethodInfo callMth = ((BaseInvokeNode) innerInsn).getCallMth(); MethodNode callMthNode = mth.root().resolveMethod(callMth); if (callMthNode != null) { callMthNode.setUseIn(ListUtils.safeReplace(new ArrayList<>(callMthNode.getUseIn()), inlinedMth, mth)); replaceClsUsage(mth, inlinedMth, callMthNode.getParentClass()); } break; case IGET: case IPUT: case SPUT: case SGET: FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) innerInsn).getIndex(); FieldNode fieldNode = mth.root().resolveField(fieldInfo); if (fieldNode != null) { fieldNode.setUseIn(ListUtils.safeReplace(new ArrayList<>(fieldNode.getUseIn()), inlinedMth, mth)); replaceClsUsage(mth, inlinedMth, fieldNode.getParentClass()); } break; } }); } private void replaceClsUsage(MethodNode mth, MethodNode inlinedMth, ClassNode parentClass) { parentClass.setUseInMth(ListUtils.safeReplace(parentClass.getUseInMth(), inlinedMth, mth)); parentClass.setUseIn(ListUtils.safeReplace(parentClass.getUseIn(), inlinedMth.getParentClass(), mth.getParentClass())); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/JadxVisitor.java ================================================ package jadx.core.dex.visitors; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Annotation for describe dependencies of jadx visitors */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface JadxVisitor { /** * Visitor short name (identifier) */ String name(); /** * Detailed visitor description */ String desc() default ""; /** * This visitor must be run after listed visitors */ Class[] runAfter() default {}; /** * This visitor must be run before listed visitors */ Class[] runBefore() default {}; } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/MarkMethodsForInline.java ================================================ package jadx.core.dex.visitors; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.AccessFlags; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodInlineAttr; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.fixaccessmodifiers.FixAccessModifiers; import jadx.core.utils.BlockUtils; import jadx.core.utils.ListUtils; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "MarkMethodsForInline", desc = "Mark synthetic static methods for inline", runAfter = { FixAccessModifiers.class, ClassModifier.class } ) public class MarkMethodsForInline extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { process(mth); } /** * @return null if method can't be analyzed (not loaded) */ @Nullable public static MethodInlineAttr process(MethodNode mth) { try { MethodInlineAttr mia = mth.get(AType.METHOD_INLINE); if (mia != null) { return mia; } if (mth.contains(AFlag.METHOD_CANDIDATE_FOR_INLINE)) { if (mth.getBasicBlocks() == null) { return null; } MethodInlineAttr inlined = inlineMth(mth); if (inlined != null) { return inlined; } } } catch (Exception e) { mth.addWarnComment("Method inline analysis failed", e); } return MethodInlineAttr.inlineNotNeeded(mth); } @Nullable private static MethodInlineAttr inlineMth(MethodNode mth) { List insns = BlockUtils.collectInsnsWithLimit(mth.getBasicBlocks(), 2); int insnsCount = insns.size(); if (insnsCount == 0) { return null; } if (insnsCount == 1) { InsnNode insn = insns.get(0); if (insn.getType() == InsnType.RETURN && insn.getArgsCount() == 1) { // synthetic field getter // set arg from 'return' instruction InsnArg arg = insn.getArg(0); if (!arg.isInsnWrap()) { return null; } return addInlineAttr(mth, ((InsnWrapArg) arg).unWrapWithCopy(), true); } // method invoke return addInlineAttr(mth, insn, false); } if (insnsCount == 2 && insns.get(1).getType() == InsnType.RETURN) { InsnNode firstInsn = insns.get(0); InsnNode retInsn = insns.get(1); if (retInsn.getArgsCount() == 0 || isSyntheticAccessPattern(mth, firstInsn, retInsn)) { return addInlineAttr(mth, firstInsn, false); } } // TODO: inline field arithmetics. Disabled tests: TestAnonymousClass3a and TestAnonymousClass5 return null; } private static boolean isSyntheticAccessPattern(MethodNode mth, InsnNode firstInsn, InsnNode retInsn) { List mthRegs = mth.getArgRegs(); switch (firstInsn.getType()) { case IGET: return mthRegs.size() == 1 && retInsn.getArg(0).isSameVar(firstInsn.getResult()) && firstInsn.getArg(0).isSameVar(mthRegs.get(0)); case SGET: return mthRegs.isEmpty() && retInsn.getArg(0).isSameVar(firstInsn.getResult()); case IPUT: return mthRegs.size() == 2 && retInsn.getArg(0).isSameVar(mthRegs.get(1)) && firstInsn.getArg(0).isSameVar(mthRegs.get(1)) && firstInsn.getArg(1).isSameVar(mthRegs.get(0)); case SPUT: return mthRegs.size() == 1 && retInsn.getArg(0).isSameVar(mthRegs.get(0)) && firstInsn.getArg(0).isSameVar(mthRegs.get(0)); case INVOKE: if (!retInsn.getArg(0).isSameVar(firstInsn.getResult())) { return false; } return ListUtils.orderedEquals( mth.getArgRegs(), firstInsn.getArgList(), (mthArg, insnArg) -> insnArg.isSameVar(mthArg)); default: return false; } } private static @Nullable MethodInlineAttr addInlineAttr(MethodNode mth, InsnNode insn, boolean isCopy) { if (!fixVisibilityOfInlineCode(mth, insn)) { if (isCopy) { unbindSsaVars(insn); } return null; } InsnNode inlInsn = isCopy ? insn : insn.copyWithoutResult(); unbindSsaVars(inlInsn); return MethodInlineAttr.markForInline(mth, inlInsn); } private static void unbindSsaVars(InsnNode insn) { insn.visitArgs(arg -> { if (arg.isRegister()) { RegisterArg reg = (RegisterArg) arg; SSAVar ssaVar = reg.getSVar(); if (ssaVar != null) { ssaVar.removeUse(reg); reg.resetSSAVar(); } } }); } private static boolean fixVisibilityOfInlineCode(MethodNode mth, InsnNode insn) { int newVisFlag = AccessFlags.PUBLIC; // TODO: calculate more precisely InsnType insnType = insn.getType(); if (insnType == InsnType.INVOKE) { InvokeNode invoke = (InvokeNode) insn; MethodNode callMthNode = mth.root().resolveMethod(invoke.getCallMth()); if (callMthNode != null && !callMthNode.root().getArgs().isRespectBytecodeAccModifiers()) { FixAccessModifiers.changeVisibility(callMthNode, newVisFlag); } return true; } if (insnType == InsnType.ONE_ARG) { InsnArg arg = insn.getArg(0); if (!arg.isInsnWrap()) { return false; } return fixVisibilityOfInlineCode(mth, ((InsnWrapArg) arg).getWrapInsn()); } if (insn instanceof IndexInsnNode) { Object indexObj = ((IndexInsnNode) insn).getIndex(); if (indexObj instanceof FieldInfo) { // field access must be already fixed in ModVisitor.fixFieldUsage method return true; } } mth.addDebugComment("Can't inline method, not implemented redirect type for insn: " + insn); return false; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/MethodInvokeVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.utils.TypeUtils; import jadx.core.dex.visitors.methods.MutableMethodDetails; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.dex.visitors.typeinference.TypeCompare; import jadx.core.dex.visitors.typeinference.TypeCompareEnum; import jadx.core.utils.InsnUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; @JadxVisitor( name = "MethodInvokeVisitor", desc = "Process additional info for method invocation (overload, vararg)", runAfter = { CodeShrinkVisitor.class, ModVisitor.class }, runBefore = { SimplifyVisitor.class // run before cast remove and StringBuilder replace } ) public class MethodInvokeVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(MethodInvokeVisitor.class); private RootNode root; @Override public void init(RootNode root) { this.root = root; } @Override public void visit(MethodNode mth) { if (mth.isNoCode()) { return; } for (BlockNode block : mth.getBasicBlocks()) { if (block.contains(AFlag.DONT_GENERATE)) { continue; } for (InsnNode insn : block.getInstructions()) { if (insn.contains(AFlag.DONT_GENERATE)) { continue; } insn.visitInsns(in -> { if (in instanceof BaseInvokeNode) { processInvoke(mth, ((BaseInvokeNode) in)); } }); } } } private void processInvoke(MethodNode parentMth, BaseInvokeNode invokeInsn) { MethodInfo callMth = invokeInsn.getCallMth(); if (callMth.getArgsCount() == 0) { return; } IMethodDetails mthDetails = root.getMethodUtils().getMethodDetails(invokeInsn); if (mthDetails == null) { if (Consts.DEBUG) { parentMth.addDebugComment("Method info not found: " + callMth); } processUnknown(invokeInsn); } else { if (mthDetails.isVarArg()) { ArgType last = Utils.last(mthDetails.getArgTypes()); if (last != null && last.isArray()) { invokeInsn.add(AFlag.VARARG_CALL); } } processOverloaded(parentMth, invokeInsn, mthDetails); } } private void processOverloaded(MethodNode parentMth, BaseInvokeNode invokeInsn, IMethodDetails mthDetails) { MethodInfo callMth = invokeInsn.getCallMth(); ArgType callCls = getCallClassFromInvoke(parentMth, invokeInsn, callMth); List overloadMethods = root.getMethodUtils().collectOverloadedMethods(callCls, callMth); if (overloadMethods.isEmpty()) { // not overloaded return; } // resolve generic type variables Map typeVarsMapping = getTypeVarsMapping(invokeInsn); IMethodDetails effectiveMthDetails = resolveTypeVars(mthDetails, typeVarsMapping); List effectiveOverloadMethods = new ArrayList<>(overloadMethods.size() + 1); for (IMethodDetails overloadMethod : overloadMethods) { effectiveOverloadMethods.add(resolveTypeVars(overloadMethod, typeVarsMapping)); } effectiveOverloadMethods.add(effectiveMthDetails); // search cast types to resolve overloading int argsOffset = invokeInsn.getFirstArgOffset(); List compilerVarTypes = collectCompilerVarTypes(invokeInsn, argsOffset); List castTypes = searchCastTypes(parentMth, effectiveMthDetails, effectiveOverloadMethods, compilerVarTypes); List resultCastTypes = expandTypes(parentMth, effectiveMthDetails, castTypes); applyArgsCast(invokeInsn, argsOffset, compilerVarTypes, resultCastTypes); } /** * Method details not found => add cast for 'null' args */ private void processUnknown(BaseInvokeNode invokeInsn) { int argsOffset = invokeInsn.getFirstArgOffset(); List compilerVarTypes = collectCompilerVarTypes(invokeInsn, argsOffset); List castTypes = new ArrayList<>(compilerVarTypes); if (replaceUnknownTypes(castTypes, invokeInsn.getCallMth().getArgumentsTypes())) { applyArgsCast(invokeInsn, argsOffset, compilerVarTypes, castTypes); } } private ArgType getCallClassFromInvoke(MethodNode parentMth, BaseInvokeNode invokeInsn, MethodInfo callMth) { if (invokeInsn instanceof ConstructorInsn) { ConstructorInsn constrInsn = (ConstructorInsn) invokeInsn; if (constrInsn.isSuper()) { return parentMth.getParentClass().getSuperClass(); } } InsnArg instanceArg = invokeInsn.getInstanceArg(); if (instanceArg != null) { return instanceArg.getType(); } // static call return callMth.getDeclClass().getType(); } private Map getTypeVarsMapping(BaseInvokeNode invokeInsn) { MethodInfo callMthInfo = invokeInsn.getCallMth(); ArgType declClsType = callMthInfo.getDeclClass().getType(); ArgType callClsType = getClsCallType(invokeInsn, declClsType); TypeUtils typeUtils = root.getTypeUtils(); Map clsTypeVars = typeUtils.getTypeVariablesMapping(callClsType); Map mthTypeVars = typeUtils.getTypeVarMappingForInvoke(invokeInsn); return Utils.mergeMaps(clsTypeVars, mthTypeVars); } private ArgType getClsCallType(BaseInvokeNode invokeInsn, ArgType declClsType) { InsnArg instanceArg = invokeInsn.getInstanceArg(); if (instanceArg != null) { return instanceArg.getType(); } if (invokeInsn.getType() == InsnType.CONSTRUCTOR && invokeInsn.getResult() != null) { return invokeInsn.getResult().getType(); } return declClsType; } private void applyArgsCast(BaseInvokeNode invokeInsn, int argsOffset, List compilerVarTypes, List castTypes) { int argsCount = invokeInsn.getArgsCount(); for (int i = argsOffset; i < argsCount; i++) { InsnArg arg = invokeInsn.getArg(i); int origPos = i - argsOffset; ArgType compilerType = compilerVarTypes.get(origPos); ArgType castType = castTypes.get(origPos); if (castType != null) { if (!castType.equals(compilerType)) { if (arg.isLiteral() && compilerType.isPrimitive() && castType.isPrimitive()) { arg.setType(castType); arg.add(AFlag.EXPLICIT_PRIMITIVE_TYPE); } else if (InsnUtils.isWrapped(arg, InsnType.CHECK_CAST)) { IndexInsnNode wrapInsn = (IndexInsnNode) ((InsnWrapArg) arg).getWrapInsn(); wrapInsn.updateIndex(castType); } else { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.info("Insert cast for invoke insn arg: {}, insn: {}", arg, invokeInsn); } InsnNode castInsn = new IndexInsnNode(InsnType.CAST, castType, 1); castInsn.addArg(arg); castInsn.add(AFlag.EXPLICIT_CAST); InsnArg wrapCast = InsnArg.wrapArg(castInsn); wrapCast.setType(castType); invokeInsn.setArg(i, wrapCast); } } else { // protect already existed cast if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); if (wrapInsn.getType() == InsnType.CHECK_CAST) { wrapInsn.add(AFlag.EXPLICIT_CAST); } } } } } } private IMethodDetails resolveTypeVars(IMethodDetails mthDetails, Map typeVarsMapping) { List argTypes = mthDetails.getArgTypes(); int argsCount = argTypes.size(); boolean fixed = false; List fixedArgTypes = new ArrayList<>(argsCount); for (int argNum = 0; argNum < argsCount; argNum++) { ArgType argType = argTypes.get(argNum); if (argType == null) { throw new JadxRuntimeException("Null arg type in " + mthDetails + " at: " + argNum + " in: " + argTypes); } if (argType.containsTypeVariable()) { ArgType resolvedType = root.getTypeUtils().replaceTypeVariablesUsingMap(argType, typeVarsMapping); if (resolvedType == null || resolvedType.equals(argType)) { // type variables erased from method info by compiler resolvedType = mthDetails.getMethodInfo().getArgumentsTypes().get(argNum); } fixedArgTypes.add(resolvedType); fixed = true; } else { fixedArgTypes.add(argType); } } ArgType returnType = mthDetails.getReturnType(); if (returnType.containsTypeVariable()) { ArgType resolvedType = root.getTypeUtils().replaceTypeVariablesUsingMap(returnType, typeVarsMapping); if (resolvedType == null || resolvedType.containsTypeVariable()) { returnType = mthDetails.getMethodInfo().getReturnType(); fixed = true; } } if (!fixed) { return mthDetails; } MutableMethodDetails mutableMethodDetails = new MutableMethodDetails(mthDetails); mutableMethodDetails.setArgTypes(fixedArgTypes); mutableMethodDetails.setRetType(returnType); return mutableMethodDetails; } private List searchCastTypes(MethodNode parentMth, IMethodDetails mthDetails, List overloadedMethods, List compilerVarTypes) { // try compiler types if (isOverloadResolved(mthDetails, overloadedMethods, compilerVarTypes)) { return compilerVarTypes; } int argsCount = compilerVarTypes.size(); List castTypes = new ArrayList<>(compilerVarTypes); // replace unknown types boolean changed = replaceUnknownTypes(castTypes, mthDetails.getArgTypes()); if (changed && isOverloadResolved(mthDetails, overloadedMethods, castTypes)) { return castTypes; } // replace generic types changed = false; for (int i = 0; i < argsCount; i++) { ArgType castType = castTypes.get(i); ArgType mthType = mthDetails.getArgTypes().get(i); if (!castType.isGeneric() && mthType.isGeneric()) { castTypes.set(i, mthType); changed = true; } } if (changed && isOverloadResolved(mthDetails, overloadedMethods, castTypes)) { return castTypes; } // if just one arg => cast will resolve if (argsCount == 1) { return mthDetails.getArgTypes(); } if (Consts.DEBUG_OVERLOADED_CASTS) { // TODO: try to minimize casts count parentMth.addDebugComment("Failed to find minimal casts for resolve overloaded methods, cast all args instead" + "\n method: " + mthDetails + "\n arg types: " + compilerVarTypes + "\n candidates:" + "\n " + Utils.listToString(overloadedMethods, "\n ")); } // not resolved -> cast all args return mthDetails.getArgTypes(); } private boolean replaceUnknownTypes(List castTypes, List mthArgTypes) { int argsCount = castTypes.size(); boolean changed = false; for (int i = 0; i < argsCount; i++) { ArgType castType = castTypes.get(i); if (!castType.isTypeKnown()) { ArgType mthType = mthArgTypes.get(i); castTypes.set(i, mthType); changed = true; } } return changed; } /** * Use generified types if available */ private List expandTypes(MethodNode parentMth, IMethodDetails methodDetails, List castTypes) { TypeCompare typeCompare = parentMth.root().getTypeCompare(); List mthArgTypes = methodDetails.getArgTypes(); int argsCount = castTypes.size(); List list = new ArrayList<>(argsCount); for (int i = 0; i < argsCount; i++) { ArgType mthType = mthArgTypes.get(i); ArgType castType = castTypes.get(i); TypeCompareEnum result = typeCompare.compareTypes(mthType, castType); if (result == TypeCompareEnum.NARROW_BY_GENERIC) { list.add(mthType); } else { list.add(castType); } } return list; } private boolean isOverloadResolved(IMethodDetails expectedMthDetails, List overloadedMethods, List castTypes) { if (overloadedMethods.isEmpty()) { return false; } // TODO: search closest method, instead filtering List strictMethods = filterApplicableMethods(overloadedMethods, castTypes, MethodInvokeVisitor::isStrictTypes); if (strictMethods.size() == 1) { return strictMethods.get(0).equals(expectedMthDetails); } List resolvedMethods = filterApplicableMethods(overloadedMethods, castTypes, MethodInvokeVisitor::isTypeApplicable); if (resolvedMethods.size() == 1) { return resolvedMethods.get(0).equals(expectedMthDetails); } return false; } private static boolean isStrictTypes(TypeCompareEnum result) { return result.isEqual(); } private static boolean isTypeApplicable(TypeCompareEnum result) { return result.isNarrowOrEqual() || result == TypeCompareEnum.WIDER_BY_GENERIC; } private List filterApplicableMethods(List methods, List types, Function acceptFunction) { List list = new ArrayList<>(methods.size()); for (IMethodDetails m : methods) { if (isMethodAcceptable(m, types, acceptFunction)) { list.add(m); } } return list; } private boolean isMethodAcceptable(IMethodDetails methodDetails, List types, Function acceptFunction) { List mthTypes = methodDetails.getArgTypes(); int argCount = mthTypes.size(); if (argCount != types.size()) { return false; } TypeCompare typeCompare = root.getTypeUpdate().getTypeCompare(); for (int i = 0; i < argCount; i++) { ArgType mthType = mthTypes.get(i); ArgType argType = types.get(i); TypeCompareEnum result = typeCompare.compareTypes(argType, mthType); if (!acceptFunction.apply(result)) { return false; } } return true; } private List collectCompilerVarTypes(BaseInvokeNode insn, int argOffset) { int argsCount = insn.getArgsCount(); List result = new ArrayList<>(argsCount); for (int i = argOffset; i < argsCount; i++) { InsnArg arg = insn.getArg(i); result.add(getCompilerVarType(arg)); } return result; } /** * Return type as seen by compiler */ private ArgType getCompilerVarType(InsnArg arg) { if (arg instanceof LiteralArg) { LiteralArg literalArg = (LiteralArg) arg; ArgType type = literalArg.getType(); if (literalArg.getLiteral() == 0) { if (type.isObject() || type.isArray()) { // null return ArgType.UNKNOWN_OBJECT; } } if (type.isPrimitive() && !arg.contains(AFlag.EXPLICIT_PRIMITIVE_TYPE)) { return ArgType.INT; } return arg.getType(); } if (arg instanceof RegisterArg) { return arg.getType(); } if (arg instanceof InsnWrapArg) { InsnWrapArg wrapArg = (InsnWrapArg) arg; return getInsnCompilerType(arg, wrapArg.getWrapInsn()); } throw new JadxRuntimeException("Unknown var type for: " + arg); } private static ArgType getInsnCompilerType(InsnArg arg, InsnNode insn) { switch (insn.getType()) { case CAST: case CHECK_CAST: return ((IndexInsnNode) insn).getIndexAsType(); default: if (insn.getResult() != null) { return insn.getResult().getType(); } return arg.getType(); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/MethodThrowsVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.ExceptionsAttr; import jadx.core.Consts; import jadx.core.clsp.ClspClass; import jadx.core.clsp.ClspMethod; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodThrowsAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.visitors.regions.RegionMakerVisitor; import jadx.core.dex.visitors.typeinference.TypeCompare; import jadx.core.dex.visitors.typeinference.TypeCompareEnum; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "MethodThrowsVisitor", desc = "Scan methods to collect thrown exceptions", runAfter = { RegionMakerVisitor.class // Run after RegionMakerVisitor to ignore throw instructions of synchronized regions } ) public class MethodThrowsVisitor extends AbstractVisitor { private enum ExceptionType { THROWS_REQUIRED, RUNTIME, UNKNOWN_TYPE, NO_EXCEPTION } private RootNode root; @Override public void init(RootNode root) throws JadxException { this.root = root; } @Override public void visit(MethodNode mth) throws JadxException { MethodThrowsAttr attr = mth.get(AType.METHOD_THROWS); if (attr == null) { attr = new MethodThrowsAttr(new HashSet<>()); mth.addAttr(attr); } if (!attr.isVisited()) { attr.setVisited(true); processInstructions(mth); } List invalid = new ArrayList<>(); ExceptionsAttr exceptions = mth.get(JadxAttrType.EXCEPTIONS); if (exceptions != null && !exceptions.getList().isEmpty()) { for (String throwsTypeStr : exceptions.getList()) { ArgType excType = ArgType.object(throwsTypeStr); if (validateException(excType) == ExceptionType.NO_EXCEPTION) { invalid.add(excType); } else { attr.getList().add(excType.getObject()); } } } if (!invalid.isEmpty()) { mth.addWarnComment("Byte code manipulation detected: skipped illegal throws declarations: " + invalid); } mergeExceptions(attr.getList()); } private void mergeExceptions(Set excSet) { if (excSet.contains(Consts.CLASS_EXCEPTION)) { excSet.removeIf(e -> !e.equals(Consts.CLASS_EXCEPTION)); return; } if (excSet.contains(Consts.CLASS_THROWABLE)) { excSet.removeIf(e -> !e.equals(Consts.CLASS_THROWABLE)); return; } List toRemove = new ArrayList<>(); for (String ex1 : excSet) { for (String ex2 : excSet) { if (ex1.equals(ex2)) { continue; } if (isBaseException(ex1, ex2)) { toRemove.add(ex1); } } } toRemove.forEach(excSet::remove); } private void processInstructions(MethodNode mth) { if (mth.isNoCode() || mth.getBasicBlocks() == null) { return; } try { blocks: for (final BlockNode block : mth.getBasicBlocks()) { // Skip e.g. throw instructions of synchronized regions boolean skipExceptions = block.contains(AFlag.REMOVE) || block.contains(AFlag.DONT_GENERATE); Set excludedExceptions = new HashSet<>(); CatchAttr catchAttr = block.get(AType.EXC_CATCH); if (catchAttr != null) { for (ExceptionHandler handler : catchAttr.getHandlers()) { if (handler.isCatchAll()) { continue blocks; } excludedExceptions.add(handler.getArgType().toString()); } } for (final InsnNode insn : block.getInstructions()) { checkInsn(mth, insn, excludedExceptions, skipExceptions); } } } catch (Exception e) { mth.addWarnComment("Failed to analyze thrown exceptions", e); } } private void checkInsn(MethodNode mth, InsnNode insn, Set excludedExceptions, boolean skipExceptions) throws JadxException { if (!skipExceptions && insn.getType() == InsnType.THROW && !insn.contains(AFlag.DONT_GENERATE)) { InsnArg throwArg = insn.getArg(0); if (throwArg instanceof RegisterArg) { RegisterArg regArg = (RegisterArg) throwArg; ArgType exceptionType = regArg.getType(); if (exceptionType.equals(ArgType.THROWABLE)) { InsnNode assignInsn = regArg.getAssignInsn(); if (assignInsn != null && assignInsn.getType() == InsnType.MOVE_EXCEPTION && assignInsn.getResult().contains(AFlag.CUSTOM_DECLARE)) { // arg variable is from catch statement, ignore Throwable rethrow return; } } visitThrows(mth, exceptionType, excludedExceptions); } else { if (throwArg instanceof InsnWrapArg) { InsnWrapArg insnWrapArg = (InsnWrapArg) throwArg; ArgType exceptionType = insnWrapArg.getType(); visitThrows(mth, exceptionType, excludedExceptions); } } return; } if (insn.getType() == InsnType.INVOKE) { InvokeNode invokeNode = (InvokeNode) insn; MethodInfo callMth = invokeNode.getCallMth(); String signature = callMth.makeSignature(true); ClassInfo classInfo = callMth.getDeclClass(); ClassNode classNode = root.resolveClass(classInfo); if (classNode != null) { MethodNode cMth = searchOverriddenMethod(classNode, callMth, signature); if (cMth == null) { return; } visit(cMth); MethodThrowsAttr cAttr = cMth.get(AType.METHOD_THROWS); MethodThrowsAttr attr = mth.get(AType.METHOD_THROWS); if (attr != null && cAttr != null && !cAttr.getList().isEmpty()) { for (String argTypeStr : cAttr.getList()) { visitThrows(mth, ArgType.object(argTypeStr), excludedExceptions); } } } else { ClspClass clsDetails = root.getClsp().getClsDetails(classInfo.getType()); if (clsDetails != null) { ClspMethod cMth = searchOverriddenMethod(clsDetails, signature); if (cMth != null && cMth.getThrows() != null && !cMth.getThrows().isEmpty()) { MethodThrowsAttr attr = mth.get(AType.METHOD_THROWS); if (attr != null) { for (ArgType argType : cMth.getThrows()) { visitThrows(mth, argType, excludedExceptions); } } } } } } } private void visitThrows(MethodNode mth, ArgType excType, Set excludedExceptions) { if (excType.isTypeKnown() && isThrowsRequired(mth, excType)) { for (String excludedException : excludedExceptions) { if (isBaseException(excType.getObject(), excludedException)) { return; } } mth.get(AType.METHOD_THROWS).getList().add(excType.getObject()); } } private boolean isThrowsRequired(MethodNode mth, ArgType type) { ExceptionType result = validateException(type); if (result == ExceptionType.UNKNOWN_TYPE) { mth.addInfoComment("Thrown type has an unknown type hierarchy: " + type); return true; // assume an exception } return result == ExceptionType.THROWS_REQUIRED; } private ExceptionType validateException(ArgType clsType) { if (clsType == null || clsType.equals(ArgType.OBJECT)) { return ExceptionType.NO_EXCEPTION; } if (!clsType.isTypeKnown() || !root.getClsp().isClsKnown(clsType.getObject())) { return ExceptionType.UNKNOWN_TYPE; } if (isImplements(clsType, ArgType.RUNTIME_EXCEPTION) || isImplements(clsType, ArgType.ERROR)) { return ExceptionType.RUNTIME; } if (isImplements(clsType, ArgType.THROWABLE) || isImplements(clsType, ArgType.EXCEPTION)) { return ExceptionType.THROWS_REQUIRED; } return ExceptionType.NO_EXCEPTION; } /** * @return is 'possibleParent' an exception class of 'exception' */ private boolean isBaseException(String exception, String possibleParent) { if (exception.equals(possibleParent)) { return true; } return root.getClsp().isImplements(exception, possibleParent); } private boolean isImplements(ArgType type, ArgType baseType) { if (type.equals(baseType)) { return true; } return root.getClsp().isImplements(type.getObject(), baseType.getObject()); } private @Nullable MethodNode searchOverriddenMethod(ClassNode cls, MethodInfo mth, String signature) { // search by exact full signature (with return value) to fight obfuscation (see test // 'TestOverrideWithSameName') String shortId = mth.getShortId(); for (MethodNode supMth : cls.getMethods()) { if (supMth.getMethodInfo().getShortId().equals(shortId)) { return supMth; } } // search by signature without return value and check if return value is wider type for (MethodNode supMth : cls.getMethods()) { if (supMth.getMethodInfo().getShortId().startsWith(signature) && !supMth.getAccessFlags().isStatic()) { TypeCompare typeCompare = cls.root().getTypeCompare(); ArgType supRetType = supMth.getMethodInfo().getReturnType(); ArgType mthRetType = mth.getReturnType(); TypeCompareEnum res = typeCompare.compareTypes(supRetType, mthRetType); if (res.isWider()) { return supMth; } } } return null; } private ClspMethod searchOverriddenMethod(ClspClass clsDetails, String signature) { Map methodsMap = clsDetails.getMethodsMap(); for (Map.Entry entry : methodsMap.entrySet()) { String mthShortId = entry.getKey(); // do not check full signature, classpath methods can be trusted // i.e. doesn't contain methods with same signature in one class if (mthShortId.startsWith(signature)) { return entry.getValue(); } } return null; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/MethodVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.function.Consumer; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.exceptions.JadxException; public class MethodVisitor extends AbstractVisitor { private final String name; private final Consumer visitor; public MethodVisitor(String name, Consumer visitor) { this.name = name; this.visitor = visitor; } @Override public void visit(MethodNode mth) throws JadxException { visitor.accept(mth); } @Override public String getName() { return name; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.annotations.AnnotationVisibility; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttrNode; import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.ArithNode; import jadx.core.dex.instructions.ConstClassNode; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.FillArrayInsn; import jadx.core.dex.instructions.FilledNewArrayNode; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.IfOp; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.NewArrayNode; import jadx.core.dex.instructions.SwitchInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.NamedArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.IFieldInfoRef; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.visitors.regions.variables.ProcessVariables; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.dex.visitors.typeinference.TypeCompareEnum; import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnUtils; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.BlockUtils.replaceInsn; import static jadx.core.utils.ListUtils.allMatch; /** * Visitor for modify method instructions * (remove, replace, process exception handlers) */ @JadxVisitor( name = "ModVisitor", desc = "Modify method instructions", runBefore = { CodeShrinkVisitor.class, ProcessVariables.class } ) public class ModVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(ModVisitor.class); private static final long DOUBLE_TO_BITS = Double.doubleToLongBits(1); private static final long FLOAT_TO_BITS = Float.floatToIntBits(1); @Override public boolean visit(ClassNode cls) throws JadxException { replaceConstInAnnotations(cls); return true; } @Override public void visit(MethodNode mth) { if (mth.isNoCode()) { return; } InsnRemover remover = new InsnRemover(mth); replaceStep(mth, remover); removeStep(mth, remover); iterativeRemoveStep(mth); } private static void replaceStep(MethodNode mth, InsnRemover remover) { ClassNode parentClass = mth.getParentClass(); for (BlockNode block : mth.getBasicBlocks()) { remover.setBlock(block); List insnsList = block.getInstructions(); int size = insnsList.size(); for (int i = 0; i < size; i++) { InsnNode insn = insnsList.get(i); switch (insn.getType()) { case CONSTRUCTOR: processAnonymousConstructor(mth, ((ConstructorInsn) insn)); break; case CONST: case CONST_STR: case CONST_CLASS: replaceConst(mth, parentClass, block, i, insn); break; case SWITCH: replaceConstKeys(mth, parentClass, (SwitchInsn) insn); break; case NEW_ARRAY: // replace with filled array if 'fill-array' is next instruction NewArrayNode newArrInsn = (NewArrayNode) insn; InsnNode nextInsn = getFirstUseSkipMove(insn.getResult()); if (nextInsn != null && nextInsn.getType() == InsnType.FILL_ARRAY) { FillArrayInsn fillArrInsn = (FillArrayInsn) nextInsn; if (checkArrSizes(mth, newArrInsn, fillArrInsn)) { InsnNode filledArr = makeFilledArrayInsn(mth, newArrInsn, fillArrInsn); replaceInsn(mth, block, i, filledArr); remover.addAndUnbind(nextInsn); } } break; case MOVE_EXCEPTION: processMoveException(mth, block, insn, remover); break; case ARITH: processArith(mth, parentClass, (ArithNode) insn); break; case CMP_L: case CMP_G: inlineCMPInsns(mth, block, i, insn, remover); break; case CHECK_CAST: removeCheckCast(mth, block, i, (IndexInsnNode) insn); break; case CAST: fixPrimitiveCast(mth, block, i, insn); break; case IPUT: case IGET: fixFieldUsage(mth, (IndexInsnNode) insn); break; default: break; } } remover.perform(); } } /** * If field is not visible from use site => cast to origin class */ private static void fixFieldUsage(MethodNode mth, IndexInsnNode insn) { InsnArg instanceArg = insn.getArg(insn.getType() == InsnType.IGET ? 0 : 1); if (instanceArg.contains(AFlag.SUPER)) { return; } if (instanceArg.isInsnWrap() && ((InsnWrapArg) instanceArg).getWrapInsn().getType() == InsnType.CAST) { return; } FieldInfo fieldInfo = (FieldInfo) insn.getIndex(); ArgType clsType = fieldInfo.getDeclClass().getType(); ArgType instanceType = instanceArg.getType(); if (Objects.equals(clsType, instanceType)) { // cast not needed return; } FieldNode fieldNode = mth.root().resolveField(fieldInfo); if (fieldNode == null) { // unknown field TypeCompareEnum result = mth.root().getTypeCompare().compareTypes(instanceType, clsType); if (result.isEqual() || (result == TypeCompareEnum.NARROW_BY_GENERIC && !instanceType.isGenericType())) { return; } } else if (isFieldVisibleInMethod(fieldNode, mth)) { return; } // insert cast IndexInsnNode castInsn = new IndexInsnNode(InsnType.CAST, clsType, 1); castInsn.addArg(instanceArg.duplicate()); castInsn.add(AFlag.SYNTHETIC); castInsn.add(AFlag.EXPLICIT_CAST); InsnArg castArg = InsnArg.wrapInsnIntoArg(castInsn); castArg.setType(clsType); insn.replaceArg(instanceArg, castArg); InsnRemover.unbindArgUsage(mth, instanceArg); } private static boolean isFieldVisibleInMethod(FieldNode field, MethodNode mth) { AccessInfo accessFlags = field.getAccessFlags(); if (accessFlags.isPublic()) { return true; } ClassNode useCls = mth.getParentClass(); ClassNode fieldCls = field.getParentClass(); boolean sameScope = Objects.equals(useCls, fieldCls) && !mth.getAccessFlags().isStatic(); if (sameScope) { return true; } if (accessFlags.isPrivate()) { return false; } // package-private or protected if (Objects.equals(useCls.getClassInfo().getPackage(), fieldCls.getClassInfo().getPackage())) { // same package return true; } if (accessFlags.isPackagePrivate()) { return false; } // protected TypeCompareEnum result = mth.root().getTypeCompare().compareTypes(useCls, fieldCls); return result == TypeCompareEnum.NARROW; // true if use class is subclass of field class } private static void replaceConstKeys(MethodNode mth, ClassNode parentClass, SwitchInsn insn) { int[] keys = insn.getKeys(); int len = keys.length; for (int k = 0; k < len; k++) { IFieldInfoRef f = parentClass.getConstField(keys[k]); if (f != null) { insn.modifyKey(k, f); addFieldUsage(f, mth); } } } private static void fixPrimitiveCast(MethodNode mth, BlockNode block, int i, InsnNode insn) { // replace boolean to (byte/char/short/long/double/float) cast with ternary InsnArg castArg = insn.getArg(0); if (castArg.getType() == ArgType.BOOLEAN) { ArgType type = insn.getResult().getType(); if (type.isPrimitive()) { TernaryInsn ternary = makeBooleanConvertInsn(insn.getResult(), castArg, type); replaceInsn(mth, block, i, ternary); } } } public static TernaryInsn makeBooleanConvertInsn(RegisterArg result, InsnArg castArg, ArgType type) { InsnArg zero = LiteralArg.make(0, type); long litVal = 1; if (type == ArgType.DOUBLE) { litVal = DOUBLE_TO_BITS; } else if (type == ArgType.FLOAT) { litVal = FLOAT_TO_BITS; } InsnArg one = LiteralArg.make(litVal, type); IfNode ifNode = new IfNode(IfOp.EQ, -1, castArg, LiteralArg.litTrue()); IfCondition condition = IfCondition.fromIfNode(ifNode); return new TernaryInsn(condition, result, one, zero); } private void replaceConstInAnnotations(ClassNode cls) { if (cls.root().getArgs().isReplaceConsts()) { replaceConstsInAnnotationForAttrNode(cls, cls); cls.getFields().forEach(f -> replaceConstsInAnnotationForAttrNode(cls, f)); cls.getMethods().forEach((m) -> { replaceConstsInAnnotationForAttrNode(cls, m); replaceConstsInAnnotationForMethodParamsAttr(cls, m); }); } } private void replaceConstsInAnnotationForMethodParamsAttr(ClassNode cls, MethodNode m) { AnnotationMethodParamsAttr paramsAnnotation = m.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS); if (paramsAnnotation == null) { return; } paramsAnnotation.getParamList().forEach(annotationsList -> replaceConstsInAnnotationsAttr(cls, annotationsList)); } private void replaceConstsInAnnotationForAttrNode(ClassNode parentCls, AttrNode attrNode) { AnnotationsAttr annotationsList = attrNode.get(JadxAttrType.ANNOTATION_LIST); replaceConstsInAnnotationsAttr(parentCls, annotationsList); } private void replaceConstsInAnnotationsAttr(ClassNode parentCls, AnnotationsAttr annotationsList) { if (annotationsList == null) { return; } for (IAnnotation annotation : annotationsList.getAll()) { if (annotation.getVisibility() == AnnotationVisibility.SYSTEM) { continue; } for (Map.Entry entry : annotation.getValues().entrySet()) { entry.setValue(replaceConstValue(parentCls, entry.getValue())); } } } @SuppressWarnings("unchecked") private EncodedValue replaceConstValue(ClassNode parentCls, EncodedValue encodedValue) { if (encodedValue.getType() == EncodedType.ENCODED_ANNOTATION) { IAnnotation annotation = (IAnnotation) encodedValue.getValue(); for (Map.Entry entry : annotation.getValues().entrySet()) { entry.setValue(replaceConstValue(parentCls, entry.getValue())); } return encodedValue; } if (encodedValue.getType() == EncodedType.ENCODED_ARRAY) { List listVal = (List) encodedValue.getValue(); if (!listVal.isEmpty()) { listVal.replaceAll(v -> replaceConstValue(parentCls, v)); } return new EncodedValue(EncodedType.ENCODED_ARRAY, listVal); } IFieldInfoRef constField = parentCls.getConstField(encodedValue.getValue()); if (constField != null) { return new EncodedValue(EncodedType.ENCODED_FIELD, constField.getFieldInfo()); } return encodedValue; } private static void replaceConst(MethodNode mth, ClassNode parentClass, BlockNode block, int i, InsnNode insn) { IFieldInfoRef f; if (insn.getType() == InsnType.CONST_STR) { String s = ((ConstStringNode) insn).getString(); f = parentClass.getConstField(s); } else if (insn.getType() == InsnType.CONST_CLASS) { ArgType t = ((ConstClassNode) insn).getClsType(); f = parentClass.getConstField(t); } else { f = parentClass.getConstFieldByLiteralArg((LiteralArg) insn.getArg(0)); } if (f != null) { InsnNode inode = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0); inode.setResult(insn.getResult()); replaceInsn(mth, block, i, inode); addFieldUsage(f, mth); } } private static void processArith(MethodNode mth, ClassNode parentClass, ArithNode arithNode) { if (arithNode.getArgsCount() != 2) { throw new JadxRuntimeException("Invalid args count in insn: " + arithNode); } InsnArg litArg = arithNode.getArg(1); if (litArg.isLiteral()) { IFieldInfoRef f = parentClass.getConstFieldByLiteralArg((LiteralArg) litArg); if (f != null) { InsnNode fGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0); if (arithNode.replaceArg(litArg, InsnArg.wrapArg(fGet))) { addFieldUsage(f, mth); } } } } /** * Inline CMP instructions into 'if' to help conditions merging */ private static void inlineCMPInsns(MethodNode mth, BlockNode block, int i, InsnNode insn, InsnRemover remover) { RegisterArg resArg = insn.getResult(); List useList = resArg.getSVar().getUseList(); if (allMatch(useList, use -> InsnUtils.isInsnType(use.getParentInsn(), InsnType.IF))) { for (RegisterArg useArg : new ArrayList<>(useList)) { InsnNode useInsn = useArg.getParentInsn(); if (useInsn != null) { InsnArg wrapArg = InsnArg.wrapInsnIntoArg(insn.copyWithoutResult()); if (!useInsn.replaceArg(useArg, wrapArg)) { mth.addWarnComment("Failed to inline CMP insn: " + insn + " into " + useInsn); return; } } } remover.addAndUnbind(insn); } } private static boolean checkArrSizes(MethodNode mth, NewArrayNode newArrInsn, FillArrayInsn fillArrInsn) { int dataSize = fillArrInsn.getSize(); InsnArg arrSizeArg = newArrInsn.getArg(0); Object value = InsnUtils.getConstValueByArg(mth.root(), arrSizeArg); if (value instanceof LiteralArg) { long literal = ((LiteralArg) value).getLiteral(); return dataSize == (int) literal; } return false; } private static void removeCheckCast(MethodNode mth, BlockNode block, int i, IndexInsnNode insn) { InsnArg castArg = insn.getArg(0); if (castArg.isZeroLiteral()) { // always keep cast for 'null' insn.add(AFlag.EXPLICIT_CAST); return; } ArgType castType = (ArgType) insn.getIndex(); if (!ArgType.isCastNeeded(mth.root(), castArg.getType(), castType)) { RegisterArg result = insn.getResult(); result.setType(castArg.getType()); InsnNode move = new InsnNode(InsnType.MOVE, 1); move.setResult(result); move.addArg(castArg); replaceInsn(mth, block, i, move); return; } InsnNode prevCast = isCastDuplicate(insn); if (prevCast != null) { // replace previous cast with move InsnNode move = new InsnNode(InsnType.MOVE, 1); move.setResult(prevCast.getResult()); move.addArg(prevCast.getArg(0)); replaceInsn(mth, block, prevCast, move); } } private static @Nullable InsnNode isCastDuplicate(IndexInsnNode castInsn) { InsnArg arg = castInsn.getArg(0); if (arg.isRegister()) { SSAVar sVar = ((RegisterArg) arg).getSVar(); if (sVar != null && sVar.getUseCount() == 1 && !sVar.isUsedInPhi()) { InsnNode assignInsn = sVar.getAssign().getParentInsn(); if (assignInsn != null && assignInsn.getType() == InsnType.CHECK_CAST) { ArgType assignCastType = (ArgType) ((IndexInsnNode) assignInsn).getIndex(); if (assignCastType.equals(castInsn.getIndex())) { return assignInsn; } } } } return null; } /** * Remove unnecessary instructions */ private static void removeStep(MethodNode mth, InsnRemover remover) { for (BlockNode block : mth.getBasicBlocks()) { remover.setBlock(block); for (InsnNode insn : block.getInstructions()) { switch (insn.getType()) { case NOP: case GOTO: case NEW_INSTANCE: remover.addAndUnbind(insn); break; default: if (insn.contains(AFlag.REMOVE)) { remover.addAndUnbind(insn); } break; } } remover.perform(); } } private static void iterativeRemoveStep(MethodNode mth) { boolean changed; do { changed = false; for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { if (insn.getType() == InsnType.MOVE && insn.isAttrStorageEmpty() && isResultArgNotUsed(insn)) { InsnRemover.remove(mth, block, insn); changed = true; break; } } } } while (changed); } private static boolean isResultArgNotUsed(InsnNode insn) { RegisterArg result = insn.getResult(); if (result != null) { SSAVar ssaVar = result.getSVar(); return ssaVar.getUseCount() == 0; } return false; } /** * For args in anonymous constructor invoke apply: * - forbid inline into constructor call * - make variables final (compiler require this implicitly) */ private static void processAnonymousConstructor(MethodNode mth, ConstructorInsn co) { IMethodDetails callMthDetails = mth.root().getMethodUtils().getMethodDetails(co); if (!(callMthDetails instanceof MethodNode)) { return; } MethodNode callMth = (MethodNode) callMthDetails; if (!callMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR) || callMth.contains(AFlag.NO_SKIP_ARGS)) { return; } SkipMethodArgsAttr attr = callMth.get(AType.SKIP_MTH_ARGS); if (attr != null) { int argsCount = Math.min(callMth.getMethodInfo().getArgsCount(), co.getArgsCount()); for (int i = 0; i < argsCount; i++) { if (attr.isSkip(i)) { anonymousCallArgMod(co.getArg(i)); } } } else { // additional info not available apply mods to all args (the safest solution) co.getArguments().forEach(ModVisitor::anonymousCallArgMod); } } private static void anonymousCallArgMod(InsnArg arg) { arg.add(AFlag.DONT_INLINE); if (arg.isRegister()) { ((RegisterArg) arg).getSVar().getCodeVar().setFinal(true); } } /** * Return first usage instruction for arg. * If used only once try to follow move chain */ @Nullable private static InsnNode getFirstUseSkipMove(RegisterArg arg) { SSAVar sVar = arg.getSVar(); int useCount = sVar.getUseCount(); if (useCount == 0) { return null; } RegisterArg useArg = sVar.getUseList().get(0); InsnNode parentInsn = useArg.getParentInsn(); if (parentInsn == null) { return null; } if (useCount == 1 && parentInsn.getType() == InsnType.MOVE) { return getFirstUseSkipMove(parentInsn.getResult()); } return parentInsn; } private static InsnNode makeFilledArrayInsn(MethodNode mth, NewArrayNode newArrayNode, FillArrayInsn insn) { ArgType insnArrayType = newArrayNode.getArrayType(); ArgType insnElementType = insnArrayType.getArrayElement(); ArgType elType = insn.getElementType(); if (!elType.isTypeKnown() && insnElementType.isPrimitive() && elType.contains(insnElementType.getPrimitiveType())) { elType = insnElementType; } if (!elType.equals(insnElementType) && !insnArrayType.equals(ArgType.OBJECT)) { mth.addWarn("Incorrect type for fill-array insn " + InsnUtils.formatOffset(insn.getOffset()) + ", element type: " + elType + ", insn element type: " + insnElementType); } if (!elType.isTypeKnown()) { LOG.warn("Unknown array element type: {} in mth: {}", elType, mth); elType = insnElementType.isTypeKnown() ? insnElementType : elType.selectFirst(); if (elType == null) { throw new JadxRuntimeException("Null array element type"); } } List list = insn.getLiteralArgs(elType); InsnNode filledArr = new FilledNewArrayNode(elType, list.size()); filledArr.setResult(newArrayNode.getResult().duplicate()); for (LiteralArg arg : list) { IFieldInfoRef f = mth.getParentClass().getConstFieldByLiteralArg(arg); if (f != null) { InsnNode fGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0); filledArr.addArg(InsnArg.wrapArg(fGet)); addFieldUsage(f, mth); } else { filledArr.addArg(arg.duplicate()); } } return filledArr; } private static void processMoveException(MethodNode mth, BlockNode block, InsnNode insn, InsnRemover remover) { ExcHandlerAttr excHandlerAttr = block.get(AType.EXC_HANDLER); if (excHandlerAttr == null) { return; } ExceptionHandler excHandler = excHandlerAttr.getHandler(); // result arg used both in this insn and exception handler, RegisterArg resArg = insn.getResult(); ArgType type = excHandler.getArgType(); String name = excHandler.isCatchAll() ? "th" : "e"; if (resArg.getName() == null) { resArg.setName(name); } SSAVar sVar = insn.getResult().getSVar(); if (sVar.getUseCount() == 0) { excHandler.setArg(new NamedArg(name, type)); remover.addAndUnbind(insn); } else if (sVar.isUsedInPhi()) { // exception var moved to external variable => replace with 'move' insn InsnNode moveInsn = new InsnNode(InsnType.MOVE, 1); moveInsn.setResult(insn.getResult()); NamedArg namedArg = new NamedArg(name, type); moveInsn.addArg(namedArg); excHandler.setArg(namedArg); replaceInsn(mth, block, 0, moveInsn); } block.copyAttributeFrom(insn, AType.CODE_COMMENTS); // save comment } public static void addFieldUsage(IFieldInfoRef fieldData, MethodNode mth) { if (fieldData instanceof FieldNode) { ((FieldNode) fieldData).addUseIn(mth); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/MoveInlineVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.dex.visitors.ssa.SSATransform; import jadx.core.utils.InsnRemover; @JadxVisitor( name = "MoveInlineVisitor", desc = "Inline redundant move instructions", runAfter = SSATransform.class, runBefore = CodeShrinkVisitor.class ) public class MoveInlineVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) { if (mth.isNoCode()) { return; } moveInline(mth); } public static void moveInline(MethodNode mth) { InsnRemover remover = new InsnRemover(mth); for (BlockNode block : mth.getBasicBlocks()) { remover.setBlock(block); for (InsnNode insn : block.getInstructions()) { if (insn.getType() != InsnType.MOVE) { continue; } if (processMove(mth, insn)) { block.add(AFlag.MOVE_INLINED); remover.addAndUnbind(insn); } } remover.perform(); } } private static boolean processMove(MethodNode mth, InsnNode move) { RegisterArg resultArg = move.getResult(); InsnArg moveArg = move.getArg(0); if (resultArg.sameRegAndSVar(moveArg)) { return true; } if (moveArg.isRegister()) { RegisterArg moveReg = (RegisterArg) moveArg; if (moveReg.getSVar().isAssignInPhi()) { // don't mix already merged variables return false; } } SSAVar ssaVar = resultArg.getSVar(); if (ssaVar.getUseList().isEmpty()) { // unused result return true; } if (ssaVar.isUsedInPhi()) { return false; // TODO: review conditions of 'up' move inline (test TestMoveInline) // return deleteMove(mth, move); } RegDebugInfoAttr debugInfo = moveArg.get(AType.REG_DEBUG_INFO); for (RegisterArg useArg : ssaVar.getUseList()) { InsnNode useInsn = useArg.getParentInsn(); if (useInsn == null) { return false; } if (debugInfo == null) { RegDebugInfoAttr debugInfoAttr = useArg.get(AType.REG_DEBUG_INFO); if (debugInfoAttr != null) { debugInfo = debugInfoAttr; } } } // all checks passed, execute inline for (RegisterArg useArg : new ArrayList<>(ssaVar.getUseList())) { InsnNode useInsn = useArg.getParentInsn(); if (useInsn == null) { continue; } InsnArg replaceArg; if (moveArg.isRegister()) { replaceArg = ((RegisterArg) moveArg).duplicate(useArg.getInitType()); } else { replaceArg = moveArg.duplicate(); } useInsn.inheritMetadata(move); replaceArg.copyAttributesFrom(useArg); if (debugInfo != null) { replaceArg.addAttr(debugInfo); } if (!useInsn.replaceArg(useArg, replaceArg)) { mth.addWarnComment("Failed to replace arg in insn: " + useInsn); } } return true; } private static boolean deleteMove(MethodNode mth, InsnNode move) { InsnArg moveArg = move.getArg(0); if (!moveArg.isRegister()) { return false; } RegisterArg moveReg = (RegisterArg) moveArg; SSAVar ssaVar = moveReg.getSVar(); if (ssaVar.getUseCount() != 1 || ssaVar.isUsedInPhi()) { return false; } RegisterArg assignArg = ssaVar.getAssign(); InsnNode parentInsn = assignArg.getParentInsn(); if (parentInsn == null) { return false; } if (parentInsn.getSourceLine() != move.getSourceLine() || moveArg.contains(AType.REG_DEBUG_INFO)) { // preserve debug info return false; } // set move result into parent insn result InsnRemover.unbindAllArgs(mth, move); InsnRemover.unbindResult(mth, parentInsn); RegisterArg resArg = parentInsn.getResult(); RegisterArg newResArg = move.getResult().duplicate(resArg.getInitType()); newResArg.copyAttributesFrom(resArg); parentInsn.setResult(newResArg); return true; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.core.clsp.ClspClass; import jadx.core.clsp.ClspMethod; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodBridgeAttr; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.RenameReasonAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.rename.RenameVisitor; import jadx.core.dex.visitors.typeinference.TypeCompare; import jadx.core.dex.visitors.typeinference.TypeCompareEnum; import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; @JadxVisitor( name = "OverrideMethodVisitor", desc = "Mark override methods and revert type erasure", runBefore = { TypeInferenceVisitor.class, RenameVisitor.class } ) public class OverrideMethodVisitor extends AbstractVisitor { @Override public boolean visit(ClassNode cls) throws JadxException { SuperTypesData superData = collectSuperTypes(cls); if (superData != null) { for (MethodNode mth : cls.getMethods()) { processMth(mth, superData); } } return true; } private void processMth(MethodNode mth, SuperTypesData superData) { if (mth.isConstructor() || mth.getAccessFlags().isStatic() || mth.getAccessFlags().isPrivate()) { return; } MethodOverrideAttr attr = processOverrideMethods(mth, superData); if (attr != null) { if (attr.getBaseMethods().isEmpty()) { throw new JadxRuntimeException("No base methods for override attribute: " + attr.getOverrideList()); } mth.addAttr(attr); IMethodDetails baseMth = Utils.getOne(attr.getBaseMethods()); if (baseMth != null) { boolean updated = fixMethodReturnType(mth, baseMth, superData); updated |= fixMethodArgTypes(mth, baseMth, superData); if (updated) { // check if new signature cause method collisions checkMethodSignatureCollisions(mth, mth.root().getArgs().isRenameValid()); } } } } private MethodOverrideAttr processOverrideMethods(MethodNode mth, SuperTypesData superData) { MethodOverrideAttr result = mth.get(AType.METHOD_OVERRIDE); if (result != null) { return result; } ClassNode cls = mth.getParentClass(); String signature = mth.getMethodInfo().makeSignature(false); List overrideList = new ArrayList<>(); Set baseMethods = new HashSet<>(); for (ArgType superType : superData.getSuperTypes()) { ClassNode classNode = mth.root().resolveClass(superType); if (classNode != null) { MethodNode ovrdMth = searchOverriddenMethod(classNode, mth, signature); if (ovrdMth != null) { if (isMethodVisibleInCls(ovrdMth, cls)) { overrideList.add(ovrdMth); MethodOverrideAttr attr = ovrdMth.get(AType.METHOD_OVERRIDE); if (attr != null) { addBaseMethod(superData, overrideList, baseMethods, superType); return buildOverrideAttr(mth, overrideList, baseMethods, attr); } } } } else { ClspClass clsDetails = mth.root().getClsp().getClsDetails(superType); if (clsDetails != null) { Map methodsMap = clsDetails.getMethodsMap(); for (Map.Entry entry : methodsMap.entrySet()) { String mthShortId = entry.getKey(); // do not check full signature, classpath methods can be trusted // i.e. doesn't contain methods with same signature in one class if (mthShortId.startsWith(signature)) { overrideList.add(entry.getValue()); break; } } } } addBaseMethod(superData, overrideList, baseMethods, superType); } return buildOverrideAttr(mth, overrideList, baseMethods, null); } private void addBaseMethod(SuperTypesData superData, List overrideList, Set baseMethods, ArgType superType) { if (superData.getEndTypes().contains(superType.getObject())) { IMethodDetails last = Utils.last(overrideList); if (last != null) { baseMethods.add(last); } } } @Nullable private MethodNode searchOverriddenMethod(ClassNode cls, MethodNode mth, String signature) { // search by exact full signature (with return value) to fight obfuscation (see test // 'TestOverrideWithSameName') String shortId = mth.getMethodInfo().getShortId(); for (MethodNode supMth : cls.getMethods()) { if (supMth.getMethodInfo().getShortId().equals(shortId) && !supMth.getAccessFlags().isStatic()) { return supMth; } } // search by signature without return value and check if return value is wider type for (MethodNode supMth : cls.getMethods()) { if (supMth.getMethodInfo().getShortId().startsWith(signature) && !supMth.getAccessFlags().isStatic()) { TypeCompare typeCompare = cls.root().getTypeCompare(); ArgType supRetType = supMth.getMethodInfo().getReturnType(); ArgType mthRetType = mth.getMethodInfo().getReturnType(); TypeCompareEnum res = typeCompare.compareTypes(supRetType, mthRetType); if (res.isWider()) { return supMth; } if (res == TypeCompareEnum.UNKNOWN || res == TypeCompareEnum.CONFLICT) { mth.addDebugComment("Possible override for method " + supMth.getMethodInfo().getFullId()); } } } return null; } @Nullable private MethodOverrideAttr buildOverrideAttr(MethodNode mth, List overrideList, Set baseMethods, @Nullable MethodOverrideAttr attr) { if (overrideList.isEmpty() && attr == null) { return null; } if (attr == null) { // traced to base method List cleanOverrideList = overrideList.stream().distinct().collect(Collectors.toList()); return applyOverrideAttr(mth, cleanOverrideList, baseMethods, false); } // trace stopped at already processed method -> start merging List mergedOverrideList = Utils.mergeLists(overrideList, attr.getOverrideList()); List cleanOverrideList = mergedOverrideList.stream().distinct().collect(Collectors.toList()); Set mergedBaseMethods = Utils.mergeSets(baseMethods, attr.getBaseMethods()); return applyOverrideAttr(mth, cleanOverrideList, mergedBaseMethods, true); } private MethodOverrideAttr applyOverrideAttr(MethodNode mth, List overrideList, Set baseMethods, boolean update) { // don't rename method if override list contains not resolved method boolean dontRename = overrideList.stream().anyMatch(m -> !(m instanceof MethodNode)); SortedSet relatedMethods = null; List mthNodes = getMethodNodes(mth, overrideList); if (update) { // merge related methods from all override attributes for (MethodNode mthNode : mthNodes) { MethodOverrideAttr ovrdAttr = mthNode.get(AType.METHOD_OVERRIDE); if (ovrdAttr != null) { // use one of already allocated sets relatedMethods = ovrdAttr.getRelatedMthNodes(); break; } } if (relatedMethods != null) { relatedMethods.addAll(mthNodes); } else { relatedMethods = new TreeSet<>(mthNodes); } for (MethodNode mthNode : mthNodes) { MethodOverrideAttr ovrdAttr = mthNode.get(AType.METHOD_OVERRIDE); if (ovrdAttr != null) { SortedSet set = ovrdAttr.getRelatedMthNodes(); if (relatedMethods != set) { relatedMethods.addAll(set); } } } } else { relatedMethods = new TreeSet<>(mthNodes); } int depth = 0; for (MethodNode mthNode : mthNodes) { if (dontRename) { mthNode.add(AFlag.DONT_RENAME); } if (depth == 0) { // skip current (first) method depth = 1; continue; } if (update) { MethodOverrideAttr ovrdAttr = mthNode.get(AType.METHOD_OVERRIDE); if (ovrdAttr != null) { ovrdAttr.setRelatedMthNodes(relatedMethods); continue; } } mthNode.addAttr(new MethodOverrideAttr(Utils.listTail(overrideList, depth), relatedMethods, baseMethods)); depth++; } return new MethodOverrideAttr(overrideList, relatedMethods, baseMethods); } @NotNull private List getMethodNodes(MethodNode mth, List overrideList) { List list = new ArrayList<>(1 + overrideList.size()); list.add(mth); for (IMethodDetails md : overrideList) { if (md instanceof MethodNode) { list.add((MethodNode) md); } } return list; } /** * NOTE: Simplified version of method from ModVisitor.isFieldVisibleInMethod */ private boolean isMethodVisibleInCls(MethodNode superMth, ClassNode cls) { AccessInfo accessFlags = superMth.getAccessFlags(); if (accessFlags.isPrivate()) { return false; } if (accessFlags.isPublic() || accessFlags.isProtected()) { return true; } // package-private return Objects.equals(superMth.getParentClass().getPackage(), cls.getPackage()); } private static final class SuperTypesData { private final List superTypes; private final Set endTypes; private SuperTypesData(List superTypes, Set endTypes) { this.superTypes = superTypes; this.endTypes = endTypes; } public List getSuperTypes() { return superTypes; } public Set getEndTypes() { return endTypes; } } @Nullable private SuperTypesData collectSuperTypes(ClassNode cls) { Set superTypes = new LinkedHashSet<>(); Set endTypes = new HashSet<>(); collectSuperTypes(cls, superTypes, endTypes); if (superTypes.isEmpty()) { return null; } if (endTypes.isEmpty()) { throw new JadxRuntimeException("No end types in class hierarchy: " + cls); } return new SuperTypesData(new ArrayList<>(superTypes), endTypes); } private void collectSuperTypes(ClassNode cls, Set superTypes, Set endTypes) { RootNode root = cls.root(); int k = 0; ArgType superClass = cls.getSuperClass(); if (superClass != null) { k += addSuperType(root, superTypes, endTypes, superClass); } for (ArgType iface : cls.getInterfaces()) { k += addSuperType(root, superTypes, endTypes, iface); } if (k == 0) { endTypes.add(cls.getType().getObject()); } } private int addSuperType(RootNode root, Set superTypes, Set endTypes, ArgType superType) { if (Objects.equals(superType, ArgType.OBJECT)) { return 0; } if (!superTypes.add(superType)) { // found 'super' loop, stop processing return 0; } ClassNode classNode = root.resolveClass(superType); if (classNode != null) { collectSuperTypes(classNode, superTypes, endTypes); return 1; } ClspClass clsDetails = root.getClsp().getClsDetails(superType); if (clsDetails != null) { int k = 0; for (ArgType parentType : clsDetails.getParents()) { k += addSuperType(root, superTypes, endTypes, parentType); } if (k == 0) { endTypes.add(superType.getObject()); } return 1; } // no info found => treat as hierarchy end endTypes.add(superType.getObject()); return 1; } private boolean fixMethodReturnType(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData) { ArgType returnType = mth.getReturnType(); if (returnType == ArgType.VOID) { return false; } boolean updated = updateReturnType(mth, baseMth, superData); if (updated) { mth.addDebugComment("Return type fixed from '" + returnType + "' to match base method"); } return updated; } private boolean updateReturnType(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData) { ArgType baseReturnType = baseMth.getReturnType(); if (mth.getReturnType().equals(baseReturnType)) { return false; } if (!baseReturnType.containsTypeVariable()) { return false; } TypeCompare typeCompare = mth.root().getTypeUpdate().getTypeCompare(); ArgType baseCls = baseMth.getMethodInfo().getDeclClass().getType(); for (ArgType superType : superData.getSuperTypes()) { TypeCompareEnum compareResult = typeCompare.compareTypes(superType, baseCls); if (compareResult == TypeCompareEnum.NARROW_BY_GENERIC) { ArgType targetRetType = mth.root().getTypeUtils().replaceClassGenerics(superType, baseReturnType); if (targetRetType != null && !targetRetType.containsTypeVariable() && !targetRetType.equals(mth.getReturnType())) { mth.updateReturnType(targetRetType); return true; } } } return false; } private boolean fixMethodArgTypes(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData) { List mthArgTypes = mth.getArgTypes(); List baseArgTypes = baseMth.getArgTypes(); if (mthArgTypes.equals(baseArgTypes)) { return false; } int argCount = mthArgTypes.size(); if (argCount != baseArgTypes.size()) { return false; } boolean changed = false; List newArgTypes = new ArrayList<>(argCount); for (int argNum = 0; argNum < argCount; argNum++) { ArgType newType = updateArgType(mth, baseMth, superData, argNum); if (newType != null) { changed = true; newArgTypes.add(newType); } else { newArgTypes.add(mthArgTypes.get(argNum)); } } if (changed) { mth.updateArgTypes(newArgTypes, "Method arguments types fixed to match base method"); } return changed; } private ArgType updateArgType(MethodNode mth, IMethodDetails baseMth, SuperTypesData superData, int argNum) { ArgType arg = mth.getArgTypes().get(argNum); ArgType baseArg = baseMth.getArgTypes().get(argNum); if (arg.equals(baseArg)) { return null; } if (!baseArg.containsTypeVariable()) { return null; } TypeCompare typeCompare = mth.root().getTypeUpdate().getTypeCompare(); ArgType baseCls = baseMth.getMethodInfo().getDeclClass().getType(); for (ArgType superType : superData.getSuperTypes()) { TypeCompareEnum compareResult = typeCompare.compareTypes(superType, baseCls); if (compareResult == TypeCompareEnum.NARROW_BY_GENERIC) { ArgType targetArgType = mth.root().getTypeUtils().replaceClassGenerics(superType, baseArg); if (targetArgType != null && !targetArgType.containsTypeVariable() && !targetArgType.equals(arg)) { return targetArgType; } } } return null; } private void checkMethodSignatureCollisions(MethodNode mth, boolean rename) { String mthName = mth.getMethodInfo().getAlias(); String newSignature = MethodInfo.makeShortId(mthName, mth.getArgTypes(), null); for (MethodNode otherMth : mth.getParentClass().getMethods()) { String otherMthName = otherMth.getAlias(); if (otherMthName.equals(mthName) && otherMth != mth) { String otherSignature = otherMth.getMethodInfo().makeSignature(true, false); if (otherSignature.equals(newSignature)) { if (rename) { if (otherMth.contains(AFlag.DONT_RENAME) || otherMth.contains(AType.METHOD_OVERRIDE)) { otherMth.addWarnComment("Can't rename method to resolve collision"); } else { otherMth.getMethodInfo().setAlias(makeNewAlias(otherMth)); otherMth.addAttr(new RenameReasonAttr("avoid collision after fix types in other method")); } } otherMth.addAttr(new MethodBridgeAttr(mth)); return; } } } } // TODO: at this point deobfuscator is not available and map file already saved private static String makeNewAlias(MethodNode mth) { ClassNode cls = mth.getParentClass(); String baseName = mth.getAlias(); int k = 2; while (true) { String alias = baseName + k; MethodNode methodNode = cls.searchMethodByShortName(alias); if (methodNode == null) { return alias; } k++; } } @Override public String getName() { return "OverrideMethodVisitor"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/PrepareForCodeGen.java ================================================ package jadx.core.dex.visitors; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Stream; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.IFieldRef; import jadx.api.plugins.input.data.annotations.AnnotationVisibility; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttrNode; import jadx.core.dex.attributes.nodes.DeclareVariablesAttr; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.ArithNode; import jadx.core.dex.instructions.ArithOp; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnContainer; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.regions.conditions.IfCondition.Mode; import jadx.core.dex.visitors.regions.variables.ProcessVariables; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnList; import jadx.core.utils.exceptions.JadxException; /** * Prepare instructions for code generation pass, * most of this modification breaks register dependencies, * so this pass must be just before CodeGen. */ @JadxVisitor( name = "PrepareForCodeGen", desc = "Prepare instructions for code generation pass", runAfter = { CodeShrinkVisitor.class, ClassModifier.class, ProcessVariables.class } ) public class PrepareForCodeGen extends AbstractVisitor { @Override public String getName() { return "PrepareForCodeGen"; } @Override public boolean visit(ClassNode cls) throws JadxException { if (cls.root().getArgs().isDebugInfo()) { setClassSourceLine(cls); } collectFieldsUsageInAnnotations(cls); return true; } @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } for (BlockNode block : mth.getBasicBlocks()) { if (block.contains(AFlag.DONT_GENERATE)) { continue; } removeInstructions(block); checkInline(block); removeParenthesis(block); modifyArith(block); checkConstUsage(block); addNullCasts(mth, block); } moveConstructorInConstructor(mth); collectFieldsUsageInAnnotations(mth, mth); } private static void removeInstructions(BlockNode block) { Iterator it = block.getInstructions().iterator(); while (it.hasNext()) { InsnNode insn = it.next(); switch (insn.getType()) { case NOP: case MONITOR_ENTER: case MONITOR_EXIT: case MOVE_EXCEPTION: it.remove(); break; case CONSTRUCTOR: ConstructorInsn co = (ConstructorInsn) insn; if (co.isSelf()) { it.remove(); } break; case MOVE: // remove redundant moves: unused result and same args names (a = a;) RegisterArg result = insn.getResult(); if (result.getSVar().getUseCount() == 0 && result.isNameEquals(insn.getArg(0))) { it.remove(); } break; default: break; } } } private static void checkInline(BlockNode block) { List list = block.getInstructions(); for (int i = 0; i < list.size(); i++) { InsnNode insn = list.get(i); // replace 'move' with inner wrapped instruction if (insn.getType() == InsnType.MOVE && insn.getArg(0).isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) insn.getArg(0)).getWrapInsn(); wrapInsn.setResult(insn.getResult()); wrapInsn.copyAttributesFrom(insn); list.set(i, wrapInsn); } } } /** * Add explicit type for non int constants */ private static void checkConstUsage(BlockNode block) { for (InsnNode blockInsn : block.getInstructions()) { blockInsn.visitInsns(insn -> { if (forbidExplicitType(insn.getType())) { return; } for (InsnArg arg : insn.getArguments()) { if (arg.isLiteral() && arg.getType() != ArgType.INT) { arg.add(AFlag.EXPLICIT_PRIMITIVE_TYPE); } } }); } } private static boolean forbidExplicitType(InsnType type) { switch (type) { case CONST: case CAST: case IF: case FILLED_NEW_ARRAY: case APUT: case ARITH: return true; default: return false; } } private static void removeParenthesis(BlockNode block) { for (InsnNode insn : block.getInstructions()) { removeParenthesis(insn); } } /** * Remove parenthesis for wrapped insn in arith '+' or '-' * ('(a + b) +c' => 'a + b + c') */ private static void removeParenthesis(InsnNode insn) { if (insn.getType() == InsnType.ARITH) { ArithNode arith = (ArithNode) insn; ArithOp op = arith.getOp(); if (op == ArithOp.ADD || op == ArithOp.MUL || op == ArithOp.AND || op == ArithOp.OR) { for (int i = 0; i < 2; i++) { InsnArg arg = arith.getArg(i); if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); if (wrapInsn.getType() == InsnType.ARITH && ((ArithNode) wrapInsn).getOp() == op) { wrapInsn.add(AFlag.DONT_WRAP); } removeParenthesis(wrapInsn); } } } } else { if (insn.getType() == InsnType.TERNARY) { removeParenthesis(((TernaryInsn) insn).getCondition()); } for (InsnArg arg : insn.getArguments()) { if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); removeParenthesis(wrapInsn); } } } } private static void removeParenthesis(IfCondition cond) { Mode mode = cond.getMode(); for (IfCondition c : cond.getArgs()) { if (c.getMode() == mode) { c.add(AFlag.DONT_WRAP); } } } /** * Replace arithmetic operation with short form * ('a = a + 2' => 'a += 2') */ private static void modifyArith(BlockNode block) { List list = block.getInstructions(); for (InsnNode insn : list) { if (insn.getType() == InsnType.ARITH && !insn.contains(AFlag.ARITH_ONEARG) && !insn.contains(AFlag.DECLARE_VAR)) { RegisterArg res = insn.getResult(); InsnArg arg = insn.getArg(0); boolean replace = false; if (res.equals(arg)) { replace = true; } else if (arg.isRegister()) { RegisterArg regArg = (RegisterArg) arg; replace = res.sameCodeVar(regArg); } if (replace) { insn.setResult(null); insn.add(AFlag.ARITH_ONEARG); } } } } /** * Check that 'super' or 'this' call in constructor is a first instruction. * Otherwise, move to the top and add a warning. */ private void moveConstructorInConstructor(MethodNode mth) { if (!mth.isConstructor()) { return; } ConstructorInsn ctrInsn = searchConstructorCall(mth); if (ctrInsn == null || ctrInsn.contains(AFlag.DONT_GENERATE)) { return; } boolean firstInsn = BlockUtils.isFirstInsn(mth, ctrInsn); DeclareVariablesAttr declVarsAttr = mth.getRegion().get(AType.DECLARE_VARIABLES); if (firstInsn && declVarsAttr == null) { // move not needed return; } String callType = ctrInsn.getCallType().toString().toLowerCase(); BlockNode blockByInsn = BlockUtils.getBlockByInsn(mth, ctrInsn); if (blockByInsn == null) { mth.addWarn("Failed to move " + callType + " instruction to top"); return; } if (!firstInsn) { Set regArgs = new HashSet<>(); ctrInsn.getRegisterArgs(regArgs); regArgs.remove(mth.getThisArg()); mth.getArgRegs().forEach(regArgs::remove); if (!regArgs.isEmpty()) { mth.addWarnComment("Illegal instructions before constructor call"); return; } mth.addWarnComment("'" + callType + "' call moved to the top of the method (can break code semantics)"); } // move confirmed InsnList.remove(blockByInsn, ctrInsn); mth.getRegion().getSubBlocks().add(0, new InsnContainer(ctrInsn)); } private @Nullable ConstructorInsn searchConstructorCall(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { if (insn.getType() == InsnType.CONSTRUCTOR) { ConstructorInsn ctrInsn = (ConstructorInsn) insn; if (ctrInsn.isSuper() || ctrInsn.isThis()) { return ctrInsn; } return null; } } } return null; } /** * Use source line from top method */ private void setClassSourceLine(ClassNode cls) { for (ClassNode innerClass : cls.getInnerClasses()) { setClassSourceLine(innerClass); } int minLine = Stream.of(cls.getMethods(), cls.getInnerClasses(), cls.getFields()) .flatMap(Collection::stream) .filter(mth -> !mth.contains(AFlag.DONT_GENERATE)) .filter(mth -> mth.getSourceLine() != 0) .mapToInt(LineAttrNode::getSourceLine) .min() .orElse(0); if (minLine != 0) { cls.setSourceLine(minLine - 1); } } private void collectFieldsUsageInAnnotations(ClassNode cls) { MethodNode useMth = cls.getDefaultConstructor(); if (useMth == null && !cls.getMethods().isEmpty()) { useMth = cls.getMethods().get(0); } if (useMth == null) { return; } collectFieldsUsageInAnnotations(useMth, cls); MethodNode finalUseMth = useMth; cls.getFields().forEach(f -> collectFieldsUsageInAnnotations(finalUseMth, f)); } private void collectFieldsUsageInAnnotations(MethodNode mth, AttrNode attrNode) { AnnotationsAttr annotationsList = attrNode.get(JadxAttrType.ANNOTATION_LIST); if (annotationsList == null) { return; } for (IAnnotation annotation : annotationsList.getAll()) { if (annotation.getVisibility() == AnnotationVisibility.SYSTEM) { continue; } for (Map.Entry entry : annotation.getValues().entrySet()) { checkEncodedValue(mth, entry.getValue()); } } } @SuppressWarnings("unchecked") private void checkEncodedValue(MethodNode mth, EncodedValue encodedValue) { switch (encodedValue.getType()) { case ENCODED_FIELD: Object fieldData = encodedValue.getValue(); FieldInfo fieldInfo; if (fieldData instanceof IFieldRef) { fieldInfo = FieldInfo.fromRef(mth.root(), (IFieldRef) fieldData); } else { fieldInfo = (FieldInfo) fieldData; } FieldNode fieldNode = mth.root().resolveField(fieldInfo); if (fieldNode != null) { fieldNode.addUseIn(mth); } break; case ENCODED_ANNOTATION: IAnnotation annotation = (IAnnotation) encodedValue.getValue(); annotation.getValues().forEach((k, v) -> checkEncodedValue(mth, v)); break; case ENCODED_ARRAY: List valueList = (List) encodedValue.getValue(); valueList.forEach(v -> checkEncodedValue(mth, v)); break; } } private void addNullCasts(MethodNode mth, BlockNode block) { for (InsnNode insn : block.getInstructions()) { switch (insn.getType()) { case INVOKE: verifyNullCast(mth, ((InvokeNode) insn).getInstanceArg()); break; case ARRAY_LENGTH: verifyNullCast(mth, insn.getArg(0)); break; } } } private void verifyNullCast(MethodNode mth, InsnArg arg) { if (arg != null && arg.isZeroConst()) { ArgType castType = arg.getType(); IndexInsnNode castInsn = new IndexInsnNode(InsnType.CAST, castType, 1); castInsn.addArg(InsnArg.lit(0, castType)); arg.wrapInstruction(mth, castInsn); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.AccessFlags; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.AnonymousClassAttr; import jadx.core.dex.attributes.nodes.AnonymousClassAttr.InlineType; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.usage.UsageInfoVisitor; import jadx.core.utils.ListUtils; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "ProcessAnonymous", desc = "Mark anonymous and lambda classes (for future inline)", runAfter = { UsageInfoVisitor.class } ) @SuppressWarnings("BooleanMethodIsAlwaysInverted") public class ProcessAnonymous extends AbstractVisitor { private boolean inlineAnonymousClasses; @Override public void init(RootNode root) { inlineAnonymousClasses = root.getArgs().isInlineAnonymousClasses(); if (!inlineAnonymousClasses) { return; } root.getClasses().forEach(ProcessAnonymous::processClass); mergeAnonymousDeps(root); } @Override public boolean visit(ClassNode cls) throws JadxException { if (inlineAnonymousClasses && cls.contains(AFlag.CLASS_UNLOADED)) { // enter only on class reload visitClassAndInners(cls); } return false; } private void visitClassAndInners(ClassNode cls) { processClass(cls); cls.getInnerClasses().forEach(this::visitClassAndInners); } private static void processClass(ClassNode cls) { try { markAnonymousClass(cls); } catch (StackOverflowError | Exception e) { cls.addError("Anonymous visitor error", e); } } private static void markAnonymousClass(ClassNode cls) { if (!canBeAnonymous(cls)) { return; } MethodNode anonymousConstructor = ListUtils.filterOnlyOne(cls.getMethods(), MethodNode::isConstructor); if (anonymousConstructor == null) { return; } InlineType inlineType = checkUsage(cls, anonymousConstructor); if (inlineType == null) { return; } ArgType baseType = getBaseType(cls); if (baseType == null) { return; } ClassNode outerCls; if (inlineType == InlineType.INSTANCE_FIELD) { outerCls = cls.getUseInMth().get(0).getParentClass(); } else { outerCls = anonymousConstructor.getUseIn().get(0).getParentClass(); } outerCls.addInlinedClass(cls); cls.addAttr(new AnonymousClassAttr(outerCls, baseType, inlineType)); cls.add(AFlag.DONT_GENERATE); anonymousConstructor.add(AFlag.ANONYMOUS_CONSTRUCTOR); // force anonymous class to be processed before outer class, // actual usage of outer class will be removed at anonymous class process, // see ModVisitor.processAnonymousConstructor method ClassNode topOuterCls = outerCls.getTopParentClass(); cls.removeDependency(topOuterCls); ListUtils.safeRemove(outerCls.getUseIn(), cls); // move dependency to codegen stage if (cls.isTopClass()) { topOuterCls.removeDependency(cls); topOuterCls.addCodegenDep(cls); } } private static void undoAnonymousMark(ClassNode cls) { AnonymousClassAttr attr = cls.get(AType.ANONYMOUS_CLASS); ClassNode outerCls = attr.getOuterCls(); cls.setDependencies(ListUtils.safeAdd(cls.getDependencies(), outerCls.getTopParentClass())); outerCls.setUseIn(ListUtils.safeAdd(outerCls.getUseIn(), cls)); cls.remove(AType.ANONYMOUS_CLASS); cls.remove(AFlag.DONT_GENERATE); for (MethodNode mth : cls.getMethods()) { if (mth.isConstructor()) { mth.remove(AFlag.ANONYMOUS_CONSTRUCTOR); } } cls.addDebugComment("Anonymous mark cleared"); } private void mergeAnonymousDeps(RootNode root) { // Collect edges to build bidirectional tree: // inline edge: anonymous -> outer (one-to-one) // use edges: outer -> *anonymous (one-to-many) Map inlineMap = new HashMap<>(); Map> useMap = new HashMap<>(); for (ClassNode anonymousCls : root.getClasses()) { AnonymousClassAttr attr = anonymousCls.get(AType.ANONYMOUS_CLASS); if (attr != null) { ClassNode outerCls = attr.getOuterCls(); List list = useMap.get(outerCls); if (list == null || list.isEmpty()) { list = new ArrayList<>(2); useMap.put(outerCls, list); } list.add(anonymousCls); useMap.putIfAbsent(anonymousCls, Collections.emptyList()); // put leaf explicitly inlineMap.put(anonymousCls, outerCls); } } if (inlineMap.isEmpty()) { return; } // starting from leaf process deps in nodes up to root Set added = new HashSet<>(); useMap.forEach((key, list) -> { if (list.isEmpty()) { added.clear(); updateDeps(key, inlineMap, added); } }); for (ClassNode cls : root.getClasses()) { List deps = cls.getCodegenDeps(); if (deps.size() > 1) { // distinct sorted dep, reusing collections to reduce memory allocations :) added.clear(); added.addAll(deps); deps.clear(); deps.addAll(added); Collections.sort(deps); } } } private void updateDeps(ClassNode leafCls, Map inlineMap, Set added) { ClassNode topNode; ClassNode current = leafCls; while (true) { if (!added.add(current)) { current.addWarnComment("Loop in anonymous inline: " + current + ", path: " + added); added.forEach(ProcessAnonymous::undoAnonymousMark); return; } ClassNode next = inlineMap.get(current); if (next == null) { topNode = current.getTopParentClass(); break; } current = next; } if (added.size() <= 2) { // first level deps already processed return; } List deps = topNode.getCodegenDeps(); if (deps.isEmpty()) { deps = new ArrayList<>(added.size()); topNode.setCodegenDeps(deps); } for (ClassNode add : added) { deps.add(add.getTopParentClass()); } } private static boolean canBeAnonymous(ClassNode cls) { if (cls.getAccessFlags().isSynthetic()) { return true; } String shortName = cls.getClassInfo().getShortName(); if (shortName.contains("$") || Character.isDigit(shortName.charAt(0))) { return true; } if (cls.getUseIn().size() == 1 && cls.getUseInMth().size() == 1) { MethodNode useMth = cls.getUseInMth().get(0); // allow use in enum class init return useMth.getMethodInfo().isClassInit() && useMth.getParentClass().isEnum(); } return false; } /** * Checks: * - class have only one constructor which used only once (allow common code for field init) * - methods or fields not used outside (allow only nested inner classes with synthetic usage) * - if constructor used only in class init check if possible inline by instance field * * @return decided inline type */ private static InlineType checkUsage(ClassNode cls, MethodNode ctr) { if (ctr.getUseIn().size() != 1) { // check if used in common field init in all constructors if (!checkForCommonFieldInit(ctr)) { return null; } } MethodNode ctrUseMth = ctr.getUseIn().get(0); ClassNode ctrUseCls = ctrUseMth.getParentClass(); if (ctrUseCls.equals(cls)) { if (checkForInstanceFieldUsage(cls, ctr)) { return InlineType.INSTANCE_FIELD; } // exclude self usage return null; } if (ctrUseCls.getTopParentClass().equals(cls)) { // exclude usage inside inner classes return null; } if (!checkMethodsUsage(cls, ctr, ctrUseMth)) { return null; } for (FieldNode field : cls.getFields()) { for (MethodNode useMth : field.getUseIn()) { if (badMethodUsage(cls, useMth, field.getAccessFlags())) { return null; } } } return InlineType.CONSTRUCTOR; } private static boolean checkMethodsUsage(ClassNode cls, MethodNode ctr, MethodNode ctrUseMth) { for (MethodNode mth : cls.getMethods()) { if (mth == ctr) { continue; } for (MethodNode useMth : mth.getUseIn()) { if (useMth.equals(ctrUseMth)) { continue; } if (badMethodUsage(cls, useMth, mth.getAccessFlags())) { return false; } } } return true; } private static boolean checkForInstanceFieldUsage(ClassNode cls, MethodNode ctr) { MethodNode ctrUseMth = ctr.getUseIn().get(0); if (!ctrUseMth.getMethodInfo().isClassInit()) { return false; } if (cls.getUseInMth().isEmpty()) { // no outside usage, inline not needed return false; } FieldNode instFld = ListUtils.filterOnlyOne(cls.getFields(), f -> f.getAccessFlags().containsFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL) && f.getFieldInfo().getType().equals(cls.getClassInfo().getType())); if (instFld == null) { return false; } List instFldUseIn = instFld.getUseIn(); if (instFldUseIn.size() != 2 || !instFldUseIn.contains(ctrUseMth) // initialized in class init || !instFldUseIn.containsAll(cls.getUseInMth()) // class used only with this field ) { return false; } if (!checkMethodsUsage(cls, ctr, ctrUseMth)) { return false; } for (FieldNode field : cls.getFields()) { if (field == instFld) { continue; } for (MethodNode useMth : field.getUseIn()) { if (badMethodUsage(cls, useMth, field.getAccessFlags())) { return false; } } } instFld.add(AFlag.INLINE_INSTANCE_FIELD); return true; } private static boolean badMethodUsage(ClassNode cls, MethodNode useMth, AccessInfo accessFlags) { ClassNode useCls = useMth.getParentClass(); if (useCls.equals(cls)) { return false; } if (accessFlags.isSynthetic()) { // allow synthetic usage in inner class return !useCls.getParentClass().equals(cls); } return true; } /** * Checks: * + all in constructors * + all usage in one class * - same field put (ignored: methods not loaded yet) */ private static boolean checkForCommonFieldInit(MethodNode ctrMth) { List ctrUse = ctrMth.getUseIn(); if (ctrUse.isEmpty()) { return false; } ClassNode firstUseCls = ctrUse.get(0).getParentClass(); return ListUtils.allMatch(ctrUse, m -> m.isConstructor() && m.getParentClass().equals(firstUseCls)); } @Nullable private static ArgType getBaseType(ClassNode cls) { int interfacesCount = cls.getInterfaces().size(); if (interfacesCount > 1) { return null; } ArgType superCls = cls.getSuperClass(); if (superCls == null || superCls.equals(ArgType.OBJECT)) { if (interfacesCount == 1) { return cls.getInterfaces().get(0); } return ArgType.OBJECT; } if (interfacesCount == 0) { return superCls; } // check if super class already implement that interface (weird case) ArgType interfaceType = cls.getInterfaces().get(0); if (cls.root().getClsp().isImplements(superCls.getObject(), interfaceType.getObject())) { return superCls; } if (cls.root().getArgs().isAllowInlineKotlinLambda()) { if (superCls.getObject().equals("kotlin.jvm.internal.Lambda")) { // Inline such class with have different semantic: missing 'arity' property. // For now, it is unclear how it may affect code execution. return interfaceType; } } return null; } @Override public String getName() { return "ProcessAnonymous"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ProcessInstructionsVisitor.java ================================================ package jadx.core.dex.visitors; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.JumpInfo; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.FillArrayData; import jadx.core.dex.instructions.FillArrayInsn; import jadx.core.dex.instructions.FilledNewArrayNode; import jadx.core.dex.instructions.GotoNode; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.SwitchData; import jadx.core.dex.instructions.SwitchInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.java.JsrNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.blocks.BlockSplitter; import jadx.core.utils.InsnUtils; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; @JadxVisitor( name = "Process Instructions Visitor", desc = "Init instructions info", runBefore = { BlockSplitter.class } ) public class ProcessInstructionsVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } initJumps(mth, mth.getInstructions()); } private static void initJumps(MethodNode mth, InsnNode[] insnByOffset) { for (int offset = 0; offset < insnByOffset.length; offset++) { InsnNode insn = insnByOffset[offset]; if (insn == null) { continue; } switch (insn.getType()) { case SWITCH: SwitchInsn sw = (SwitchInsn) insn; if (sw.needData()) { attachSwitchData(insnByOffset, offset, sw); } int defCaseOffset = sw.getDefaultCaseOffset(); if (defCaseOffset != -1) { addJump(mth, insnByOffset, offset, defCaseOffset); } for (int target : sw.getTargets()) { addJump(mth, insnByOffset, offset, target); } break; case IF: int next = getNextInsnOffset(insnByOffset, offset); if (next != -1) { addJump(mth, insnByOffset, offset, next); } addJump(mth, insnByOffset, offset, ((IfNode) insn).getTarget()); break; case GOTO: addJump(mth, insnByOffset, offset, ((GotoNode) insn).getTarget()); break; case JAVA_JSR: addJump(mth, insnByOffset, offset, ((JsrNode) insn).getTarget()); int onRet = getNextInsnOffset(insnByOffset, offset); if (onRet != -1) { addJump(mth, insnByOffset, offset, onRet); } break; case INVOKE: if (insn.getResult() == null) { ArgType retType = ((BaseInvokeNode) insn).getCallMth().getReturnType(); mergeMoveResult(insnByOffset, offset, insn, retType); } break; case STR_CONCAT: // invoke-custom with string concatenation translated directly to STR_CONCAT, merge next move-result if (insn.getResult() == null) { mergeMoveResult(insnByOffset, offset, insn, ArgType.STRING); } break; case FILLED_NEW_ARRAY: ArgType arrType = ((FilledNewArrayNode) insn).getArrayType(); mergeMoveResult(insnByOffset, offset, insn, arrType); break; case FILL_ARRAY: FillArrayInsn fillArrayInsn = (FillArrayInsn) insn; int target = fillArrayInsn.getTarget(); InsnNode arrDataInsn = getInsnAtOffset(insnByOffset, target); if (arrDataInsn != null && arrDataInsn.getType() == InsnType.FILL_ARRAY_DATA) { fillArrayInsn.setArrayData((FillArrayData) arrDataInsn); removeInsn(insnByOffset, arrDataInsn); } else { throw new JadxRuntimeException("Payload for fill-array not found at " + InsnUtils.formatOffset(target)); } break; default: break; } } } private static void attachSwitchData(InsnNode[] insnByOffset, int offset, SwitchInsn sw) { int nextInsnOffset = getNextInsnOffset(insnByOffset, offset); int dataTarget = sw.getDataTarget(); InsnNode switchDataInsn = getInsnAtOffset(insnByOffset, dataTarget); if (switchDataInsn != null && switchDataInsn.getType() == InsnType.SWITCH_DATA) { SwitchData data = (SwitchData) switchDataInsn; data.fixTargets(offset); sw.attachSwitchData(data, nextInsnOffset); removeInsn(insnByOffset, switchDataInsn); } else { throw new JadxRuntimeException("Payload for switch not found at " + InsnUtils.formatOffset(dataTarget)); } } private static void mergeMoveResult(InsnNode[] insnByOffset, int offset, InsnNode insn, ArgType resType) { int nextInsnOffset = getNextInsnOffset(insnByOffset, offset); if (nextInsnOffset == -1) { return; } InsnNode nextInsn = insnByOffset[nextInsnOffset]; if (nextInsn.getType() != InsnType.MOVE_RESULT) { return; } RegisterArg moveRes = nextInsn.getResult(); insn.setResult(moveRes.duplicate(resType)); insn.copyAttributesFrom(nextInsn); removeInsn(insnByOffset, nextInsn); } private static void addJump(MethodNode mth, InsnNode[] insnByOffset, int offset, int target) { try { insnByOffset[target].addAttr(AType.JUMP, new JumpInfo(offset, target)); } catch (Exception e) { mth.addError("Failed to set jump: " + InsnUtils.formatOffset(offset) + " -> " + InsnUtils.formatOffset(target), e); } } public static int getNextInsnOffset(InsnNode[] insnByOffset, int offset) { int len = insnByOffset.length; for (int i = offset + 1; i < len; i++) { InsnNode insnNode = insnByOffset[i]; if (insnNode != null && insnNode.getType() != InsnType.NOP) { return i; } } return -1; } @Nullable private static InsnNode getInsnAtOffset(InsnNode[] insnByOffset, int offset) { int len = insnByOffset.length; for (int i = offset; i < len; i++) { InsnNode insnNode = insnByOffset[i]; if (insnNode != null && insnNode.getType() != InsnType.NOP) { return insnNode; } } return null; } private static void removeInsn(InsnNode[] insnByOffset, InsnNode insn) { insnByOffset[insn.getOffset()] = null; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ProcessMethodsForInline.java ================================================ package jadx.core.dex.visitors; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.usage.UsageInfoVisitor; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "ProcessMethodsForInline", desc = "Mark methods for future inline", runAfter = { UsageInfoVisitor.class } ) public class ProcessMethodsForInline extends AbstractVisitor { private boolean inlineMethods; @Override public void init(RootNode root) { inlineMethods = root.getArgs().isInlineMethods(); } @Override public boolean visit(ClassNode cls) throws JadxException { if (!inlineMethods) { return false; } for (MethodNode mth : cls.getMethods()) { if (canInline(mth)) { mth.add(AFlag.METHOD_CANDIDATE_FOR_INLINE); fixClassDependencies(mth); } } return true; } private static boolean canInline(MethodNode mth) { if (mth.isNoCode() || mth.contains(AFlag.DONT_GENERATE)) { return false; } AccessInfo accessFlags = mth.getAccessFlags(); boolean isSynthetic = accessFlags.isSynthetic() || mth.getName().contains("$"); return isSynthetic && canInlineMethod(mth, accessFlags); } private static boolean canInlineMethod(MethodNode mth, AccessInfo accessFlags) { if (accessFlags.isStatic()) { return true; } return mth.isConstructor() && mth.root().getArgs().isInlineAnonymousClasses(); } private static void fixClassDependencies(MethodNode mth) { ClassNode parentClass = mth.getTopParentClass(); for (MethodNode useInMth : mth.getUseIn()) { // remove possible cross dependency // to force class with inline method to be processed before its usage ClassNode useTopCls = useInMth.getTopParentClass(); if (useTopCls != parentClass) { parentClass.removeDependency(useTopCls); useTopCls.addCodegenDep(parentClass); if (Consts.DEBUG_USAGE) { parentClass.addDebugComment("Remove dependency: " + useTopCls + " to inline " + mth); useTopCls.addDebugComment("Add dependency: " + parentClass + " to inline " + mth); } } } } @Override public String getName() { return "ProcessMethodsForInline"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ReplaceNewArray.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; import jadx.core.dex.attributes.nodes.CodeFeaturesAttr.CodeFeature; import jadx.core.dex.instructions.FilledNewArrayNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.NewArrayNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IFieldInfoRef; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.InsnList; import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnUtils; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "ReplaceNewArray", desc = "Replace new-array and sequence of array-put to new filled-array instruction", runAfter = CodeShrinkVisitor.class ) public class ReplaceNewArray extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { if (!CodeFeaturesAttr.contains(mth, CodeFeature.NEW_ARRAY)) { return; } InsnRemover remover = new InsnRemover(mth); int k = 0; while (true) { boolean changed = false; for (BlockNode block : mth.getBasicBlocks()) { List insnList = block.getInstructions(); int size = insnList.size(); for (int i = 0; i < size; i++) { changed |= processInsn(mth, insnList, i, remover); } remover.performForBlock(block); } if (changed) { CodeShrinkVisitor.shrinkMethod(mth); } else { break; } if (k++ > 100) { mth.addWarnComment("Reached limit for ReplaceNewArray iterations"); break; } } } private static boolean processInsn(MethodNode mth, List instructions, int i, InsnRemover remover) { InsnNode insn = instructions.get(i); if (insn.getType() == InsnType.NEW_ARRAY && !insn.contains(AFlag.REMOVE)) { return processNewArray(mth, (NewArrayNode) insn, instructions, remover); } return false; } private static boolean processNewArray(MethodNode mth, NewArrayNode newArrayInsn, List instructions, InsnRemover remover) { Object arrayLenConst = InsnUtils.getConstValueByArg(mth.root(), newArrayInsn.getArg(0)); if (!(arrayLenConst instanceof LiteralArg)) { return false; } int len = (int) ((LiteralArg) arrayLenConst).getLiteral(); if (len == 0) { return false; } ArgType arrType = newArrayInsn.getArrayType(); ArgType elemType = arrType.getArrayElement(); boolean allowMissingKeys = arrType.getArrayDimension() == 1 && elemType.isPrimitive(); int minLen = allowMissingKeys ? len / 2 : len; RegisterArg arrArg = newArrayInsn.getResult(); List useList = arrArg.getSVar().getUseList(); if (useList.size() < minLen) { return false; } // quick check if APUT is used boolean foundPut = false; for (RegisterArg registerArg : useList) { InsnNode parentInsn = registerArg.getParentInsn(); if (parentInsn != null && parentInsn.getType() == InsnType.APUT) { foundPut = true; break; } } if (!foundPut) { return false; } // collect put instructions sorted by array index SortedMap arrPuts = new TreeMap<>(); InsnNode firstNotAPutUsage = null; for (RegisterArg registerArg : useList) { InsnNode parentInsn = registerArg.getParentInsn(); if (parentInsn == null || parentInsn.getType() != InsnType.APUT || !arrArg.sameRegAndSVar(parentInsn.getArg(0))) { if (firstNotAPutUsage == null) { firstNotAPutUsage = parentInsn; } continue; } Object constVal = InsnUtils.getConstValueByArg(mth.root(), parentInsn.getArg(1)); if (!(constVal instanceof LiteralArg)) { return false; } long index = ((LiteralArg) constVal).getLiteral(); if (index >= len) { return false; } if (arrPuts.containsKey(index)) { // stop on index rewrite break; } arrPuts.put(index, parentInsn); } if (arrPuts.size() < minLen) { return false; } if (!verifyPutInsns(arrArg, instructions, arrPuts)) { return false; } // checks complete, apply InsnNode filledArr = new FilledNewArrayNode(elemType, len); filledArr.setResult(arrArg.duplicate()); filledArr.copyAttributesFrom(newArrayInsn); filledArr.inheritMetadata(newArrayInsn); filledArr.setOffset(newArrayInsn.getOffset()); long prevIndex = -1; for (Map.Entry entry : arrPuts.entrySet()) { long index = entry.getKey(); if (index != prevIndex) { // use zero for missing keys for (long i = prevIndex + 1; i < index; i++) { filledArr.addArg(InsnArg.lit(0, elemType)); } } InsnNode put = entry.getValue(); filledArr.addArg(replaceConstInArg(mth, put.getArg(2))); remover.addAndUnbind(put); prevIndex = index; } // add missing trailing zeros for (long i = prevIndex + 1; i < len; i++) { filledArr.addArg(InsnArg.lit(0, elemType)); } remover.addAndUnbind(newArrayInsn); // place new insn at last array put or before first usage InsnNode lastPut = arrPuts.get(arrPuts.lastKey()); int newInsnPos = InsnList.getIndex(instructions, lastPut); if (firstNotAPutUsage != null) { int idx = InsnList.getIndex(instructions, firstNotAPutUsage); if (idx != -1) { // TODO: check that all args already assigned newInsnPos = Math.min(idx, newInsnPos); } } instructions.add(newInsnPos, filledArr); return true; } private static boolean verifyPutInsns(RegisterArg arrReg, List insnList, SortedMap arrPuts) { List puts = new ArrayList<>(arrPuts.values()); int putsCount = puts.size(); // expect all puts to be in the same block if (insnList.size() < putsCount) { return false; } Set insnSet = Collections.newSetFromMap(new IdentityHashMap<>()); insnSet.addAll(insnList); if (!insnSet.containsAll(puts)) { return false; } // array arg shouldn't be used in puts insns for (InsnNode put : puts) { InsnArg putArg = put.getArg(2); if (putArg.isUseVar(arrReg)) { return false; } } return true; } private static InsnArg replaceConstInArg(MethodNode mth, InsnArg valueArg) { if (valueArg.isLiteral()) { IFieldInfoRef f = mth.getParentClass().getConstFieldByLiteralArg((LiteralArg) valueArg); if (f != null) { InsnNode fGet = new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0); InsnArg arg = InsnArg.wrapArg(fGet); ModVisitor.addFieldUsage(f, mth); return arg; } } return valueArg.duplicate(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java ================================================ package jadx.core.dex.visitors; import java.io.File; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.JadxArgs; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; public class SaveCode { private static final Logger LOG = LoggerFactory.getLogger(SaveCode.class); private SaveCode() { } public static void save(File dir, ClassNode cls, ICodeInfo code) { if (cls.contains(AFlag.DONT_GENERATE)) { return; } if (code == null) { throw new JadxRuntimeException("Code not generated for class " + cls.getFullName()); } if (code == ICodeInfo.EMPTY) { return; } String codeStr = code.getCodeStr(); if (codeStr.isEmpty()) { return; } JadxArgs args = cls.root().getArgs(); if (args.isSkipFilesSave()) { return; } String fileName = cls.getClassInfo().getAliasFullPath() + getFileExtension(cls.root()); if (!args.getSecurity().isValidEntryName(fileName)) { return; } save(codeStr, new File(dir, fileName)); } public static void save(ICodeInfo codeInfo, File file) { save(codeInfo.getCodeStr(), file); } public static void save(String code, File file) { File outFile = FileUtils.prepareFile(file); try (PrintWriter out = new PrintWriter(outFile, StandardCharsets.UTF_8)) { out.println(code); } catch (Exception e) { LOG.error("Save file error", e); } } public static String getFileExtension(RootNode root) { JadxArgs.OutputFormatEnum outputFormat = root.getArgs().getOutputFormat(); switch (outputFormat) { case JAVA: return ".java"; case JSON: return ".json"; default: throw new JadxRuntimeException("Unknown output format: " + outputFormat); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ShadowFieldVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "ShadowFieldVisitor", desc = "Fix shadowed field access", runAfter = TypeInferenceVisitor.class, runBefore = CodeShrinkVisitor.class ) public class ShadowFieldVisitor extends AbstractVisitor { private Map fixInfoMap; @Override public void init(RootNode root) { Map map = new HashMap<>(); for (ClassNode cls : root.getClasses(true)) { Map fieldFixMap = searchShadowedFields(cls); if (!fieldFixMap.isEmpty()) { FieldFixInfo fixInfo = new FieldFixInfo(); fixInfo.fieldFixMap = fieldFixMap; map.put(cls.getRawName(), fixInfo); } } this.fixInfoMap = map; } @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } fixShadowFieldAccess(mth, fixInfoMap); } private static class FieldFixInfo { Map fieldFixMap; } private enum FieldFixType { SUPER, CAST } private static Map searchShadowedFields(ClassNode thisCls) { List allFields = collectAllInstanceFields(thisCls); if (allFields.isEmpty()) { return Collections.emptyMap(); } Map> mapByName = groupByName(allFields); mapByName.entrySet().removeIf(entry -> entry.getValue().size() == 1); if (mapByName.isEmpty()) { return Collections.emptyMap(); } Map fixMap = new HashMap<>(); for (List fields : mapByName.values()) { boolean fromThisCls = fields.get(0).getParentClass() == thisCls; if (fromThisCls && fields.size() == 2) { // only one super class contains same field => can use super FieldNode otherField = fields.get(1); if (otherField.getParentClass() != thisCls) { fixMap.put(otherField.getFieldInfo(), FieldFixType.SUPER); } } else { // several super classes contains same field => can't use super, need cast to exact class for (FieldNode field : fields) { if (field.getParentClass() != thisCls) { fixMap.put(field.getFieldInfo(), FieldFixType.CAST); } } } } return fixMap; } private static Map> groupByName(List allFields) { Map> groupByName = new HashMap<>(allFields.size()); for (FieldNode field : allFields) { groupByName .computeIfAbsent(field.getName(), k -> new ArrayList<>()) .add(field); } return groupByName; } private static List collectAllInstanceFields(ClassNode cls) { List fieldsList = new ArrayList<>(); Set visited = new HashSet<>(); ClassNode currentClass = cls; while (currentClass != null) { if (!visited.add(currentClass)) { String msg = "Found 'super' loop in classes: " + visited; visited.forEach(c -> c.addWarnComment(msg)); return fieldsList; } for (FieldNode field : currentClass.getFields()) { if (!field.getAccessFlags().isStatic()) { fieldsList.add(field); } } ArgType superClass = currentClass.getSuperClass(); if (superClass == null) { break; } currentClass = cls.root().resolveClass(superClass); } return fieldsList; } private static void fixShadowFieldAccess(MethodNode mth, Map fixInfoMap) { for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { processInsn(mth, insn, fixInfoMap); } } } private static void processInsn(MethodNode mth, InsnNode insn, Map fixInfoMap) { FieldInfo fieldInfo = getFieldInfo(insn); if (fieldInfo == null) { return; } InsnArg arg = insn.getArg(insn.getArgsCount() - 1); ArgType type = arg.getType(); if (!type.isTypeKnown() || !type.isObject()) { return; } FieldFixInfo fieldFixInfo = fixInfoMap.get(type.getObject()); if (fieldFixInfo == null) { return; } FieldFixType fieldFixType = fieldFixInfo.fieldFixMap.get(fieldInfo); if (fieldFixType == null) { return; } fixFieldAccess(mth, fieldInfo, fieldFixType, arg); } @Nullable private static FieldInfo getFieldInfo(InsnNode insn) { switch (insn.getType()) { case IPUT: case IGET: return (FieldInfo) ((IndexInsnNode) insn).getIndex(); default: return null; } } private static void fixFieldAccess(MethodNode mth, FieldInfo fieldInfo, FieldFixType fieldFixType, InsnArg arg) { if (fieldFixType == FieldFixType.SUPER) { if (arg.isThis()) { // convert 'this' to 'super' arg.add(AFlag.SUPER); return; } } // apply cast InsnNode castInsn = new IndexInsnNode(InsnType.CAST, fieldInfo.getDeclClass().getType(), 1); castInsn.addArg(arg.duplicate()); castInsn.add(AFlag.SYNTHETIC); castInsn.add(AFlag.EXPLICIT_CAST); arg.wrapInstruction(mth, castInsn, false); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.core.deobf.NameMapper; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.parser.SignatureParser; import jadx.core.dex.nodes.utils.TypeUtils; import jadx.core.dex.visitors.typeinference.TypeCompareEnum; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; public class SignatureProcessor extends AbstractVisitor { private RootNode root; @Override public void init(RootNode root) { this.root = root; } @Override public boolean visit(ClassNode cls) throws JadxException { parseClassSignature(cls); for (FieldNode field : cls.getFields()) { parseFieldSignature(field); } for (MethodNode mth : cls.getMethods()) { parseMethodSignature(mth); } return true; } private void parseClassSignature(ClassNode cls) { SignatureParser sp = SignatureParser.fromNode(cls); if (sp == null) { return; } try { List generics = sp.consumeGenericTypeParameters(); ArgType superClass = processSuperType(cls, sp.consumeType()); List interfaces = processInterfaces(cls, sp.consumeTypeList()); List resultGenerics = fixTypeParamDeclarations(cls, generics, superClass, interfaces); cls.updateGenericClsData(resultGenerics, superClass, interfaces); } catch (Exception e) { cls.addWarnComment("Failed to parse class signature: " + sp.getSignature(), e); } } private ArgType processSuperType(ClassNode cls, ArgType parsedType) { ArgType superType = cls.getSuperClass(); if (Objects.equals(parsedType.getObject(), cls.getClassInfo().getType().getObject())) { cls.addWarnComment("Incorrect class signature: super class is equals to this class"); return superType; } return bestClsType(cls, parsedType, superType); } /** * Parse, validate and update class interfaces types. */ private List processInterfaces(ClassNode cls, List parsedTypes) { List interfaces = cls.getInterfaces(); if (parsedTypes.isEmpty()) { return interfaces; } int parsedCount = parsedTypes.size(); int interfacesCount = interfaces.size(); List result = new ArrayList<>(interfacesCount); int count = Math.min(interfacesCount, parsedCount); for (int i = 0; i < interfacesCount; i++) { if (i < count) { result.add(bestClsType(cls, parsedTypes.get(i), interfaces.get(i))); } else { result.add(interfaces.get(i)); } } if (interfacesCount < parsedCount) { cls.addWarnComment("Unexpected interfaces in signature: " + parsedTypes.subList(interfacesCount, parsedCount)); } return result; } /** * Add missing type parameters from super type and interfaces to make code compilable */ private static List fixTypeParamDeclarations(ClassNode cls, List generics, ArgType superClass, List interfaces) { if (interfaces.isEmpty() && superClass.equals(ArgType.OBJECT)) { return generics; } Set typeParams = new HashSet<>(); superClass.visitTypes(t -> addGenericType(typeParams, t)); interfaces.forEach(i -> i.visitTypes(t -> addGenericType(typeParams, t))); if (typeParams.isEmpty()) { return generics; } List knownTypeParams; if (cls.isInner()) { knownTypeParams = new ArrayList<>(generics); cls.visitParentClasses(p -> knownTypeParams.addAll(p.getGenericTypeParameters())); } else { knownTypeParams = generics; } for (ArgType declTypeParam : knownTypeParams) { typeParams.remove(declTypeParam.getObject()); } if (typeParams.isEmpty()) { return generics; } cls.addInfoComment("Add missing generic type declarations: " + typeParams); List fixedGenerics = new ArrayList<>(generics.size() + typeParams.size()); fixedGenerics.addAll(generics); typeParams.stream().sorted().map(ArgType::genericType).forEach(fixedGenerics::add); return fixedGenerics; } private static @Nullable Object addGenericType(Set usedTypeParameters, ArgType t) { if (t.isGenericType()) { usedTypeParameters.add(t.getObject()); } return null; } private ArgType bestClsType(ClassNode cls, ArgType candidateType, ArgType currentType) { if (validateClsType(cls, candidateType)) { return candidateType; } return currentType; } private boolean validateClsType(ClassNode cls, ArgType candidateType) { if (candidateType == null) { return false; } if (!candidateType.isObject()) { cls.addWarnComment("Incorrect class signature, class is not an object: " + candidateType); return false; } return true; } private void parseFieldSignature(FieldNode field) { SignatureParser sp = SignatureParser.fromNode(field); if (sp == null) { return; } ClassNode cls = field.getParentClass(); try { ArgType signatureType = sp.consumeType(); if (signatureType == null) { return; } if (!validateInnerType(signatureType)) { field.addWarnComment("Incorrect inner types in field signature: " + sp.getSignature()); return; } ArgType type = root.getTypeUtils().expandTypeVariables(cls, signatureType); if (!validateParsedType(type, field.getType())) { field.addInfoComment("Incorrect field signature: " + sp.getSignature()); return; } field.updateType(type); } catch (Exception e) { cls.addWarnComment("Field signature parse error: " + field.getName(), e); } } private void parseMethodSignature(MethodNode mth) { SignatureParser sp = SignatureParser.fromNode(mth); if (sp == null) { return; } try { List typeParameters = sp.consumeGenericTypeParameters(); List parsedArgTypes = sp.consumeMethodArgs(mth.getMethodInfo().getArgsCount()); ArgType parsedRetType = sp.consumeType(); if (!validateInnerType(parsedRetType) || !validateInnerType(parsedArgTypes)) { mth.addWarnComment("Incorrect inner types in method signature: " + sp.getSignature()); return; } mth.updateTypeParameters(typeParameters); // apply before expand args TypeUtils typeUtils = root.getTypeUtils(); ArgType retType = typeUtils.expandTypeVariables(mth, parsedRetType); List argTypes = Utils.collectionMap(parsedArgTypes, t -> typeUtils.expandTypeVariables(mth, t)); if (!validateAndApplyTypes(mth, sp, retType, argTypes)) { // bad types -> reset typed parameters mth.updateTypeParameters(Collections.emptyList()); } } catch (Exception e) { mth.addWarnComment("Failed to parse method signature: " + sp.getSignature(), e); } } private boolean validateAndApplyTypes(MethodNode mth, SignatureParser sp, ArgType retType, List argTypes) { try { if (!validateParsedType(retType, mth.getMethodInfo().getReturnType())) { mth.addWarnComment("Incorrect return type in method signature: " + sp.getSignature()); return false; } List checkedArgTypes = checkArgTypes(mth, sp, argTypes); if (checkedArgTypes == null) { return false; } mth.updateTypes(Collections.unmodifiableList(checkedArgTypes), retType); return true; } catch (Exception e) { mth.addWarnComment("Type validation failed for signature: " + sp.getSignature(), e); return false; } } private List checkArgTypes(MethodNode mth, SignatureParser sp, List parsedArgTypes) { MethodInfo mthInfo = mth.getMethodInfo(); List mthArgTypes = mthInfo.getArgumentsTypes(); int len = parsedArgTypes.size(); if (len != mthArgTypes.size()) { if (mth.getParentClass().getAccessFlags().isEnum()) { // ignore for enums return null; } if (mthInfo.isConstructor() && !mthArgTypes.isEmpty() && !parsedArgTypes.isEmpty()) { // add synthetic arg for outer class (see test TestGeneric8) List newArgTypes = new ArrayList<>(parsedArgTypes); newArgTypes.add(0, mthArgTypes.get(0)); if (newArgTypes.size() == mthArgTypes.size()) { return newArgTypes; } } mth.addDebugComment("Incorrect args count in method signature: " + sp.getSignature()); return null; } for (int i = 0; i < len; i++) { ArgType parsedType = parsedArgTypes.get(i); ArgType mthArgType = mthArgTypes.get(i); if (!validateParsedType(parsedType, mthArgType)) { mth.addWarnComment("Incorrect types in method signature: " + sp.getSignature()); return null; } } return parsedArgTypes; } private boolean validateParsedType(ArgType parsedType, ArgType currentType) { TypeCompareEnum result = root.getTypeCompare().compareTypes(parsedType, currentType); if (result == TypeCompareEnum.UNKNOWN && parsedType.isObject() && !validateFullClsName(parsedType.getObject())) { // ignore external invalid class names: may be a reserved words or garbage return false; } return result != TypeCompareEnum.CONFLICT; } private boolean validateFullClsName(String fullClsName) { if (!NameMapper.isValidFullIdentifier(fullClsName)) { return false; } if (fullClsName.indexOf('.') > 0) { for (String namePart : fullClsName.split("\\.")) { if (!NameMapper.isValidIdentifier(namePart)) { return false; } } } return true; } private boolean validateInnerType(List types) { for (ArgType type : types) { if (!validateInnerType(type)) { return false; } } return true; } private boolean validateInnerType(ArgType type) { ArgType innerType = type.getInnerType(); if (innerType == null) { return true; } // check in outer type has inner type as inner class ArgType outerType = type.getOuterType(); ClassNode outerCls = root.resolveClass(outerType); if (outerCls == null) { // can't check class not found return true; } String innerObj; if (innerType.getOuterType() != null) { innerObj = innerType.getOuterType().getObject(); // "next" inner type will be processed at end of method } else { innerObj = innerType.getObject(); } if (!innerObj.contains(".")) { // short reference for (ClassNode innerClass : outerCls.getInnerClasses()) { if (innerClass.getShortName().equals(innerObj)) { return true; } } return false; } // full name ClassNode innerCls = root.resolveClass(innerObj); if (innerCls == null) { return false; } if (!innerCls.getParentClass().equals(outerCls)) { // not inner => fixing outerCls.addInnerClass(innerCls); innerCls.getClassInfo().convertToInner(outerCls); } return validateInnerType(innerType); } @Override public String getName() { return "SignatureProcessor"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java ================================================ package jadx.core.dex.visitors; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.codegen.TypeGen; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.ArithNode; import jadx.core.dex.instructions.ArithOp; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.FilledNewArrayNode; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.dex.visitors.typeinference.TypeCompareEnum; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnList; import jadx.core.utils.InsnRemover; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; public class SimplifyVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(SimplifyVisitor.class); private MethodInfo stringGetBytesMth; @Override public void init(RootNode root) { stringGetBytesMth = MethodInfo.fromDetails( root, ClassInfo.fromType(root, ArgType.STRING), "getBytes", Collections.emptyList(), ArgType.array(ArgType.BYTE)); } @Override public void visit(MethodNode mth) { if (mth.isNoCode()) { return; } boolean changed = false; for (BlockNode block : mth.getBasicBlocks()) { if (simplifyBlock(mth, block)) { changed = true; } } if (changed || mth.contains(AFlag.REQUEST_CODE_SHRINK)) { CodeShrinkVisitor.shrinkMethod(mth); } } private boolean simplifyBlock(MethodNode mth, BlockNode block) { boolean changed = false; List list = block.getInstructions(); for (int i = 0; i < list.size(); i++) { InsnNode insn = list.get(i); int insnCount = list.size(); InsnNode modInsn = simplifyInsn(mth, insn, null); if (modInsn != null) { modInsn.rebindArgs(); if (i < list.size() && list.get(i) == insn) { list.set(i, modInsn); } else { int idx = InsnList.getIndex(list, insn); if (idx == -1) { throw new JadxRuntimeException("Failed to replace insn"); } list.set(idx, modInsn); } if (list.size() < insnCount) { // some insns removed => restart block processing simplifyBlock(mth, block); return true; } changed = true; } } return changed; } private void simplifyArgs(MethodNode mth, InsnNode insn) { boolean changed = false; for (InsnArg arg : insn.getArguments()) { if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); InsnNode replaceInsn = simplifyInsn(mth, wrapInsn, insn); if (replaceInsn != null) { arg.wrapInstruction(mth, replaceInsn, false); InsnRemover.unbindInsn(mth, wrapInsn); changed = true; } } } if (changed) { insn.rebindArgs(); mth.add(AFlag.REQUEST_CODE_SHRINK); } } private InsnNode simplifyInsn(MethodNode mth, InsnNode insn, @Nullable InsnNode parentInsn) { if (insn.contains(AFlag.DONT_GENERATE)) { return null; } simplifyArgs(mth, insn); switch (insn.getType()) { case ARITH: return simplifyArith((ArithNode) insn); case IF: simplifyIf(mth, (IfNode) insn); break; case TERNARY: simplifyTernary(mth, (TernaryInsn) insn); break; case INVOKE: return convertInvoke(mth, (InvokeNode) insn); case IPUT: case SPUT: return convertFieldArith(mth, insn); case CAST: case CHECK_CAST: return processCast(mth, (IndexInsnNode) insn, parentInsn); case MOVE: InsnArg firstArg = insn.getArg(0); if (firstArg.isLiteral()) { InsnNode constInsn = new InsnNode(InsnType.CONST, 1); constInsn.setResult(insn.getResult()); constInsn.addArg(firstArg); constInsn.copyAttributesFrom(insn); return constInsn; } break; case CONSTRUCTOR: return simplifyStringConstructor(mth, (ConstructorInsn) insn); default: break; } return null; } private InsnNode simplifyStringConstructor(MethodNode mth, ConstructorInsn insn) { if (insn.getCallMth().getDeclClass().getType().equals(ArgType.STRING) && insn.getArgsCount() != 0 && insn.getArg(0).isInsnWrap()) { InsnNode arrInsn = ((InsnWrapArg) insn.getArg(0)).getWrapInsn(); if (arrInsn.getType() == InsnType.FILLED_NEW_ARRAY && arrInsn.getArgsCount() != 0) { ArgType elemType = ((FilledNewArrayNode) arrInsn).getElemType(); if (elemType == ArgType.BYTE || elemType == ArgType.CHAR) { int printable = 0; byte[] arr = new byte[arrInsn.getArgsCount()]; for (int i = 0; i < arr.length; i++) { InsnArg arrArg = arrInsn.getArg(i); if (!arrArg.isLiteral()) { return null; } arr[i] = (byte) ((LiteralArg) arrArg).getLiteral(); if (NameMapper.isPrintableChar((char) arr[i])) { printable++; } } if (printable >= arr.length - printable) { InsnNode constStr = new ConstStringNode(new String(arr)); if (insn.getArgsCount() == 1) { constStr.setResult(insn.getResult()); constStr.copyAttributesFrom(insn); InsnRemover.unbindArgUsage(mth, insn.getArg(0)); return constStr; } else { InvokeNode in = new InvokeNode(stringGetBytesMth, InvokeType.VIRTUAL, 1); in.addArg(InsnArg.wrapArg(constStr)); InsnArg bytesArg = InsnArg.wrapArg(in); bytesArg.setType(stringGetBytesMth.getReturnType()); insn.setArg(0, bytesArg); return null; } } } } } return null; } private static InsnNode processCast(MethodNode mth, IndexInsnNode castInsn, @Nullable InsnNode parentInsn) { if (castInsn.contains(AFlag.EXPLICIT_CAST)) { return null; } InsnArg castArg = castInsn.getArg(0); ArgType argType = castArg.getType(); // Don't removes CHECK_CAST for wrapped INVOKE if invoked method returns different type if (castArg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) castArg).getWrapInsn(); if (wrapInsn.getType() == InsnType.INVOKE) { argType = ((InvokeNode) wrapInsn).getCallMth().getReturnType(); } } ArgType castToType = (ArgType) castInsn.getIndex(); if (isArithWideUpCast(parentInsn, argType, castToType)) { return null; } if (!ArgType.isCastNeeded(mth.root(), argType, castToType) || isCastDuplicate(castInsn) || shadowedByOuterCast(mth.root(), castToType, parentInsn)) { InsnNode insnNode = new InsnNode(InsnType.MOVE, 1); insnNode.setOffset(castInsn.getOffset()); insnNode.setResult(castInsn.getResult()); insnNode.addArg(castArg); return insnNode; } return null; } /** * Keep cast to wide types in arith instructions, * because arguments type determine instruction used in result bytecode. * Example: (long) i << 32 - without 'long' cast will be used 'int shift' instruction and result * will be incorrect */ private static boolean isArithWideUpCast(@Nullable InsnNode parentInsn, ArgType argType, ArgType castToType) { if (parentInsn != null && parentInsn.getType() == InsnType.ARITH && argType.isPrimitive() && castToType.isPrimitive()) { return castToType.getRegCount() > argType.getRegCount(); } return false; } private static boolean isCastDuplicate(IndexInsnNode castInsn) { InsnArg arg = castInsn.getArg(0); if (arg.isRegister()) { SSAVar sVar = ((RegisterArg) arg).getSVar(); if (sVar != null && sVar.getUseCount() == 1 && !sVar.isUsedInPhi()) { InsnNode assignInsn = sVar.getAssign().getParentInsn(); if (assignInsn != null && assignInsn.getType() == InsnType.CHECK_CAST) { ArgType assignCastType = (ArgType) ((IndexInsnNode) assignInsn).getIndex(); return assignCastType.equals(castInsn.getIndex()); } } } return false; } private static boolean shadowedByOuterCast(RootNode root, ArgType castType, @Nullable InsnNode parentInsn) { if (parentInsn != null && parentInsn.getType() == InsnType.CAST) { ArgType parentCastType = (ArgType) ((IndexInsnNode) parentInsn).getIndex(); TypeCompareEnum result = root.getTypeCompare().compareTypes(parentCastType, castType); return result.isNarrow(); } return false; } /** * Simplify 'cmp' instruction in if condition */ private static void simplifyIf(MethodNode mth, IfNode insn) { InsnArg f = insn.getArg(0); if (f.isInsnWrap()) { InsnNode wi = ((InsnWrapArg) f).getWrapInsn(); if (wi.getType() == InsnType.CMP_L || wi.getType() == InsnType.CMP_G) { if (insn.getArg(1).isZeroLiteral()) { insn.changeCondition(insn.getOp(), wi.getArg(0).duplicate(), wi.getArg(1).duplicate()); InsnRemover.unbindInsn(mth, wi); } else { LOG.warn("TODO: cmp {}", insn); } } } } /** * Simplify condition in ternary operation */ private static void simplifyTernary(MethodNode mth, TernaryInsn insn) { IfCondition condition = insn.getCondition(); if (condition.isCompare()) { simplifyIf(mth, condition.getCompare().getInsn()); } else { insn.simplifyCondition(); } } /** * Simplify chains of calls to StringBuilder#append() plus constructor of StringBuilder. * Those chains are usually automatically generated by the Java compiler when you create String * concatenations like "text " + 1 + " text". */ private static InsnNode convertInvoke(MethodNode mth, InvokeNode insn) { MethodInfo callMth = insn.getCallMth(); if (callMth.getDeclClass().getFullName().equals(Consts.CLASS_STRING_BUILDER) && callMth.getShortId().equals(Consts.MTH_TOSTRING_SIGNATURE)) { InsnArg instanceArg = insn.getArg(0); if (instanceArg.isInsnWrap()) { // Convert 'new StringBuilder(xxx).append(yyy).append(zzz).toString() to STRING_CONCAT insn List callChain = flattenInsnChainUntil(insn, InsnType.CONSTRUCTOR); return convertStringBuilderChain(mth, insn, callChain); } if (instanceArg.isRegister()) { // Convert 'StringBuilder sb = new StringBuilder(xxx); sb.append(yyy); String str = sb.toString();' List useChain = collectUseChain(mth, insn, (RegisterArg) instanceArg); return convertStringBuilderChain(mth, insn, useChain); } } return null; } private static List collectUseChain(MethodNode mth, InvokeNode insn, RegisterArg instanceArg) { SSAVar sVar = instanceArg.getSVar(); if (sVar.isUsedInPhi() || sVar.getUseCount() == 0) { return Collections.emptyList(); } List useChain = new ArrayList<>(sVar.getUseCount() + 1); InsnNode assignInsn = sVar.getAssign().getParentInsn(); if (assignInsn == null) { return Collections.emptyList(); } useChain.add(assignInsn); for (RegisterArg reg : sVar.getUseList()) { InsnNode parentInsn = reg.getParentInsn(); if (parentInsn == null) { return Collections.emptyList(); } useChain.add(parentInsn); } int toStrIdx = InsnList.getIndex(useChain, insn); if (useChain.size() - 1 != toStrIdx) { return Collections.emptyList(); } useChain.remove(toStrIdx); // all insns must be in one block and sequential BlockNode assignBlock = BlockUtils.getBlockByInsn(mth, assignInsn); if (assignBlock == null) { return Collections.emptyList(); } List blockInsns = assignBlock.getInstructions(); int assignIdx = InsnList.getIndex(blockInsns, assignInsn); int chainSize = useChain.size(); int lastInsn = blockInsns.size() - assignIdx; if (lastInsn < chainSize) { return Collections.emptyList(); } for (int i = 1; i < chainSize; i++) { if (blockInsns.get(assignIdx + i) != useChain.get(i)) { return Collections.emptyList(); } } return useChain; } private static InsnNode convertStringBuilderChain(MethodNode mth, InvokeNode toStrInsn, List chain) { try { int chainSize = chain.size(); if (chainSize < 2) { return null; } List args = new ArrayList<>(chainSize); InsnNode firstInsn = chain.get(0); if (firstInsn.getType() != InsnType.CONSTRUCTOR) { return null; } ConstructorInsn constrInsn = (ConstructorInsn) firstInsn; if (constrInsn.getArgsCount() == 1) { ArgType argType = constrInsn.getCallMth().getArgumentsTypes().get(0); if (!argType.isObject()) { return null; } args.add(constrInsn.getArg(0)); } for (int i = 1; i < chainSize; i++) { InsnNode chainInsn = chain.get(i); InsnArg arg = getArgFromAppend(chainInsn); if (arg == null) { return null; } args.add(arg); } boolean stringArgFound = false; for (InsnArg arg : args) { if (arg.getType().equals(ArgType.STRING)) { stringArgFound = true; break; } } if (!stringArgFound) { String argStr = Utils.listToString(args, InsnArg::toShortString); mth.addDebugComment("TODO: convert one arg to string using `String.valueOf()`, args: " + argStr); return null; } // all check passed List dupArgs = Utils.collectionMap(args, InsnArg::duplicate); List simplifiedArgs = concatConstArgs(dupArgs); InsnNode concatInsn = new InsnNode(InsnType.STR_CONCAT, simplifiedArgs); concatInsn.add(AFlag.SYNTHETIC); if (toStrInsn.getResult() == null && !toStrInsn.contains(AFlag.WRAPPED)) { // string concat without assign to variable will cause compilation error concatInsn.setResult(mth.makeSyntheticRegArg(ArgType.STRING)); } else { concatInsn.setResult(toStrInsn.getResult()); } concatInsn.copyAttributesFrom(toStrInsn); removeStringBuilderInsns(mth, toStrInsn, chain); return concatInsn; } catch (Exception e) { mth.addWarnComment("String concatenation convert failed", e); } return null; } private static boolean isConstConcatNeeded(List args) { boolean prevConst = false; for (InsnArg arg : args) { boolean curConst = arg.isConst(); if (curConst && prevConst) { // found 2 consecutive constants return true; } prevConst = curConst; } return false; } private static List concatConstArgs(List args) { if (!isConstConcatNeeded(args)) { return args; } int size = args.size(); List newArgs = new ArrayList<>(size); List concatList = new ArrayList<>(size); for (int i = 0; i < size; i++) { InsnArg arg = args.get(i); String constStr = getConstString(arg); if (constStr != null) { concatList.add(constStr); } else { if (!concatList.isEmpty()) { newArgs.add(getConcatArg(concatList, args, i)); concatList.clear(); } newArgs.add(arg); } } if (!concatList.isEmpty()) { newArgs.add(getConcatArg(concatList, args, size)); } return newArgs; } private static InsnArg getConcatArg(List concatList, List args, int idx) { if (concatList.size() == 1) { return args.get(idx - 1); } String str = Utils.concatStrings(concatList); return InsnArg.wrapArg(new ConstStringNode(str)); } @Nullable private static String getConstString(InsnArg arg) { if (arg.isLiteral()) { return TypeGen.literalToRawString((LiteralArg) arg); } if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); if (wrapInsn instanceof ConstStringNode) { return ((ConstStringNode) wrapInsn).getString(); } } return null; } /** * Remove and unbind all instructions with StringBuilder */ private static void removeStringBuilderInsns(MethodNode mth, InvokeNode toStrInsn, List chain) { InsnRemover.unbindAllArgs(mth, toStrInsn); for (InsnNode insnNode : chain) { InsnRemover.unbindAllArgs(mth, insnNode); } InsnRemover insnRemover = new InsnRemover(mth); for (InsnNode insnNode : chain) { if (insnNode != toStrInsn) { insnRemover.addAndUnbind(insnNode); } } insnRemover.perform(); } private static List flattenInsnChainUntil(InsnNode insn, InsnType insnType) { List chain = new ArrayList<>(); InsnArg arg = insn.getArg(0); while (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); chain.add(wrapInsn); if (wrapInsn.getType() == insnType || wrapInsn.getArgsCount() == 0) { break; } arg = wrapInsn.getArg(0); } Collections.reverse(chain); return chain; } private static InsnArg getArgFromAppend(InsnNode chainInsn) { if (chainInsn.getType() == InsnType.INVOKE && chainInsn.getArgsCount() == 2) { MethodInfo callMth = ((InvokeNode) chainInsn).getCallMth(); if (callMth.getDeclClass().getFullName().equals(Consts.CLASS_STRING_BUILDER) && callMth.getName().equals("append")) { return chainInsn.getArg(1); } } return null; } private static InsnNode simplifyArith(ArithNode arith) { if (arith.getArgsCount() != 2) { return null; } LiteralArg litArg = null; InsnArg secondArg = arith.getArg(1); if (secondArg.isInsnWrap()) { InsnNode wr = ((InsnWrapArg) secondArg).getWrapInsn(); if (wr.getType() == InsnType.CONST) { InsnArg arg = wr.getArg(0); if (arg.isLiteral()) { litArg = (LiteralArg) arg; } } } else if (secondArg.isLiteral()) { litArg = (LiteralArg) secondArg; } if (litArg == null) { return null; } switch (arith.getOp()) { case ADD: // fix 'c + (-1)' to 'c - (1)' if (litArg.isNegative()) { LiteralArg negLitArg = litArg.negate(); if (negLitArg != null) { return new ArithNode(ArithOp.SUB, arith.getResult(), arith.getArg(0), negLitArg); } } break; case XOR: // simplify xor on boolean InsnArg firstArg = arith.getArg(0); long lit = litArg.getLiteral(); if (firstArg.getType() == ArgType.BOOLEAN && (lit == 0 || lit == 1)) { InsnNode node = new InsnNode(lit == 0 ? InsnType.MOVE : InsnType.NOT, 1); node.setResult(arith.getResult()); node.addArg(firstArg); return node; } break; } return null; } /** * Convert field arith operation to arith instruction * (IPUT (ARITH (IGET, lit)) -> ARITH ((IGET)) = lit)) */ private static ArithNode convertFieldArith(MethodNode mth, InsnNode insn) { InsnArg arg = insn.getArg(0); if (!arg.isInsnWrap()) { return null; } InsnNode wrap = ((InsnWrapArg) arg).getWrapInsn(); InsnType wrapType = wrap.getType(); if (wrapType != InsnType.ARITH && wrapType != InsnType.STR_CONCAT || !wrap.getArg(0).isInsnWrap()) { return null; } InsnArg getWrap = wrap.getArg(0); InsnNode get = ((InsnWrapArg) getWrap).getWrapInsn(); InsnType getType = get.getType(); if (getType != InsnType.IGET && getType != InsnType.SGET) { return null; } FieldInfo field = (FieldInfo) ((IndexInsnNode) insn).getIndex(); FieldInfo innerField = (FieldInfo) ((IndexInsnNode) get).getIndex(); if (!field.equals(innerField)) { return null; } try { if (getType == InsnType.IGET && insn.getType() == InsnType.IPUT) { InsnArg reg = get.getArg(0); InsnArg putReg = insn.getArg(1); if (!reg.equals(putReg)) { return null; } } InsnArg fArg = getWrap.duplicate(); InsnRemover.unbindInsn(mth, get); if (insn.getType() == InsnType.IPUT) { InsnRemover.unbindArgUsage(mth, insn.getArg(1)); } if (wrapType == InsnType.ARITH) { ArithNode ar = (ArithNode) wrap; return ArithNode.oneArgOp(ar.getOp(), fArg, ar.getArg(1)); } int argsCount = wrap.getArgsCount(); InsnNode concat = new InsnNode(InsnType.STR_CONCAT, argsCount - 1); for (int i = 1; i < argsCount; i++) { concat.addArg(wrap.getArg(i)); } InsnArg concatArg = InsnArg.wrapArg(concat); concatArg.setType(ArgType.STRING); return ArithNode.oneArgOp(ArithOp.ADD, fArg, concatArg); } catch (Exception e) { LOG.debug("Can't convert field arith insn: {}, mth: {}", insn, mth, e); } return null; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockExceptionHandler.java ================================================ package jadx.core.dex.visitors.blocks; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.utils.Utils; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.ExcSplitCrossAttr; import jadx.core.dex.attributes.nodes.TmpEdgeAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.NamedArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.trycatch.TryCatchBlockAttr; import jadx.core.dex.visitors.typeinference.TypeCompare; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnRemover; import jadx.core.utils.ListUtils; import jadx.core.utils.blocks.BlockSet; import jadx.core.utils.exceptions.JadxRuntimeException; public class BlockExceptionHandler { private static final Logger LOG = LoggerFactory.getLogger(BlockExceptionHandler.class); public static boolean process(MethodNode mth) { if (mth.isNoExceptionHandlers()) { return false; } BlockProcessor.updateCleanSuccessors(mth); DominatorTree.computeDominanceFrontier(mth); processCatchAttr(mth); initExcHandlers(mth); List tryBlocks = prepareTryBlocks(mth); connectExcHandlers(mth, tryBlocks); mth.addAttr(AType.TRY_BLOCKS_LIST, tryBlocks); mth.getBasicBlocks().forEach(BlockNode::updateCleanSuccessors); for (ExceptionHandler eh : mth.getExceptionHandlers()) { removeMonitorExitFromExcHandler(mth, eh); } BlockProcessor.removeMarkedBlocks(mth); BlockSet sorted = new BlockSet(mth); BlockUtils.visitDFS(mth, sorted::add); removeUnusedExcHandlers(mth, tryBlocks, sorted); return true; } /** * Wrap try blocks with top/bottom splitter and connect them to handler block. * Sometimes try block can be handler block itself and should be connected before wrapping. * Use queue for postpone try blocks not ready for wrap. */ private static void connectExcHandlers(MethodNode mth, List tryBlocks) { if (tryBlocks.isEmpty()) { return; } int limit = tryBlocks.size() * 3; int count = 0; Deque queue = new ArrayDeque<>(tryBlocks); while (!queue.isEmpty()) { TryCatchBlockAttr tryBlock = queue.removeFirst(); boolean complete = wrapBlocksWithTryCatch(mth, tryBlock); if (!complete) { queue.addLast(tryBlock); // return to queue at the end } if (count++ > limit) { throw new JadxRuntimeException("Try blocks wrapping queue limit reached! Please report as an issue!"); } } } private static void processCatchAttr(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { if (insn.contains(AType.EXC_CATCH) && !insn.canThrowException()) { insn.remove(AType.EXC_CATCH); } } } // if all instructions in block have same 'catch' attribute -> add this attribute for whole block. for (BlockNode block : mth.getBasicBlocks()) { CatchAttr commonCatchAttr = getCommonCatchAttr(block); if (commonCatchAttr != null) { block.addAttr(commonCatchAttr); for (InsnNode insn : block.getInstructions()) { if (insn.contains(AFlag.TRY_ENTER)) { block.add(AFlag.TRY_ENTER); } if (insn.contains(AFlag.TRY_LEAVE)) { block.add(AFlag.TRY_LEAVE); } } } } } @Nullable private static CatchAttr getCommonCatchAttr(BlockNode block) { CatchAttr commonCatchAttr = null; for (InsnNode insn : block.getInstructions()) { CatchAttr catchAttr = insn.get(AType.EXC_CATCH); if (catchAttr != null) { if (commonCatchAttr == null) { commonCatchAttr = catchAttr; continue; } if (!commonCatchAttr.equals(catchAttr)) { return null; } } } return commonCatchAttr; } @SuppressWarnings("ForLoopReplaceableByForEach") private static void initExcHandlers(MethodNode mth) { List blocks = mth.getBasicBlocks(); int blocksCount = blocks.size(); for (int i = 0; i < blocksCount; i++) { // will add new blocks to list end BlockNode block = blocks.get(i); InsnNode firstInsn = BlockUtils.getFirstInsn(block); if (firstInsn == null) { continue; } ExcHandlerAttr excHandlerAttr = firstInsn.get(AType.EXC_HANDLER); if (excHandlerAttr == null) { continue; } firstInsn.remove(AType.EXC_HANDLER); removeTmpConnection(block); ExceptionHandler excHandler = excHandlerAttr.getHandler(); if (block.getPredecessors().isEmpty()) { excHandler.setHandlerBlock(block); block.addAttr(excHandlerAttr); excHandler.addBlock(block); BlockUtils.collectBlocksDominatedByWithExcHandlers(mth, block, block) .forEach(excHandler::addBlock); } else { // ignore already connected handlers -> make catch empty BlockNode emptyHandlerBlock = BlockSplitter.startNewBlock(mth, block.getStartOffset()); emptyHandlerBlock.add(AFlag.SYNTHETIC); emptyHandlerBlock.addAttr(excHandlerAttr); BlockSplitter.connect(emptyHandlerBlock, block); excHandler.setHandlerBlock(emptyHandlerBlock); excHandler.addBlock(emptyHandlerBlock); } fixMoveExceptionInsn(block, excHandlerAttr); } } private static void removeTmpConnection(BlockNode block) { TmpEdgeAttr tmpEdgeAttr = block.get(AType.TMP_EDGE); if (tmpEdgeAttr != null) { // remove temp connection BlockSplitter.removeConnection(tmpEdgeAttr.getBlock(), block); block.remove(AType.TMP_EDGE); } } private static List prepareTryBlocks(MethodNode mth) { Map> blocksByHandler = new HashMap<>(); for (BlockNode block : mth.getBasicBlocks()) { CatchAttr catchAttr = block.get(AType.EXC_CATCH); if (catchAttr != null) { for (ExceptionHandler eh : catchAttr.getHandlers()) { blocksByHandler .computeIfAbsent(eh, c -> new ArrayList<>()) .add(block); } } } if (Consts.DEBUG_EXC_HANDLERS) { LOG.debug("Input exception handlers:"); blocksByHandler.forEach((eh, blocks) -> LOG.debug(" {}, throw blocks: {}, handler blocks: {}", eh, blocks, eh.getBlocks())); } if (blocksByHandler.isEmpty()) { // no catch blocks -> remove all handlers mth.getExceptionHandlers().forEach(eh -> removeExcHandler(mth, eh)); } else { // remove handlers without blocks in catch attribute blocksByHandler.forEach((eh, blocks) -> { if (blocks.isEmpty()) { removeExcHandler(mth, eh); } }); } BlockSplitter.detachMarkedBlocks(mth); mth.clearExceptionHandlers(); if (mth.isNoExceptionHandlers()) { return Collections.emptyList(); } blocksByHandler.forEach((eh, blocks) -> { // remove catches from same handler blocks.removeAll(eh.getBlocks()); }); List tryBlocks = new ArrayList<>(); blocksByHandler.forEach((eh, blocks) -> { List handlers = new ArrayList<>(1); handlers.add(eh); tryBlocks.add(new TryCatchBlockAttr(tryBlocks.size(), handlers, blocks)); }); if (tryBlocks.size() > 1) { // merge or mark as outer/inner while (true) { boolean restart = combineTryCatchBlocks(tryBlocks); if (!restart) { break; } } } checkForMultiCatch(mth, tryBlocks); clearTryBlocks(mth, tryBlocks); sortHandlers(mth, tryBlocks); if (Consts.DEBUG_EXC_HANDLERS) { LOG.debug("Result try-catch blocks:"); tryBlocks.forEach(tryBlock -> LOG.debug(" {}", tryBlock)); } return tryBlocks; } private static void clearTryBlocks(MethodNode mth, List tryBlocks) { tryBlocks.forEach(tc -> tc.getBlocks().removeIf(b -> b.contains(AFlag.REMOVE))); tryBlocks.removeIf(tb -> tb.getBlocks().isEmpty() || tb.getHandlers().isEmpty()); mth.clearExceptionHandlers(); BlockSplitter.detachMarkedBlocks(mth); } private static boolean combineTryCatchBlocks(List tryBlocks) { for (TryCatchBlockAttr outerTryBlock : tryBlocks) { for (TryCatchBlockAttr innerTryBlock : tryBlocks) { if (outerTryBlock == innerTryBlock || innerTryBlock.getOuterTryBlock() != null) { continue; } if (checkTryCatchRelation(tryBlocks, outerTryBlock, innerTryBlock)) { return true; } } } return false; } private static boolean checkTryCatchRelation(List tryBlocks, TryCatchBlockAttr outerTryBlock, TryCatchBlockAttr innerTryBlock) { if (outerTryBlock.getBlocks().equals(innerTryBlock.getBlocks())) { // same try blocks -> merge handlers List handlers = Utils.concatDistinct(outerTryBlock.getHandlers(), innerTryBlock.getHandlers()); tryBlocks.add(new TryCatchBlockAttr(tryBlocks.size(), handlers, outerTryBlock.getBlocks())); tryBlocks.remove(outerTryBlock); tryBlocks.remove(innerTryBlock); return true; } Set handlerBlocks = innerTryBlock.getHandlers().stream() .flatMap(eh -> eh.getBlocks().stream()) .collect(Collectors.toSet()); boolean catchInHandler = handlerBlocks.stream().anyMatch(isHandlersIntersects(outerTryBlock)); boolean catchInTry = innerTryBlock.getBlocks().stream().anyMatch(isHandlersIntersects(outerTryBlock)); boolean blocksOutsideHandler = outerTryBlock.getBlocks().stream().anyMatch(b -> !handlerBlocks.contains(b)); if (catchInHandler && (catchInTry || blocksOutsideHandler)) { // convert to inner List mergedBlocks = Utils.concatDistinct(outerTryBlock.getBlocks(), innerTryBlock.getBlocks()); innerTryBlock.getHandlers().removeAll(outerTryBlock.getHandlers()); innerTryBlock.setOuterTryBlock(outerTryBlock); outerTryBlock.addInnerTryBlock(innerTryBlock); outerTryBlock.setBlocks(mergedBlocks); return false; } Set innerHandlerSet = new HashSet<>(innerTryBlock.getHandlers()); if (innerHandlerSet.containsAll(outerTryBlock.getHandlers())) { // merge List mergedBlocks = Utils.concatDistinct(outerTryBlock.getBlocks(), innerTryBlock.getBlocks()); List handlers = Utils.concatDistinct(outerTryBlock.getHandlers(), innerTryBlock.getHandlers()); tryBlocks.add(new TryCatchBlockAttr(tryBlocks.size(), handlers, mergedBlocks)); tryBlocks.remove(outerTryBlock); tryBlocks.remove(innerTryBlock); return true; } return false; } @NotNull private static Predicate isHandlersIntersects(TryCatchBlockAttr outerTryBlock) { return block -> { CatchAttr catchAttr = block.get(AType.EXC_CATCH); return catchAttr != null && Objects.equals(catchAttr.getHandlers(), outerTryBlock.getHandlers()); }; } private static void removeExcHandler(MethodNode mth, ExceptionHandler excHandler) { excHandler.markForRemove(); BlockSplitter.removeConnection(mth.getEnterBlock(), excHandler.getHandlerBlock()); } private static boolean wrapBlocksWithTryCatch(MethodNode mth, TryCatchBlockAttr tryCatchBlock) { List blocks = tryCatchBlock.getBlocks(); BlockNode top = searchTopBlock(mth, blocks); if (top.getPredecessors().isEmpty() && top != mth.getEnterBlock()) { return false; } BlockNode bottom = searchBottomBlock(mth, blocks); BlockNode splitReturn; if (bottom != null && bottom.isReturnBlock()) { if (Consts.DEBUG_EXC_HANDLERS) { LOG.debug("TryCatch #{} bottom block ({}) is return, split", tryCatchBlock.id(), bottom); } splitReturn = bottom; bottom = BlockSplitter.blockSplitTop(mth, bottom); bottom.add(AFlag.SYNTHETIC); } else { splitReturn = null; } if (Consts.DEBUG_EXC_HANDLERS) { LOG.debug("TryCatch #{} split: top {}, bottom: {}", tryCatchBlock.id(), top, bottom); } BlockNode topSplitterBlock = getTopSplitterBlock(mth, top); topSplitterBlock.add(AFlag.EXC_TOP_SPLITTER); topSplitterBlock.add(AFlag.SYNTHETIC); int totalHandlerBlocks = tryCatchBlock.getHandlers().stream().mapToInt(eh -> eh.getBlocks().size()).sum(); BlockNode bottomSplitterBlock; if (bottom == null || totalHandlerBlocks == 0) { bottomSplitterBlock = null; } else { BlockNode existBottomSplitter = BlockUtils.getBlockWithFlag(bottom.getSuccessors(), AFlag.EXC_BOTTOM_SPLITTER); bottomSplitterBlock = existBottomSplitter != null ? existBottomSplitter : BlockSplitter.startNewBlock(mth, -1); bottomSplitterBlock.add(AFlag.EXC_BOTTOM_SPLITTER); bottomSplitterBlock.add(AFlag.SYNTHETIC); BlockSplitter.connect(bottom, bottomSplitterBlock); if (splitReturn != null) { // redirect handler to return block instead synthetic split block to avoid self-loop BlockSet bottomPreds = BlockSet.from(mth, bottom.getPredecessors()); for (ExceptionHandler handler : tryCatchBlock.getHandlers()) { if (bottomPreds.intersects(handler.getBlocks())) { BlockNode lastBlock = bottomPreds.intersect(handler.getBlocks()).getOne(); if (lastBlock != null) { BlockSplitter.replaceConnection(lastBlock, bottom, splitReturn); } } } } } if (Consts.DEBUG_EXC_HANDLERS) { LOG.debug("TryCatch #{} result splitters: top {}, bottom: {}", tryCatchBlock.id(), topSplitterBlock, bottomSplitterBlock); } connectSplittersAndHandlers(tryCatchBlock, topSplitterBlock, bottomSplitterBlock); // At this point, it's possible that a cross edge to the original bottom has been turned into a back // edge by the insertion of the new bottom. This causes problems because back edges usually signifiy // loops, but this is not a loop. To fix this, predecessors of the bottom that also have a path from // the bottom are rewritten to point to the original path crossing point (before synthetic blocks). if (bottom != null && bottom.contains(AType.EXC_SPLIT_CROSS)) { List convertBlocks = new ArrayList<>(); for (BlockNode b : bottom.getPredecessors()) { if (BlockUtils.isAnyPathExists(bottom, b)) { convertBlocks.add(b); } } for (BlockNode b : convertBlocks) { // The connection can't be replaced during the first loop because it would modify the preds list. BlockSplitter.replaceConnection(b, bottom, bottom.get(AType.EXC_SPLIT_CROSS).getOriginalPathCross()); } } for (BlockNode block : blocks) { TryCatchBlockAttr currentTCBAttr = block.get(AType.TRY_BLOCK); if (currentTCBAttr == null || currentTCBAttr.getInnerTryBlocks().contains(tryCatchBlock)) { block.addAttr(tryCatchBlock); } } tryCatchBlock.setTopSplitter(topSplitterBlock); topSplitterBlock.updateCleanSuccessors(); if (bottomSplitterBlock != null) { bottomSplitterBlock.updateCleanSuccessors(); } return true; } private static BlockNode getTopSplitterBlock(MethodNode mth, BlockNode top) { if (top == mth.getEnterBlock()) { BlockNode fixedTop = mth.getEnterBlock().getSuccessors().get(0); return BlockSplitter.blockSplitTop(mth, fixedTop); } BlockNode existPredTopSplitter = BlockUtils.getBlockWithFlag(top.getPredecessors(), AFlag.EXC_TOP_SPLITTER); if (existPredTopSplitter != null) { return existPredTopSplitter; } // try to reuse exists splitter on empty simple path below top block if (top.getCleanSuccessors().size() == 1 && top.getInstructions().isEmpty()) { BlockNode otherTopSplitter = BlockUtils.getBlockWithFlag(top.getCleanSuccessors(), AFlag.EXC_TOP_SPLITTER); if (otherTopSplitter != null && otherTopSplitter.getPredecessors().size() == 1) { return otherTopSplitter; } } return BlockSplitter.blockSplitTop(mth, top); } private static BlockNode searchTopBlock(MethodNode mth, List blocks) { BlockNode top = BlockUtils.getTopBlock(blocks); if (top != null) { return adjustTopBlock(top); } BlockNode topDom = BlockUtils.getCommonDominator(mth, blocks); if (topDom != null) { // dominator always return one up block if blocks already contains dominator, use successor instead if (topDom.getSuccessors().size() == 1) { BlockNode upBlock = topDom.getSuccessors().get(0); if (blocks.contains(upBlock)) { return upBlock; } } return adjustTopBlock(topDom); } throw new JadxRuntimeException("Failed to find top block for try-catch from: " + blocks); } private static BlockNode adjustTopBlock(BlockNode topBlock) { if (topBlock.getSuccessors().size() == 1 && !topBlock.contains(AType.EXC_CATCH)) { // top block can be lifted by other exception handlers included in blocks list, trying to undo that return topBlock.getSuccessors().get(0); } return topBlock; } @Nullable private static BlockNode searchBottomBlock(MethodNode mth, List blocks) { // search common post-dominator block inside input set BlockNode bottom = BlockUtils.getBottomBlock(blocks); if (bottom != null) { return bottom; } // not found -> blocks don't have same dominator // try to search common cross block outside input set // NOTE: bottom block not needed for exit nodes (no data flow from them) BlockNode pathCross = BlockUtils.getPathCross(mth, blocks); if (pathCross == null) { return null; } List preds = new ArrayList<>(pathCross.getPredecessors()); preds.removeAll(blocks); List outsidePredecessors = preds.stream() .filter(p -> !BlockUtils.atLeastOnePathExists(blocks, p)) .collect(Collectors.toList()); // if we have no predecessors or every predecessor is outside (which would mean that inserting the // new synthetic block does nothing), just return the existing path cross instead. if (outsidePredecessors.isEmpty() || outsidePredecessors.size() == pathCross.getPredecessors().size()) { return pathCross; } // some predecessors outside of input set paths -> split block only for input set BlockNode splitCross = BlockSplitter.blockSplitTop(mth, pathCross); splitCross.add(AFlag.SYNTHETIC); splitCross.addAttr(new ExcSplitCrossAttr(pathCross)); for (BlockNode outsidePredecessor : outsidePredecessors) { // return predecessors to split bottom block (original) BlockSplitter.replaceConnection(outsidePredecessor, splitCross, pathCross); } return splitCross; } private static void connectSplittersAndHandlers(TryCatchBlockAttr tryCatchBlock, BlockNode topSplitterBlock, @Nullable BlockNode bottomSplitterBlock) { for (ExceptionHandler handler : tryCatchBlock.getHandlers()) { BlockNode handlerBlock = handler.getHandlerBlock(); BlockSplitter.connect(topSplitterBlock, handlerBlock); if (bottomSplitterBlock != null) { BlockSplitter.connect(bottomSplitterBlock, handlerBlock); } } TryCatchBlockAttr outerTryBlock = tryCatchBlock.getOuterTryBlock(); if (outerTryBlock != null) { connectSplittersAndHandlers(outerTryBlock, topSplitterBlock, bottomSplitterBlock); } } private static void fixMoveExceptionInsn(BlockNode block, ExcHandlerAttr excHandlerAttr) { ExceptionHandler excHandler = excHandlerAttr.getHandler(); ArgType argType = excHandler.getArgType(); InsnNode me = BlockUtils.getLastInsn(block); if (me != null && me.getType() == InsnType.MOVE_EXCEPTION) { // set correct type for 'move-exception' operation RegisterArg resArg = InsnArg.reg(me.getResult().getRegNum(), argType); resArg.copyAttributesFrom(me); me.setResult(resArg); me.add(AFlag.DONT_INLINE); resArg.add(AFlag.CUSTOM_DECLARE); excHandler.setArg(resArg); me.addAttr(excHandlerAttr); return; } // handler arguments not used excHandler.setArg(new NamedArg("unused", argType)); } private static void removeMonitorExitFromExcHandler(MethodNode mth, ExceptionHandler excHandler) { for (BlockNode excBlock : excHandler.getBlocks()) { InsnRemover remover = new InsnRemover(mth, excBlock); for (InsnNode insn : excBlock.getInstructions()) { if (insn.getType() == InsnType.MONITOR_ENTER) { break; } if (insn.getType() == InsnType.MONITOR_EXIT) { remover.addAndUnbind(insn); } } remover.perform(); } } private static void checkForMultiCatch(MethodNode mth, List tryBlocks) { boolean merged = false; for (TryCatchBlockAttr tryBlock : tryBlocks) { if (mergeMultiCatch(mth, tryBlock)) { merged = true; } } if (merged) { BlockSplitter.detachMarkedBlocks(mth); mth.clearExceptionHandlers(); } } private static boolean mergeMultiCatch(MethodNode mth, TryCatchBlockAttr tryCatch) { if (tryCatch.getHandlers().size() < 2) { return false; } for (ExceptionHandler handler : tryCatch.getHandlers()) { if (handler.getBlocks().size() != 1) { return false; } BlockNode block = handler.getHandlerBlock(); if (block.getInstructions().size() != 1 || !BlockUtils.checkLastInsnType(block, InsnType.MOVE_EXCEPTION)) { return false; } } List handlerBlocks = ListUtils.map(tryCatch.getHandlers(), ExceptionHandler::getHandlerBlock); List successorBlocks = handlerBlocks.stream() .flatMap(h -> h.getSuccessors().stream()) .distinct() .collect(Collectors.toList()); if (successorBlocks.size() != 1) { return false; } BlockNode successorBlock = successorBlocks.get(0); if (!ListUtils.unorderedEquals(successorBlock.getPredecessors(), handlerBlocks)) { return false; } List regs = tryCatch.getHandlers().stream() .map(h -> Objects.requireNonNull(BlockUtils.getLastInsn(h.getHandlerBlock())).getResult()) .distinct() .collect(Collectors.toList()); if (regs.size() != 1) { return false; } // merge confirm, leave only first handler, remove others ExceptionHandler resultHandler = tryCatch.getHandlers().get(0); tryCatch.getHandlers().removeIf(handler -> { if (handler == resultHandler) { return false; } resultHandler.addCatchTypes(mth, handler.getCatchTypes()); handler.markForRemove(); return true; }); return true; } private static void sortHandlers(MethodNode mth, List tryBlocks) { TypeCompare typeCompare = mth.root().getTypeCompare(); Comparator comparator = typeCompare.getReversedComparator(); for (TryCatchBlockAttr tryBlock : tryBlocks) { for (ExceptionHandler handler : tryBlock.getHandlers()) { handler.getCatchTypes().sort((first, second) -> compareByTypeAndName(comparator, first, second)); } tryBlock.getHandlers().sort((first, second) -> { if (first.equals(second)) { throw new JadxRuntimeException("Same handlers in try block: " + tryBlock); } if (first.isCatchAll()) { return 1; } if (second.isCatchAll()) { return -1; } return compareByTypeAndName(comparator, ListUtils.first(first.getCatchTypes()), ListUtils.first(second.getCatchTypes())); }); } } @SuppressWarnings("ComparatorResultComparison") private static int compareByTypeAndName(Comparator comparator, ClassInfo first, ClassInfo second) { int r = comparator.compare(first.getType(), second.getType()); if (r == -2) { // on conflict sort by name return first.compareTo(second); } return r; } /** * Remove excHandlers that were not used when connecting. * Check first if the blocks are unreachable. */ private static void removeUnusedExcHandlers(MethodNode mth, List tryBlocks, BlockSet blocks) { for (ExceptionHandler eh : mth.getExceptionHandlers()) { boolean notProcessed = true; BlockNode handlerBlock = eh.getHandlerBlock(); if (handlerBlock == null || blocks.contains(handlerBlock)) { continue; } for (TryCatchBlockAttr tcb : tryBlocks) { if (tcb.getHandlers().contains(eh)) { notProcessed = false; break; } } if (notProcessed) { BlockProcessor.removeUnreachableBlock(handlerBlock, mth); } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockFinisher.java ================================================ package jadx.core.dex.visitors.blocks; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.AbstractVisitor; public class BlockFinisher extends AbstractVisitor { @Override public void visit(MethodNode mth) { if (mth.isNoCode() || mth.getBasicBlocks().isEmpty()) { return; } if (!mth.contains(AFlag.DISABLE_BLOCKS_LOCK)) { mth.finishBasicBlocks(); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockProcessor.java ================================================ package jadx.core.dex.visitors.blocks; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.Edge; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.dex.trycatch.TryCatchBlockAttr; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.dex.visitors.blocks.BlockSplitter.connect; public class BlockProcessor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(BlockProcessor.class); private static final boolean DEBUG_MODS = false; @Override public void visit(MethodNode mth) { if (mth.isNoCode() || mth.getBasicBlocks().isEmpty()) { return; } processBlocksTree(mth); } private static void processBlocksTree(MethodNode mth) { removeUnreachableBlocks(mth); computeDominators(mth); if (independentBlockTreeMod(mth)) { checkForUnreachableBlocks(mth); computeDominators(mth); } if (FixMultiEntryLoops.process(mth)) { computeDominators(mth); } updateCleanSuccessors(mth); int blocksCount = mth.getBasicBlocks().size(); int modLimit = Math.max(100, blocksCount); if (DEBUG_MODS) { mth.addAttr(new DebugModAttr()); } int i = 0; while (modifyBlocksTree(mth)) { computeDominators(mth); if (i++ > modLimit) { mth.addWarn("CFG modification limit reached, blocks count: " + blocksCount); break; } } if (DEBUG_MODS && i != 0) { String stats = "CFG modifications count: " + i + ", blocks count: " + blocksCount + '\n' + mth.get(DebugModAttr.TYPE).formatStats() + '\n'; mth.addDebugComment(stats); LOG.debug("Method: {}\n{}", mth, stats); mth.remove(DebugModAttr.TYPE); } checkForUnreachableBlocks(mth); DominatorTree.computeDominanceFrontier(mth); registerLoops(mth); processNestedLoops(mth); PostDominatorTree.compute(mth); updateCleanSuccessors(mth); } /** * Recalculate all additional info attached to blocks: * *
	 * - dominators
	 * - dominance frontier
	 * - post dominators (only if {@link AFlag#COMPUTE_POST_DOM} added to method)
	 * - loops and nested loop info
	 * 
*

* This method should be called after changing a block tree in custom passes added before * {@link BlockFinisher}. */ public static void updateBlocksData(MethodNode mth) { clearBlocksState(mth); DominatorTree.compute(mth); markLoops(mth); DominatorTree.computeDominanceFrontier(mth); registerLoops(mth); processNestedLoops(mth); PostDominatorTree.compute(mth); updateCleanSuccessors(mth); } static void updateCleanSuccessors(MethodNode mth) { mth.getBasicBlocks().forEach(BlockNode::updateCleanSuccessors); } private static void checkForUnreachableBlocks(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { if (block.getPredecessors().isEmpty() && block != mth.getEnterBlock()) { // Sometimes a split cross block will have all it's predecessors moved elsewhere after it's been // created. This is usually detected at the time of it's creation, but in certain edge cases it // is difficult to do so. In those cases it will be cleanly removed here, along with the associated // bottom splitter. if (block.contains(AType.EXC_SPLIT_CROSS) && fixUnreachableSplitCross(mth, block)) { mth.addInfoComment("Removed unreachable split cross block " + block.toString()); } else { throw new JadxRuntimeException("Unreachable block: " + block); } } } } /** * Attempts to remove an unreachable synthetic split cross block that has been added previously, * along with the associated bottom splitter. * * @param mth the method containing the unreachable block * @param splitCross the unreachable block * @return true if the operation was successful, false if a precondition was not satisfied and no * changes were made. */ private static boolean fixUnreachableSplitCross(MethodNode mth, BlockNode splitCross) { BlockNode bottomSplitter = null; for (BlockNode succ : splitCross.getSuccessors()) { if (succ.contains(AFlag.EXC_BOTTOM_SPLITTER)) { bottomSplitter = succ; break; } } if (bottomSplitter == null || bottomSplitter.getPredecessors().size() != 1) { return false; } Set removeSet = new HashSet<>(); removeSet.add(bottomSplitter); removeSet.add(splitCross); removeFromMethod(removeSet, mth); return true; } private static boolean deduplicateBlockInsns(MethodNode mth, BlockNode block) { if (block.contains(AFlag.LOOP_START) || block.contains(AFlag.LOOP_END)) { // search for same instruction at end of all predecessors blocks List predecessors = block.getPredecessors(); int predsCount = predecessors.size(); if (predsCount > 1) { InsnNode lastInsn = BlockUtils.getLastInsn(block); if (lastInsn != null && lastInsn.getType() == InsnType.IF) { return false; } if (BlockUtils.checkFirstInsn(block, insn -> insn.contains(AType.EXC_HANDLER))) { return false; } // TODO: implement insn extraction into separate block for partial predecessors int sameInsnCount = getSameLastInsnCount(predecessors); if (sameInsnCount > 0) { List insns = getLastInsns(predecessors.get(0), sameInsnCount); insertAtStart(block, insns); predecessors.forEach(pred -> getLastInsns(pred, sameInsnCount).clear()); mth.addDebugComment("Move duplicate insns, count: " + sameInsnCount + " to block " + block); return true; } } } return false; } private static List getLastInsns(BlockNode blockNode, int sameInsnCount) { List instructions = blockNode.getInstructions(); int size = instructions.size(); return instructions.subList(size - sameInsnCount, size); } private static void insertAtStart(BlockNode block, List insns) { List blockInsns = block.getInstructions(); List newInsnList = new ArrayList<>(insns.size() + blockInsns.size()); newInsnList.addAll(insns); newInsnList.addAll(blockInsns); blockInsns.clear(); blockInsns.addAll(newInsnList); } private static int getSameLastInsnCount(List predecessors) { int sameInsnCount = 0; while (true) { InsnNode insn = null; for (BlockNode pred : predecessors) { InsnNode curInsn = getInsnsFromEnd(pred, sameInsnCount); if (curInsn == null) { return sameInsnCount; } if (insn == null) { insn = curInsn; } else { if (!isSame(insn, curInsn)) { return sameInsnCount; } } } sameInsnCount++; } } private static boolean isSame(InsnNode insn, InsnNode curInsn) { return isInsnsEquals(insn, curInsn) && insn.canReorder(); } private static boolean isInsnsEquals(InsnNode insn, InsnNode otherInsn) { if (insn == otherInsn) { return true; } if (insn.isSame(otherInsn) && sameArgs(insn.getResult(), otherInsn.getResult())) { int argsCount = insn.getArgsCount(); for (int i = 0; i < argsCount; i++) { if (!sameArgs(insn.getArg(i), otherInsn.getArg(i))) { return false; } } return true; } return false; } private static boolean sameArgs(@Nullable InsnArg arg, @Nullable InsnArg otherArg) { if (arg == otherArg) { return true; } if (arg == null || otherArg == null) { return false; } if (arg.getClass().equals(otherArg.getClass())) { if (arg.isRegister()) { return ((RegisterArg) arg).getRegNum() == ((RegisterArg) otherArg).getRegNum(); } if (arg.isLiteral()) { return ((LiteralArg) arg).getLiteral() == ((LiteralArg) otherArg).getLiteral(); } throw new JadxRuntimeException("Unexpected InsnArg types: " + arg + " and " + otherArg); } return false; } private static InsnNode getInsnsFromEnd(BlockNode block, int number) { List instructions = block.getInstructions(); int insnCount = instructions.size(); if (insnCount <= number) { return null; } return instructions.get(insnCount - number - 1); } private static void computeDominators(MethodNode mth) { clearBlocksState(mth); DominatorTree.compute(mth); markLoops(mth); } private static void markLoops(MethodNode mth) { mth.getBasicBlocks().forEach(block -> { // Every successor that dominates its predecessor is a header of a loop, // block -> successor is a back edge. block.getSuccessors().forEach(successor -> { if (block.getDoms().get(successor.getId()) || block == successor) { successor.add(AFlag.LOOP_START); block.add(AFlag.LOOP_END); Set loopBlocks = BlockUtils.getAllPathsBlocks(successor, block); LoopInfo loop = new LoopInfo(successor, block, loopBlocks); successor.addAttr(AType.LOOP, loop); block.addAttr(AType.LOOP, loop); } }); }); } private static void registerLoops(MethodNode mth) { mth.resetLoops(); mth.getBasicBlocks().forEach(block -> { if (block.contains(AFlag.LOOP_START)) { block.getAll(AType.LOOP).forEach(mth::registerLoop); } }); } private static void processNestedLoops(MethodNode mth) { if (mth.getLoopsCount() == 0) { return; } for (LoopInfo outLoop : mth.getLoops()) { for (LoopInfo innerLoop : mth.getLoops()) { if (outLoop == innerLoop) { continue; } if (outLoop.getLoopBlocks().containsAll(innerLoop.getLoopBlocks())) { LoopInfo parentLoop = innerLoop.getParentLoop(); if (parentLoop != null) { if (parentLoop.getLoopBlocks().containsAll(outLoop.getLoopBlocks())) { outLoop.setParentLoop(parentLoop); innerLoop.setParentLoop(outLoop); } else { parentLoop.setParentLoop(outLoop); } } else { innerLoop.setParentLoop(outLoop); } } } } } private static boolean modifyBlocksTree(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { if (checkLoops(mth, block)) { return true; } } if (mergeConstReturn(mth)) { return true; } if (CodeFeaturesAttr.contains(mth, CodeFeaturesAttr.CodeFeature.SWITCH)) { for (BlockNode basicBlock : mth.getBasicBlocks()) { if (duplicateSimpleMoveBlock(mth, basicBlock)) { return true; } } } return splitExitBlocks(mth); } private static boolean mergeConstReturn(MethodNode mth) { if (mth.isVoidReturn()) { return false; } boolean changed = false; for (BlockNode retBlock : new ArrayList<>(mth.getPreExitBlocks())) { BlockNode pred = Utils.getOne(retBlock.getPredecessors()); if (pred != null) { InsnNode constInsn = Utils.getOne(pred.getInstructions()); if (constInsn != null && constInsn.isConstInsn()) { RegisterArg constArg = constInsn.getResult(); InsnNode returnInsn = BlockUtils.getLastInsn(retBlock); if (returnInsn != null && returnInsn.getType() == InsnType.RETURN) { InsnArg retArg = returnInsn.getArg(0); if (constArg.sameReg(retArg)) { mergeConstAndReturnBlocks(mth, retBlock, pred); changed = true; } } } } } if (changed) { removeMarkedBlocks(mth); if (DEBUG_MODS) { mth.get(DebugModAttr.TYPE).addEvent("Merge const return"); } } return changed; } private static void mergeConstAndReturnBlocks(MethodNode mth, BlockNode retBlock, BlockNode pred) { pred.getInstructions().addAll(retBlock.getInstructions()); pred.copyAttributesFrom(retBlock); BlockSplitter.removeConnection(pred, retBlock); retBlock.getInstructions().clear(); retBlock.add(AFlag.REMOVE); BlockNode exitBlock = mth.getExitBlock(); BlockSplitter.removeConnection(retBlock, exitBlock); BlockSplitter.connect(pred, exitBlock); pred.updateCleanSuccessors(); } private static boolean independentBlockTreeMod(MethodNode mth) { boolean changed = false; List basicBlocks = mth.getBasicBlocks(); for (BlockNode basicBlock : basicBlocks) { if (deduplicateBlockInsns(mth, basicBlock)) { changed = true; } } if (BlockExceptionHandler.process(mth)) { changed = true; } for (BlockNode basicBlock : basicBlocks) { if (BlockSplitter.removeEmptyBlock(basicBlock)) { changed = true; } } if (BlockSplitter.removeEmptyDetachedBlocks(mth)) { changed = true; } return changed; } /** * Duplicate block if it contains only one 'move' insn and all predecessors are 'switch' and 'if'. * This will help to resolve switch cases order and fallthrough detection * because such move blocks can be deduplicated by compiler. */ private static boolean duplicateSimpleMoveBlock(MethodNode mth, BlockNode block) { List insns = block.getInstructions(); if (insns.size() == 1 && block.getSuccessors().size() == 1) { InsnNode insn = insns.get(0); if (insn.getType() == InsnType.MOVE) { List preds = block.getPredecessors(); int predSize = preds.size(); if (predSize >= 3 && onlySwitchAndIfInLastInsns(preds)) { // confirmed, duplicate block BlockNode successor = block.getSuccessors().get(0); List predsCopy = new ArrayList<>(preds); for (int i = 1; i < predSize; i++) { BlockNode pred = predsCopy.get(i); BlockNode newBlock = BlockSplitter.startNewBlock(mth, -1); newBlock.add(AFlag.SYNTHETIC); for (InsnNode oldInsn : block.getInstructions()) { InsnNode copyInsn = oldInsn.copyWithoutSsa(); copyInsn.add(AFlag.SYNTHETIC); newBlock.getInstructions().add(copyInsn); } newBlock.copyAttributesFrom(block); BlockSplitter.replaceConnection(pred, block, newBlock); BlockSplitter.connect(newBlock, successor); } return true; } } } return false; } private static boolean onlySwitchAndIfInLastInsns(List preds) { boolean hasSwitch = false; boolean hasIf = false; for (BlockNode pred : preds) { InsnNode lastInsn = BlockUtils.getLastInsn(pred); if (lastInsn == null) { return false; } InsnType insnType = lastInsn.getType(); switch (insnType) { case SWITCH: hasSwitch = true; break; case IF: hasIf = true; break; default: return false; } } return hasSwitch && hasIf; } private static boolean simplifyLoopEnd(MethodNode mth, LoopInfo loop) { BlockNode loopEnd = loop.getEnd(); if (loopEnd.getSuccessors().size() <= 1) { return false; } // make loop end a simple path block BlockNode newLoopEnd = BlockSplitter.startNewBlock(mth, -1); newLoopEnd.add(AFlag.SYNTHETIC); newLoopEnd.add(AFlag.LOOP_END); BlockNode loopStart = loop.getStart(); BlockSplitter.replaceConnection(loopEnd, loopStart, newLoopEnd); BlockSplitter.connect(newLoopEnd, loopStart); if (DEBUG_MODS) { mth.get(DebugModAttr.TYPE).addEvent("Simplify loop end"); } return true; } private static boolean checkLoops(MethodNode mth, BlockNode block) { if (!block.contains(AFlag.LOOP_START)) { return false; } List loops = block.getAll(AType.LOOP); int loopsCount = loops.size(); if (loopsCount == 0) { return false; } for (LoopInfo loop : loops) { if (insertBlocksForBreak(mth, loop)) { return true; } } if (loopsCount > 1 && splitLoops(mth, block, loops)) { return true; } if (loopsCount == 1) { LoopInfo loop = loops.get(0); return insertBlocksForContinue(mth, loop) || insertPreHeader(mth, loop) || simplifyLoopEnd(mth, loop); } return false; } /** * Insert simple path block before loop header */ private static boolean insertPreHeader(MethodNode mth, LoopInfo loop) { BlockNode start = loop.getStart(); List preds = start.getPredecessors(); int predsCount = preds.size() - 1; // don't count back edge if (predsCount == 1) { return false; } if (predsCount == 0) { if (!start.contains(AFlag.MTH_ENTER_BLOCK)) { mth.addWarnComment("Unexpected block without predecessors: " + start); } BlockNode newEnterBlock = BlockSplitter.startNewBlock(mth, -1); newEnterBlock.add(AFlag.SYNTHETIC); newEnterBlock.add(AFlag.MTH_ENTER_BLOCK); mth.setEnterBlock(newEnterBlock); start.remove(AFlag.MTH_ENTER_BLOCK); BlockSplitter.connect(newEnterBlock, start); } else { // multiple predecessors BlockNode preHeader = BlockSplitter.startNewBlock(mth, -1); preHeader.add(AFlag.SYNTHETIC); BlockNode loopEnd = loop.getEnd(); for (BlockNode pred : new ArrayList<>(preds)) { if (pred != loopEnd) { BlockSplitter.replaceConnection(pred, start, preHeader); } } BlockSplitter.connect(preHeader, start); } if (DEBUG_MODS) { mth.get(DebugModAttr.TYPE).addEvent("Insert loop pre header"); } return true; } /** * Insert additional blocks for possible 'break' insertion */ private static boolean insertBlocksForBreak(MethodNode mth, LoopInfo loop) { boolean change = false; List edges = loop.getExitEdges(); if (!edges.isEmpty()) { for (Edge edge : edges) { BlockNode target = edge.getTarget(); BlockNode source = edge.getSource(); if (!target.contains(AFlag.SYNTHETIC) && !source.contains(AFlag.SYNTHETIC)) { BlockSplitter.insertBlockBetween(mth, source, target); change = true; } } } if (DEBUG_MODS && change) { mth.get(DebugModAttr.TYPE).addEvent("Insert loop break blocks"); } return change; } /** * Insert additional blocks for possible 'continue' insertion */ private static boolean insertBlocksForContinue(MethodNode mth, LoopInfo loop) { BlockNode loopEnd = loop.getEnd(); boolean change = false; List preds = loopEnd.getPredecessors(); if (preds.size() > 1) { for (BlockNode pred : new ArrayList<>(preds)) { if (!pred.contains(AFlag.SYNTHETIC)) { BlockSplitter.insertBlockBetween(mth, pred, loopEnd); change = true; } } } if (DEBUG_MODS && change) { mth.get(DebugModAttr.TYPE).addEvent("Insert loop continue block"); } return change; } private static boolean splitLoops(MethodNode mth, BlockNode block, List loops) { boolean oneHeader = true; for (LoopInfo loop : loops) { if (loop.getStart() != block) { oneHeader = false; break; } } if (!oneHeader) { return false; } // several back edges connected to one loop header => make additional block BlockNode newLoopEnd = BlockSplitter.startNewBlock(mth, block.getStartOffset()); newLoopEnd.add(AFlag.SYNTHETIC); connect(newLoopEnd, block); for (LoopInfo la : loops) { BlockSplitter.replaceConnection(la.getEnd(), block, newLoopEnd); } if (DEBUG_MODS) { mth.get(DebugModAttr.TYPE).addEvent("Split loops"); } return true; } private static boolean splitExitBlocks(MethodNode mth) { boolean changed = false; for (BlockNode preExitBlock : mth.getPreExitBlocks()) { if (splitReturn(mth, preExitBlock)) { changed = true; } else if (splitThrow(mth, preExitBlock)) { changed = true; } } if (changed) { updateExitBlockConnections(mth); if (DEBUG_MODS) { mth.get(DebugModAttr.TYPE).addEvent("Split exit block"); } } return changed; } private static void updateExitBlockConnections(MethodNode mth) { BlockNode exitBlock = mth.getExitBlock(); BlockSplitter.removePredecessors(exitBlock); for (BlockNode block : mth.getBasicBlocks()) { if (block != exitBlock && block.getSuccessors().isEmpty() && !block.contains(AFlag.REMOVE)) { BlockSplitter.connect(block, exitBlock); } } } /** * Splice return block if several predecessors presents */ private static boolean splitReturn(MethodNode mth, BlockNode returnBlock) { if (returnBlock.contains(AFlag.SYNTHETIC) || returnBlock.contains(AFlag.ORIG_RETURN) || returnBlock.contains(AType.EXC_HANDLER)) { return false; } List preds = returnBlock.getPredecessors(); if (preds.size() < 2) { return false; } InsnNode returnInsn = BlockUtils.getLastInsn(returnBlock); if (returnInsn == null) { return false; } if (returnInsn.getArgsCount() == 1 && returnBlock.getInstructions().size() == 1 && !isArgAssignInPred(preds, returnInsn.getArg(0))) { return false; } boolean first = true; for (BlockNode pred : new ArrayList<>(preds)) { if (first) { returnBlock.add(AFlag.ORIG_RETURN); first = false; } else { BlockNode newRetBlock = BlockSplitter.startNewBlock(mth, -1); newRetBlock.add(AFlag.SYNTHETIC); newRetBlock.add(AFlag.RETURN); for (InsnNode oldInsn : returnBlock.getInstructions()) { InsnNode copyInsn = oldInsn.copyWithoutSsa(); copyInsn.add(AFlag.SYNTHETIC); newRetBlock.getInstructions().add(copyInsn); } BlockSplitter.replaceConnection(pred, returnBlock, newRetBlock); } } return true; } private static boolean splitThrow(MethodNode mth, BlockNode exitBlock) { if (exitBlock.contains(AFlag.IGNORE_THROW_SPLIT)) { return false; } List preds = exitBlock.getPredecessors(); if (preds.size() < 2) { return false; } InsnNode throwInsn = BlockUtils.getLastInsn(exitBlock); if (throwInsn == null || throwInsn.getType() != InsnType.THROW) { return false; } // split only for several exception handlers // traverse predecessors to exception handler Map handlersMap = new HashMap<>(preds.size()); Set handlers = new HashSet<>(preds.size()); for (BlockNode pred : preds) { BlockUtils.visitPredecessorsUntil(mth, pred, block -> { ExcHandlerAttr excHandlerAttr = block.get(AType.EXC_HANDLER); if (excHandlerAttr == null) { return false; } boolean correctHandler = excHandlerAttr.getHandler().getBlocks().contains(block); if (correctHandler && isArgAssignInPred(Collections.singletonList(block), throwInsn.getArg(0))) { handlersMap.put(pred, excHandlerAttr); handlers.add(block); } return correctHandler; }); } if (handlers.size() == 1) { exitBlock.add(AFlag.IGNORE_THROW_SPLIT); return false; } boolean first = true; for (BlockNode pred : new ArrayList<>(preds)) { if (first) { first = false; } else { BlockNode newThrowBlock = BlockSplitter.startNewBlock(mth, -1); newThrowBlock.add(AFlag.SYNTHETIC); for (InsnNode oldInsn : exitBlock.getInstructions()) { InsnNode copyInsn = oldInsn.copyWithoutSsa(); copyInsn.add(AFlag.SYNTHETIC); newThrowBlock.getInstructions().add(copyInsn); } newThrowBlock.copyAttributesFrom(exitBlock); ExcHandlerAttr excHandlerAttr = handlersMap.get(pred); if (excHandlerAttr != null) { excHandlerAttr.getHandler().addBlock(newThrowBlock); } BlockSplitter.replaceConnection(pred, exitBlock, newThrowBlock); } } return true; } private static boolean isArgAssignInPred(List preds, InsnArg arg) { if (arg.isRegister()) { int regNum = ((RegisterArg) arg).getRegNum(); for (BlockNode pred : preds) { for (InsnNode insnNode : pred.getInstructions()) { RegisterArg result = insnNode.getResult(); if (result != null && result.getRegNum() == regNum) { return true; } } } } return false; } public static void removeMarkedBlocks(MethodNode mth) { boolean removed = mth.getBasicBlocks().removeIf(block -> { if (block.contains(AFlag.REMOVE)) { if (!block.getPredecessors().isEmpty() || !block.getSuccessors().isEmpty()) { LOG.warn("Block {} not deleted, method: {}", block, mth); } else { TryCatchBlockAttr tryBlockAttr = block.get(AType.TRY_BLOCK); if (tryBlockAttr != null) { tryBlockAttr.removeBlock(block); } return true; } } return false; }); if (removed) { mth.updateBlockPositions(); } } private static void removeUnreachableBlocks(MethodNode mth) { Set toRemove = new LinkedHashSet<>(); for (BlockNode block : mth.getBasicBlocks()) { computeUnreachableFromBlock(toRemove, block, mth); } removeFromMethod(toRemove, mth); } public static void removeUnreachableBlock(BlockNode blockToRemove, MethodNode mth) { Set toRemove = new LinkedHashSet<>(); computeUnreachableFromBlock(toRemove, blockToRemove, mth); removeFromMethod(toRemove, mth); } private static void computeUnreachableFromBlock(Set toRemove, BlockNode block, MethodNode mth) { if (block.getPredecessors().isEmpty() && block != mth.getEnterBlock()) { BlockSplitter.collectSuccessors(block, mth.getEnterBlock(), toRemove); } } private static void removeFromMethod(Set toRemove, MethodNode mth) { if (toRemove.isEmpty()) { return; } long notEmptyBlocks = toRemove.stream().filter(block -> !block.getInstructions().isEmpty()).count(); if (notEmptyBlocks != 0) { int insnsCount = toRemove.stream().mapToInt(block -> block.getInstructions().size()).sum(); mth.addWarnComment("Unreachable blocks removed: " + notEmptyBlocks + ", instructions: " + insnsCount); } toRemove.forEach(BlockSplitter::detachBlock); mth.getBasicBlocks().removeAll(toRemove); mth.updateBlockPositions(); } private static void clearBlocksState(MethodNode mth) { mth.getBasicBlocks().forEach(block -> { block.remove(AType.LOOP); block.remove(AFlag.LOOP_START); block.remove(AFlag.LOOP_END); block.setDoms(null); block.setIDom(null); block.setDomFrontier(null); block.getDominatesOn().clear(); }); } private static final class DebugModAttr implements IJadxAttribute { static final IJadxAttrType TYPE = IJadxAttrType.create("DebugModAttr"); private final Map statMap = new HashMap<>(); public void addEvent(String name) { statMap.merge(name, 1, Integer::sum); } public String formatStats() { return statMap.entrySet().stream() .map(entry -> " " + entry.getKey() + ": " + entry.getValue()) .collect(Collectors.joining("\n")); } @Override public IJadxAttrType getAttrType() { return TYPE; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockSplitter.java ================================================ package jadx.core.dex.visitors.blocks; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.JumpInfo; import jadx.core.dex.attributes.nodes.TmpEdgeAttr; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.TargetInsnNode; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.exceptions.JadxRuntimeException; public class BlockSplitter extends AbstractVisitor { /** * Leave these instructions alone in the block node */ private static final Set SEPARATE_INSNS = EnumSet.of( InsnType.RETURN, InsnType.IF, InsnType.SWITCH, InsnType.MONITOR_ENTER, InsnType.MONITOR_EXIT, InsnType.THROW, InsnType.MOVE_EXCEPTION); public static boolean isSeparate(InsnType insnType) { return SEPARATE_INSNS.contains(insnType); } /** * Split without connecting to the next block */ private static final Set SPLIT_WITHOUT_CONNECT = EnumSet.of( InsnType.RETURN, InsnType.THROW, InsnType.GOTO, InsnType.IF, InsnType.SWITCH, InsnType.JAVA_JSR, InsnType.JAVA_RET); @Override public void visit(MethodNode mth) { if (mth.isNoCode()) { return; } mth.initBasicBlocks(); Map blocksMap = splitBasicBlocks(mth); setupConnectionsFromJumps(mth, blocksMap); initBlocksInTargetNodes(mth); expandMoveMulti(mth); if (mth.contains(AFlag.RESOLVE_JAVA_JSR)) { ResolveJavaJSR.process(mth); } removeJumpAttr(mth); removeInsns(mth); removeEmptyDetachedBlocks(mth); mth.getBasicBlocks().removeIf(BlockSplitter::removeEmptyBlock); addTempConnectionsForExcHandlers(mth, blocksMap); setupExitConnections(mth); mth.updateBlockPositions(); mth.unloadInsnArr(); } private static Map splitBasicBlocks(MethodNode mth) { BlockNode enterBlock = startNewBlock(mth, -1); enterBlock.add(AFlag.MTH_ENTER_BLOCK); mth.setEnterBlock(enterBlock); BlockNode exitBlock = startNewBlock(mth, -1); exitBlock.add(AFlag.MTH_EXIT_BLOCK); mth.setExitBlock(exitBlock); Map blocksMap = new HashMap<>(); BlockNode curBlock = enterBlock; InsnNode prevInsn = null; for (InsnNode insn : mth.getInstructions()) { if (insn == null) { continue; } if (insn.getType() == InsnType.NOP && insn.isAttrStorageEmpty()) { continue; } int insnOffset = insn.getOffset(); if (prevInsn == null) { // first block after method enter block curBlock = connectNewBlock(mth, curBlock, insnOffset); } else { InsnType prevType = prevInsn.getType(); if (SPLIT_WITHOUT_CONNECT.contains(prevType)) { curBlock = startNewBlock(mth, insnOffset); } else if (isSeparate(prevType) || isSeparate(insn.getType()) || insn.contains(AFlag.TRY_ENTER) || prevInsn.contains(AFlag.TRY_LEAVE) || insn.contains(AType.EXC_HANDLER) || isSplitByJump(prevInsn, insn) || isDoWhile(blocksMap, curBlock, insn)) { curBlock = connectNewBlock(mth, curBlock, insnOffset); } } blocksMap.put(insnOffset, curBlock); curBlock.getInstructions().add(insn); prevInsn = insn; } return blocksMap; } /** * Init 'then' and 'else' blocks for 'if' instruction. */ private static void initBlocksInTargetNodes(MethodNode mth) { mth.getBasicBlocks().forEach(block -> { InsnNode lastInsn = BlockUtils.getLastInsn(block); if (lastInsn instanceof TargetInsnNode) { ((TargetInsnNode) lastInsn).initBlocks(block); } }); } static BlockNode connectNewBlock(MethodNode mth, BlockNode block, int offset) { BlockNode newBlock = startNewBlock(mth, offset); connect(block, newBlock); return newBlock; } static BlockNode startNewBlock(MethodNode mth, int offset) { List blocks = mth.getBasicBlocks(); BlockNode block = new BlockNode(mth.getNextBlockCId(), blocks.size(), offset); blocks.add(block); return block; } public static void connect(BlockNode from, BlockNode to) { if (!from.getSuccessors().contains(to)) { from.getSuccessors().add(to); } if (!to.getPredecessors().contains(from)) { to.getPredecessors().add(from); } } public static void removeConnection(BlockNode from, BlockNode to) { from.getSuccessors().remove(to); to.getPredecessors().remove(from); } public static void removePredecessors(BlockNode block) { for (BlockNode pred : block.getPredecessors()) { pred.getSuccessors().remove(block); } block.getPredecessors().clear(); } public static void replaceConnection(BlockNode source, BlockNode oldDest, BlockNode newDest) { removeConnection(source, oldDest); connect(source, newDest); replaceTarget(source, oldDest, newDest); } public static BlockNode insertBlockBetween(MethodNode mth, BlockNode source, BlockNode target) { BlockNode newBlock = startNewBlock(mth, target.getStartOffset()); newBlock.add(AFlag.SYNTHETIC); removeConnection(source, target); connect(source, newBlock); connect(newBlock, target); replaceTarget(source, target, newBlock); source.updateCleanSuccessors(); newBlock.updateCleanSuccessors(); return newBlock; } static BlockNode blockSplitTop(MethodNode mth, BlockNode block) { BlockNode newBlock = startNewBlock(mth, block.getStartOffset()); for (BlockNode pred : new ArrayList<>(block.getPredecessors())) { replaceConnection(pred, block, newBlock); pred.updateCleanSuccessors(); } connect(newBlock, block); newBlock.updateCleanSuccessors(); return newBlock; } static void copyBlockData(BlockNode from, BlockNode to) { List toInsns = to.getInstructions(); for (InsnNode insn : from.getInstructions()) { toInsns.add(insn.copyWithoutSsa()); } to.copyAttributesFrom(from); } static List copyBlocksTree(MethodNode mth, List blocks) { List copyBlocks = new ArrayList<>(blocks.size()); Map map = new HashMap<>(); for (BlockNode block : blocks) { BlockNode newBlock = startNewBlock(mth, block.getStartOffset()); copyBlockData(block, newBlock); copyBlocks.add(newBlock); map.put(block, newBlock); } for (BlockNode block : blocks) { BlockNode newBlock = getNewBlock(block, map); for (BlockNode successor : block.getSuccessors()) { BlockNode newSuccessor = getNewBlock(successor, map); BlockSplitter.connect(newBlock, newSuccessor); } } return copyBlocks; } private static BlockNode getNewBlock(BlockNode block, Map map) { BlockNode newBlock = map.get(block); if (newBlock == null) { throw new JadxRuntimeException("Copy blocks tree failed. Missing block for connection: " + block); } return newBlock; } static void replaceTarget(BlockNode source, BlockNode oldTarget, BlockNode newTarget) { InsnNode lastInsn = BlockUtils.getLastInsn(source); if (lastInsn instanceof TargetInsnNode) { ((TargetInsnNode) lastInsn).replaceTargetBlock(oldTarget, newTarget); } } private static void setupConnectionsFromJumps(MethodNode mth, Map blocksMap) { for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { List jumps = insn.getAll(AType.JUMP); for (JumpInfo jump : jumps) { BlockNode srcBlock = getBlock(jump.getSrc(), blocksMap); BlockNode thisBlock = getBlock(jump.getDest(), blocksMap); connect(srcBlock, thisBlock); } } } } /** * Connect exception handlers to the throw block. * This temporary connection is necessary to build close to a final dominator tree. * Will be used and removed in {@code jadx.core.dex.visitors.blocks.BlockExceptionHandler} */ private static void addTempConnectionsForExcHandlers(MethodNode mth, Map blocksMap) { if (mth.isNoExceptionHandlers()) { return; } for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { CatchAttr catchAttr = insn.get(AType.EXC_CATCH); if (catchAttr == null) { continue; } for (ExceptionHandler handler : catchAttr.getHandlers()) { BlockNode handlerBlock = getBlock(handler.getHandlerOffset(), blocksMap); if (!handlerBlock.contains(AType.TMP_EDGE)) { List preds = block.getPredecessors(); if (preds.isEmpty()) { throw new JadxRuntimeException("Unexpected missing predecessor for block: " + block); } BlockNode start = preds.size() == 1 ? preds.get(0) : block; if (!start.getSuccessors().contains(handlerBlock)) { connect(start, handlerBlock); handlerBlock.addAttr(new TmpEdgeAttr(start)); } } } } } } private static void setupExitConnections(MethodNode mth) { BlockNode exitBlock = mth.getExitBlock(); for (BlockNode block : mth.getBasicBlocks()) { if (block.getSuccessors().isEmpty() && block != exitBlock) { connect(block, exitBlock); if (BlockUtils.checkLastInsnType(block, InsnType.RETURN)) { block.add(AFlag.RETURN); } } } } private static boolean isSplitByJump(InsnNode prevInsn, InsnNode currentInsn) { List pJumps = prevInsn.getAll(AType.JUMP); for (JumpInfo jump : pJumps) { if (jump.getSrc() == prevInsn.getOffset()) { return true; } } List cJumps = currentInsn.getAll(AType.JUMP); for (JumpInfo jump : cJumps) { if (jump.getDest() == currentInsn.getOffset()) { return true; } } return false; } private static boolean isDoWhile(Map blocksMap, BlockNode curBlock, InsnNode insn) { // split 'do-while' block (last instruction: 'if', target this block) if (insn.getType() != InsnType.IF) { return false; } IfNode ifs = (IfNode) insn; BlockNode targetBlock = blocksMap.get(ifs.getTarget()); return targetBlock == curBlock; } private static BlockNode getBlock(int offset, Map blocksMap) { BlockNode block = blocksMap.get(offset); if (block == null) { throw new JadxRuntimeException("Missing block: " + offset); } return block; } private static void expandMoveMulti(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { List insnsList = block.getInstructions(); int len = insnsList.size(); for (int i = 0; i < len; i++) { InsnNode insn = insnsList.get(i); if (insn.getType() == InsnType.MOVE_MULTI) { int mvCount = insn.getArgsCount() / 2; for (int j = 0; j < mvCount; j++) { InsnNode mv = new InsnNode(InsnType.MOVE, 1); int startArg = j * 2; mv.setResult((RegisterArg) insn.getArg(startArg)); mv.addArg(insn.getArg(startArg + 1)); mv.copyAttributesFrom(insn); if (j == 0) { mv.setOffset(insn.getOffset()); insnsList.set(i, mv); } else { insnsList.add(i + j, mv); } } i += mvCount - 1; len = insnsList.size(); } } } } private static void removeJumpAttr(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { insn.remove(AType.JUMP); } } } private static void removeInsns(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { block.getInstructions().removeIf(insn -> { if (!insn.isAttrStorageEmpty()) { return false; } InsnType insnType = insn.getType(); return insnType == InsnType.GOTO || insnType == InsnType.NOP; }); } } public static void detachMarkedBlocks(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { if (block.contains(AFlag.REMOVE)) { detachBlock(block); } } } static boolean removeEmptyDetachedBlocks(MethodNode mth) { return mth.getBasicBlocks().removeIf(block -> block.getInstructions().isEmpty() && block.getPredecessors().isEmpty() && block.getSuccessors().isEmpty() && !block.contains(AFlag.MTH_ENTER_BLOCK) && !block.contains(AFlag.MTH_EXIT_BLOCK)); } static boolean removeEmptyBlock(BlockNode block) { if (canRemoveBlock(block)) { if (block.getSuccessors().size() == 1) { BlockNode successor = block.getSuccessors().get(0); block.getPredecessors().forEach(pred -> { pred.getSuccessors().remove(block); BlockSplitter.connect(pred, successor); BlockSplitter.replaceTarget(pred, block, successor); pred.updateCleanSuccessors(); }); BlockSplitter.removeConnection(block, successor); } else { block.getPredecessors().forEach(pred -> { pred.getSuccessors().remove(block); pred.updateCleanSuccessors(); }); } block.add(AFlag.REMOVE); block.getSuccessors().clear(); block.getPredecessors().clear(); return true; } return false; } private static boolean canRemoveBlock(BlockNode block) { return block.getInstructions().isEmpty() && block.isAttrStorageEmpty() && block.getSuccessors().size() <= 1 && !block.getPredecessors().isEmpty() && !block.contains(AFlag.MTH_ENTER_BLOCK) && !block.contains(AFlag.MTH_EXIT_BLOCK) && !block.getSuccessors().contains(block); // no self loop } static void collectSuccessors(BlockNode startBlock, BlockNode methodEnterBlock, Set toRemove) { Deque stack = new ArrayDeque<>(); stack.add(startBlock); while (!stack.isEmpty()) { BlockNode block = stack.pop(); if (!toRemove.contains(block)) { toRemove.add(block); for (BlockNode successor : block.getSuccessors()) { if (successor != methodEnterBlock && toRemove.containsAll(successor.getPredecessors())) { stack.push(successor); } } } } } static void detachBlock(BlockNode block) { for (BlockNode pred : block.getPredecessors()) { pred.getSuccessors().remove(block); pred.updateCleanSuccessors(); } for (BlockNode successor : block.getSuccessors()) { successor.getPredecessors().remove(block); } block.add(AFlag.REMOVE); block.getPredecessors().clear(); block.getSuccessors().clear(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/blocks/DominatorTree.java ================================================ package jadx.core.dex.visitors.blocks; import java.util.ArrayList; import java.util.BitSet; import java.util.List; import java.util.function.Function; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.BlockUtils; import jadx.core.utils.EmptyBitSet; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Build dominator tree based on the algorithm described in paper: * Cooper, Keith D.; Harvey, Timothy J; Kennedy, Ken (2001). * "A Simple, Fast Dominance Algorithm" * http://www.hipersoft.rice.edu/grads/publications/dom14.pdf */ @SuppressWarnings("JavadocLinkAsPlainText") public class DominatorTree { public static void compute(MethodNode mth) { List sorted = sortBlocks(mth); BlockNode[] doms = build(sorted, BlockNode::getPredecessors); apply(sorted, doms); } private static List sortBlocks(MethodNode mth) { int blocksCount = mth.getBasicBlocks().size(); List sorted = new ArrayList<>(blocksCount); BlockUtils.visitDFS(mth, sorted::add); if (sorted.size() != blocksCount) { throw new JadxRuntimeException("Found unreachable blocks"); } mth.setBasicBlocks(sorted); return sorted; } static BlockNode[] build(List sorted, Function> predFunc) { int blocksCount = sorted.size(); BlockNode[] doms = new BlockNode[blocksCount]; doms[0] = sorted.get(0); boolean changed = true; while (changed) { changed = false; for (int blockId = 1; blockId < blocksCount; blockId++) { BlockNode b = sorted.get(blockId); List preds = predFunc.apply(b); int pickedPred = -1; BlockNode newIDom = null; for (BlockNode pred : preds) { int id = pred.getId(); if (doms[id] != null) { newIDom = pred; pickedPred = id; break; } } if (newIDom == null) { throw new JadxRuntimeException("No immediate dominator for block: " + b); } for (BlockNode predBlock : preds) { int predId = predBlock.getId(); if (predId == pickedPred) { continue; } if (doms[predId] != null) { newIDom = intersect(sorted, doms, predBlock, newIDom); } } if (doms[blockId] != newIDom) { doms[blockId] = newIDom; changed = true; } } } return doms; } private static BlockNode intersect(List sorted, BlockNode[] doms, BlockNode b1, BlockNode b2) { int f1 = b1.getId(); int f2 = b2.getId(); while (f1 != f2) { while (f1 > f2) { f1 = doms[f1].getId(); } while (f2 > f1) { f2 = doms[f2].getId(); } } return sorted.get(f1); } private static void apply(List sorted, BlockNode[] doms) { BlockNode enterBlock = sorted.get(0); enterBlock.setDoms(EmptyBitSet.EMPTY); enterBlock.setIDom(null); int blocksCount = sorted.size(); for (int i = 1; i < blocksCount; i++) { BlockNode block = sorted.get(i); BlockNode idom = doms[i]; block.setIDom(idom); idom.addDominatesOn(block); BitSet domBS = collectDoms(doms, idom); domBS.clear(i); block.setDoms(domBS); } } static BitSet collectDoms(BlockNode[] doms, BlockNode idom) { BitSet domBS = new BitSet(doms.length); BlockNode nextIDom = idom; while (true) { int id = nextIDom.getId(); if (domBS.get(id)) { break; } domBS.set(id); BitSet curDoms = nextIDom.getDoms(); if (curDoms != null) { // use already collected set domBS.or(curDoms); break; } nextIDom = doms[id]; } return domBS; } public static void computeDominanceFrontier(MethodNode mth) { List blocks = mth.getBasicBlocks(); for (BlockNode block : blocks) { block.setDomFrontier(null); } int blocksCount = blocks.size(); for (BlockNode block : blocks) { List preds = block.getPredecessors(); if (preds.size() >= 2) { BlockNode idom = block.getIDom(); for (BlockNode pred : preds) { BlockNode runner = pred; while (runner != idom) { addToDF(runner, block, blocksCount); runner = runner.getIDom(); } } } } for (BlockNode block : blocks) { BitSet df = block.getDomFrontier(); if (df == null || df.isEmpty()) { block.setDomFrontier(EmptyBitSet.EMPTY); } } } private static void addToDF(BlockNode block, BlockNode dfBlock, int blocksCount) { BitSet df = block.getDomFrontier(); if (df == null) { df = new BitSet(blocksCount); block.setDomFrontier(df); } df.set(dfBlock.getId()); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/blocks/FixMultiEntryLoops.java ================================================ package jadx.core.dex.visitors.blocks; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.SpecialEdgeAttr; import jadx.core.dex.attributes.nodes.SpecialEdgeAttr.SpecialEdgeType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.ListUtils; public class FixMultiEntryLoops { public static boolean process(MethodNode mth) { try { detectSpecialEdges(mth); } catch (Exception e) { mth.addWarnComment("Failed to detect multi-entry loops", e); return false; } List specialEdges = mth.getAll(AType.SPECIAL_EDGE); List multiEntryLoops = specialEdges.stream() .filter(e -> e.getType() == SpecialEdgeType.BACK_EDGE && !isSingleEntryLoop(e)) .collect(Collectors.toList()); if (multiEntryLoops.isEmpty()) { return false; } try { List crossEdges = ListUtils.filter(specialEdges, e -> e.getType() == SpecialEdgeType.CROSS_EDGE); boolean changed = false; for (SpecialEdgeAttr backEdge : multiEntryLoops) { changed |= fixLoop(mth, backEdge, crossEdges); } return changed; } catch (Exception e) { mth.addWarnComment("Failed to fix multi-entry loops", e); return false; } } private static boolean fixLoop(MethodNode mth, SpecialEdgeAttr backEdge, List crossEdges) { if (isHeaderSuccessorEntry(mth, backEdge, crossEdges)) { return true; } if (isEndBlockEntry(mth, backEdge, crossEdges)) { return true; } mth.addWarnComment("Unsupported multi-entry loop pattern (" + backEdge + "). Please report as a decompilation issue!!!"); return false; } private static boolean isHeaderSuccessorEntry(MethodNode mth, SpecialEdgeAttr backEdge, List crossEdges) { BlockNode header = backEdge.getEnd(); BlockNode headerIDom = header.getIDom(); SpecialEdgeAttr subEntry = ListUtils.filterOnlyOne(crossEdges, e -> e.getStart() == headerIDom); if (subEntry == null || !ListUtils.isSingleElement(header.getSuccessors(), subEntry.getEnd())) { return false; } BlockNode loopEnd = backEdge.getStart(); BlockNode subEntryBlock = subEntry.getEnd(); BlockNode copyHeader = BlockSplitter.insertBlockBetween(mth, loopEnd, header); BlockSplitter.copyBlockData(header, copyHeader); BlockSplitter.replaceConnection(copyHeader, header, subEntryBlock); mth.addDebugComment("Duplicate block (" + header + ") to fix multi-entry loop: " + backEdge); return true; } private static boolean isEndBlockEntry(MethodNode mth, SpecialEdgeAttr backEdge, List crossEdges) { BlockNode loopEnd = backEdge.getStart(); SpecialEdgeAttr subEntry = ListUtils.filterOnlyOne(crossEdges, e -> e.getEnd() == loopEnd); if (subEntry == null) { return false; } dupPath(mth, subEntry.getStart(), loopEnd, backEdge.getEnd()); mth.addDebugComment("Duplicate block (" + loopEnd + ") to fix multi-entry loop: " + backEdge); return true; } /** * Duplicate 'center' block on path from 'start' to 'end' */ private static void dupPath(MethodNode mth, BlockNode start, BlockNode center, BlockNode end) { BlockNode copyCenter = BlockSplitter.insertBlockBetween(mth, start, end); BlockSplitter.copyBlockData(center, copyCenter); BlockSplitter.removeConnection(start, center); } private static boolean isSingleEntryLoop(SpecialEdgeAttr e) { BlockNode header = e.getEnd(); BlockNode loopEnd = e.getStart(); return header == loopEnd || loopEnd.getDoms().get(header.getId()); // header dominates loop end } private enum BlockColor { WHITE, GRAY, BLACK } private static void detectSpecialEdges(MethodNode mth) { BlockColor[] colors = new BlockColor[mth.getBasicBlocks().size()]; Arrays.fill(colors, BlockColor.WHITE); colorDFS(mth, colors, mth.getEnterBlock()); } // TODO: transform to non-recursive form private static void colorDFS(MethodNode mth, BlockColor[] colors, BlockNode block) { colors[block.getId()] = BlockColor.GRAY; for (BlockNode v : block.getSuccessors()) { switch (colors[v.getId()]) { case WHITE: colorDFS(mth, colors, v); break; case GRAY: mth.addAttr(AType.SPECIAL_EDGE, new SpecialEdgeAttr(SpecialEdgeType.BACK_EDGE, block, v)); break; case BLACK: mth.addAttr(AType.SPECIAL_EDGE, new SpecialEdgeAttr(SpecialEdgeType.CROSS_EDGE, block, v)); break; } } colors[block.getId()] = BlockColor.BLACK; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/blocks/PostDominatorTree.java ================================================ package jadx.core.dex.visitors.blocks; import java.util.ArrayList; import java.util.BitSet; import java.util.List; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.BlockUtils; import jadx.core.utils.EmptyBitSet; public class PostDominatorTree { public static void compute(MethodNode mth) { if (!mth.contains(AFlag.COMPUTE_POST_DOM)) { return; } try { int mthBlocksCount = mth.getBasicBlocks().size(); List sorted = new ArrayList<>(mthBlocksCount); BlockUtils.visitReverseDFS(mth, sorted::add); // temporary set block positions to match reverse sorted order // save old positions for later remapping int blocksCount = sorted.size(); int[] posMapping = new int[mthBlocksCount]; for (int i = 0; i < blocksCount; i++) { posMapping[i] = sorted.get(i).getPos(); } BlockNode.updateBlockPositions(sorted); BlockNode[] postDoms = DominatorTree.build(sorted, BlockNode::getSuccessors); BlockNode firstBlock = sorted.get(0); firstBlock.setPostDoms(EmptyBitSet.EMPTY); firstBlock.setIPostDom(null); for (int i = 1; i < blocksCount; i++) { BlockNode block = sorted.get(i); BlockNode iPostDom = postDoms[i]; block.setIPostDom(iPostDom); BitSet postDomBS = DominatorTree.collectDoms(postDoms, iPostDom); block.setPostDoms(postDomBS); } for (int i = 1; i < blocksCount; i++) { BlockNode block = sorted.get(i); BitSet bs = new BitSet(blocksCount); block.getPostDoms().stream().forEach(n -> bs.set(posMapping[n])); bs.clear(posMapping[i]); block.setPostDoms(bs); } // check for missing blocks in 'sorted' list // can be caused by infinite loops int blocksDelta = mthBlocksCount - blocksCount; if (blocksDelta != 0) { int insnsCount = 0; for (BlockNode block : mth.getBasicBlocks()) { if (block.getPostDoms() == null) { block.setPostDoms(EmptyBitSet.EMPTY); block.setIPostDom(null); insnsCount += block.getInstructions().size(); } } mth.addInfoComment("Infinite loop detected, blocks: " + blocksDelta + ", insns: " + insnsCount); } } catch (StackOverflowError | Exception e) { // show error as a warning because this info not always used mth.addWarnComment("Failed to build post-dominance tree", e); } finally { // revert block positions change mth.updateBlockPositions(); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/blocks/ResolveJavaJSR.java ================================================ package jadx.core.dex.visitors.blocks; import java.util.ArrayList; import java.util.List; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.BlockUtils; import jadx.core.utils.ListUtils; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Duplicate code to resolve java jsr/ret. * JSR (jump subroutine) allows executing the same code from different places. * Used mostly for 'finally' blocks, deprecated in Java 7. */ public class ResolveJavaJSR { public static void process(MethodNode mth) { int blocksCount = mth.getBasicBlocks().size(); int k = 0; while (true) { boolean changed = resolve(mth); if (!changed) { break; } if (k++ > blocksCount) { throw new JadxRuntimeException("Fail to resolve jsr instructions"); } } } private static boolean resolve(MethodNode mth) { List blocks = mth.getBasicBlocks(); int blocksCount = blocks.size(); for (BlockNode block : blocks) { if (BlockUtils.checkLastInsnType(block, InsnType.JAVA_RET)) { resolveForRetBlock(mth, block); if (blocksCount != mth.getBasicBlocks().size()) { return true; } } } return false; } private static void resolveForRetBlock(MethodNode mth, BlockNode retBlock) { BlockUtils.visitPredecessorsUntil(mth, retBlock, startBlock -> { List preds = startBlock.getPredecessors(); if (preds.size() > 1 && preds.stream().allMatch(p -> BlockUtils.checkLastInsnType(p, InsnType.JAVA_JSR))) { List jsrBlocks = new ArrayList<>(preds); List dupBlocks = BlockUtils.collectAllSuccessors(mth, startBlock, false); removeInsns(retBlock, startBlock, jsrBlocks); processBlocks(mth, retBlock, startBlock, jsrBlocks, dupBlocks); return true; } return false; }); } private static void removeInsns(BlockNode retBlock, BlockNode startBlock, List jsrBlocks) { InsnNode retInsn = ListUtils.removeLast(retBlock.getInstructions()); if (retInsn != null && retInsn.getType() == InsnType.JAVA_RET) { InsnArg retArg = retInsn.getArg(0); if (retArg.isRegister()) { int regNum = ((RegisterArg) retArg).getRegNum(); InsnNode startInsn = BlockUtils.getFirstInsn(startBlock); if (startInsn != null && startInsn.getType() == InsnType.MOVE && startInsn.getResult().getRegNum() == regNum) { startBlock.getInstructions().remove(0); } } } jsrBlocks.forEach(p -> ListUtils.removeLast(p.getInstructions())); } private static void processBlocks(MethodNode mth, BlockNode retBlock, BlockNode startBlock, List jsrBlocks, List dupBlocks) { BlockNode first = null; for (BlockNode jsrBlock : jsrBlocks) { if (first == null) { first = jsrBlock; } else { BlockNode pathBlock = BlockUtils.selectOther(startBlock, jsrBlock.getSuccessors()); BlockSplitter.removeConnection(jsrBlock, startBlock); BlockSplitter.removeConnection(jsrBlock, pathBlock); List newBlocks = BlockSplitter.copyBlocksTree(mth, dupBlocks); BlockNode newStart = newBlocks.get(dupBlocks.indexOf(startBlock)); BlockNode newRetBlock = newBlocks.get(dupBlocks.indexOf(retBlock)); BlockSplitter.connect(jsrBlock, newStart); BlockSplitter.connect(newRetBlock, pathBlock); } } if (first != null) { BlockNode pathBlock = BlockUtils.selectOther(startBlock, first.getSuccessors()); BlockSplitter.removeConnection(first, pathBlock); BlockSplitter.connect(retBlock, pathBlock); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java ================================================ package jadx.core.dex.visitors.debuginfo; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.OptionalInt; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.ILocalVar; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.MethodParametersAttr; import jadx.core.Consts; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.LocalVarsDebugInfoAttr; import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.Named; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.ssa.SSATransform; import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; import jadx.core.dex.visitors.typeinference.TypeUpdateResult; import jadx.core.utils.BlockUtils; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "Debug Info Apply", desc = "Apply debug info to registers (type and names)", runAfter = { SSATransform.class, TypeInferenceVisitor.class } ) public class DebugInfoApplyVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(DebugInfoApplyVisitor.class); @Override public void visit(MethodNode mth) throws JadxException { try { if (mth.contains(AType.LOCAL_VARS_DEBUG_INFO)) { applyDebugInfo(mth); mth.remove(AType.LOCAL_VARS_DEBUG_INFO); } processMethodParametersAttribute(mth); } catch (Exception e) { mth.addWarnComment("Failed to apply debug info", e); } } private static void applyDebugInfo(MethodNode mth) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.info("Apply debug info for method: {}", mth); } mth.getSVars().forEach(ssaVar -> searchAndApplyVarDebugInfo(mth, ssaVar)); fixLinesForReturn(mth); fixNamesForPhiInsns(mth); } private static void searchAndApplyVarDebugInfo(MethodNode mth, SSAVar ssaVar) { if (applyDebugInfo(mth, ssaVar, ssaVar.getAssign())) { return; } for (RegisterArg useArg : ssaVar.getUseList()) { if (applyDebugInfo(mth, ssaVar, useArg)) { return; } } searchDebugInfoByOffset(mth, ssaVar); } private static void searchDebugInfoByOffset(MethodNode mth, SSAVar ssaVar) { LocalVarsDebugInfoAttr debugInfoAttr = mth.get(AType.LOCAL_VARS_DEBUG_INFO); if (debugInfoAttr == null) { return; } OptionalInt max = ssaVar.getUseList().stream().mapToInt(DebugInfoApplyVisitor::getInsnOffsetByArg).max(); if (max.isEmpty()) { return; } int startOffset = getInsnOffsetByArg(ssaVar.getAssign()); int endOffset = max.getAsInt(); int regNum = ssaVar.getRegNum(); for (ILocalVar localVar : debugInfoAttr.getLocalVars()) { if (localVar.getRegNum() == regNum) { int startAddr = localVar.getStartOffset(); int endAddr = localVar.getEndOffset(); if (isInside(startOffset, startAddr, endAddr) || isInside(endOffset, startAddr, endAddr)) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Apply debug info by offset for: {} to {}", ssaVar, localVar); } ArgType type = DebugInfoAttachVisitor.getVarType(mth, localVar); applyDebugInfo(mth, ssaVar, type, localVar.getName()); break; } } } } private static boolean isInside(int var, int start, int end) { return start <= var && var <= end; } private static int getInsnOffsetByArg(InsnArg arg) { if (arg != null) { InsnNode insn = arg.getParentInsn(); if (insn != null) { return insn.getOffset(); } } return -1; } public static boolean applyDebugInfo(MethodNode mth, SSAVar ssaVar, RegisterArg arg) { RegDebugInfoAttr debugInfoAttr = arg.get(AType.REG_DEBUG_INFO); if (debugInfoAttr == null) { return false; } return applyDebugInfo(mth, ssaVar, debugInfoAttr.getRegType(), debugInfoAttr.getName()); } public static boolean applyDebugInfo(MethodNode mth, SSAVar ssaVar, ArgType type, String varName) { TypeUpdateResult result = mth.root().getTypeUpdate().applyDebugInfo(mth, ssaVar, type); if (result == TypeUpdateResult.REJECT) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Reject debug info of type: {} and name: '{}' for {}, mth: {}", type, varName, ssaVar, mth); } return false; } if (NameMapper.isValidAndPrintable(varName)) { ssaVar.setName(varName); } return true; } /** * Fix debug info for splitter 'return' instructions */ private static void fixLinesForReturn(MethodNode mth) { if (mth.isVoidReturn()) { return; } InsnNode origReturn = null; List newReturns = new ArrayList<>(mth.getPreExitBlocks().size()); for (BlockNode exit : mth.getPreExitBlocks()) { InsnNode ret = BlockUtils.getLastInsn(exit); if (ret != null) { if (ret.contains(AFlag.ORIG_RETURN)) { origReturn = ret; } else { newReturns.add(ret); } } } if (origReturn != null) { for (InsnNode ret : newReturns) { InsnArg oldArg = origReturn.getArg(0); InsnArg newArg = ret.getArg(0); if (oldArg.isRegister() && newArg.isRegister()) { RegisterArg oldArgReg = (RegisterArg) oldArg; RegisterArg newArgReg = (RegisterArg) newArg; applyDebugInfo(mth, newArgReg.getSVar(), oldArgReg.getType(), oldArgReg.getName()); } ret.setSourceLine(origReturn.getSourceLine()); } } } private static void fixNamesForPhiInsns(MethodNode mth) { mth.getSVars().forEach(ssaVar -> { for (PhiInsn phiInsn : ssaVar.getUsedInPhi()) { Set names = new HashSet<>(1 + phiInsn.getArgsCount()); addArgName(phiInsn.getResult(), names); phiInsn.getArguments().forEach(arg -> addArgName(arg, names)); if (names.size() == 1) { setNameForInsn(phiInsn, names.iterator().next()); } else if (names.size() > 1) { mth.addDebugComment("Different variable names in phi insn: " + names + ", use first"); setNameForInsn(phiInsn, names.iterator().next()); } } }); } private static void addArgName(InsnArg arg, Set names) { if (arg instanceof Named) { String name = ((Named) arg).getName(); if (name != null) { names.add(name); } } } private static void setNameForInsn(PhiInsn phiInsn, String name) { phiInsn.getResult().setName(name); phiInsn.getArguments().forEach(arg -> { if (arg instanceof Named) { ((Named) arg).setName(name); } }); } private void processMethodParametersAttribute(MethodNode mth) { MethodParametersAttr parametersAttr = mth.get(JadxAttrType.METHOD_PARAMETERS); if (parametersAttr == null) { return; } try { List params = parametersAttr.getList(); if (params.size() != mth.getMethodInfo().getArgsCount()) { return; } int i = 0; for (RegisterArg mthArg : mth.getArgRegs()) { MethodParametersAttr.Info paramInfo = params.get(i++); String name = paramInfo.getName(); if (NameMapper.isValidAndPrintable(name)) { CodeVar codeVar = mthArg.getSVar().getCodeVar(); codeVar.setName(name); if (AccessFlags.hasFlag(paramInfo.getAccFlags(), AccessFlags.FINAL)) { codeVar.setFinal(true); } } } } catch (Exception e) { mth.addWarnComment("Failed to process method parameters attribute: " + parametersAttr.getList(), e); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoAttachVisitor.java ================================================ package jadx.core.dex.visitors.debuginfo; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import jadx.api.plugins.input.data.IDebugInfo; import jadx.api.plugins.input.data.ILocalVar; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.nodes.LocalVarsDebugInfoAttr; import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.parser.SignatureParser; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.blocks.BlockSplitter; import jadx.core.dex.visitors.ssa.SSATransform; import jadx.core.utils.exceptions.InvalidDataException; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "Debug Info Parser", desc = "Attach debug information (variable names and types, instruction lines)", runBefore = { BlockSplitter.class, SSATransform.class } ) public class DebugInfoAttachVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { try { IDebugInfo debugInfo = mth.getDebugInfo(); if (debugInfo != null) { processDebugInfo(mth, debugInfo); } } catch (InvalidDataException e) { mth.addWarnComment(e.getMessage()); } catch (Exception e) { mth.addWarnComment("Failed to parse debug info", e); } } private void processDebugInfo(MethodNode mth, IDebugInfo debugInfo) { InsnNode[] insnArr = mth.getInstructions(); attachSourceLines(mth, debugInfo.getSourceLineMapping(), insnArr); attachDebugInfo(mth, debugInfo.getLocalVars(), insnArr); setMethodSourceLine(mth, insnArr); } private void attachSourceLines(MethodNode mth, Map lineMapping, InsnNode[] insnArr) { if (lineMapping.isEmpty()) { return; } for (Map.Entry entry : lineMapping.entrySet()) { try { InsnNode insn = insnArr[entry.getKey()]; if (insn != null) { insn.setSourceLine(entry.getValue()); } } catch (Exception e) { mth.addWarnComment("Error attach source line", e); return; } } String ignoreReason = verifyDebugLines(lineMapping); if (ignoreReason != null) { mth.addDebugComment("Don't trust debug lines info. " + ignoreReason); } else { mth.add(AFlag.USE_LINES_HINTS); } } private String verifyDebugLines(Map lineMapping) { // search min line in method int minLine = lineMapping.values().stream().mapToInt(v -> v).min().orElse(Integer.MAX_VALUE); if (minLine < 3) { return "Lines numbers was adjusted: min line is " + minLine; } // count repeating lines // 3 here is allowed maximum for line repeat count // can occur in indexed 'for' loops (3 instructions with the same line) var repeatingLines = lineMapping.values().stream() .collect(Collectors.toMap(l -> l, l -> 1, Integer::sum)) .entrySet().stream() .filter(p -> p.getValue() > 3) .collect(Collectors.toList()); if (!repeatingLines.isEmpty()) { return "Repeating lines: " + repeatingLines; } return null; } private void attachDebugInfo(MethodNode mth, List localVars, InsnNode[] insnArr) { if (localVars.isEmpty()) { return; } for (ILocalVar var : localVars) { int regNum = var.getRegNum(); int start = var.getStartOffset(); int end = var.getEndOffset(); ArgType type = getVarType(mth, var); RegDebugInfoAttr debugInfoAttr = new RegDebugInfoAttr(type, var.getName()); if (start <= 0) { // attach to method arguments RegisterArg thisArg = mth.getThisArg(); if (thisArg != null) { attachDebugInfo(thisArg, debugInfoAttr, regNum); } for (RegisterArg arg : mth.getArgRegs()) { attachDebugInfo(arg, debugInfoAttr, regNum); } start = 0; } for (int i = start; i <= end; i++) { InsnNode insn = insnArr[i]; if (insn == null) { continue; } int count = 0; for (InsnArg arg : insn.getArguments()) { count += attachDebugInfo(arg, debugInfoAttr, regNum); } if (count != 0) { // don't apply same info for result if applied to args continue; } attachDebugInfo(insn.getResult(), debugInfoAttr, regNum); } } mth.addAttr(new LocalVarsDebugInfoAttr(localVars)); } private int attachDebugInfo(InsnArg arg, RegDebugInfoAttr debugInfoAttr, int regNum) { if (arg instanceof RegisterArg) { RegisterArg reg = (RegisterArg) arg; if (regNum == reg.getRegNum()) { reg.addAttr(debugInfoAttr); return 1; } } return 0; } public static ArgType getVarType(MethodNode mth, ILocalVar var) { ArgType type = ArgType.parse(var.getType()); String sign = var.getSignature(); if (sign == null) { return type; } try { ArgType gType = new SignatureParser(sign).consumeType(); ArgType expandedType = mth.root().getTypeUtils().expandTypeVariables(mth, gType); if (checkSignature(mth, type, expandedType)) { return expandedType; } } catch (Exception e) { mth.addWarnComment("Can't parse signature for local variable: " + sign, e); } return type; } private static boolean checkSignature(MethodNode mth, ArgType type, ArgType gType) { boolean apply; ArgType el = gType.getArrayRootElement(); if (el.isGeneric()) { if (!type.getArrayRootElement().getObject().equals(el.getObject())) { mth.addWarnComment("Generic types in debug info not equals: " + type + " != " + gType); } apply = true; } else { apply = el.isGenericType(); } return apply; } /** * Set method source line from first instruction */ private void setMethodSourceLine(MethodNode mth, InsnNode[] insnArr) { for (InsnNode insn : insnArr) { if (insn != null) { int line = insn.getSourceLine(); if (line != 0) { mth.setSourceLine(line - 1); return; } } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/CentralityState.java ================================================ package jadx.core.dex.visitors.finaly; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; /** * A centrality state is an object which helps track how instructions can be skipped. * When looking for a finally, one of the things we have to do is make sure that instructions * are not part of the return and are actually part of the "finally" block. * This object helps keep track of registers, instructions etc to see if instructions can be * skipped. */ public final class CentralityState { private final Set allowableOutputArguments = new HashSet<>(); private final SameInstructionsStrategy sameInstructionsStrategy; private boolean allowsCentral = true; private boolean allowsNonStartingNode; public CentralityState(final SameInstructionsStrategy sameInstructionsStrategy, final boolean allowsNonStartingNode) { this.sameInstructionsStrategy = sameInstructionsStrategy; this.allowsNonStartingNode = allowsNonStartingNode; } @Override public final String toString() { final StringBuilder sb = new StringBuilder("CentralityState - "); if (allowsCentral) { sb.append("allows central"); } else { sb.append("disallows central"); } sb.append(" | "); for (RegisterArg registerArg : allowableOutputArguments) { sb.append(registerArg.getName()); sb.append(" "); } return sb.toString(); } public final SameInstructionsStrategy getSameInstructionsStrategy() { return sameInstructionsStrategy; } public final boolean getAllowsCentral() { return allowsCentral; } public final void setAllowsCentral(final boolean allowsCentral) { this.allowsCentral = allowsCentral; } public final boolean getAllowsNonStartingNode() { return allowsNonStartingNode; } public final void setAllowsNonStartingNode(final boolean allowsNonStartingNode) { this.allowsNonStartingNode = allowsNonStartingNode; } public final void addAllowableOutput(final RegisterArg allowableOutput) { allowableOutputArguments.add(allowableOutput); } public final void addAllowableOutputs(final Collection allowableOutputs) { allowableOutputArguments.addAll(allowableOutputs); } /** * Adds all inputs register arguments from an instruction as allowable output arguments. * * @param allowableOutputInsn The instruction to retrieve the list of inputs from. */ public final void addAllowableOutputs(final InsnNode allowableOutputInsn) { final List registerArgs = new LinkedList<>(); for (final InsnArg arg : allowableOutputInsn.getArgList()) { if (!(arg instanceof RegisterArg)) { continue; } registerArgs.add((RegisterArg) arg); } registerArgs.forEach(this::addAllowableOutput); } public final boolean hasAllowableOutput(final InsnNode insn) { if (allowableOutputArguments.isEmpty()) { return false; } final RegisterArg registerArg; if (insn.getResult() != null) { registerArg = insn.getResult(); } else { registerArg = null; } if (registerArg == null) { return false; } for (final RegisterArg allowableOutput : allowableOutputArguments) { if (allowableOutput.equals(registerArg)) { return true; } } return false; } @SuppressWarnings("unused") public final boolean hasAllowableInputs(final InsnNode insn) { if (allowableOutputArguments.isEmpty()) { return false; } final List registerArgs = new ArrayList<>(); for (final InsnArg arg : insn.getArgList()) { if (arg instanceof RegisterArg) { registerArgs.add((RegisterArg) arg); } } if (registerArgs.isEmpty() || allowableOutputArguments.isEmpty()) { return false; } for (final RegisterArg regArg : registerArgs) { boolean foundMatch = false; for (final RegisterArg allowableOutput : allowableOutputArguments) { if (regArg.equals(allowableOutput)) { foundMatch = true; break; } } if (!foundMatch) { return false; } } return true; } public final CentralityState duplicate() { final CentralityState state = new CentralityState(sameInstructionsStrategy, allowsNonStartingNode); state.allowsCentral = allowsCentral; state.allowableOutputArguments.addAll(allowableOutputArguments); return state; } public final Set getAllowableOutputArguments() { return allowableOutputArguments; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/FinallyExtractInfo.java ================================================ package jadx.core.dex.visitors.finaly; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.utils.Utils; public class FinallyExtractInfo { private final MethodNode mth; private final ExceptionHandler finallyHandler; private final List allHandlerBlocks; private final List duplicateSlices = new ArrayList<>(); private final Set checkedBlocks = new HashSet<>(); private final InsnsSlice finallyInsnsSlice = new InsnsSlice(); private final BlockNode startBlock; private InsnsSlice curDupSlice; private List curDupInsns; private int curDupInsnsOffset; public FinallyExtractInfo(MethodNode mth, ExceptionHandler finallyHandler, BlockNode startBlock, List allHandlerBlocks) { this.mth = mth; this.finallyHandler = finallyHandler; this.startBlock = startBlock; this.allHandlerBlocks = allHandlerBlocks; } public MethodNode getMth() { return mth; } public ExceptionHandler getFinallyHandler() { return finallyHandler; } public List getAllHandlerBlocks() { return allHandlerBlocks; } public InsnsSlice getFinallyInsnsSlice() { return finallyInsnsSlice; } public List getDuplicateSlices() { return duplicateSlices; } public Set getCheckedBlocks() { return checkedBlocks; } public BlockNode getStartBlock() { return startBlock; } public InsnsSlice getCurDupSlice() { return curDupSlice; } public void setCurDupSlice(InsnsSlice curDupSlice) { this.curDupSlice = curDupSlice; } public List getCurDupInsns() { return curDupInsns; } public int getCurDupInsnsOffset() { return curDupInsnsOffset; } public void setCurDupInsns(List insns, int offset) { this.curDupInsns = insns; this.curDupInsnsOffset = offset; } @Override public String toString() { return "FinallyExtractInfo{" + "\n finally:\n " + finallyInsnsSlice + "\n dups:\n " + Utils.listToString(duplicateSlices, "\n ") + "\n}"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/InsnsSlice.java ================================================ package jadx.core.dex.visitors.finaly; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; public class InsnsSlice { private final List insnsList = new ArrayList<>(); private final Map insnMap = new IdentityHashMap<>(); private boolean complete; public void addInsn(InsnNode insn, BlockNode block) { insnsList.add(insn); insnMap.put(insn, block); } public void addBlock(BlockNode block) { for (InsnNode insn : block.getInstructions()) { addInsn(insn, block); } } public void addInsns(BlockNode block, int startIndex, int endIndex) { List insns = block.getInstructions(); for (int i = startIndex; i < endIndex; i++) { addInsn(insns.get(i), block); } } @Nullable public BlockNode getBlock(InsnNode insn) { return insnMap.get(insn); } public List getInsnsList() { return insnsList; } public Set getBlocks() { Set set = new LinkedHashSet<>(); for (InsnNode insn : insnsList) { set.add(insnMap.get(insn)); } return set; } public void resetIncomplete() { if (!complete) { insnsList.clear(); insnMap.clear(); } } public boolean isComplete() { return complete; } public void setComplete(boolean complete) { this.complete = complete; } @Override public String toString() { return "{[" + insnsList.stream().map(insn -> insn.getType().toString()).collect(Collectors.joining(", ")) + ']' + (complete ? " complete" : "") + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/MarkFinallyVisitor.java ================================================ package jadx.core.dex.visitors.finaly; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.trycatch.TryCatchBlockAttr; import jadx.core.dex.trycatch.TryEdge; import jadx.core.dex.trycatch.TryEdgeScopeGroupMap; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.ConstInlineVisitor; import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.finaly.traverser.TraverserController; import jadx.core.dex.visitors.finaly.traverser.TraverserException; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.ssa.SSATransform; import jadx.core.utils.BlockUtils; import jadx.core.utils.ListUtils; import jadx.core.utils.Pair; import jadx.core.utils.exceptions.DecodeException; import jadx.core.utils.exceptions.JadxRuntimeException; /** * This visitor is responsible for extracting finally blocks from duplicated instructions located * within the ends of each branch leading to the terminating point of all code paths. * * To do this, the terminating point of each handler / exit from try body is found relative to every * other handler / exit from try body. This, in effect, is used to identify the "scopes" of each * possible path within the try block and thus can be used to find a common series of included * blocks * within the "scope" of each handler and a block to start searching from in reverse to find common * instructions between that and the "nominated finally" handler. These groups are described by the * {@link TryEdgeScopeGroupMap} object. * * After this, the {@link TraverserController} is responsible for traversing the block graphs from * each "scope terminus" along the blocks contained with each handlers "scope", comparing them * against * the "nominated finally" block. If the control flow and instructions of each block match, then * they * are added as duplicate instructions. At the end, the visitor will mark the identified duplicated * instructions and identified finally instructions with the respective {@link AFlag} to be handled * during regioning of the block graph. */ @JadxVisitor( name = "MarkFinallyVisitor", desc = "Search and mark duplicate code generated for finally block", runAfter = SSATransform.class, runBefore = ConstInlineVisitor.class ) public class MarkFinallyVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(MarkFinallyVisitor.class); private static final class TryExtractInfo { private final TryCatchBlockAttr tryBlock; private final TryEdgeScopeGroupMap scopeGroups; private final ExceptionHandler finallyHandler; private final Map> scopeTerminusGroups; private final TryCatchEdgeBlockMap handlerScopes; private final Set allHandlerBlocks; private final Set rethrowBlocks; private Set completeFinallyBlocks = null; private Set completeCandidateBlocks = null; private TryExtractInfo(final TryCatchBlockAttr tryBlock, final TryEdgeScopeGroupMap scopeGroups, final ExceptionHandler finallyHandler, final Map> fallthroughGroups, final TryCatchEdgeBlockMap handlerScopes) { this.tryBlock = tryBlock; this.scopeGroups = scopeGroups; this.finallyHandler = finallyHandler; this.scopeTerminusGroups = fallthroughGroups; this.handlerScopes = handlerScopes; this.allHandlerBlocks = new HashSet<>(); this.rethrowBlocks = new HashSet<>(); for (final List handlerBlocks : handlerScopes.values()) { allHandlerBlocks.addAll(handlerBlocks); } } } @Override public void visit(final MethodNode mth) { if (mth.isNoCode() || mth.isNoExceptionHandlers()) { return; } try { boolean implicitHandlerRemoved = false; final List tryBlocks = mth.getAll(AType.TRY_BLOCKS_LIST); final List processRequiredTryBlocks = new ArrayList<>(); // Search through all exception handlers and: // - Remove implicit handlers // - Mark non-implicit handlers to be searched for a finally block for (final TryCatchBlockAttr tryBlock : tryBlocks) { final TryExtractInfo tryInfo = getTryBlockData(mth, tryBlock); if (tryInfo == null) { continue; } final List cutHandlerBlocks = cutHandlerBlocks(tryInfo, tryInfo.finallyHandler); if (cutHandlerBlocks == null) { continue; } if (attemptRemoveImplicitHandlers(cutHandlerBlocks, tryInfo)) { implicitHandlerRemoved = true; } else { processRequiredTryBlocks.add(tryBlock); } } // If any implicit handlers have been found, remove them if (implicitHandlerRemoved) { resetTryBlocks(mth, tryBlocks); } // Search through all non-implicit handlers and search for a finally block. boolean finallyExtracted = false; for (final TryCatchBlockAttr tryBlock : processRequiredTryBlocks) { // Refresh scope groups now due to implicit handlers final TryExtractInfo tryInfo = getTryBlockData(mth, tryBlock); if (tryInfo == null) { continue; } cutHandlerBlocks(tryInfo, tryInfo.finallyHandler); finallyExtracted |= processTryBlock(mth, tryInfo); } // If any handlers have been merged, remove them if (finallyExtracted) { resetTryBlocks(mth, tryBlocks); } } catch (final Exception e) { LOG.error(e.getMessage()); undoFinallyVisitor(mth); mth.addWarnComment("Undo finally extract visitor", e); } } private static void resetTryBlocks(final MethodNode mth, final List tryBlocks) { mth.clearExceptionHandlers(); // remove merged or empty try blocks from list in method attribute final List clearedTryBlocks = new ArrayList<>(tryBlocks); if (clearedTryBlocks.removeIf(TryCatchBlockAttr::isImplicitOrMerged)) { mth.remove(AType.TRY_BLOCKS_LIST); mth.addAttr(AType.TRY_BLOCKS_LIST, clearedTryBlocks); } } /** * For a given try block, attempts to calculate try block data. This includes the handler blocks for * each try branch, data regarding the scope of each try branch relative to every other branch, and * the blocks logically contained within each try branch. This information is stored via internal * class members and is not returned by the function. * * @param mth The method containing the try block. * @param tryBlock The try block to determine the scope information of. * @return The handler identified as the "all" handler. */ @Nullable private static TryExtractInfo getTryBlockData(final MethodNode mth, final TryCatchBlockAttr tryBlock) { if (tryBlock.isMerged()) { return null; } // Find the all handler ExceptionHandler allHandler = null; for (final ExceptionHandler excHandler : tryBlock.getHandlers()) { if (excHandler.isCatchAll()) { allHandler = excHandler; break; } } if (allHandler == null) { return null; } final TryEdgeScopeGroupMap scopeGroups = tryBlock.getExecutionScopeGroups(mth); final var fallthroughGroups = tryBlock.getHandlerFallthroughGroups(mth, scopeGroups); final var handlerScopes = TryCatchEdgeBlockMap.getAllInScope(mth, tryBlock, scopeGroups, allHandler, fallthroughGroups); return new TryExtractInfo(tryBlock, scopeGroups, allHandler, fallthroughGroups, handlerScopes); } /** * Processes a try block, attempting to extract a finally by locating common instruction patterns * between all * try branches. * * @param mth The method containing the try block. * @param tryInfo The try block information. * @return Whether a finally block has been successfully extracted. */ private static boolean processTryBlock(final MethodNode mth, final TryExtractInfo tryInfo) { if (tryInfo.rethrowBlocks.isEmpty()) { return false; } if (extractFinally(mth, tryInfo)) { for (final BlockNode rethrowBlock : tryInfo.rethrowBlocks) { final InsnNode lastInsn = BlockUtils.getLastInsn(rethrowBlock); if (lastInsn == null) { continue; } lastInsn.add(AFlag.DONT_GENERATE); } return true; } return false; } @Nullable private static List cutHandlerBlocks(final TryExtractInfo tryInfo, final ExceptionHandler handler) { final BlockNode handlerBlock = handler.getHandlerBlock(); final List handlerBlocks = tryInfo.handlerScopes.getBlocksForHandler(handler); if (handlerBlocks == null) { return null; } final InsnNode handlerFinalInsn = BlockUtils.getFirstInsn(handlerBlock); if (handlerFinalInsn != null && handlerFinalInsn.getType() == InsnType.MOVE_EXCEPTION) { handlerBlocks.remove(handlerBlock); // exclude block with 'move-exception' } final BlockNode bottomBlock = BlockUtils.getBottomBlock(handlerBlocks); final List pathExits = BlockUtils.followEmptyUpPathWithinSet(bottomBlock, handlerBlocks); if (pathExits.isEmpty()) { return handlerBlocks; } for (final BlockNode pathExit : pathExits) { // For this to be able to extract a finally, we must ensure that all paths into the handler's logic // end with a THROW equal to the output of the move-exception instruction located at the start of // this handler, if any. final InsnNode bottomBlockLastInsn = BlockUtils.getLastInsn(pathExit); final boolean isValidPathExit = bottomBlockLastInsn != null && handlerFinalInsn != null && bottomBlockLastInsn.getType() == InsnType.THROW && bottomBlockLastInsn.getArgsCount() > 0 && bottomBlockLastInsn.getArg(0).equals(handlerFinalInsn.getResult()); if (!isValidPathExit) { return handlerBlocks; } } final List cutHandlerBlocks = new ArrayList<>(handlerBlocks); for (final BlockNode pathExit : pathExits) { cutHandlerBlocks.remove(pathExit); removeEmptyUpPath(cutHandlerBlocks, pathExit); tryInfo.rethrowBlocks.add(pathExit); } return cutHandlerBlocks; } /** * Attempts to identify and remove an implicit try catch block. * * @param cutHandlerBlocks The cut handler blocks of the all handler. * @return Whether the try block is implicit and has been removed. */ private static boolean attemptRemoveImplicitHandlers(final List cutHandlerBlocks, final TryExtractInfo tryInfo) { if (!(cutHandlerBlocks.isEmpty() || BlockUtils.isAllBlocksEmpty(cutHandlerBlocks))) { return false; } // remove empty catch tryInfo.finallyHandler.getTryBlock().removeHandler(tryInfo.finallyHandler); return true; } /** * Search and mark common code from 'try' block and 'handlers'. */ private static boolean extractFinally(final MethodNode mth, final TryExtractInfo tryInfo) { // Get all handlers from this and inner try blocks. final boolean hasInnerBlocks = !tryInfo.tryBlock.getInnerTryBlocks().isEmpty(); final List handlers = getHandlersForTryCatch(tryInfo.tryBlock); if (handlers.isEmpty()) { return false; } final Map> insns = findCommonInsns(mth, tryInfo); if (insns == null || insns.isEmpty()) { return false; } final Set ignoredFinallyInsns = new HashSet<>(); final Set ignoredCandidateInsns = new HashSet<>(); final Map> insnMap = new HashMap<>(); for (final InsnNode finallyInsn : insns.keySet()) { final List candidateInsns = insns.get(finallyInsn); // For an instruction to have matched, the number of times it has been found must be // equal to the number of edges that the exception handler has which aren't the // finally handler. if (candidateInsns.size() != tryInfo.handlerScopes.size() - 1) { ignoredFinallyInsns.add(finallyInsn); ignoredCandidateInsns.addAll(candidateInsns); // TODO: Add support for partial `catch (Throwable)` finally clauses. // continue; return false; } insnMap.put(finallyInsn, candidateInsns); } for (final InsnNode finallyInsn : insnMap.keySet()) { finallyInsn.add(AFlag.FINALLY_INSNS); final List candidateInsns = insnMap.get(finallyInsn); for (final InsnNode candidateInsn : candidateInsns) { copyCodeVars(finallyInsn, candidateInsn); candidateInsn.add(AFlag.DONT_GENERATE); } } for (final BlockNode finallyBlock : tryInfo.completeFinallyBlocks) { if (ListUtils.anyMatch(finallyBlock.getInstructions(), ignoredFinallyInsns::contains)) { // If this block contains an instruction which was not found in all try edges, // don't mark it as a finally block. continue; } finallyBlock.add(AFlag.FINALLY_INSNS); } for (final BlockNode candidateBlock : tryInfo.completeCandidateBlocks) { if (ListUtils.anyMatch(candidateBlock.getInstructions(), ignoredCandidateInsns::contains)) { // If this block contains an instruction which was found to "duplicate" a finally // instruction which was not found in all try edges, don't mark it as a duplicated // block. continue; } candidateBlock.add(AFlag.DONT_GENERATE); } // If any scope has been merged with the fallthrough case of the try catch, don't merge inner trys. // Otherwise, merge inner trys. final boolean mergedFallthroughScope = ListUtils.anyMatch(tryInfo.scopeGroups.getMergedScopes(), scopePair -> scopePair.getFirst().isNotHandlerExit()); final boolean mergeInnerTryBlocks = hasInnerBlocks && !mergedFallthroughScope; tryInfo.finallyHandler.setFinally(true); if (mergeInnerTryBlocks) { final List innerTryBlocks = tryInfo.tryBlock.getInnerTryBlocks(); for (final TryCatchBlockAttr innerTryBlock : innerTryBlocks) { tryInfo.tryBlock.getHandlers().addAll(innerTryBlock.getHandlers()); tryInfo.tryBlock.getBlocks().addAll(innerTryBlock.getBlocks()); innerTryBlock.setMerged(true); } tryInfo.tryBlock.setBlocks(ListUtils.distinctList(tryInfo.tryBlock.getBlocks())); innerTryBlocks.clear(); } return true; } /** * Gets a list of every exception handler attached to this try block, including handlers of inner * try blocks. * * @param tryBlock The source try block to get the list of exception handlers for * @return The list of exception handlers. */ private static List getHandlersForTryCatch(final TryCatchBlockAttr tryBlock) { final boolean hasInnerBlocks = !tryBlock.getInnerTryBlocks().isEmpty(); final List handlers; if (hasInnerBlocks) { // collect handlers from this and all inner blocks handlers = new ArrayList<>(tryBlock.getHandlers()); for (final TryCatchBlockAttr innerTryBlock : tryBlock.getInnerTryBlocks()) { handlers.addAll(getHandlersForTryCatch(innerTryBlock)); } } else { handlers = tryBlock.getHandlers(); } return handlers; } @Nullable private static Map> findCommonInsns(final MethodNode mth, final TryExtractInfo tryInfo) { final List allHandlerBlocks = tryInfo.handlerScopes.getBlocksForHandler(tryInfo.finallyHandler); final BlockNode finallyScopeTerminus = getTerminusForHandler(tryInfo.finallyHandler, tryInfo); final Map> matchingInsns = new HashMap<>(); for (final TryEdge edge : tryInfo.handlerScopes.keySet()) { if (edge.isHandlerExit() && edge.getExceptionHandler() == tryInfo.finallyHandler) { continue; } final List handlerBlocks = tryInfo.handlerScopes.get(edge); BlockNode scopeTerminus = null; for (final BlockNode edgeTerminusBlock : tryInfo.scopeTerminusGroups.keySet()) { final List edgesWithTerminus = tryInfo.scopeTerminusGroups.get(edgeTerminusBlock); if (edgesWithTerminus.contains(edge)) { scopeTerminus = edgeTerminusBlock; break; } } if (scopeTerminus == null) { throw new JadxRuntimeException("Expected to find fallthrough terminus for handler " + edge); } final TraverserActivePathState comparatorState = new TraverserActivePathState(mth, new SameInstructionsStrategyImpl(), finallyScopeTerminus, scopeTerminus, allHandlerBlocks, handlerBlocks); final TraverserController controller = new TraverserController(); final List pathResults; try { pathResults = controller.process(comparatorState); } catch (final TraverserException e) { LOG.error("Could not search for finally duplicate instructions in path", e); return null; } final Set completeFinally = new HashSet<>(); final Set completeCandidate = new HashSet<>(); for (final TraverserActivePathState pathResult : pathResults) { for (final Pair matchingInsnPair : pathResult.getMatchedInsns()) { final InsnNode finallyInsn = matchingInsnPair.getFirst(); final InsnNode candidateInsn = matchingInsnPair.getSecond(); final List candidateInsnsList; if (!matchingInsns.containsKey(finallyInsn)) { candidateInsnsList = new LinkedList<>(); matchingInsns.put(finallyInsn, candidateInsnsList); } else { candidateInsnsList = matchingInsns.get(finallyInsn); } candidateInsnsList.add(candidateInsn); } completeFinally.addAll(pathResult.getAllFullyMatchedFinallyBlocks()); completeCandidate.addAll(pathResult.getAllFullyMatchedCandidateBlocks()); } if (tryInfo.completeFinallyBlocks == null) { tryInfo.completeFinallyBlocks = completeFinally; } else { tryInfo.completeFinallyBlocks.retainAll(completeFinally); } if (tryInfo.completeCandidateBlocks == null) { tryInfo.completeCandidateBlocks = completeCandidate; } else { tryInfo.completeCandidateBlocks.addAll(completeCandidate); } } return matchingInsns; } private static void removeEmptyUpPath(final List handlerBlocks, final BlockNode startBlock) { for (final BlockNode pred : startBlock.getPredecessors()) { if (pred.isEmpty()) { if (handlerBlocks.remove(pred) && !BlockUtils.isBackEdge(pred, startBlock)) { removeEmptyUpPath(handlerBlocks, pred); } } } } private static void copyCodeVars(final InsnNode fromInsn, final InsnNode toInsn) { copyCodeVars(fromInsn.getResult(), toInsn.getResult()); final int argsCount = fromInsn.getArgsCount(); for (int i = 0; i < argsCount; i++) { copyCodeVars(fromInsn.getArg(i), toInsn.getArg(i)); } } private static void copyCodeVars(final InsnArg fromArg, final InsnArg toArg) { if (fromArg == null || toArg == null || !fromArg.isRegister() || !toArg.isRegister()) { return; } final SSAVar fromSsaVar = ((RegisterArg) fromArg).getSVar(); final SSAVar toSsaVar = ((RegisterArg) toArg).getSVar(); toSsaVar.setCodeVar(fromSsaVar.getCodeVar()); } /** * Reload method without applying this visitor */ private static void undoFinallyVisitor(final MethodNode mth) { try { // TODO: make more common and less hacky mth.unload(); mth.load(); for (IDexTreeVisitor visitor : mth.root().getPasses()) { if (visitor instanceof MarkFinallyVisitor) { break; // All visitors after MarkFinally will be invoked as usual after this because // the original decompilation request will proceed. } DepthTraversal.visit(visitor, mth); } } catch (final DecodeException e) { mth.addError("Undo finally extract failed", e); } } @Nullable private static BlockNode getTerminusForHandler(final ExceptionHandler handler, final TryExtractInfo tryInfo) { for (final BlockNode terminus : tryInfo.scopeTerminusGroups.keySet()) { final List edgesWithTerminus = tryInfo.scopeTerminusGroups.get(terminus); for (final TryEdge edge : edgesWithTerminus) { if (edge.isNotHandlerExit()) { continue; } if (edge.getExceptionHandler().equals(handler)) { return terminus; } } } return null; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/SameInstructionsStrategy.java ================================================ package jadx.core.dex.visitors.finaly; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.InsnNode; public abstract class SameInstructionsStrategy { public abstract boolean sameInsns(InsnNode dupInsn, InsnNode fInsn); public abstract boolean isSameArgs(InsnArg dupArg, InsnArg fArg); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/SameInstructionsStrategyImpl.java ================================================ package jadx.core.dex.visitors.finaly; import java.util.Objects; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; public final class SameInstructionsStrategyImpl extends SameInstructionsStrategy { private static boolean sameDebugInfo(final RegisterArg dupReg, final RegisterArg fReg) { final RegDebugInfoAttr fDbgInfo = fReg.get(AType.REG_DEBUG_INFO); final RegDebugInfoAttr dupDbgInfo = dupReg.get(AType.REG_DEBUG_INFO); if (fDbgInfo == null || dupDbgInfo == null) { return false; } return dupDbgInfo.equals(fDbgInfo); } private static boolean assignInsnDifferent(final RegisterArg dupReg, final RegisterArg fReg) { final InsnNode assignInsn = fReg.getAssignInsn(); final InsnNode dupAssign = dupReg.getAssignInsn(); if (assignInsn == null || dupAssign == null) { return true; } if (!assignInsn.isSame(dupAssign)) { return true; } if (assignInsn.isConstInsn() && dupAssign.isConstInsn()) { // Do this and not deep equals since we already know that the result is the same and that the insn // type is the same return !(Objects.equals(assignInsn.getArguments(), assignInsn.getArguments())); } return false; } @Override public final boolean sameInsns(final InsnNode dupInsn, final InsnNode fInsn) { if (!dupInsn.isSame(fInsn)) { return false; } for (int i = 0; i < dupInsn.getArgsCount(); i++) { final InsnArg dupArg = dupInsn.getArg(i); final InsnArg fArg = fInsn.getArg(i); if (!isSameArgs(dupArg, fArg)) { return false; } } return true; } @Override public final boolean isSameArgs(final InsnArg dupArg, final InsnArg fArg) { if (dupArg == null) { return false; } final boolean isReg = dupArg.isRegister(); if (isReg != fArg.isRegister()) { return false; } if (isReg) { final RegisterArg dupReg = (RegisterArg) dupArg; final RegisterArg fReg = (RegisterArg) fArg; if (!dupReg.sameCodeVar(fReg) && !sameDebugInfo(dupReg, fReg) && assignInsnDifferent(dupReg, fReg)) { return false; } } final boolean remConst = dupArg.isConst(); if (remConst != fArg.isConst()) { return false; } return !(remConst && !dupArg.isSameConst(fArg)); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/TryCatchEdgeBlockMap.java ================================================ package jadx.core.dex.visitors.finaly; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.trycatch.TryCatchBlockAttr; import jadx.core.dex.trycatch.TryEdge; import jadx.core.dex.trycatch.TryEdgeScopeGroupMap; import jadx.core.utils.BlockUtils; import jadx.core.utils.ListUtils; /** * A map containing all edges within a try catch block as the key and all blocks that the * respective edge can traverse to within the "scope" of that edge (relative to the * entire try / catch). */ public final class TryCatchEdgeBlockMap implements Map> { public static boolean anyBlockHasNonImplicitTry(final List blocks) { final List blocksWithTries = ListUtils.filter(blocks, blk -> blk.contains(AFlag.EXC_TOP_SPLITTER)); if (blocksWithTries.isEmpty()) { return false; } for (final BlockNode topSplitter : blocksWithTries) { TryCatchBlockAttr block = null; for (final BlockNode topSplitterSuccessor : topSplitter.getCleanSuccessors()) { if (topSplitterSuccessor.contains(AType.TRY_BLOCK)) { block = topSplitterSuccessor.get(AType.TRY_BLOCK); } } if (block == null) { continue; } if (!TryCatchBlockAttr.isImplicitOrMerged(block)) { return true; } } return false; } public static TryCatchEdgeBlockMap getAllInScope(final MethodNode mth, final TryCatchBlockAttr tryCatch, final TryEdgeScopeGroupMap scopeGroups, final ExceptionHandler finallyHandler, final Map> scopeTerminusGroups) { final Map edgeBlocks = tryCatch.getEdgeBlockMap(mth); final TryCatchEdgeBlockMap result = new TryCatchEdgeBlockMap(); for (final BlockNode scopeTerminus : scopeTerminusGroups.keySet()) { final List sourceEdges = scopeTerminusGroups.get(scopeTerminus); for (final TryEdge sourceEdge : sourceEdges) { final BlockNode edgeBlock = edgeBlocks.get(sourceEdge); final boolean useClean = !(sourceEdge.isNotHandlerExit() && ListUtils.anyMatch(scopeGroups.getMergedScopes(), pair -> pair.getSecond().isNotHandlerExit())); List allBlocks = BlockUtils.collectAllSuccessorsUntil(mth, edgeBlock, useClean, (block) -> block == scopeTerminus); final boolean anyBlockHasTry = anyBlockHasNonImplicitTry(allBlocks); if (anyBlockHasTry && useClean) { // If there's a try edge in the found blocks, collect all successors, not just clean successors. allBlocks = BlockUtils.collectAllSuccessorsUntil(mth, edgeBlock, false, (block) -> block == scopeTerminus); } if (sourceEdge.isNotHandlerExit()) { // If source edge is a fallthrough case, add the try body. allBlocks = new ArrayList<>(allBlocks); allBlocks.addAll(tryCatch.getBlocks()); } result.put(sourceEdge, allBlocks); } } final List finallyBlocks = result.getBlocksForHandler(finallyHandler); for (final TryEdge edge : result.keySet()) { if (edge.isHandlerExit() && edge.getExceptionHandler() == finallyHandler) { continue; } final List blocks = result.get(edge); blocks.removeAll(finallyBlocks); } return result; } private final Map> underlying; public TryCatchEdgeBlockMap() { underlying = new HashMap<>(); } @Override public final void clear() { underlying.clear(); } @Override public final boolean containsKey(Object key) { return underlying.containsKey(key); } @Override public final boolean containsValue(Object value) { if (!(value instanceof TryEdge)) { return false; } final TryEdge edge = (TryEdge) value; return underlying.containsKey(edge); } @Override public final Set>> entrySet() { return underlying.entrySet(); } @Override public final List get(Object key) { return underlying.get(key); } @Override public final boolean isEmpty() { return underlying.isEmpty(); } @Override public final Set keySet() { return underlying.keySet(); } @Override public final List put(TryEdge key, List value) { return underlying.put(key, value); } @Override public final void putAll(Map> otherMap) { underlying.putAll(otherMap); } @Override public final List remove(Object key) { return underlying.remove(key); } @Override public final int size() { return underlying.size(); } @Override public final Collection> values() { return underlying.values(); } @Nullable public final List getBlocksForHandler(final ExceptionHandler handler) { TryEdge edgeWithHandler = null; for (final TryEdge edge : keySet()) { if (edge.isNotHandlerExit()) { continue; } if (!edge.getExceptionHandler().equals(handler)) { continue; } edgeWithHandler = edge; break; } if (edgeWithHandler == null) { return null; } return get(edgeWithHandler); } public final List getBlocksForAllFallthroughs() { final List blks = new ArrayList<>(); for (final TryEdge edge : keySet()) { if (edge.isHandlerExit()) { continue; } blks.addAll(get(edge)); } return blks; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/GlobalTraverserSourceState.java ================================================ package jadx.core.dex.visitors.finaly.traverser; import java.util.Set; import jadx.core.dex.nodes.BlockNode; /** * A state used by the traverser controller for storing information regarding an entire path to * take during traversal. This should be static amongst all states following the same path. */ public final class GlobalTraverserSourceState { private final Set containedBlocks; public GlobalTraverserSourceState(final Set containedBlocks) { this.containedBlocks = containedBlocks; } public final boolean isBlockContained(final BlockNode block) { return containedBlocks.contains(block); } public final Set getContainedBlocks() { return containedBlocks; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/TraverserController.java ================================================ package jadx.core.dex.visitors.finaly.traverser; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractActivePathTraverserHandler; import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockPathTraverserHandler; import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockTraverserHandler; import jadx.core.dex.visitors.finaly.traverser.state.RecoveredFromCacheTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.TerminalTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; import jadx.core.dex.visitors.finaly.traverser.state.TraverserGlobalCommonState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Responsible for determining if two distinct subgraphs are the same within a graph by comparing * all blocks and their instructions. * This is used for identifying duplicated instructions for extracting finally blocks. * * The terms "finally" and "candidate" are used to represent the two distinct subgraphs explored * by this controller; the "finally" subgraph, which is the subgraph which is what is being used * as a finally block, and the "candidate" subgraph, which is the subgraph which is being * compared to the "finally" subgraph to see if they are the same. There is only ever one * "finally" subgraph, however it is run against multiple different "candidate" subgraphs depending * on the complexity of the try catch block that this is being run for. */ public final class TraverserController { private static List processHandlerImplementations(final TraverserActivePathState state, final AbstractBlockTraverserHandler handler) throws TraverserException { if (handler instanceof AbstractBlockPathTraverserHandler) { ((AbstractBlockPathTraverserHandler) handler).process(); return List.of(state); } else if (handler instanceof AbstractActivePathTraverserHandler) { return ((AbstractActivePathTraverserHandler) handler).process(); } else { throw new JadxRuntimeException( "A sealed class, " + AbstractBlockPathTraverserHandler.class.getSimpleName() + ", has an unknown implementation"); } } private final @Nullable Function stateAbortCondition; public TraverserController() { this(null); } public TraverserController(final @Nullable Function stateAbortCondition) { this.stateAbortCondition = stateAbortCondition; } /** * Processes a traverser path state using from a {@link TraverserActivePathState}. This * function will continue evaluating an active path until either: *

    *
  • The state abort condition is met by both "finally" and "candidate" path, if there is * one.
  • *
  • The path state of either the "finally" or "candidate" path has terminated.
  • *
  • The path has began a comparison of two blocks which have already been compared.
  • *
  • The "finally" and "candidate" states, on two different executions of * {@link TraverserController#advance}, did not change. *
* This function will return a list of all of the different paths taken at the point of * termination of each individual branch. * * @param state * @return */ public final List process(final TraverserActivePathState state) throws TraverserException { TraverserActivePathState nextState = state; final AtomicReference previousFinallyState = new AtomicReference<>(null); final AtomicReference previousCandidateState = new AtomicReference<>(null); while (true) { final List advancedStates = advance(nextState, previousFinallyState, previousCandidateState); if (advancedStates == null || advancedStates.isEmpty()) { break; } if (advancedStates.size() != 1) { final TraverserController nextController = new TraverserController(stateAbortCondition); final List returnStates = new ArrayList<>(); for (final TraverserActivePathState advancedState : advancedStates) { final List childStates = nextController.process(advancedState); returnStates.addAll(childStates); } return returnStates; } nextState = advancedStates.get(0); } return List.of(nextState); } /** * Processes a singular traverser state once. * * @param state * @param previousFinallyState * @param previousCandidateState * @return */ public final List advance(final TraverserActivePathState state, final AtomicReference previousFinallyState, final AtomicReference previousCandidateState) throws TraverserException { final TraverserGlobalCommonState commonState = state.getGlobalCommonState(); final TraverserState finallyState = state.getFinallyState(); final TraverserState candidateState = state.getCandidateState(); if (previousFinallyState.get() == finallyState && previousCandidateState.get() == candidateState) { final TraverserStateFactory finallyStateProducer = TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.UNRESOLVABLE_STATES); final TraverserStateFactory candidateStateProducer = TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.UNRESOLVABLE_STATES); return List.of(TraverserActivePathState.produceFromFactories(state, finallyStateProducer, candidateStateProducer)); } previousFinallyState.set(finallyState); previousCandidateState.set(candidateState); if (finallyState.isTerminal() || candidateState.isTerminal()) { return null; } if (finallyState.getClass().equals(candidateState.getClass()) && finallyState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE && candidateState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE) { final BlockNode finallyBlock; final BlockNode candidateBlock; final TraverserBlockInfo finallyBlockInfo = finallyState.getBlockInsnInfo(); final TraverserBlockInfo candidateBlockInfo = candidateState.getBlockInsnInfo(); if (finallyBlockInfo != null && candidateBlockInfo != null) { finallyBlock = finallyBlockInfo.getBlock(); candidateBlock = candidateBlockInfo.getBlock(); } else { finallyBlock = null; candidateBlock = null; } final boolean isCached; if (finallyBlock != null && candidateBlock != null) { isCached = commonState.hasBlocksBeenCached(finallyBlock, candidateBlock); } else { isCached = false; } if (isCached) { final List dupStates = commonState.getCachedStateFor(finallyBlock, candidateBlock); final List recoveredFromCacheStates = new ArrayList<>(dupStates.size()); for (final TraverserActivePathState dupState : dupStates) { final TraverserState reusedFinallyState = dupState.getFinallyState(); final TraverserState reusedCandidateState = dupState.getCandidateState(); final TraverserStateFactory finallyStateProducer = RecoveredFromCacheTraverserState.getFactory(reusedFinallyState); final TraverserStateFactory candidateStateProducer = RecoveredFromCacheTraverserState.getFactory(reusedCandidateState); final TraverserActivePathState recoveredFromCacheState = TraverserActivePathState.produceFromFactories(state, finallyStateProducer, candidateStateProducer); recoveredFromCacheState.mergeWith(dupStates); recoveredFromCacheStates.add(recoveredFromCacheState); } return recoveredFromCacheStates; } final AbstractBlockTraverserHandler handler = candidateState.getNextHandler(); final List resultingStates = processHandlerImplementations(state, handler); return resultingStates; } final boolean hasReadyToCompare = finallyState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE || candidateState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE; final boolean finallyStateAborted = advanceSingleState(state, finallyState, hasReadyToCompare); final boolean candidateStateAborted = advanceSingleState(state, candidateState, hasReadyToCompare); if (finallyStateAborted && candidateStateAborted) { return null; } return List.of(state); } /** * Advances a singular state once. * * @return Whether this state has been aborted by the state abort function. */ private boolean advanceSingleState(final TraverserActivePathState activePathState, final TraverserState singleState, final boolean hasReadyToCompare) throws TraverserException { final boolean stateAborted = stateAbortCondition != null && stateAbortCondition.apply(singleState); if (stateAbortCondition == null || !stateAborted) { if (singleState.getCompareState() == TraverserState.ComparisonState.NOT_READY || (singleState.getCompareState() == TraverserState.ComparisonState.AWAITING_OPTIONAL_PREDECESSOR_MERGE && hasReadyToCompare)) { final AbstractBlockTraverserHandler handler = singleState.getNextHandler(); final List results = processHandlerImplementations(activePathState, handler); if (results.size() != 1 || results.get(0) != activePathState) { throw new JadxRuntimeException("A traverser handler which was not expected to change path states actually did"); } } } return stateAborted; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/TraverserException.java ================================================ package jadx.core.dex.visitors.finaly.traverser; public class TraverserException extends Exception { public TraverserException(final String msg) { super(msg); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/factory/DuplicatedTraverserStateFactory.java ================================================ package jadx.core.dex.visitors.finaly.traverser.factory; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; import jadx.core.utils.exceptions.JadxRuntimeException; public final class DuplicatedTraverserStateFactory extends TraverserStateFactory { private final T baseState; public DuplicatedTraverserStateFactory(final T baseState) { this.baseState = baseState; } @Override public final T generateInternalState(final TraverserActivePathState state) { final Class baseStateClass = (Class) baseState.getClass(); final TraverserState duplicated = baseState.duplicate(state); if (!baseStateClass.isInstance(duplicated)) { throw new JadxRuntimeException( "A state of class " + baseState.getClass() + " has duplicated to produce a class of " + duplicated.getClass()); } return baseStateClass.cast(duplicated); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/factory/TraverserStateFactory.java ================================================ package jadx.core.dex.visitors.finaly.traverser.factory; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; public abstract class TraverserStateFactory { protected abstract T generateInternalState(final TraverserActivePathState state); public final T generateState(final TraverserActivePathState state) { final T generatedState = generateInternalState(state); return generatedState; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractActivePathTraverserHandler.java ================================================ package jadx.core.dex.visitors.finaly.traverser.handlers; import java.util.List; import jadx.core.dex.visitors.finaly.traverser.TraverserException; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; public abstract class AbstractActivePathTraverserHandler extends AbstractBlockTraverserHandler { private final TraverserActivePathState comparatorState; public AbstractActivePathTraverserHandler(final TraverserActivePathState comparatorState) { this.comparatorState = comparatorState; } protected abstract List handle() throws TraverserException; public final List process() throws TraverserException { return handle(); } public final TraverserActivePathState getComparator() { return comparatorState; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractBlockPathTraverserHandler.java ================================================ package jadx.core.dex.visitors.finaly.traverser.handlers; import java.util.concurrent.atomic.AtomicReference; import jadx.core.dex.visitors.finaly.traverser.TraverserException; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; /** * Traverser handlers are responsible for deducing how blocks should be searched within a path * whilst * searching for duplicate 'finally' instructions. */ public abstract class AbstractBlockPathTraverserHandler extends AbstractBlockTraverserHandler { private final AtomicReference stateRef; public AbstractBlockPathTraverserHandler(final TraverserState initialState) { this.stateRef = new AtomicReference<>(initialState); } public AbstractBlockPathTraverserHandler(final AtomicReference initialStateRef) { this.stateRef = initialStateRef; } protected abstract void handle() throws TraverserException; public final void process() throws TraverserException { handle(); } public final TraverserState getState() { return stateRef.get(); } public final AtomicReference getStateReference() { return stateRef; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractBlockTraverserHandler.java ================================================ package jadx.core.dex.visitors.finaly.traverser.handlers; /** * Sealed class */ public abstract class AbstractBlockTraverserHandler { } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/BaseBlockTraverserHandler.java ================================================ package jadx.core.dex.visitors.finaly.traverser.handlers; import java.util.concurrent.atomic.AtomicReference; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; import jadx.core.dex.visitors.finaly.traverser.visitors.ImplicitInsnBlockTraverserVisitor; import jadx.core.dex.visitors.finaly.traverser.visitors.PathEndBlockTraverserVisitor; import jadx.core.utils.exceptions.JadxRuntimeException; public class BaseBlockTraverserHandler extends AbstractBlockPathTraverserHandler { public BaseBlockTraverserHandler(final TraverserState initialState) { super(initialState); } public BaseBlockTraverserHandler(final AtomicReference initialStateRef) { super(initialStateRef); } @Override protected void handle() { final TraverserBlockInfo blockInsnInfo = getState().getBlockInsnInfo(); if (blockInsnInfo == null) { throw new JadxRuntimeException("Expected to find block info within " + getClass().getSimpleName()); } final TraverserActivePathState comparator = getState().getComparatorState(); final AtomicReference stateRef = comparator.getReferenceForState(getState()); if (stateRef == null) { throw new JadxRuntimeException("Orphaned traverser state"); } final BlockNode block = blockInsnInfo.getBlock(); final ImplicitInsnBlockTraverserVisitor implicitVisitor = new ImplicitInsnBlockTraverserVisitor(getState()); final TraverserState stateAfterImplicit = implicitVisitor.visit(block); final PathEndBlockTraverserVisitor pathEndVisitor = new PathEndBlockTraverserVisitor(stateAfterImplicit); final TraverserState nextState = pathEndVisitor.visit(block); stateRef.set(nextState); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/InstructionActivePathTraverserHandler.java ================================================ package jadx.core.dex.visitors.finaly.traverser.handlers; import java.util.List; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.visitors.finaly.traverser.TraverserException; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; import jadx.core.dex.visitors.finaly.traverser.state.TraverserGlobalCommonState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; import jadx.core.dex.visitors.finaly.traverser.visitors.comparator.InstructionBlockComparatorTraverserVisitor; public final class InstructionActivePathTraverserHandler extends AbstractActivePathTraverserHandler { public static final class UnresolvableBlockException extends TraverserException { public UnresolvableBlockException(final BlockNode block, final String reason) { super("A block, " + block.toString() + ", could not have instructions compared.\n\t" + reason); } } public InstructionActivePathTraverserHandler(final TraverserActivePathState state) { super(state); } @Override protected List handle() throws TraverserException { final TraverserActivePathState comparator = getComparator(); final TraverserGlobalCommonState commonState = comparator.getGlobalCommonState(); final TraverserState finallyState = comparator.getFinallyState(); final TraverserState candidateState = comparator.getCandidateState(); final TraverserBlockInfo finallyBlockInfo = finallyState.getBlockInsnInfo(); final TraverserBlockInfo candidateBlockInfo = candidateState.getBlockInsnInfo(); final BlockNode finallyBlock = finallyBlockInfo.getBlock(); final BlockNode candidateBlock = candidateBlockInfo.getBlock(); final InstructionBlockComparatorTraverserVisitor visitor = new InstructionBlockComparatorTraverserVisitor(); final TraverserActivePathState newState = visitor.visit(comparator); if (finallyBlock != null && candidateBlock != null) { commonState.addCachedStateFor(finallyBlock, candidateBlock, List.of(newState)); } return List.of(newState); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/MergePathActivePathTraverserHandler.java ================================================ package jadx.core.dex.visitors.finaly.traverser.handlers; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Stack; import java.util.function.Function; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.GlobalTraverserSourceState; import jadx.core.dex.visitors.finaly.traverser.TraverserController; import jadx.core.dex.visitors.finaly.traverser.TraverserException; import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; import jadx.core.dex.visitors.finaly.traverser.state.IdentifiedScopeWithTerminatorTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.NewBlockTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.RecoveredFromCacheTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.TerminalTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; import jadx.core.dex.visitors.finaly.traverser.state.TraverserGlobalCommonState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; import jadx.core.utils.exceptions.JadxRuntimeException; public final class MergePathActivePathTraverserHandler extends AbstractActivePathTraverserHandler { private static TraverserActivePathState createNonMatchingTerminator(final TraverserActivePathState state) { final TraverserStateFactory finallyStateFactory = TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.NON_MATCHING_PATHS); final TraverserStateFactory candidateStateFactory = TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.NON_MATCHING_PATHS); return TraverserActivePathState.produceFromFactories(state, finallyStateFactory, candidateStateFactory); } private static boolean isStateOnTerminus(final TraverserState state, final BlockNode terminus) { final TraverserBlockInfo blockInfo = state.getBlockInsnInfo(); if (blockInfo == null) { return false; } return blockInfo.getBlock() == terminus; } private static Function getStateAbortOnTerminusFunction( final IdentifiedScopeWithTerminatorTraverserState finallyState, final IdentifiedScopeWithTerminatorTraverserState candidateState) { final BlockNode finallyTerminus = finallyState.getTerminus(); final BlockNode candidateTerminus = candidateState.getTerminus(); final GlobalTraverserSourceState finallyGlobalState = finallyState.getGlobalState(); final GlobalTraverserSourceState candidateGlobalState = candidateState.getGlobalState(); return (final TraverserState state) -> { if (state.getGlobalState() == finallyGlobalState) { return isStateOnTerminus(state, finallyTerminus); } else if (state.getGlobalState() == candidateGlobalState) { return isStateOnTerminus(state, candidateTerminus); } else { throw new JadxRuntimeException("Unknown global traverser state. Has a global state been duplicated?"); } }; } private static PostMergeStatus getScopeSplitPostMergeStatus(final List pathsTaken) { // If the scope split is the same, all branches must not end in a terminator. final PostMergeStatus status = new PostMergeStatus(); for (final TraverserActivePathState path : pathsTaken) { final TraverserState finallyState; final TraverserState candidateState; if (path.getFinallyState().isTerminal() || path.getCandidateState().isTerminal()) { final TraverserState rawFinallyState = path.getFinallyState(); final TraverserState rawCandidateState = path.getCandidateState(); final boolean finallyIsCached = rawFinallyState instanceof RecoveredFromCacheTraverserState; final boolean candidateIsCached = rawCandidateState instanceof RecoveredFromCacheTraverserState; if (!(finallyIsCached && candidateIsCached)) { status.perfectMatch = false; continue; } final RecoveredFromCacheTraverserState finallyCachedState = (RecoveredFromCacheTraverserState) rawFinallyState; final RecoveredFromCacheTraverserState candidateCachedState = (RecoveredFromCacheTraverserState) rawCandidateState; if (finallyCachedState.canContinue() || candidateCachedState.canContinue()) { status.perfectMatch = false; continue; } finallyState = finallyCachedState.getUnderlying(); candidateState = candidateCachedState.getUnderlying(); } else { finallyState = path.getFinallyState(); candidateState = path.getCandidateState(); } final CentralityState finallyCentralityState = finallyState.getCentralityState(); final CentralityState candidateCentralityState = candidateState.getCentralityState(); status.finallyAllowsCentral &= finallyCentralityState.getAllowsCentral(); status.candidateAllowsCentral &= candidateCentralityState.getAllowsCentral(); status.finallyAllowableOutputs.addAll(finallyCentralityState.getAllowableOutputArguments()); status.candidateAllowableOutputs.addAll(candidateCentralityState.getAllowableOutputArguments()); } return status; } private static final class PostMergeStatus { public final Set finallyAllowableOutputs = new HashSet<>(); public final Set candidateAllowableOutputs = new HashSet<>(); public boolean finallyAllowsCentral; public boolean candidateAllowsCentral; public boolean perfectMatch = true; } public MergePathActivePathTraverserHandler(final TraverserActivePathState comparatorState) { super(comparatorState); } @Override protected final List handle() { final TraverserActivePathState comparator = getComparator().duplicate(); final TraverserGlobalCommonState commonState = comparator.getGlobalCommonState(); final IdentifiedScopeWithTerminatorTraverserState finallyState = (IdentifiedScopeWithTerminatorTraverserState) comparator.getFinallyState(); final IdentifiedScopeWithTerminatorTraverserState candidateState = (IdentifiedScopeWithTerminatorTraverserState) comparator.getCandidateState(); final BlockNode finallyTerminus = finallyState.getTerminus(); final BlockNode candidateTerminus = candidateState.getTerminus(); final Function abortFunction = getStateAbortOnTerminusFunction(finallyState, candidateState); final List allPermutationsPaths = getAllPermutationsOfCollection(candidateState.getRoots()); List paths = null; PostMergeStatus postMerge = null; for (final BlockNode[] candidateRootsPermutation : allPermutationsPaths) { final List traversalPaths = new ArrayList<>(); for (int i = 0; i < finallyState.getRoots().size(); i++) { final var finallyRoot = finallyState.getRoots().get(i); final var candidateRoot = candidateRootsPermutation[i]; final var finallyCentrality = finallyState.getCentralityState().duplicate(); final var candidateCentrality = candidateState.getCentralityState().duplicate(); final var finallyBlockInfo = new TraverserBlockInfo(finallyRoot); final var candidateBlockInfo = new TraverserBlockInfo(candidateRoot); final var finallyStateFactory = NewBlockTraverserState.getFactory(finallyCentrality, finallyBlockInfo); final var candidateStateFactory = NewBlockTraverserState.getFactory(candidateCentrality, candidateBlockInfo); final var newState = TraverserActivePathState.produceFromFactories(comparator, finallyStateFactory, candidateStateFactory); traversalPaths.add(newState); } final List currentPaths = new ArrayList<>(); boolean errorOccurred = false; for (final TraverserActivePathState pathState : traversalPaths) { final TraverserController branchController = new TraverserController(abortFunction); final List out; try { out = branchController.process(pathState); } catch (final TraverserException e) { errorOccurred = true; break; } currentPaths.addAll(out); } if (errorOccurred) { // If an error occurred, this path was not successful. continue; } // If the finally terminus and candidate terminus have been cached at this stage, it means that a // path that we searched evaluated the two termini. At this point, we can ignore a non-perfect // match if the path could continue from the point of the termini. final boolean hasTerminusBeenEvaluatedInPaths = commonState.hasBlocksBeenCached(finallyTerminus, candidateTerminus); final PostMergeStatus currentPostMerge = getScopeSplitPostMergeStatus(currentPaths); if (!currentPostMerge.perfectMatch && !hasTerminusBeenEvaluatedInPaths) { // No match continue; } paths = currentPaths; postMerge = currentPostMerge; break; } if (paths == null || postMerge == null) { final TraverserActivePathState nonMatchingState = createNonMatchingTerminator(comparator); return List.of(nonMatchingState); } final CentralityState newFinallyCentralityState = finallyState.getCentralityState().duplicate(); newFinallyCentralityState.setAllowsCentral(postMerge.finallyAllowsCentral); newFinallyCentralityState.addAllowableOutputs(postMerge.finallyAllowableOutputs); final CentralityState newCandidateCentralityState = candidateState.getCentralityState().duplicate(); newCandidateCentralityState.setAllowsCentral(postMerge.candidateAllowsCentral); newCandidateCentralityState.addAllowableOutputs(postMerge.candidateAllowableOutputs); final TraverserBlockInfo finallyTerminusBlockInfo = new TraverserBlockInfo(finallyState.getTerminus()); final TraverserBlockInfo candidateTerminusBlockInfo = new TraverserBlockInfo(candidateState.getTerminus()); final TraverserStateFactory finallyStateFactory = NewBlockTraverserState.getFactory(newFinallyCentralityState, finallyTerminusBlockInfo); final TraverserStateFactory candidateStateFactory = NewBlockTraverserState.getFactory(newCandidateCentralityState, candidateTerminusBlockInfo); final TraverserActivePathState nextState = TraverserActivePathState.produceFromFactories(comparator, finallyStateFactory, candidateStateFactory); nextState.mergeWith(paths); return List.of(nextState); } public static List getAllPermutationsOfCollection(final Collection elements) { final Stack permutationStack = new Stack<>(); final List permutations = new ArrayList<>(); permutations(permutations, elements, permutationStack, elements.size()); return permutations; } public static void permutations(final List permutations, final Collection elements, final Stack permutationStack, final int size) { if (permutationStack.size() == size) { permutations.add(permutationStack.toArray(BlockNode[]::new)); } BlockNode[] availableItems = elements.toArray(BlockNode[]::new); for (BlockNode i : availableItems) { permutationStack.push(i); elements.remove(i); permutations(permutations, elements, permutationStack, size); elements.add(permutationStack.pop()); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/PredecessorBlockPathTraverserHandler.java ================================================ package jadx.core.dex.visitors.finaly.traverser.handlers; import java.util.concurrent.atomic.AtomicReference; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.visitors.finaly.traverser.state.ISourceBlockState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; import jadx.core.dex.visitors.finaly.traverser.visitors.AbstractBlockTraverserVisitor; import jadx.core.dex.visitors.finaly.traverser.visitors.PredecessorBlockTraverserVisitor; import jadx.core.utils.exceptions.JadxRuntimeException; public final class PredecessorBlockPathTraverserHandler extends AbstractBlockPathTraverserHandler { private final ISourceBlockState sourceBlockState; public PredecessorBlockPathTraverserHandler(final T initialState) { super(initialState); this.sourceBlockState = initialState; } public PredecessorBlockPathTraverserHandler(final AtomicReference initialStateRef) { super(initialStateRef); this.sourceBlockState = initialStateRef.get(); } @Override protected final void handle() { final TraverserState baseState = getState(); final TraverserActivePathState comparator = baseState.getComparatorState(); final AtomicReference stateRef = comparator.getReferenceForState(baseState); if (stateRef == null) { throw new JadxRuntimeException("Orphaned traverser state"); } final BlockNode sourceBlock = sourceBlockState.getSourceBlock(); final AbstractBlockTraverserVisitor visitor = new PredecessorBlockTraverserVisitor(baseState); final TraverserState nextState = visitor.visit(sourceBlock); stateRef.set(nextState); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/PredecessorMergeActivePathTraverserHandler.java ================================================ package jadx.core.dex.visitors.finaly.traverser.handlers; import java.util.ArrayList; import java.util.List; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.GlobalTraverserSourceState; import jadx.core.dex.visitors.finaly.traverser.TraverserException; import jadx.core.dex.visitors.finaly.traverser.factory.DuplicatedTraverserStateFactory; import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; import jadx.core.dex.visitors.finaly.traverser.state.IdentifiedScopeWithTerminatorTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.NewBlockTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.TerminalTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; import jadx.core.dex.visitors.finaly.traverser.state.UnknownAdvanceStrategyTraverserState; import jadx.core.utils.BlockUtils; public final class PredecessorMergeActivePathTraverserHandler extends AbstractActivePathTraverserHandler { private static List orderBlocks(final List blocks) { final List dup = new ArrayList<>(blocks); // Collections.sort(dup, (blk1, blk2) -> Integer.compare(blk1.getCId(), blk2.getCId())); return dup; } public PredecessorMergeActivePathTraverserHandler(TraverserActivePathState initialState) { super(initialState); } @Override protected final List handle() throws TraverserException { // At this point, we expect the handler to contain the block state of the path which is // requesting a predecessor merge. If the other handler also requests a predecessor merge, // we can merge the two. If not, we'll split the active handler to support the multiple // paths. final TraverserActivePathState comparator = getComparator(); final TraverserState finallyState = comparator.getFinallyState(); final TraverserState candidateState = comparator.getCandidateState(); final boolean finallyNeedsDuplicate = finallyState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE; final boolean candidateNeedsDuplicate = candidateState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE; final boolean shouldMerge = finallyNeedsDuplicate && candidateNeedsDuplicate; if (shouldMerge) { return mergeScopes((UnknownAdvanceStrategyTraverserState) finallyState, (UnknownAdvanceStrategyTraverserState) candidateState); } else { final UnknownAdvanceStrategyTraverserState advancingState; final TraverserState otherState; if (finallyNeedsDuplicate) { advancingState = (UnknownAdvanceStrategyTraverserState) finallyState; otherState = candidateState; } else { advancingState = (UnknownAdvanceStrategyTraverserState) candidateState; otherState = finallyState; } return duplicateForPaths(comparator, advancingState, otherState, finallyNeedsDuplicate); } } private List mergeScopes(final UnknownAdvanceStrategyTraverserState finallyState, final UnknownAdvanceStrategyTraverserState candidateState) throws TraverserException { final List finallyBlocks = finallyState.getNextBlocks(); final List candidateBlocks = candidateState.getNextBlocks(); final int finallyBlocksSize = finallyBlocks.size(); final int candidateBlocksSize = candidateBlocks.size(); final List states; if (candidateBlocksSize % finallyBlocksSize == 0 && candidateBlocksSize == finallyBlocksSize) { final List finallyBlocksOrdered = orderBlocks(finallyBlocks); final List candidateBlocksOrdered = orderBlocks(candidateBlocks); final int duplicationCount = candidateBlocksSize / finallyBlocksSize; states = new ArrayList<>(duplicationCount); for (int i = 0; i < duplicationCount; i++) { final List candidateBlocksSubset = new ArrayList<>(finallyBlocksSize); for (int j = 0; j < finallyBlocksSize; j++) { candidateBlocksSubset.add(candidateBlocksOrdered.get(i * finallyBlocksSize + j)); } final TraverserActivePathState comparatorState = getScopeForBlocks(finallyBlocksOrdered, candidateBlocksSubset); states.add(comparatorState); } } else { final TraverserStateFactory finallyStateFactory = TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.UNMERGEABLE_STATE); final TraverserStateFactory candidateStateFactory = TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.UNMERGEABLE_STATE); final TraverserActivePathState newState = TraverserActivePathState.produceFromFactories(getComparator(), finallyStateFactory, candidateStateFactory); states = List.of(newState); } return states; } private List duplicateForPaths(final TraverserActivePathState comparator, final UnknownAdvanceStrategyTraverserState advancingState, final TraverserState otherState, final boolean duplicateIsFromFinally) { final List nextPredecessors = advancingState.getNextBlocks(); final List newPaths = new ArrayList<>(nextPredecessors.size()); for (final BlockNode predecessor : nextPredecessors) { final CentralityState centralityState = advancingState.getCentralityState(); final TraverserBlockInfo duplicatePathBlockInfo = new TraverserBlockInfo(predecessor); final TraverserStateFactory duplicatePathStateFactory = NewBlockTraverserState.getFactory(centralityState, duplicatePathBlockInfo); final TraverserStateFactory otherStateFactory = new DuplicatedTraverserStateFactory<>(otherState); final TraverserActivePathState comparatorDuplicated = comparator.duplicate(); final TraverserActivePathState newPathState; if (duplicateIsFromFinally) { newPathState = TraverserActivePathState.produceFromFactories(comparatorDuplicated, duplicatePathStateFactory, otherStateFactory); } else { newPathState = TraverserActivePathState.produceFromFactories(comparatorDuplicated, otherStateFactory, duplicatePathStateFactory); } newPaths.add(newPathState); } return newPaths; } private TraverserActivePathState getScopeForBlocks(final List finallyBlocks, final List candidateBlocks) { final TraverserActivePathState comparator = getComparator(); final MethodNode mth = getComparator().getGlobalCommonState().getMethodNode(); final TraverserState finallyState = comparator.getFinallyState(); final TraverserState candidateState = comparator.getCandidateState(); final GlobalTraverserSourceState finallyGlobalState = comparator.getGlobalStateFor(finallyState); final CentralityState finallyCentralityState = finallyState.getCentralityState(); final BlockNode finallyTerminator = BlockUtils.getBottomCommonPredecessor(mth, finallyBlocks, finallyGlobalState.getContainedBlocks()); final TraverserStateFactory finallyStateFactory = IdentifiedScopeWithTerminatorTraverserState.getFactory(finallyCentralityState, finallyBlocks, finallyTerminator); final GlobalTraverserSourceState candidateGlobalState = comparator.getGlobalStateFor(candidateState); final CentralityState candidateCentralityState = candidateState.getCentralityState(); final BlockNode candidateTerminator = BlockUtils.getBottomCommonPredecessor(mth, candidateBlocks, candidateGlobalState.getContainedBlocks()); final TraverserStateFactory candidateStateFactory = IdentifiedScopeWithTerminatorTraverserState.getFactory(candidateCentralityState, candidateBlocks, candidateTerminator); return TraverserActivePathState.produceFromFactories(comparator, finallyStateFactory, candidateStateFactory); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/AwaitingInsnCompareTraverserState.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import org.jetbrains.annotations.Nullable; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockTraverserHandler; import jadx.core.dex.visitors.finaly.traverser.handlers.InstructionActivePathTraverserHandler; public final class AwaitingInsnCompareTraverserState extends TraverserState { private final CentralityState centralityState; private final @Nullable TraverserBlockInfo blockInsnInfo; public AwaitingInsnCompareTraverserState(final TraverserActivePathState state, final CentralityState centralityState, final TraverserBlockInfo blockInsnInfo) { super(state); this.centralityState = centralityState; this.blockInsnInfo = blockInsnInfo; } @Override public final @Nullable AbstractBlockTraverserHandler getNextHandler() { return new InstructionActivePathTraverserHandler(getComparatorState()); } @Override public final ComparisonState getCompareState() { return ComparisonState.READY_TO_COMPARE; } @Override public final boolean isTerminal() { return false; } @Override protected final @Nullable CentralityState getUnderlyingCentralityState() { return centralityState; } @Override protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { return blockInsnInfo; } @Override protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { final CentralityState dCentralityState = centralityState.duplicate(); final TraverserBlockInfo dBlockInsnInfo = blockInsnInfo.duplicate(); final TraverserState duplicated = new AwaitingInsnCompareTraverserState(comparatorState, dCentralityState, dBlockInsnInfo); return duplicated; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/ISourceBlockState.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import jadx.core.dex.nodes.BlockNode; public interface ISourceBlockState { public abstract BlockNode getSourceBlock(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/IdentifiedScopeWithTerminatorTraverserState.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockTraverserHandler; import jadx.core.dex.visitors.finaly.traverser.handlers.MergePathActivePathTraverserHandler; public final class IdentifiedScopeWithTerminatorTraverserState extends TraverserState { public static TraverserStateFactory getFactory(final CentralityState centralityState, final List roots, final BlockNode scopeTerminator) { return new IdentifiedScopeWithTerminatorStateFactory(centralityState, roots, scopeTerminator); } private static final class IdentifiedScopeWithTerminatorStateFactory extends TraverserStateFactory { private final CentralityState centralityState; private final List roots; private final BlockNode scopeTerminator; public IdentifiedScopeWithTerminatorStateFactory(final CentralityState centralityState, final List roots, final BlockNode scopeTerminator) { this.centralityState = centralityState; this.roots = roots; this.scopeTerminator = scopeTerminator; } @Override public final IdentifiedScopeWithTerminatorTraverserState generateInternalState(final TraverserActivePathState state) { return new IdentifiedScopeWithTerminatorTraverserState(state, centralityState, roots, scopeTerminator); } } private final CentralityState centralityState; private final List roots; private final BlockNode scopeTerminator; public IdentifiedScopeWithTerminatorTraverserState(final TraverserActivePathState state, final CentralityState centralityState, final List roots, final BlockNode scopeTerminator) { super(state); this.roots = roots; this.scopeTerminator = scopeTerminator; this.centralityState = centralityState; } @Override public final @Nullable AbstractBlockTraverserHandler getNextHandler() { return new MergePathActivePathTraverserHandler(getComparatorState()); } @Override public final ComparisonState getCompareState() { return ComparisonState.READY_TO_COMPARE; } @Override public final boolean isTerminal() { return false; } @Override protected final @Nullable CentralityState getUnderlyingCentralityState() { return centralityState; } @Override protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { return null; } @Override protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { return new IdentifiedScopeWithTerminatorTraverserState(comparatorState, centralityState, roots, scopeTerminator); } public final BlockNode getTerminus() { return scopeTerminator; } public final List getRoots() { return roots; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/NewBlockTraverserState.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import org.jetbrains.annotations.Nullable; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockPathTraverserHandler; import jadx.core.dex.visitors.finaly.traverser.handlers.BaseBlockTraverserHandler; public final class NewBlockTraverserState extends TraverserState { public static final TraverserStateFactory getFactory(final CentralityState centralityState, final TraverserBlockInfo blockInsnInfo) { return new NewBlockStateFactory(centralityState, blockInsnInfo); } private static class NewBlockStateFactory extends TraverserStateFactory { private final CentralityState centralityState; private final TraverserBlockInfo blockInsnInfo; public NewBlockStateFactory(final CentralityState centralityState, final TraverserBlockInfo blockInsnInfo) { this.centralityState = centralityState; this.blockInsnInfo = blockInsnInfo; } @Override public NewBlockTraverserState generateInternalState(final TraverserActivePathState state) { return new NewBlockTraverserState(state, centralityState, blockInsnInfo); } } private final CentralityState centralityState; private final @Nullable TraverserBlockInfo blockInsnInfo; public NewBlockTraverserState(final TraverserActivePathState state, final CentralityState centralityState, final TraverserBlockInfo blockInsnInfo) { super(state); this.centralityState = centralityState; this.blockInsnInfo = blockInsnInfo; } @Override public final ComparisonState getCompareState() { return ComparisonState.NOT_READY; } @Override public final boolean isTerminal() { return false; } @Override public @Nullable AbstractBlockPathTraverserHandler getNextHandler() { // We have no data on this block, so we'll give it the base block handler to gain // information about it to gather more state information regarding the block. return new BaseBlockTraverserHandler(this); } @Override protected @Nullable CentralityState getUnderlyingCentralityState() { return centralityState; } @Override protected @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { return blockInsnInfo; } @Override protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { final CentralityState dCentralityState = centralityState.duplicate(); final TraverserBlockInfo dBlockInsnInfo = blockInsnInfo.duplicate(); final TraverserState duplicated = new NewBlockTraverserState(comparatorState, dCentralityState, dBlockInsnInfo); return duplicated; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/NoBlockTraverserState.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockPathTraverserHandler; import jadx.core.dex.visitors.finaly.traverser.handlers.PredecessorBlockPathTraverserHandler; public final class NoBlockTraverserState extends TraverserState implements ISourceBlockState { public static TraverserStateFactory getFactory(final CentralityState centralityState, final BlockNode sourceBlock) { return new NoBlockStateFactory(centralityState, sourceBlock); } private static class NoBlockStateFactory extends TraverserStateFactory { private final CentralityState centralityState; private final BlockNode sourceBlock; public NoBlockStateFactory(final CentralityState centralityState, final BlockNode sourceBlock) { this.centralityState = centralityState; this.sourceBlock = sourceBlock; } @Override public NoBlockTraverserState generateInternalState(final TraverserActivePathState state) { return new NoBlockTraverserState(state, centralityState, sourceBlock); } } private final BlockNode sourceBlock; private final CentralityState centralityState; public NoBlockTraverserState(final TraverserActivePathState state, final CentralityState centralityState, final BlockNode sourceBlock) { super(state); this.sourceBlock = sourceBlock; this.centralityState = centralityState; } @Override public final @Nullable AbstractBlockPathTraverserHandler getNextHandler() { return new PredecessorBlockPathTraverserHandler<>(this); } @Override public final ComparisonState getCompareState() { return ComparisonState.NOT_READY; } @Override public final boolean isTerminal() { return false; } @Override protected final @Nullable CentralityState getUnderlyingCentralityState() { return centralityState; } @Override protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { return null; } @Override public final BlockNode getSourceBlock() { return sourceBlock; } @Override protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { final CentralityState dCentralityState = centralityState.duplicate(); return new NoBlockTraverserState(comparatorState, dCentralityState, sourceBlock); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/RecoveredFromCacheTraverserState.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import org.jetbrains.annotations.Nullable; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockTraverserHandler; public final class RecoveredFromCacheTraverserState extends TraverserState { public static TraverserStateFactory getFactory(final TraverserState underlying) { return new RecoveredFromCacheStateFactory(underlying); } private static final class RecoveredFromCacheStateFactory extends TraverserStateFactory { private final TraverserState underlying; private RecoveredFromCacheStateFactory(final TraverserState underlying) { this.underlying = underlying; } @Override protected final RecoveredFromCacheTraverserState generateInternalState(final TraverserActivePathState state) { return new RecoveredFromCacheTraverserState(underlying); } } private final TraverserState underlying; public RecoveredFromCacheTraverserState(final TraverserState underlying) { super(underlying.getComparatorState()); this.underlying = underlying; } @Override public final @Nullable AbstractBlockTraverserHandler getNextHandler() { return null; } @Override public final ComparisonState getCompareState() { return ComparisonState.NOT_READY; } @Override public final boolean isTerminal() { return true; } @Override protected final @Nullable CentralityState getUnderlyingCentralityState() { return null; } @Override protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { return null; } @Override protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { return new RecoveredFromCacheTraverserState(underlying); } public final TraverserState getUnderlying() { return underlying; } public final boolean canContinue() { return underlying.isTerminal(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TerminalTraverserState.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import org.jetbrains.annotations.Nullable; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockPathTraverserHandler; public final class TerminalTraverserState extends TraverserState { public static TraverserStateFactory getFactory(final TerminationReason terminationReason) { return new TerminalStateFactory(terminationReason); } public static enum TerminationReason { /** * When comparing instructions within a finally and candidate block, non-matching * instructions were found calling for the termination of the Traverser. */ NON_MATCHING_INSTRUCTIONS, NON_MATCHING_PATHS, /** * When a handler was requested to find the predecessors of a block, no predecessors within * the scope existed. */ END_OF_PATH, /** * When a handler was requested to process a block, a cached result for that handler * already existed. */ USING_CACHED_RESULTS, UNMERGEABLE_STATE, UNRESOLVABLE_STATES, } private static class TerminalStateFactory extends TraverserStateFactory { private final TerminationReason terminationReason; public TerminalStateFactory(final TerminationReason terminationReason) { this.terminationReason = terminationReason; } @Override public TerminalTraverserState generateInternalState(TraverserActivePathState state) { return new TerminalTraverserState(state, terminationReason); } } private final TerminationReason terminationReason; public TerminalTraverserState(final TraverserActivePathState state, final TerminationReason terminationReason) { super(state); this.terminationReason = terminationReason; } @Override public final boolean isTerminal() { return true; } @Override @Nullable public final AbstractBlockPathTraverserHandler getNextHandler() { return null; } public final TerminationReason getTerminationReason() { return terminationReason; } @Override public final ComparisonState getCompareState() { return ComparisonState.NOT_READY; } @Override protected final @Nullable CentralityState getUnderlyingCentralityState() { return null; } @Override protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { return null; } @Override protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { final TraverserState duplicated = new TerminalTraverserState(comparatorState, terminationReason); return duplicated; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserActivePathState.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.SameInstructionsStrategy; import jadx.core.dex.visitors.finaly.traverser.GlobalTraverserSourceState; import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; import jadx.core.utils.Pair; import jadx.core.utils.exceptions.JadxRuntimeException; /** * A state used by the traverser controller. For two given branches, the "finally" branch and the * "candidate" branch whilst determining similar instructions and blocks, the active path state * contains information regarding the current matched instructions and blocks, as well as the * current state of both the finally and candidate path being explored. */ public class TraverserActivePathState { /** * Produces a shallow clone of the {@link TraverserActivePathState}. Since this is a shallow clone, * it should only be used for a singular branch. If a branch is used and this needs to be * duplicated, be sure to use the deep clone duplication on the previous * {@link TraverserActivePathState} before invoking this method on it. * * @param previousTraverserState The previous active path state to create a shallow clone of. * @param finallyStateProducer The factory responsible for producing the new finally state to be * held by the resulting active path state. * @param candidateStateProducer The factory responsible for producing the new candidate state to * be held by the resulting active path state. * @return The cloned active path state. */ public static TraverserActivePathState produceFromFactories(final TraverserActivePathState previousTraverserState, final TraverserStateFactory finallyStateProducer, final TraverserStateFactory candidateStateProducer) { final TraverserActivePathState dState = new TraverserActivePathState(previousTraverserState.matchedInsns, previousTraverserState.finallyCompletionMonitor, previousTraverserState.candidateCompletionMonitor, previousTraverserState.commonGlobalState, previousTraverserState.finallyGlobalState, previousTraverserState.candidateGlobalState); final TraverserState dFinallyState = finallyStateProducer.generateState(dState); final TraverserState dCandidateState = candidateStateProducer.generateState(dState); dState.candidateStateRef.set(dCandidateState); dState.finallyStateRef.set(dFinallyState); return dState; } /** * Tracks the comparison state of a given block. * i.e. how many of the instructions have been compared in the Traversal. */ private static final class BlockCompletionMonitor { private final BlockNode block; private final Set matchedIndices; private final int insnCount; private BlockCompletionMonitor(final BlockNode block) { this.block = block; this.insnCount = block.getInstructions().size(); this.matchedIndices = new HashSet<>(insnCount); for (int i = 0; i < insnCount; i++) { matchedIndices.add(i); } } private void registerWithBlockInfo(final TraverserBlockInfo info, final int numberMatched) { if (info.getBlock() != block) { return; } final int botPointer = info.getBottomOffset(); for (int i = 0; i < numberMatched; i++) { final int indexMatched = botPointer + i; matchedIndices.remove(indexMatched); } final int bottomImplicitCount = info.getBottomImplicitCount(); final boolean noPathEndInsns = botPointer - bottomImplicitCount == 0; if (noPathEndInsns) { for (int i = 0; i < bottomImplicitCount; i++) { matchedIndices.remove(i); } } } private BlockCompletionMonitor duplicate() { final BlockCompletionMonitor dup = new BlockCompletionMonitor(block); dup.matchedIndices.retainAll(matchedIndices); return dup; } private void mergeWith(final BlockCompletionMonitor other) { if (other.block != block) { return; } matchedIndices.retainAll(other.matchedIndices); } private boolean isEntireBlock() { return matchedIndices.isEmpty(); } } private static final class BlockCompletionMonitorMap implements Map { private final Map underlying; public BlockCompletionMonitorMap() { underlying = new HashMap<>(); } @Override public final void clear() { underlying.clear(); } @Override public final boolean containsKey(Object key) { return underlying.containsKey(key); } @Override public final boolean containsValue(Object value) { if (!(value instanceof BlockNode)) { return false; } final BlockNode edge = (BlockNode) value; return underlying.containsKey(edge); } @Override public final Set> entrySet() { return underlying.entrySet(); } @Override public final BlockCompletionMonitor get(Object key) { return underlying.get(key); } @Override public final boolean isEmpty() { return underlying.isEmpty(); } @Override public final Set keySet() { return underlying.keySet(); } @Override public final BlockCompletionMonitor put(BlockNode key, BlockCompletionMonitor value) { return underlying.put(key, value); } @Override public final void putAll(Map otherMap) { underlying.putAll(otherMap); } @Override public final BlockCompletionMonitor remove(Object key) { return underlying.remove(key); } @Override public final int size() { return underlying.size(); } @Override public final Collection values() { return underlying.values(); } private void registerWithBlockInfo(final TraverserBlockInfo info, final int numberMatched) { final BlockNode block = info.getBlock(); if (containsKey(block)) { get(block).registerWithBlockInfo(info, numberMatched); } else { final BlockCompletionMonitor monitor = new BlockCompletionMonitor(block); monitor.registerWithBlockInfo(info, numberMatched); put(block, monitor); } } private void mergeEntry(final BlockCompletionMonitor other) { final BlockNode block = other.block; if (containsKey(block)) { get(block).mergeWith(other); } else { final BlockCompletionMonitor monitor = other.duplicate(); put(block, monitor); } } private void mergeMap(final BlockCompletionMonitorMap other) { for (final BlockCompletionMonitor monitor : other.values()) { mergeEntry(monitor); } } private BlockCompletionMonitorMap duplicate() { final BlockCompletionMonitorMap dup = new BlockCompletionMonitorMap(); for (final BlockNode sourceBlock : keySet()) { final BlockCompletionMonitor monitor = get(sourceBlock); dup.put(sourceBlock, monitor.duplicate()); } return dup; } } private final AtomicReference finallyStateRef; private final AtomicReference candidateStateRef; private final GlobalTraverserSourceState finallyGlobalState; private final GlobalTraverserSourceState candidateGlobalState; private final TraverserGlobalCommonState commonGlobalState; private final Set> matchedInsns; private final BlockCompletionMonitorMap finallyCompletionMonitor; private final BlockCompletionMonitorMap candidateCompletionMonitor; /** * Creates a new instance of a traversal active path. This constructor is used to create a new * path to be used by the traverser controller to begin a new traversal. * * @param mth * @param sameInstructionsStrategy * @param finallyBlockTerminus * @param candidateBlockTerminus * @param finallyBlocks * @param candidateBlocks */ public TraverserActivePathState(final MethodNode mth, final SameInstructionsStrategy sameInstructionsStrategy, final BlockNode finallyBlockTerminus, final BlockNode candidateBlockTerminus, final List finallyBlocks, final List candidateBlocks) { final boolean shouldFinallyAllowFirstBlockSkip = !finallyBlockTerminus.getInstructions().isEmpty(); final boolean shouldCandidateAllowFirstBlockSkip = !candidateBlockTerminus.getInstructions().isEmpty(); final CentralityState finallyCentralityState = new CentralityState(sameInstructionsStrategy, shouldFinallyAllowFirstBlockSkip); final CentralityState candidateCentralityState = new CentralityState(sameInstructionsStrategy, shouldCandidateAllowFirstBlockSkip); final TraverserBlockInfo finallyBlockInfo = new TraverserBlockInfo(finallyBlockTerminus); final TraverserBlockInfo candidateBlockInfo = new TraverserBlockInfo(candidateBlockTerminus); final TraverserState finallyState = new NewBlockTraverserState(this, finallyCentralityState, finallyBlockInfo); final TraverserState candidateState = new NewBlockTraverserState(this, candidateCentralityState, candidateBlockInfo); this.finallyGlobalState = new GlobalTraverserSourceState(new HashSet<>(finallyBlocks)); this.candidateGlobalState = new GlobalTraverserSourceState(new HashSet<>(candidateBlocks)); this.commonGlobalState = new TraverserGlobalCommonState(mth); this.finallyStateRef = new AtomicReference<>(finallyState); this.candidateStateRef = new AtomicReference<>(candidateState); this.matchedInsns = new HashSet<>(); this.finallyCompletionMonitor = new BlockCompletionMonitorMap(); this.candidateCompletionMonitor = new BlockCompletionMonitorMap(); } /** * Creates a new instance of a traversal active path. This constructor is used to duplicate a * state between a previous traverser controller and is a liaison for initialising non-null * final fields for the {@link TraverserActivePathState#produceFromFactories} function. * * @param matchedInsns * @param finallyCompletionMonitor * @param candidateCompletionMonitor * @param commonGlobalState * @param finallyGlobalState * @param candidateGlobalState */ private TraverserActivePathState(final Set> matchedInsns, final BlockCompletionMonitorMap finallyCompletionMonitor, final BlockCompletionMonitorMap candidateCompletionMonitor, final TraverserGlobalCommonState commonGlobalState, final GlobalTraverserSourceState finallyGlobalState, final GlobalTraverserSourceState candidateGlobalState) { this.finallyStateRef = new AtomicReference<>(); this.candidateStateRef = new AtomicReference<>(); this.matchedInsns = matchedInsns; this.finallyGlobalState = finallyGlobalState; this.candidateGlobalState = candidateGlobalState; this.commonGlobalState = commonGlobalState; this.finallyCompletionMonitor = finallyCompletionMonitor; this.candidateCompletionMonitor = candidateCompletionMonitor; } public final TraverserActivePathState duplicate() { final Set> dMatchedInsns = new HashSet<>(matchedInsns); final BlockCompletionMonitorMap dFinallyCompletionMonitor = finallyCompletionMonitor.duplicate(); final BlockCompletionMonitorMap dCandidateCompletionMonitor = candidateCompletionMonitor.duplicate(); final TraverserActivePathState dState = new TraverserActivePathState(dMatchedInsns, dFinallyCompletionMonitor, dCandidateCompletionMonitor, commonGlobalState, finallyGlobalState, candidateGlobalState); final TraverserState dFinallyState = getFinallyState().duplicate(dState); final TraverserState dCandidateState = getCandidateState().duplicate(dState); dState.candidateStateRef.set(dCandidateState); dState.finallyStateRef.set(dFinallyState); return dState; } public final TraverserState getFinallyState() { return finallyStateRef.get(); } public final TraverserState getCandidateState() { return candidateStateRef.get(); } public final AtomicReference getFinallyStateRef() { return finallyStateRef; } public final AtomicReference getCandidateStateRef() { return candidateStateRef; } public final Set> getMatchedInsns() { return matchedInsns; } @Nullable public final AtomicReference getReferenceForState(final TraverserState state) { if (finallyStateRef.get() == state) { return finallyStateRef; } else if (candidateStateRef.get() == state) { return candidateStateRef; } else { return null; } } public final GlobalTraverserSourceState getGlobalStateFor(final TraverserState state) { if (finallyStateRef.get() == state) { return finallyGlobalState; } else if (candidateStateRef.get() == state) { return candidateGlobalState; } else { throw new JadxRuntimeException("Orphaned TraverserState node"); } } public final GlobalTraverserSourceState getFinallyGlobalState() { return finallyGlobalState; } public final GlobalTraverserSourceState getCandidateGlobalState() { return candidateGlobalState; } public final TraverserGlobalCommonState getGlobalCommonState() { return commonGlobalState; } public final void mergeWith(final List otherStates) { for (final TraverserActivePathState otherState : otherStates) { matchedInsns.addAll(otherState.getMatchedInsns()); finallyCompletionMonitor.mergeMap(otherState.finallyCompletionMonitor); candidateCompletionMonitor.mergeMap(otherState.candidateCompletionMonitor); } } public final void registerWithBlockInfo(final TraverserBlockInfo info, final int numberMatched) { final BlockNode block = info.getBlock(); final boolean isFinallyBlock = finallyGlobalState.isBlockContained(block); final BlockCompletionMonitorMap monitorMap; if (isFinallyBlock) { monitorMap = finallyCompletionMonitor; } else { monitorMap = candidateCompletionMonitor; } monitorMap.registerWithBlockInfo(info, numberMatched); } public final Set getAllFullyMatchedFinallyBlocks() { return getAllFullyMatchedBlocks(finallyCompletionMonitor); } public final Set getAllFullyMatchedCandidateBlocks() { return getAllFullyMatchedBlocks(candidateCompletionMonitor); } private Set getAllFullyMatchedBlocks(final BlockCompletionMonitorMap monitorMap) { final Set matches = new HashSet<>(); for (final BlockCompletionMonitor monitor : monitorMap.values()) { if (!monitor.isEntireBlock()) { continue; } matches.add(monitor.block); } return matches; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserBlockInfo.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import java.util.List; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; public final class TraverserBlockInfo { private final BlockNode block; // These offsets are the instruction indices NOT an instruction size. private int bottomOffset; private int topOffset; private int bottomImplicitCount; public TraverserBlockInfo(final BlockNode block) { this(block, 0, 0, 0); } public TraverserBlockInfo(final BlockNode block, final int bottomOffset, final int topOffset, final int bottomImplicitCount) { this.bottomOffset = bottomOffset; this.topOffset = topOffset; this.block = block; this.bottomImplicitCount = bottomImplicitCount; } @Override public final String toString() { return toString(""); } public final String toString(final String indent) { final StringBuilder sb = new StringBuilder("BlockInsnInfo - "); sb.append(block.toString()); sb.append(" [↑ "); sb.append(bottomOffset); sb.append("] [↓ "); sb.append(topOffset); sb.append("] "); return sb.toString(); } public final TraverserBlockInfo duplicate() { return new TraverserBlockInfo(block, bottomOffset, topOffset, bottomImplicitCount); } public final BlockNode getBlock() { return block; } public final int getTopOffset() { return topOffset; } public final void setTopOffset(final int topOffset) { this.topOffset = topOffset; } public final int getBottomOffset() { return bottomOffset; } public final void setBottomOffset(final int bottomOffset) { this.bottomOffset = bottomOffset; } public final int getBottomImplicitCount() { return bottomImplicitCount; } public final void setBottomImplicitOffset(final int bottomImplicitCount) { this.bottomImplicitCount = bottomImplicitCount; } public final List getInsnsSlice() { final List insns = block.getInstructions(); final int totalSkippedCount = bottomOffset + topOffset; if (totalSkippedCount > insns.size()) { throw new IndexOutOfBoundsException("Attempted to get instructions slice of block " + block.toString() + " with " + totalSkippedCount + " skipped instructions whilst only having " + insns.size() + " instructions in block."); } final int startIndex = topOffset; final int endIndex = insns.size() - bottomOffset; return insns.subList(startIndex, endIndex); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserGlobalCommonState.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.Pair; public final class TraverserGlobalCommonState { private final MethodNode mth; private final Map, List> searchedStates; public TraverserGlobalCommonState(final MethodNode mth) { this.mth = mth; this.searchedStates = new HashMap<>(); } public final void addCachedStateFor(final BlockNode finallyBlock, final BlockNode candidateBlock, final List state) { final Pair blocks = new Pair<>(finallyBlock, candidateBlock); searchedStates.put(blocks, state); } @Nullable public final List getCachedStateFor(final BlockNode finallyBlock, final BlockNode candidateBlock) { final Pair blocks = new Pair<>(finallyBlock, candidateBlock); return searchedStates.get(blocks); } public final boolean hasBlocksBeenCached(final BlockNode finallyBlock, final BlockNode candidateBlock) { final Pair blocks = new Pair<>(finallyBlock, candidateBlock); return searchedStates.containsKey(blocks); } public final MethodNode getMethodNode() { return mth; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserState.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import org.jetbrains.annotations.Nullable; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.GlobalTraverserSourceState; import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockTraverserHandler; public abstract class TraverserState { public static enum ComparisonState { NOT_READY, AWAITING_OPTIONAL_PREDECESSOR_MERGE, READY_TO_COMPARE } private final TraverserActivePathState comparatorState; public TraverserState(final TraverserActivePathState comparatorState) { this.comparatorState = comparatorState; } @Nullable public abstract AbstractBlockTraverserHandler getNextHandler(); public abstract ComparisonState getCompareState(); public abstract boolean isTerminal(); protected abstract @Nullable CentralityState getUnderlyingCentralityState(); protected abstract @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo(); /** * Performs a deep clone of this Traverser state. * * @return The deep cloned duplication of this Traverser state. */ protected abstract TraverserState duplicateInternalState(final TraverserActivePathState comparatorState); @Override public final String toString() { return toString(0); } public final TraverserState duplicate(final TraverserActivePathState comparatorState) { final TraverserState duplicatedState = duplicateInternalState(comparatorState); return duplicatedState; } public final TraverserActivePathState getComparatorState() { return comparatorState; } public final String toString(final int indentAmount) { final String baseIndent = " ".repeat(indentAmount); final String secondIndent = " ".repeat(indentAmount + 2); final StringBuilder sb = new StringBuilder(baseIndent); sb.append(getClass().getSimpleName()); sb.append(' '); if (isTerminal()) { sb.append("TERMINAL "); } sb.append(" {"); sb.append(System.lineSeparator()); sb.append(secondIndent); sb.append("centrality: "); final CentralityState centralityState = getUnderlyingCentralityState(); if (centralityState == null) { sb.append("none"); } else { sb.append(getCentralityState()); } sb.append(System.lineSeparator()); sb.append(secondIndent); sb.append(getCompareState()); sb.append(System.lineSeparator()); sb.append(secondIndent); final TraverserBlockInfo blockInsnInfo = getBlockInsnInfo(); if (blockInsnInfo != null) { sb.append(blockInsnInfo.toString(secondIndent)); } else { sb.append("NO ACTIVE BLOCK"); } sb.append(System.lineSeparator()); sb.append(baseIndent); sb.append("}"); return sb.toString(); } public final CentralityState getCentralityState() { final CentralityState underlying = getUnderlyingCentralityState(); if (underlying == null) { throw new UnsupportedOperationException("Centrality state is not supported for " + getClass().getName()); } return underlying; } public final @Nullable TraverserBlockInfo getBlockInsnInfo() { return getUnderlyingBlockInsnInfo(); } public final GlobalTraverserSourceState getGlobalState() { return getComparatorState().getGlobalStateFor(this); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/UnknownAdvanceStrategyTraverserState.java ================================================ package jadx.core.dex.visitors.finaly.traverser.state; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractActivePathTraverserHandler; import jadx.core.dex.visitors.finaly.traverser.handlers.PredecessorMergeActivePathTraverserHandler; public final class UnknownAdvanceStrategyTraverserState extends TraverserState { private final CentralityState centralityState; private final List nextBlocks; public UnknownAdvanceStrategyTraverserState(final TraverserActivePathState state, final CentralityState centralityState, final List nextBlocks) { super(state); this.centralityState = centralityState; this.nextBlocks = nextBlocks; } @Override public final @Nullable AbstractActivePathTraverserHandler getNextHandler() { return new PredecessorMergeActivePathTraverserHandler(getComparatorState()); } @Override public final ComparisonState getCompareState() { return ComparisonState.READY_TO_COMPARE; } @Override public final boolean isTerminal() { return false; } @Override protected final @Nullable CentralityState getUnderlyingCentralityState() { return centralityState; } @Override protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { return null; } @Override protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { final CentralityState dCentralityState = centralityState.duplicate(); final List dNextBlocks = new ArrayList<>(nextBlocks); final TraverserState duplicated = new UnknownAdvanceStrategyTraverserState(comparatorState, dCentralityState, dNextBlocks); return duplicated; } public final List getNextBlocks() { return nextBlocks; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/AbstractBlockTraverserVisitor.java ================================================ package jadx.core.dex.visitors.finaly.traverser.visitors; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; public abstract class AbstractBlockTraverserVisitor { private final TraverserState state; public AbstractBlockTraverserVisitor(TraverserState state) { this.state = state; } public abstract TraverserState visit(BlockNode block); public TraverserState getState() { return state; } public TraverserActivePathState getComparator() { return state.getComparatorState(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/ImplicitInsnBlockTraverserVisitor.java ================================================ package jadx.core.dex.visitors.finaly.traverser.visitors; import java.util.List; import java.util.ListIterator; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; public final class ImplicitInsnBlockTraverserVisitor extends AbstractBlockTraverserVisitor { public static boolean isInstructionImplicit(final InsnNode node) { // An instruction is implicit if it can be safely skipped for comparison when traversing in reverse // order. // The presence of a GOTO should be reflected in the structure of a block graph. // i.e. a GOTO should be the last instruction of a block with a single successor. // Another example might be NOP. final InsnType type = node.getType(); switch (type) { case GOTO: return true; default: return false; } } public ImplicitInsnBlockTraverserVisitor(final TraverserState state) { super(state); } @Override public final TraverserState visit(final BlockNode block) { final TraverserBlockInfo insnInfo = getState().getBlockInsnInfo(); final List insns = insnInfo.getInsnsSlice(); final ListIterator insnsIterator = insns.listIterator(insns.size()); /** * The number of instructions that have been identified as "implicit" instructions. */ int bottomDelta = 0; while (insnsIterator.hasPrevious()) { final InsnNode insn = insnsIterator.previous(); if (!isInstructionImplicit(insn)) { break; } bottomDelta++; } insnInfo.setBottomOffset(insnInfo.getBottomOffset() + bottomDelta); insnInfo.setBottomImplicitOffset(insnInfo.getBottomImplicitCount() + bottomDelta); return getState(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/PathEndBlockTraverserVisitor.java ================================================ package jadx.core.dex.visitors.finaly.traverser.visitors; import java.util.List; import java.util.ListIterator; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.state.AwaitingInsnCompareTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.NoBlockTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; public final class PathEndBlockTraverserVisitor extends AbstractBlockTraverserVisitor { public static boolean isInstructionPathEnd(final InsnNode insn) { final InsnType type = insn.getType(); switch (type) { case RETURN: case THROW: return true; default: return false; } } public PathEndBlockTraverserVisitor(final TraverserState state) { super(state); } @Override public final TraverserState visit(final BlockNode block) { final CentralityState centralityState = getState().getCentralityState(); final TraverserBlockInfo insnInfo = getState().getBlockInsnInfo(); if (!centralityState.getAllowsCentral()) { return new AwaitingInsnCompareTraverserState(getComparator(), centralityState, insnInfo); } final List insns = insnInfo.getInsnsSlice(); final ListIterator insnsIterator = insns.listIterator(insns.size()); /** * The number of instructions that have been identified as "path end" instructions. */ int bottomDelta = 0; while (insnsIterator.hasPrevious()) { final InsnNode insn = insnsIterator.previous(); // Check if we should ignore the instruction due to it being a "path end" instruction. if (isInstructionPathEnd(insn)) { // This instruction is a path end instruction - this instruction causes the handler to exit. Here, // we will check the argument // of the path end instruction. If the instruction is a THROW or RETURN, this will indicate the // argument, so long as it exists // and is a register argument, which is operated upon before exiting this scope. Thus, we will mark // this argument as an allowable // path end instruction so long as an instruction returns this argument. // // Example: // CONST_STR r2 = "return this string" <-- A path end instruction since it sets an arg which is used // by path end insn // RETURN r2 <-- A path end instruction if (insn.getArgsCount() != 0) { final InsnArg handlerExitArg = insn.getArg(0); // Returned values from instructions can only be register args so we check that the input to the // path end insn is a register arg if (handlerExitArg instanceof RegisterArg) { centralityState.addAllowableOutput((RegisterArg) handlerExitArg); } } bottomDelta++; // If this instruction is not a path end instruction, check if it sets or invokes a value which is // used by a path end instruction. } else if (centralityState.hasAllowableOutput(insn)) { bottomDelta++; centralityState.addAllowableOutputs(insn); } else { break; } } insnInfo.setBottomOffset(insnInfo.getBottomOffset() + bottomDelta); final BlockNode sourceBlock = insnInfo.getBlock(); final boolean noInstructionsLeft = insnInfo.getBottomOffset() >= sourceBlock.getInstructions().size(); if (noInstructionsLeft) { // Mark the state to request finding predecessors to search for duplicate instructions for return new NoBlockTraverserState(getComparator(), centralityState, sourceBlock); } else { // Mark the current state to await comparing of instructions return new AwaitingInsnCompareTraverserState(getComparator(), centralityState, insnInfo); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/PredecessorBlockTraverserVisitor.java ================================================ package jadx.core.dex.visitors.finaly.traverser.visitors; import java.util.List; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.traverser.GlobalTraverserSourceState; import jadx.core.dex.visitors.finaly.traverser.state.NewBlockTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.TerminalTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; import jadx.core.dex.visitors.finaly.traverser.state.UnknownAdvanceStrategyTraverserState; import jadx.core.utils.ListUtils; public final class PredecessorBlockTraverserVisitor extends AbstractBlockTraverserVisitor { public PredecessorBlockTraverserVisitor(final TraverserState state) { super(state); } @Override public final TraverserState visit(final BlockNode block) { final TraverserState currentState = getState(); final CentralityState centralityState = currentState.getCentralityState(); final GlobalTraverserSourceState globalState = currentState.getGlobalState(); final List predecessors = block.getPredecessors(); final List containedPredecessors = ListUtils.filter(predecessors, globalState::isBlockContained); final int predecessorsCount = containedPredecessors.size(); switch (predecessorsCount) { case 0: return new TerminalTraverserState(getComparator(), TerminalTraverserState.TerminationReason.END_OF_PATH); case 1: final BlockNode nextBlock = containedPredecessors.get(0); final TraverserBlockInfo blockInfo = new TraverserBlockInfo(nextBlock); return new NewBlockTraverserState(getComparator(), centralityState, blockInfo); default: return new UnknownAdvanceStrategyTraverserState(getComparator(), centralityState, containedPredecessors); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/comparator/AbstractTraverserComparatorVisitor.java ================================================ package jadx.core.dex.visitors.finaly.traverser.visitors.comparator; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; public abstract class AbstractTraverserComparatorVisitor { public abstract TraverserActivePathState visit(final TraverserActivePathState state); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/comparator/InstructionBlockComparatorTraverserVisitor.java ================================================ package jadx.core.dex.visitors.finaly.traverser.visitors.comparator; import java.util.ArrayList; import java.util.List; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.visitors.finaly.CentralityState; import jadx.core.dex.visitors.finaly.SameInstructionsStrategy; import jadx.core.dex.visitors.finaly.SameInstructionsStrategyImpl; import jadx.core.dex.visitors.finaly.traverser.factory.DuplicatedTraverserStateFactory; import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; import jadx.core.dex.visitors.finaly.traverser.state.NoBlockTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.TerminalTraverserState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; import jadx.core.utils.Pair; public final class InstructionBlockComparatorTraverserVisitor extends AbstractTraverserComparatorVisitor { private static TraverserActivePathState createStateForPerfectMatch(final TraverserActivePathState previousState, final BlockNode finallyBlock, final BlockNode candidateBlock) { final CentralityState finallyCentralityState = previousState.getFinallyState().getCentralityState().duplicate(); final CentralityState candidateCentralityState = previousState.getCandidateState().getCentralityState().duplicate(); finallyCentralityState.setAllowsCentral(false); candidateCentralityState.setAllowsCentral(false); finallyCentralityState.setAllowsNonStartingNode(false); candidateCentralityState.setAllowsNonStartingNode(false); final TraverserStateFactory finallyStateProducer = NoBlockTraverserState.getFactory(finallyCentralityState, finallyBlock); final TraverserStateFactory candidateStateProducer = NoBlockTraverserState.getFactory(candidateCentralityState, candidateBlock); return TraverserActivePathState.produceFromFactories(previousState, finallyStateProducer, candidateStateProducer); } private static TraverserActivePathState createStateForUnevenMatch(final TraverserActivePathState previousState, final TraverserState finallyState, final TraverserState candidateState, final BlockNode finallyBlock, final BlockNode candidateBlock, final int finallyInsnsSize, final int candidateInsnsSize) { final int maxIterateCount = Math.max(finallyInsnsSize, candidateInsnsSize); final boolean finallyOverruns = finallyInsnsSize > candidateInsnsSize; final int insnsDelta; final TraverserStateFactory newFinallyStateProducer; final TraverserStateFactory newCandidateStateProducer; final TraverserBlockInfo adjustedBlockInfo; if (finallyOverruns) { // More finally instructions than candidate instructions final CentralityState candidateCentralityState = candidateState.getCentralityState().duplicate(); candidateCentralityState.setAllowsCentral(false); candidateCentralityState.setAllowsNonStartingNode(false); final CentralityState finallyCentralityState = finallyState.getCentralityState(); finallyCentralityState.setAllowsCentral(false); finallyCentralityState.setAllowsNonStartingNode(false); insnsDelta = finallyInsnsSize - maxIterateCount; newFinallyStateProducer = new DuplicatedTraverserStateFactory<>(finallyState); adjustedBlockInfo = finallyState.getBlockInsnInfo(); newCandidateStateProducer = NoBlockTraverserState.getFactory(candidateCentralityState, candidateBlock); } else { // More candidate instructions than finally instructions final CentralityState finallyCentralityState = finallyState.getCentralityState().duplicate(); finallyCentralityState.setAllowsCentral(false); finallyCentralityState.setAllowsNonStartingNode(false); final CentralityState candidateCentralityState = candidateState.getCentralityState(); candidateCentralityState.setAllowsCentral(false); candidateCentralityState.setAllowsNonStartingNode(false); insnsDelta = candidateInsnsSize - maxIterateCount; candidateState.getCentralityState().setAllowsCentral(false); newCandidateStateProducer = new DuplicatedTraverserStateFactory<>(candidateState); adjustedBlockInfo = candidateState.getBlockInsnInfo(); newFinallyStateProducer = NoBlockTraverserState.getFactory(finallyCentralityState, finallyBlock); } adjustedBlockInfo.setBottomOffset(adjustedBlockInfo.getBottomOffset() + insnsDelta); return TraverserActivePathState.produceFromFactories(previousState, newFinallyStateProducer, newCandidateStateProducer); } private static TraverserActivePathState createStateForBlockSkip(final TraverserActivePathState previousState, final TraverserState finallyState, final TraverserState candidateState, final BlockNode finallyBlock, final BlockNode candidateBlock) { final CentralityState finallyCentralityState = finallyState.getCentralityState(); final CentralityState candidateCentralityState = candidateState.getCentralityState(); // TODO: Maybe replace this with controller logic so that we can determine if we need to use these // as path ends and then merge above path? // Fix up finally path first. If this continues to fail, check if candidate can be fixed up in a // later iteration. if (finallyCentralityState.getAllowsNonStartingNode()) { finallyCentralityState.setAllowsNonStartingNode(false); final TraverserStateFactory newFinallyStateProducer = NoBlockTraverserState.getFactory(finallyCentralityState, finallyBlock); final TraverserStateFactory newCandidateStateProducer = new DuplicatedTraverserStateFactory<>(candidateState); return TraverserActivePathState.produceFromFactories(previousState, newFinallyStateProducer, newCandidateStateProducer); } else { candidateCentralityState.setAllowsNonStartingNode(false); final TraverserStateFactory newCandidateStateProducer = NoBlockTraverserState.getFactory(candidateCentralityState, candidateBlock); final TraverserStateFactory newFinallyStateProducer = new DuplicatedTraverserStateFactory<>(finallyState); return TraverserActivePathState.produceFromFactories(previousState, newFinallyStateProducer, newCandidateStateProducer); } } private static TraverserActivePathState createStateForTerminatorState(final TraverserActivePathState previousState) { final TraverserStateFactory finallyStateProducer = TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.NON_MATCHING_INSTRUCTIONS); final TraverserStateFactory candidateStateProducer = TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.NON_MATCHING_INSTRUCTIONS); return TraverserActivePathState.produceFromFactories(previousState, finallyStateProducer, candidateStateProducer); } private final SameInstructionsStrategy sameInstructionsStrategy = new SameInstructionsStrategyImpl(); @Override public final TraverserActivePathState visit(final TraverserActivePathState state) { final TraverserState finallyState = state.getFinallyState(); final TraverserState candidateState = state.getCandidateState(); final TraverserBlockInfo finallyBlockInfo = finallyState.getBlockInsnInfo(); final TraverserBlockInfo candidateBlockInfo = candidateState.getBlockInsnInfo(); if (finallyBlockInfo == null || candidateBlockInfo == null) { throw new UnsupportedOperationException( "The instruction comparator handler has received a state which does not support block insn info"); } final BlockNode finallyBlock = finallyBlockInfo.getBlock(); final BlockNode candidateBlock = candidateBlockInfo.getBlock(); final List finallyInsns = finallyBlockInfo.getInsnsSlice(); final List candidateInsns = candidateBlockInfo.getInsnsSlice(); final int finallyInsnsSize = finallyInsns.size(); final int candidateInsnsSize = candidateInsns.size(); final int maxIterateCount = Math.min(finallyInsnsSize, candidateInsnsSize); final List> matchingInsns = new ArrayList<>(maxIterateCount); // Search through each instruction in reverse and see how many match for (int i = 0; i < maxIterateCount; i++) { final InsnNode candidateInsn = candidateInsns.get(candidateInsnsSize - i - 1); final InsnNode finallyInsn = finallyInsns.get(finallyInsnsSize - i - 1); if (!sameInstructionsStrategy.sameInsns(candidateInsn, finallyInsn)) { break; } final Pair match = new Pair<>(finallyInsn, candidateInsn); matchingInsns.add(match); } final int matchedInsnsCount = matchingInsns.size(); state.registerWithBlockInfo(finallyBlockInfo, matchedInsnsCount); state.registerWithBlockInfo(candidateBlockInfo, matchedInsnsCount); final boolean finallyOverruns = finallyInsnsSize > candidateInsnsSize; final boolean candidateOverruns = finallyInsnsSize < candidateInsnsSize; final boolean sameSizedSlices = !finallyOverruns && !candidateOverruns; final boolean allMatched = matchedInsnsCount == maxIterateCount; final boolean noneMatched = matchedInsnsCount == 0; state.getMatchedInsns().addAll(matchingInsns); final TraverserActivePathState newState; if (allMatched) { if (sameSizedSlices) { // All instructions matched and there are no more instructions to match in either // block. Continue to the next set of blocks. newState = createStateForPerfectMatch(state, finallyBlock, candidateBlock); } else { // All instructions matched, however one block contained more instructions than the // other. Continue to next set of blocks for the handler whose instructions list was // fully searched. newState = createStateForUnevenMatch(state, finallyState, candidateState, finallyBlock, candidateBlock, finallyInsnsSize, candidateInsnsSize); } } else if (noneMatched && eitherStateAllowsBlockSkip(finallyState, candidateState)) { newState = createStateForBlockSkip(state, finallyState, candidateState, finallyBlock, candidateBlock); } else { // If any didn't match, this means that the first instructions of the block don't // match. This therefore means that no future blocks should be marked as duplicate // instructions and thus we should return a terminator state to stop the search. newState = createStateForTerminatorState(state); } return newState; } private boolean eitherStateAllowsBlockSkip(final TraverserState finallyState, final TraverserState candidateState) { final CentralityState finallyCentralityState = finallyState.getCentralityState(); final CentralityState candidateCentralityState = candidateState.getCentralityState(); return finallyCentralityState.getAllowsNonStartingNode() || candidateCentralityState.getAllowsNonStartingNode(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/fixaccessmodifiers/FixAccessModifiers.java ================================================ package jadx.core.dex.visitors.fixaccessmodifiers; import java.util.Set; import java.util.stream.Collectors; import jadx.api.plugins.input.data.AccessFlags; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodInlineAttr; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.NotificationAttrNode; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.ModVisitor; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "FixAccessModifiers", desc = "Change class and method access modifiers if needed", runAfter = ModVisitor.class ) public class FixAccessModifiers extends AbstractVisitor { private VisibilityUtils visibilityUtils; private boolean respectAccessModifiers; @Override public void init(RootNode root) { this.visibilityUtils = new VisibilityUtils(root); this.respectAccessModifiers = root.getArgs().isRespectBytecodeAccModifiers(); } @Override public boolean visit(ClassNode cls) throws JadxException { if (respectAccessModifiers) { return true; } fixClassVisibility(cls); return true; } @Override public void visit(MethodNode mth) { if (respectAccessModifiers || mth.contains(AFlag.DONT_GENERATE)) { return; } fixMethodVisibility(mth); } private void fixClassVisibility(ClassNode cls) { AccessInfo accessFlags = cls.getAccessFlags(); if (cls.isTopClass() && accessFlags.isPublic()) { return; } if (cls.isTopClass() && (accessFlags.isPrivate() || accessFlags.isProtected())) { changeVisibility(cls, AccessFlags.PUBLIC); return; } for (ClassNode useCls : cls.getUseIn()) { visibilityUtils.checkVisibility(cls, useCls, (node, visFlag) -> { changeVisibility((NotificationAttrNode) node, visFlag); }); } for (MethodNode useMth : cls.getUseInMth()) { MethodInlineAttr inlineAttr = useMth.get(AType.METHOD_INLINE); boolean isInline = inlineAttr != null && !inlineAttr.notNeeded(); boolean isCandidateForInline = useMth.contains(AFlag.METHOD_CANDIDATE_FOR_INLINE); if (isInline || isCandidateForInline) { Set usedInClss = useMth.getUseIn().stream() .map(MethodNode::getParentClass) .collect(Collectors.toSet()); for (ClassNode useCls : usedInClss) { visibilityUtils.checkVisibility(cls, useCls, (node, visFlag) -> { changeVisibility((NotificationAttrNode) node, visFlag); }); } } } } private void fixMethodVisibility(MethodNode mth) { AccessInfo accessFlags = mth.getAccessFlags(); MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE); if (overrideAttr != null && !overrideAttr.getOverrideList().isEmpty()) { // visibility can't be weaker IMethodDetails parentMD = overrideAttr.getOverrideList().get(0); AccessInfo parentAccInfo = new AccessInfo(parentMD.getRawAccessFlags(), AccessInfo.AFType.METHOD); if (accessFlags.isVisibilityWeakerThan(parentAccInfo)) { changeVisibility(mth, parentAccInfo.getVisibility().rawValue()); } } for (MethodNode useMth : mth.getUseIn()) { visibilityUtils.checkVisibility(mth, useMth, (node, visFlag) -> { changeVisibility((NotificationAttrNode) node, visFlag); }); } } public static void changeVisibility(NotificationAttrNode node, int newVisFlag) { AccessInfo accessFlags = node.getAccessFlags(); AccessInfo newAccFlags = accessFlags.changeVisibility(newVisFlag); if (newAccFlags != accessFlags) { node.setAccessFlags(newAccFlags); node.addInfoComment("Access modifiers changed from: " + accessFlags.visibilityName()); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/fixaccessmodifiers/VisibilityUtils.java ================================================ package jadx.core.dex.visitors.fixaccessmodifiers; import java.util.function.Consumer; import jadx.api.plugins.input.data.AccessFlags; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ICodeNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxRuntimeException; class VisibilityUtils { private final RootNode root; VisibilityUtils(RootNode rootNode) { this.root = rootNode; } void checkVisibility(ICodeNode targetNode, ICodeNode callerNode, OnBadVisibilityCallback callback) { ClassNode targetCls = targetNode instanceof ClassNode ? (ClassNode) targetNode : targetNode.getDeclaringClass(); ClassNode callerCls = callerNode instanceof ClassNode ? (ClassNode) callerNode : callerNode.getDeclaringClass(); if (targetCls.equals(callerCls) || inSameTopClass(targetCls, callerCls)) { return; } if (inSamePkg(targetCls, callerCls)) { visitDeclaringNodes(targetNode, node -> { if (node.getAccessFlags().isPrivate()) { callback.onBadVisibility(node, 0); // PACKAGE_PRIVATE } }); } else { visitDeclaringNodes(targetNode, node -> { AccessInfo nodeVisFlags = node.getAccessFlags().getVisibility(); if (nodeVisFlags.isPublic()) { return; } if (nodeVisFlags.isPrivate() || nodeVisFlags.isPackagePrivate()) { ClassNode nodeDeclaringCls = node.getDeclaringClass(); int expectedVisFlag = nodeDeclaringCls != null && isSuperType(callerCls, nodeDeclaringCls) ? AccessFlags.PROTECTED : AccessFlags.PUBLIC; callback.onBadVisibility(node, expectedVisFlag); } else if (nodeVisFlags.isProtected()) { ClassNode nodeDeclaringCls = node.getDeclaringClass(); if (nodeDeclaringCls == null || !isSuperType(callerCls, nodeDeclaringCls)) { callback.onBadVisibility(node, AccessFlags.PUBLIC); } } else { throw new JadxRuntimeException(nodeVisFlags + " is not supported"); } }); } } private static void visitDeclaringNodes(ICodeNode targetNode, Consumer action) { ICodeNode currentNode = targetNode; do { action.accept(currentNode); currentNode = currentNode.getDeclaringClass(); } while (currentNode != null); } private static boolean inSamePkg(ClassNode cls1, ClassNode cls2) { return cls1.getPackageNode().equals(cls2.getPackageNode()); } private static boolean inSameTopClass(ClassNode cls1, ClassNode cls2) { return cls1.getTopParentClass().equals(cls2.getTopParentClass()); } private boolean isSuperType(ClassNode cls, ClassNode superCls) { return root.getClsp().getSuperTypes(cls.getRawName()).stream() .anyMatch(x -> x.equals(superCls.getRawName())); } interface OnBadVisibilityCallback { void onBadVisibility(ICodeNode node, int expectedVisFlag); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/gradle/NonFinalResIdsVisitor.java ================================================ package jadx.core.dex.visitors.gradle; import java.util.Map; import jadx.api.plugins.input.data.annotations.AnnotationVisibility; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.IFieldInfoRef; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.FixSwitchOverEnum; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.regions.DepthRegionTraversal; import jadx.core.dex.visitors.regions.IRegionIterativeVisitor; import jadx.core.export.GradleInfoStorage; import jadx.core.utils.android.AndroidResourcesUtils; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "NonFinalResIdsVisitor", desc = "Detect usage of android resource constants in cases where constant expressions are required.", runAfter = FixSwitchOverEnum.class ) public class NonFinalResIdsVisitor extends AbstractVisitor implements IRegionIterativeVisitor { private boolean nonFinalResIdsFlagRequired = false; private GradleInfoStorage gradleInfoStorage; public void init(RootNode root) throws JadxException { gradleInfoStorage = root.getGradleInfoStorage(); } @Override public boolean visit(ClassNode cls) throws JadxException { if (nonFinalResIdsFlagRequired) { return false; } AnnotationsAttr annotationsList = cls.get(JadxAttrType.ANNOTATION_LIST); if (visitAnnotationList(annotationsList)) { return false; } return super.visit(cls); } private static boolean isCustomResourceClass(ClassInfo cls) { ClassInfo parentClass = cls.getParentClass(); return parentClass != null && parentClass.getShortName().equals("R") && !parentClass.getFullName().equals("android.R"); } @Override public void visit(MethodNode mth) throws JadxException { AnnotationsAttr annotationsList = mth.get(JadxAttrType.ANNOTATION_LIST); if (visitAnnotationList(annotationsList)) { nonFinalResIdsFlagRequired = true; return; } if (nonFinalResIdsFlagRequired || !CodeFeaturesAttr.contains(mth, CodeFeaturesAttr.CodeFeature.SWITCH)) { return; } DepthRegionTraversal.traverseIterative(mth, this); } private boolean visitAnnotationList(AnnotationsAttr annotationsList) { if (annotationsList != null) { for (IAnnotation annotation : annotationsList.getAll()) { if (annotation.getVisibility() == AnnotationVisibility.SYSTEM) { continue; } for (Map.Entry entry : annotation.getValues().entrySet()) { Object value = entry.getValue().getValue(); if (value instanceof IFieldInfoRef && isCustomResourceClass(((IFieldInfoRef) value).getFieldInfo().getDeclClass())) { gradleInfoStorage.setNonFinalResIds(true); return true; } } } } return false; } @Override public boolean visitRegion(MethodNode mth, IRegion region) { if (nonFinalResIdsFlagRequired) { return false; } if (region instanceof SwitchRegion) { return detectSwitchOverResIds((SwitchRegion) region); } return false; } private boolean detectSwitchOverResIds(SwitchRegion switchRegion) { for (SwitchRegion.CaseInfo caseInfo : switchRegion.getCases()) { for (Object key : caseInfo.getKeys()) { if (key instanceof FieldNode) { ClassNode topParentClass = ((FieldNode) key).getTopParentClass(); if (AndroidResourcesUtils.isResourceClass(topParentClass) && !"android.R".equals(topParentClass.getFullName())) { this.nonFinalResIdsFlagRequired = true; gradleInfoStorage.setNonFinalResIds(true); return false; } } } } return false; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/kotlin/ProcessKotlinInternals.java ================================================ package jadx.core.dex.visitors.kotlin; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs.UseKotlinMethodsForVarNames; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.InitCodeVariables; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor; import jadx.core.dex.visitors.rename.CodeRenameVisitor; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "ProcessKotlinInternals", desc = "Use variable names from Kotlin intrinsic1 methods", runAfter = { InitCodeVariables.class, DebugInfoApplyVisitor.class }, runBefore = { CodeRenameVisitor.class } ) public class ProcessKotlinInternals extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(ProcessKotlinInternals.class); private static final String KOTLIN_INTERNAL_PKG = "kotlin.jvm.internal."; private static final String KOTLIN_INTRINSICS_CLS_SHORT_NAME = "Intrinsics"; private static final String KOTLIN_INTRINSICS_CLS = KOTLIN_INTERNAL_PKG + KOTLIN_INTRINSICS_CLS_SHORT_NAME; private static final String KOTLIN_VARNAME_SOURCE_MTH1 = "(Ljava/lang/Object;Ljava/lang/String;)V"; private static final String KOTLIN_VARNAME_SOURCE_MTH2 = "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V"; private @Nullable ClassInfo kotlinIntrinsicsCls; private Set kotlinVarNameSourceMethods; private boolean hideInsns; @Override public void init(RootNode root) throws JadxException { ClassNode kotlinCls = searchKotlinIntrinsicsClass(root); if (kotlinCls != null) { kotlinIntrinsicsCls = kotlinCls.getClassInfo(); kotlinVarNameSourceMethods = collectMethods(kotlinCls); LOG.debug("Kotlin Intrinsics class: {}, methods: {}", kotlinCls, kotlinVarNameSourceMethods.size()); } else { kotlinIntrinsicsCls = null; LOG.debug("Kotlin Intrinsics class not found"); } hideInsns = root.getArgs().getUseKotlinMethodsForVarNames() == UseKotlinMethodsForVarNames.APPLY_AND_HIDE; } @Override public boolean visit(ClassNode cls) { if (kotlinIntrinsicsCls == null) { return false; } for (MethodNode mth : cls.getMethods()) { processMth(mth); } return true; } private void processMth(MethodNode mth) { if (mth.isNoCode() || mth.contains(AType.JADX_ERROR)) { return; } for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { if (insn.getType() == InsnType.INVOKE) { try { processInvoke(mth, insn); } catch (Exception e) { mth.addWarnComment("Failed to extract var names", e); } } } } } private void processInvoke(MethodNode mth, InsnNode insn) { int argsCount = insn.getArgsCount(); if (argsCount < 2) { return; } MethodInfo invokeMth = ((InvokeNode) insn).getCallMth(); if (!kotlinVarNameSourceMethods.contains(invokeMth)) { return; } InsnArg firstArg = insn.getArg(0); if (!firstArg.isRegister()) { return; } RegisterArg varArg = (RegisterArg) firstArg; boolean renamed = false; if (argsCount == 2) { String str = getConstString(mth, insn, 1); if (str != null) { renamed = checkAndRename(varArg, str); } } else if (argsCount == 3) { // TODO: use second arg for rename class String str = getConstString(mth, insn, 2); if (str != null) { renamed = checkAndRename(varArg, str); } } if (renamed && hideInsns) { insn.add(AFlag.DONT_GENERATE); } } private boolean checkAndRename(RegisterArg arg, String str) { String name = trimName(str); if (NameMapper.isValidAndPrintable(name)) { arg.getSVar().getCodeVar().setName(name); return true; } return false; } @Nullable private String getConstString(MethodNode mth, InsnNode insn, int arg) { InsnArg strArg = insn.getArg(arg); if (!strArg.isInsnWrap()) { return null; } InsnNode constInsn = ((InsnWrapArg) strArg).getWrapInsn(); InsnType insnType = constInsn.getType(); if (insnType == InsnType.CONST_STR) { return ((ConstStringNode) constInsn).getString(); } if (insnType == InsnType.SGET) { // revert const field inline :( FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) constInsn).getIndex(); FieldNode fieldNode = mth.root().resolveField(fieldInfo); if (fieldNode != null) { String str = (String) fieldNode.get(JadxAttrType.CONSTANT_VALUE).getValue(); InsnArg newArg = InsnArg.wrapArg(new ConstStringNode(str)); insn.replaceArg(strArg, newArg); return str; } } return null; } private String trimName(String str) { if (str.startsWith("$this$")) { return str.substring(6); } if (str.startsWith("$")) { return str.substring(1); } return str; } @Nullable private static ClassNode searchKotlinIntrinsicsClass(RootNode root) { ClassNode kotlinCls = root.resolveClass(KOTLIN_INTRINSICS_CLS); if (kotlinCls != null) { return kotlinCls; } List candidates = new ArrayList<>(); for (ClassNode cls : root.getClasses()) { if (isKotlinIntrinsicsClass(cls)) { candidates.add(cls); } } return Utils.getOne(candidates); } private static boolean isKotlinIntrinsicsClass(ClassNode cls) { ClassInfo clsInfo = cls.getClassInfo(); if (clsInfo.getAliasShortName().equals(KOTLIN_INTRINSICS_CLS_SHORT_NAME) && clsInfo.getAliasFullName().equals(KOTLIN_INTRINSICS_CLS)) { return true; } if (!clsInfo.getFullName().startsWith(KOTLIN_INTERNAL_PKG)) { return false; } if (cls.getMethods().size() < 5) { return false; } int mthCount = 0; for (MethodNode mth : cls.getMethods()) { if (mth.getAccessFlags().isStatic() && mth.getMethodInfo().getShortId().endsWith(KOTLIN_VARNAME_SOURCE_MTH1)) { mthCount++; } } return mthCount > 2; } private Set collectMethods(ClassNode kotlinCls) { Set set = new HashSet<>(); for (MethodNode mth : kotlinCls.getMethods()) { if (!mth.getAccessFlags().isStatic()) { continue; } String shortId = mth.getMethodInfo().getShortId(); if (shortId.endsWith(KOTLIN_VARNAME_SOURCE_MTH1) || shortId.endsWith(KOTLIN_VARNAME_SOURCE_MTH2)) { set.add(mth.getMethodInfo()); } } return set; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/methods/MutableMethodDetails.java ================================================ package jadx.core.dex.visitors.methods; import java.util.Collections; import java.util.List; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.IMethodDetails; public class MutableMethodDetails implements IMethodDetails { private final MethodInfo mthInfo; private ArgType retType; private List argTypes; private List typeParams; private List throwTypes; private boolean varArg; private int accFlags; public MutableMethodDetails(IMethodDetails base) { this.mthInfo = base.getMethodInfo(); this.retType = base.getReturnType(); this.argTypes = Collections.unmodifiableList(base.getArgTypes()); this.typeParams = Collections.unmodifiableList(base.getTypeParameters()); this.throwTypes = Collections.unmodifiableList(base.getThrows()); this.varArg = base.isVarArg(); this.accFlags = base.getRawAccessFlags(); } @Override public MethodInfo getMethodInfo() { return mthInfo; } @Override public ArgType getReturnType() { return retType; } @Override public List getArgTypes() { return argTypes; } @Override public List getTypeParameters() { return typeParams; } @Override public List getThrows() { return throwTypes; } @Override public boolean isVarArg() { return varArg; } public void setRetType(ArgType retType) { this.retType = retType; } public void setArgTypes(List argTypes) { this.argTypes = argTypes; } public void setTypeParams(List typeParams) { this.typeParams = typeParams; } public void setThrowTypes(List throwTypes) { this.throwTypes = throwTypes; } public void setVarArg(boolean varArg) { this.varArg = varArg; } @Override public int getRawAccessFlags() { return accFlags; } public void setRawAccessFlags(int accFlags) { this.accFlags = accFlags; } @Override public String toAttrString() { return IMethodDetails.super.toAttrString() + " (mut)"; } @Override public String toString() { return "Mutable" + toAttrString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/prepare/AddAndroidConstants.java ================================================ package jadx.core.dex.visitors.prepare; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.ConstStorage; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.utils.android.AndroidResourcesMap; import jadx.core.utils.exceptions.JadxException; // TODO: move this pass to separate "Android plugin" @JadxVisitor( name = "AddAndroidConstants", desc = "Insert Android constants from resource mapping file", runBefore = { CollectConstValues.class } ) public class AddAndroidConstants extends AbstractVisitor { private static final String R_CLS = "android.R"; private static final String R_INNER_CLS = R_CLS + '$'; @Override public void init(RootNode root) throws JadxException { if (!root.getArgs().isReplaceConsts()) { return; } if (root.resolveClass(R_CLS) != null) { // Android R class already loaded return; } ConstStorage constStorage = root.getConstValues(); AndroidResourcesMap.getMap().forEach((resId, path) -> { int sep = path.indexOf('/'); String clsName = R_INNER_CLS + path.substring(0, sep); String resName = path.substring(sep + 1); ClassInfo cls = ClassInfo.fromName(root, clsName); FieldInfo field = FieldInfo.from(root, cls, resName, ArgType.INT); constStorage.addGlobalConstField(field, resId); }); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/prepare/CollectConstValues.java ================================================ package jadx.core.dex.visitors.prepare; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.ConstStorage; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.usage.UsageInfoVisitor; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "CollectConstValues", desc = "Collect and store values from static final fields", runAfter = { UsageInfoVisitor.class // check field usage (do not restore if used somewhere) } ) public class CollectConstValues extends AbstractVisitor { @Override public boolean visit(ClassNode cls) throws JadxException { RootNode root = cls.root(); if (!root.getArgs().isReplaceConsts()) { return true; } if (cls.getFields().isEmpty()) { return true; } ConstStorage constStorage = root.getConstValues(); for (FieldNode fld : cls.getFields()) { try { Object value = getFieldConstValue(fld); if (value != null) { constStorage.addConstField(fld, value, fld.getAccessFlags().isPublic()); } } catch (Exception e) { cls.addWarnComment("Failed to process value of field: " + fld, e); } } return true; } public static @Nullable Object getFieldConstValue(FieldNode fld) { AccessInfo accFlags = fld.getAccessFlags(); if (!accFlags.isStatic() || !accFlags.isFinal()) { return null; } EncodedValue constVal = fld.get(JadxAttrType.CONSTANT_VALUE); if (constVal == null || constVal == EncodedValue.NULL) { return null; } if (!fld.getUseIn().isEmpty()) { // field still used somewhere and not inlined by compiler, so we don't need to restore it return null; } return constVal.getValue(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/AbstractRegionVisitor.java ================================================ package jadx.core.dex.visitors.regions; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; public abstract class AbstractRegionVisitor implements IRegionVisitor { @Override public boolean enterRegion(MethodNode mth, IRegion region) { return true; } @Override public void processBlock(MethodNode mth, IBlock block) { } @Override public void leaveRegion(MethodNode mth, IRegion region) { } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/CheckRegions.java ================================================ package jadx.core.dex.visitors.regions; import java.util.HashSet; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeWriter; import jadx.api.impl.SimpleCodeWriter; import jadx.core.Consts; import jadx.core.codegen.InsnGen; import jadx.core.codegen.MethodGen; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.exceptions.CodegenException; import jadx.core.utils.exceptions.JadxException; public class CheckRegions extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(CheckRegions.class); @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode() || mth.getRegion() == null || mth.getBasicBlocks().isEmpty() || mth.contains(AType.JADX_ERROR)) { return; } // check if all blocks included in regions Set blocksInRegions = new HashSet<>(); DepthRegionTraversal.traverse(mth, new AbstractRegionVisitor() { @Override public void processBlock(MethodNode mth, IBlock container) { if (!(container instanceof BlockNode)) { return; } BlockNode block = (BlockNode) container; if (blocksInRegions.add(block)) { return; } if (Consts.DEBUG_RESTRUCTURE && LOG.isDebugEnabled() && !block.contains(AFlag.RETURN) && !block.contains(AFlag.REMOVE) && !block.contains(AFlag.SYNTHETIC) && !block.getInstructions().isEmpty()) { LOG.debug("Duplicated block: {} - {}", mth, block); } } }); if (mth.getBasicBlocks().size() != blocksInRegions.size()) { for (BlockNode block : mth.getBasicBlocks()) { if (!blocksInRegions.contains(block) && !block.getInstructions().isEmpty() && !block.contains(AFlag.ADDED_TO_REGION) && !block.contains(AFlag.DONT_GENERATE) && !block.contains(AFlag.REMOVE)) { String blockCode = getBlockInsnStr(mth, block).replace("*/", "*\\/"); mth.addWarn("Code restructure failed: missing block: " + block + ", code lost:" + blockCode); } } } DepthRegionTraversal.traverse(mth, new AbstractRegionVisitor() { @Override public boolean enterRegion(MethodNode mth, IRegion region) { if (region instanceof LoopRegion) { // check loop conditions BlockNode loopHeader = ((LoopRegion) region).getHeader(); if (loopHeader != null && !loopHeader.contains(AFlag.ALLOW_MULTIPLE_INSNS_LOOP_COND) && loopHeader.getInstructions().size() != 1) { mth.addWarn("Incorrect condition in loop: " + loopHeader); } } return true; } }); } private static String getBlockInsnStr(MethodNode mth, IBlock block) { ICodeWriter code = new SimpleCodeWriter(); code.incIndent(); code.newLine(); MethodGen mg = MethodGen.getFallbackMethodGen(mth); InsnGen ig = new InsnGen(mg, true); for (InsnNode insn : block.getInstructions()) { try { ig.makeInsn(insn, code); } catch (CodegenException e) { // ignore } } code.newLine(); return code.getCodeStr(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/CleanRegions.java ================================================ package jadx.core.dex.visitors.regions; import java.util.List; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.visitors.AbstractVisitor; public class CleanRegions extends AbstractVisitor { private static final IRegionVisitor REMOVE_REGION_VISITOR = new RemoveRegionVisitor(); @Override public void visit(MethodNode mth) { process(mth); } public static void process(MethodNode mth) { if (mth.isNoCode() || mth.getBasicBlocks().isEmpty()) { return; } DepthRegionTraversal.traverse(mth, REMOVE_REGION_VISITOR); } private static class RemoveRegionVisitor extends AbstractRegionVisitor { @Override public boolean enterRegion(MethodNode mth, IRegion region) { if (region instanceof Region) { region.getSubBlocks().removeIf(RemoveRegionVisitor::canRemoveRegion); } return true; } private static boolean canRemoveRegion(IContainer container) { if (container.contains(AFlag.DONT_GENERATE)) { return true; } if (container instanceof BlockNode) { BlockNode block = (BlockNode) container; return block.getInstructions().isEmpty(); } if (container instanceof LoopRegion) { LoopRegion loopRegion = (LoopRegion) container; if (loopRegion.isEndless()) { // keep empty endless loops return false; } } if (container instanceof IRegion) { List subBlocks = ((IRegion) container).getSubBlocks(); for (IContainer subBlock : subBlocks) { if (!canRemoveRegion(subBlock)) { return false; } } return true; } return false; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/DebugRegionCounter.java ================================================ package jadx.core.dex.visitors.regions; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.AbstractVisitor; public class DebugRegionCounter extends AbstractVisitor { @Override public void visit(MethodNode mth) { RegionCounterVisitor visitor = new RegionCounterVisitor(); DepthRegionTraversal.traverse(mth, visitor); List sortedBlocks = visitor.getSortedEntries(); for (BlockDepthEntry x : sortedBlocks) { System.out.println(x.depth + " : " + x.block.toString() + " // " + x.block.getInstructions().toString()); } System.out.println("nregions :: " + visitor.getNRegions()); } private static class RegionCounterVisitor extends AbstractRegionVisitor { private int depth = 0; private int nregions = 0; private List blockDepths = new ArrayList<>(); @Override public boolean enterRegion(MethodNode mth, IRegion region) { depth += 1; nregions += 1; return true; } @Override public void processBlock(MethodNode mth, IBlock container) { if (container instanceof BlockNode) { BlockNode b = (BlockNode) container; blockDepths.add(new BlockDepthEntry(depth, b)); } } @Override public void leaveRegion(MethodNode mth, IRegion region) { depth -= 1; } public List getSortedEntries() { blockDepths.sort(Comparator.comparingInt(x -> x.depth)); return blockDepths; } public int getNRegions() { return nregions; } } private static class BlockDepthEntry { public int depth; public BlockNode block; public BlockDepthEntry(int depth, BlockNode block) { this.depth = depth; this.block = block; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/DepthRegionTraversal.java ================================================ package jadx.core.dex.visitors.regions; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.utils.exceptions.JadxOverflowException; import jadx.core.utils.exceptions.JadxRuntimeException; public class DepthRegionTraversal { private static final int ITERATIVE_LIMIT_MULTIPLIER = 5; private DepthRegionTraversal() { } public static void traverse(MethodNode mth, IRegionVisitor visitor) { traverseInternal(mth, visitor, mth.getRegion()); } public static void traverse(MethodNode mth, IContainer container, IRegionVisitor visitor) { traverseInternal(mth, visitor, container); } public static void traverseIterative(MethodNode mth, IRegionIterativeVisitor visitor) { boolean repeat; int k = 0; int limit = ITERATIVE_LIMIT_MULTIPLIER * mth.getBasicBlocks().size(); do { repeat = traverseIterativeStepInternal(mth, visitor, mth.getRegion()); if (k++ > limit) { throw new JadxRuntimeException("Iterative traversal limit reached: " + "limit: " + limit + ", visitor: " + visitor.getClass().getName() + ", blocks count: " + mth.getBasicBlocks().size()); } } while (repeat); } public static void traverseIncludingExcHandlers(MethodNode mth, IRegionIterativeVisitor visitor) { boolean repeat; int k = 0; int limit = ITERATIVE_LIMIT_MULTIPLIER * mth.getBasicBlocks().size(); do { repeat = traverseIterativeStepInternal(mth, visitor, mth.getRegion()); if (!repeat) { for (ExceptionHandler h : mth.getExceptionHandlers()) { repeat = traverseIterativeStepInternal(mth, visitor, h.getHandlerRegion()); if (repeat) { break; } } } if (k++ > limit) { throw new JadxRuntimeException("Iterative traversal limit reached: " + "limit: " + limit + ", visitor: " + visitor.getClass().getName() + ", blocks count: " + mth.getBasicBlocks().size()); } } while (repeat); } private static void traverseInternal(MethodNode mth, IRegionVisitor visitor, IContainer container) { if (container instanceof IBlock) { visitor.processBlock(mth, (IBlock) container); } else if (container instanceof IRegion) { IRegion region = (IRegion) container; if (visitor.enterRegion(mth, region)) { region.getSubBlocks().forEach(subCont -> traverseInternal(mth, visitor, subCont)); } visitor.leaveRegion(mth, region); } } private static boolean traverseIterativeStepInternal(MethodNode mth, IRegionIterativeVisitor visitor, IContainer container) { if (container instanceof IRegion) { IRegion region = (IRegion) container; if (visitor.visitRegion(mth, region)) { return true; } for (IContainer subCont : region.getSubBlocks()) { try { if (traverseIterativeStepInternal(mth, visitor, subCont)) { return true; } } catch (StackOverflowError overflow) { throw new JadxOverflowException("Region traversal failed: Recursive call in traverseIterativeStepInternal method"); } } } return false; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/IRegionIterativeVisitor.java ================================================ package jadx.core.dex.visitors.regions; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; public interface IRegionIterativeVisitor { /** * If return 'true' traversal will be restarted. */ boolean visitRegion(MethodNode mth, IRegion region); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/IRegionVisitor.java ================================================ package jadx.core.dex.visitors.regions; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; public interface IRegionVisitor { void processBlock(MethodNode mth, IBlock container); /** * @return true for traverse sub-blocks, false otherwise. */ boolean enterRegion(MethodNode mth, IRegion region); void leaveRegion(MethodNode mth, IRegion region); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/IfRegionVisitor.java ================================================ package jadx.core.dex.visitors.regions; import java.util.List; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.regions.conditions.IfCondition.Mode; import jadx.core.dex.regions.conditions.IfRegion; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.InsnUtils; import jadx.core.utils.RegionUtils; import static jadx.core.utils.RegionUtils.insnsCount; public class IfRegionVisitor extends AbstractVisitor { private static final ProcessIfRegionVisitor PROCESS_IF_REGION_VISITOR = new ProcessIfRegionVisitor(); private static final RemoveRedundantElseVisitor REMOVE_REDUNDANT_ELSE_VISITOR = new RemoveRedundantElseVisitor(); @Override public void visit(MethodNode mth) { if (mth.isNoCode()) { return; } process(mth); } public static void processIfRequested(MethodNode mth) { if (mth.contains(AFlag.REQUEST_IF_REGION_OPTIMIZE)) { try { process(mth); } finally { mth.remove(AFlag.REQUEST_IF_REGION_OPTIMIZE); } } } private static void process(MethodNode mth) { TernaryMod.process(mth); DepthRegionTraversal.traverse(mth, PROCESS_IF_REGION_VISITOR); DepthRegionTraversal.traverseIterative(mth, REMOVE_REDUNDANT_ELSE_VISITOR); } private static class ProcessIfRegionVisitor extends AbstractRegionVisitor { @Override public boolean enterRegion(MethodNode mth, IRegion region) { if (region instanceof IfRegion) { IfRegion ifRegion = (IfRegion) region; orderBranches(mth, ifRegion); markElseIfChains(mth, ifRegion); } return true; } } @SuppressWarnings({ "UnnecessaryReturnStatement" }) private static void orderBranches(MethodNode mth, IfRegion ifRegion) { if (RegionUtils.isEmpty(ifRegion.getElseRegion())) { return; } if (RegionUtils.isEmpty(ifRegion.getThenRegion())) { invertIfRegion(ifRegion); return; } if (mth.contains(AFlag.USE_LINES_HINTS)) { int thenLine = RegionUtils.getFirstSourceLine(ifRegion.getThenRegion()); int elseLine = RegionUtils.getFirstSourceLine(ifRegion.getElseRegion()); if (thenLine != 0 && elseLine != 0) { if (thenLine > elseLine) { invertIfRegion(ifRegion); } return; } } if (ifRegion.simplifyCondition()) { IfCondition condition = ifRegion.getCondition(); if (condition != null && condition.getMode() == Mode.NOT) { invertIfRegion(ifRegion); } } int thenSize = insnsCount(ifRegion.getThenRegion()); int elseSize = insnsCount(ifRegion.getElseRegion()); if (isSimpleExitBlock(mth, ifRegion.getElseRegion())) { if (isSimpleExitBlock(mth, ifRegion.getThenRegion())) { if (elseSize < thenSize) { invertIfRegion(ifRegion); return; } } if (elseSize == 1) { boolean lastRegion = RegionUtils.hasExitEdge(ifRegion); if (lastRegion && mth.isVoidReturn()) { InsnNode lastElseInsn = RegionUtils.getLastInsn(ifRegion.getElseRegion()); if (InsnUtils.isInsnType(lastElseInsn, InsnType.THROW)) { // move `throw` into `then` block invertIfRegion(ifRegion); } else { // single return at method end will be removed later } return; } if (thenSize > 2 && !(lastRegion && thenSize < 4 /* keep small code block inside else */)) { invertIfRegion(ifRegion); return; } } } boolean thenExit = RegionUtils.hasExitBlock(ifRegion.getThenRegion()); boolean elseExit = RegionUtils.hasExitBlock(ifRegion.getElseRegion()); if (elseExit && (!thenExit || elseSize < thenSize)) { invertIfRegion(ifRegion); return; } // move 'if' from 'then' branch to make 'else if' chain if (isIfRegion(ifRegion.getThenRegion()) && !isIfRegion(ifRegion.getElseRegion()) && !thenExit) { invertIfRegion(ifRegion); return; } // move 'break' into 'then' branch if (RegionUtils.hasBreakInsn(ifRegion.getElseRegion())) { invertIfRegion(ifRegion); return; } } private static boolean isIfRegion(IContainer container) { if (container instanceof IfRegion) { return true; } if (container instanceof IRegion) { List subBlocks = ((IRegion) container).getSubBlocks(); return subBlocks.size() == 1 && subBlocks.get(0) instanceof IfRegion; } return false; } /** * Mark if-else-if chains */ private static void markElseIfChains(MethodNode mth, IfRegion ifRegion) { if (isSimpleExitBlock(mth, ifRegion.getThenRegion())) { return; } IContainer elsRegion = ifRegion.getElseRegion(); if (elsRegion instanceof Region) { List subBlocks = ((Region) elsRegion).getSubBlocks(); if (subBlocks.size() == 1 && subBlocks.get(0) instanceof IfRegion) { subBlocks.get(0).add(AFlag.ELSE_IF_CHAIN); elsRegion.add(AFlag.ELSE_IF_CHAIN); } } } private static class RemoveRedundantElseVisitor implements IRegionIterativeVisitor { @Override public boolean visitRegion(MethodNode mth, IRegion region) { if (region instanceof IfRegion) { return removeRedundantElseBlock(mth, (IfRegion) region); } return false; } } @SuppressWarnings("UnnecessaryParentheses") private static boolean removeRedundantElseBlock(MethodNode mth, IfRegion ifRegion) { if (ifRegion.getElseRegion() == null) { return false; } if (!RegionUtils.hasExitBlock(ifRegion.getThenRegion())) { return false; } InsnNode lastThanInsn = RegionUtils.getLastInsn(ifRegion.getThenRegion()); if (InsnUtils.isInsnType(lastThanInsn, InsnType.THROW)) { // always omit else after 'throw' } else { // code style check: // will remove 'return;' from 'then' and 'else' with one instruction // see #jadx.tests.integration.conditions.TestConditions9 if (mth.isVoidReturn()) { int thenSize = insnsCount(ifRegion.getThenRegion()); // keep small blocks with same or 'similar' size unchanged if (thenSize < 5) { int elseSize = insnsCount(ifRegion.getElseRegion()); int range = ifRegion.getElseRegion().contains(AFlag.ELSE_IF_CHAIN) ? 4 : 2; if (thenSize == elseSize || (thenSize * range > elseSize && thenSize < elseSize * range)) { return false; } } } } IRegion parent = ifRegion.getParent(); Region newRegion = new Region(parent); if (parent.replaceSubBlock(ifRegion, newRegion)) { newRegion.add(ifRegion); newRegion.add(ifRegion.getElseRegion()); ifRegion.setElseRegion(null); return true; } return false; } private static void invertIfRegion(IfRegion ifRegion) { IContainer elseRegion = ifRegion.getElseRegion(); if (elseRegion != null) { ifRegion.invert(); } } private static boolean isSimpleExitBlock(MethodNode mth, IContainer container) { if (container == null) { return false; } if (container.contains(AFlag.RETURN) || RegionUtils.isExitBlock(mth, container)) { return true; } if (container instanceof IRegion) { List subBlocks = ((IRegion) container).getSubBlocks(); return subBlocks.size() == 1 && RegionUtils.isExitBlock(mth, subBlocks.get(0)); } return false; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java ================================================ package jadx.core.dex.visitors.regions; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.ArithNode; import jadx.core.dex.instructions.ArithOp; import jadx.core.dex.instructions.IfOp; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.conditions.Compare; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.regions.loops.ForEachLoop; import jadx.core.dex.regions.loops.ForLoop; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.regions.loops.LoopType; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.regions.variables.ProcessVariables; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnUtils; import jadx.core.utils.RegionUtils; import jadx.core.utils.exceptions.JadxOverflowException; @JadxVisitor( name = "LoopRegionVisitor", desc = "Convert 'while' loops to 'for' loops (indexed or for-each)", runBefore = ProcessVariables.class ) public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor { private static final Logger LOG = LoggerFactory.getLogger(LoopRegionVisitor.class); @Override public void visit(MethodNode mth) { DepthRegionTraversal.traverse(mth, this); IfRegionVisitor.processIfRequested(mth); } @Override public boolean enterRegion(MethodNode mth, IRegion region) { if (region instanceof LoopRegion) { if (processLoopRegion(mth, (LoopRegion) region)) { // optimize `if` block after instructions remove mth.add(AFlag.REQUEST_IF_REGION_OPTIMIZE); } } return true; } private static boolean processLoopRegion(MethodNode mth, LoopRegion loopRegion) { if (loopRegion.isConditionAtEnd()) { return false; } IfCondition condition = loopRegion.getCondition(); if (condition == null) { return false; } if (checkForIndexedLoop(mth, loopRegion, condition)) { return true; } return checkIterableForEach(mth, loopRegion, condition); } /** * Check for indexed loop. */ private static boolean checkForIndexedLoop(MethodNode mth, LoopRegion loopRegion, IfCondition condition) { BlockNode loopEndBlock = loopRegion.getInfo().getEnd(); InsnNode incrInsn = BlockUtils.getLastInsn(BlockUtils.skipSyntheticPredecessor(loopEndBlock)); if (incrInsn == null) { return false; } RegisterArg incrArg = incrInsn.getResult(); if (incrArg == null || incrArg.getSVar() == null || !incrArg.getSVar().isUsedInPhi()) { return false; } List phiInsnList = incrArg.getSVar().getUsedInPhi(); if (phiInsnList.size() != 1) { return false; } PhiInsn phiInsn = phiInsnList.get(0); if (phiInsn.getArgsCount() != 2 || !phiInsn.containsVar(incrArg) || incrArg.getSVar().getUseCount() != 1) { return false; } RegisterArg arg = phiInsn.getResult(); List condArgs = condition.getRegisterArgs(); if (!condArgs.contains(arg) || arg.getSVar().isUsedInPhi()) { return false; } RegisterArg initArg = phiInsn.getArg(0); InsnNode initInsn = initArg.getAssignInsn(); if (initInsn == null || initInsn.contains(AFlag.DONT_GENERATE) || initArg.getSVar().getUseCount() != 1) { return false; } if (!usedOnlyInLoop(mth, loopRegion, arg)) { return false; } // can't make loop if argument from increment instruction is assign in loop List args = new ArrayList<>(); incrInsn.getRegisterArgs(args); for (RegisterArg iArg : args) { try { if (assignOnlyInLoop(mth, loopRegion, iArg)) { return false; } } catch (StackOverflowError error) { throw new JadxOverflowException("LoopRegionVisitor.assignOnlyInLoop endless recursion"); } } // all checks passed initInsn.add(AFlag.DONT_GENERATE); incrInsn.add(AFlag.DONT_GENERATE); LoopType arrForEach = checkArrayForEach(mth, loopRegion, initInsn, incrInsn, condition); loopRegion.setType(arrForEach != null ? arrForEach : new ForLoop(initInsn, incrInsn)); return true; } private static LoopType checkArrayForEach(MethodNode mth, LoopRegion loopRegion, InsnNode initInsn, InsnNode incrInsn, IfCondition condition) { if (!(incrInsn instanceof ArithNode)) { return null; } ArithNode arithNode = (ArithNode) incrInsn; if (arithNode.getOp() != ArithOp.ADD) { return null; } InsnArg lit = incrInsn.getArg(1); if (!lit.isLiteral() || ((LiteralArg) lit).getLiteral() != 1) { return null; } if (initInsn.getType() != InsnType.CONST || !initInsn.getArg(0).isLiteral() || ((LiteralArg) initInsn.getArg(0)).getLiteral() != 0) { return null; } InsnArg condArg = incrInsn.getArg(0); if (!condArg.isRegister()) { return null; } SSAVar sVar = ((RegisterArg) condArg).getSVar(); List args = sVar.getUseList(); if (args.size() != 3) { return null; } condArg = InsnUtils.getRegFromInsn(args, InsnType.IF); if (condArg == null) { return null; } RegisterArg arrIndex = InsnUtils.getRegFromInsn(args, InsnType.AGET); if (arrIndex == null) { return null; } InsnNode arrGetInsn = arrIndex.getParentInsn(); if (arrGetInsn == null || arrGetInsn.containsWrappedInsn()) { return null; } if (!condition.isCompare()) { return null; } Compare compare = condition.getCompare(); if (compare.getOp() != IfOp.LT || compare.getA() != condArg) { return null; } InsnNode len; InsnArg bCondArg = compare.getB(); if (bCondArg.isInsnWrap()) { len = ((InsnWrapArg) bCondArg).getWrapInsn(); } else if (bCondArg.isRegister()) { len = ((RegisterArg) bCondArg).getAssignInsn(); } else { return null; } if (len == null || len.getType() != InsnType.ARRAY_LENGTH) { return null; } InsnArg arrayArg = len.getArg(0); if (!arrayArg.equals(arrGetInsn.getArg(0))) { return null; } RegisterArg iterVar = arrGetInsn.getResult(); if (iterVar != null) { if (!usedOnlyInLoop(mth, loopRegion, iterVar)) { return null; } } else { if (!arrGetInsn.contains(AFlag.WRAPPED)) { return null; } // create new variable and replace wrapped insn InsnArg wrapArg = BlockUtils.searchWrappedInsnParent(mth, arrGetInsn); if (wrapArg == null || wrapArg.getParentInsn() == null) { mth.addWarnComment("checkArrayForEach: Wrapped insn not found: " + arrGetInsn); return null; } iterVar = mth.makeSyntheticRegArg(wrapArg.getType()); InsnNode parentInsn = wrapArg.getParentInsn(); parentInsn.replaceArg(wrapArg, iterVar.duplicate()); parentInsn.rebindArgs(); } // array for each loop confirmed incrInsn.getResult().add(AFlag.DONT_GENERATE); condArg.add(AFlag.DONT_GENERATE); bCondArg.add(AFlag.DONT_GENERATE); arrGetInsn.add(AFlag.DONT_GENERATE); compare.getInsn().add(AFlag.DONT_GENERATE); ForEachLoop forEachLoop = new ForEachLoop(iterVar, len.getArg(0)); forEachLoop.injectFakeInsns(loopRegion); if (InsnUtils.dontGenerateIfNotUsed(len)) { InsnRemover.remove(mth, len); } CodeShrinkVisitor.shrinkMethod(mth); return forEachLoop; } private static boolean checkIterableForEach(MethodNode mth, LoopRegion loopRegion, IfCondition condition) { List condArgs = condition.getRegisterArgs(); if (condArgs.size() != 1) { return false; } RegisterArg iteratorArg = condArgs.get(0); SSAVar sVar = iteratorArg.getSVar(); if (sVar == null || sVar.isUsedInPhi()) { return false; } List itUseList = sVar.getUseList(); InsnNode assignInsn = iteratorArg.getAssignInsn(); if (itUseList.size() != 2) { return false; } if (!checkInvoke(assignInsn, null, "iterator()Ljava/util/Iterator;")) { return false; } InsnArg iterableArg = assignInsn.getArg(0); InsnNode hasNextCall = itUseList.get(0).getParentInsn(); InsnNode nextCall = itUseList.get(1).getParentInsn(); if (!checkInvoke(hasNextCall, "java.util.Iterator", "hasNext()Z") || !checkInvoke(nextCall, "java.util.Iterator", "next()Ljava/lang/Object;")) { return false; } List toSkip = new ArrayList<>(); RegisterArg iterVar; if (nextCall.contains(AFlag.WRAPPED)) { InsnArg wrapArg = BlockUtils.searchWrappedInsnParent(mth, nextCall); if (wrapArg != null && wrapArg.getParentInsn() != null) { InsnNode parentInsn = wrapArg.getParentInsn(); BlockNode block = BlockUtils.getBlockByInsn(mth, parentInsn); if (block == null) { return false; } if (!RegionUtils.isRegionContainsBlock(loopRegion, block)) { return false; } if (parentInsn.getType() == InsnType.CHECK_CAST) { iterVar = parentInsn.getResult(); if (iterVar == null || !fixIterableType(mth, iterableArg, iterVar)) { return false; } InsnArg castArg = BlockUtils.searchWrappedInsnParent(mth, parentInsn); if (castArg != null && castArg.getParentInsn() != null) { castArg.getParentInsn().replaceArg(castArg, iterVar); } else { // cast not inlined toSkip.add(parentInsn); } } else { iterVar = nextCall.getResult(); if (iterVar == null) { return false; } iterVar.remove(AFlag.REMOVE); // restore variable from inlined insn nextCall.add(AFlag.DONT_GENERATE); if (!fixIterableType(mth, iterableArg, iterVar)) { return false; } parentInsn.replaceArg(wrapArg, iterVar); } } else { LOG.warn(" checkIterableForEach: Wrapped insn not found: {}, mth: {}", nextCall, mth); return false; } } else { iterVar = nextCall.getResult(); if (iterVar == null) { return false; } if (!usedOnlyInLoop(mth, loopRegion, iterVar)) { return false; } if (!assignOnlyInLoop(mth, loopRegion, iterVar)) { return false; } toSkip.add(nextCall); } assignInsn.add(AFlag.DONT_GENERATE); assignInsn.getResult().add(AFlag.DONT_GENERATE); for (InsnNode insnNode : toSkip) { insnNode.setResult(null); insnNode.add(AFlag.DONT_GENERATE); } for (RegisterArg itArg : itUseList) { itArg.add(AFlag.DONT_GENERATE); } ForEachLoop forEachLoop = new ForEachLoop(iterVar, iterableArg); forEachLoop.injectFakeInsns(loopRegion); loopRegion.setType(forEachLoop); return true; } private static boolean fixIterableType(MethodNode mth, InsnArg iterableArg, RegisterArg iterVar) { ArgType iterableType = iterableArg.getType(); ArgType varType = iterVar.getType(); if (iterableType.isGeneric()) { List genericTypes = iterableType.getGenericTypes(); if (genericTypes == null || genericTypes.size() != 1) { return false; } ArgType gType = genericTypes.get(0); if (gType.equals(varType)) { return true; } if (gType.isGenericType()) { iterVar.setType(gType); return true; } if (ArgType.isInstanceOf(mth.root(), gType, varType)) { return true; } ArgType wildcardType = gType.getWildcardType(); if (wildcardType != null && gType.getWildcardBound() == ArgType.WildcardBound.EXTENDS && ArgType.isInstanceOf(mth.root(), wildcardType, varType)) { return true; } LOG.warn("Generic type differs: '{}' and '{}' in {}", gType, varType, mth); return false; } if (!iterableArg.isRegister() || !iterableType.isObject()) { return true; } ArgType genericType = ArgType.generic(iterableType.getObject(), varType); if (iterableArg.isRegister()) { ArgType immutableType = ((RegisterArg) iterableArg).getImmutableType(); if (immutableType != null && !immutableType.equals(genericType)) { // can't change type // allow to iterate over not generified collection only for Object vars return varType.equals(ArgType.OBJECT); } } iterableArg.setType(genericType); return true; } /** * Check if instruction is a interface invoke with corresponding parameters. */ private static boolean checkInvoke(InsnNode insn, String declClsFullName, String mthId) { if (insn == null) { return false; } if (insn.getType() == InsnType.INVOKE) { InvokeNode inv = (InvokeNode) insn; MethodInfo callMth = inv.getCallMth(); if ((inv.getInvokeType() == InvokeType.INTERFACE || inv.getInvokeType() == InvokeType.VIRTUAL) && callMth.getShortId().equals(mthId)) { if (declClsFullName == null) { return true; } return callMth.getDeclClass().getFullName().equals(declClsFullName); } } return false; } private static boolean assignOnlyInLoop(MethodNode mth, LoopRegion loopRegion, RegisterArg arg) { InsnNode assignInsn = arg.getAssignInsn(); if (assignInsn == null) { return true; } if (!argInLoop(mth, loopRegion, assignInsn.getResult())) { return false; } if (assignInsn instanceof PhiInsn) { PhiInsn phiInsn = (PhiInsn) assignInsn; for (InsnArg phiArg : phiInsn.getArguments()) { if (!assignOnlyInLoop(mth, loopRegion, (RegisterArg) phiArg)) { return false; } } } return true; } private static boolean usedOnlyInLoop(MethodNode mth, LoopRegion loopRegion, RegisterArg arg) { List useList = arg.getSVar().getUseList(); for (RegisterArg useArg : useList) { if (!argInLoop(mth, loopRegion, useArg)) { return false; } } return true; } private static boolean argInLoop(MethodNode mth, LoopRegion loopRegion, RegisterArg arg) { InsnNode parentInsn = arg.getParentInsn(); if (parentInsn == null) { return false; } BlockNode block = BlockUtils.getBlockByInsn(mth, parentInsn); if (block == null) { LOG.debug(" LoopRegionVisitor: instruction not found: {}, mth: {}", parentInsn, mth); return false; } return RegionUtils.isRegionContainsBlock(loopRegion, block); } @Override public void leaveRegion(MethodNode mth, IRegion region) { } @Override public void processBlock(MethodNode mth, IBlock container) { } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/PostProcessRegions.java ================================================ package jadx.core.dex.visitors.regions; import java.util.Collections; import java.util.List; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.EdgeInsnAttr; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnContainer; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.visitors.regions.maker.SwitchRegionMaker; public final class PostProcessRegions extends AbstractRegionVisitor { private static final IRegionVisitor INSTANCE = new PostProcessRegions(); static void process(MethodNode mth) { DepthRegionTraversal.traverse(mth, INSTANCE); } @Override public void leaveRegion(MethodNode mth, IRegion region) { if (region instanceof LoopRegion) { // merge conditions in loops LoopRegion loop = (LoopRegion) region; loop.mergePreCondition(); } else if (region instanceof SwitchRegion) { SwitchRegionMaker.insertBreaks(mth, (SwitchRegion) region); } else if (region instanceof Region) { insertEdgeInsn((Region) region); } } /** * Insert insn block from edge insn attribute. */ private static void insertEdgeInsn(Region region) { List subBlocks = region.getSubBlocks(); if (subBlocks.isEmpty()) { return; } IContainer last = subBlocks.get(subBlocks.size() - 1); List edgeInsnAttrs = last.getAll(AType.EDGE_INSN); if (edgeInsnAttrs.isEmpty()) { return; } EdgeInsnAttr insnAttr = edgeInsnAttrs.get(0); if (!insnAttr.getStart().equals(last)) { return; } if (last instanceof BlockNode) { BlockNode block = (BlockNode) last; if (block.getInstructions().isEmpty()) { block.getInstructions().add(insnAttr.getInsn()); return; } } List insns = Collections.singletonList(insnAttr.getInsn()); region.add(new InsnContainer(insns)); } private PostProcessRegions() { // singleton } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/ProcessTryCatchRegions.java ================================================ package jadx.core.dex.visitors.regions; import java.util.ArrayList; import java.util.Collections; import java.util.List; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBranchRegion; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.AbstractRegion; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.TryCatchRegion; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.trycatch.TryCatchBlockAttr; import jadx.core.utils.RegionUtils; /** * Extract blocks to separate try/catch region */ public class ProcessTryCatchRegions extends AbstractRegionVisitor { public static void process(MethodNode mth) { if (mth.isNoCode() || mth.isNoExceptionHandlers()) { return; } List tryBlocks = collectTryCatchBlocks(mth); if (tryBlocks.isEmpty()) { return; } DepthRegionTraversal.traverseIncludingExcHandlers(mth, (regionMth, region) -> { boolean changed = checkAndWrap(regionMth, tryBlocks, region); return changed && !tryBlocks.isEmpty(); }); } private static List collectTryCatchBlocks(MethodNode mth) { List list = mth.getAll(AType.TRY_BLOCKS_LIST); if (list.isEmpty()) { return Collections.emptyList(); } List tryBlocks = new ArrayList<>(list); tryBlocks.sort((a, b) -> a == b ? 0 : a.getOuterTryBlock() == b ? 1 : -1); // move parent try block to top return tryBlocks; } private static boolean checkAndWrap(MethodNode mth, List tryBlocks, IRegion region) { // search top splitter block in this region (don't need to go deeper) for (TryCatchBlockAttr tb : tryBlocks) { BlockNode topSplitter = tb.getTopSplitter(); if (region.getSubBlocks().contains(topSplitter)) { if (!wrapBlocks(region, tb, topSplitter)) { mth.addWarn("Can't wrap try/catch for region: " + region); } tryBlocks.remove(tb); return true; } } return false; } /** * Extract all block dominated by 'dominator' to separate region and mark as try/catch block */ private static boolean wrapBlocks(IRegion replaceRegion, TryCatchBlockAttr tb, BlockNode dominator) { if (replaceRegion == null) { return false; } if (replaceRegion instanceof LoopRegion) { LoopRegion loop = (LoopRegion) replaceRegion; return wrapBlocks(loop.getBody(), tb, dominator); } if (replaceRegion instanceof IBranchRegion) { return wrapBlocks(replaceRegion.getParent(), tb, dominator); } Region tryRegion = new Region(replaceRegion); List subBlocks = replaceRegion.getSubBlocks(); // traverse the enclosing region for blocks that have a path from the dominator but don't have a // path from any of the exception handlers i.e. they are not before the end of the try block so // should be inside the try block. for (IContainer cont : subBlocks) { if (RegionUtils.hasPathThroughBlock(dominator, cont)) { if (isHandlerPath(tb, cont)) { // this block/region has a path from an exception handler so is after the end of the try block continue; } tryRegion.getSubBlocks().add(cont); } } if (tryRegion.getSubBlocks().isEmpty()) { return false; } TryCatchRegion tryCatchRegion = new TryCatchRegion(replaceRegion, tryRegion); tryRegion.setParent(tryCatchRegion); tryCatchRegion.setTryCatchBlock(tb); // replace first node by region IContainer firstNode = tryRegion.getSubBlocks().get(0); if (!replaceRegion.replaceSubBlock(firstNode, tryCatchRegion)) { return false; } subBlocks.removeAll(tryRegion.getSubBlocks()); // fix parents for tryRegion sub blocks for (IContainer cont : tryRegion.getSubBlocks()) { if (cont instanceof AbstractRegion) { AbstractRegion aReg = (AbstractRegion) cont; aReg.setParent(tryRegion); } } return true; } private static boolean isHandlerPath(TryCatchBlockAttr tb, IContainer cont) { for (ExceptionHandler h : tb.getHandlers()) { BlockNode handlerBlock = h.getHandlerBlock(); if (handlerBlock != null && !handlerBlock.contains(AFlag.REMOVE) && RegionUtils.hasPathThroughBlock(handlerBlock, cont)) { return true; } } return false; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMakerVisitor.java ================================================ package jadx.core.dex.visitors.regions; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.regions.maker.ExcHandlersRegionMaker; import jadx.core.dex.visitors.regions.maker.RegionMaker; import jadx.core.dex.visitors.regions.maker.SynchronizedRegionMaker; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "RegionMakerVisitor", desc = "Pack blocks into regions for code generation" ) public class RegionMakerVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode() || mth.getBasicBlocks().isEmpty()) { return; } RegionMaker rm = new RegionMaker(mth); mth.setRegion(rm.makeMthRegion()); if (!mth.isNoExceptionHandlers()) { new ExcHandlersRegionMaker(mth, rm).process(); } processForceInlineInsns(mth); ProcessTryCatchRegions.process(mth); PostProcessRegions.process(mth); CleanRegions.process(mth); if (mth.getAccessFlags().isSynchronized()) { SynchronizedRegionMaker.removeSynchronized(mth); } } private static void processForceInlineInsns(MethodNode mth) { boolean needShrink = mth.getBasicBlocks().stream() .flatMap(block -> block.getInstructions().stream()) .anyMatch(insn -> insn.contains(AFlag.FORCE_ASSIGN_INLINE)); if (needShrink) { CodeShrinkVisitor.shrinkMethod(mth); } } @Override public String getName() { return "RegionMakerVisitor"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/ReturnVisitor.java ================================================ package jadx.core.dex.visitors.regions; import java.util.List; import java.util.ListIterator; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IBranchRegion; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Remove unnecessary return instructions for void methods */ public class ReturnVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { if (mth.isVoidReturn()) { DepthRegionTraversal.traverse(mth, new ReturnRemoverVisitor()); } } private static final class ReturnRemoverVisitor extends TracedRegionVisitor { @Override public boolean enterRegion(MethodNode mth, IRegion region) { super.enterRegion(mth, region); return !(region instanceof SwitchRegion); } @Override public void processBlockTraced(MethodNode mth, IBlock container, IRegion currentRegion) { if (container.getClass() != BlockNode.class) { return; } BlockNode block = (BlockNode) container; if (block.contains(AFlag.RETURN)) { List insns = block.getInstructions(); if (insns.size() == 1 && blockNotInLoop(mth, block) && noTrailInstructions(block)) { insns.remove(0); block.remove(AFlag.RETURN); } } } private boolean blockNotInLoop(MethodNode mth, BlockNode block) { if (mth.getLoopsCount() == 0) { return true; } if (mth.getLoopForBlock(block) != null) { return false; } for (IRegion region : regionStack) { if (region.getClass() == LoopRegion.class) { return false; } } return true; } /** * Check that there are no code after this block in regions structure */ private boolean noTrailInstructions(BlockNode block) { IContainer curContainer = block; for (IRegion region : regionStack) { // ignore paths on other branches if (region instanceof IBranchRegion) { curContainer = region; continue; } List subBlocks = region.getSubBlocks(); if (!subBlocks.isEmpty()) { ListIterator itSubBlock = subBlocks.listIterator(subBlocks.size()); while (itSubBlock.hasPrevious()) { IContainer subBlock = itSubBlock.previous(); if (subBlock == curContainer) { break; } else if (!isEmpty(subBlock)) { return false; } } } curContainer = region; } return true; } /** * Check if container not contains instructions, * don't count one 'return' instruction (it will be removed later). */ private static boolean isEmpty(IContainer container) { if (container instanceof IBlock) { IBlock block = (IBlock) container; return block.getInstructions().isEmpty() || block.contains(AFlag.RETURN); } else if (container instanceof IRegion) { IRegion region = (IRegion) container; for (IContainer block : region.getSubBlocks()) { if (!isEmpty(block)) { return false; } } return true; } else { throw new JadxRuntimeException("Unknown container type: " + container.getClass()); } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/SwitchBreakVisitor.java ================================================ package jadx.core.dex.visitors.regions; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Supplier; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; import jadx.core.dex.attributes.nodes.RegionRefAttr; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IBranchRegion; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.regions.maker.SwitchRegionMaker; import jadx.core.utils.BlockInsnPair; import jadx.core.utils.BlockParentContainer; import jadx.core.utils.BlockUtils; import jadx.core.utils.ListUtils; import jadx.core.utils.RegionUtils; import jadx.core.utils.exceptions.JadxException; import static jadx.core.dex.attributes.nodes.CodeFeaturesAttr.CodeFeature.SWITCH; @JadxVisitor( name = "SwitchBreakVisitor", desc = "Optimize 'break' instruction: common code extract, remove unreachable", runAfter = LoopRegionVisitor.class // can add 'continue' at case end ) public class SwitchBreakVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { if (CodeFeaturesAttr.contains(mth, SWITCH)) { runSwitchTraverse(mth, ExtractCommonBreak::new); runSwitchTraverse(mth, RemoveUnreachableBreak::new); IfRegionVisitor.processIfRequested(mth); } } private static void runSwitchTraverse(MethodNode mth, Supplier builder) { DepthRegionTraversal.traverse(mth, new IterativeSwitchRegionVisitor(builder)); } /** * Add common 'break' if 'break' or exit insn ('return', 'throw', 'continue') found in all branches. * Remove exist common break if all branches contain exit insn. */ private static final class ExtractCommonBreak extends BaseSwitchRegionVisitor { @Override public void processRegion(MethodNode mth, IRegion region) { if (region instanceof IBranchRegion && !(region instanceof SwitchRegion)) { // if break in all branches extract to parent region processBranchRegion(mth, region); } } private void processBranchRegion(MethodNode mth, IRegion region) { IRegion parentRegion = region.getParent(); if (parentRegion.contains(AFlag.FALL_THROUGH)) { // fallthrough case, can't extract break return; } boolean dontAddCommonBreak = false; IBlock lastParentBlock = RegionUtils.getLastBlock(parentRegion); if (BlockUtils.containsExitInsn(lastParentBlock)) { if (isBreakBlock(lastParentBlock)) { // parent block already contains 'break' dontAddCommonBreak = true; } else { // can't add 'break' after 'return', 'throw' or 'continue' return; } } List branches = ((IBranchRegion) region).getBranches(); boolean removeCommonBreak = true; // all branches contain exit insns, common break is unreachable List forBreakRemove = new ArrayList<>(); for (IContainer branch : branches) { if (branch == null) { removeCommonBreak = false; continue; } BlockInsnPair last = RegionUtils.getLastInsnWithBlock(branch); if (last == null) { return; } InsnNode lastInsn = last.getInsn(); if (lastInsn.getType() == InsnType.BREAK) { IBlock block = last.getBlock(); IContainer parent = RegionUtils.getBlockContainer(branch, block); forBreakRemove.add(new BlockParentContainer(parent, block)); removeCommonBreak = false; } else if (!lastInsn.isExitEdgeInsn()) { removeCommonBreak = false; } } if (!forBreakRemove.isEmpty()) { // common 'break' confirmed for (BlockParentContainer breakData : forBreakRemove) { removeBreak(breakData.getBlock(), breakData.getParent()); } if (!dontAddCommonBreak) { addBreakRegion.add(parentRegion); // new 'break' might become 'common' for upper branch region, request to run checks again requestReRun(); } // removed 'break' may allow to use 'else-if' chain mth.add(AFlag.REQUEST_IF_REGION_OPTIMIZE); } if (removeCommonBreak && lastParentBlock != null) { removeBreak(lastParentBlock, parentRegion); } } } private static final class RemoveUnreachableBreak extends BaseSwitchRegionVisitor { @Override public void processRegion(MethodNode mth, IRegion region) { List subBlocks = region.getSubBlocks(); IContainer lastContainer = ListUtils.last(subBlocks); if (lastContainer instanceof IBlock) { IBlock block = (IBlock) lastContainer; if (isBreakBlock(block) && isPrevInsnIsExit(block, subBlocks)) { removeBreak(block, region); } } } private boolean isPrevInsnIsExit(IBlock breakBlock, List subBlocks) { InsnNode prevInsn = null; if (breakBlock.getInstructions().size() > 1) { // check prev insn in same block List insns = breakBlock.getInstructions(); prevInsn = insns.get(insns.size() - 2); } else if (subBlocks.size() > 1) { IContainer prev = subBlocks.get(subBlocks.size() - 2); if (prev instanceof IBlock) { List insns = ((IBlock) prev).getInstructions(); prevInsn = ListUtils.last(insns); } } return prevInsn != null && prevInsn.isExitEdgeInsn(); } } /** * For every 'switch' region run new instance of provided 'switch' visitor. * If rerun requested, run traverse for that visitor again. */ private static final class IterativeSwitchRegionVisitor extends AbstractRegionVisitor { private final Supplier builder; public IterativeSwitchRegionVisitor(Supplier builder) { this.builder = builder; } @Override public void leaveRegion(MethodNode mth, IRegion region) { if (region instanceof SwitchRegion) { SwitchRegion switchRegion = (SwitchRegion) region; BaseSwitchRegionVisitor switchVisitor = builder.get(); switchVisitor.setCurrentSwitch(switchRegion); boolean runAgain; int k = 0; do { runAgain = false; DepthRegionTraversal.traverse(mth, switchRegion, switchVisitor); if (switchVisitor.isReRunRequested()) { switchVisitor.reset(); runAgain = true; } if (k++ > 20) { // 20 nested 'if' are not expected mth.addWarnComment("Unexpected iteration count in SwitchBreakVisitor. Please report as an issue"); break; } } while (runAgain); } } } private abstract static class BaseSwitchRegionVisitor extends AbstractRegionVisitor { protected final Set addBreakRegion = new HashSet<>(); protected final Set cleanupSet = new HashSet<>(); protected SwitchRegion currentSwitch; private boolean reRunRequested = false; public abstract void processRegion(MethodNode mth, IRegion region); @Override public boolean enterRegion(MethodNode mth, IRegion region) { processRegion(mth, region); return true; } @Override public void leaveRegion(MethodNode mth, IRegion region) { if (addBreakRegion.contains(region)) { addBreakRegion.remove(region); region.getSubBlocks().add(SwitchRegionMaker.buildBreakContainer(currentSwitch)); } if (cleanupSet.contains(region)) { cleanupSet.remove(region); region.getSubBlocks().removeIf(r -> r.contains(AFlag.REMOVE)); } } /** * Method called before visitor rerun */ public void reset() { reRunRequested = false; addBreakRegion.clear(); cleanupSet.clear(); } public void requestReRun() { reRunRequested = true; } public boolean isReRunRequested() { return reRunRequested; } public void setCurrentSwitch(SwitchRegion currentSwitch) { this.currentSwitch = currentSwitch; } protected boolean isBreakBlock(@Nullable IBlock block) { if (block != null) { InsnNode lastInsn = ListUtils.last(block.getInstructions()); if (lastInsn != null && lastInsn.getType() == InsnType.BREAK) { RegionRefAttr regionRefAttr = lastInsn.get(AType.REGION_REF); return regionRefAttr != null && regionRefAttr.getRegion() == currentSwitch; } } return false; } protected void removeBreak(IBlock breakBlock, IContainer parentContainer) { List instructions = breakBlock.getInstructions(); InsnNode last = ListUtils.last(instructions); if (last != null && last.getType() == InsnType.BREAK) { ListUtils.removeLast(instructions); if (instructions.isEmpty()) { breakBlock.add(AFlag.REMOVE); cleanupSet.add(parentContainer); } } } } @Override public String getName() { return "SwitchBreakVisitor"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/SwitchOverStringVisitor.java ================================================ package jadx.core.dex.visitors.regions; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; import jadx.core.dex.attributes.nodes.CodeFeaturesAttr.CodeFeature; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.IfOp; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.regions.conditions.Compare; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.regions.conditions.IfRegion; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnRemover; import jadx.core.utils.InsnUtils; import jadx.core.utils.RegionUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "SwitchOverStringVisitor", desc = "Restore switch over string", runAfter = IfRegionVisitor.class, runBefore = ReturnVisitor.class ) public class SwitchOverStringVisitor extends AbstractVisitor implements IRegionIterativeVisitor { @Override public void visit(MethodNode mth) throws JadxException { if (!CodeFeaturesAttr.contains(mth, CodeFeature.SWITCH)) { return; } DepthRegionTraversal.traverseIterative(mth, this); } @Override public boolean visitRegion(MethodNode mth, IRegion region) { if (region instanceof SwitchRegion) { return restoreSwitchOverString(mth, (SwitchRegion) region); } return false; } private boolean restoreSwitchOverString(MethodNode mth, SwitchRegion switchRegion) { try { InsnNode swInsn = BlockUtils.getLastInsnWithType(switchRegion.getHeader(), InsnType.SWITCH); if (swInsn == null) { return false; } RegisterArg strArg = getStrHashCodeArg(swInsn.getArg(0)); if (strArg == null) { return false; } int casesCount = switchRegion.getCases().size(); boolean defaultCaseAdded = switchRegion.getCases().stream().anyMatch(SwitchRegion.CaseInfo::isDefaultCase); int casesWithString = defaultCaseAdded ? casesCount - 1 : casesCount; SSAVar strVar = strArg.getSVar(); if (strVar.getUseCount() - 1 < casesWithString) { // one 'hashCode' invoke and at least one 'equals' per case return false; } // quick checks done, start collecting data to create a new switch region Map strEqInsns = collectEqualsInsns(mth, strVar); if (strEqInsns.size() < casesWithString) { return false; } SwitchData switchData = new SwitchData(mth, switchRegion); switchData.setStrEqInsns(strEqInsns); switchData.setCases(new ArrayList<>(casesCount)); for (SwitchRegion.CaseInfo swCaseInfo : switchRegion.getCases()) { if (!processCase(switchData, swCaseInfo)) { mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue"); return false; } } // match remapping var to collect code from second switch if (!mergeWithCode(switchData)) { mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue"); return false; } // all checks passed, replace with new switch IRegion parentRegion = switchRegion.getParent(); SwitchRegion replaceRegion = new SwitchRegion(parentRegion, switchRegion.getHeader()); for (SwitchRegion.CaseInfo caseInfo : switchData.getNewCases()) { replaceRegion.addCase(Collections.unmodifiableList(caseInfo.getKeys()), caseInfo.getContainer()); } if (!parentRegion.replaceSubBlock(switchRegion, replaceRegion)) { mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue"); return false; } // replace confirmed, remove original code markCodeForRemoval(switchData); // use string arg directly in switch swInsn.replaceArg(swInsn.getArg(0), strArg.duplicate()); return true; } catch (StackOverflowError | Exception e) { mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue", e); return false; } } private static void markCodeForRemoval(SwitchData switchData) { MethodNode mth = switchData.getMth(); try { switchData.getToRemove().forEach(i -> i.add(AFlag.REMOVE)); SwitchRegion codeSwitch = switchData.getCodeSwitch(); if (codeSwitch != null) { IRegion parentRegion = switchData.getSwitchRegion().getParent(); parentRegion.getSubBlocks().remove(codeSwitch); codeSwitch.getHeader().add(AFlag.REMOVE); } RegisterArg numArg = switchData.getNumArg(); if (numArg != null) { for (SSAVar ssaVar : numArg.getSVar().getCodeVar().getSsaVars()) { InsnNode assignInsn = ssaVar.getAssignInsn(); if (assignInsn != null) { assignInsn.add(AFlag.REMOVE); } for (RegisterArg useArg : ssaVar.getUseList()) { InsnNode parentInsn = useArg.getParentInsn(); if (parentInsn != null) { parentInsn.add(AFlag.REMOVE); } } mth.removeSVar(ssaVar); } } InsnRemover.removeAllMarked(mth); } catch (StackOverflowError | Exception e) { mth.addWarnComment("Failed to clean up code after switch over string restore", e); } } private boolean mergeWithCode(SwitchData switchData) { // check for second switch IContainer nextContainer = RegionUtils.getNextContainer(switchData.getMth(), switchData.getSwitchRegion()); if (!(nextContainer instanceof SwitchRegion)) { return false; } SwitchRegion codeSwitch = (SwitchRegion) nextContainer; InsnNode swInsn = BlockUtils.getLastInsnWithType(codeSwitch.getHeader(), InsnType.SWITCH); if (swInsn == null || !swInsn.getArg(0).isRegister()) { return false; } RegisterArg numArg = (RegisterArg) swInsn.getArg(0); List cases = switchData.getCases(); // search index assign in cases code int extracted = 0; for (CaseData caseData : cases) { InsnNode numInsn = searchConstInsn(switchData, caseData, swInsn); Integer num = extractConstNumber(switchData, numInsn, numArg); if (num != null) { caseData.setCodeNum(num); extracted++; } } if (extracted == 0) { // nothing to merge, code already inside first switch cases return true; } if (extracted != cases.size()) { return false; } // TODO: additional checks for found index numbers cases.sort(Comparator.comparingInt(CaseData::getCodeNum)); // extract complete Map casesMap = new HashMap<>(cases.size()); for (CaseData caseData : cases) { CaseData prev = casesMap.put(caseData.getCodeNum(), caseData); if (prev != null) { return false; } RegionUtils.visitBlocks(switchData.getMth(), caseData.getCode(), block -> switchData.getToRemove().add(block)); } List newCases = new ArrayList<>(); for (SwitchRegion.CaseInfo caseInfo : codeSwitch.getCases()) { SwitchRegion.CaseInfo newCase = null; for (Object key : caseInfo.getKeys()) { Integer intKey = unwrapIntKey(key); if (intKey != null) { CaseData caseData = casesMap.remove(intKey); if (caseData == null) { return false; } if (newCase == null) { List keys = new ArrayList<>(caseData.getStrValues()); newCase = new SwitchRegion.CaseInfo(keys, caseInfo.getContainer()); } else { // merge cases newCase.getKeys().addAll(caseData.getStrValues()); } } else if (key == SwitchRegion.DEFAULT_CASE_KEY) { var iterator = casesMap.entrySet().iterator(); while (iterator.hasNext()) { CaseData caseData = iterator.next().getValue(); if (newCase == null) { List keys = new ArrayList<>(caseData.getStrValues()); newCase = new SwitchRegion.CaseInfo(keys, caseInfo.getContainer()); } else { // merge cases newCase.getKeys().addAll(caseData.getStrValues()); } iterator.remove(); } if (newCase == null) { newCase = new SwitchRegion.CaseInfo(new ArrayList<>(), caseInfo.getContainer()); } newCase.getKeys().add(SwitchRegion.DEFAULT_CASE_KEY); } else { return false; } } newCases.add(newCase); } switchData.setCodeSwitch(codeSwitch); switchData.setNumArg(numArg); switchData.setNewCases(newCases); return true; } private @Nullable Integer extractConstNumber(SwitchData switchData, @Nullable InsnNode numInsn, RegisterArg numArg) { if (numInsn == null || numInsn.getArgsCount() != 1) { return null; } Object constVal = InsnUtils.getConstValueByArg(switchData.getMth().root(), numInsn.getArg(0)); if (constVal instanceof LiteralArg) { if (numArg.sameCodeVar(numInsn.getResult())) { return (int) ((LiteralArg) constVal).getLiteral(); } } return null; } private static @Nullable InsnNode searchConstInsn(SwitchData switchData, CaseData caseData, InsnNode swInsn) { IContainer container = caseData.getCode(); if (container != null) { List insns = RegionUtils.collectInsns(switchData.getMth(), container); insns.removeIf(i -> i.getType() == InsnType.BREAK); if (insns.size() == 1) { return insns.get(0); } } else if (caseData.getBlockRef() != null) { // variable used unchanged on path from block ref BlockNode blockRef = caseData.getBlockRef(); InsnArg swArg = swInsn.getArg(0); if (swArg.isRegister()) { InsnNode assignInsn = ((RegisterArg) swArg).getSVar().getAssignInsn(); if (assignInsn != null && assignInsn.getType() == InsnType.PHI) { RegisterArg arg = ((PhiInsn) assignInsn).getArgByBlock(blockRef); if (arg != null) { return arg.getAssignInsn(); } } } } return null; } private Integer unwrapIntKey(Object key) { if (key instanceof Integer) { return (Integer) key; } if (key instanceof FieldNode) { EncodedValue encodedValue = ((FieldNode) key).get(JadxAttrType.CONSTANT_VALUE); if (encodedValue != null && encodedValue.getType() == EncodedType.ENCODED_INT) { return (Integer) encodedValue.getValue(); } return null; } return null; } private static Map collectEqualsInsns(MethodNode mth, SSAVar strVar) { Map map = new IdentityHashMap<>(strVar.getUseCount() - 1); for (RegisterArg useReg : strVar.getUseList()) { InsnNode parentInsn = useReg.getParentInsn(); if (parentInsn != null && parentInsn.getType() == InsnType.INVOKE) { InvokeNode inv = (InvokeNode) parentInsn; if (inv.getCallMth().getRawFullId().equals("java.lang.String.equals(Ljava/lang/Object;)Z")) { InsnArg strArg = inv.getArg(1); Object strValue = InsnUtils.getConstValueByArg(mth.root(), strArg); if (strValue instanceof String) { map.put(parentInsn, (String) strValue); } } } } return map; } private boolean processCase(SwitchData switchData, SwitchRegion.CaseInfo caseInfo) { if (caseInfo.isDefaultCase()) { CaseData caseData = new CaseData(); caseData.setCode(caseInfo.getContainer()); return true; } AtomicBoolean fail = new AtomicBoolean(false); RegionUtils.visitRegions(switchData.getMth(), caseInfo.getContainer(), region -> { if (fail.get()) { return false; } if (region instanceof IfRegion) { CaseData caseData = fillCaseData((IfRegion) region, switchData); if (caseData == null) { fail.set(true); return false; } switchData.getCases().add(caseData); } return true; }); return !fail.get(); } private @Nullable CaseData fillCaseData(IfRegion ifRegion, SwitchData switchData) { IfCondition condition = Objects.requireNonNull(ifRegion.getCondition()); boolean neg = false; if (condition.getMode() == IfCondition.Mode.NOT) { condition = condition.getArgs().get(0); neg = true; } Compare compare = condition.getCompare(); if (compare == null) { return null; } IfNode ifInsn = compare.getInsn(); InsnArg firstArg = ifInsn.getArg(0); String str = null; if (firstArg.isInsnWrap()) { str = switchData.getStrEqInsns().get(((InsnWrapArg) firstArg).getWrapInsn()); } if (str == null) { return null; } if (ifInsn.getOp() == IfOp.NE && ifInsn.getArg(1).isTrue()) { neg = true; } if (ifInsn.getOp() == IfOp.EQ && ifInsn.getArg(1).isFalse()) { neg = true; } switchData.getToRemove().add(ifInsn); switchData.getToRemove().addAll(ifRegion.getConditionBlocks()); CaseData caseData = new CaseData(); caseData.getStrValues().add(str); IContainer codeContainer = neg ? ifRegion.getElseRegion() : ifRegion.getThenRegion(); if (codeContainer == null) { // no code // use last condition block for later data tracing caseData.setBlockRef(Utils.last(ifRegion.getConditionBlocks())); } else { caseData.setCode(codeContainer); } return caseData; } private @Nullable RegisterArg getStrHashCodeArg(InsnArg arg) { if (arg.isRegister()) { return getStrFromInsn(((RegisterArg) arg).getAssignInsn()); } if (arg.isInsnWrap()) { return getStrFromInsn(((InsnWrapArg) arg).getWrapInsn()); } return null; } private @Nullable RegisterArg getStrFromInsn(@Nullable InsnNode insn) { if (insn == null || insn.getType() != InsnType.INVOKE) { return null; } InvokeNode invInsn = (InvokeNode) insn; MethodInfo callMth = invInsn.getCallMth(); if (!callMth.getRawFullId().equals("java.lang.String.hashCode()I")) { return null; } InsnArg arg = invInsn.getInstanceArg(); if (arg == null || !arg.isRegister()) { return null; } return (RegisterArg) arg; } private static final class SwitchData { private final MethodNode mth; private final SwitchRegion switchRegion; private final List toRemove = new ArrayList<>(); private Map strEqInsns; private List cases; private List newCases; private SwitchRegion codeSwitch; private RegisterArg numArg; private SwitchData(MethodNode mth, SwitchRegion switchRegion) { this.mth = mth; this.switchRegion = switchRegion; } public List getCases() { return cases; } public void setCases(List cases) { this.cases = cases; } public List getNewCases() { return newCases; } public void setNewCases(List cases) { this.newCases = cases; } public MethodNode getMth() { return mth; } public Map getStrEqInsns() { return strEqInsns; } public void setStrEqInsns(Map strEqInsns) { this.strEqInsns = strEqInsns; } public SwitchRegion getSwitchRegion() { return switchRegion; } public List getToRemove() { return toRemove; } public SwitchRegion getCodeSwitch() { return codeSwitch; } public void setCodeSwitch(SwitchRegion codeSwitch) { this.codeSwitch = codeSwitch; } public RegisterArg getNumArg() { return numArg; } public void setNumArg(RegisterArg numArg) { this.numArg = numArg; } } private static final class CaseData { private final List strValues = new ArrayList<>(); private @Nullable IContainer code = null; private @Nullable BlockNode blockRef = null; private int codeNum = -1; public List getStrValues() { return strValues; } public @Nullable IContainer getCode() { return code; } public void setCode(@Nullable IContainer code) { this.code = code; } public @Nullable BlockNode getBlockRef() { return blockRef; } public void setBlockRef(@Nullable BlockNode blockRef) { this.blockRef = blockRef; } public int getCodeNum() { return codeNum; } public void setCodeNum(int codeNum) { this.codeNum = codeNum; } @Override public String toString() { return "CaseData{" + strValues + '}'; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/TernaryMod.java ================================================ package jadx.core.dex.visitors.regions; import java.util.HashMap; import java.util.List; import java.util.Map; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.conditions.IfRegion; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.InsnList; import jadx.core.utils.InsnRemover; /** * Convert 'if' to ternary operation */ public class TernaryMod extends AbstractRegionVisitor implements IRegionIterativeVisitor { private static final TernaryMod INSTANCE = new TernaryMod(); public static void process(MethodNode mth) { // first: convert all found ternary nodes in one iteration DepthRegionTraversal.traverse(mth, INSTANCE); if (mth.contains(AFlag.REQUEST_CODE_SHRINK)) { CodeShrinkVisitor.shrinkMethod(mth); } // second: iterative runs with shrink after each change DepthRegionTraversal.traverseIterative(mth, INSTANCE); } @Override public boolean enterRegion(MethodNode mth, IRegion region) { if (processRegion(mth, region)) { mth.add(AFlag.REQUEST_CODE_SHRINK); } return true; } @Override public boolean visitRegion(MethodNode mth, IRegion region) { if (processRegion(mth, region)) { CodeShrinkVisitor.shrinkMethod(mth); return true; } return false; } private static boolean processRegion(MethodNode mth, IRegion region) { if (region instanceof IfRegion) { return makeTernaryInsn(mth, (IfRegion) region); } return false; } private static boolean makeTernaryInsn(MethodNode mth, IfRegion ifRegion) { if (ifRegion.contains(AFlag.ELSE_IF_CHAIN)) { return false; } IContainer thenRegion = ifRegion.getThenRegion(); IContainer elseRegion = ifRegion.getElseRegion(); if (thenRegion == null) { return false; } if (elseRegion == null) { return processOneBranchTernary(mth, ifRegion); } BlockNode tb = getTernaryInsnBlock(thenRegion); BlockNode eb = getTernaryInsnBlock(elseRegion); if (tb == null || eb == null) { return false; } List conditionBlocks = ifRegion.getConditionBlocks(); if (conditionBlocks.isEmpty()) { return false; } BlockNode header = conditionBlocks.get(0); InsnNode thenInsn = tb.getInstructions().get(0); InsnNode elseInsn = eb.getInstructions().get(0); if (!verifyLineHints(mth, thenInsn, elseInsn)) { return false; } RegisterArg thenResArg = thenInsn.getResult(); RegisterArg elseResArg = elseInsn.getResult(); if (thenResArg != null && elseResArg != null) { PhiInsn thenPhi = thenResArg.getSVar().getOnlyOneUseInPhi(); PhiInsn elsePhi = elseResArg.getSVar().getOnlyOneUseInPhi(); if (thenPhi == null || thenPhi != elsePhi) { return false; } if (!ifRegion.getParent().replaceSubBlock(ifRegion, header)) { return false; } InsnList.remove(tb, thenInsn); InsnList.remove(eb, elseInsn); RegisterArg resArg; if (thenPhi.getArgsCount() == 2) { resArg = thenPhi.getResult(); InsnRemover.unbindResult(mth, thenInsn); } else { resArg = thenResArg; thenPhi.removeArg(elseResArg); } InsnArg thenArg = InsnArg.wrapInsnIntoArg(thenInsn); InsnArg elseArg = InsnArg.wrapInsnIntoArg(elseInsn); TernaryInsn ternInsn = new TernaryInsn(ifRegion.getCondition(), resArg, thenArg, elseArg); int branchLine = Math.max(thenInsn.getSourceLine(), elseInsn.getSourceLine()); ternInsn.setSourceLine(Math.max(ifRegion.getSourceLine(), branchLine)); thenInsn.setResult(null); // unset without unbind, SSA var still in use InsnRemover.unbindResult(mth, elseInsn); // remove 'if' instruction header.getInstructions().clear(); ternInsn.rebindArgs(); header.getInstructions().add(ternInsn); clearConditionBlocks(conditionBlocks, header); return true; } if (!mth.isVoidReturn() && thenInsn.getType() == InsnType.RETURN && elseInsn.getType() == InsnType.RETURN) { InsnArg thenArg = thenInsn.getArg(0); InsnArg elseArg = elseInsn.getArg(0); if (thenArg.isLiteral() != elseArg.isLiteral()) { // one arg is literal return false; } if (!ifRegion.getParent().replaceSubBlock(ifRegion, header)) { return false; } InsnList.remove(tb, thenInsn); InsnList.remove(eb, elseInsn); tb.remove(AFlag.RETURN); eb.remove(AFlag.RETURN); TernaryInsn ternInsn = new TernaryInsn(ifRegion.getCondition(), null, thenArg, elseArg); InsnNode retInsn = new InsnNode(InsnType.RETURN, 1); InsnArg arg = InsnArg.wrapInsnIntoArg(ternInsn); arg.setType(thenArg.getType()); retInsn.addArg(arg); header.getInstructions().clear(); retInsn.rebindArgs(); header.getInstructions().add(retInsn); header.add(AFlag.RETURN); clearConditionBlocks(conditionBlocks, header); return true; } return false; } private static boolean verifyLineHints(MethodNode mth, InsnNode thenInsn, InsnNode elseInsn) { if (mth.contains(AFlag.USE_LINES_HINTS) && thenInsn.getSourceLine() != elseInsn.getSourceLine()) { if (thenInsn.getSourceLine() != 0 && elseInsn.getSourceLine() != 0) { // sometimes source lines incorrect return checkLineStats(thenInsn, elseInsn); } // don't make nested ternary by default // TODO: add addition checks return !containsTernary(thenInsn) && !containsTernary(elseInsn); } return true; } private static void clearConditionBlocks(List conditionBlocks, BlockNode header) { for (BlockNode block : conditionBlocks) { if (block != header) { block.getInstructions().clear(); block.add(AFlag.REMOVE); } } } private static BlockNode getTernaryInsnBlock(IContainer thenRegion) { if (thenRegion instanceof Region) { Region r = (Region) thenRegion; if (r.getSubBlocks().size() == 1) { IContainer container = r.getSubBlocks().get(0); if (container instanceof BlockNode) { BlockNode block = (BlockNode) container; if (block.getInstructions().size() == 1) { return block; } } } } return null; } private static boolean containsTernary(InsnNode insn) { if (insn.getType() == InsnType.TERNARY) { return true; } for (int i = 0; i < insn.getArgsCount(); i++) { InsnArg arg = insn.getArg(i); if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); if (containsTernary(wrapInsn)) { return true; } } } return false; } /** * Return 'true' if there are several args with same source lines */ private static boolean checkLineStats(InsnNode t, InsnNode e) { if (t.getResult() == null || e.getResult() == null) { return false; } PhiInsn tPhi = t.getResult().getSVar().getOnlyOneUseInPhi(); PhiInsn ePhi = e.getResult().getSVar().getOnlyOneUseInPhi(); if (ePhi == null || tPhi != ePhi) { return false; } Map map = new HashMap<>(tPhi.getArgsCount()); for (InsnArg arg : tPhi.getArguments()) { if (!arg.isRegister()) { continue; } InsnNode assignInsn = ((RegisterArg) arg).getAssignInsn(); if (assignInsn == null) { continue; } int sourceLine = assignInsn.getSourceLine(); if (sourceLine != 0) { map.merge(sourceLine, 1, Integer::sum); } } for (Map.Entry entry : map.entrySet()) { if (entry.getValue() >= 2) { return true; } } return false; } /** * Convert one variable change with only 'then' branch: * 'if (c) {r = a;}' to 'r = c ? a : r' * Convert if 'r' used only once */ private static boolean processOneBranchTernary(MethodNode mth, IfRegion ifRegion) { IContainer thenRegion = ifRegion.getThenRegion(); BlockNode block = getTernaryInsnBlock(thenRegion); if (block != null) { InsnNode insn = block.getInstructions().get(0); RegisterArg result = insn.getResult(); if (result != null) { replaceWithTernary(mth, ifRegion, block, insn); } } return false; } @SuppressWarnings("StatementWithEmptyBody") private static void replaceWithTernary(MethodNode mth, IfRegion ifRegion, BlockNode block, InsnNode insn) { RegisterArg resArg = insn.getResult(); if (resArg.getSVar().getUseList().size() != 1) { return; } PhiInsn phiInsn = resArg.getSVar().getOnlyOneUseInPhi(); if (phiInsn == null || phiInsn.getArgsCount() != 2) { return; } RegisterArg otherArg = null; for (InsnArg arg : phiInsn.getArguments()) { if (!resArg.sameRegAndSVar(arg)) { otherArg = (RegisterArg) arg; break; } } if (otherArg == null) { return; } InsnNode elseAssign = otherArg.getAssignInsn(); if (mth.isConstructor() || (mth.getParentClass().isEnum() && mth.getMethodInfo().isClassInit())) { // forcing ternary inline for constructors (will help in moving super call to the top) and enums // skip code style checks } else { if (elseAssign != null && elseAssign.isConstInsn()) { if (!verifyLineHints(mth, insn, elseAssign)) { return; } } else { if (insn.getResult().sameCodeVar(otherArg)) { // don't use same variable in else branch to prevent: l = (l == 0) ? 1 : l return; } } } // all checks passed BlockNode header = ifRegion.getConditionBlocks().get(0); if (!ifRegion.getParent().replaceSubBlock(ifRegion, header)) { return; } InsnArg elseArg; if (elseAssign != null && elseAssign.isConstInsn()) { // inline constant SSAVar elseVar = elseAssign.getResult().getSVar(); if (elseVar.getUseCount() == 1 && elseVar.getOnlyOneUseInPhi() == phiInsn) { InsnRemover.remove(mth, elseAssign); } elseArg = InsnArg.wrapInsnIntoArg(elseAssign); } else { elseArg = otherArg.duplicate(); } InsnArg thenArg = InsnArg.wrapInsnIntoArg(insn); RegisterArg resultArg = phiInsn.getResult().duplicate(); TernaryInsn ternInsn = new TernaryInsn(ifRegion.getCondition(), resultArg, thenArg, elseArg); ternInsn.simplifyCondition(); InsnRemover.unbindAllArgs(mth, phiInsn); InsnRemover.delistPhi(mth, phiInsn); InsnRemover.unbindResult(mth, insn); InsnList.remove(block, insn); header.getInstructions().clear(); ternInsn.rebindArgs(); header.getInstructions().add(ternInsn); clearConditionBlocks(ifRegion.getConditionBlocks(), header); // shrink method again CodeShrinkVisitor.shrinkMethod(mth); } private TernaryMod() { } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/TracedRegionVisitor.java ================================================ package jadx.core.dex.visitors.regions; import java.util.ArrayDeque; import java.util.Deque; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; public abstract class TracedRegionVisitor implements IRegionVisitor { protected final Deque regionStack = new ArrayDeque<>(); @Override public boolean enterRegion(MethodNode mth, IRegion region) { regionStack.push(region); return true; } @Override public void processBlock(MethodNode mth, IBlock block) { IRegion curRegion = regionStack.peek(); processBlockTraced(mth, block, curRegion); } public abstract void processBlockTraced(MethodNode mth, IBlock block, IRegion parentRegion); @Override public void leaveRegion(MethodNode mth, IRegion region) { regionStack.pop(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/ExcHandlersRegionMaker.java ================================================ package jadx.core.dex.visitors.regions.maker; import java.util.ArrayList; import java.util.BitSet; import java.util.HashSet; import java.util.List; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.trycatch.TryCatchBlockAttr; import jadx.core.utils.BlockUtils; import jadx.core.utils.RegionUtils; public class ExcHandlersRegionMaker { private final MethodNode mth; private final RegionMaker regionMaker; public ExcHandlersRegionMaker(MethodNode mth, RegionMaker regionMaker) { this.mth = mth; this.regionMaker = regionMaker; } public void process() { if (mth.isNoExceptionHandlers()) { return; } IRegion excOutBlock = collectHandlerRegions(); if (excOutBlock != null) { mth.getRegion().add(excOutBlock); } } private @Nullable IRegion collectHandlerRegions() { List tcs = mth.getAll(AType.TRY_BLOCKS_LIST); for (TryCatchBlockAttr tc : tcs) { List blocks = new ArrayList<>(tc.getHandlersCount()); Set splitters = new HashSet<>(); for (ExceptionHandler handler : tc.getHandlers()) { BlockNode handlerBlock = handler.getHandlerBlock(); if (handlerBlock != null) { blocks.add(handlerBlock); splitters.add(BlockUtils.getTopSplitterForHandler(handlerBlock)); } else { mth.addDebugComment("No exception handler block: " + handler); } } Set exits = new HashSet<>(); for (BlockNode splitter : splitters) { for (BlockNode handler : blocks) { if (handler.contains(AFlag.REMOVE)) { continue; } List s = splitter.getSuccessors(); if (s.isEmpty()) { mth.addDebugComment("No successors for splitter: " + splitter); continue; } BlockNode ss = s.get(0); BlockNode cross = BlockUtils.getPathCross(mth, ss, handler); if (cross != null && cross != ss && cross != handler) { exits.add(cross); } } } for (ExceptionHandler handler : tc.getHandlers()) { processExcHandler(handler, exits); } } return processHandlersOutBlocks(tcs); } /** * Search handlers successor blocks aren't included in any region. */ private @Nullable IRegion processHandlersOutBlocks(List tcs) { Set allRegionBlocks = new HashSet<>(); RegionUtils.getAllRegionBlocks(mth.getRegion(), allRegionBlocks); Set successorBlocks = new HashSet<>(); for (TryCatchBlockAttr tc : tcs) { for (ExceptionHandler handler : tc.getHandlers()) { IContainer region = handler.getHandlerRegion(); if (region != null) { IBlock lastBlock = RegionUtils.getLastBlock(region); if (lastBlock instanceof BlockNode) { successorBlocks.addAll(((BlockNode) lastBlock).getSuccessors()); } RegionUtils.getAllRegionBlocks(region, allRegionBlocks); } } } successorBlocks.removeAll(allRegionBlocks); if (successorBlocks.isEmpty()) { return null; } RegionStack stack = regionMaker.getStack(); Region excOutRegion = new Region(mth.getRegion()); for (IBlock block : successorBlocks) { if (block instanceof BlockNode) { stack.clear(); stack.push(excOutRegion); excOutRegion.add(regionMaker.makeRegion((BlockNode) block)); } } return excOutRegion; } private void processExcHandler(ExceptionHandler handler, Set exits) { BlockNode start = handler.getHandlerBlock(); if (start == null) { return; } RegionStack stack = regionMaker.getStack().clear(); BlockNode dom; if (handler.isFinally()) { dom = BlockUtils.getTopSplitterForHandler(start); } else { dom = start; stack.addExits(exits); } if (dom.contains(AFlag.REMOVE)) { return; } List handlerExits = new ArrayList<>(); BlockNode handlerOutBlock = BlockUtils.getTryAndHandlerCrossBlock(mth, handler); if (handlerOutBlock != null) { // ensure frontier's other predecessors comes from try end handlerExits.add(handlerOutBlock); } else { // fallback to simple frontier BitSet domFrontier = dom.getDomFrontier(); handlerExits.addAll(BlockUtils.bitSetToBlocks(mth, domFrontier)); } boolean inLoop = mth.getLoopForBlock(start) != null; for (BlockNode exit : handlerExits) { if ((!inLoop || BlockUtils.isPathExists(start, exit)) && RegionUtils.isRegionContainsBlock(mth.getRegion(), exit)) { stack.addExit(exit); } } handler.setHandlerRegion(regionMaker.makeRegion(start)); ExcHandlerAttr excHandlerAttr = start.get(AType.EXC_HANDLER); if (excHandlerAttr == null) { mth.addWarn("Missing exception handler attribute for start block: " + start); } else { handler.getHandlerRegion().addAttr(excHandlerAttr); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/IfRegionMaker.java ================================================ package jadx.core.dex.visitors.regions.maker; import java.util.ArrayList; import java.util.BitSet; import java.util.List; import java.util.Objects; import java.util.Set; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.EdgeInsnAttr; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnContainer; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.regions.conditions.IfInfo; import jadx.core.dex.regions.conditions.IfRegion; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.utils.BlockUtils; import jadx.core.utils.blocks.BlockSet; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.BlockUtils.bitSetToBlocks; import static jadx.core.utils.BlockUtils.bitSetToOneBlock; import static jadx.core.utils.BlockUtils.followEmptyPath; import static jadx.core.utils.BlockUtils.getBottomBlock; import static jadx.core.utils.BlockUtils.getPathCross; import static jadx.core.utils.BlockUtils.isEqualPaths; import static jadx.core.utils.BlockUtils.isEqualReturnBlocks; import static jadx.core.utils.BlockUtils.isPathExists; import static jadx.core.utils.BlockUtils.newBlocksBitSet; final class IfRegionMaker { private static final Logger LOG = LoggerFactory.getLogger(IfRegionMaker.class); private final MethodNode mth; private final RegionMaker regionMaker; IfRegionMaker(MethodNode mth, RegionMaker regionMaker) { this.mth = mth; this.regionMaker = regionMaker; } BlockNode process(IRegion currentRegion, BlockNode block, IfNode ifnode, RegionStack stack) { if (block.contains(AFlag.ADDED_TO_REGION)) { // block already included in other 'if' region return ifnode.getThenBlock(); } IfInfo currentIf = makeIfInfo(mth, block); if (currentIf == null) { return null; } IfInfo mergedIf = mergeNestedIfNodes(currentIf); if (mergedIf != null) { currentIf = mergedIf; } else { // invert simple condition (compiler often do it) // ensure that we only ever invert once, because if multiple regions contain this block // we'll change the block after it's already been included in a region, which can cause // other regions containing the block to believe the condition has been flipped when it // has not, or vice versa. if (!block.contains(AFlag.DONT_INVERT)) { currentIf = IfInfo.invert(currentIf); block.add(AFlag.DONT_INVERT); } } IfInfo modifiedIf = restructureIf(mth, block, currentIf); if (modifiedIf != null) { currentIf = modifiedIf; } else { if (currentIf.getMergedBlocks().size() <= 1) { return null; } currentIf = makeIfInfo(mth, block); currentIf = restructureIf(mth, block, currentIf); if (currentIf == null) { // all attempts failed return null; } } confirmMerge(currentIf); IfRegion ifRegion = new IfRegion(currentRegion); ifRegion.updateCondition(currentIf); currentRegion.getSubBlocks().add(ifRegion); BlockNode outBlock = currentIf.getOutBlock(); stack.push(ifRegion); stack.addExit(outBlock); BlockNode thenBlock = currentIf.getThenBlock(); if (thenBlock == null) { // empty then block, not normal, but maybe correct ifRegion.setThenRegion(new Region(ifRegion)); } else { ifRegion.setThenRegion(regionMaker.makeRegion(thenBlock)); } BlockNode elseBlock = currentIf.getElseBlock(); if (elseBlock == null || stack.containsExit(elseBlock)) { ifRegion.setElseRegion(null); } else { ifRegion.setElseRegion(regionMaker.makeRegion(elseBlock)); } // insert edge insns in new 'else' branch if (ifRegion.getElseRegion() == null && outBlock != null) { List edgeInsnAttrs = outBlock.getAll(AType.EDGE_INSN); if (!edgeInsnAttrs.isEmpty()) { List instructions = new ArrayList<>(); for (EdgeInsnAttr edgeInsnAttr : edgeInsnAttrs) { if (edgeInsnAttr.getEnd().equals(outBlock)) { if (currentIf.getMergedBlocks().contains(followEmptyPath(edgeInsnAttr.getStart(), true))) { instructions.add(edgeInsnAttr.getInsn()); } } } if (!instructions.isEmpty()) { Region elseRegion = new Region(ifRegion); InsnContainer newBlock = new InsnContainer(instructions); elseRegion.add(newBlock); ifRegion.setElseRegion(elseRegion); } } } stack.pop(); return outBlock; } @NotNull IfInfo buildIfInfo(LoopRegion loopRegion) { IfInfo condInfo = makeIfInfo(mth, loopRegion.getHeader()); condInfo = searchNestedIf(condInfo); confirmMerge(condInfo); return condInfo; } @Nullable static IfInfo makeIfInfo(MethodNode mth, BlockNode ifBlock) { InsnNode lastInsn = BlockUtils.getLastInsn(ifBlock); if (lastInsn == null || lastInsn.getType() != InsnType.IF) { return null; } IfNode ifNode = (IfNode) lastInsn; IfCondition condition = IfCondition.fromIfNode(ifNode); IfInfo info = new IfInfo(mth, condition, ifNode.getThenBlock(), ifNode.getElseBlock()); info.getMergedBlocks().add(ifBlock); return info; } static IfInfo searchNestedIf(IfInfo info) { IfInfo next = mergeNestedIfNodes(info); if (next != null) { return next; } return info; } static IfInfo restructureIf(MethodNode mth, BlockNode block, IfInfo info) { BlockNode thenBlock = info.getThenBlock(); BlockNode elseBlock = info.getElseBlock(); if (Objects.equals(thenBlock, elseBlock)) { IfInfo ifInfo = new IfInfo(info, null, null); ifInfo.setOutBlock(thenBlock); return ifInfo; } // select 'then', 'else' and 'exit' blocks if (thenBlock.contains(AFlag.RETURN) && elseBlock.contains(AFlag.RETURN)) { info.setOutBlock(null); return info; } // init outblock, which will be used in isBadBranchBlock to compare with branch block info.setOutBlock(findOutBlock(mth, thenBlock, elseBlock)); boolean badThen = isBadBranchBlock(info, thenBlock); boolean badElse = isBadBranchBlock(info, elseBlock); if (badThen && badElse) { if (Consts.DEBUG_RESTRUCTURE) { LOG.debug("Stop processing blocks after 'if': {}, method: {}", info.getMergedBlocks(), mth); } return null; } if (badElse) { info = new IfInfo(info, thenBlock, null); info.setOutBlock(elseBlock); } else if (badThen) { info = IfInfo.invert(info); info = new IfInfo(info, elseBlock, null); info.setOutBlock(thenBlock); } // getPathCross may not find outBlock (e.g. one branch has return, outBlock definitely is // null), so should check further if (info.getOutBlock() == null) { BlockNode scopeOutBlockThen = findScopeOutBlock(mth, info.getThenBlock()); BlockNode scopeOutBlockElse = findScopeOutBlock(mth, info.getElseBlock()); if (scopeOutBlockThen == null && scopeOutBlockElse != null) { info.setOutBlock(scopeOutBlockElse); } else if (scopeOutBlockThen != null && scopeOutBlockElse == null) { info.setOutBlock(scopeOutBlockThen); } else if (scopeOutBlockThen != null && scopeOutBlockThen == scopeOutBlockElse) { info.setOutBlock(scopeOutBlockThen); } } if (BlockUtils.isBackEdge(block, info.getOutBlock())) { info.setOutBlock(null); } return info; } static BlockNode findOutBlock(MethodNode mth, BlockNode thenBlock, BlockNode elseBlock) { if (thenBlock == elseBlock) { return thenBlock; } if (thenBlock == null || elseBlock == null) { return null; } BitSet thenDomFrontier = newBlocksBitSet(mth); thenDomFrontier.or(thenBlock.getDomFrontier()); thenDomFrontier.set(thenBlock.getPos()); BitSet elseDomFrontier = newBlocksBitSet(mth); elseDomFrontier.or(elseBlock.getDomFrontier()); elseDomFrontier.set(elseBlock.getPos()); BitSet intersection = newBlocksBitSet(mth); intersection.or(thenDomFrontier); intersection.and(elseDomFrontier); intersection.clear(mth.getExitBlock().getPos()); BlockNode oneBlock = bitSetToOneBlock(mth, intersection); // Attempt one: there's a unique block in the intersection of dom frontiers, and no path from // then->else or else->then if (oneBlock != null) { return oneBlock; } BitSet union = newBlocksBitSet(mth); union.or(thenBlock.getDomFrontier()); union.or(elseBlock.getDomFrontier()); union.clear(mth.getExitBlock().getPos()); // Attempt two: look for a suitable block in the union. BitSet candidates = newBlocksBitSet(mth); for (BlockNode candidate : bitSetToBlocks(mth, union)) { if (isCandidateForOutBlock(mth, thenBlock, elseBlock, candidate)) { candidates.set(candidate.getPos()); } } BlockNode bottom = getBottomBlock(bitSetToBlocks(mth, candidates), true); if (bottom != null) { return bottom; } // Attempt three: fallback to path cross again return getPathCross(mth, thenBlock, elseBlock); } static boolean isCandidateForOutBlock(MethodNode mth, BlockNode thenBlock, BlockNode elseBlock, BlockNode candidate) { // a candidate block requires: // - >1 predecessor // - each predecessor has a clean path from elseBlock or thenBlock, and there exist predecessors // covering both cases // - inside the union of the two dom frontiers if (candidate.getPredecessors().size() < 2) { return false; // block has only one pred, and so can't be the outblock } BitSet coverageThenPreds = newBlocksBitSet(mth); BitSet coverageElsePreds = newBlocksBitSet(mth); if (candidate == elseBlock) { coverageElsePreds.set(candidate.getPos()); } if (candidate == thenBlock) { coverageThenPreds.set(candidate.getPos()); } for (BlockNode pred : candidate.getPredecessors()) { if (isPathExists(thenBlock, pred)) { coverageThenPreds.set(pred.getPos()); } if (isPathExists(elseBlock, pred)) { coverageElsePreds.set(pred.getPos()); } } if (coverageElsePreds.cardinality() == 0 || coverageThenPreds.cardinality() == 0) { return false; // block has no path to both the then and else blocks } BlockNode coverageElsePred = bitSetToOneBlock(mth, coverageElsePreds); BlockNode coverageThenPred = bitSetToOneBlock(mth, coverageThenPreds); if (coverageElsePred != null && coverageElsePred == coverageThenPred) { return false; // the only paths from else and then go through the same block } return true; } private static boolean isBadBranchBlock(IfInfo info, BlockNode block) { // check if block at end of loop edge if (block.contains(AFlag.LOOP_START) && block.getPredecessors().size() == 1) { BlockNode pred = block.getPredecessors().get(0); if (pred.contains(AFlag.LOOP_END)) { List startLoops = block.getAll(AType.LOOP); List endLoops = pred.getAll(AType.LOOP); // search for same loop for (LoopInfo startLoop : startLoops) { for (LoopInfo endLoop : endLoops) { if (startLoop == endLoop) { return true; } } } } } // if branch block itself is outblock if (info.getOutBlock() != null) { return block == info.getOutBlock(); } return !allPathsFromIf(block, info); } private static boolean allPathsFromIf(BlockNode block, IfInfo info) { List preds = block.getPredecessors(); BlockSet ifBlocks = info.getMergedBlocks(); for (BlockNode pred : preds) { if (pred.contains(AFlag.LOOP_END)) { // ignore loop back edge continue; } BlockNode top = BlockUtils.skipSyntheticPredecessor(pred); if (!ifBlocks.contains(top)) { return false; } } return true; } /** * if startBlock is in a (try) scope, find the scope end as outBlock */ private @Nullable static BlockNode findScopeOutBlock(MethodNode mth, BlockNode startBlock) { if (startBlock == null) { return null; } List domFrontiers = BlockUtils.bitSetToBlocks(mth, startBlock.getDomFrontier()); BlockNode scopeOutBlock = null; // find handler from domFrontier(could be scope end), if domFrontier is handler // and its topSplitter dominates branch block, then branch should end for (BlockNode domFrontier : domFrontiers) { ExcHandlerAttr handler = domFrontier.get(AType.EXC_HANDLER); if (handler == null) { continue; } BlockNode topSplitter = handler.getTryBlock().getTopSplitter(); if (startBlock.isDominator(topSplitter)) { scopeOutBlock = BlockUtils.getTryAndHandlerCrossBlock(mth, handler.getHandler()); break; } } return scopeOutBlock; } static IfInfo mergeNestedIfNodes(IfInfo currentIf) { BlockNode curThen = currentIf.getThenBlock(); BlockNode curElse = currentIf.getElseBlock(); if (curThen == curElse) { return null; } if (BlockUtils.isFollowBackEdge(curThen) || BlockUtils.isFollowBackEdge(curElse)) { return null; } boolean followThenBranch; IfInfo nextIf = getNextIf(currentIf, curThen); if (nextIf != null) { followThenBranch = true; } else { nextIf = getNextIf(currentIf, curElse); if (nextIf != null) { followThenBranch = false; } else { return null; } } boolean assignInlineNeeded = !nextIf.getForceInlineInsns().isEmpty(); if (assignInlineNeeded) { for (BlockNode mergedBlock : currentIf.getMergedBlocks()) { if (mergedBlock.contains(AFlag.LOOP_START)) { // don't inline assigns into loop condition return currentIf; } } } if (isInversionNeeded(currentIf, nextIf)) { // invert current node for match pattern nextIf = IfInfo.invert(nextIf); } boolean thenPathSame = isEqualPaths(curThen, nextIf.getThenBlock()); boolean elsePathSame = isEqualPaths(curElse, nextIf.getElseBlock()); if (!thenPathSame && !elsePathSame) { // complex condition, run additional checks if (checkConditionBranches(curThen, curElse) || checkConditionBranches(curElse, curThen)) { return null; } BlockNode otherBranchBlock = followThenBranch ? curElse : curThen; otherBranchBlock = BlockUtils.followEmptyPath(otherBranchBlock); if (!isPathExists(nextIf.getFirstIfBlock(), otherBranchBlock)) { return checkForTernaryInCondition(currentIf); } // this is nested conditions with different mode (i.e (a && b) || c), // search next condition for merge, get null if failed IfInfo tmpIf = mergeNestedIfNodes(nextIf); if (tmpIf != null) { nextIf = tmpIf; if (isInversionNeeded(currentIf, nextIf)) { nextIf = IfInfo.invert(nextIf); } if (!canMerge(currentIf, nextIf, followThenBranch)) { return currentIf; } } else { return currentIf; } } else { if (assignInlineNeeded) { boolean sameOuts = (thenPathSame && !followThenBranch) || (elsePathSame && followThenBranch); if (!sameOuts) { // don't inline assigns inside simple condition currentIf.resetForceInlineInsns(); return currentIf; } } } IfInfo result = mergeIfInfo(currentIf, nextIf, followThenBranch); // search next nested if block return searchNestedIf(result); } private static IfInfo checkForTernaryInCondition(IfInfo currentIf) { IfInfo nextThen = getNextIf(currentIf, currentIf.getThenBlock()); IfInfo nextElse = getNextIf(currentIf, currentIf.getElseBlock()); if (nextThen == null || nextElse == null) { return null; } if (!nextThen.getFirstIfBlock().getDomFrontier().equals(nextElse.getFirstIfBlock().getDomFrontier())) { return null; } nextThen = searchNestedIf(nextThen); nextElse = searchNestedIf(nextElse); if (nextThen.getThenBlock() == nextElse.getThenBlock() && nextThen.getElseBlock() == nextElse.getElseBlock()) { return mergeTernaryConditions(currentIf, nextThen, nextElse); } if (nextThen.getThenBlock() == nextElse.getElseBlock() && nextThen.getElseBlock() == nextElse.getThenBlock()) { nextElse = IfInfo.invert(nextElse); return mergeTernaryConditions(currentIf, nextThen, nextElse); } return null; } private static IfInfo mergeTernaryConditions(IfInfo currentIf, IfInfo nextThen, IfInfo nextElse) { IfCondition newCondition = IfCondition.ternary(currentIf.getCondition(), nextThen.getCondition(), nextElse.getCondition()); IfInfo result = new IfInfo(currentIf.getMth(), newCondition, nextThen.getThenBlock(), nextThen.getElseBlock()); result.merge(currentIf, nextThen, nextElse); confirmMerge(result); return result; } private static boolean isInversionNeeded(IfInfo currentIf, IfInfo nextIf) { return isEqualPaths(currentIf.getElseBlock(), nextIf.getThenBlock()) || isEqualPaths(currentIf.getThenBlock(), nextIf.getElseBlock()); } private static boolean canMerge(IfInfo a, IfInfo b, boolean followThenBranch) { if (followThenBranch) { return isEqualPaths(a.getElseBlock(), b.getElseBlock()); } else { return isEqualPaths(a.getThenBlock(), b.getThenBlock()); } } private static boolean checkConditionBranches(BlockNode from, BlockNode to) { return from.getCleanSuccessors().size() == 1 && from.getCleanSuccessors().contains(to); } static IfInfo mergeIfInfo(IfInfo first, IfInfo second, boolean followThenBranch) { MethodNode mth = first.getMth(); Set skipBlocks = first.getSkipBlocks(); BlockNode thenBlock; BlockNode elseBlock; if (followThenBranch) { thenBlock = second.getThenBlock(); elseBlock = getBranchBlock(first.getElseBlock(), second.getElseBlock(), skipBlocks, mth); } else { thenBlock = getBranchBlock(first.getThenBlock(), second.getThenBlock(), skipBlocks, mth); elseBlock = second.getElseBlock(); } IfCondition.Mode mergeOperation = followThenBranch ? IfCondition.Mode.AND : IfCondition.Mode.OR; IfCondition condition = IfCondition.merge(mergeOperation, first.getCondition(), second.getCondition()); IfInfo result = new IfInfo(mth, condition, thenBlock, elseBlock); result.merge(first, second); return result; } private static BlockNode getBranchBlock(BlockNode first, BlockNode second, Set skipBlocks, MethodNode mth) { if (first == second) { return second; } if (isEqualReturnBlocks(first, second)) { skipBlocks.add(first); return second; } if (BlockUtils.isDuplicateBlockPath(first, second)) { first.add(AFlag.REMOVE); skipBlocks.add(first); return second; } BlockNode cross = BlockUtils.getPathCross(mth, first, second); if (cross != null) { BlockUtils.visitBlocksOnPath(mth, first, cross, skipBlocks::add); BlockUtils.visitBlocksOnPath(mth, second, cross, skipBlocks::add); skipBlocks.remove(cross); return cross; } BlockNode firstSkip = BlockUtils.followEmptyPath(first); BlockNode secondSkip = BlockUtils.followEmptyPath(second); if (firstSkip.equals(secondSkip) || isEqualReturnBlocks(firstSkip, secondSkip)) { skipBlocks.add(first); skipBlocks.add(second); BlockUtils.visitBlocksOnEmptyPath(first, skipBlocks::add); BlockUtils.visitBlocksOnEmptyPath(second, skipBlocks::add); return secondSkip; } throw new JadxRuntimeException("Unexpected merge pattern"); } static void confirmMerge(IfInfo info) { if (info.getMergedBlocks().size() > 1) { for (BlockNode block : info.getMergedBlocks()) { if (block != info.getFirstIfBlock()) { block.add(AFlag.ADDED_TO_REGION); } } } if (!info.getSkipBlocks().isEmpty()) { for (BlockNode block : info.getSkipBlocks()) { block.add(AFlag.ADDED_TO_REGION); } info.getSkipBlocks().clear(); } for (InsnNode forceInlineInsn : info.getForceInlineInsns()) { forceInlineInsn.add(AFlag.FORCE_ASSIGN_INLINE); } } private static IfInfo getNextIf(IfInfo info, BlockNode block) { if (!canSelectNext(info, block)) { return null; } return getNextIfNodeInfo(info, block); } private static boolean canSelectNext(IfInfo info, BlockNode block) { if (block.getPredecessors().size() == 1) { return true; } return info.getMergedBlocks().containsAll(block.getPredecessors()); } private static IfInfo getNextIfNodeInfo(IfInfo info, BlockNode block) { if (block == null || block.contains(AType.LOOP) || block.contains(AFlag.ADDED_TO_REGION)) { return null; } InsnNode lastInsn = BlockUtils.getLastInsn(block); if (lastInsn != null && lastInsn.getType() == InsnType.IF) { return makeIfInfo(info.getMth(), block); } BlockNode next = getNextBlockInIfSuccessorChain(block); if (next == null) { return null; } if (next.getPredecessors().size() != 1 || next.contains(AFlag.ADDED_TO_REGION)) { return null; } List forceInlineInsns = new ArrayList<>(); if (!checkInsnsInline(block, next, forceInlineInsns)) { return null; } IfInfo nextInfo = makeIfInfo(info.getMth(), next); if (nextInfo == null) { return getNextIfNodeInfo(info, next); } nextInfo.addInsnsForForcedInline(forceInlineInsns); return nextInfo; } /** * Allow singular successor to block or 2 successors where one is a EXC_BOTTOM_SPLITTER */ private static @Nullable BlockNode getNextBlockInIfSuccessorChain(BlockNode block) { // skip this block and search in successors chain List successors = block.getSuccessors(); if (successors.size() > 2 || successors.size() == 0) { return null; } // We might have the next IF and a EXC_BOTTOM_SPLITTER block to delimit a try region BlockNode first = successors.get(0); if (successors.size() == 1) { return first; } BlockNode second = successors.get(1); boolean firstIsHandlerPath = first.contains(AFlag.EXC_BOTTOM_SPLITTER); boolean secondIsHandlerPath = second.contains(AFlag.EXC_BOTTOM_SPLITTER); if (!firstIsHandlerPath && !secondIsHandlerPath) { // unknown case return null; } if (firstIsHandlerPath && secondIsHandlerPath) { // unknown case return null; } BlockNode candidate = firstIsHandlerPath ? second : first; // Continue to recurse through blocks as long as none of them have any instructions if (candidate.getInstructions().isEmpty()) { return getNextBlockInIfSuccessorChain(candidate); } return candidate; } /** * Check that all instructions can be inlined */ private static boolean checkInsnsInline(BlockNode block, BlockNode next, List forceInlineInsns) { List insns = block.getInstructions(); if (insns.isEmpty()) { return true; } boolean pass = true; for (InsnNode insn : insns) { RegisterArg res = insn.getResult(); if (res == null) { return false; } List useList = res.getSVar().getUseList(); int useCount = useList.size(); if (useCount == 0) { // TODO? return false; } InsnArg arg = useList.get(0); InsnNode usePlace = arg.getParentInsn(); if (!BlockUtils.blockContains(block, usePlace) && !BlockUtils.blockContains(next, usePlace)) { return false; } if (useCount > 1) { forceInlineInsns.add(insn); } else { // allow only forced assign inline pass = false; } } return pass; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/LoopRegionMaker.java ================================================ package jadx.core.dex.visitors.regions.maker; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Queue; import java.util.Set; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.EdgeInsnAttr; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.attributes.nodes.LoopLabelAttr; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.Edge; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.conditions.IfInfo; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.utils.BlockUtils; import jadx.core.utils.ListUtils; import jadx.core.utils.RegionUtils; import jadx.core.utils.blocks.BlockSet; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.BlockUtils.followEmptyPath; import static jadx.core.utils.BlockUtils.getNextBlock; import static jadx.core.utils.BlockUtils.isPathExists; /* * Definitions: * main loop body - the set of nodes that form a loop in the control flow graph e.g. they can all * reach the loop start and are all reachable from the loop start * loop exit edge - an edge with a source in the main loop body and a target outside the main loop * body * outblock - the first node after the entire loop has finished that should be regioned next * header block - an IF node that implements the loop condition * crossing - a block that is reachable from two different source nodes and represents where control * flow paths from the two blocks cross * exit block/node - an overloaded term. May mean the source or target of an exit edge, or a route * to exit the overall region e.g. the outblock */ final class LoopRegionMaker { private final MethodNode mth; private final RegionMaker regionMaker; private final IfRegionMaker ifMaker; LoopRegionMaker(MethodNode mth, RegionMaker regionMaker, IfRegionMaker ifMaker) { this.mth = mth; this.regionMaker = regionMaker; this.ifMaker = ifMaker; } BlockNode process(IRegion curRegion, LoopInfo loop, RegionStack stack) { BlockNode loopStart = loop.getStart(); Set exitBlocksSet = loop.getExitNodes(); // set exit blocks scan order priority // this can help if loop has several exits (after using 'break' or 'return' in loop) List exitBlocks = new ArrayList<>(exitBlocksSet.size()); BlockNode nextStart = getNextBlock(loopStart); if (nextStart != null && exitBlocksSet.remove(nextStart)) { exitBlocks.add(nextStart); } if (exitBlocksSet.remove(loopStart)) { exitBlocks.add(loopStart); } if (exitBlocksSet.remove(loop.getEnd())) { exitBlocks.add(loop.getEnd()); } exitBlocks.addAll(exitBlocksSet); LoopRegion loopRegion = makeLoopRegion(curRegion, loop, exitBlocks); if (loopRegion == null) { BlockNode exit = makeEndlessLoop(curRegion, stack, loop, loopStart); insertContinue(loop); return exit; } curRegion.getSubBlocks().add(loopRegion); IRegion outerRegion = stack.peekRegion(); stack.push(loopRegion); IfInfo condInfo = ifMaker.buildIfInfo(loopRegion); if (!loop.getLoopBlocks().contains(condInfo.getThenBlock())) { // invert loop condition if 'then' points to exit condInfo = IfInfo.invert(condInfo); } loopRegion.updateCondition(condInfo); // prevent if's merge with loop condition condInfo.getMergedBlocks().forEach(b -> b.add(AFlag.ADDED_TO_REGION)); exitBlocks.removeAll(condInfo.getMergedBlocks().toList()); if (!exitBlocks.isEmpty()) { // Blocks associated with the loop condition List loopConditionBlocks = loopRegion.getConditionBlocks(); for (Edge exitEdge : loop.getExitEdges()) { // An exit edge from the loop condition blocks BlockNode exitSource = exitEdge.getSource(); if (loopConditionBlocks.contains(exitSource)) { BlockNode outBlock = followEmptyPath(exitEdge.getTarget()); for (BlockNode pred : outBlock.getPredecessors()) { // Restarting search through exit edges from the beginning ("top") for (Edge exitEdgeTop : loop.getExitEdges()) { if (!loopConditionBlocks.contains(exitEdgeTop.getSource())) { if (isPathExists(exitEdgeTop.getTarget(), pred) || exitEdgeTop.getTarget() == outBlock) { insertLoopBreak(stack, loop, outBlock, exitEdgeTop.getSource(), new Edge(pred, outBlock)); } } } } // Exit edge found - no need to check further regardless of break outcome break; } } } BlockNode out; if (loopRegion.isConditionAtEnd()) { BlockNode thenBlock = condInfo.getThenBlock(); out = thenBlock == loop.getEnd() || thenBlock == loopStart ? condInfo.getElseBlock() : thenBlock; out = BlockUtils.followEmptyPath(out); loopStart.remove(AType.LOOP); loop.getEnd().add(AFlag.ADDED_TO_REGION); stack.addExit(loop.getEnd()); regionMaker.clearBlockProcessedState(loopStart); Region body = regionMaker.makeRegion(loopStart); loopRegion.setBody(body); loopStart.addAttr(AType.LOOP, loop); loop.getEnd().remove(AFlag.ADDED_TO_REGION); } else { out = condInfo.getElseBlock(); // Following Jadx convention, this must be the next synthetic block, not actual (theoretical) out // block if (outerRegion != null && out != null && out.contains(AFlag.LOOP_START) && !out.getAll(AType.LOOP).contains(loop) && RegionUtils.isRegionContainsBlock(outerRegion, out)) { // exit to already processed outer loop out = null; } stack.addExit(out); BlockNode loopBody = condInfo.getThenBlock(); Region body; if (Objects.equals(loopBody, loopStart)) { // empty loop body body = new Region(loopRegion); } else { body = regionMaker.makeRegion(loopBody); } // add blocks from loop start to first condition block BlockNode conditionBlock = condInfo.getFirstIfBlock(); if (loopStart != conditionBlock) { Set blocks = BlockUtils.getAllPathsBlocks(loopStart, conditionBlock); blocks.remove(conditionBlock); for (BlockNode block : blocks) { if (block.getInstructions().isEmpty() && !block.contains(AFlag.ADDED_TO_REGION) && !RegionUtils.isRegionContainsBlock(body, block)) { body.add(block); } } } loopRegion.setBody(body); } stack.pop(); insertContinue(loop); return out; } /** * Select loop exit and construct LoopRegion */ private LoopRegion makeLoopRegion(IRegion curRegion, LoopInfo loop, List exitBlocks) { for (BlockNode block : exitBlocks) { // Ignore blocks that lead to exception handlers if (block.contains(AType.EXC_HANDLER)) { continue; } // Ignore blocks that do not branch based on an if statement InsnNode lastInsn = BlockUtils.getLastInsn(block); if (lastInsn == null || lastInsn.getType() != InsnType.IF) { continue; } // Skip any nested if statements List loops = block.getAll(AType.LOOP); if (!loops.isEmpty() && loops.get(0) != loop) { // skip nested loop condition continue; } boolean exitAtLoopEnd = isExitAtLoopEnd(block, loop); LoopRegion loopRegion = new LoopRegion(curRegion, loop, block, exitAtLoopEnd); boolean found; if (block == loop.getStart() || exitAtLoopEnd || BlockUtils.isEmptySimplePath(loop.getStart(), block)) { found = true; } else if (block.getPredecessors().contains(loop.getStart())) { loopRegion.setPreCondition(loop.getStart()); // if we can't merge pre-condition this is not correct header found = loopRegion.checkPreCondition(); } else { found = false; } if (found) { List list = mth.getAllLoopsForBlock(block); if (list.size() >= 2) { // bad condition if successors going out of all loops boolean allOuter = true; for (BlockNode outerBlock : block.getCleanSuccessors()) { List outLoopList = mth.getAllLoopsForBlock(outerBlock); outLoopList.remove(loop); if (!outLoopList.isEmpty()) { // goes to outer loop allOuter = false; break; } } if (allOuter) { found = false; } } } if (found && !checkLoopExits(loop, block)) { found = false; } if (found) { return loopRegion; } } // no exit found => endless loop return null; } private static boolean isExitAtLoopEnd(BlockNode exit, LoopInfo loop) { BlockNode loopEnd = loop.getEnd(); if (exit == loopEnd) { return true; } BlockNode loopStart = loop.getStart(); if (loopStart.getInstructions().isEmpty() && ListUtils.isSingleElement(loopStart.getSuccessors(), exit)) { return false; } return loopEnd.getInstructions().isEmpty() && ListUtils.isSingleElement(loopEnd.getPredecessors(), exit); } /* * Check that the exits suggested by treating mainExitBlock as the header block * are consistent with a loop condition */ private boolean checkLoopExits(LoopInfo loop, BlockNode mainExitBlock) { List exitEdges = loop.getExitEdges(); if (exitEdges.size() < 2) { return true; } // If the header selected does not have an exit edge, raise an exception Optional mainEdgeOpt = exitEdges.stream().filter(edge -> edge.getSource() == mainExitBlock).findFirst(); if (mainEdgeOpt.isEmpty()) { throw new JadxRuntimeException("Not found exit edge by exit block: " + mainExitBlock); } Edge mainExitEdge = mainEdgeOpt.get(); BlockNode mainOutBlock = mainExitEdge.getTarget(); BlockNode firstWorkAfterMainExitBlock = BlockUtils.followEmptyPath(mainOutBlock); List firstInstructions = firstWorkAfterMainExitBlock.getInstructions(); // If there is a direct path to a return from the header, all exits are inside the loop if (firstInstructions.size() == 1 && firstInstructions.get(0).getType() == InsnType.RETURN) { return true; } // Otherwise the exit must lead to a valid out block return validOutBlock(firstWorkAfterMainExitBlock, loop); } /* * An out block is valid if every exit path passes through it or doesn't cross any other exit path * (permitting one block of duplication) * @param outblock The proposed region exit block * @param exitEdges All edges leaving a section at the start of the region e.g. edges leaving a loop * body */ private Boolean validOutBlock(BlockNode outBlock, LoopInfo loop) { /* * Not permitted: * - An edge which cannot reach outblock, but does cross with another exit path * --- This crossing could be on a path that never reaches outblock * --- This crossing could be after outblock * - An edge which can reach outblock, but has another crossing with an exit path * --- This crossing could be before outblock * --- This crossing could be after outblock * --- This crossing could be on a branch that does not reach outblock * Permitted: * - If any of these inconsistent crossings occur at or near the method exit * - If the node can reach the outblock but has no crossing there because it dominates the outnode * - A number of other edge cases */ List exitEdges = loop.getExitEdges(); Queue edgesToCheck = new LinkedList<>(exitEdges); while (!edgesToCheck.isEmpty()) { Edge exitEdge = edgesToCheck.remove(); BlockNode exitBlock = exitEdge.getTarget(); // Get the dominance frontier of exitEdge.getTarget() only along paths through exitEdge List dominanceFrontier; if (!exitEdge.isSynthetic()) { dominanceFrontier = BlockUtils.bitSetToBlocks(mth, BlockUtils.getDomFrontierThroughEdge(exitEdge)); } else { dominanceFrontier = BlockUtils.bitSetToBlocks(mth, exitEdge.getTarget().getDomFrontier()); } if (outBlock.isDominator(exitBlock) || outBlock == exitBlock) { // Accept if the loop exit block is a dominator of the suggested out block continue; } for (BlockNode crossing : dominanceFrontier) { if (crossing == outBlock) { // Accept if the crossing is at the outblock continue; } if (BlockUtils.isExitBlock(mth, crossing)) { // Accept if the crossing is at the method end continue; } // Find the first block after the crossing with instructions BlockNode firstInstructionBlock = crossing; List cInsns = crossing.getInstructions(); if (cInsns.isEmpty()) { firstInstructionBlock = BlockUtils.followEmptyPath(crossing); } // Return false if the crossing doesn't satisfy any relevant edge case if (!(viaValidUncleanSuccessor(exitBlock, crossing, loop) || noWorkBeforeEnd(firstInstructionBlock, outBlock) || oneBlockOfWorkBeforeEnd(firstInstructionBlock, outBlock) || isNestedIfCross(crossing, edgesToCheck) || isOuterOutblock(crossing, loop))) { return false; } } } return true; } /* * @param exitBlock the target of an exit edge * @param crossing the crossing block between exitBlock and a possible outblock * @param loop the loop */ private boolean viaValidUncleanSuccessor(BlockNode exitBlock, BlockNode crossing, LoopInfo loop) { // Return true if the path from exitBlock is to an exception handler or via a backwards loop edge // with continue if (isPathExists(exitBlock, crossing)) { // This case does not apply if there is a path via clean successors return false; } // If to a loop start check if the backwards edge has a branch without a valid continue if (crossing.contains(AFlag.LOOP_START)) { // Note: This loop start cannot be for the internal loop else exitEdge would not leave the loop // Find the outer loop containing the loop start LoopInfo parent = loop.getParentLoop(); LoopInfo outerLoop = null; while (parent != null) { if (parent.getStart() == crossing) { outerLoop = parent; break; } parent = parent.getParentLoop(); } if (outerLoop != null) { BlockNode loopEnd = outerLoop.getEnd(); List predecessors = loopEnd.getPredecessors(); if (predecessors.size() > 1) { for (BlockNode predecessor : predecessors) { // Do not accept if a predecessor to the loop end reachable from the exit would not have a // continue inserted if (BlockUtils.isPathExists(exitBlock, predecessor) && !canInsertContinue(predecessor, predecessors, loopEnd, outerLoop.getExitNodes())) { return false; } } } else { // Do not accept if no continues would be placed return false; } } } // Accept if all branches have a valid continue or if not to a loop start (to an exception handler) return true; } /* * @param firstInstructionBlock the first block containing instructions off the main loop body * @param outBlock a possible outblock of the loop */ private boolean noWorkBeforeEnd(BlockNode firstInstructionBlock, BlockNode outBlock) { // Return true if there is no work between the crossing and an exit block return (BlockUtils.isExitBlock(mth, firstInstructionBlock) || firstInstructionBlock == outBlock); } /* * @param firstInstructionBlock the first block containing instructions off the main loop body * @param outBlock a possible outblock of the loop */ private boolean oneBlockOfWorkBeforeEnd(BlockNode firstInstructionBlock, BlockNode outBlock) { // Return true if down every path there is no more than one block of work between the crossing and // an exit block List cleanSuccessors = firstInstructionBlock.getCleanSuccessors(); if (cleanSuccessors.isEmpty()) { return false; } for (BlockNode cleanSuccessor : cleanSuccessors) { BlockNode nextInstructionBlock = BlockUtils.followEmptyPath(cleanSuccessor); if (!BlockUtils.isExitBlock(mth, nextInstructionBlock) && nextInstructionBlock != outBlock) { return false; } } return true; } /* * @param crossing the block that may be the joint block of a merged if * @param edgesToCheck the list of edges that will be processed to add to */ private boolean isNestedIfCross(BlockNode crossing, Queue edgesToCheck) { // Return true if the crossing is due to merged control flow after a nested if // Add the edges out of the crossing to be investigated // If the crossing is the branch of a merged if, all predecessors will be synthetic up to the if // statements, and the first if statement will dominate the crossing List predecessors = crossing.getPredecessors(); // Find a predecessor that dominates all other predecessors BlockNode possibleFirstIF = BlockUtils.followEmptyPath(predecessors.get(0), true); for (BlockNode predecessor : predecessors) { // Follow the predecessor up to the first node with instructions BlockNode possibleIF = followEmptyPath(predecessor, true); if (crossing.isDominator(possibleIF)) { possibleFirstIF = possibleIF; } } // This case does not apply if a merged if cannot be made IfInfo currentIf = IfRegionMaker.makeIfInfo(mth, possibleFirstIF); if (currentIf == null) { return false; } IfInfo mergedIf = IfRegionMaker.mergeNestedIfNodes(currentIf); if (mergedIf == null) { return false; } // Note: work will be repeated for large merged ifs. Results could be cached to improve performance // Accept if following every predecessor path from the crossing reaches a merged if node BlockSet mergedBlocks = mergedIf.getMergedBlocks(); for (BlockNode predecessor : predecessors) { BlockNode possibleIF = followEmptyPath(predecessor, true); if (!mergedBlocks.contains(possibleIF)) { return false; } } // If this crossing is the result of merged ifs, check the next crossing after this one Edge placeHolderEdge = new Edge(crossing, crossing, true); if (!edgesToCheck.contains(placeHolderEdge)) { edgesToCheck.add(placeHolderEdge); } return true; } /* * @param crossing the block that may be an outblock for a parent loop * @param loop the inner loop currently being considered */ private boolean isOuterOutblock(BlockNode crossing, LoopInfo loop) { // Return true if the crossing is the outblock for an outer loop and is jumped to using a labelled // break List edgeInsns = crossing.getAll(AType.EDGE_INSN); for (EdgeInsnAttr edgeInsn : edgeInsns) { InsnNode insn = edgeInsn.getInsn(); // If there is a break edge instruction if (insn.getType() == InsnType.BREAK) { List loopsBrokenFrom = insn.get(AType.LOOP).getList(); for (LoopInfo loopBrokenFrom : loopsBrokenFrom) { // If it is for a parent of the current loop if (loop.hasParent(loopBrokenFrom)) { BlockNode target = edgeInsn.getEnd(); // If it points at the crossing if (target == crossing) { // Accept if the crossing block is already the target of a break instruction from a parent loop return true; } } } } } return false; } private BlockNode makeEndlessLoop(IRegion curRegion, RegionStack stack, LoopInfo loop, BlockNode loopStart) { LoopRegion loopRegion = new LoopRegion(curRegion, loop, null, false); curRegion.getSubBlocks().add(loopRegion); loopStart.remove(AType.LOOP); regionMaker.clearBlockProcessedState(loopStart); stack.push(loopRegion); BlockNode out = null; // insert 'break' for exits List exitEdges = loop.getExitEdges(); if (exitEdges.size() == 1) { Edge exitEdge = exitEdges.get(0); BlockNode exit = exitEdge.getTarget(); if (insertLoopBreak(stack, loop, exit, exitEdge.getSource(), exitEdge)) { BlockNode nextBlock = getNextBlock(exit); if (nextBlock != null) { stack.addExit(nextBlock); out = nextBlock; } } } else { loop0: for (Edge exitEdge : exitEdges) { BlockNode exit = exitEdge.getTarget(); List blocks = BlockUtils.bitSetToBlocks(mth, BlockUtils.getDomFrontierThroughEdge(exitEdge)); // Only select the method exit if there is no other valid outblock BlockNode methodExit = mth.getExitBlock(); if (blocks.contains(methodExit)) { blocks.remove(methodExit); blocks.add(methodExit); } for (BlockNode block : blocks) { if (BlockUtils.isPathExists(exit, block)) { if (validOutBlock(block, loop)) { out = block; break loop0; } } else if (block.contains(AFlag.LOOP_START)) { // Special case if there is no joining control flow before an outer loop back edge if (validOutBlock(exit, loop)) { out = exit; break loop0; } } } } // Add breaks stack.addExit(out); if (out != null && out != mth.getExitBlock()) { // Add a break on every incoming edge where the predecessor is reachable from the loop for (BlockNode predecessor : out.getPredecessors()) { for (Edge exitEdge : loop.getExitEdges()) { BlockNode target = exitEdge.getTarget(); if (BlockUtils.isPathExists(exitEdge.getTarget(), predecessor) || target == out) { insertLoopBreak(stack, loop, out, exitEdge.getSource(), new Edge(predecessor, out)); } } } } } Region body = regionMaker.makeRegion(loopStart); BlockNode loopEnd = loop.getEnd(); if (!RegionUtils.isRegionContainsBlock(body, loopEnd) && !loopEnd.contains(AType.EXC_HANDLER) && !inExceptionHandlerBlocks(loopEnd)) { body.getSubBlocks().add(loopEnd); } loopRegion.setBody(body); if (out == null) { BlockNode next = getNextBlock(loopEnd); out = RegionUtils.isRegionContainsBlock(body, next) ? null : next; } stack.pop(); loopStart.addAttr(AType.LOOP, loop); return out; } private boolean inExceptionHandlerBlocks(BlockNode loopEnd) { if (mth.getExceptionHandlersCount() == 0) { return false; } for (ExceptionHandler eh : mth.getExceptionHandlers()) { if (eh.getBlocks().contains(loopEnd)) { return true; } } return false; } private boolean canInsertBreak(BlockNode exit) { if (BlockUtils.containsExitInsn(exit)) { return false; } List simplePath = BlockUtils.buildSimplePath(exit); if (!simplePath.isEmpty()) { BlockNode lastBlock = simplePath.get(simplePath.size() - 1); if (lastBlock.isMthExitBlock() || lastBlock.isReturnBlock() || mth.isPreExitBlock(lastBlock)) { return false; } } // check if there no outer switch (TODO: very expensive check) Set paths = BlockUtils.getAllPathsBlocks(mth.getEnterBlock(), exit); for (BlockNode block : paths) { if (BlockUtils.checkLastInsnType(block, InsnType.SWITCH)) { return false; } } return true; } /* * Insert a break instruction where exitEdge meets loopExit * @param stack the region stack * @param loop the loop being broken out of * @param loopExit the outblock for loop * @param blockOnLoop an exit block on loop through which exitEdge is reachable * @param exitEdge an edge on the path between blockOnLoop and loopExit indicative of the breaking * path */ private boolean insertLoopBreak(RegionStack stack, LoopInfo loop, BlockNode loopExit, BlockNode blockOnLoop, Edge exitEdge) { BlockNode exit = exitEdge.getTarget(); Edge insertEdge = null; boolean confirm = false; // process special cases: // 1. jump to outer loop BlockNode exitEnd = BlockUtils.followEmptyPath(exit); List loops = exitEnd.getAll(AType.LOOP); for (LoopInfo loopAtEnd : loops) { if (loopAtEnd != loop && loop.hasParent(loopAtEnd)) { insertEdge = exitEdge; confirm = true; break; } } if (!confirm) { // Start search from the next edge if the target is simple (e.g. first // node after loop exit) Boolean isSimple = BlockUtils.followEmptyPath(exit) != exit; BlockNode insertBlock = isSimple ? null : exitEdge.getSource(); BlockSet visited = new BlockSet(mth); while (true) { if (exit == null || visited.contains(exit)) { break; } visited.add(exit); if (insertBlock != null && isPathExists(loopExit, exit)) { // found cross if (canInsertBreak(insertBlock)) { insertEdge = new Edge(insertBlock, exit); confirm = true; break; } return false; } insertBlock = exit; List cs = exit.getCleanSuccessors(); exit = cs.size() == 1 ? cs.get(0) : null; } } if (!confirm) { return false; } InsnNode breakInsn = new InsnNode(InsnType.BREAK, 0); breakInsn.addAttr(AType.LOOP, loop); EdgeInsnAttr.addEdgeInsn(insertEdge, breakInsn); stack.addExit(exit); // add label to 'break' if needed addBreakLabel(blockOnLoop, exit, breakInsn); return true; } /* * Adds a label to a break instruction if reaching the exit from the loop involves leaving multiple * loops * @param blockOnLoop the exit block on the loop to which breakInsn is currently associated * @param exit the out block of the loop to which breakInsn is currently associated * @param breakInsn a break instruction */ private void addBreakLabel(BlockNode blockOnLoop, BlockNode exit, InsnNode breakInsn) { List exitLoop = mth.getAllLoopsForBlock(exit); if (!exitLoop.isEmpty()) { return; } List inLoops = mth.getAllLoopsForBlock(blockOnLoop); if (inLoops.size() < 2) { return; } // search for parent loop LoopInfo parentLoop = null; for (LoopInfo loop : inLoops) { if (loop.getParentLoop() == null) { parentLoop = loop; break; } } if (parentLoop == null) { return; } if (parentLoop.getEnd() != exit && !parentLoop.getExitNodes().contains(exit)) { LoopLabelAttr labelAttr = new LoopLabelAttr(parentLoop); breakInsn.addAttr(labelAttr); parentLoop.getStart().addAttr(labelAttr); } } private static void insertContinue(LoopInfo loop) { BlockNode loopEnd = loop.getEnd(); List predecessors = loopEnd.getPredecessors(); if (predecessors.size() <= 1) { return; } Set loopExitNodes = loop.getExitNodes(); for (BlockNode pred : predecessors) { if (canInsertContinue(pred, predecessors, loopEnd, loopExitNodes)) { InsnNode cont = new InsnNode(InsnType.CONTINUE, 0); pred.getInstructions().add(cont); } } } private static boolean canInsertContinue(BlockNode pred, List predecessors, BlockNode loopEnd, Set loopExitNodes) { if (!pred.contains(AFlag.SYNTHETIC) || BlockUtils.checkLastInsnType(pred, InsnType.CONTINUE)) { return false; } List preds = pred.getPredecessors(); if (preds.isEmpty()) { return false; } BlockNode codePred = preds.get(0); if (codePred.contains(AFlag.ADDED_TO_REGION)) { return false; } if (loopEnd.isDominator(codePred) || loopExitNodes.contains(codePred)) { return false; } if (isDominatedOnBlocks(codePred, predecessors)) { return false; } if (!pred.getAll(AType.EDGE_INSN).isEmpty()) { // if we've already inserted a break, don't also insert a continue in the same spot List insns = pred.getAll(AType.EDGE_INSN); for (EdgeInsnAttr insn : insns) { if (insn.getInsn().getType() == InsnType.BREAK) { return false; } } } boolean gotoExit = false; for (BlockNode exit : loopExitNodes) { if (BlockUtils.isPathExists(codePred, exit)) { gotoExit = true; break; } } return gotoExit; } private static boolean isDominatedOnBlocks(BlockNode dom, List blocks) { for (BlockNode node : blocks) { if (!node.isDominator(dom)) { return false; } } return true; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/RegionMaker.java ================================================ package jadx.core.dex.visitors.regions.maker; import java.util.ArrayList; import java.util.List; import java.util.Objects; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.EdgeInsnAttr; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.SwitchInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnContainer; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.utils.BlockUtils; import jadx.core.utils.blocks.BlockSet; import jadx.core.utils.exceptions.JadxOverflowException; import static jadx.core.utils.BlockUtils.getNextBlock; public class RegionMaker { private final MethodNode mth; private final RegionStack stack; private final IfRegionMaker ifMaker; private final LoopRegionMaker loopMaker; private final BlockSet processedBlocks; private final int regionsLimit; private int regionsCount; public RegionMaker(MethodNode mth) { this.mth = mth; this.stack = new RegionStack(mth); this.ifMaker = new IfRegionMaker(mth, this); this.loopMaker = new LoopRegionMaker(mth, this, ifMaker); this.processedBlocks = BlockSet.empty(mth); this.regionsLimit = mth.getBasicBlocks().size() * 400; } public Region makeMthRegion() { return makeRegion(mth.getEnterBlock()); } Region makeRegion(BlockNode startBlock) { Objects.requireNonNull(startBlock); Region region = new Region(stack.peekRegion()); if (stack.containsExit(startBlock)) { insertEdgeInsns(region, startBlock); return region; } if (processedBlocks.addChecked(startBlock)) { mth.addWarnComment("Found duplicated region for block: " + startBlock + ' ' + startBlock.getAttributesString()); // Add block to multiple regions (duplicate the instructions in decompiled code) and allow // processing to continue } BlockNode next = startBlock; while (next != null) { next = traverse(region, next); regionsCount++; if (regionsCount > regionsLimit) { throw new JadxOverflowException("Regions count limit reached at block " + startBlock.toString()); } } return region; } /** * Recursively traverse all blocks from 'block' until block from 'exits' */ private BlockNode traverse(IRegion r, BlockNode block) { if (block.contains(AFlag.MTH_EXIT_BLOCK)) { return null; } BlockNode next = null; boolean processed = false; List loops = block.getAll(AType.LOOP); int loopCount = loops.size(); if (loopCount != 0 && block.contains(AFlag.LOOP_START)) { if (loopCount == 1) { next = loopMaker.process(r, loops.get(0), stack); processed = true; } else { for (LoopInfo loop : loops) { if (loop.getStart() == block) { next = loopMaker.process(r, loop, stack); processed = true; break; } } } } InsnNode insn = BlockUtils.getLastInsn(block); if (!processed && insn != null) { switch (insn.getType()) { case IF: next = ifMaker.process(r, block, (IfNode) insn, stack); processed = true; break; case SWITCH: SwitchRegionMaker switchMaker = new SwitchRegionMaker(mth, this); next = switchMaker.process(r, block, (SwitchInsn) insn, stack); processed = true; break; case MONITOR_ENTER: SynchronizedRegionMaker syncMaker = new SynchronizedRegionMaker(mth, this); next = syncMaker.process(r, block, insn, stack); processed = true; break; } } if (!processed) { r.getSubBlocks().add(block); next = getNextBlock(block); } if (next != null && !stack.containsExit(block) && !stack.containsExit(next)) { return next; } return null; } private void insertEdgeInsns(Region region, BlockNode exitBlock) { List edgeInsns = exitBlock.getAll(AType.EDGE_INSN); if (edgeInsns.isEmpty()) { return; } List insns = new ArrayList<>(edgeInsns.size()); addOneInsnOfType(insns, edgeInsns, InsnType.BREAK); addOneInsnOfType(insns, edgeInsns, InsnType.CONTINUE); region.add(new InsnContainer(insns)); } private void addOneInsnOfType(List insns, List edgeInsns, InsnType insnType) { for (EdgeInsnAttr edgeInsn : edgeInsns) { InsnNode insn = edgeInsn.getInsn(); if (insn.getType() == insnType) { insns.add(insn); return; } } } RegionStack getStack() { return stack; } boolean isProcessed(BlockNode block) { return processedBlocks.contains(block); } void clearBlockProcessedState(BlockNode block) { processedBlocks.remove(block); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/RegionStack.java ================================================ package jadx.core.dex.visitors.regions.maker; import java.util.ArrayDeque; import java.util.Collection; import java.util.Deque; import java.util.HashSet; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.exceptions.JadxOverflowException; final class RegionStack { private static final Logger LOG = LoggerFactory.getLogger(RegionStack.class); private static final boolean DEBUG = false; private static final int REGIONS_STACK_LIMIT = 1000; static { if (DEBUG) { LOG.debug("Debug enabled for {}", RegionStack.class); } } private static final class State { final Set exits; IRegion region; public State() { exits = new HashSet<>(); } private State(State c, IRegion region) { this.exits = new HashSet<>(c.exits); this.region = region; } public State copyWith(IRegion region) { return new State(this, region); } @Override public String toString() { return "Region: " + region + ", exits: " + exits; } } private final Deque stack; private State curState; public RegionStack(MethodNode mth) { if (DEBUG) { LOG.debug("New RegionStack: {}", mth); } this.stack = new ArrayDeque<>(); this.curState = new State(); } public void push(IRegion region) { stack.push(curState); if (stack.size() > REGIONS_STACK_LIMIT) { throw new JadxOverflowException("Regions stack size limit reached"); } curState = curState.copyWith(region); if (DEBUG) { LOG.debug("Stack push: {}: {}", size(), curState); } } public void pop() { curState = stack.pop(); if (DEBUG) { LOG.debug("Stack pop: {}: {}", size(), curState); } } /** * Add boundary(exit) node for current stack frame * * @param exit boundary node, null will be ignored */ public void addExit(@Nullable BlockNode exit) { if (exit != null) { curState.exits.add(exit); } } public void addExits(Collection exits) { for (BlockNode exit : exits) { addExit(exit); } } public void removeExit(@Nullable BlockNode exit) { if (exit != null) { curState.exits.remove(exit); } } public boolean containsExit(BlockNode exit) { return curState.exits.contains(exit); } public IRegion peekRegion() { return curState.region; } public int size() { return stack.size(); } public RegionStack clear() { stack.clear(); curState = new State(); return this; } @Override public String toString() { return "Region stack size: " + size() + ", last: " + curState; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/SwitchRegionMaker.java ================================================ package jadx.core.dex.visitors.regions.maker; import java.util.ArrayList; import java.util.BitSet; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.attributes.nodes.RegionRefAttr; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.SwitchInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnContainer; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.regions.SwitchRegion.CaseInfo; import jadx.core.dex.visitors.regions.AbstractRegionVisitor; import jadx.core.dex.visitors.regions.DepthRegionTraversal; import jadx.core.dex.visitors.regions.SwitchBreakVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.ListUtils; import jadx.core.utils.RegionUtils; import jadx.core.utils.Utils; import jadx.core.utils.blocks.BlockSet; public final class SwitchRegionMaker { private final MethodNode mth; private final RegionMaker regionMaker; private static final Logger LOG = LoggerFactory.getLogger(SwitchRegionMaker.class); SwitchRegionMaker(MethodNode mth, RegionMaker regionMaker) { this.mth = mth; this.regionMaker = regionMaker; } BlockNode process(IRegion currentRegion, BlockNode block, SwitchInsn insn, RegionStack stack) { // map case blocks to keys int len = insn.getTargets().length; Map> blocksMap = new LinkedHashMap<>(len); BlockNode[] targetBlocksArr = insn.getTargetBlocks(); for (int i = 0; i < len; i++) { List keys = blocksMap.computeIfAbsent(targetBlocksArr[i], k -> new ArrayList<>(2)); keys.add(insn.getKey(i)); } BlockNode defCase = insn.getDefTargetBlock(); if (defCase != null) { List keys = blocksMap.computeIfAbsent(defCase, k -> new ArrayList<>(1)); keys.add(SwitchRegion.DEFAULT_CASE_KEY); } SwitchRegion sw = new SwitchRegion(currentRegion, block); insn.addAttr(new RegionRefAttr(sw)); currentRegion.getSubBlocks().add(sw); stack.push(sw); BlockNode out = calcSwitchOut(block, insn, stack); stack.addExit(out); addCases(sw, out, stack, blocksMap); removeEmptyCases(insn, sw, defCase, out); stack.pop(); return out; } /** * Insert 'break' for all cases in switch region * Executed in {@link jadx.core.dex.visitors.regions.PostProcessRegions} after try/catch wrap to * handle all blocks */ public static void insertBreaks(MethodNode mth, SwitchRegion sw) { for (SwitchRegion.CaseInfo caseInfo : sw.getCases()) { insertBreaksForCase(mth, sw, caseInfo.getContainer()); } } private void addCases(SwitchRegion sw, @Nullable BlockNode out, RegionStack stack, Map> blocksMap) { Map fallThroughCases = new LinkedHashMap<>(); if (out != null) { // detect fallthrough cases BitSet caseBlocks = BlockUtils.blocksToBitSet(mth, blocksMap.keySet()); caseBlocks.clear(out.getPos()); for (BlockNode successor : sw.getHeader().getSuccessors()) { BitSet df = successor.getDomFrontier(); if (df.intersects(caseBlocks)) { BlockNode fallThroughBlock = getOneIntersectionBlock(out, caseBlocks, df); fallThroughCases.put(successor, fallThroughBlock); } } // check fallthrough cases order if (!fallThroughCases.isEmpty() && isBadCasesOrder(blocksMap, fallThroughCases)) { Map> newBlocksMap = reOrderSwitchCases(blocksMap, fallThroughCases); if (isBadCasesOrder(newBlocksMap, fallThroughCases)) { mth.addWarnComment("Can't fix incorrect switch cases order, some code will duplicate"); fallThroughCases.clear(); } else { blocksMap = newBlocksMap; } } } for (Map.Entry> entry : blocksMap.entrySet()) { List keysList = entry.getValue(); BlockNode caseBlock = entry.getKey(); Region caseRegion; if (stack.containsExit(caseBlock)) { caseRegion = new Region(stack.peekRegion()); } else { BlockNode next = fallThroughCases.get(caseBlock); stack.addExit(next); caseRegion = regionMaker.makeRegion(caseBlock); stack.removeExit(next); if (next != null) { next.add(AFlag.FALL_THROUGH); caseRegion.add(AFlag.FALL_THROUGH); } } sw.addCase(keysList, caseRegion); } } @Nullable private BlockNode getOneIntersectionBlock(BlockNode out, BitSet caseBlocks, BitSet fallThroughSet) { BitSet caseExits = BlockUtils.copyBlocksBitSet(mth, fallThroughSet); caseExits.clear(out.getPos()); caseExits.and(caseBlocks); return BlockUtils.bitSetToOneBlock(mth, caseExits); } private @Nullable BlockNode calcSwitchOut(BlockNode block, SwitchInsn insn, RegionStack stack) { // union of case blocks dominance frontier // works if no fallthrough cases and no returns inside switch BitSet outs = BlockUtils.newBlocksBitSet(mth); for (BlockNode s : block.getCleanSuccessors()) { if (s.contains(AFlag.LOOP_END)) { // loop end dom frontier is loop start, ignore it continue; } outs.or(s.getDomFrontier()); } outs.clear(block.getId()); outs.clear(mth.getExitBlock().getId()); BlockNode out = null; if (outs.cardinality() == 1) { // single exit out = BlockUtils.bitSetToOneBlock(mth, outs); } else { // several switch exits // possible 'return', 'continue' or fallthrough in one of the cases LoopInfo loop = mth.getLoopForBlock(block); if (loop != null) { outs.andNot(loop.getStart().getPostDoms()); outs.andNot(loop.getEnd().getPostDoms()); BlockNode loopEnd = loop.getEnd(); if (outs.cardinality() == 2 && outs.get(loopEnd.getId())) { // insert 'continue' for cases lead to loop end // expect only 2 exits: loop end and switch out List outList = BlockUtils.bitSetToBlocks(mth, outs); outList.remove(loopEnd); BlockNode possibleOut = Utils.getOne(outList); if (possibleOut != null && insertContinueInSwitch(block, possibleOut, loopEnd)) { outs.clear(loopEnd.getId()); out = possibleOut; } } if (outs.isEmpty()) { // all exits inside switch, keep inside to exit from loop return mth.getExitBlock(); } } if (out == null) { BlockNode imPostDom = block.getIPostDom(); if (outs.get(imPostDom.getId())) { out = imPostDom; } else { outs.andNot(block.getPostDoms()); out = BlockUtils.bitSetToOneBlock(mth, outs); } } } if (out != null && mth.isPreExitBlock(out)) { // include 'return' or 'throw' in case blocks out = mth.getExitBlock(); } BlockNode imPostDom = block.getIPostDom(); if (out == null && imPostDom == mth.getExitBlock()) { // all exits inside switch // check if all returns are equals and should be treated as single out block return allSameReturns(stack); } if (imPostDom == insn.getDefTargetBlock() && block.getCleanSuccessors().contains(imPostDom) && block.getDomFrontier().get(imPostDom.getId())) { // add exit to stop on empty 'default' block stack.addExit(imPostDom); } if (out == null) { mth.addWarnComment("Failed to find 'out' block for switch in " + block + ". Please report as an issue."); // fallback option; should work in most cases out = block.getIPostDom(); } if (out != null && regionMaker.isProcessed(out)) { // 'out' block already processed, prevent endless loop // in this case it might be that 'out' is the LOOP_START of a loop and occurs before 'block' // just try the immediate post dominator as a fallback mth.addWarnComment("Switch 'out' block " + out + " for " + block + " already processed. Defaulting to fallback option."); out = block.getIPostDom(); } return out; } private BlockNode allSameReturns(RegionStack stack) { BlockNode exitBlock = mth.getExitBlock(); List preds = exitBlock.getPredecessors(); int count = preds.size(); if (count == 1) { return preds.get(0); } if (mth.getReturnType() == ArgType.VOID) { for (BlockNode pred : preds) { InsnNode insn = BlockUtils.getLastInsn(pred); if (insn == null || insn.getType() != InsnType.RETURN) { return exitBlock; } } } else { List returnArgs = new ArrayList<>(); for (BlockNode pred : preds) { InsnNode insn = BlockUtils.getLastInsn(pred); if (insn == null || insn.getType() != InsnType.RETURN) { return exitBlock; } returnArgs.add(insn.getArg(0)); } InsnArg firstArg = returnArgs.get(0); if (firstArg.isRegister()) { RegisterArg reg = (RegisterArg) firstArg; for (int i = 1; i < count; i++) { InsnArg arg = returnArgs.get(i); if (!arg.isRegister() || !((RegisterArg) arg).sameCodeVar(reg)) { return exitBlock; } } } else { for (int i = 1; i < count; i++) { InsnArg arg = returnArgs.get(i); if (!arg.equals(firstArg)) { return exitBlock; } } } } // confirmed stack.addExits(preds); // ignore other returns for (int i = 1; i < count; i++) { BlockNode block = preds.get(i); block.add(AFlag.REMOVE); block.add(AFlag.ADDED_TO_REGION); } return preds.get(0); } /** * Remove empty case blocks: * 1. single 'default' case * 2. filler cases if switch is 'packed' and 'default' case is empty */ private void removeEmptyCases(SwitchInsn insn, SwitchRegion sw, BlockNode defCase, BlockNode outBlock) { boolean defaultCaseIsEmpty; if (defCase == null) { defaultCaseIsEmpty = true; } else { defaultCaseIsEmpty = sw.getCases().stream() .anyMatch(c -> c.getKeys().contains(SwitchRegion.DEFAULT_CASE_KEY) && canRemove(c.getContainer(), outBlock)); } if (defaultCaseIsEmpty) { List cases = new ArrayList<>(sw.getCases()); for (CaseInfo caseInfo : cases) { if (canRemove(caseInfo.getContainer(), outBlock)) { List keys = caseInfo.getKeys(); if (keys.contains(SwitchRegion.DEFAULT_CASE_KEY) || insn.isPacked()) { // Remove case and mark all blocks as don't generate RegionUtils.addToAll(mth, caseInfo.getContainer(), AFlag.DONT_GENERATE); sw.getCases().remove(caseInfo); } } } } } /* * Check container is empty and all paths through container are empty up until outBlock */ private boolean canRemove(IContainer container, BlockNode outBlock) { if (RegionUtils.isEmpty(container)) { if (container instanceof BlockNode) { // Base case - empty path from block node to outBlock return BlockUtils.followEmptyPath((BlockNode) container) == outBlock; } else if (container instanceof IRegion) { // Recursive case - every subBlock can be removed List subBlocks = ((IRegion) container).getSubBlocks(); for (IContainer subBlock : subBlocks) { if (!canRemove(subBlock, outBlock)) { return false; } } return true; } LOG.debug("Unexpected container type in switch"); } return false; } private boolean isBadCasesOrder(Map> blocksMap, Map fallThroughCases) { BlockNode nextCaseBlock = null; for (BlockNode caseBlock : blocksMap.keySet()) { if (nextCaseBlock != null && !caseBlock.equals(nextCaseBlock)) { return true; } nextCaseBlock = fallThroughCases.get(caseBlock); } return nextCaseBlock != null; } private Map> reOrderSwitchCases(Map> blocksMap, Map fallThroughCases) { List list = new ArrayList<>(blocksMap.size()); list.addAll(blocksMap.keySet()); list.sort((a, b) -> { BlockNode nextA = fallThroughCases.get(a); if (nextA != null) { if (b.equals(nextA)) { return -1; } } else if (a.equals(fallThroughCases.get(b))) { return 1; } return 0; }); Map> newBlocksMap = new LinkedHashMap<>(blocksMap.size()); for (BlockNode key : list) { newBlocksMap.put(key, blocksMap.get(key)); } return newBlocksMap; } private boolean insertContinueInSwitch(BlockNode switchBlock, BlockNode switchOut, BlockNode loopEnd) { boolean inserted = false; for (BlockNode caseBlock : switchBlock.getCleanSuccessors()) { if (caseBlock.getDomFrontier().get(loopEnd.getId()) && caseBlock != switchOut) { // search predecessor of loop end on path from this successor Set list = new HashSet<>(BlockUtils.collectBlocksDominatedBy(mth, caseBlock, caseBlock)); if (list.contains(switchOut) || switchOut.getPredecessors().stream().anyMatch(list::contains)) { // 'continue' not needed } else { for (BlockNode p : loopEnd.getPredecessors()) { if (list.contains(p) || p == caseBlock) { if (p.isSynthetic()) { p.getInstructions().add(new InsnNode(InsnType.CONTINUE, 0)); inserted = true; } break; } } } } } return inserted; } /** * Add break to every exit edge from 'case' region. * 'Break' optimizations (code duplication, unreachable, etc.) will be done at * {@link SwitchBreakVisitor} */ private static void insertBreaksForCase(MethodNode mth, SwitchRegion switchRegion, IContainer caseContainer) { BlockSet caseBlocks = new BlockSet(mth); RegionUtils.visitBlockNodes(mth, caseContainer, caseBlocks::add); DepthRegionTraversal.traverse(mth, caseContainer, new AbstractRegionVisitor() { @Override public void leaveRegion(MethodNode mth, IRegion region) { boolean insertBreak = false; if (region == caseContainer) { // top region insertBreak = true; } else { IContainer lastContainer = ListUtils.last(region.getSubBlocks()); if (lastContainer instanceof BlockNode) { BlockNode lastBlock = (BlockNode) lastContainer; for (BlockNode successor : lastBlock.getSuccessors()) { if (!caseBlocks.contains(successor)) { insertBreak = true; break; } } } } if (insertBreak && canAppendBreak(region)) { region.getSubBlocks().add(buildBreakContainer(switchRegion)); } } }); } public static boolean canAppendBreak(IRegion region) { return !region.contains(AFlag.FALL_THROUGH) && !RegionUtils.hasExitBlock(region); } public static InsnContainer buildBreakContainer(SwitchRegion switchRegion) { InsnNode breakInsn = new InsnNode(InsnType.BREAK, 0); breakInsn.add(AFlag.SYNTHETIC); breakInsn.addAttr(new RegionRefAttr(switchRegion)); return new InsnContainer(breakInsn); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/SynchronizedRegionMaker.java ================================================ package jadx.core.dex.visitors.regions.maker; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.ConstClassNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.SynchronizedRegion; import jadx.core.dex.visitors.regions.CleanRegions; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnRemover; import jadx.core.utils.Utils; import static jadx.core.utils.BlockUtils.getNextBlock; import static jadx.core.utils.BlockUtils.isPathExists; public class SynchronizedRegionMaker { private static final Logger LOG = LoggerFactory.getLogger(SynchronizedRegionMaker.class); private final MethodNode mth; private final RegionMaker regionMaker; SynchronizedRegionMaker(MethodNode mth, RegionMaker regionMaker) { this.mth = mth; this.regionMaker = regionMaker; } BlockNode process(IRegion curRegion, BlockNode block, InsnNode insn, RegionStack stack) { SynchronizedRegion synchRegion = new SynchronizedRegion(curRegion, insn); synchRegion.getSubBlocks().add(block); curRegion.getSubBlocks().add(synchRegion); Set exits = new LinkedHashSet<>(); Set cacheSet = new HashSet<>(); traverseMonitorExits(synchRegion, insn.getArg(0), block, exits, cacheSet); for (InsnNode exitInsn : synchRegion.getExitInsns()) { BlockNode insnBlock = BlockUtils.getBlockByInsn(mth, exitInsn); if (insnBlock != null) { insnBlock.add(AFlag.DONT_GENERATE); } // remove arg from MONITOR_EXIT to allow inline in MONITOR_ENTER exitInsn.removeArg(0); exitInsn.add(AFlag.DONT_GENERATE); } BlockNode body = getNextBlock(block); if (body == null) { mth.addWarn("Unexpected end of synchronized block"); return null; } BlockNode exit = null; if (exits.size() == 1) { exit = getNextBlock(exits.iterator().next()); } else if (exits.size() > 1) { cacheSet.clear(); exit = traverseMonitorExitsCross(body, exits, cacheSet); } stack.push(synchRegion); if (exit != null) { stack.addExit(exit); } else { for (BlockNode exitBlock : exits) { // don't add exit blocks which leads to method end blocks ('return', 'throw', etc) List list = BlockUtils.buildSimplePath(exitBlock); if (list.isEmpty() || !BlockUtils.isExitBlock(mth, Utils.last(list))) { stack.addExit(exitBlock); // we can still try using this as an exit block to make sure it's visited. exit = exitBlock; } } } synchRegion.getSubBlocks().add(regionMaker.makeRegion(body)); stack.pop(); return exit; } /** * Traverse from monitor-enter thru successors and collect blocks contains monitor-exit */ private static void traverseMonitorExits(SynchronizedRegion region, InsnArg arg, BlockNode block, Set exits, Set visited) { visited.add(block); for (InsnNode insn : block.getInstructions()) { if (insn.getType() == InsnType.MONITOR_EXIT && insn.getArgsCount() > 0 && insn.getArg(0).equals(arg)) { exits.add(block); region.getExitInsns().add(insn); return; } } for (BlockNode node : block.getSuccessors()) { if (!visited.contains(node)) { traverseMonitorExits(region, arg, node, exits, visited); } } } /** * Traverse from monitor-enter thru successors and search for exit paths cross */ private static BlockNode traverseMonitorExitsCross(BlockNode block, Set exits, Set visited) { visited.add(block); for (BlockNode node : block.getCleanSuccessors()) { boolean cross = true; for (BlockNode exitBlock : exits) { boolean p = isPathExists(exitBlock, node); if (!p) { cross = false; break; } } if (cross) { return node; } if (!visited.contains(node)) { BlockNode res = traverseMonitorExitsCross(node, exits, visited); if (res != null) { return res; } } } return null; } public static void removeSynchronized(MethodNode mth) { Region startRegion = mth.getRegion(); List subBlocks = startRegion.getSubBlocks(); if (!subBlocks.isEmpty() && subBlocks.get(0) instanceof SynchronizedRegion) { SynchronizedRegion synchRegion = (SynchronizedRegion) subBlocks.get(0); InsnNode syncInsn = synchRegion.getEnterInsn(); if (canRemoveSyncBlock(mth, syncInsn)) { // replace synchronized block with an inner region startRegion.getSubBlocks().set(0, synchRegion.getRegion()); // remove 'monitor-enter' instruction InsnRemover.remove(mth, syncInsn); // remove 'monitor-exit' instruction for (InsnNode exit : synchRegion.getExitInsns()) { InsnRemover.remove(mth, exit); } // run region cleaner again CleanRegions.process(mth); // assume that CodeShrinker will be run after this } } } private static boolean canRemoveSyncBlock(MethodNode mth, InsnNode synchInsn) { InsnArg syncArg = synchInsn.getArg(0); if (mth.getAccessFlags().isStatic()) { if (syncArg.isInsnWrap() && syncArg.isConst()) { InsnNode constInsn = syncArg.unwrap(); if (constInsn.getType() == InsnType.CONST_CLASS) { ArgType clsType = ((ConstClassNode) constInsn).getClsType(); if (clsType.equals(mth.getParentClass().getType())) { return true; } } } mth.addWarnComment("In static synchronized method top region not synchronized by class const: " + syncArg); } else { if (syncArg.isThis()) { return true; } mth.addWarnComment("In synchronized method top region not synchronized by 'this': " + syncArg); } return false; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/CollectUsageRegionVisitor.java ================================================ package jadx.core.dex.visitors.regions.variables; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.loops.ForLoop; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.regions.loops.LoopType; import jadx.core.dex.visitors.regions.TracedRegionVisitor; class CollectUsageRegionVisitor extends TracedRegionVisitor { private final List args = new ArrayList<>(); private final Map usageMap = new LinkedHashMap<>(); public Map getUsageMap() { return usageMap; } @Override public void processBlockTraced(MethodNode mth, IBlock block, IRegion curRegion) { UsePlace usePlace = new UsePlace(curRegion, block); regionProcess(usePlace); int len = block.getInstructions().size(); for (int i = 0; i < len; i++) { InsnNode insn = block.getInstructions().get(i); processInsn(insn, usePlace); } } private void regionProcess(UsePlace usePlace) { IRegion region = usePlace.getRegion(); if (region instanceof LoopRegion) { LoopRegion loopRegion = (LoopRegion) region; LoopType loopType = loopRegion.getType(); if (loopType instanceof ForLoop) { ForLoop forLoop = (ForLoop) loopType; processInsn(forLoop.getInitInsn(), usePlace); processInsn(forLoop.getIncrInsn(), usePlace); } } } void processInsn(InsnNode insn, UsePlace usePlace) { if (insn == null) { return; } // result RegisterArg result = insn.getResult(); if (result != null && result.isRegister()) { if (!result.contains(AFlag.DONT_GENERATE)) { VarUsage usage = getUsage(result.getSVar()); usage.getAssigns().add(usePlace); } } // args args.clear(); insn.getRegisterArgs(args); for (RegisterArg arg : args) { if (!arg.contains(AFlag.DONT_GENERATE)) { VarUsage usage = getUsage(arg.getSVar()); usage.getUses().add(usePlace); } } } private VarUsage getUsage(SSAVar ssaVar) { return usageMap.computeIfAbsent(ssaVar, VarUsage::new); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/ProcessVariables.java ================================================ package jadx.core.dex.visitors.regions.variables; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.DeclareVariablesAttr; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.regions.AbstractRegionVisitor; import jadx.core.dex.visitors.regions.DepthRegionTraversal; import jadx.core.dex.visitors.typeinference.TypeCompare; import jadx.core.dex.visitors.typeinference.TypeCompareEnum; import jadx.core.utils.ListUtils; import jadx.core.utils.RegionUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; public class ProcessVariables extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(ProcessVariables.class); @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode() || mth.getSVars().isEmpty()) { return; } removeUnusedResults(mth); List codeVars = collectCodeVars(mth); if (codeVars.isEmpty()) { return; } checkCodeVars(mth, codeVars); // TODO: reduce code vars by name if debug info applied (need checks for variable scopes) // collect all variables usage CollectUsageRegionVisitor usageCollector = new CollectUsageRegionVisitor(); DepthRegionTraversal.traverse(mth, usageCollector); Map ssaUsageMap = usageCollector.getUsageMap(); if (ssaUsageMap.isEmpty()) { return; } Map> codeVarUsage = mergeUsageMaps(codeVars, ssaUsageMap); for (Entry> entry : codeVarUsage.entrySet()) { declareVar(mth, entry.getKey(), entry.getValue()); } } private static void removeUnusedResults(MethodNode mth) { DepthRegionTraversal.traverse(mth, new AbstractRegionVisitor() { @Override public void processBlock(MethodNode mth, IBlock container) { for (InsnNode insn : container.getInstructions()) { RegisterArg resultArg = insn.getResult(); if (resultArg == null) { continue; } SSAVar ssaVar = resultArg.getSVar(); if (isVarUnused(mth, ssaVar)) { boolean remove = false; if (insn.canRemoveResult()) { // remove unused result remove = true; } else if (canRemoveInsn(insn)) { // remove whole insn insn.add(AFlag.REMOVE); insn.add(AFlag.DONT_GENERATE); remove = true; } if (remove) { insn.setResult(null); mth.removeSVar(ssaVar); for (RegisterArg arg : ssaVar.getUseList()) { arg.resetSSAVar(); } } } } } /** * Remove insn if a result is not used */ private boolean canRemoveInsn(InsnNode insn) { if (insn.isConstInsn()) { return true; } switch (insn.getType()) { case CAST: case CHECK_CAST: return true; default: return false; } } private boolean isVarUnused(MethodNode mth, @Nullable SSAVar ssaVar) { if (ssaVar == null) { return true; } List useList = ssaVar.getUseList(); if (useList.isEmpty()) { return true; } if (ssaVar.isUsedInPhi()) { return false; } return ListUtils.allMatch(useList, arg -> isArgUnused(mth, arg)); } private boolean isArgUnused(MethodNode mth, RegisterArg arg) { if (arg.contains(AFlag.REMOVE)) { return true; } // check constructors for removed args InsnNode parentInsn = arg.getParentInsn(); if (parentInsn != null && parentInsn.getType() == InsnType.CONSTRUCTOR && parentInsn.contains(AType.METHOD_DETAILS)) { MethodNode resolveMth = mth.root().getMethodUtils().resolveMethod(((ConstructorInsn) parentInsn)); if (resolveMth != null && resolveMth.contains(AType.SKIP_MTH_ARGS)) { int insnPos = parentInsn.getArgIndex(arg); List mthArgs = resolveMth.getArgRegs(); if (0 <= insnPos && insnPos < mthArgs.size()) { RegisterArg mthArg = mthArgs.get(insnPos); if (mthArg.contains(AFlag.REMOVE) && arg.sameType(mthArg)) { arg.add(AFlag.DONT_GENERATE); return true; } } } } return false; } }); } private void checkCodeVars(MethodNode mth, List codeVars) { int unknownTypesCount = 0; for (CodeVar codeVar : codeVars) { ArgType codeVarType = codeVar.getType(); if (codeVarType == null) { codeVar.setType(ArgType.UNKNOWN); unknownTypesCount++; } else { codeVar.getSsaVars() .forEach(ssaVar -> { ArgType ssaType = ssaVar.getImmutableType(); if (ssaType != null && ssaType.isTypeKnown()) { TypeCompare comparator = mth.root().getTypeUpdate().getTypeCompare(); TypeCompareEnum result = comparator.compareTypes(ssaType, codeVarType); if (result == TypeCompareEnum.CONFLICT || result.isNarrow()) { mth.addWarn("Incorrect type for immutable var: ssa=" + ssaType + ", code=" + codeVarType + ", for " + ssaVar.getDetailedVarInfo(mth)); } } }); } } if (unknownTypesCount != 0) { mth.addWarn("Unknown variable types count: " + unknownTypesCount); } } private void declareVar(MethodNode mth, CodeVar codeVar, List usageList) { if (codeVar.isDeclared()) { return; } VarUsage mergedUsage = new VarUsage(null); for (VarUsage varUsage : usageList) { mergedUsage.getAssigns().addAll(varUsage.getAssigns()); mergedUsage.getUses().addAll(varUsage.getUses()); } if (mergedUsage.getAssigns().isEmpty() && mergedUsage.getUses().isEmpty()) { return; } // check if variable can be declared at one of assigns if (checkDeclareAtAssign(usageList, mergedUsage)) { return; } // TODO: search closest region for declare // region not found, declare at method start declareVarInRegion(mth.getRegion(), codeVar); } private List collectCodeVars(MethodNode mth) { Map> codeVars = new LinkedHashMap<>(); for (SSAVar ssaVar : mth.getSVars()) { if (ssaVar.getCodeVar().isThis()) { continue; } CodeVar codeVar = ssaVar.getCodeVar(); List list = codeVars.computeIfAbsent(codeVar, k -> new ArrayList<>()); list.add(ssaVar); } for (Entry> entry : codeVars.entrySet()) { CodeVar codeVar = entry.getKey(); List list = entry.getValue(); for (SSAVar ssaVar : list) { CodeVar localCodeVar = ssaVar.getCodeVar(); codeVar.mergeFlagsFrom(localCodeVar); } if (list.size() > 1) { for (SSAVar ssaVar : list) { ssaVar.setCodeVar(codeVar); } } codeVar.setSsaVars(list); } return new ArrayList<>(codeVars.keySet()); } private Map> mergeUsageMaps(List codeVars, Map ssaUsageMap) { Map> codeVarUsage = new LinkedHashMap<>(codeVars.size()); for (CodeVar codeVar : codeVars) { List list = new ArrayList<>(); for (SSAVar ssaVar : codeVar.getSsaVars()) { VarUsage usage = ssaUsageMap.get(ssaVar); if (usage != null) { list.add(usage); } } codeVarUsage.put(codeVar, Utils.lockList(list)); } return codeVarUsage; } private boolean checkDeclareAtAssign(List list, VarUsage mergedUsage) { if (mergedUsage.getAssigns().isEmpty()) { return false; } for (VarUsage u : list) { for (UsePlace assign : u.getAssigns()) { if (canDeclareAt(mergedUsage, assign)) { return checkDeclareAtAssign(u.getVar()); } } } return false; } private static boolean canDeclareAt(VarUsage usage, UsePlace usePlace) { IRegion region = usePlace.getRegion(); // workaround for declare variables used in several loops if (region instanceof LoopRegion) { for (UsePlace use : usage.getAssigns()) { if (!RegionUtils.isRegionContainsRegion(region, use.getRegion())) { return false; } } } // can't declare in else-if chain between 'else' and next 'if' if (region.contains(AFlag.ELSE_IF_CHAIN)) { return false; } return isAllUseAfter(usePlace, usage.getAssigns()) && isAllUseAfter(usePlace, usage.getUses()); } /** * Check if all {@code usePlaces} are after {@code checkPlace} */ private static boolean isAllUseAfter(UsePlace checkPlace, List usePlaces) { IRegion region = checkPlace.getRegion(); IBlock block = checkPlace.getBlock(); Set toCheck = new HashSet<>(usePlaces); boolean blockFound = false; for (IContainer subBlock : region.getSubBlocks()) { if (!blockFound && subBlock == block) { blockFound = true; } if (blockFound) { toCheck.removeIf(usePlace -> isContainerContainsUsePlace(subBlock, usePlace)); if (toCheck.isEmpty()) { return true; } } } return false; } private static boolean isContainerContainsUsePlace(IContainer subBlock, UsePlace usePlace) { if (subBlock == usePlace.getBlock()) { return true; } if (subBlock instanceof IRegion) { // TODO: make index for faster check return RegionUtils.isRegionContainsRegion(subBlock, usePlace.getRegion()); } return false; } private static boolean checkDeclareAtAssign(SSAVar var) { RegisterArg arg = var.getAssign(); InsnNode parentInsn = arg.getParentInsn(); if (parentInsn == null || parentInsn.contains(AFlag.WRAPPED) || parentInsn.getType() == InsnType.PHI) { return false; } if (!arg.equals(parentInsn.getResult())) { return false; } parentInsn.add(AFlag.DECLARE_VAR); var.getCodeVar().setDeclared(true); return true; } private static void declareVarInRegion(IContainer region, CodeVar var) { if (var.isDeclared()) { LOG.warn("Try to declare already declared variable: {}", var); return; } DeclareVariablesAttr dv = region.get(AType.DECLARE_VARIABLES); if (dv == null) { dv = new DeclareVariablesAttr(); region.addAttr(dv); } dv.addVar(var); var.setDeclared(true); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/UsePlace.java ================================================ package jadx.core.dex.visitors.regions.variables; import java.util.Objects; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IRegion; public class UsePlace { public final IRegion region; public final IBlock block; public UsePlace(IRegion region, IBlock block) { this.region = region; this.block = block; } public IRegion getRegion() { return region; } public IBlock getBlock() { return block; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } UsePlace usePlace = (UsePlace) o; return Objects.equals(region, usePlace.region) && Objects.equals(block, usePlace.block); } @Override public int hashCode() { return Objects.hash(region, block); } @Override public String toString() { return "UsePlace{region=" + region + ", block=" + block + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/VarUsage.java ================================================ package jadx.core.dex.visitors.regions.variables; import java.util.ArrayList; import java.util.List; import jadx.core.dex.instructions.args.SSAVar; class VarUsage { private final SSAVar var; private final List assigns = new ArrayList<>(3); private final List uses = new ArrayList<>(3); VarUsage(SSAVar var) { this.var = var; } public SSAVar getVar() { return var; } public List getAssigns() { return assigns; } public List getUses() { return uses; } @Override public String toString() { return '{' + (var == null ? "-" : var.toShortString()) + ", a:" + assigns + ", u:" + uses + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/rename/CodeRenameVisitor.java ================================================ package jadx.core.dex.visitors.rename; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.data.ICodeData; import jadx.api.data.ICodeRename; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.InitCodeVariables; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "ApplyCodeRename", desc = "Rename variables and other entities in methods", runAfter = { InitCodeVariables.class, DebugInfoApplyVisitor.class } ) public class CodeRenameVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(CodeRenameVisitor.class); private Map> clsRenamesMap; @Override public void init(RootNode root) throws JadxException { updateRenamesMap(root.getArgs().getCodeData()); root.registerCodeDataUpdateListener(this::updateRenamesMap); } @Override public boolean visit(ClassNode cls) { List renames = getRenames(cls); if (!renames.isEmpty()) { applyRenames(cls, renames); } cls.getInnerClasses().forEach(this::visit); return false; } private static void applyRenames(ClassNode cls, List renames) { for (ICodeRename rename : renames) { IJavaNodeRef nodeRef = rename.getNodeRef(); if (nodeRef.getType() == IJavaNodeRef.RefType.METHOD) { MethodNode methodNode = cls.searchMethodByShortId(nodeRef.getShortId()); if (methodNode == null) { LOG.warn("Method reference not found: {}", nodeRef); } else { IJavaCodeRef codeRef = rename.getCodeRef(); if (codeRef != null) { processRename(methodNode, codeRef, rename); } } } } } private static void processRename(MethodNode mth, IJavaCodeRef codeRef, ICodeRename rename) { switch (codeRef.getAttachType()) { case MTH_ARG: { List argRegs = mth.getArgRegs(); int argNum = codeRef.getIndex(); if (argNum < argRegs.size()) { argRegs.get(argNum).getSVar().getCodeVar().setName(rename.getNewName()); } else { LOG.warn("Incorrect method arg ref {}, should be less than {}", argNum, argRegs.size()); } break; } case VAR: { int regNum = codeRef.getIndex() >> 16; int ssaVer = codeRef.getIndex() & 0xFFFF; for (SSAVar ssaVar : mth.getSVars()) { if (ssaVar.getRegNum() == regNum && ssaVar.getVersion() == ssaVer) { ssaVar.getCodeVar().setName(rename.getNewName()); return; } } LOG.warn("Can't find variable ref by {}_{}", regNum, ssaVer); break; } default: LOG.warn("Rename code ref type {} not yet supported", codeRef.getAttachType()); break; } } private List getRenames(ClassNode cls) { if (clsRenamesMap == null) { return Collections.emptyList(); } List clsComments = clsRenamesMap.get(cls.getClassInfo().getRawName()); if (clsComments == null) { return Collections.emptyList(); } return clsComments; } private void updateRenamesMap(@Nullable ICodeData data) { if (data == null) { this.clsRenamesMap = Collections.emptyMap(); } else { this.clsRenamesMap = data.getRenames().stream() .filter(r -> r.getCodeRef() != null) .collect(Collectors.groupingBy(r -> r.getNodeRef().getDeclaringClass())); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java ================================================ package jadx.core.dex.visitors.rename; import java.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import org.jetbrains.annotations.Nullable; import jadx.api.JadxArgs; import jadx.api.deobf.IAliasProvider; import jadx.core.Consts; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.attributes.nodes.RenameReasonAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.StringUtils; public class RenameVisitor extends AbstractVisitor { private static final Pattern ANONYMOUS_CLASS_PATTERN = Pattern.compile("^\\d+$"); @Override public void init(RootNode root) { List inputFiles = root.getArgs().getInputFiles(); if (inputFiles.isEmpty()) { return; } process(root); root.registerCodeDataUpdateListener(codeData -> process(root)); } private void process(RootNode root) { UserRenames.apply(root); checkNames(root); } private static void checkNames(RootNode root) { JadxArgs args = root.getArgs(); if (args.getRenameFlags().isEmpty()) { return; } IAliasProvider aliasProvider = args.getAliasProvider(); List classes = root.getClasses(true); for (ClassNode cls : classes) { checkClassName(aliasProvider, cls, args); checkFields(aliasProvider, cls, args); checkMethods(aliasProvider, cls, args); } if (!args.isFsCaseSensitive() && args.isRenameCaseSensitive()) { Set clsFullPaths = new HashSet<>(classes.size()); for (ClassNode cls : classes) { ClassInfo clsInfo = cls.getClassInfo(); if (!clsFullPaths.add(clsInfo.getAliasFullPath().toLowerCase())) { clsInfo.changeShortName(aliasProvider.forClass(cls)); cls.addAttr(new RenameReasonAttr(cls).append("case insensitive filesystem")); clsFullPaths.add(clsInfo.getAliasFullPath().toLowerCase()); } } } boolean pkgUpdated = false; for (PackageNode pkg : root.getPackages()) { pkgUpdated |= checkPackage(args, aliasProvider, pkg); } if (pkgUpdated) { root.runPackagesUpdate(); } processRootPackages(aliasProvider, root, classes); } private static void checkClassName(IAliasProvider aliasProvider, ClassNode cls, JadxArgs args) { if (cls.contains(AFlag.DONT_RENAME)) { return; } ClassInfo classInfo = cls.getClassInfo(); String clsName = classInfo.getAliasShortName(); String newShortName = fixClsShortName(args, clsName); if (newShortName == null) { // rename failed, use deobfuscator cls.rename(aliasProvider.forClass(cls)); cls.addAttr(new RenameReasonAttr(cls).notPrintable()); return; } if (!newShortName.equals(clsName)) { classInfo.changeShortName(newShortName); cls.addAttr(new RenameReasonAttr(cls).append("invalid class name")); } if (classInfo.isInner() && args.isRenameValid()) { // check inner classes names ClassInfo parentClass = classInfo.getParentClass(); while (parentClass != null) { if (parentClass.getAliasShortName().equals(newShortName)) { cls.rename(aliasProvider.forClass(cls)); cls.addAttr(new RenameReasonAttr(cls).append("collision with other inner class name")); break; } parentClass = parentClass.getParentClass(); } } } private static boolean checkPackage(JadxArgs args, IAliasProvider aliasProvider, PackageNode pkg) { if (args.isRenameValid() && pkg.getAliasPkgInfo().isDefaultPkg()) { pkg.setFullAlias(Consts.DEFAULT_PACKAGE_NAME, false); return true; } String pkgName = pkg.getAliasPkgInfo().getName(); boolean notValid = args.isRenameValid() && !NameMapper.isValidIdentifier(pkgName); boolean notPrintable = args.isRenamePrintable() && !NameMapper.isAllCharsPrintable(pkgName); if (notValid || notPrintable) { pkg.setLeafAlias(aliasProvider.forPackage(pkg), false); return true; } return false; } @Nullable private static String fixClsShortName(JadxArgs args, String clsName) { if (StringUtils.isEmpty(clsName)) { return null; } boolean renameValid = args.isRenameValid(); if (renameValid) { if (ANONYMOUS_CLASS_PATTERN.matcher(clsName).matches()) { return Consts.ANONYMOUS_CLASS_PREFIX + NameMapper.removeInvalidCharsMiddle(clsName); } char firstChar = clsName.charAt(0); if (firstChar == '$' || Character.isDigit(firstChar)) { return 'C' + NameMapper.removeInvalidCharsMiddle(clsName); } } String cleanClsName = args.isRenamePrintable() ? NameMapper.removeNonPrintableCharacters(clsName) : clsName; if (cleanClsName.isEmpty()) { return null; } if (renameValid) { cleanClsName = NameMapper.removeInvalidChars(clsName, "C"); if (!NameMapper.isValidIdentifier(cleanClsName)) { return 'C' + cleanClsName; } } return cleanClsName; } private static void checkFields(IAliasProvider aliasProvider, ClassNode cls, JadxArgs args) { Set names = new HashSet<>(); for (FieldNode field : cls.getFields()) { FieldInfo fieldInfo = field.getFieldInfo(); String fieldName = fieldInfo.getAlias(); boolean notUnique = !names.add(fieldName); boolean notValid = args.isRenameValid() && !NameMapper.isValidIdentifier(fieldName); boolean notPrintable = args.isRenamePrintable() && !NameMapper.isAllCharsPrintable(fieldName); if (notUnique || notValid || notPrintable) { field.rename(aliasProvider.forField(field)); field.addAttr(new RenameReasonAttr(field, notValid, notPrintable)); if (notUnique) { field.addAttr(new RenameReasonAttr(field).append("collision with other field name")); } } } } private static void checkMethods(IAliasProvider aliasProvider, ClassNode cls, JadxArgs args) { List methods = new ArrayList<>(cls.getMethods().size()); for (MethodNode method : cls.getMethods()) { if (!method.getAccessFlags().isConstructor()) { methods.add(method); } } for (MethodNode mth : methods) { String alias = mth.getAlias(); boolean notValid = args.isRenameValid() && !NameMapper.isValidIdentifier(alias); boolean notPrintable = args.isRenamePrintable() && !NameMapper.isAllCharsPrintable(alias); if (notValid || notPrintable) { mth.rename(aliasProvider.forMethod(mth)); mth.addAttr(new RenameReasonAttr(mth, notValid, notPrintable)); } } // Rename methods with same signature if (args.isRenameValid()) { Set names = new HashSet<>(methods.size()); for (MethodNode mth : methods) { String signature = mth.getMethodInfo().makeSignature(true, false); if (!names.add(signature) && canRename(mth)) { mth.rename(aliasProvider.forMethod(mth)); mth.addAttr(new RenameReasonAttr("collision with other method in class")); } } } } private static boolean canRename(MethodNode mth) { if (mth.contains(AFlag.DONT_RENAME)) { return false; } MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE); if (overrideAttr != null) { for (MethodNode relatedMth : overrideAttr.getRelatedMthNodes()) { if (relatedMth != mth && mth.getParentClass().equals(relatedMth.getParentClass())) { // ignore rename if exists related method from same class (bridge method in most cases) // such rename will also rename current method and will not help to resolve name collision return false; } } } return true; } private static void processRootPackages(IAliasProvider aliasProvider, RootNode root, List classes) { Set rootPkgs = collectRootPkgs(root); root.getCacheStorage().setRootPkgs(rootPkgs); if (root.getArgs().isRenameValid()) { // rename field if collide with any root package for (ClassNode cls : classes) { for (FieldNode field : cls.getFields()) { if (rootPkgs.contains(field.getAlias())) { field.rename(aliasProvider.forField(field)); field.addAttr(new RenameReasonAttr("collision with root package name")); } } } } } private static Set collectRootPkgs(RootNode root) { Set rootPkgs = new HashSet<>(); for (PackageNode pkg : root.getPackages()) { if (pkg.isRoot()) { rootPkgs.add(pkg.getPkgInfo().getName()); } } return rootPkgs; } @Override public String getName() { return "RenameVisitor"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/rename/SourceFileRename.java ================================================ package jadx.core.dex.visitors.rename; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; import jadx.api.args.UseSourceNameAsClassNameAlias; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.SourceFileAttr; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.nodes.RenameReasonAttr; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.BetterName; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; public class SourceFileRename extends AbstractVisitor { @Override public String getName() { return "SourceFileRename"; } @Override public void init(RootNode root) throws JadxException { final var useSourceName = root.getArgs().getUseSourceNameAsClassNameAlias(); if (useSourceName == UseSourceNameAsClassNameAlias.NEVER) { return; } int repeatLimit = root.getArgs().getSourceNameRepeatLimit(); if (repeatLimit <= 1) { return; } List classes = root.getClasses(); Map aliasUseCount = new HashMap<>(); for (ClassNode cls : classes) { aliasUseCount.put(cls.getClassInfo().getShortName(), 1); } List renames = new ArrayList<>(); for (ClassNode cls : classes) { if (cls.contains(AFlag.DONT_RENAME)) { continue; } String alias = getAliasFromSourceFile(cls); if (alias != null) { int count = aliasUseCount.merge(alias, 1, Integer::sum); if (count < repeatLimit) { renames.add(new ClsRename(cls, alias, count)); } } } for (ClsRename clsRename : renames) { String alias = clsRename.getAlias(); Integer count = aliasUseCount.get(alias); if (count < repeatLimit) { applyRename(clsRename.getCls(), clsRename.buildAlias(), useSourceName); } } } private static void applyRename(ClassNode cls, String alias, UseSourceNameAsClassNameAlias useSourceName) { if (cls.getClassInfo().hasAlias()) { String currentAlias = cls.getAlias(); String betterName = getBetterName(currentAlias, alias, useSourceName); if (betterName.equals(currentAlias)) { return; } } cls.getClassInfo().changeShortName(alias); cls.addAttr(new RenameReasonAttr(cls).append("use source file name")); } private static String getBetterName(String currentName, String sourceName, UseSourceNameAsClassNameAlias useSourceName) { switch (useSourceName) { case ALWAYS: return sourceName; case IF_BETTER: return BetterName.getBetterClassName(sourceName, currentName); case NEVER: return currentName; default: throw new JadxRuntimeException("Unhandled strategy: " + useSourceName); } } private static @Nullable String getAliasFromSourceFile(ClassNode cls) { SourceFileAttr sourceFileAttr = cls.get(JadxAttrType.SOURCE_FILE); if (sourceFileAttr == null) { return null; } if (cls.getClassInfo().isInner()) { return null; } String name = sourceFileAttr.getFileName(); name = StringUtils.removeSuffix(name, ".java"); name = StringUtils.removeSuffix(name, ".kt"); if (!NameMapper.isValidAndPrintable(name)) { return null; } if (name.equals(cls.getName())) { return null; } return name; } private static final class ClsRename { private final ClassNode cls; private final String alias; private final int suffix; private ClsRename(ClassNode cls, String alias, int suffix) { this.cls = cls; this.alias = alias; this.suffix = suffix; } public ClassNode getCls() { return cls; } public String getAlias() { return alias; } public int getSuffix() { return suffix; } public String buildAlias() { return suffix < 2 ? alias : alias + suffix; } @Override public String toString() { return "ClsRename{" + cls + " -> '" + alias + suffix + "'}"; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/rename/UserRenames.java ================================================ package jadx.core.dex.visitors.rename; import java.util.List; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.data.ICodeData; import jadx.api.data.ICodeRename; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.InfoStorage; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.PackageNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.StringUtils; public class UserRenames { private static final Logger LOG = LoggerFactory.getLogger(UserRenames.class); public static void apply(RootNode root) { ICodeData codeData = root.getArgs().getCodeData(); if (codeData == null || codeData.getRenames().isEmpty()) { return; } InfoStorage infoStorage = root.getInfoStorage(); codeData.getRenames().stream() .filter(r -> r.getCodeRef() == null && r.getNodeRef().getType() != IJavaNodeRef.RefType.PKG) .collect(Collectors.groupingBy(r -> r.getNodeRef().getDeclaringClass())) .forEach((clsRawName, renames) -> { ClassInfo clsInfo = infoStorage.getCls(ArgType.object(clsRawName)); if (clsInfo != null) { ClassNode cls = root.resolveClass(clsInfo); if (cls != null) { for (ICodeRename rename : renames) { applyRename(cls, rename); } return; } } LOG.warn("Class info with reference '{}' not found", clsRawName); }); applyPkgRenames(root, codeData.getRenames()); } private static void applyRename(ClassNode cls, ICodeRename rename) { IJavaNodeRef nodeRef = rename.getNodeRef(); switch (nodeRef.getType()) { case CLASS: cls.rename(rename.getNewName()); break; case FIELD: FieldNode fieldNode = cls.searchFieldByShortId(nodeRef.getShortId()); if (fieldNode == null) { String fieldName = StringUtils.getPrefix(nodeRef.getShortId(), ":"); String fieldSign = cls.getFields().stream() .filter(f -> f.getFieldInfo().getName().equals(fieldName)) .map(f -> f.getFieldInfo().getShortId()) .collect(Collectors.joining()); LOG.warn("Field reference not found: {}. Fields with same name: {}", nodeRef, fieldSign); } else { fieldNode.rename(rename.getNewName()); } break; case METHOD: MethodNode mth = cls.searchMethodByShortId(nodeRef.getShortId()); if (mth == null) { LOG.warn("Method reference not found: {}", nodeRef); } else { IJavaCodeRef codeRef = rename.getCodeRef(); if (codeRef == null) { mth.rename(rename.getNewName()); } } break; } } private static void applyPkgRenames(RootNode root, List renames) { renames.stream() .filter(r -> r.getNodeRef().getType() == IJavaNodeRef.RefType.PKG) .forEach(pkgRename -> { String pkgFullName = pkgRename.getNodeRef().getDeclaringClass(); PackageNode pkgNode = root.resolvePackage(pkgFullName); if (pkgNode == null) { LOG.warn("Package for rename not found: {}", pkgFullName); } else { pkgNode.rename(pkgRename.getNewName()); } }); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/shrink/ArgsInfo.java ================================================ package jadx.core.dex.visitors.shrink; import java.util.ArrayList; import java.util.BitSet; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.EmptyBitSet; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; final class ArgsInfo { private final InsnNode insn; private final List argsList; private final List args; private final int pos; private int inlineBorder; private ArgsInfo inlinedInsn; private @Nullable List wrappedInsns; public ArgsInfo(InsnNode insn, List argsList, int pos) { this.insn = insn; this.argsList = argsList; this.pos = pos; this.inlineBorder = pos; this.args = getArgs(insn); } public static List getArgs(InsnNode insn) { List args = new ArrayList<>(); addArgs(insn, args); return args; } private static void addArgs(InsnNode insn, List args) { if (insn.getType() == InsnType.TERNARY) { args.addAll(((TernaryInsn) insn).getCondition().getRegisterArgs()); } for (InsnArg arg : insn.getArguments()) { if (arg.isRegister()) { args.add((RegisterArg) arg); } } for (InsnArg arg : insn.getArguments()) { if (arg.isInsnWrap()) { addArgs(((InsnWrapArg) arg).getWrapInsn(), args); } } } public InsnNode getInsn() { return insn; } List getArgs() { return args; } public BitSet getArgsSet() { if (args.isEmpty() && Utils.isEmpty(wrappedInsns)) { return EmptyBitSet.EMPTY; } BitSet set = new BitSet(); fillArgsSet(set); return set; } private void fillArgsSet(BitSet set) { for (RegisterArg arg : args) { set.set(arg.getRegNum()); } List wrapList = wrappedInsns; if (wrapList != null) { for (ArgsInfo wrappedInsn : wrapList) { wrappedInsn.fillArgsSet(set); } } } public WrapInfo checkInline(int assignPos, RegisterArg arg) { if (assignPos >= inlineBorder || !canMove(assignPos, inlineBorder)) { return null; } inlineBorder = assignPos; return inline(assignPos, arg); } private boolean canMove(int from, int to) { ArgsInfo startInfo = argsList.get(from); int start = from + 1; if (start == to) { // previous instruction or on edge of inline border return true; } if (start > to) { throw new JadxRuntimeException("Invalid inline insn positions: " + start + " - " + to); } BitSet movedSet = startInfo.getArgsSet(); if (movedSet == EmptyBitSet.EMPTY && startInfo.insn.isConstInsn()) { return true; } boolean canReorder = startInfo.canReorder(); for (int i = start; i < to; i++) { ArgsInfo argsInfo = argsList.get(i); if (argsInfo.getInlinedInsn() == this) { continue; } InsnNode curInsn = argsInfo.insn; if (canReorder) { if (usedArgAssign(curInsn, movedSet)) { return false; } } else { if (!curInsn.canReorder() || usedArgAssign(curInsn, movedSet)) { return false; } } } return true; } private boolean canReorder() { if (!insn.canReorder()) { return false; } List wrapList = wrappedInsns; if (wrapList != null) { for (ArgsInfo wrapInsn : wrapList) { if (!wrapInsn.canReorder()) { return false; } } } return true; } static boolean usedArgAssign(InsnNode insn, BitSet args) { if (args.isEmpty()) { return false; } RegisterArg result = insn.getResult(); if (result == null) { return false; } return args.get(result.getRegNum()); } WrapInfo inline(int assignInsnPos, RegisterArg arg) { ArgsInfo argsInfo = argsList.get(assignInsnPos); argsInfo.inlinedInsn = this; if (wrappedInsns == null) { wrappedInsns = new ArrayList<>(args.size()); } wrappedInsns.add(argsInfo); return new WrapInfo(argsInfo.insn, arg); } ArgsInfo getInlinedInsn() { if (inlinedInsn != null) { ArgsInfo parent = inlinedInsn.getInlinedInsn(); if (parent != null) { inlinedInsn = parent; } } return inlinedInsn; } @Override public String toString() { return "ArgsInfo: |" + inlineBorder + " ->" + (inlinedInsn == null ? "-" : inlinedInsn.pos) + ' ' + args + " : " + insn; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java ================================================ package jadx.core.dex.visitors.shrink; import java.util.ArrayList; import java.util.BitSet; import java.util.List; import java.util.Objects; import java.util.Set; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeCustomNode; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.Named; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.ModVisitor; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnList; import jadx.core.utils.InsnRemover; import jadx.core.utils.RegionUtils; import jadx.core.utils.exceptions.JadxRuntimeException; @JadxVisitor( name = "CodeShrinkVisitor", desc = "Inline variables to make code smaller", runAfter = { ModVisitor.class } ) public class CodeShrinkVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) { shrinkMethod(mth); } public static void shrinkMethod(MethodNode mth) { if (mth.isNoCode()) { return; } mth.remove(AFlag.REQUEST_CODE_SHRINK); for (BlockNode block : mth.getBasicBlocks()) { shrinkBlock(mth, block); simplifyMoveInsns(mth, block); } } private static void shrinkBlock(MethodNode mth, BlockNode block) { if (block.getInstructions().isEmpty()) { return; } InsnList insnList = new InsnList(block.getInstructions()); int insnCount = insnList.size(); List argsList = new ArrayList<>(insnCount); for (int i = 0; i < insnCount; i++) { argsList.add(new ArgsInfo(insnList.get(i), argsList, i)); } List wrapList = new ArrayList<>(); for (ArgsInfo argsInfo : argsList) { List args = argsInfo.getArgs(); for (int i = args.size() - 1; i >= 0; i--) { RegisterArg arg = args.get(i); checkInline(mth, block, insnList, wrapList, argsInfo, arg); } } if (!wrapList.isEmpty()) { for (WrapInfo wrapInfo : wrapList) { inline(mth, wrapInfo.getArg(), wrapInfo.getInsn(), block); } } } private static void checkInline(MethodNode mth, BlockNode block, InsnList insnList, List wrapList, ArgsInfo argsInfo, RegisterArg arg) { if (arg.contains(AFlag.DONT_INLINE) || arg.getParentInsn() == null || arg.getParentInsn().contains(AFlag.DONT_GENERATE)) { return; } SSAVar sVar = arg.getSVar(); if (sVar == null || sVar.getAssign().contains(AFlag.DONT_INLINE)) { return; } InsnNode assignInsn = sVar.getAssign().getParentInsn(); if (assignInsn == null || assignInsn.contains(AFlag.DONT_INLINE) || assignInsn.contains(AFlag.WRAPPED)) { return; } boolean assignInline = assignInsn.contains(AFlag.FORCE_ASSIGN_INLINE); if (!assignInline && sVar.isUsedInPhi()) { return; } // allow inline only one use arg int useCount = 0; for (RegisterArg useArg : sVar.getUseList()) { InsnNode parentInsn = useArg.getParentInsn(); if (parentInsn != null && parentInsn.contains(AFlag.DONT_GENERATE)) { continue; } if (!assignInline && useArg.contains(AFlag.DONT_INLINE_CONST)) { return; } useCount++; } if (!assignInline && useCount != 1) { return; } if (!assignInline && sVar.getName() != null) { if (searchArgWithName(assignInsn, sVar.getName())) { // allow inline if name is reused in result } else if (varWithSameNameExists(mth, sVar)) { // allow inline if var name is duplicated } else { // reject inline of named variable return; } } if (!checkLambdaInline(arg, assignInsn)) { return; } int assignPos = insnList.getIndex(assignInsn); if (assignPos != -1) { WrapInfo wrapInfo = argsInfo.checkInline(assignPos, arg); if (wrapInfo != null) { wrapList.add(wrapInfo); } } else { // another block BlockNode assignBlock = BlockUtils.getBlockByInsn(mth, assignInsn); if (assignBlock != null && assignInsn != arg.getParentInsn() && canMoveBetweenBlocks(mth, assignInsn, assignBlock, block, argsInfo.getInsn())) { if (assignInline) { assignInline(mth, arg, assignInsn, assignBlock); } else { inline(mth, arg, assignInsn, assignBlock); } } } } /** * Forbid inline lambda into invoke as an instance arg, i.e. this will not compile: * {@code () -> { ... }.apply(); } */ private static boolean checkLambdaInline(RegisterArg arg, InsnNode assignInsn) { if (assignInsn.getType() == InsnType.INVOKE && assignInsn instanceof InvokeCustomNode) { for (RegisterArg useArg : arg.getSVar().getUseList()) { InsnNode parentInsn = useArg.getParentInsn(); if (parentInsn != null && parentInsn.getType() == InsnType.INVOKE) { InvokeNode invokeNode = (InvokeNode) parentInsn; InsnArg instArg = invokeNode.getInstanceArg(); if (instArg != null && instArg == useArg) { return false; } } } } return true; } private static boolean varWithSameNameExists(MethodNode mth, SSAVar inlineVar) { for (SSAVar ssaVar : mth.getSVars()) { if (ssaVar == inlineVar || ssaVar.getCodeVar() == inlineVar.getCodeVar()) { continue; } if (Objects.equals(ssaVar.getName(), inlineVar.getName())) { return ssaVar.getUseCount() > inlineVar.getUseCount(); } } return false; } private static boolean searchArgWithName(InsnNode assignInsn, String varName) { InsnArg result = assignInsn.visitArgs(insnArg -> { if (insnArg instanceof Named) { String argName = ((Named) insnArg).getName(); if (Objects.equals(argName, varName)) { return insnArg; } } return null; }); return result != null; } private static boolean assignInline(MethodNode mth, RegisterArg arg, InsnNode assignInsn, BlockNode assignBlock) { RegisterArg useArg = arg.getSVar().getUseList().get(0); InsnNode useInsn = useArg.getParentInsn(); if (useInsn == null || useInsn.contains(AFlag.DONT_GENERATE)) { return false; } if (!InsnRemover.removeWithoutUnbind(mth, assignBlock, assignInsn)) { return false; } InsnArg replaceArg = InsnArg.wrapInsnIntoArg(assignInsn); useInsn.replaceArg(useArg, replaceArg); return true; } private static boolean inline(MethodNode mth, RegisterArg arg, InsnNode insn, BlockNode block) { if (insn.contains(AFlag.FORCE_ASSIGN_INLINE)) { return assignInline(mth, arg, insn, block); } // just move instruction into arg, don't unbind/copy/duplicate InsnArg wrappedArg = arg.wrapInstruction(mth, insn, false); boolean replaced = wrappedArg != null; if (replaced) { InsnNode parentInsn = arg.getParentInsn(); if (parentInsn != null) { parentInsn.inheritMetadata(insn); } InsnRemover.unbindResult(mth, insn); InsnRemover.removeWithoutUnbind(mth, block, insn); } return replaced; } private static boolean canMoveBetweenBlocks(MethodNode mth, InsnNode assignInsn, BlockNode assignBlock, BlockNode useBlock, InsnNode useInsn) { if (!BlockUtils.isPathExists(assignBlock, useBlock)) { return false; } List argsList = ArgsInfo.getArgs(assignInsn); BitSet args = new BitSet(); for (RegisterArg arg : argsList) { args.set(arg.getRegNum()); } boolean startCheck = false; for (InsnNode insn : assignBlock.getInstructions()) { if (startCheck && (!insn.canReorder() || ArgsInfo.usedArgAssign(insn, args))) { return false; } if (insn == assignInsn) { startCheck = true; } } Set pathsBlocks = BlockUtils.getAllPathsBlocks(assignBlock, useBlock); pathsBlocks.remove(assignBlock); pathsBlocks.remove(useBlock); for (BlockNode block : pathsBlocks) { if (block.contains(AFlag.DONT_GENERATE)) { if (BlockUtils.checkLastInsnType(block, InsnType.MONITOR_EXIT)) { if (RegionUtils.isBlocksInSameRegion(mth, assignBlock, useBlock)) { // allow move inside same synchronized region } else { // don't move from synchronized block return false; } } // skip checks for not generated blocks continue; } for (InsnNode insn : block.getInstructions()) { if (!insn.canReorder() || ArgsInfo.usedArgAssign(insn, args)) { return false; } } } for (InsnNode insn : useBlock.getInstructions()) { if (insn == useInsn) { return true; } if (!insn.canReorder() || ArgsInfo.usedArgAssign(insn, args)) { return false; } } throw new JadxRuntimeException("Can't process instruction move : " + assignBlock); } private static void simplifyMoveInsns(MethodNode mth, BlockNode block) { List insns = block.getInstructions(); int size = insns.size(); for (int i = 0; i < size; i++) { InsnNode insn = insns.get(i); if (insn.getType() == InsnType.MOVE) { // replace 'move' with wrapped insn InsnArg arg = insn.getArg(0); if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); InsnRemover.unbindResult(mth, wrapInsn); wrapInsn.setResult(insn.getResult().duplicate()); wrapInsn.inheritMetadata(insn); wrapInsn.setOffset(insn.getOffset()); wrapInsn.remove(AFlag.WRAPPED); block.getInstructions().set(i, wrapInsn); } } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/shrink/WrapInfo.java ================================================ package jadx.core.dex.visitors.shrink; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; final class WrapInfo { private final InsnNode insn; private final RegisterArg arg; WrapInfo(InsnNode assignInsn, RegisterArg arg) { this.insn = assignInsn; this.arg = arg; } InsnNode getInsn() { return insn; } RegisterArg getArg() { return arg; } @Override public String toString() { return "WrapInfo: " + arg + " -> " + insn; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ssa/LiveVarAnalysis.java ================================================ package jadx.core.dex.visitors.ssa; import java.util.BitSet; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.exceptions.JadxRuntimeException; public class LiveVarAnalysis { private static final Logger LOG = LoggerFactory.getLogger(LiveVarAnalysis.class); private final MethodNode mth; private BitSet[] uses; private BitSet[] defs; private BitSet[] liveIn; private BitSet[] assignBlocks; public LiveVarAnalysis(MethodNode mth) { this.mth = mth; } public void runAnalysis() { int bbCount = mth.getBasicBlocks().size(); int regsCount = mth.getRegsCount(); this.uses = initBitSetArray(bbCount, regsCount); this.defs = initBitSetArray(bbCount, regsCount); this.assignBlocks = initBitSetArray(regsCount, bbCount); fillBasicBlockInfo(); processLiveInfo(); } public BitSet getAssignBlocks(int regNum) { return assignBlocks[regNum]; } public boolean isLive(int blockId, int regNum) { if (blockId >= liveIn.length) { LOG.warn("LiveVarAnalysis: out of bounds block: {}, max: {}", blockId, liveIn.length); return false; } return liveIn[blockId].get(regNum); } public boolean isLive(BlockNode block, int regNum) { return isLive(block.getId(), regNum); } private void fillBasicBlockInfo() { for (BlockNode block : mth.getBasicBlocks()) { int blockId = block.getId(); BitSet gen = uses[blockId]; BitSet kill = defs[blockId]; for (InsnNode insn : block.getInstructions()) { for (InsnArg arg : insn.getArguments()) { if (arg.isRegister()) { int regNum = ((RegisterArg) arg).getRegNum(); if (!kill.get(regNum)) { gen.set(regNum); } } } RegisterArg result = insn.getResult(); if (result != null) { int regNum = result.getRegNum(); kill.set(regNum); assignBlocks[regNum].set(blockId); } } } } private void processLiveInfo() { int bbCount = mth.getBasicBlocks().size(); int regsCount = mth.getRegsCount(); BitSet[] liveInBlocks = initBitSetArray(bbCount, regsCount); List blocks = mth.getBasicBlocks(); int blocksCount = blocks.size(); int iterationsLimit = blocksCount * 10; boolean changed; int k = 0; do { changed = false; for (BlockNode block : blocks) { int blockId = block.getId(); BitSet prevIn = liveInBlocks[blockId]; BitSet newIn = new BitSet(regsCount); for (BlockNode successor : block.getSuccessors()) { newIn.or(liveInBlocks[successor.getId()]); } newIn.andNot(defs[blockId]); newIn.or(uses[blockId]); if (!prevIn.equals(newIn)) { changed = true; liveInBlocks[blockId] = newIn; } } if (k++ > iterationsLimit) { throw new JadxRuntimeException("Live variable analysis reach iterations limit, blocks count: " + blocksCount); } } while (changed); this.liveIn = liveInBlocks; } private static BitSet[] initBitSetArray(int length, int bitsCount) { BitSet[] array = new BitSet[length]; for (int i = 0; i < length; i++) { array[i] = new BitSet(bitsCount); } return array; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ssa/RenameState.java ================================================ package jadx.core.dex.visitors.ssa; import java.util.Arrays; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; final class RenameState { private final MethodNode mth; private final BlockNode block; private final SSAVar[] vars; private final int[] versions; public static RenameState init(MethodNode mth) { int regsCount = mth.getRegsCount(); RenameState state = new RenameState( mth, mth.getEnterBlock(), new SSAVar[regsCount], new int[regsCount]); RegisterArg thisArg = mth.getThisArg(); if (thisArg != null) { state.startVar(thisArg); } for (RegisterArg arg : mth.getArgRegs()) { state.startVar(arg); } return state; } public static RenameState copyFrom(RenameState state, BlockNode block) { return new RenameState( state.mth, block, Arrays.copyOf(state.vars, state.vars.length), state.versions); } private RenameState(MethodNode mth, BlockNode block, SSAVar[] vars, int[] versions) { this.mth = mth; this.block = block; this.vars = vars; this.versions = versions; } public BlockNode getBlock() { return block; } public SSAVar getVar(int regNum) { return vars[regNum]; } public SSAVar startVar(RegisterArg regArg) { int regNum = regArg.getRegNum(); int version = versions[regNum]++; SSAVar ssaVar = mth.makeNewSVar(regNum, version, regArg); vars[regNum] = ssaVar; return ssaVar; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/ssa/SSATransform.java ================================================ package jadx.core.dex.visitors.ssa; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.BitSet; import java.util.Deque; import java.util.Iterator; import java.util.List; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.PhiListAttr; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.blocks.BlockProcessor; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnList; import jadx.core.utils.InsnRemover; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; @JadxVisitor( name = "SSATransform", desc = "Calculate Single Side Assign (SSA) variables", runAfter = BlockProcessor.class ) public class SSATransform extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { return; } process(mth); } private static void process(MethodNode mth) { if (!mth.getSVars().isEmpty()) { return; } LiveVarAnalysis la = new LiveVarAnalysis(mth); la.runAnalysis(); int regsCount = mth.getRegsCount(); for (int i = 0; i < regsCount; i++) { placePhi(mth, i, la); } renameVariables(mth); fixLastAssignInTry(mth); removeBlockerInsns(mth); tryToFixUselessPhi(mth); markThisArgs(mth.getThisArg()); hidePhiInsns(mth); removeUnusedInvokeResults(mth); } private static void placePhi(MethodNode mth, int regNum, LiveVarAnalysis la) { List blocks = mth.getBasicBlocks(); int blocksCount = blocks.size(); BitSet hasPhi = new BitSet(blocksCount); BitSet processed = new BitSet(blocksCount); Deque workList = new ArrayDeque<>(); BitSet assignBlocks = la.getAssignBlocks(regNum); for (int id = assignBlocks.nextSetBit(0); id >= 0; id = assignBlocks.nextSetBit(id + 1)) { processed.set(id); workList.add(blocks.get(id)); } while (!workList.isEmpty()) { BlockNode block = workList.pop(); BitSet domFrontier = block.getDomFrontier(); for (int id = domFrontier.nextSetBit(0); id >= 0; id = domFrontier.nextSetBit(id + 1)) { if (!hasPhi.get(id) && la.isLive(id, regNum)) { BlockNode df = blocks.get(id); PhiInsn phiInsn = addPhi(mth, df, regNum); df.getInstructions().add(0, phiInsn); hasPhi.set(id); if (!processed.get(id)) { processed.set(id); workList.add(df); } } } } } public static PhiInsn addPhi(MethodNode mth, BlockNode block, int regNum) { PhiListAttr phiList = block.get(AType.PHI_LIST); if (phiList == null) { phiList = new PhiListAttr(); block.addAttr(phiList); } int size = block.getPredecessors().size(); if (mth.getEnterBlock() == block) { RegisterArg thisArg = mth.getThisArg(); if (thisArg != null && thisArg.getRegNum() == regNum) { size++; } else { for (RegisterArg arg : mth.getArgRegs()) { if (arg.getRegNum() == regNum) { size++; break; } } } } PhiInsn phiInsn = new PhiInsn(regNum, size); phiList.getList().add(phiInsn); phiInsn.setOffset(block.getStartOffset()); return phiInsn; } private static void renameVariables(MethodNode mth) { RenameState initState = RenameState.init(mth); initPhiInEnterBlock(initState); Deque stack = new ArrayDeque<>(); stack.push(initState); while (!stack.isEmpty()) { RenameState state = stack.pop(); renameVarsInBlock(mth, state); for (BlockNode dominated : state.getBlock().getDominatesOn()) { stack.push(RenameState.copyFrom(state, dominated)); } } } private static void initPhiInEnterBlock(RenameState initState) { PhiListAttr phiList = initState.getBlock().get(AType.PHI_LIST); if (phiList != null) { for (PhiInsn phiInsn : phiList.getList()) { bindPhiArg(initState, phiInsn); } } } private static void renameVarsInBlock(MethodNode mth, RenameState state) { BlockNode block = state.getBlock(); for (InsnNode insn : block.getInstructions()) { if (insn.getType() != InsnType.PHI) { for (InsnArg arg : insn.getArguments()) { if (!arg.isRegister()) { continue; } RegisterArg reg = (RegisterArg) arg; int regNum = reg.getRegNum(); SSAVar var = state.getVar(regNum); if (var == null) { // TODO: in most cases issue in incorrectly attached exception handlers mth.addWarnComment("Not initialized variable reg: " + regNum + ", insn: " + insn + ", block:" + block); var = state.startVar(reg); } var.use(reg); } } RegisterArg result = insn.getResult(); if (result != null) { state.startVar(result); } } for (BlockNode s : block.getSuccessors()) { PhiListAttr phiList = s.get(AType.PHI_LIST); if (phiList == null) { continue; } for (PhiInsn phiInsn : phiList.getList()) { bindPhiArg(state, phiInsn); } } } private static void bindPhiArg(RenameState state, PhiInsn phiInsn) { int regNum = phiInsn.getResult().getRegNum(); SSAVar var = state.getVar(regNum); if (var == null) { return; } RegisterArg arg = phiInsn.bindArg(state.getBlock()); var.use(arg); var.addUsedInPhi(phiInsn); } /** * Fix last try/catch assign instruction */ private static void fixLastAssignInTry(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { PhiListAttr phiList = block.get(AType.PHI_LIST); if (phiList != null) { ExcHandlerAttr handlerAttr = block.get(AType.EXC_HANDLER); if (handlerAttr != null) { for (PhiInsn phi : phiList.getList()) { fixPhiInTryCatch(mth, phi, handlerAttr); } } } } } private static void fixPhiInTryCatch(MethodNode mth, PhiInsn phi, ExcHandlerAttr handlerAttr) { int argsCount = phi.getArgsCount(); int k = 0; while (k < argsCount) { RegisterArg arg = phi.getArg(k); if (shouldSkipInsnResult(mth, arg.getAssignInsn(), handlerAttr)) { phi.removeArg(arg); argsCount--; } else { k++; } } if (phi.getArgsCount() == 0) { throw new JadxRuntimeException("PHI empty after try-catch fix!"); } } private static boolean shouldSkipInsnResult(MethodNode mth, InsnNode insn, ExcHandlerAttr handlerAttr) { if (insn != null && insn.getResult() != null && insn.contains(AFlag.TRY_LEAVE)) { CatchAttr catchAttr = BlockUtils.getCatchAttrForInsn(mth, insn); return catchAttr != null && catchAttr.getHandlers().contains(handlerAttr.getHandler()); } return false; } private static boolean removeBlockerInsns(MethodNode mth) { boolean removed = false; for (BlockNode block : mth.getBasicBlocks()) { PhiListAttr phiList = block.get(AType.PHI_LIST); if (phiList == null) { continue; } // check if args must be removed for (PhiInsn phi : phiList.getList()) { for (int i = 0; i < phi.getArgsCount(); i++) { RegisterArg arg = phi.getArg(i); InsnNode parentInsn = arg.getAssignInsn(); if (parentInsn != null && parentInsn.contains(AFlag.REMOVE)) { phi.removeArg(arg); InsnRemover.remove(mth, block, parentInsn); removed = true; } } } } return removed; } private static void tryToFixUselessPhi(MethodNode mth) { int k = 0; int maxTries = mth.getSVars().size() * 2; while (fixUselessPhi(mth)) { if (k++ > maxTries) { throw new JadxRuntimeException("Phi nodes fix limit reached!"); } } } private static boolean fixUselessPhi(MethodNode mth) { boolean changed = false; List insnToRemove = new ArrayList<>(); for (SSAVar var : mth.getSVars()) { // phi result not used if (var.getUseCount() == 0) { InsnNode assignInsn = var.getAssign().getParentInsn(); if (assignInsn != null && assignInsn.getType() == InsnType.PHI) { insnToRemove.add((PhiInsn) assignInsn); changed = true; } } } for (BlockNode block : mth.getBasicBlocks()) { PhiListAttr phiList = block.get(AType.PHI_LIST); if (phiList == null) { continue; } Iterator it = phiList.getList().iterator(); while (it.hasNext()) { PhiInsn phi = it.next(); if (fixPhiWithSameArgs(mth, block, phi)) { it.remove(); changed = true; } } } removePhiList(mth, insnToRemove); return changed; } private static boolean fixPhiWithSameArgs(MethodNode mth, BlockNode block, PhiInsn phi) { if (phi.getArgsCount() == 0) { for (RegisterArg useArg : phi.getResult().getSVar().getUseList()) { InsnNode useInsn = useArg.getParentInsn(); if (useInsn != null && useInsn.getType() == InsnType.PHI) { phi.removeArg(useArg); } } InsnRemover.remove(mth, block, phi); return true; } boolean allSame = phi.getArgsCount() == 1 || isSameArgs(phi); if (allSame) { return replacePhiWithMove(mth, block, phi, phi.getArg(0)); } SSAVar sameVar = isSameMove(phi); if (sameVar != null) { RegisterArg sameArg = sameVar.getAssign().duplicate(); if (inlinePhiInsn(mth, block, phi, sameArg)) { for (InsnArg arg : phi.getArguments()) { InsnNode moveInsn = ((RegisterArg) arg).getAssignInsn(); if (moveInsn != null) { moveInsn.add(AFlag.REMOVE); InsnRemover.remove(mth, moveInsn); } } return true; } } return false; } private static boolean isSameArgs(PhiInsn phi) { boolean allSame = true; SSAVar var = null; for (int i = 0; i < phi.getArgsCount(); i++) { RegisterArg arg = phi.getArg(i); if (var == null) { var = arg.getSVar(); } else if (var != arg.getSVar()) { allSame = false; break; } } return allSame; } private static SSAVar isSameMove(PhiInsn phi) { SSAVar var = null; int argsCount = phi.getArgsCount(); for (int i = 0; i < argsCount; i++) { RegisterArg arg = phi.getArg(i); if (arg.getSVar().getUseCount() != 1) { return null; } InsnNode assignInsn = arg.getAssignInsn(); if (assignInsn == null || assignInsn.getType() != InsnType.MOVE) { return null; } InsnArg moveArg = assignInsn.getArg(0); if (!moveArg.isRegister()) { return null; } SSAVar moveVar = ((RegisterArg) moveArg).getSVar(); if (var == null) { var = moveVar; } else if (var != moveVar) { return null; } } return var; } private static boolean removePhiList(MethodNode mth, List insnToRemove) { for (BlockNode block : mth.getBasicBlocks()) { PhiListAttr phiList = block.get(AType.PHI_LIST); if (phiList == null) { continue; } List list = phiList.getList(); for (PhiInsn phiInsn : insnToRemove) { if (list.remove(phiInsn)) { for (InsnArg arg : phiInsn.getArguments()) { if (arg == null) { continue; } SSAVar sVar = ((RegisterArg) arg).getSVar(); if (sVar != null) { sVar.removeUsedInPhi(phiInsn); } } InsnRemover.remove(mth, block, phiInsn); } } if (list.isEmpty()) { block.remove(AType.PHI_LIST); } } insnToRemove.clear(); return true; } private static boolean replacePhiWithMove(MethodNode mth, BlockNode block, PhiInsn phi, RegisterArg arg) { List insns = block.getInstructions(); int phiIndex = InsnList.getIndex(insns, phi); if (phiIndex == -1) { return false; } SSAVar assign = phi.getResult().getSVar(); SSAVar argVar = arg.getSVar(); if (argVar != null) { argVar.removeUse(arg); argVar.removeUsedInPhi(phi); } // try inline if (inlinePhiInsn(mth, block, phi, phi.getArg(0))) { insns.remove(phiIndex); } else { assign.removeUsedInPhi(phi); InsnNode m = new InsnNode(InsnType.MOVE, 1); m.add(AFlag.SYNTHETIC); m.setResult(phi.getResult()); m.addArg(arg); arg.getSVar().use(arg); insns.set(phiIndex, m); } return true; } private static boolean inlinePhiInsn(MethodNode mth, BlockNode block, PhiInsn phi, RegisterArg inlineArg) { SSAVar resVar = phi.getResult().getSVar(); if (resVar == null) { return false; } if (inlineArg.getSVar() == null) { return false; } List useList = resVar.getUseList(); for (RegisterArg useArg : new ArrayList<>(useList)) { InsnNode useInsn = useArg.getParentInsn(); if (useInsn == null || useInsn == phi) { return false; } if (useArg.getRegNum() == inlineArg.getRegNum()) { // replace SSAVar in 'useArg' to SSAVar from 'arg' // no need to replace whole RegisterArg useArg.getSVar().removeUse(useArg); inlineArg.getSVar().use(useArg); } else { if (!useInsn.replaceArg(useArg, inlineArg)) { return false; } } } if (block.contains(AType.EXC_HANDLER)) { // don't inline into exception handler InsnNode assignInsn = inlineArg.getAssignInsn(); if (assignInsn != null && !assignInsn.isConstInsn()) { assignInsn.add(AFlag.DONT_INLINE); } } InsnRemover.unbindInsn(mth, phi); return true; } private static void markThisArgs(RegisterArg thisArg) { if (thisArg != null) { markOneArgAsThis(thisArg); thisArg.getSVar().getUseList().forEach(SSATransform::markOneArgAsThis); } } private static void markOneArgAsThis(RegisterArg arg) { if (arg == null) { return; } arg.add(AFlag.THIS); arg.add(AFlag.IMMUTABLE_TYPE); // mark all moved 'this' InsnNode parentInsn = arg.getParentInsn(); if (parentInsn != null && parentInsn.getType() == InsnType.MOVE && parentInsn.getArg(0) == arg) { RegisterArg resArg = parentInsn.getResult(); if (resArg.getRegNum() != arg.getRegNum() && !resArg.getSVar().isUsedInPhi()) { markThisArgs(resArg); parentInsn.add(AFlag.DONT_GENERATE); } } } private static void hidePhiInsns(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { block.getInstructions().removeIf(insn -> insn.getType() == InsnType.PHI); } } private static void removeUnusedInvokeResults(MethodNode mth) { Iterator it = mth.getSVars().iterator(); while (it.hasNext()) { SSAVar ssaVar = it.next(); if (ssaVar.getUseCount() == 0) { InsnNode parentInsn = ssaVar.getAssign().getParentInsn(); if (parentInsn != null && parentInsn.getType() == InsnType.INVOKE) { parentInsn.setResult(null); it.remove(); } } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/AbstractTypeConstraint.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.ArrayList; import java.util.List; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.Utils; public abstract class AbstractTypeConstraint implements ITypeConstraint { protected final InsnNode insn; protected final List relatedVars; public AbstractTypeConstraint(InsnNode insn, InsnArg arg) { this.insn = insn; this.relatedVars = collectRelatedVars(insn, arg); } private List collectRelatedVars(InsnNode insn, InsnArg arg) { List list = new ArrayList<>(insn.getArgsCount()); if (insn.getResult() == arg) { for (InsnArg insnArg : insn.getArguments()) { if (insnArg.isRegister()) { list.add(((RegisterArg) insnArg).getSVar()); } } } else { list.add(insn.getResult().getSVar()); for (InsnArg insnArg : insn.getArguments()) { if (insnArg != arg && insnArg.isRegister()) { list.add(((RegisterArg) insnArg).getSVar()); } } } return list; } @Override public List getRelatedVars() { return relatedVars; } @Override public String toString() { return "(" + insn.getType() + ':' + Utils.listToString(relatedVars, SSAVar::toShortString) + ')'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/BoundEnum.java ================================================ package jadx.core.dex.visitors.typeinference; public enum BoundEnum { ASSIGN, USE } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/FinishTypeInference.java ================================================ package jadx.core.dex.visitors.typeinference; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; @JadxVisitor( name = "Finish Type Inference", desc = "Check used types", runAfter = { TypeInferenceVisitor.class } ) public final class FinishTypeInference extends AbstractVisitor { @Override public void visit(MethodNode mth) { if (mth.isNoCode() || mth.getSVars().isEmpty()) { return; } mth.getSVars().forEach(var -> { ArgType type = var.getTypeInfo().getType(); if (!type.isTypeKnown()) { mth.addWarnComment("Type inference failed for: " + var.getDetailedVarInfo(mth)); } ArgType codeVarType = var.getCodeVar().getType(); if (codeVarType == null) { var.getCodeVar().setType(ArgType.UNKNOWN); } }); } @Override public String getName() { return "FinishTypeInference"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/FixTypesVisitor.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.clsp.ClspGraph; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.PhiListAttr; import jadx.core.dex.instructions.ArithNode; import jadx.core.dex.instructions.ArithOp; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.InitCodeVariables; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.ModVisitor; import jadx.core.dex.visitors.blocks.BlockSplitter; import jadx.core.utils.BlockUtils; import jadx.core.utils.InsnList; import jadx.core.utils.InsnUtils; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxOverflowException; @JadxVisitor( name = "Fix Types Visitor", desc = "Try various methods to fix unresolved types", runAfter = { TypeInferenceVisitor.class }, runBefore = { FinishTypeInference.class } ) public final class FixTypesVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(FixTypesVisitor.class); private final TypeInferenceVisitor typeInference = new TypeInferenceVisitor(); private TypeUpdate typeUpdate; private List> resolvers; @Override public void init(RootNode root) { this.typeUpdate = root.getTypeUpdate(); this.typeInference.init(root); this.resolvers = Arrays.asList( this::applyFieldType, this::tryRestoreTypeVarCasts, this::tryInsertCasts, this::tryDeduceTypes, this::trySplitConstInsns, this::tryToFixIncompatiblePrimitives, this::tryToForceImmutableTypes, this::tryInsertAdditionalMove, this::runMultiVariableSearch, this::tryRemoveGenerics); } @Override public void visit(MethodNode mth) { if (mth.isNoCode() || checkTypes(mth)) { return; } try { for (Function resolver : resolvers) { if (resolver.apply(mth) && checkTypes(mth)) { break; } } } catch (Exception e) { mth.addError("Types fix failed", e); } } /** * Check if all types resolved */ private static boolean checkTypes(MethodNode mth) { for (SSAVar var : mth.getSVars()) { ArgType type = var.getTypeInfo().getType(); if (!type.isTypeKnown()) { return false; } } return true; } private boolean runMultiVariableSearch(MethodNode mth) { try { TypeSearch typeSearch = new TypeSearch(mth); if (!typeSearch.run()) { mth.addWarnComment("Multi-variable type inference failed"); } for (SSAVar var : mth.getSVars()) { if (!var.getTypeInfo().getType().isTypeKnown()) { return false; } } return true; } catch (Exception e) { mth.addWarnComment("Multi-variable type inference failed. Error: " + Utils.getStackTrace(e)); return false; } } private boolean setBestType(MethodNode mth, SSAVar ssaVar) { try { return calculateFromBounds(mth, ssaVar); } catch (JadxOverflowException e) { throw e; } catch (Exception e) { mth.addWarnComment("Failed to calculate best type for var: " + ssaVar, e); return false; } } private boolean calculateFromBounds(MethodNode mth, SSAVar ssaVar) { TypeInfo typeInfo = ssaVar.getTypeInfo(); Set bounds = typeInfo.getBounds(); Optional bestTypeOpt = selectBestTypeFromBounds(bounds); if (bestTypeOpt.isEmpty()) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.warn("Failed to select best type from bounds, count={} : ", bounds.size()); for (ITypeBound bound : bounds) { LOG.warn(" {}", bound); } } return false; } ArgType candidateType = bestTypeOpt.get(); TypeUpdateResult result = typeUpdate.apply(mth, ssaVar, candidateType); if (result == TypeUpdateResult.REJECT) { if (Consts.DEBUG_TYPE_INFERENCE) { if (ssaVar.getTypeInfo().getType().equals(candidateType)) { LOG.info("Same type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds); } else if (candidateType.isTypeKnown()) { LOG.debug("Type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds); } } return false; } return result == TypeUpdateResult.CHANGED; } private Optional selectBestTypeFromBounds(Set bounds) { return bounds.stream() .map(ITypeBound::getType) .filter(Objects::nonNull) .max(typeUpdate.getTypeCompare().getComparator()); } private boolean tryPossibleTypes(MethodNode mth, SSAVar var, ArgType type) { List types = makePossibleTypesList(type, var); if (types.isEmpty()) { return false; } for (ArgType candidateType : types) { TypeUpdateResult result = typeUpdate.apply(mth, var, candidateType); if (result == TypeUpdateResult.CHANGED) { return true; } } return false; } private List makePossibleTypesList(ArgType type, @Nullable SSAVar var) { if (type.isArray()) { List list = new ArrayList<>(); for (ArgType arrElemType : makePossibleTypesList(type.getArrayElement(), null)) { list.add(ArgType.array(arrElemType)); } return list; } if (var != null) { for (ITypeBound b : var.getTypeInfo().getBounds()) { ArgType boundType = b.getType(); if (boundType.isObject() || boundType.isArray()) { // don't add primitive types return Collections.emptyList(); } } } List list = new ArrayList<>(); for (PrimitiveType possibleType : type.getPossibleTypes()) { if (possibleType == PrimitiveType.VOID) { continue; } list.add(ArgType.convertFromPrimitiveType(possibleType)); } return list; } private boolean tryDeduceTypes(MethodNode mth) { boolean fixed = false; for (SSAVar ssaVar : mth.getSVars()) { if (deduceType(mth, ssaVar)) { fixed = true; } } return fixed; } @SuppressWarnings("RedundantIfStatement") private boolean deduceType(MethodNode mth, SSAVar var) { if (var.isTypeImmutable()) { return false; } ArgType type = var.getTypeInfo().getType(); if (type.isTypeKnown()) { return false; } // try best type from bounds again if (setBestType(mth, var)) { return true; } // try all possible types (useful for primitives) if (tryPossibleTypes(mth, var, type)) { return true; } // for objects try super types if (tryWiderObjects(mth, var)) { return true; } return false; } private boolean tryRemoveGenerics(MethodNode mth) { boolean resolved = true; for (SSAVar var : mth.getSVars()) { ArgType type = var.getTypeInfo().getType(); if (!type.isTypeKnown() && !var.isTypeImmutable() && !tryRawType(mth, var)) { resolved = false; } } return resolved; } private boolean tryRawType(MethodNode mth, SSAVar var) { Set objTypes = new LinkedHashSet<>(); for (ITypeBound bound : var.getTypeInfo().getBounds()) { ArgType boundType = bound.getType(); if (boundType.isTypeKnown() && boundType.isObject()) { objTypes.add(boundType); } } if (objTypes.isEmpty()) { return false; } for (ArgType objType : objTypes) { if (checkRawType(mth, var, objType)) { mth.addDebugComment("Type inference failed for " + var.toShortString() + "." + " Raw type applied. Possible types: " + Utils.listToString(objTypes)); return true; } } return false; } private boolean checkRawType(MethodNode mth, SSAVar var, ArgType objType) { if (objType.isObject() && objType.containsGeneric()) { ArgType rawType = objType.isGenericType() ? ArgType.OBJECT : ArgType.object(objType.getObject()); TypeUpdateResult result = typeUpdate.applyWithWiderAllow(mth, var, rawType); return result == TypeUpdateResult.CHANGED; } return false; } /** * Use type for var assigned from field (IGET or SGET). * Insert additional casts at var use places. */ private Boolean applyFieldType(MethodNode mth) { try { boolean changed = false; // will add new SSA vars, can't use for-each loop List sVars = mth.getSVars(); for (int i = 0, varsCount = sVars.size(); i < varsCount; i++) { SSAVar ssaVar = sVars.get(i); if (tryFieldTypeWithNewCasts(mth, ssaVar, true)) { changed = true; } } if (!changed) { return false; } // rerun full type inference InitCodeVariables.rerun(mth); typeInference.initTypeBounds(mth); typeInference.runTypePropagation(mth); // check if changed var types are fixed boolean success = true; for (SSAVar ssaVar : mth.getSVars()) { if (tryFieldTypeWithNewCasts(mth, ssaVar, false)) { success = false; } } if (!success) { typeInference.initTypeBounds(mth); typeInference.runTypePropagation(mth); mth.addWarnComment("Type inference incomplete: some casts might be missing"); } return success; } catch (Exception e) { mth.addWarnComment("Type inference fix 'apply assigned field type' failed", e); return false; } } private boolean tryFieldTypeWithNewCasts(MethodNode mth, SSAVar ssaVar, boolean insertCasts) { ArgType type = ssaVar.getTypeInfo().getType(); if (type.isTypeKnown() || ssaVar.isTypeImmutable()) { return false; } InsnNode assignInsn = ssaVar.getAssignInsn(); if (assignInsn == null) { return false; } InsnType insnType = assignInsn.getType(); if (insnType != InsnType.IGET && insnType != InsnType.SGET) { return false; } ArgType fieldType = assignInsn.getResult().getInitType(); // field type should be used if (insertCasts) { // try to find a use place and insert cast boolean inserted = false; for (RegisterArg useArg : ssaVar.getUseList()) { if (insertExplicitUseCast(mth, ssaVar, useArg, fieldType)) { inserted = true; } } return inserted; } // force field type, will make type inference incomplete, // but it is better that completely unknown type ssaVar.setType(fieldType); return true; } private boolean insertExplicitUseCast(MethodNode mth, SSAVar ssaVar, RegisterArg useArg, ArgType fieldType) { InsnNode parentInsn = useArg.getParentInsn(); if (!InsnUtils.isInsnType(parentInsn, InsnType.INVOKE)) { return false; } InvokeNode invoke = (InvokeNode) parentInsn; InsnArg instanceArg = invoke.getInstanceArg(); if (instanceArg == null || !instanceArg.isSameVar(ssaVar)) { return false; } IMethodDetails details = mth.root().getMethodUtils().getMethodDetails(invoke); if (details == null) { return false; } int newCasts = 0; int k = -1; for (InsnArg invArg : invoke.getArgList()) { if (invArg == instanceArg) { continue; } k++; if (!invArg.isRegister()) { continue; } ArgType detailsArg = details.getArgTypes().get(k); ArgType invArgType = invArg.getType(); ArgType resolvedType = mth.root().getTypeUtils().replaceClassGenerics(fieldType, invArgType, detailsArg); if (resolvedType != null && !resolvedType.equals(invArgType)) { IndexInsnNode castInsn = insertUseCast(mth, (RegisterArg) invArg, resolvedType); if (castInsn != null) { castInsn.add(AFlag.EXPLICIT_CAST); newCasts++; } } } return newCasts > 0; } /** * Fix check casts to type var extend type: *
* {@code T var = (Comparable) obj; => T var = (T) obj; } */ private boolean tryRestoreTypeVarCasts(MethodNode mth) { int changed = 0; List mthSVars = mth.getSVars(); for (SSAVar var : mthSVars) { changed += restoreTypeVarCasts(var); } if (changed == 0) { return false; } if (Consts.DEBUG_TYPE_INFERENCE) { mth.addDebugComment("Restore " + changed + " type vars casts"); } typeInference.initTypeBounds(mth); return typeInference.runTypePropagation(mth); } private int restoreTypeVarCasts(SSAVar var) { TypeInfo typeInfo = var.getTypeInfo(); Set bounds = typeInfo.getBounds(); if (!ListUtils.anyMatch(bounds, t -> t.getType().isGenericType())) { return 0; } List casts = ListUtils.filter(bounds, TypeBoundCheckCastAssign.class::isInstance); if (casts.isEmpty()) { return 0; } ArgType bestType = selectBestTypeFromBounds(bounds).orElse(ArgType.UNKNOWN); if (!bestType.isGenericType()) { return 0; } List extendTypes = bestType.getExtendTypes(); if (extendTypes.size() != 1) { return 0; } int fixed = 0; ArgType extendType = extendTypes.get(0); for (ITypeBound bound : casts) { TypeBoundCheckCastAssign cast = (TypeBoundCheckCastAssign) bound; ArgType castType = cast.getType(); TypeCompareEnum result = typeUpdate.getTypeCompare().compareTypes(extendType, castType); if (result.isEqual() || result == TypeCompareEnum.NARROW_BY_GENERIC) { cast.getInsn().updateIndex(bestType); fixed++; } } return fixed; } @SuppressWarnings({ "ForLoopReplaceableByWhile", "ForLoopReplaceableByForEach" }) private boolean tryInsertCasts(MethodNode mth) { int added = 0; List mthSVars = mth.getSVars(); int varsCount = mthSVars.size(); for (int i = 0; i < varsCount; i++) { SSAVar var = mthSVars.get(i); ArgType type = var.getTypeInfo().getType(); if (!type.isTypeKnown() && !var.isTypeImmutable()) { added += tryInsertVarCast(mth, var); } } if (added != 0) { InitCodeVariables.rerun(mth); typeInference.initTypeBounds(mth); return typeInference.runTypePropagation(mth); } return false; } private int tryInsertVarCast(MethodNode mth, SSAVar var) { for (ITypeBound bound : var.getTypeInfo().getBounds()) { ArgType boundType = bound.getType(); if (boundType.isTypeKnown() && !boundType.equals(var.getTypeInfo().getType()) && boundType.containsTypeVariable() && !mth.root().getTypeUtils().containsUnknownTypeVar(mth, boundType)) { IndexInsnNode castInsn = insertAssignCast(mth, var, boundType); if (castInsn != null) { castInsn.add(AFlag.SOFT_CAST); return 1; } return insertUseCasts(mth, var); } } return 0; } private int insertUseCasts(MethodNode mth, SSAVar var) { List useList = var.getUseList(); if (useList.isEmpty()) { return 0; } int useCasts = 0; for (RegisterArg useReg : new ArrayList<>(useList)) { IndexInsnNode castInsn = insertUseCast(mth, useReg, useReg.getInitType()); if (castInsn != null) { castInsn.add(AFlag.SOFT_CAST); useCasts++; } } return useCasts; } private @Nullable IndexInsnNode insertAssignCast(MethodNode mth, SSAVar var, ArgType castType) { RegisterArg assignArg = var.getAssign(); InsnNode assignInsn = assignArg.getParentInsn(); if (assignInsn == null || assignInsn.getType() == InsnType.PHI) { return null; } BlockNode assignBlock = BlockUtils.getBlockByInsn(mth, assignInsn); if (assignBlock == null) { return null; } assignInsn.setResult(assignArg.duplicateWithNewSSAVar(mth)); IndexInsnNode castInsn = makeCastInsn(assignArg.duplicate(), assignInsn.getResult().duplicate(), castType); if (!BlockUtils.insertAfterInsn(assignBlock, assignInsn, castInsn)) { return null; } return castInsn; } private @Nullable IndexInsnNode insertUseCast(MethodNode mth, RegisterArg useArg, ArgType castType) { InsnNode useInsn = useArg.getParentInsn(); if (useInsn == null || useInsn.getType() == InsnType.PHI) { return null; } if (useInsn.getType() == InsnType.IF && useInsn.getArg(1).isZeroConst()) { // cast isn't needed if compare with null return null; } BlockNode useBlock = BlockUtils.getBlockByInsn(mth, useInsn); if (useBlock == null) { return null; } IndexInsnNode castInsn = makeCastInsn( useArg.duplicateWithNewSSAVar(mth), useArg.duplicate(), castType); useInsn.replaceArg(useArg, castInsn.getResult().duplicate()); boolean inserted = BlockUtils.insertBeforeInsn(useBlock, useInsn, castInsn); if (!inserted) { return null; } if (Consts.DEBUG_TYPE_INFERENCE) { LOG.info("Insert cast for {} before {} in {}", useArg, useInsn, useBlock); } return castInsn; } private IndexInsnNode makeCastInsn(RegisterArg result, RegisterArg arg, ArgType castType) { IndexInsnNode castInsn = new IndexInsnNode(InsnType.CHECK_CAST, castType, 1); castInsn.setResult(result); castInsn.addArg(arg); castInsn.add(AFlag.SYNTHETIC); return castInsn; } private boolean trySplitConstInsns(MethodNode mth) { boolean constSplit = false; for (SSAVar var : new ArrayList<>(mth.getSVars())) { if (checkAndSplitConstInsn(mth, var)) { constSplit = true; } } if (!constSplit) { return false; } InitCodeVariables.rerun(mth); typeInference.initTypeBounds(mth); return typeInference.runTypePropagation(mth); } private boolean checkAndSplitConstInsn(MethodNode mth, SSAVar var) { ArgType type = var.getTypeInfo().getType(); if (type.isTypeKnown() || var.isTypeImmutable()) { return false; } return splitByPhi(mth, var) || dupConst(mth, var); } private boolean dupConst(MethodNode mth, SSAVar var) { InsnNode assignInsn = var.getAssign().getAssignInsn(); if (!InsnUtils.isInsnType(assignInsn, InsnType.CONST)) { return false; } if (var.getUseList().size() < 2) { return false; } BlockNode assignBlock = BlockUtils.getBlockByInsn(mth, assignInsn); if (assignBlock == null) { return false; } assignInsn.remove(AFlag.DONT_INLINE); int insertIndex = 1 + BlockUtils.getInsnIndexInBlock(assignBlock, assignInsn); List useList = new ArrayList<>(var.getUseList()); for (int i = 0, useCount = useList.size(); i < useCount; i++) { RegisterArg useArg = useList.get(i); useArg.remove(AFlag.DONT_INLINE_CONST); if (i == 0) { continue; } InsnNode useInsn = useArg.getParentInsn(); if (useInsn == null) { continue; } InsnNode newInsn = assignInsn.copyWithNewSsaVar(mth); assignBlock.getInstructions().add(insertIndex, newInsn); useInsn.replaceArg(useArg, newInsn.getResult().duplicate()); } if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Duplicate const insn {} times: {} in {}", useList.size(), assignInsn, assignBlock); } return true; } /** * For every PHI make separate CONST insn */ private static boolean splitByPhi(MethodNode mth, SSAVar var) { if (var.getUsedInPhi().size() < 2) { return false; } InsnNode assignInsn = var.getAssign().getAssignInsn(); InsnNode constInsn = InsnUtils.checkInsnType(assignInsn, InsnType.CONST); if (constInsn == null) { return false; } BlockNode blockNode = BlockUtils.getBlockByInsn(mth, constInsn); if (blockNode == null) { return false; } boolean first = true; for (PhiInsn phiInsn : var.getUsedInPhi()) { if (first) { first = false; continue; } InsnNode copyInsn = constInsn.copyWithNewSsaVar(mth); copyInsn.add(AFlag.SYNTHETIC); BlockUtils.insertAfterInsn(blockNode, constInsn, copyInsn); RegisterArg phiArg = phiInsn.getArgBySsaVar(var); phiInsn.replaceArg(phiArg, copyInsn.getResult().duplicate()); } return true; } private boolean tryInsertAdditionalMove(MethodNode mth) { int insnsAdded = 0; for (BlockNode block : mth.getBasicBlocks()) { PhiListAttr phiListAttr = block.get(AType.PHI_LIST); if (phiListAttr != null) { for (PhiInsn phiInsn : phiListAttr.getList()) { insnsAdded += tryInsertAdditionalInsn(mth, phiInsn); } } } if (insnsAdded == 0) { return false; } if (Consts.DEBUG_TYPE_INFERENCE) { mth.addDebugComment("Additional " + insnsAdded + " move instructions added to help type inference"); } InitCodeVariables.rerun(mth); typeInference.initTypeBounds(mth); if (typeInference.runTypePropagation(mth) && checkTypes(mth)) { return true; } return tryDeduceTypes(mth); } /** * Add MOVE instruction before PHI in bound blocks to make 'soft' type link. * This allows using different types in blocks merged by PHI. */ private int tryInsertAdditionalInsn(MethodNode mth, PhiInsn phiInsn) { ArgType phiType = getCommonTypeForPhiArgs(phiInsn); if (phiType != null && phiType.isTypeKnown()) { // all args have the same known type => nothing to do here return 0; } // check if instructions can be inserted if (insertMovesForPhi(mth, phiInsn, false) == 0) { return 0; } // check passed => apply return insertMovesForPhi(mth, phiInsn, true); } @Nullable private ArgType getCommonTypeForPhiArgs(PhiInsn phiInsn) { ArgType phiArgType = null; for (InsnArg arg : phiInsn.getArguments()) { ArgType type = arg.getType(); if (phiArgType == null) { phiArgType = type; } else if (!phiArgType.equals(type)) { return null; } } return phiArgType; } private int insertMovesForPhi(MethodNode mth, PhiInsn phiInsn, boolean apply) { int argsCount = phiInsn.getArgsCount(); int count = 0; for (int argIndex = 0; argIndex < argsCount; argIndex++) { RegisterArg reg = phiInsn.getArg(argIndex); BlockNode startBlock = phiInsn.getBlockByArgIndex(argIndex); BlockNode blockNode = checkBlockForInsnInsert(startBlock); if (blockNode == null) { mth.addDebugComment("Failed to insert an additional move for type inference into block " + startBlock); return 0; } boolean add = true; SSAVar var = reg.getSVar(); InsnNode assignInsn = var.getAssign().getAssignInsn(); if (assignInsn != null) { InsnType assignType = assignInsn.getType(); if (assignType == InsnType.CONST || (assignType == InsnType.MOVE && var.getUseCount() == 1)) { add = false; } } if (add) { count++; if (apply) { insertMove(mth, blockNode, phiInsn, reg); } } } return count; } private void insertMove(MethodNode mth, BlockNode blockNode, PhiInsn phiInsn, RegisterArg reg) { SSAVar var = reg.getSVar(); int regNum = reg.getRegNum(); RegisterArg resultArg = reg.duplicate(regNum, null); SSAVar newSsaVar = mth.makeNewSVar(resultArg); RegisterArg arg = reg.duplicate(regNum, var); InsnNode moveInsn = new InsnNode(InsnType.MOVE, 1); moveInsn.setResult(resultArg); moveInsn.addArg(arg); moveInsn.add(AFlag.SYNTHETIC); blockNode.getInstructions().add(moveInsn); phiInsn.replaceArg(reg, reg.duplicate(regNum, newSsaVar)); } @Nullable private BlockNode checkBlockForInsnInsert(BlockNode blockNode) { if (blockNode.isSynthetic()) { return null; } InsnNode lastInsn = BlockUtils.getLastInsn(blockNode); if (lastInsn != null && BlockSplitter.isSeparate(lastInsn.getType())) { // can't insert move in a block with 'separate' instruction => try previous block by simple path List preds = blockNode.getPredecessors(); if (preds.size() == 1) { return checkBlockForInsnInsert(preds.get(0)); } return null; } return blockNode; } private boolean tryWiderObjects(MethodNode mth, SSAVar var) { Set objTypes = new LinkedHashSet<>(); for (ITypeBound bound : var.getTypeInfo().getBounds()) { ArgType boundType = bound.getType(); if (boundType.isTypeKnown() && boundType.isObject()) { objTypes.add(boundType); } } if (objTypes.isEmpty()) { return false; } ClspGraph clsp = mth.root().getClsp(); for (ArgType objType : objTypes) { for (String ancestor : clsp.getSuperTypes(objType.getObject())) { ArgType ancestorType = ArgType.object(ancestor); TypeUpdateResult result = typeUpdate.applyWithWiderAllow(mth, var, ancestorType); if (result == TypeUpdateResult.CHANGED) { return true; } } } return false; } @SuppressWarnings("ForLoopReplaceableByForEach") private boolean tryToFixIncompatiblePrimitives(MethodNode mth) { boolean fixed = false; List ssaVars = mth.getSVars(); int ssaVarsCount = ssaVars.size(); // new vars will be added at a list end if fix is applied (can't use for-each loop here) for (int i = 0; i < ssaVarsCount; i++) { if (processIncompatiblePrimitives(mth, ssaVars.get(i))) { fixed = true; } } if (!fixed) { return false; } InitCodeVariables.rerun(mth); typeInference.initTypeBounds(mth); return typeInference.runTypePropagation(mth); } private boolean processIncompatiblePrimitives(MethodNode mth, SSAVar var) { TypeInfo typeInfo = var.getTypeInfo(); if (typeInfo.getType().isTypeKnown()) { return false; } boolean assigned = false; for (ITypeBound bound : typeInfo.getBounds()) { ArgType boundType = bound.getType(); switch (bound.getBound()) { case ASSIGN: if (!boundType.contains(PrimitiveType.BOOLEAN)) { return false; } assigned = true; break; case USE: if (!boundType.canBeAnyNumber()) { return false; } break; } } if (!assigned) { return false; } boolean fixed = false; for (RegisterArg arg : new ArrayList<>(var.getUseList())) { if (fixBooleanUsage(mth, arg)) { fixed = true; if (Consts.DEBUG_TYPE_INFERENCE) { LOG.info("Fixed boolean usage for arg {} from {}", arg, arg.getParentInsn()); } } } return fixed; } private boolean fixBooleanUsage(MethodNode mth, RegisterArg boundArg) { ArgType boundType = boundArg.getInitType(); if (boundType == ArgType.BOOLEAN || (boundType.isTypeKnown() && !boundType.isPrimitive())) { return false; } InsnNode insn = boundArg.getParentInsn(); if (insn == null || insn.getType() == InsnType.IF) { return false; } BlockNode blockNode = BlockUtils.getBlockByInsn(mth, insn); if (blockNode == null) { return false; } List insnList = blockNode.getInstructions(); int insnIndex = InsnList.getIndex(insnList, insn); if (insnIndex == -1) { return false; } InsnType insnType = insn.getType(); if (insnType == InsnType.CAST) { // replace cast ArgType type = (ArgType) ((IndexInsnNode) insn).getIndex(); TernaryInsn convertInsn = prepareBooleanConvertInsn(insn.getResult(), boundArg, type); BlockUtils.replaceInsn(mth, blockNode, insnIndex, convertInsn); return true; } if (insnType == InsnType.ARITH) { ArithNode arithInsn = (ArithNode) insn; if (arithInsn.getOp() == ArithOp.XOR && arithInsn.getArgsCount() == 2) { // replace (boolean ^ 1) with (!boolean) InsnArg secondArg = arithInsn.getArg(1); if (secondArg.isLiteral() && ((LiteralArg) secondArg).getLiteral() == 1) { InsnNode convertInsn = notBooleanToInt(arithInsn, boundArg); BlockUtils.replaceInsn(mth, blockNode, insnIndex, convertInsn); return true; } } } // insert before insn RegisterArg resultArg = boundArg.duplicateWithNewSSAVar(mth); TernaryInsn convertInsn = prepareBooleanConvertInsn(resultArg, boundArg, boundType); insnList.add(insnIndex, convertInsn); insn.replaceArg(boundArg, convertInsn.getResult().duplicate()); return true; } private InsnNode notBooleanToInt(ArithNode insn, RegisterArg boundArg) { InsnNode notInsn = new InsnNode(InsnType.NOT, 1); notInsn.addArg(boundArg.duplicate()); notInsn.add(AFlag.SYNTHETIC); ArgType resType = insn.getResult().getType(); if (resType.canBePrimitive(PrimitiveType.BOOLEAN)) { notInsn.setResult(insn.getResult()); return notInsn; } InsnArg notArg = InsnArg.wrapArg(notInsn); notArg.setType(ArgType.BOOLEAN); TernaryInsn convertInsn = ModVisitor.makeBooleanConvertInsn(insn.getResult(), notArg, ArgType.INT); convertInsn.add(AFlag.SYNTHETIC); return convertInsn; } private TernaryInsn prepareBooleanConvertInsn(RegisterArg resultArg, RegisterArg boundArg, ArgType useType) { RegisterArg useArg = boundArg.getSVar().getAssign().duplicate(); TernaryInsn convertInsn = ModVisitor.makeBooleanConvertInsn(resultArg, useArg, useType); convertInsn.add(AFlag.SYNTHETIC); return convertInsn; } private boolean tryToForceImmutableTypes(MethodNode mth) { boolean fixed = false; for (SSAVar ssaVar : mth.getSVars()) { ArgType type = ssaVar.getTypeInfo().getType(); if (!type.isTypeKnown() && ssaVar.isTypeImmutable()) { if (forceImmutableType(ssaVar)) { fixed = true; } } } if (!fixed) { return false; } return typeInference.runTypePropagation(mth); } private boolean forceImmutableType(SSAVar ssaVar) { for (RegisterArg useArg : ssaVar.getUseList()) { InsnNode parentInsn = useArg.getParentInsn(); if (parentInsn != null) { InsnType insnType = parentInsn.getType(); if (insnType == InsnType.AGET || insnType == InsnType.APUT) { ssaVar.setType(ssaVar.getImmutableType()); return true; } } } return false; } @Override public String getName() { return "FixTypesVisitor"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBound.java ================================================ package jadx.core.dex.visitors.typeinference; import org.jetbrains.annotations.Nullable; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.RegisterArg; /** * Information to restrict types by applying constraints (or boundaries) */ public interface ITypeBound { BoundEnum getBound(); ArgType getType(); @Nullable RegisterArg getArg(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBoundDynamic.java ================================================ package jadx.core.dex.visitors.typeinference; import jadx.core.dex.instructions.args.ArgType; /** * 'Dynamic' type bound allows to use requested and not yet applied types * from {@link TypeUpdateInfo} for more precise restrictions */ public interface ITypeBoundDynamic extends ITypeBound { /** * This method will be executed instead of {@link ITypeBound#getType()} * if {@link TypeUpdateInfo} is available. */ ArgType getType(TypeUpdateInfo updateInfo); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeConstraint.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.List; import jadx.core.dex.instructions.args.SSAVar; public interface ITypeConstraint { List getRelatedVars(); boolean check(TypeSearchState state); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeListener.java ================================================ package jadx.core.dex.visitors.typeinference; import org.jetbrains.annotations.NotNull; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.InsnNode; @FunctionalInterface public interface ITypeListener { /** * Listener function - triggered on type update * * @param updateInfo store all allowed type updates * @param arg apply suggested type for this arg * @param candidateType suggest new type */ TypeUpdateResult update(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, @NotNull ArgType candidateType); } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundCheckCastAssign.java ================================================ package jadx.core.dex.visitors.typeinference; import org.jetbrains.annotations.Nullable; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.RootNode; /** * Allow ignoring down casts (return arg type instead cast type) * Such casts will be removed later. */ public final class TypeBoundCheckCastAssign implements ITypeBoundDynamic { private final RootNode root; private final IndexInsnNode insn; public TypeBoundCheckCastAssign(RootNode root, IndexInsnNode insn) { this.root = root; this.insn = insn; } @Override public BoundEnum getBound() { return BoundEnum.ASSIGN; } @Override public ArgType getType(TypeUpdateInfo updateInfo) { return getReturnType(updateInfo.getType(insn.getArg(0))); } @Override public ArgType getType() { return getReturnType(insn.getArg(0).getType()); } private ArgType getReturnType(ArgType argType) { ArgType castType = insn.getIndexAsType(); TypeCompareEnum result = root.getTypeCompare().compareTypes(argType, castType); return result.isNarrow() ? argType : castType; } @Override public @Nullable RegisterArg getArg() { return insn.getResult(); } public IndexInsnNode getInsn() { return insn; } @Override public String toString() { return "CHECK_CAST_ASSIGN{(" + insn.getIndex() + ") " + insn.getArg(0).getType() + "}"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundConst.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.Objects; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.RegisterArg; public final class TypeBoundConst implements ITypeBound { private final BoundEnum bound; private final ArgType type; private final RegisterArg arg; public TypeBoundConst(BoundEnum bound, ArgType type) { this(bound, type, null); } public TypeBoundConst(BoundEnum bound, ArgType type, RegisterArg arg) { this.bound = bound; this.type = type; this.arg = arg; } @Override public BoundEnum getBound() { return bound; } @Override public ArgType getType() { return type; } @Override public RegisterArg getArg() { return arg; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } TypeBoundConst that = (TypeBoundConst) o; return bound == that.bound && Objects.equals(type, that.type); } @Override public int hashCode() { return Objects.hash(bound, type); } @Override public String toString() { return "{" + bound + ": " + type + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundFieldGetAssign.java ================================================ package jadx.core.dex.visitors.typeinference; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.RootNode; /** * Dynamic bound for instance field get of generic type. * Bound type calculated using instance generic type. */ public final class TypeBoundFieldGetAssign implements ITypeBoundDynamic { private final RootNode root; private final IndexInsnNode getNode; private final FieldInfo fieldInfo; private final ArgType initType; public TypeBoundFieldGetAssign(RootNode root, IndexInsnNode getNode, ArgType initType) { this.root = root; this.getNode = getNode; this.fieldInfo = (FieldInfo) getNode.getIndex(); this.initType = initType; } @Override public BoundEnum getBound() { return BoundEnum.ASSIGN; } @Override public ArgType getType(TypeUpdateInfo updateInfo) { return getResultType(updateInfo.getType(getInstanceArg())); } @Override public ArgType getType() { return getResultType(getInstanceArg().getType()); } private ArgType getResultType(ArgType instanceType) { ArgType resultGeneric = root.getTypeUtils().replaceClassGenerics(instanceType, initType); if (resultGeneric != null && !resultGeneric.isWildcard()) { return resultGeneric; } return initType; // TODO: check if this type is allowed in current scope } private InsnArg getInstanceArg() { return getNode.getArg(0); } @Override public RegisterArg getArg() { return getNode.getResult(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } TypeBoundFieldGetAssign that = (TypeBoundFieldGetAssign) o; return getNode.equals(that.getNode); } @Override public int hashCode() { return getNode.hashCode(); } @Override public String toString() { return "FieldGetAssign{" + fieldInfo + ", type=" + getType() + ", instanceArg=" + getInstanceArg() + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundInvokeAssign.java ================================================ package jadx.core.dex.visitors.typeinference; import org.jetbrains.annotations.Nullable; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.RootNode; /** * Special dynamic bound for invoke with generics. * Bound type calculated using instance generic type. * TODO: also can depends on argument types */ public final class TypeBoundInvokeAssign implements ITypeBoundDynamic { private final RootNode root; private final InvokeNode invokeNode; private final ArgType genericReturnType; public TypeBoundInvokeAssign(RootNode root, InvokeNode invokeNode, ArgType genericReturnType) { this.root = root; this.invokeNode = invokeNode; this.genericReturnType = genericReturnType; } @Override public BoundEnum getBound() { return BoundEnum.ASSIGN; } @Override public ArgType getType(TypeUpdateInfo updateInfo) { return getReturnType(updateInfo.getType(getInstanceArg())); } @Override public ArgType getType() { return getReturnType(getInstanceArg().getType()); } private ArgType getReturnType(ArgType instanceType) { ArgType mthDeclType; IMethodDetails methodDetails = root.getMethodUtils().getMethodDetails(invokeNode); if (methodDetails != null) { // use methods detail to resolve declaration class for virtual invokes mthDeclType = methodDetails.getMethodInfo().getDeclClass().getType(); } else { mthDeclType = instanceType; } ArgType resultGeneric = root.getTypeUtils().replaceClassGenerics(instanceType, mthDeclType, genericReturnType); ArgType result = processResultType(resultGeneric); if (result != null) { return result; } return invokeNode.getCallMth().getReturnType(); } @Nullable private ArgType processResultType(@Nullable ArgType resultGeneric) { if (resultGeneric == null) { return null; } if (!resultGeneric.isWildcard()) { return resultGeneric; } return resultGeneric.getWildcardType(); } private InsnArg getInstanceArg() { return invokeNode.getArg(0); } @Override public RegisterArg getArg() { return invokeNode.getResult(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } TypeBoundInvokeAssign that = (TypeBoundInvokeAssign) o; return invokeNode.equals(that.invokeNode); } @Override public int hashCode() { return invokeNode.hashCode(); } @Override public String toString() { return "InvokeAssign{" + invokeNode.getCallMth().getShortId() + ", returnType=" + genericReturnType + ", currentType=" + getType() + ", instanceArg=" + getInstanceArg() + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundInvokeUse.java ================================================ package jadx.core.dex.visitors.typeinference; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.RootNode; /** * Special dynamic bound for invoke with generics. * Arguments bound type calculated using instance generic type. */ public final class TypeBoundInvokeUse implements ITypeBoundDynamic { private final RootNode root; private final BaseInvokeNode invokeNode; private final RegisterArg arg; private final ArgType genericArgType; public TypeBoundInvokeUse(RootNode root, BaseInvokeNode invokeNode, RegisterArg arg, ArgType genericArgType) { this.root = root; this.invokeNode = invokeNode; this.arg = arg; this.genericArgType = genericArgType; } @Override public BoundEnum getBound() { return BoundEnum.USE; } @Override public ArgType getType(TypeUpdateInfo updateInfo) { return getArgType(updateInfo.getType(invokeNode.getInstanceArg()), updateInfo.getType(arg)); } @Override public ArgType getType() { return getArgType(invokeNode.getInstanceArg().getType(), arg.getType()); } private ArgType getArgType(ArgType instanceType, ArgType argType) { ArgType resultGeneric = root.getTypeUtils().replaceClassGenerics(instanceType, genericArgType); if (resultGeneric != null) { return resultGeneric; } return argType; } @Override public RegisterArg getArg() { return arg; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } TypeBoundInvokeUse that = (TypeBoundInvokeUse) o; return invokeNode.equals(that.invokeNode); } @Override public int hashCode() { return invokeNode.hashCode(); } @Override public String toString() { return "InvokeAssign{" + invokeNode.getCallMth().getShortId() + ", argType=" + genericArgType + ", currentType=" + getType() + ", instanceArg=" + invokeNode.getInstanceArg() + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompare.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.ArgType.WildcardBound; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.CONFLICT; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.CONFLICT_BY_GENERIC; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.EQUAL; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.NARROW; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.NARROW_BY_GENERIC; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.UNKNOWN; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.WIDER; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.WIDER_BY_GENERIC; import static jadx.core.utils.Utils.isEmpty; public class TypeCompare { private static final Logger LOG = LoggerFactory.getLogger(TypeCompare.class); private final RootNode root; private final Comparator comparator; private final Comparator reversedComparator; public TypeCompare(RootNode root) { this.root = root; this.comparator = new ArgTypeComparator(); this.reversedComparator = comparator.reversed(); } public TypeCompareEnum compareTypes(ClassNode first, ClassNode second) { return compareObjects(first.getType(), second.getType()); } public TypeCompareEnum compareTypes(ClassInfo first, ClassInfo second) { return compareObjects(first.getType(), second.getType()); } public TypeCompareEnum compareObjects(ArgType first, ArgType second) { if (first == second || Objects.equals(first, second)) { return TypeCompareEnum.EQUAL; } return compareObjectsNoPreCheck(first, second); } /** * Compare two type and return result for first argument (narrow, wider or conflict) */ public TypeCompareEnum compareTypes(ArgType first, ArgType second) { if (first == second || Objects.equals(first, second)) { return TypeCompareEnum.EQUAL; } boolean firstKnown = first.isTypeKnown(); boolean secondKnown = second.isTypeKnown(); if (firstKnown != secondKnown) { if (firstKnown) { return compareWithUnknown(first, second); } else { return compareWithUnknown(second, first).invert(); } } boolean firstArray = first.isArray(); boolean secondArray = second.isArray(); if (firstArray != secondArray) { if (firstArray) { return compareArrayWithOtherType(first, second); } else { return compareArrayWithOtherType(second, first).invert(); } } if (firstArray /* && secondArray */) { // both arrays return compareTypes(first.getArrayElement(), second.getArrayElement()); } if (!firstKnown /* && !secondKnown */) { int variantLen = Integer.compare(first.getPossibleTypes().length, second.getPossibleTypes().length); return variantLen > 0 ? WIDER : NARROW; } boolean firstPrimitive = first.isPrimitive(); boolean secondPrimitive = second.isPrimitive(); boolean firstObj = first.isObject(); boolean secondObj = second.isObject(); if (firstObj && secondObj) { return compareObjectsNoPreCheck(first, second); } else { // primitive types conflicts with objects if (firstObj && secondPrimitive) { return CONFLICT; } if (firstPrimitive && secondObj) { return CONFLICT; } } if (firstPrimitive && secondPrimitive) { return comparePrimitives(first.getPrimitiveType(), second.getPrimitiveType()); } LOG.warn("Type compare function not complete, can't compare {} and {}", first, second); return TypeCompareEnum.CONFLICT; } private TypeCompareEnum compareArrayWithOtherType(ArgType array, ArgType other) { if (!other.isTypeKnown()) { if (other.contains(PrimitiveType.ARRAY)) { return NARROW; } return CONFLICT; } if (other.isObject()) { if (other.equals(ArgType.OBJECT)) { return NARROW; } return CONFLICT; } if (other.isPrimitive()) { return CONFLICT; } throw new JadxRuntimeException("Unprocessed type: " + other + " in array compare"); } private TypeCompareEnum compareWithUnknown(ArgType known, ArgType unknown) { if (unknown == ArgType.UNKNOWN) { return NARROW; } if (unknown == ArgType.UNKNOWN_OBJECT && (known.isObject() || known.isArray())) { return NARROW; } if (known.equals(ArgType.OBJECT) && unknown.isArray()) { return WIDER; } PrimitiveType knownPrimitive; if (known.isPrimitive()) { knownPrimitive = known.getPrimitiveType(); } else if (known.isArray()) { knownPrimitive = PrimitiveType.ARRAY; } else { knownPrimitive = PrimitiveType.OBJECT; } PrimitiveType[] possibleTypes = unknown.getPossibleTypes(); for (PrimitiveType possibleType : possibleTypes) { if (possibleType == knownPrimitive) { return NARROW; } } return CONFLICT; } private TypeCompareEnum compareObjectsNoPreCheck(ArgType first, ArgType second) { boolean objectsEquals = first.getObject().equals(second.getObject()); boolean firstGenericType = first.isGenericType(); boolean secondGenericType = second.isGenericType(); if (firstGenericType && secondGenericType && !objectsEquals) { return CONFLICT; } boolean firstGeneric = first.isGeneric(); boolean secondGeneric = second.isGeneric(); if (firstGenericType || secondGenericType) { ArgType firstWildcardType = first.getWildcardType(); ArgType secondWildcardType = second.getWildcardType(); if (firstWildcardType != null || secondWildcardType != null) { if (firstWildcardType != null && secondGenericType && first.getWildcardBound() == WildcardBound.UNBOUND) { return CONFLICT; } if (firstGenericType && secondWildcardType != null && second.getWildcardBound() == WildcardBound.UNBOUND) { return CONFLICT; } } if (firstGenericType) { return compareGenericTypeWithObject(first, second); } else { return compareGenericTypeWithObject(second, first).invert(); } } if (objectsEquals) { if (firstGeneric != secondGeneric) { return firstGeneric ? NARROW_BY_GENERIC : WIDER_BY_GENERIC; } // both generics on same object if (first.getWildcardBound() != null && second.getWildcardBound() != null) { // both wildcards return compareWildcardTypes(first, second); } List firstGenericTypes = first.getGenericTypes(); List secondGenericTypes = second.getGenericTypes(); if (isEmpty(firstGenericTypes) || isEmpty(secondGenericTypes)) { // check outer types ArgType firstOuterType = first.getOuterType(); ArgType secondOuterType = second.getOuterType(); if (firstOuterType != null && secondOuterType != null) { return compareTypes(firstOuterType, secondOuterType); } } else { // compare generics arrays int len = firstGenericTypes.size(); if (len == secondGenericTypes.size()) { for (int i = 0; i < len; i++) { TypeCompareEnum res = compareTypes(firstGenericTypes.get(i), secondGenericTypes.get(i)); if (res != EQUAL) { return res; } } } } } boolean firstIsObjCls = first.equals(ArgType.OBJECT); if (firstIsObjCls || second.equals(ArgType.OBJECT)) { return firstIsObjCls ? WIDER : NARROW; } if (ArgType.isInstanceOf(root, first, second)) { return NARROW; } if (ArgType.isInstanceOf(root, second, first)) { return WIDER; } if (!ArgType.isClsKnown(root, first) || !ArgType.isClsKnown(root, second)) { return UNKNOWN; } return TypeCompareEnum.CONFLICT; } private TypeCompareEnum compareWildcardTypes(ArgType first, ArgType second) { WildcardBound firstWildcardBound = first.getWildcardBound(); WildcardBound secondWildcardBound = second.getWildcardBound(); if (firstWildcardBound == WildcardBound.UNBOUND) { return WIDER; } if (secondWildcardBound == WildcardBound.UNBOUND) { return NARROW; } TypeCompareEnum wildcardCompare = compareTypes(first.getWildcardType(), second.getWildcardType()); if (firstWildcardBound == secondWildcardBound) { return wildcardCompare; } return CONFLICT; } private TypeCompareEnum compareGenericTypeWithObject(ArgType genericType, ArgType objType) { if (objType.isGenericType()) { return compareTypeVariables(genericType, objType); } if (objType.isWildcard()) { return CONFLICT_BY_GENERIC; } boolean rootObject = objType.equals(ArgType.OBJECT); List extendTypes = genericType.getExtendTypes(); if (extendTypes.isEmpty()) { return rootObject ? NARROW : CONFLICT; } if (extendTypes.contains(objType) || rootObject) { return NARROW; } for (ArgType extendType : extendTypes) { TypeCompareEnum res = compareObjectsNoPreCheck(extendType, objType); if (!res.isNarrow()) { return res; } } return NARROW; } private TypeCompareEnum compareTypeVariables(ArgType first, ArgType second) { if (first.getObject().equals(second.getObject())) { List firstExtendTypes = removeObject(first.getExtendTypes()); List secondExtendTypes = removeObject(second.getExtendTypes()); if (firstExtendTypes.equals(secondExtendTypes)) { return EQUAL; } int firstExtSize = firstExtendTypes.size(); int secondExtSize = secondExtendTypes.size(); if (firstExtSize == 0) { return WIDER; } if (secondExtSize == 0) { return NARROW; } if (firstExtSize == 1 && secondExtSize == 1) { return compareTypes(firstExtendTypes.get(0), secondExtendTypes.get(0)); } } return CONFLICT; } private List removeObject(List extendTypes) { if (extendTypes.contains(ArgType.OBJECT)) { if (extendTypes.size() == 1) { return Collections.emptyList(); } List result = new ArrayList<>(extendTypes); result.remove(ArgType.OBJECT); return result; } return extendTypes; } private TypeCompareEnum comparePrimitives(PrimitiveType type1, PrimitiveType type2) { if (type1 == PrimitiveType.BOOLEAN || type2 == PrimitiveType.BOOLEAN) { return type1 == type2 ? EQUAL : CONFLICT; } if (type1 == PrimitiveType.VOID || type2 == PrimitiveType.VOID) { return type1 == type2 ? EQUAL : CONFLICT; } if (type1 == PrimitiveType.BYTE && type2 == PrimitiveType.CHAR) { return WIDER; } if (type1 == PrimitiveType.SHORT && type2 == PrimitiveType.CHAR) { return WIDER; } final int type1Width = getTypeWidth(type1); final int type2Width = getTypeWidth(type2); if (type1Width > type2Width) { return WIDER; } else if (type1Width < type2Width) { return NARROW; } else { return EQUAL; } } private byte getTypeWidth(PrimitiveType type) { switch (type) { case BYTE: return 0; case SHORT: return 1; case CHAR: return 2; case INT: return 3; case LONG: return 4; case FLOAT: return 5; case DOUBLE: return 6; case BOOLEAN: case OBJECT: case ARRAY: case VOID: throw new JadxRuntimeException("Type " + type + " should not be here"); } throw new JadxRuntimeException("Unhandled type: " + type); } public Comparator getComparator() { return comparator; } public Comparator getReversedComparator() { return reversedComparator; } private final class ArgTypeComparator implements Comparator { @Override public int compare(ArgType a, ArgType b) { TypeCompareEnum result = compareTypes(a, b); switch (result) { case CONFLICT: return -2; case WIDER: case WIDER_BY_GENERIC: return -1; case NARROW: case NARROW_BY_GENERIC: return 1; case EQUAL: default: return 0; } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java ================================================ package jadx.core.dex.visitors.typeinference; public enum TypeCompareEnum { EQUAL, NARROW, NARROW_BY_GENERIC, // same basic type with generic WIDER, WIDER_BY_GENERIC, // same basic type without generic CONFLICT, CONFLICT_BY_GENERIC, // same basic type, conflict in generics UNKNOWN; public TypeCompareEnum invert() { switch (this) { case NARROW: return WIDER; case NARROW_BY_GENERIC: return WIDER_BY_GENERIC; case WIDER: return NARROW; case WIDER_BY_GENERIC: return NARROW_BY_GENERIC; case CONFLICT: case CONFLICT_BY_GENERIC: case EQUAL: case UNKNOWN: default: return this; } } public boolean isEqual() { return this == EQUAL; } public boolean isWider() { return this == WIDER || this == WIDER_BY_GENERIC; } public boolean isWiderOrEqual() { return isEqual() || isWider(); } public boolean isNarrow() { return this == NARROW || this == NARROW_BY_GENERIC; } public boolean isNarrowOrEqual() { return isEqual() || isNarrow(); } public boolean isConflict() { return this == CONFLICT || this == CONFLICT_BY_GENERIC; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.AnonymousClassAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.utils.MethodUtils; import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.AttachMethodDetails; import jadx.core.dex.visitors.ConstInlineVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.ssa.SSATransform; import jadx.core.utils.exceptions.JadxOverflowException; @JadxVisitor( name = "Type Inference", desc = "Calculate best types for SSA variables", runAfter = { SSATransform.class, ConstInlineVisitor.class, AttachMethodDetails.class } ) public final class TypeInferenceVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(TypeInferenceVisitor.class); private RootNode root; private TypeUpdate typeUpdate; @Override public void init(RootNode root) { this.root = root; this.typeUpdate = root.getTypeUpdate(); } @Override public void visit(MethodNode mth) { if (mth.isNoCode()) { return; } if (Consts.DEBUG_TYPE_INFERENCE) { LOG.info("Start type inference in method: {}", mth); } try { assignImmutableTypes(mth); initTypeBounds(mth); runTypePropagation(mth); } catch (StackOverflowError | BootstrapMethodError e) { mth.addError("Type inference failed with stack overflow", new JadxOverflowException(e.getMessage())); } catch (Exception e) { mth.addError("Type inference failed", e); } } /** * Collect initial type bounds from assign and usages */ void initTypeBounds(MethodNode mth) { List ssaVars = mth.getSVars(); ssaVars.forEach(this::attachBounds); ssaVars.forEach(this::mergePhiBounds); if (Consts.DEBUG_TYPE_INFERENCE) { ssaVars.stream().sorted() .forEach(ssaVar -> LOG.debug("Type bounds for {}: {}", ssaVar.toShortString(), ssaVar.getTypeInfo().getBounds())); } } /** * Guess type from usage and try to set it to current variable * and all connected instructions with {@link TypeUpdate#apply(MethodNode, SSAVar, ArgType)} */ boolean runTypePropagation(MethodNode mth) { List ssaVars = mth.getSVars(); ssaVars.forEach(var -> setImmutableType(mth, var)); ssaVars.forEach(var -> setBestType(mth, var)); return true; } private void setImmutableType(MethodNode mth, SSAVar ssaVar) { try { ArgType immutableType = ssaVar.getImmutableType(); if (immutableType != null) { TypeUpdateResult result = typeUpdate.applyWithWiderIgnSame(mth, ssaVar, immutableType); if (Consts.DEBUG_TYPE_INFERENCE && result == TypeUpdateResult.REJECT) { LOG.info("Reject initial immutable type {} for {}", immutableType, ssaVar); } } } catch (JadxOverflowException e) { throw e; } catch (Exception e) { mth.addWarnComment("Failed to set immutable type for var: " + ssaVar, e); } } private void setBestType(MethodNode mth, SSAVar ssaVar) { try { calculateFromBounds(mth, ssaVar); } catch (JadxOverflowException e) { throw e; } catch (Exception e) { mth.addWarnComment("Failed to calculate best type for var: " + ssaVar, e); } } private void calculateFromBounds(MethodNode mth, SSAVar ssaVar) { TypeInfo typeInfo = ssaVar.getTypeInfo(); Set bounds = typeInfo.getBounds(); Optional bestTypeOpt = selectBestTypeFromBounds(bounds); if (bestTypeOpt.isEmpty()) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.warn("Failed to select best type from bounds, count={} : ", bounds.size()); for (ITypeBound bound : bounds) { LOG.warn(" {}", bound); } } return; } ArgType candidateType = bestTypeOpt.get(); TypeUpdateResult result = typeUpdate.apply(mth, ssaVar, candidateType); if (Consts.DEBUG_TYPE_INFERENCE && result == TypeUpdateResult.REJECT) { if (ssaVar.getTypeInfo().getType().equals(candidateType)) { LOG.info("Same type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds); } else if (candidateType.isTypeKnown()) { LOG.debug("Type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds); } } } private Optional selectBestTypeFromBounds(Set bounds) { return bounds.stream() .map(ITypeBound::getType) .filter(Objects::nonNull) .max(typeUpdate.getTypeCompare().getComparator()); } private void attachBounds(SSAVar var) { TypeInfo typeInfo = var.getTypeInfo(); typeInfo.getBounds().clear(); RegisterArg assign = var.getAssign(); addAssignBound(typeInfo, assign); for (RegisterArg regArg : var.getUseList()) { addBound(typeInfo, makeUseBound(regArg)); } } private void mergePhiBounds(SSAVar ssaVar) { for (PhiInsn usedInPhi : ssaVar.getUsedInPhi()) { Set bounds = ssaVar.getTypeInfo().getBounds(); bounds.addAll(usedInPhi.getResult().getSVar().getTypeInfo().getBounds()); for (InsnArg arg : usedInPhi.getArguments()) { bounds.addAll(((RegisterArg) arg).getSVar().getTypeInfo().getBounds()); } } } private void addBound(TypeInfo typeInfo, ITypeBound bound) { if (bound == null) { return; } if (bound instanceof ITypeBoundDynamic || bound.getType() != ArgType.UNKNOWN) { typeInfo.getBounds().add(bound); } } private void addAssignBound(TypeInfo typeInfo, RegisterArg assign) { ArgType immutableType = assign.getImmutableType(); if (immutableType != null) { addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, immutableType)); return; } InsnNode insn = assign.getParentInsn(); if (insn == null || insn.getResult() == null) { addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, assign.getInitType())); return; } switch (insn.getType()) { case NEW_INSTANCE: ArgType clsType = (ArgType) ((IndexInsnNode) insn).getIndex(); addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, clsType)); break; case CONSTRUCTOR: ArgType ctrClsType = replaceAnonymousType((ConstructorInsn) insn); addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, ctrClsType)); break; case CONST: LiteralArg constLit = (LiteralArg) insn.getArg(0); addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, constLit.getType())); break; case MOVE_EXCEPTION: ExcHandlerAttr excHandlerAttr = insn.get(AType.EXC_HANDLER); if (excHandlerAttr != null) { for (ClassInfo catchType : excHandlerAttr.getHandler().getCatchTypes()) { addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, catchType.getType())); } } else { addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, insn.getResult().getInitType())); } break; case INVOKE: addBound(typeInfo, makeAssignInvokeBound((InvokeNode) insn)); break; case IGET: addBound(typeInfo, makeAssignFieldGetBound((IndexInsnNode) insn)); break; case CHECK_CAST: if (insn.contains(AFlag.SOFT_CAST)) { // ignore bound, will run checks on update } else { addBound(typeInfo, new TypeBoundCheckCastAssign(root, (IndexInsnNode) insn)); } break; default: ArgType type = insn.getResult().getInitType(); addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, type)); break; } } private ArgType replaceAnonymousType(ConstructorInsn ctr) { if (ctr.isNewInstance()) { ClassNode ctrCls = root.resolveClass(ctr.getClassType()); if (ctrCls != null && ctrCls.contains(AFlag.DONT_GENERATE)) { AnonymousClassAttr baseTypeAttr = ctrCls.get(AType.ANONYMOUS_CLASS); if (baseTypeAttr != null && baseTypeAttr.getInlineType() == AnonymousClassAttr.InlineType.CONSTRUCTOR) { return baseTypeAttr.getBaseType(); } } } return ctr.getClassType().getType(); } private ITypeBound makeAssignFieldGetBound(IndexInsnNode insn) { ArgType initType = insn.getResult().getInitType(); if (initType.containsTypeVariable()) { return new TypeBoundFieldGetAssign(root, insn, initType); } return new TypeBoundConst(BoundEnum.ASSIGN, initType); } private ITypeBound makeAssignInvokeBound(InvokeNode invokeNode) { ArgType boundType = invokeNode.getCallMth().getReturnType(); ArgType genericReturnType = root.getMethodUtils().getMethodGenericReturnType(invokeNode); if (genericReturnType != null) { if (genericReturnType.containsTypeVariable()) { InvokeType invokeType = invokeNode.getInvokeType(); if (invokeNode.getArgsCount() != 0 && invokeType != InvokeType.STATIC && invokeType != InvokeType.SUPER) { return new TypeBoundInvokeAssign(root, invokeNode, genericReturnType); } } else { boundType = genericReturnType; } } return new TypeBoundConst(BoundEnum.ASSIGN, boundType); } @Nullable private ITypeBound makeUseBound(RegisterArg regArg) { InsnNode insn = regArg.getParentInsn(); if (insn == null) { return null; } if (insn instanceof BaseInvokeNode) { ITypeBound invokeUseBound = makeInvokeUseBound(regArg, (BaseInvokeNode) insn); if (invokeUseBound != null) { return invokeUseBound; } } if (insn.getType() == InsnType.CHECK_CAST && insn.contains(AFlag.SOFT_CAST)) { // ignore return null; } return new TypeBoundConst(BoundEnum.USE, regArg.getInitType(), regArg); } private ITypeBound makeInvokeUseBound(RegisterArg regArg, BaseInvokeNode invoke) { InsnArg instanceArg = invoke.getInstanceArg(); if (instanceArg == null) { return null; } MethodUtils methodUtils = root.getMethodUtils(); IMethodDetails methodDetails = methodUtils.getMethodDetails(invoke); if (methodDetails == null) { return null; } if (instanceArg != regArg) { int argIndex = invoke.getArgIndex(regArg) - invoke.getFirstArgOffset(); ArgType argType = methodDetails.getArgTypes().get(argIndex); if (!argType.containsTypeVariable()) { return null; } return new TypeBoundInvokeUse(root, invoke, regArg, argType); } // for override methods use origin declared class as a type if (methodDetails instanceof MethodNode) { MethodNode callMth = (MethodNode) methodDetails; ClassInfo declCls = methodUtils.getMethodOriginDeclClass(callMth); return new TypeBoundConst(BoundEnum.USE, declCls.getType(), regArg); } return null; } private void assignImmutableTypes(MethodNode mth) { for (SSAVar ssaVar : mth.getSVars()) { ArgType immutableType = getSsaImmutableType(ssaVar); if (immutableType != null) { ssaVar.markAsImmutable(immutableType); } } } @Nullable private static ArgType getSsaImmutableType(SSAVar ssaVar) { if (ssaVar.getAssign().contains(AFlag.IMMUTABLE_TYPE)) { return ssaVar.getAssign().getInitType(); } for (RegisterArg reg : ssaVar.getUseList()) { if (reg.contains(AFlag.IMMUTABLE_TYPE)) { return reg.getInitType(); } } return null; } @Override public String getName() { return "TypeInferenceVisitor"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInfo.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.LinkedHashSet; import java.util.Set; import org.jetbrains.annotations.NotNull; import jadx.core.dex.instructions.args.ArgType; public class TypeInfo { private ArgType type = ArgType.UNKNOWN; private final Set bounds = new LinkedHashSet<>(); @NotNull public ArgType getType() { return type; } public void setType(ArgType type) { this.type = type; } public Set getBounds() { return bounds; } @Override public String toString() { return "TypeInfo{type=" + type + ", bounds=" + bounds + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; /** * Slow and memory consuming multi-variable type search algorithm. * Used only if fast type propagation is failed for some variables. *

* Stages description: * - find all possible candidate types within bounds * - build dynamic constraint list for every variable * - run search by checking all candidates */ public class TypeSearch { private static final Logger LOG = LoggerFactory.getLogger(TypeSearch.class); private static final int VARS_PROCESS_LIMIT = 5_000; private static final int CANDIDATES_COUNT_LIMIT = 10; private static final int SEARCH_ITERATION_LIMIT = 1_000_000; private final MethodNode mth; private final TypeSearchState state; private final TypeCompare typeCompare; private final TypeUpdate typeUpdate; public TypeSearch(MethodNode mth) { this.mth = mth; this.state = new TypeSearchState(mth); this.typeUpdate = mth.root().getTypeUpdate(); this.typeCompare = typeUpdate.getTypeCompare(); } public boolean run() { if (mth.getSVars().size() > VARS_PROCESS_LIMIT) { mth.addWarnComment("Multi-variable search skipped. Vars limit reached: " + mth.getSVars().size() + " (expected less than " + VARS_PROCESS_LIMIT + ")"); return false; } mth.getSVars().forEach(this::fillTypeCandidates); mth.getSVars().forEach(this::collectConstraints); // quick search for variables without dependencies state.getUnresolvedVars().forEach(this::resolveIndependentVariables); boolean searchSuccess; List vars = state.getUnresolvedVars(); if (vars.isEmpty()) { searchSuccess = true; } else { searchSuccess = search(vars) && fullCheck(vars); if (Consts.DEBUG_TYPE_INFERENCE && !searchSuccess) { LOG.debug("Multi-variable search failed"); } } if (searchSuccess) { return applyResolvedVars(); } return false; } private boolean applyResolvedVars() { List resolvedVars = state.getResolvedVars(); List updatedVars = new ArrayList<>(); for (TypeSearchVarInfo var : resolvedVars) { SSAVar ssaVar = var.getVar(); ArgType resolvedType = var.getCurrentType(); if (!resolvedType.isTypeKnown()) { // ignore unknown variables continue; } if (resolvedType.equals(ssaVar.getTypeInfo().getType())) { // type already set continue; } ssaVar.setType(resolvedType); updatedVars.add(var); } boolean applySuccess = true; for (TypeSearchVarInfo var : updatedVars) { TypeUpdateResult res = typeUpdate.applyWithWiderIgnSame(mth, var.getVar(), var.getCurrentType()); if (res == TypeUpdateResult.REJECT) { mth.addDebugComment("Multi-variable search result rejected for " + var); applySuccess = false; } } return applySuccess; } private boolean search(List vars) { int len = vars.size(); if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Run multi-variable search for {} vars: ", len); StringBuilder sb = new StringBuilder(); long count = 1; for (TypeSearchVarInfo var : vars) { LOG.debug(" {}", var); int size = var.getCandidateTypes().size(); sb.append(" * ").append(size); count *= size; } sb.append(" = ").append(count); LOG.debug(" max iterations count = {}", sb); } // prepare vars for (TypeSearchVarInfo var : vars) { var.reset(); } // check all types combinations int n = 0; int i = 0; while (!fullCheck(vars)) { TypeSearchVarInfo first = vars.get(i); if (first.nextType()) { int k = i + 1; if (k >= len) { return false; } TypeSearchVarInfo next = vars.get(k); while (true) { if (next.nextType()) { k++; if (k >= len) { return false; } next = vars.get(k); } else { break; } } } n++; if (n > SEARCH_ITERATION_LIMIT) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug(" > iterations limit reached: {}", SEARCH_ITERATION_LIMIT); } return false; } } if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug(" > done after {} iterations", n); } // mark all vars as resolved for (TypeSearchVarInfo var : vars) { var.setTypeResolved(true); } return true; } private boolean resolveIndependentVariables(TypeSearchVarInfo varInfo) { boolean allRelatedVarsResolved = varInfo.getConstraints().stream() .flatMap(c -> c.getRelatedVars().stream()) .allMatch(v -> state.getVarInfo(v).isTypeResolved()); if (!allRelatedVarsResolved) { return false; } // variable is independent, run single search varInfo.reset(); do { if (singleCheck(varInfo)) { varInfo.setTypeResolved(true); return true; } } while (!varInfo.nextType()); return false; } private boolean fullCheck(List vars) { for (TypeSearchVarInfo var : vars) { if (!singleCheck(var)) { return false; } } return true; } private boolean singleCheck(TypeSearchVarInfo var) { if (var.isTypeResolved()) { return true; } for (ITypeConstraint constraint : var.getConstraints()) { if (!constraint.check(state)) { return false; } } return true; } private void fillTypeCandidates(SSAVar ssaVar) { TypeSearchVarInfo varInfo = state.getVarInfo(ssaVar); ArgType immutableType = ssaVar.getImmutableType(); if (immutableType != null) { varInfo.markResolved(immutableType); return; } ArgType currentType = ssaVar.getTypeInfo().getType(); if (currentType.isTypeKnown()) { varInfo.markResolved(currentType); return; } Set assigns = new LinkedHashSet<>(); Set uses = new LinkedHashSet<>(); Set bounds = ssaVar.getTypeInfo().getBounds(); for (ITypeBound bound : bounds) { if (bound.getBound() == BoundEnum.ASSIGN) { assigns.add(bound.getType()); } else { uses.add(bound.getType()); } } Set candidateTypes = new LinkedHashSet<>(); addCandidateTypes(bounds, candidateTypes, assigns); addCandidateTypes(bounds, candidateTypes, uses); for (ArgType assignType : assigns) { addCandidateTypes(bounds, candidateTypes, getWiderTypes(assignType)); } for (ArgType useType : uses) { addCandidateTypes(bounds, candidateTypes, getNarrowTypes(useType)); } addUsageTypeCandidates(ssaVar, bounds, candidateTypes); int size = candidateTypes.size(); if (size == 0) { varInfo.setTypeResolved(true); varInfo.setCurrentType(ArgType.UNKNOWN); varInfo.setCandidateTypes(Collections.emptyList()); } else if (size == 1) { varInfo.setTypeResolved(true); varInfo.setCurrentType(candidateTypes.iterator().next()); varInfo.setCandidateTypes(Collections.emptyList()); } else { varInfo.setTypeResolved(false); varInfo.setCurrentType(ArgType.UNKNOWN); List types = new ArrayList<>(candidateTypes); types.sort(typeCompare.getReversedComparator()); varInfo.setCandidateTypes(Collections.unmodifiableList(types)); } } private void addUsageTypeCandidates(SSAVar ssaVar, Set bounds, Set candidateTypes) { for (RegisterArg useArg : ssaVar.getUseList()) { InsnNode parentInsn = useArg.getParentInsn(); if (parentInsn != null) { InsnType insnType = parentInsn.getType(); if (insnType == InsnType.APUT) { ArgType aputType = parentInsn.getArg(2).getType(); if (aputType.isTypeKnown()) { addCandidateType(bounds, candidateTypes, ArgType.array(aputType)); } } } } } private void addCandidateTypes(Set bounds, Set collectedTypes, Collection candidateTypes) { for (ArgType candidateType : candidateTypes) { if (addCandidateType(bounds, collectedTypes, candidateType)) { return; } } } private boolean addCandidateType(Set bounds, Set collectedTypes, ArgType candidateType) { if (candidateType.isTypeKnown() && typeUpdate.inBounds(bounds, candidateType)) { collectedTypes.add(candidateType); if (collectedTypes.size() > CANDIDATES_COUNT_LIMIT) { return true; } } return false; } private List getWiderTypes(ArgType type) { if (type.isTypeKnown()) { if (type.isObject()) { Set ancestors = mth.root().getClsp().getSuperTypes(type.getObject()); return ancestors.stream().map(ArgType::object).collect(Collectors.toList()); } } else { return expandUnknownType(type); } return Collections.emptyList(); } private List getNarrowTypes(ArgType type) { if (type.isTypeKnown()) { if (type.isObject()) { if (type.equals(ArgType.OBJECT)) { // a lot of objects to return return Collections.singletonList(ArgType.OBJECT); } List impList = mth.root().getClsp().getImplementations(type.getObject()); return impList.stream().map(ArgType::object).collect(Collectors.toList()); } } else { return expandUnknownType(type); } return Collections.emptyList(); } private List expandUnknownType(ArgType type) { List list = new ArrayList<>(); for (PrimitiveType possibleType : type.getPossibleTypes()) { list.add(ArgType.convertFromPrimitiveType(possibleType)); } return list; } private void collectConstraints(SSAVar var) { TypeSearchVarInfo varInfo = state.getVarInfo(var); if (varInfo.isTypeResolved()) { varInfo.setConstraints(Collections.emptyList()); return; } varInfo.setConstraints(new ArrayList<>()); addConstraint(varInfo, makeConstraint(var.getAssign())); for (RegisterArg regArg : var.getUseList()) { addConstraint(varInfo, makeConstraint(regArg)); } } private void addConstraint(TypeSearchVarInfo varInfo, ITypeConstraint constraint) { if (constraint != null) { varInfo.getConstraints().add(constraint); } } @Nullable private ITypeConstraint makeConstraint(RegisterArg arg) { InsnNode insn = arg.getParentInsn(); if (insn == null || arg.isTypeImmutable()) { return null; } switch (insn.getType()) { case MOVE: return makeMoveConstraint(insn, arg); case PHI: return makePhiConstraint(insn, arg); default: return null; } } @Nullable private ITypeConstraint makeMoveConstraint(InsnNode insn, RegisterArg arg) { if (!insn.getArg(0).isRegister()) { return null; } return new AbstractTypeConstraint(insn, arg) { @Override public boolean check(TypeSearchState state) { ArgType resType = state.getArgType(insn.getResult()); ArgType argType = state.getArgType(insn.getArg(0)); TypeCompareEnum res = typeCompare.compareTypes(resType, argType); return res.isEqual() || res.isWider(); } }; } private ITypeConstraint makePhiConstraint(InsnNode insn, RegisterArg arg) { return new AbstractTypeConstraint(insn, arg) { @Override public boolean check(TypeSearchState state) { ArgType resType = state.getArgType(insn.getResult()); for (InsnArg insnArg : insn.getArguments()) { ArgType argType = state.getArgType(insnArg); if (!argType.equals(resType)) { return false; } } return true; } }; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchState.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.exceptions.JadxRuntimeException; public class TypeSearchState { private final Map varInfoMap; public TypeSearchState(MethodNode mth) { List vars = mth.getSVars(); this.varInfoMap = new LinkedHashMap<>(vars.size()); for (SSAVar var : vars) { varInfoMap.put(var, new TypeSearchVarInfo(var)); } } @NotNull public TypeSearchVarInfo getVarInfo(SSAVar var) { TypeSearchVarInfo varInfo = this.varInfoMap.get(var); if (varInfo == null) { throw new JadxRuntimeException("TypeSearchVarInfo not found in map for var: " + var); } return varInfo; } public ArgType getArgType(InsnArg arg) { if (arg.isRegister()) { RegisterArg reg = (RegisterArg) arg; return getVarInfo(reg.getSVar()).getCurrentType(); } return arg.getType(); } public List getAllVars() { return new ArrayList<>(varInfoMap.values()); } public List getUnresolvedVars() { return varInfoMap.values().stream() .filter(varInfo -> !varInfo.isTypeResolved()) .collect(Collectors.toList()); } public List getResolvedVars() { return varInfoMap.values().stream() .filter(TypeSearchVarInfo::isTypeResolved) .collect(Collectors.toList()); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchVarInfo.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.Collections; import java.util.List; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.SSAVar; public class TypeSearchVarInfo { private final SSAVar var; private boolean typeResolved; private ArgType currentType; private List candidateTypes; private int currentIndex = -1; private List constraints; public TypeSearchVarInfo(SSAVar var) { this.var = var; } public void markResolved(ArgType type) { this.currentType = type; this.typeResolved = true; this.candidateTypes = Collections.emptyList(); } public void reset() { if (typeResolved) { return; } currentIndex = 0; currentType = candidateTypes.get(0); } /** * Switch {@code currentType} to next candidate * * @return true - if this is the first candidate */ public boolean nextType() { if (typeResolved) { return false; } int len = candidateTypes.size(); currentIndex = (currentIndex + 1) % len; currentType = candidateTypes.get(currentIndex); return currentIndex == 0; } public SSAVar getVar() { return var; } public boolean isTypeResolved() { return typeResolved; } public void setTypeResolved(boolean typeResolved) { this.typeResolved = typeResolved; } public ArgType getCurrentType() { return currentType; } public void setCurrentType(ArgType currentType) { this.currentType = currentType; } public List getCandidateTypes() { return candidateTypes; } public void setCandidateTypes(List candidateTypes) { this.candidateTypes = candidateTypes; } public List getConstraints() { return constraints; } public void setConstraints(List constraints) { this.constraints = constraints; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(var.toShortString()); if (typeResolved) { sb.append(", resolved type: ").append(currentType); } else { sb.append(", currentType=").append(currentType); sb.append(", candidateTypes=").append(candidateTypes); sb.append(", constraints=").append(constraints); } return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.core.Consts; import jadx.core.clsp.ClspClass; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.ArithNode; import jadx.core.dex.instructions.BaseInvokeNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.utils.TypeUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.dex.visitors.typeinference.TypeUpdateResult.CHANGED; import static jadx.core.dex.visitors.typeinference.TypeUpdateResult.REJECT; import static jadx.core.dex.visitors.typeinference.TypeUpdateResult.SAME; public final class TypeUpdate { private static final Logger LOG = LoggerFactory.getLogger(TypeUpdate.class); private final RootNode root; private final Map listenerRegistry; private final TypeCompare comparator; private final JadxArgs args; public TypeUpdate(RootNode root) { this.root = root; this.args = root.getArgs(); this.listenerRegistry = initListenerRegistry(); this.comparator = new TypeCompare(root); } /** * Perform recursive type checking and type propagation for all related variables */ public TypeUpdateResult apply(MethodNode mth, SSAVar ssaVar, ArgType candidateType) { return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_EMPTY); } /** * Allow wider types for apply from debug info and some special cases */ public TypeUpdateResult applyWithWiderAllow(MethodNode mth, SSAVar ssaVar, ArgType candidateType) { return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_WIDER); } /** * Force type setting */ public TypeUpdateResult applyWithWiderIgnSame(MethodNode mth, SSAVar ssaVar, ArgType candidateType) { return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_WIDER_IGNORE_SAME); } public TypeUpdateResult applyDebugInfo(MethodNode mth, SSAVar ssaVar, ArgType candidateType) { return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_APPLY_DEBUG); } private TypeUpdateResult apply(MethodNode mth, SSAVar ssaVar, ArgType candidateType, TypeUpdateFlags flags) { try { if (candidateType == null || !candidateType.isTypeKnown()) { return REJECT; } TypeUpdateInfo updateInfo = new TypeUpdateInfo(mth, flags, args); TypeUpdateResult result = updateTypeChecked(updateInfo, ssaVar.getAssign(), candidateType); if (result == REJECT) { return result; } if (updateInfo.isEmpty()) { return SAME; } if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Applying type {} to {}:", candidateType, ssaVar.toShortString()); updateInfo.getSortedUpdates().forEach(upd -> LOG.debug(" {} -> {} in {}", upd.getType(), upd.getArg().toShortString(), upd.getArg().getParentInsn())); } updateInfo.applyUpdates(); return CHANGED; } catch (Exception e) { mth.addWarnComment("Type update failed for variable: " + ssaVar + ", new type: " + candidateType, e); return REJECT; } } private TypeUpdateResult updateTypeChecked(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { if (candidateType == null) { throw new JadxRuntimeException("Null type update for arg: " + arg); } if (updateInfo.isProcessed(arg)) { return CHANGED; } TypeUpdateResult res = verifyType(updateInfo, arg, candidateType); if (res != null) { return res; } if (arg instanceof RegisterArg) { RegisterArg reg = (RegisterArg) arg; return updateTypeForSsaVar(updateInfo, reg.getSVar(), candidateType); } return requestUpdate(updateInfo, arg, candidateType); } private @Nullable TypeUpdateResult verifyType(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { ArgType currentType = arg.getType(); TypeUpdateFlags typeUpdateFlags = updateInfo.getFlags(); if (Objects.equals(currentType, candidateType)) { if (!typeUpdateFlags.isIgnoreSame()) { return SAME; } } else { if (candidateType.isWildcard()) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Wildcard type rejected for {}: candidate={}, current={}", arg, candidateType, currentType); } return REJECT; } TypeCompareEnum compareResult = comparator.compareTypes(candidateType, currentType); if (compareResult.isConflict()) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Type rejected for {}: candidate={} in conflict with current={}", arg, candidateType, currentType); } return REJECT; } if (compareResult == TypeCompareEnum.UNKNOWN && typeUpdateFlags.isIgnoreUnknown()) { return REJECT; } if (arg.isTypeImmutable() && currentType != ArgType.UNKNOWN) { // don't changed type if (compareResult == TypeCompareEnum.EQUAL) { return SAME; } if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Type rejected for {} due to conflict: candidate={}, current={}", arg, candidateType, currentType); } return REJECT; } if (compareResult == TypeCompareEnum.WIDER_BY_GENERIC && typeUpdateFlags.isKeepGenerics()) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Type rejected for {}: candidate={} is removing generic from current={}", arg, candidateType, currentType); } return REJECT; } if (compareResult.isWider() && !typeUpdateFlags.isAllowWider()) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Type rejected for {}: candidate={} is wider than current={}", arg, candidateType, currentType); } return REJECT; } if (candidateType.containsTypeVariable()) { // reject unknown type vars ArgType unknownTypeVar = root.getTypeUtils().checkForUnknownTypeVars(updateInfo.getMth(), candidateType); if (unknownTypeVar != null) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Type rejected for {}: candidate: '{}' has unknown type var: '{}'", arg, candidateType, unknownTypeVar); } return REJECT; } } } return null; } private TypeUpdateResult updateTypeForSsaVar(TypeUpdateInfo updateInfo, SSAVar ssaVar, ArgType candidateType) { TypeInfo typeInfo = ssaVar.getTypeInfo(); ArgType immutableType = ssaVar.getImmutableType(); if (immutableType != null && !Objects.equals(immutableType, candidateType)) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.info("Reject change immutable type {} to {} for {}", immutableType, candidateType, ssaVar); } return REJECT; } if (!inBounds(updateInfo, ssaVar, typeInfo.getBounds(), candidateType)) { return REJECT; } TypeUpdateResult result = requestUpdate(updateInfo, ssaVar.getAssign(), candidateType); boolean allSame = result == SAME; if (result != REJECT) { List useList = ssaVar.getUseList(); for (RegisterArg arg : useList) { result = requestUpdate(updateInfo, arg, candidateType); if (result == REJECT) { break; } if (result != SAME) { allSame = false; } } } if (result == REJECT) { // rollback update for all registers in current SSA var updateInfo.rollbackUpdate(ssaVar.getAssign()); ssaVar.getUseList().forEach(updateInfo::rollbackUpdate); return REJECT; } return allSame ? SAME : CHANGED; } private TypeUpdateResult requestUpdate(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { if (updateInfo.isProcessed(arg)) { return CHANGED; } updateInfo.requestUpdate(arg, candidateType); TypeUpdateResult result = runListeners(updateInfo, arg, candidateType); if (result == REJECT) { updateInfo.rollbackUpdate(arg); } return result; } private TypeUpdateResult runListeners(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { InsnNode insn = arg.getParentInsn(); if (insn == null) { return SAME; } ITypeListener listener = listenerRegistry.get(insn.getType()); if (listener == null) { return CHANGED; } return listener.update(updateInfo, insn, arg, candidateType); } boolean inBounds(Set bounds, ArgType candidateType) { for (ITypeBound bound : bounds) { ArgType boundType = bound.getType(); if (boundType != null && !checkBound(candidateType, bound, boundType)) { return false; } } return true; } private boolean inBounds(TypeUpdateInfo updateInfo, SSAVar ssaVar, Set bounds, ArgType candidateType) { for (ITypeBound bound : bounds) { ArgType boundType; if (updateInfo != null && bound instanceof ITypeBoundDynamic) { boundType = ((ITypeBoundDynamic) bound).getType(updateInfo); } else { boundType = bound.getType(); } if (boundType != null && !checkBound(candidateType, bound, boundType)) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Reject type '{}' for {} by bound: {} from {}", candidateType, ssaVar, boundType, bound); } return false; } } return true; } private boolean checkBound(ArgType candidateType, ITypeBound bound, ArgType boundType) { TypeCompareEnum compareResult = comparator.compareTypes(candidateType, boundType); switch (compareResult) { case EQUAL: return true; case WIDER: return bound.getBound() != BoundEnum.USE; case NARROW: if (bound.getBound() == BoundEnum.ASSIGN) { return !boundType.isTypeKnown() && checkAssignForUnknown(boundType, candidateType); } return true; case WIDER_BY_GENERIC: case NARROW_BY_GENERIC: // allow replace object to same object with known generic type // due to incomplete information about external methods and fields return true; case CONFLICT: case CONFLICT_BY_GENERIC: return false; case UNKNOWN: LOG.warn("Can't compare types, unknown hierarchy: {} and {}", candidateType, boundType); comparator.compareTypes(candidateType, boundType); return true; default: throw new JadxRuntimeException("Not processed type compare enum: " + compareResult); } } private boolean checkAssignForUnknown(ArgType boundType, ArgType candidateType) { if (boundType == ArgType.UNKNOWN) { return true; } boolean candidateArray = candidateType.isArray(); if (boundType.isArray() && candidateArray) { return checkAssignForUnknown(boundType.getArrayElement(), candidateType.getArrayElement()); } if (candidateArray && boundType.contains(PrimitiveType.ARRAY)) { return true; } if (candidateType.isObject() && boundType.contains(PrimitiveType.OBJECT)) { return true; } if (candidateType.isPrimitive() && boundType.contains(candidateType.getPrimitiveType())) { return true; } return false; } private Map initListenerRegistry() { Map registry = new EnumMap<>(InsnType.class); registry.put(InsnType.CONST, this::sameFirstArgListener); registry.put(InsnType.MOVE, this::moveListener); registry.put(InsnType.PHI, this::allSameListener); registry.put(InsnType.AGET, this::arrayGetListener); registry.put(InsnType.APUT, this::arrayPutListener); registry.put(InsnType.IF, this::ifListener); registry.put(InsnType.ARITH, this::arithListener); registry.put(InsnType.NEG, this::suggestAllSameListener); registry.put(InsnType.NOT, this::suggestAllSameListener); registry.put(InsnType.CHECK_CAST, this::checkCastListener); registry.put(InsnType.INVOKE, this::invokeListener); registry.put(InsnType.CONSTRUCTOR, this::invokeListener); return registry; } private TypeUpdateResult invokeListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { BaseInvokeNode invoke = (BaseInvokeNode) insn; if (isAssign(invoke, arg)) { // TODO: implement backward type propagation (from result to instance) return SAME; } if (invoke.getInstanceArg() == arg) { IMethodDetails methodDetails = root.getMethodUtils().getMethodDetails(invoke); if (methodDetails == null) { return SAME; } TypeUtils typeUtils = root.getTypeUtils(); Set knownTypeVars = typeUtils.getKnownTypeVarsAtMethod(updateInfo.getMth()); Map typeVarsMap = typeUtils.getTypeVariablesMapping(candidateType); ArgType returnType = methodDetails.getReturnType(); List argTypes = methodDetails.getArgTypes(); int argsCount = argTypes.size(); if (typeVarsMap.isEmpty()) { // generics can't be resolved => use as is return applyInvokeTypes(updateInfo, invoke, argsCount, knownTypeVars, () -> returnType, argTypes::get); } // resolve types before apply return applyInvokeTypes(updateInfo, invoke, argsCount, knownTypeVars, () -> typeUtils.replaceTypeVariablesUsingMap(returnType, typeVarsMap), argNum -> typeUtils.replaceClassGenerics(candidateType, argTypes.get(argNum))); } return SAME; } private TypeUpdateResult applyInvokeTypes(TypeUpdateInfo updateInfo, BaseInvokeNode invoke, int argsCount, Set knownTypeVars, Supplier getReturnType, Function getArgType) { boolean allSame = true; RegisterArg resultArg = invoke.getResult(); if (resultArg != null && !resultArg.isTypeImmutable()) { ArgType returnType = checkType(knownTypeVars, getReturnType.get()); if (returnType != null) { TypeUpdateResult result = updateTypeChecked(updateInfo, resultArg, returnType); if (result == REJECT) { TypeCompareEnum compare = comparator.compareTypes(returnType, resultArg.getType()); if (compare.isWider()) { return REJECT; } } if (result == CHANGED) { allSame = false; } } } int argOffset = invoke.getFirstArgOffset(); for (int i = 0; i < argsCount; i++) { InsnArg invokeArg = invoke.getArg(argOffset + i); if (!invokeArg.isTypeImmutable()) { ArgType argType = checkType(knownTypeVars, getArgType.apply(i)); if (argType != null) { TypeUpdateResult result = updateTypeChecked(updateInfo, invokeArg, argType); if (result == REJECT) { TypeCompareEnum compare = comparator.compareTypes(argType, invokeArg.getType()); if (compare.isNarrow()) { return REJECT; } } if (result == CHANGED) { allSame = false; } } } } return allSame ? SAME : CHANGED; } @Nullable private ArgType checkType(Set knownTypeVars, @Nullable ArgType type) { if (type == null) { return null; } if (type.isWildcard()) { return null; } if (type.containsTypeVariable()) { if (knownTypeVars.isEmpty()) { return null; } Boolean hasUnknown = type.visitTypes(t -> t.isGenericType() && !knownTypeVars.contains(t) ? Boolean.TRUE : null); if (hasUnknown != null) { return null; } } return type; } private TypeUpdateResult sameFirstArgListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { InsnArg changeArg = isAssign(insn, arg) ? insn.getArg(0) : insn.getResult(); if (updateInfo.hasUpdateWithType(changeArg, candidateType)) { return CHANGED; } return updateTypeChecked(updateInfo, changeArg, candidateType); } private TypeUpdateResult moveListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { if (insn.getResult() == null) { return CHANGED; } boolean assignChanged = isAssign(insn, arg); InsnArg changeArg = assignChanged ? insn.getArg(0) : insn.getResult(); // allow result to be wider TypeCompareEnum cmp = comparator.compareTypes(candidateType, changeArg.getType()); boolean correctType = cmp.isEqual() || (assignChanged ? cmp.isWider() : cmp.isNarrow()); TypeUpdateResult result = updateTypeChecked(updateInfo, changeArg, candidateType); if (result == SAME && !correctType) { if (Consts.DEBUG_TYPE_INFERENCE) { LOG.debug("Move insn types mismatch: {} -> {}, change arg: {}, insn: {}", candidateType, changeArg.getType(), changeArg, insn); } return REJECT; } if (result == REJECT && correctType) { return CHANGED; } return result; } /** * All args must have same types */ private TypeUpdateResult allSameListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { if (!isAssign(insn, arg)) { return updateTypeChecked(updateInfo, insn.getResult(), candidateType); } boolean allSame = true; for (InsnArg insnArg : insn.getArguments()) { if (insnArg == arg) { continue; } TypeUpdateResult result = updateTypeChecked(updateInfo, insnArg, candidateType); if (result == REJECT) { return result; } if (result != SAME) { allSame = false; } } return allSame ? SAME : CHANGED; } private TypeUpdateResult arithListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { ArithNode arithInsn = (ArithNode) insn; if (candidateType == ArgType.BOOLEAN && arithInsn.getOp().isBitOp()) { // force all args to boolean return allSameListener(updateInfo, insn, arg, candidateType); } return suggestAllSameListener(updateInfo, insn, arg, candidateType); } /** * Try to set candidate type to all args, don't fail on reject */ private TypeUpdateResult suggestAllSameListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { if (!isAssign(insn, arg)) { RegisterArg resultArg = insn.getResult(); if (resultArg != null) { updateTypeChecked(updateInfo, resultArg, candidateType); } } boolean allSame = true; for (InsnArg insnArg : insn.getArguments()) { if (insnArg != arg) { TypeUpdateResult result = updateTypeChecked(updateInfo, insnArg, candidateType); if (result == REJECT) { // ignore } else if (result != SAME) { allSame = false; } } } return allSame ? SAME : CHANGED; } private TypeUpdateResult checkCastListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { IndexInsnNode checkCast = (IndexInsnNode) insn; if (isAssign(insn, arg)) { InsnArg insnArg = insn.getArg(0); TypeUpdateResult result = updateTypeChecked(updateInfo, insnArg, candidateType); return result == REJECT ? SAME : result; } ArgType castType = (ArgType) checkCast.getIndex(); TypeCompareEnum res = comparator.compareTypes(candidateType, castType); if (res == TypeCompareEnum.CONFLICT) { // allow casting one interface to another if (!isInterfaces(candidateType, castType)) { return REJECT; } } if (res == TypeCompareEnum.CONFLICT_BY_GENERIC) { if (!insn.contains(AFlag.SOFT_CAST)) { return REJECT; } } if (res == TypeCompareEnum.NARROW_BY_GENERIC && candidateType.containsGeneric()) { // propagate generic type to result return updateTypeChecked(updateInfo, checkCast.getResult(), candidateType); } ArgType currentType = checkCast.getArg(0).getType(); return candidateType.equals(currentType) ? SAME : CHANGED; } private boolean isInterfaces(ArgType firstType, ArgType secondType) { if (!firstType.isObject() || !secondType.isObject()) { return false; } ClspClass firstCls = root.getClsp().getClsDetails(firstType); ClspClass secondCls = root.getClsp().getClsDetails(secondType); if (firstCls != null && !firstCls.isInterface()) { return false; } if (secondCls != null && !secondCls.isInterface()) { return false; } if (firstCls == null || secondCls == null) { return true; } return secondCls.isInterface() && firstCls.isInterface(); } private TypeUpdateResult arrayGetListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { if (isAssign(insn, arg)) { TypeUpdateResult result = updateTypeChecked(updateInfo, insn.getArg(0), ArgType.array(candidateType)); if (result == REJECT) { ArgType arrType = insn.getArg(0).getType(); if (arrType.isTypeKnown() && arrType.isArray() && arrType.getArrayElement().isPrimitive()) { TypeCompareEnum compResult = comparator.compareTypes(candidateType, arrType.getArrayElement()); if (compResult == TypeCompareEnum.WIDER) { // allow implicit upcast for primitive types (int a = byteArr[n]) return CHANGED; } } } return result; } InsnArg arrArg = insn.getArg(0); if (arrArg == arg) { ArgType arrayElement = candidateType.getArrayElement(); if (arrayElement == null) { return REJECT; } TypeUpdateResult result = updateTypeChecked(updateInfo, insn.getResult(), arrayElement); if (result == REJECT) { ArgType resType = insn.getResult().getType(); if (resType.isTypeKnown() && resType.isPrimitive()) { TypeCompareEnum compResult = comparator.compareTypes(resType, arrayElement); if (compResult == TypeCompareEnum.WIDER) { // allow implicit upcast for primitive types (int a = byteArr[n]) return CHANGED; } } } return result; } // index argument return SAME; } private TypeUpdateResult arrayPutListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { InsnArg arrArg = insn.getArg(0); InsnArg putArg = insn.getArg(2); if (arrArg == arg) { ArgType arrayElement = candidateType.getArrayElement(); if (arrayElement == null) { return REJECT; } TypeUpdateResult result = updateTypeChecked(updateInfo, putArg, arrayElement); if (result == REJECT) { ArgType putType = putArg.getType(); if (putType.isTypeKnown()) { TypeCompareEnum compResult = comparator.compareTypes(arrayElement, putType); if (compResult == TypeCompareEnum.WIDER || compResult == TypeCompareEnum.WIDER_BY_GENERIC) { // allow wider result (i.e. allow put any objects in Object[] or byte in int[]) return CHANGED; } } } return result; } if (arrArg == putArg) { return updateTypeChecked(updateInfo, arrArg, ArgType.array(candidateType)); } // index return SAME; } private TypeUpdateResult ifListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { InsnArg firstArg = insn.getArg(0); InsnArg secondArg = insn.getArg(1); InsnArg updateArg = firstArg == arg ? secondArg : firstArg; TypeUpdateResult result = updateTypeChecked(updateInfo, updateArg, candidateType); if (result == REJECT) { // soft checks for objects and array - exact type not compared ArgType updateArgType = updateArg.getType(); if (candidateType.isObject() && updateArgType.canBeObject()) { return SAME; } if (candidateType.isArray() && updateArgType.canBeArray()) { return SAME; } if (candidateType.isPrimitive()) { if (updateArgType.canBePrimitive(candidateType.getPrimitiveType())) { return SAME; } if (updateArgType.isTypeKnown() && candidateType.getRegCount() == updateArgType.getRegCount()) { return SAME; } } } return result; } private static boolean isAssign(InsnNode insn, InsnArg arg) { return insn.getResult() == arg; } public TypeCompare getTypeCompare() { return comparator; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateEntry.java ================================================ package jadx.core.dex.visitors.typeinference; import org.jetbrains.annotations.NotNull; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; public final class TypeUpdateEntry implements Comparable { private final int seq; private final InsnArg arg; private final ArgType type; public TypeUpdateEntry(int seq, InsnArg arg, ArgType type) { this.seq = seq; this.arg = arg; this.type = type; } public int getSeq() { return seq; } public InsnArg getArg() { return arg; } public ArgType getType() { return type; } @Override public int compareTo(@NotNull TypeUpdateEntry other) { return Integer.compare(this.seq, other.seq); } @Override public String toString() { return type + " -> " + arg.toShortString() + " in " + arg.getParentInsn(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateFlags.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.EnumSet; import java.util.List; import java.util.Set; import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.ALLOW_WIDER; import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.IGNORE_SAME; import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.IGNORE_UNKNOWN; import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.KEEP_GENERICS; public class TypeUpdateFlags { enum FlagsEnum { ALLOW_WIDER, IGNORE_SAME, IGNORE_UNKNOWN, KEEP_GENERICS, } static final TypeUpdateFlags FLAGS_EMPTY = build(); static final TypeUpdateFlags FLAGS_WIDER = build(ALLOW_WIDER); static final TypeUpdateFlags FLAGS_WIDER_IGNORE_SAME = build(ALLOW_WIDER, IGNORE_SAME); static final TypeUpdateFlags FLAGS_APPLY_DEBUG = build(ALLOW_WIDER, KEEP_GENERICS, IGNORE_UNKNOWN); private final Set flags; private static TypeUpdateFlags build(FlagsEnum... flags) { EnumSet set; if (flags.length == 0) { set = EnumSet.noneOf(FlagsEnum.class); } else { set = EnumSet.copyOf(List.of(flags)); } return new TypeUpdateFlags(set); } private TypeUpdateFlags(Set flags) { this.flags = flags; } public boolean isAllowWider() { return flags.contains(ALLOW_WIDER); } public boolean isIgnoreSame() { return flags.contains(IGNORE_SAME); } public boolean isIgnoreUnknown() { return flags.contains(IGNORE_UNKNOWN); } public boolean isKeepGenerics() { return flags.contains(KEEP_GENERICS); } @Override public String toString() { return flags.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import jadx.api.JadxArgs; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxOverflowException; import jadx.core.utils.exceptions.JadxRuntimeException; public class TypeUpdateInfo { private final MethodNode mth; private final TypeUpdateFlags flags; private final Map updateMap = new IdentityHashMap<>(); private final int updatesLimitCount; private int updateSeq = 0; public TypeUpdateInfo(MethodNode mth, TypeUpdateFlags flags, JadxArgs args) { this.mth = mth; this.flags = flags; this.updatesLimitCount = mth.getInsnsCount() * args.getTypeUpdatesLimitCount(); } public void requestUpdate(InsnArg arg, ArgType changeType) { TypeUpdateEntry prev = updateMap.put(arg, new TypeUpdateEntry(updateSeq++, arg, changeType)); if (prev != null) { throw new JadxRuntimeException("Unexpected type update override for arg: " + arg + " types: prev=" + prev.getType() + ", new=" + changeType + ", insn: " + arg.getParentInsn()); } if (updateSeq > updatesLimitCount) { throw new JadxOverflowException("Type inference error: updates count limit reached" + " with updateSeq = " + updateSeq + ". Try increasing type updates limit count."); } if (updateSeq % 100 == 0) { // check for interruption sometimes (every update is too often) Utils.checkThreadInterrupt(); } } public void rollbackUpdate(InsnArg arg) { TypeUpdateEntry removed = updateMap.remove(arg); if (removed != null) { int seq = removed.getSeq(); updateMap.values().removeIf(upd -> upd.getSeq() > seq); } } public void applyUpdates() { updateMap.values().stream().sorted() .forEach(upd -> upd.getArg().setType(upd.getType())); } public boolean isProcessed(InsnArg arg) { return updateMap.containsKey(arg); } public boolean hasUpdateWithType(InsnArg arg, ArgType type) { TypeUpdateEntry updateEntry = updateMap.get(arg); if (updateEntry != null) { return updateEntry.getType().equals(type); } return false; } public ArgType getType(InsnArg arg) { TypeUpdateEntry updateEntry = updateMap.get(arg); if (updateEntry != null) { return updateEntry.getType(); } return arg.getType(); } public MethodNode getMth() { return mth; } public boolean isEmpty() { return updateMap.isEmpty(); } public List getSortedUpdates() { return updateMap.values().stream().sorted().collect(Collectors.toList()); } public TypeUpdateFlags getFlags() { return flags; } @Override public String toString() { return "TypeUpdateInfo{" + flags + ' ' + getSortedUpdates() + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateRegistry.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.NotNull; import jadx.core.dex.instructions.InsnType; public class TypeUpdateRegistry { private final Map> listenersMap = new EnumMap<>(InsnType.class); public void add(InsnType insnType, ITypeListener listener) { listenersMap.computeIfAbsent(insnType, k -> new ArrayList<>(3)).add(listener); } @NotNull public List getListenersForInsn(InsnType insnType) { List list = listenersMap.get(insnType); if (list == null) { return Collections.emptyList(); } return list; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateResult.java ================================================ package jadx.core.dex.visitors.typeinference; public enum TypeUpdateResult { REJECT, SAME, CHANGED } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java ================================================ package jadx.core.dex.visitors.usage; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.function.Consumer; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.usage.IUsageInfoData; import jadx.api.usage.IUsageInfoVisitor; import jadx.core.clsp.ClspClass; import jadx.core.clsp.ClspClassSource; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.ICodeNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.Utils.notEmpty; public class UsageInfo implements IUsageInfoData { private final RootNode root; private final UseSet clsDeps = new UseSet<>(); private final UseSet clsUsage = new UseSet<>(); private final UseSet clsUseInMth = new UseSet<>(); private final UseSet fieldUsage = new UseSet<>(); // MethodNodeA -> Set of MethodNodes that MethodNodeA is called from private final UseSet mthUsage = new UseSet<>(); // MethodNodeA -> Set of MethodNodes that MethodNodeA calls private final UseSet mthUses = new UseSet<>(); // MethodNodeA -> Set of IMethodRefs for methods that MethodNodeA calls that cannot be resolved private final UseSet unresolvedMthUsage = new UseSet<>(); private final Map selfCalls = new HashMap<>(); public UsageInfo(RootNode root) { this.root = root; } @Override public void apply() { clsDeps.visit((cls, deps) -> cls.setDependencies(sortedList(deps))); clsUsage.visit((cls, deps) -> cls.setUseIn(sortedList(deps))); clsUseInMth.visit((cls, methods) -> cls.setUseInMth(resolveMthList(sortedList(methods)))); fieldUsage.visit((field, methods) -> field.setUseIn(resolveMthList(sortedList(methods)))); mthUsage.visit((mth, methods) -> mth.setUseIn(resolveMthList(sortedList(methods)))); mthUses.visit((mth, methods) -> mth.setUsed(resolveMthList(sortedList(methods)))); unresolvedMthUsage.visit((mth, unresolvedMethods) -> mth.setUnresolvedUsed(new ArrayList<>(unresolvedMethods))); selfCalls.forEach((mth, selfCall) -> mth.setCallsSelf(selfCall)); } @Override public void applyForClass(ClassNode cls) { cls.setDependencies(sortedList(clsDeps.getOrDefault(cls, Collections.emptySet()))); cls.setUseIn(sortedList(clsUsage.getOrDefault(cls, Collections.emptySet()))); cls.setUseInMth(resolveMthList(sortedList(clsUseInMth.getOrDefault(cls, Collections.emptySet())))); for (FieldNode fld : cls.getFields()) { fld.setUseIn(resolveMthList(sortedList(fieldUsage.getOrDefault(fld, Collections.emptySet())))); } for (MethodNode mth : cls.getMethods()) { mth.setUseIn(resolveMthList(sortedList(mthUsage.getOrDefault(mth, Collections.emptySet())))); mth.setUsed(resolveMthList(sortedList(mthUses.getOrDefault(mth, Collections.emptySet())))); mth.setUnresolvedUsed(new ArrayList<>(unresolvedMthUsage.getOrDefault(mth, Collections.emptySet()))); mth.setCallsSelf(selfCalls.getOrDefault(mth, false)); } } @Override public void visitUsageData(IUsageInfoVisitor visitor) { clsDeps.visit((cls, deps) -> visitor.visitClassDeps(cls, sortedList(deps))); clsUsage.visit((cls, deps) -> visitor.visitClassUsage(cls, sortedList(deps))); clsUseInMth.visit((cls, methods) -> visitor.visitClassUseInMethods(cls, resolveMthList(sortedList(methods)))); fieldUsage.visit((field, methods) -> visitor.visitFieldsUsage(field, resolveMthList(sortedList(methods)))); mthUsage.visit((mth, methods) -> visitor.visitMethodsUsage(mth, resolveMthList(sortedList(methods)))); mthUses.visit((mth, methods) -> visitor.visitMethodsUses(mth, resolveMthList(sortedList(methods)))); unresolvedMthUsage.visit((mth, unresolvedMethods) -> visitor.visitUnresolvedMethodsUsage(mth, new ArrayList<>(unresolvedMethods))); for (Entry entry : selfCalls.entrySet()) { MethodNode mth = entry.getKey(); Boolean selfCall = entry.getValue(); visitor.visitIsSelfCall(mth, selfCall); } visitor.visitComplete(); } public void clsUse(ClassNode cls, ArgType useType) { processType(useType, depCls -> clsUse(cls, depCls)); } public void clsUse(MethodNode mth, ArgType useType) { processType(useType, depCls -> clsUse(mth, depCls)); } public void clsUse(ICodeNode node, ArgType useType) { Consumer consumer; switch (node.getAnnType()) { case CLASS: ClassNode cls = (ClassNode) node; consumer = depCls -> clsUse(cls, depCls); break; case METHOD: MethodNode mth = (MethodNode) node; consumer = depCls -> clsUse(mth, depCls); break; case FIELD: FieldNode fld = (FieldNode) node; ClassNode fldCls = fld.getParentClass(); consumer = depCls -> clsUse(fldCls, depCls); break; default: throw new JadxRuntimeException("Unexpected use type: " + node.getAnnType()); } processType(useType, consumer); } public void clsUse(MethodNode mth, ClassNode useCls) { ClassNode parentClass = mth.getParentClass(); clsUse(parentClass, useCls); if (parentClass != useCls) { // exclude class usage in self methods clsUseInMth.add(useCls, mth); } } public void clsUse(ClassNode cls, ClassNode depCls) { ClassNode topParentClass = cls.getTopParentClass(); clsDeps.add(topParentClass, depCls.getTopParentClass()); clsUsage.add(depCls, cls); clsUsage.add(depCls, topParentClass); } /** * Add method usage: {@code useMth} occurrence found in {@code mth} code */ public void methodUse(MethodNode mth, MethodNode useMth) { clsUse(mth, useMth.getParentClass()); mthUsage.add(useMth, mth); // useMth is used in mth mthUses.add(mth, useMth); // mth uses useMth if (mth == useMth) { selfCalls.put(mth, true); } // implicit usage clsUse(mth, useMth.getReturnType()); useMth.getMethodInfo().getArgumentsTypes().forEach(argType -> clsUse(mth, argType)); } /** * Add method usage: {@code useMth} occurrence found in {@code mth} code */ public void unresolvedMethodUse(MethodNode mth, IMethodRef useMth) { unresolvedMthUsage.add(mth, useMth); } public void fieldUse(MethodNode mth, FieldNode useFld) { clsUse(mth, useFld.getParentClass()); fieldUsage.add(useFld, mth); // implicit usage clsUse(mth, useFld.getType()); } public void fieldUse(ICodeNode node, FieldInfo useFld) { FieldNode fld = root.resolveField(useFld); if (fld == null) { return; } switch (node.getAnnType()) { case CLASS: // TODO: support "field in class" usage? // now use field parent class for "class in class" usage clsUse((ClassNode) node, fld.getParentClass()); break; case METHOD: fieldUse((MethodNode) node, fld); break; } } /** * Visit all class nodes found in subtypes of the provided type. */ private void processType(ArgType type, Consumer consumer) { if (type == null || type == ArgType.OBJECT) { return; } if (type.isArray()) { processType(type.getArrayRootElement(), consumer); return; } if (type.isObject()) { // TODO: support custom handlers via API ClspClass clsDetails = root.getClsp().getClsDetails(type); if (clsDetails != null && clsDetails.getSource() == ClspClassSource.APACHE_HTTP_LEGACY_CLIENT) { root.getGradleInfoStorage().setUseApacheHttpLegacy(true); } ClassNode clsNode = root.resolveClass(type); if (clsNode != null) { consumer.accept(clsNode); } List genericTypes = type.getGenericTypes(); if (notEmpty(genericTypes)) { for (ArgType argType : genericTypes) { processType(argType, consumer); } } List extendTypes = type.getExtendTypes(); if (notEmpty(extendTypes)) { for (ArgType extendType : extendTypes) { processType(extendType, consumer); } } ArgType wildcardType = type.getWildcardType(); if (wildcardType != null) { processType(wildcardType, consumer); } // TODO: process 'outer' types (check TestOuterGeneric test) } } private static > List sortedList(Set nodes) { if (nodes == null || nodes.isEmpty()) { return Collections.emptyList(); } List list = new ArrayList<>(nodes); Collections.sort(list); return list; } private List resolveMthList(List mthNodeList) { return Utils.collectionMap(mthNodeList, m -> root.resolveDirectMethod(m.getParentClass().getRawName(), m.getMethodInfo().getShortId())); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java ================================================ package jadx.core.dex.visitors.usage; import java.util.Collections; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.ICallSite; import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IFieldRef; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.api.plugins.input.insns.InsnData; import jadx.api.plugins.input.insns.Opcode; import jadx.api.plugins.input.insns.custom.ICustomPayload; import jadx.api.usage.IUsageInfoCache; import jadx.api.usage.IUsageInfoData; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.ICodeNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.OverrideMethodVisitor; import jadx.core.dex.visitors.SignatureProcessor; import jadx.core.dex.visitors.rename.RenameVisitor; import jadx.core.utils.ListUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.input.InsnDataUtils; @JadxVisitor( name = "UsageInfoVisitor", desc = "Scan class and methods to collect usage info and class dependencies", runAfter = { SignatureProcessor.class, // use types with generics OverrideMethodVisitor.class, // add method override as use RenameVisitor.class // sort by alias name } ) public class UsageInfoVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(UsageInfoVisitor.class); @Override public void init(RootNode root) { IUsageInfoCache usageCache = root.getArgs().getUsageInfoCache(); IUsageInfoData usageInfoData = usageCache.get(root); if (usageInfoData != null) { try { apply(usageInfoData); return; } catch (Exception e) { LOG.error("Failed to apply cached usage data", e); } } IUsageInfoData collectedInfoData = buildUsageData(root); usageCache.set(root, collectedInfoData); apply(collectedInfoData); } private static void apply(IUsageInfoData usageInfoData) { long start = System.currentTimeMillis(); usageInfoData.apply(); if (LOG.isDebugEnabled()) { LOG.debug("Apply usage data in {}ms", System.currentTimeMillis() - start); } } private static IUsageInfoData buildUsageData(RootNode root) { UsageInfo usageInfo = new UsageInfo(root); for (ClassNode cls : root.getClasses()) { processClass(cls, usageInfo); } return usageInfo; } private static void processClass(ClassNode cls, UsageInfo usageInfo) { usageInfo.clsUse(cls, cls.getSuperClass()); for (ArgType interfaceType : cls.getInterfaces()) { usageInfo.clsUse(cls, interfaceType); } for (ArgType genericTypeParameter : cls.getGenericTypeParameters()) { usageInfo.clsUse(cls, genericTypeParameter); } for (FieldNode fieldNode : cls.getFields()) { usageInfo.clsUse(cls, fieldNode.getType()); processAnnotations(fieldNode, usageInfo); // TODO: process types from field 'constant value' } processAnnotations(cls, usageInfo); for (MethodNode methodNode : cls.getMethods()) { processMethod(methodNode, usageInfo); } } private static void processMethod(MethodNode mth, UsageInfo usageInfo) { processMethodAnnotations(mth, usageInfo); usageInfo.clsUse(mth, mth.getReturnType()); for (ArgType argType : mth.getArgTypes()) { usageInfo.clsUse(mth, argType); } // TODO: process exception classes from 'throws' try { processInstructions(mth, usageInfo); } catch (Exception e) { mth.addError("Dependency scan failed", e); } } private static void processInstructions(MethodNode mth, UsageInfo usageInfo) { if (mth.isNoCode()) { return; } ICodeReader codeReader = mth.getCodeReader(); if (codeReader == null) { return; } RootNode root = mth.root(); codeReader.visitInstructions(insnData -> { try { processInsn(root, mth, insnData, usageInfo); } catch (Exception e) { throw new JadxRuntimeException( "Usage info collection failed with error: " + e.getMessage() + " at insn: " + insnData, e); } }); } private static void processInsn(RootNode root, MethodNode mth, InsnData insnData, UsageInfo usageInfo) { if (insnData.getOpcode() == Opcode.UNKNOWN) { return; } switch (insnData.getIndexType()) { case TYPE_REF: insnData.decode(); ArgType usedType = ArgType.parse(insnData.getIndexAsType()); usageInfo.clsUse(mth, usedType); break; case FIELD_REF: insnData.decode(); FieldNode fieldNode = root.resolveField(FieldInfo.fromRef(root, insnData.getIndexAsField())); if (fieldNode != null) { usageInfo.fieldUse(mth, fieldNode); } break; case METHOD_REF: { insnData.decode(); IMethodRef mthRef; ICustomPayload payload = insnData.getPayload(); if (payload != null) { mthRef = (IMethodRef) payload; } else { mthRef = insnData.getIndexAsMethod(); } MethodNode methodNode = root.resolveMethod(MethodInfo.fromRef(root, mthRef)); if (methodNode != null) { usageInfo.methodUse(mth, methodNode); } else { mthRef.load(); if (mthRef.getName() != null || mthRef.getParentClassType() != null) { usageInfo.unresolvedMethodUse(mth, mthRef); } } break; } case CALL_SITE: { insnData.decode(); ICallSite callSite = InsnDataUtils.getCallSite(insnData); IMethodHandle methodHandle = InsnDataUtils.getMethodHandleAt(callSite, 4); if (methodHandle != null) { IMethodRef mthRef = methodHandle.getMethodRef(); MethodNode mthNode = root.resolveMethod(MethodInfo.fromRef(root, mthRef)); if (mthNode != null) { usageInfo.methodUse(mth, mthNode); } else if (mthRef.getName() != null || mthRef.getParentClassType() != null) { usageInfo.unresolvedMethodUse(mth, mthRef); } } break; } } } private static void processAnnotations(ICodeNode node, UsageInfo usageInfo) { AnnotationsAttr annAttr = node.get(JadxAttrType.ANNOTATION_LIST); processAnnotationAttr(node, annAttr, usageInfo); } private static void processMethodAnnotations(MethodNode mth, UsageInfo usageInfo) { processAnnotations(mth, usageInfo); AnnotationMethodParamsAttr paramsAttr = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS); if (paramsAttr != null) { for (AnnotationsAttr annAttr : paramsAttr.getParamList()) { processAnnotationAttr(mth, annAttr, usageInfo); } } } private static void processAnnotationAttr(ICodeNode node, AnnotationsAttr annAttr, UsageInfo usageInfo) { if (annAttr == null || annAttr.isEmpty()) { return; } for (IAnnotation ann : annAttr.getList()) { processAnnotation(node, ann, usageInfo); } } private static void processAnnotation(ICodeNode node, IAnnotation ann, UsageInfo usageInfo) { usageInfo.clsUse(node, ArgType.parse(ann.getAnnotationClass())); for (EncodedValue value : ann.getValues().values()) { processAnnotationValue(node, value, usageInfo); } } @SuppressWarnings("unchecked") private static void processAnnotationValue(ICodeNode node, EncodedValue value, UsageInfo usageInfo) { Object obj = value.getValue(); switch (value.getType()) { case ENCODED_TYPE: usageInfo.clsUse(node, ArgType.parse((String) obj)); break; case ENCODED_ENUM: case ENCODED_FIELD: if (obj instanceof IFieldRef) { usageInfo.fieldUse(node, FieldInfo.fromRef(node.root(), (IFieldRef) obj)); } else if (obj instanceof FieldInfo) { usageInfo.fieldUse(node, (FieldInfo) obj); } else { throw new JadxRuntimeException("Unexpected field type class: " + value.getClass()); } break; case ENCODED_ARRAY: for (EncodedValue encodedValue : (List) obj) { processAnnotationValue(node, encodedValue, usageInfo); } break; case ENCODED_ANNOTATION: processAnnotation(node, (IAnnotation) obj, usageInfo); break; } } public static void replaceMethodUsage(MethodNode mergeIntoMth, MethodNode sourceMth) { List mergedUsage = ListUtils.distinctMergeSortedLists(mergeIntoMth.getUseIn(), sourceMth.getUseIn()); mergedUsage.remove(sourceMth); mergeIntoMth.setUseIn(mergedUsage); sourceMth.setUseIn(Collections.emptyList()); } @Override public String getName() { return "UsageInfoVisitor"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/dex/visitors/usage/UseSet.java ================================================ package jadx.core.dex.visitors.usage; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; public class UseSet { private final Map> useMap = new HashMap<>(); public void add(K obj, V use) { if (obj == use) { // self excluded return; } Set set = useMap.computeIfAbsent(obj, k -> new HashSet<>()); set.add(use); } public Set get(K obj) { return useMap.get(obj); } public Set getOrDefault(K obj, Set defaultValue) { return useMap.getOrDefault(obj, defaultValue); } public void visit(BiConsumer> consumer) { for (Map.Entry> entry : useMap.entrySet()) { consumer.accept(entry.getKey(), entry.getValue()); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/export/ExportGradle.java ================================================ package jadx.core.export; import java.io.File; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.core.dex.nodes.RootNode; import jadx.core.export.gen.AndroidGradleGenerator; import jadx.core.export.gen.IExportGradleGenerator; import jadx.core.export.gen.SimpleJavaGradleGenerator; import jadx.core.utils.android.AndroidManifestParser; import jadx.core.utils.exceptions.JadxRuntimeException; public class ExportGradle { private static final Logger LOG = LoggerFactory.getLogger(ExportGradle.class); private final RootNode root; private final File projectDir; private final List resources; private IExportGradleGenerator generator; public ExportGradle(RootNode root, File projectDir, List resources) { this.root = root; this.projectDir = projectDir; this.resources = resources; } public OutDirs init() { ExportGradleType exportType = getExportGradleType(); LOG.info("Export Gradle project using '{}' template", exportType); switch (exportType) { case ANDROID_APP: case ANDROID_LIBRARY: generator = new AndroidGradleGenerator(root, projectDir, resources, exportType); break; case SIMPLE_JAVA: generator = new SimpleJavaGradleGenerator(root, projectDir, resources); break; default: throw new JadxRuntimeException("Unexpected export type: " + exportType); } generator.init(); OutDirs outDirs = generator.getOutDirs(); outDirs.makeDirs(); return outDirs; } private ExportGradleType getExportGradleType() { ExportGradleType argsExportType = root.getArgs().getExportGradleType(); ExportGradleType detectedType = detectExportType(root, resources); if (argsExportType == null || argsExportType == ExportGradleType.AUTO || argsExportType == detectedType) { return detectedType; } return argsExportType; } public static ExportGradleType detectExportType(RootNode root, List resources) { ResourceFile androidManifest = AndroidManifestParser.getAndroidManifest(resources); if (androidManifest != null) { if (resources.stream().anyMatch(r -> r.getOriginalName().equals("classes.jar"))) { return ExportGradleType.ANDROID_LIBRARY; } if (resources.stream().anyMatch(r -> r.getType() == ResourceType.ARSC)) { return ExportGradleType.ANDROID_APP; } } return ExportGradleType.SIMPLE_JAVA; } public void generateGradleFiles() { generator.generateFiles(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/export/ExportGradleType.java ================================================ package jadx.core.export; public enum ExportGradleType { AUTO("Auto"), ANDROID_APP("Android App"), ANDROID_LIBRARY("Android Library"), SIMPLE_JAVA("Simple Java"); private final String desc; ExportGradleType(String desc) { this.desc = desc; } public String getDesc() { return desc; } @Override public String toString() { return desc; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/export/GradleInfoStorage.java ================================================ package jadx.core.export; public class GradleInfoStorage { private boolean vectorPathData; private boolean vectorFillType; private boolean useApacheHttpLegacy; private boolean nonFinalResIds; public boolean isVectorPathData() { return vectorPathData; } public void setVectorPathData(boolean vectorPathData) { this.vectorPathData = vectorPathData; } public boolean isVectorFillType() { return vectorFillType; } public void setVectorFillType(boolean vectorFillType) { this.vectorFillType = vectorFillType; } public boolean isUseApacheHttpLegacy() { return useApacheHttpLegacy; } public void setUseApacheHttpLegacy(boolean useApacheHttpLegacy) { this.useApacheHttpLegacy = useApacheHttpLegacy; } public boolean isNonFinalResIds() { return nonFinalResIds; } public void setNonFinalResIds(boolean nonFinalResIds) { this.nonFinalResIds = nonFinalResIds; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/export/OutDirs.java ================================================ package jadx.core.export; import java.io.File; import jadx.core.utils.files.FileUtils; public class OutDirs { private final File srcOutDir; private final File resOutDir; public OutDirs(File srcOutDir, File resOutDir) { this.srcOutDir = srcOutDir; this.resOutDir = resOutDir; } public File getSrcOutDir() { return srcOutDir; } public File getResOutDir() { return resOutDir; } public void makeDirs() { FileUtils.makeDirs(srcOutDir); FileUtils.makeDirs(resOutDir); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/export/TemplateFile.java ================================================ package jadx.core.export; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.Map; import org.jetbrains.annotations.Nullable; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Simple template engine * Syntax for replace variable with value: '{{variable}}' */ public class TemplateFile { private enum State { NONE, START, VARIABLE, END } private static class ParserState { private State state = State.NONE; private StringBuilder curVariable; private boolean skip; } private final String templateName; private final InputStream template; private final Map values = new HashMap<>(); public static TemplateFile fromResources(String path) throws FileNotFoundException { InputStream res = TemplateFile.class.getResourceAsStream(path); if (res == null) { throw new FileNotFoundException("Resource not found: " + path); } return new TemplateFile(path, res); } private TemplateFile(String name, InputStream in) { this.templateName = name; this.template = in; } public void add(String name, @Nullable Object value) { values.put(name, String.valueOf(value)); } public String build() throws IOException { try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { process(out); return out.toString(); } } public void save(File outFile) throws IOException { try (OutputStream out = new FileOutputStream(outFile)) { process(out); } } private void process(OutputStream out) throws IOException { if (template.available() == 0) { throw new IOException("Template already processed"); } try (InputStream in = new BufferedInputStream(template)) { ParserState state = new ParserState(); while (true) { int ch = in.read(); if (ch == -1) { break; } String str = process(state, (char) ch); if (str != null) { out.write(str.getBytes()); } else if (!state.skip) { out.write(ch); } } } } @Nullable private String process(ParserState parser, char ch) { State state = parser.state; switch (ch) { case '{': switch (state) { case START: parser.state = State.VARIABLE; parser.curVariable = new StringBuilder(); break; default: parser.state = State.START; break; } parser.skip = true; return null; case '}': switch (state) { case VARIABLE: parser.state = State.END; parser.skip = true; return null; case END: parser.state = State.NONE; String varName = parser.curVariable.toString(); parser.curVariable = new StringBuilder(); return processVar(varName); } break; default: switch (state) { case VARIABLE: parser.curVariable.append(ch); parser.skip = true; return null; case START: parser.state = State.NONE; return "{" + ch; case END: throw new JadxRuntimeException("Expected variable end: '" + parser.curVariable + "' (missing second '}')"); } break; } parser.skip = false; return null; } private String processVar(String varName) { String str = values.get(varName); if (str == null) { throw new JadxRuntimeException("Unknown variable: '" + varName + "' in template: " + templateName); } return str; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/export/gen/AndroidGradleGenerator.java ================================================ package jadx.core.export.gen; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.api.security.IJadxSecurity; import jadx.core.dex.nodes.RootNode; import jadx.core.export.ExportGradleType; import jadx.core.export.GradleInfoStorage; import jadx.core.export.OutDirs; import jadx.core.export.TemplateFile; import jadx.core.utils.Utils; import jadx.core.utils.android.AndroidManifestParser; import jadx.core.utils.android.AppAttribute; import jadx.core.utils.android.ApplicationParams; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.ResContainer; public class AndroidGradleGenerator implements IExportGradleGenerator { private static final Logger LOG = LoggerFactory.getLogger(AndroidGradleGenerator.class); private static final Pattern ILLEGAL_GRADLE_CHARS = Pattern.compile("[/\\\\:>\"?*|]"); private static final ApplicationParams UNKNOWN_APP_PARAMS = new ApplicationParams("UNKNOWN", 0, 0, 0, 0, "UNKNOWN", "UNKNOWN", "UNKNOWN"); private final RootNode root; private final File projectDir; private final List resources; private final boolean exportApp; private OutDirs outDirs; private File baseDir; private ApplicationParams applicationParams; public AndroidGradleGenerator(RootNode root, File projectDir, List resources, ExportGradleType exportType) { this.root = root; this.projectDir = projectDir; this.resources = resources; this.exportApp = exportType == ExportGradleType.ANDROID_APP; } @Override public void init() { String moduleDir = exportApp ? "app" : "lib"; baseDir = new File(projectDir, moduleDir); outDirs = new OutDirs(new File(baseDir, "src/main/java"), new File(baseDir, "src/main")); applicationParams = parseApplicationParams(); } @Override public void generateFiles() { try { saveProjectBuildGradle(); if (exportApp) { saveApplicationBuildGradle(); } else { saveLibraryBuildGradle(); } saveSettingsGradle(); saveGradleProperties(); } catch (Exception e) { throw new JadxRuntimeException("Gradle export failed", e); } } @Override public OutDirs getOutDirs() { return outDirs; } private ApplicationParams parseApplicationParams() { try { ResourceFile androidManifest = AndroidManifestParser.getAndroidManifest(resources); if (androidManifest == null) { LOG.warn("AndroidManifest.xml not found, exported files will contains 'UNKNOWN' fields"); return UNKNOWN_APP_PARAMS; } ResContainer strings = null; if (exportApp) { ResourceFile arscFile = resources.stream() .filter(resourceFile -> resourceFile.getType() == ResourceType.ARSC) .findFirst().orElse(null); if (arscFile != null) { List resContainers = arscFile.loadContent().getSubFiles(); strings = resContainers .stream() .filter(resContainer -> resContainer.getName().contains("values/strings.xml")) .findFirst() .orElseGet(() -> resContainers.stream() .filter(resContainer -> resContainer.getName().contains("strings.xml")) .findFirst().orElse(null)); } } EnumSet attrs = EnumSet.noneOf(AppAttribute.class); attrs.add(AppAttribute.MIN_SDK_VERSION); if (exportApp) { attrs.add(AppAttribute.APPLICATION_LABEL); attrs.add(AppAttribute.TARGET_SDK_VERSION); attrs.add(AppAttribute.COMPILE_SDK_VERSION); attrs.add(AppAttribute.VERSION_NAME); attrs.add(AppAttribute.VERSION_CODE); } IJadxSecurity security = root.getArgs().getSecurity(); AndroidManifestParser parser = new AndroidManifestParser(androidManifest, strings, attrs, security); return parser.parse(); } catch (Exception t) { LOG.warn("Failed to parse AndroidManifest.xml", t); return UNKNOWN_APP_PARAMS; } } private void saveGradleProperties() throws IOException { GradleInfoStorage gradleInfo = root.getGradleInfoStorage(); /* * For Android Gradle Plugin >=8.0.0 the property "android.nonFinalResIds=false" has to be set in * "gradle.properties" when resource identifiers are used as constant expressions. */ if (gradleInfo.isNonFinalResIds()) { File gradlePropertiesFile = new File(projectDir, "gradle.properties"); try (FileOutputStream fos = new FileOutputStream(gradlePropertiesFile)) { fos.write("android.nonFinalResIds=false".getBytes(StandardCharsets.UTF_8)); } } } private void saveProjectBuildGradle() throws IOException { TemplateFile tmpl = TemplateFile.fromResources("/export/android/build.gradle.tmpl"); tmpl.save(new File(projectDir, "build.gradle")); } private void saveSettingsGradle() throws IOException { TemplateFile tmpl = TemplateFile.fromResources("/export/android/settings.gradle.tmpl"); String appName = applicationParams.getApplicationName(); String projectName; if (appName != null) { projectName = ILLEGAL_GRADLE_CHARS.matcher(appName).replaceAll(""); } else { projectName = GradleGeneratorTools.guessProjectName(root); } tmpl.add("projectName", projectName); tmpl.add("mainModuleName", baseDir.getName()); tmpl.save(new File(projectDir, "settings.gradle")); } private void saveApplicationBuildGradle() throws IOException { String appPackage = Utils.getOrElse(root.getAppPackage(), "UNKNOWN"); int minSdkVersion = Utils.getOrElse(applicationParams.getMinSdkVersion(), 0); TemplateFile tmpl = TemplateFile.fromResources("/export/android/app.build.gradle.tmpl"); tmpl.add("applicationId", appPackage); tmpl.add("minSdkVersion", minSdkVersion); tmpl.add("compileSdkVersion", applicationParams.getCompileSdkVersion()); tmpl.add("targetSdkVersion", applicationParams.getTargetSdkVersion()); tmpl.add("versionCode", applicationParams.getVersionCode()); tmpl.add("versionName", applicationParams.getVersionName()); tmpl.add("additionalOptions", genAdditionalAndroidPluginOptions(minSdkVersion)); tmpl.save(new File(baseDir, "build.gradle")); } private void saveLibraryBuildGradle() throws IOException { String pkg = Utils.getOrElse(root.getAppPackage(), "UNKNOWN"); int minSdkVersion = Utils.getOrElse(applicationParams.getMinSdkVersion(), 0); TemplateFile tmpl = TemplateFile.fromResources("/export/android/lib.build.gradle.tmpl"); tmpl.add("packageId", pkg); tmpl.add("minSdkVersion", minSdkVersion); tmpl.add("compileSdkVersion", applicationParams.getCompileSdkVersion()); tmpl.add("additionalOptions", genAdditionalAndroidPluginOptions(minSdkVersion)); tmpl.save(new File(baseDir, "build.gradle")); } private String genAdditionalAndroidPluginOptions(int minSdkVersion) { List additionalOptions = new ArrayList<>(); GradleInfoStorage gradleInfo = root.getGradleInfoStorage(); if (gradleInfo.isVectorPathData() && minSdkVersion < 21 || gradleInfo.isVectorFillType() && minSdkVersion < 24) { additionalOptions.add("vectorDrawables.useSupportLibrary = true"); } if (gradleInfo.isUseApacheHttpLegacy()) { additionalOptions.add("useLibrary 'org.apache.http.legacy'"); } StringBuilder sb = new StringBuilder(); for (String additionalOption : additionalOptions) { sb.append(" ").append(additionalOption).append('\n'); } return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/export/gen/GradleGeneratorTools.java ================================================ package jadx.core.export.gen; import java.io.File; import java.util.List; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.files.FileUtils; public class GradleGeneratorTools { public static String guessProjectName(RootNode root) { List inputFiles = root.getArgs().getInputFiles(); if (inputFiles.size() == 1) { return FileUtils.getPathBaseName(inputFiles.get(0).toPath()); } // default return "PROJECT_NAME"; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/export/gen/IExportGradleGenerator.java ================================================ package jadx.core.export.gen; import jadx.core.export.OutDirs; public interface IExportGradleGenerator { void init(); OutDirs getOutDirs(); void generateFiles(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/export/gen/SimpleJavaGradleGenerator.java ================================================ package jadx.core.export.gen; import java.io.File; import java.io.IOException; import java.util.List; import jadx.api.ResourceFile; import jadx.core.dex.nodes.RootNode; import jadx.core.export.OutDirs; import jadx.core.export.TemplateFile; import jadx.core.utils.exceptions.JadxRuntimeException; public class SimpleJavaGradleGenerator implements IExportGradleGenerator { private final RootNode root; private final File projectDir; private final List resources; private OutDirs outDirs; private File appDir; public SimpleJavaGradleGenerator(RootNode root, File projectDir, List resources) { this.root = root; this.projectDir = projectDir; this.resources = resources; } @Override public void init() { appDir = new File(projectDir, "app"); File srcOutDir = new File(appDir, "src/main/java"); File resOutDir = new File(appDir, "src/main/resources"); outDirs = new OutDirs(srcOutDir, resOutDir); } @Override public void generateFiles() { try { saveSettingsGradle(); saveBuildGradle(); } catch (Exception e) { throw new JadxRuntimeException("Failed to generate gradle files", e); } } private void saveSettingsGradle() throws IOException { TemplateFile tmpl = TemplateFile.fromResources("/export/java/settings.gradle.kts.tmpl"); tmpl.add("projectName", GradleGeneratorTools.guessProjectName(root)); tmpl.save(new File(projectDir, "settings.gradle.kts")); } private void saveBuildGradle() throws IOException { TemplateFile tmpl = TemplateFile.fromResources("/export/java/build.gradle.kts.tmpl"); tmpl.save(new File(appDir, "build.gradle.kts")); } @Override public OutDirs getOutDirs() { return outDirs; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/AppContext.java ================================================ package jadx.core.plugins; import java.util.Objects; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.gui.JadxGuiContext; import jadx.core.plugins.files.IJadxFilesGetter; public class AppContext { private @Nullable JadxGuiContext guiContext; private IJadxFilesGetter filesGetter; public @Nullable JadxGuiContext getGuiContext() { return guiContext; } public void setGuiContext(@Nullable JadxGuiContext guiContext) { this.guiContext = guiContext; } public IJadxFilesGetter getFilesGetter() { return Objects.requireNonNull(filesGetter); } public void setFilesGetter(IJadxFilesGetter filesGetter) { this.filesGetter = filesGetter; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/JadxPluginManager.java ================================================ package jadx.core.plugins; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import java.util.function.Consumer; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxDecompiler; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.loader.JadxPluginLoader; import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.options.OptionDescription; import jadx.core.plugins.versions.VerifyRequiredVersion; public class JadxPluginManager { private static final Logger LOG = LoggerFactory.getLogger(JadxPluginManager.class); private final JadxDecompiler decompiler; private final JadxPluginsData pluginsData; private final Set disabledPlugins; private final SortedSet allPlugins = new TreeSet<>(); private final SortedSet resolvedPlugins = new TreeSet<>(); private final Map provideSuggestions = new TreeMap<>(); private final List> addPluginListeners = new ArrayList<>(); public JadxPluginManager(JadxDecompiler decompiler) { this.decompiler = decompiler; this.pluginsData = new JadxPluginsData(decompiler, this); this.disabledPlugins = decompiler.getArgs().getDisabledPlugins(); } /** * Add suggestion how to resolve conflicting plugins */ public void providesSuggestion(String provides, String pluginId) { provideSuggestions.put(provides, pluginId); } public void load(JadxPluginLoader pluginLoader) { allPlugins.clear(); VerifyRequiredVersion verifyRequiredVersion = new VerifyRequiredVersion(); for (JadxPlugin plugin : pluginLoader.load()) { addPlugin(plugin, verifyRequiredVersion); } resolve(); } public void register(JadxPlugin plugin) { Objects.requireNonNull(plugin); PluginContext addedPlugin = addPlugin(plugin, new VerifyRequiredVersion()); if (addedPlugin == null) { LOG.debug("Can't register plugin, it was disabled: {}", plugin.getPluginInfo().getPluginId()); return; } LOG.debug("Register plugin: {}", addedPlugin.getPluginId()); resolve(); } private @Nullable PluginContext addPlugin(JadxPlugin plugin, VerifyRequiredVersion verifyRequiredVersion) { PluginContext pluginContext = new PluginContext(decompiler, pluginsData, plugin); if (disabledPlugins.contains(pluginContext.getPluginId())) { return null; } String requiredJadxVersion = pluginContext.getPluginInfo().getRequiredJadxVersion(); if (!verifyRequiredVersion.isCompatible(requiredJadxVersion)) { LOG.warn("Plugin '{}' not loaded: requires '{}' jadx version which it is not compatible with current: {}", pluginContext, requiredJadxVersion, verifyRequiredVersion.getJadxVersion()); return null; } LOG.debug("Loading plugin: {}", pluginContext); if (!allPlugins.add(pluginContext)) { throw new IllegalArgumentException("Duplicate plugin id: " + pluginContext + ", class " + plugin.getClass()); } addPluginListeners.forEach(l -> l.accept(pluginContext)); return pluginContext; } public boolean unload(String pluginId) { boolean result = allPlugins.removeIf(context -> { if (context.getPluginId().equals(pluginId)) { LOG.debug("Unload plugin: {}", pluginId); return true; } return false; }); resolve(); return result; } public SortedSet getAllPluginContexts() { return allPlugins; } public SortedSet getResolvedPluginContexts() { return resolvedPlugins; } private synchronized void resolve() { Map> provides = allPlugins.stream() .collect(Collectors.groupingBy(p -> p.getPluginInfo().getProvides())); List resolved = new ArrayList<>(provides.size()); provides.forEach((provide, list) -> { if (list.size() == 1) { resolved.add(list.get(0)); } else { String suggestion = provideSuggestions.get(provide); if (suggestion != null) { list.stream().filter(p -> p.getPluginId().equals(suggestion)) .findFirst() .ifPresent(resolved::add); } else { PluginContext selected = list.get(0); resolved.add(selected); LOG.debug("Select providing '{}' plugin '{}', candidates: {}", provide, selected, list); } } }); resolvedPlugins.clear(); resolvedPlugins.addAll(resolved); } public void initAll() { init(allPlugins); } public void initResolved() { init(resolvedPlugins); } public void init(SortedSet pluginContexts) { AppContext defAppContext = buildDefaultAppContext(); for (PluginContext context : pluginContexts) { try { if (context.getAppContext() == null) { context.setAppContext(defAppContext); } context.init(); } catch (Exception e) { LOG.error("Failed to init plugin: {}", context.getPluginId(), e); } } for (PluginContext context : pluginContexts) { JadxPluginOptions options = context.getOptions(); if (options != null) { verifyOptions(context, options); } } } public void unloadAll() { unload(allPlugins); } public void unloadResolved() { unload(resolvedPlugins); } public void unload(SortedSet pluginContexts) { for (PluginContext context : pluginContexts) { try { context.unload(); } catch (Exception e) { LOG.warn("Failed to unload plugin: {}", context.getPluginId(), e); } } } private AppContext buildDefaultAppContext() { AppContext appContext = new AppContext(); appContext.setGuiContext(null); appContext.setFilesGetter(decompiler.getArgs().getFilesGetter()); return appContext; } private void verifyOptions(PluginContext pluginContext, JadxPluginOptions options) { String pluginId = pluginContext.getPluginId(); List descriptions = options.getOptionsDescriptions(); if (descriptions == null) { throw new IllegalArgumentException("Null option descriptions in plugin id: " + pluginId); } String prefix = pluginId + '.'; descriptions.forEach(descObj -> { String optName = descObj.name(); if (optName == null || !optName.startsWith(prefix)) { throw new IllegalArgumentException("Plugin option name should start with plugin id: '" + prefix + "', option: " + optName); } String desc = descObj.description(); if (desc == null || desc.isEmpty()) { throw new IllegalArgumentException("Plugin option description not set, plugin: " + pluginId); } List values = descObj.values(); if (values == null) { throw new IllegalArgumentException("Plugin option values is null, option: " + optName + ", plugin: " + pluginId); } }); } public List getCodeInputs() { return getResolvedPluginContexts() .stream() .flatMap(p -> p.getCodeInputs().stream()) .collect(Collectors.toList()); } public void registerAddPluginListener(Consumer listener) { this.addPluginListeners.add(listener); // run for already added plugins getAllPluginContexts().forEach(listener); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/JadxPluginsData.java ================================================ package jadx.core.plugins; import jadx.api.JadxDecompiler; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.data.IJadxPlugins; import jadx.api.plugins.data.JadxPluginRuntimeData; import jadx.core.utils.exceptions.JadxRuntimeException; public class JadxPluginsData implements IJadxPlugins { private final JadxDecompiler decompiler; private final JadxPluginManager pluginManager; public JadxPluginsData(JadxDecompiler decompiler, JadxPluginManager pluginManager) { this.decompiler = decompiler; this.pluginManager = pluginManager; } @Override public JadxPluginRuntimeData getById(String pluginId) { return pluginManager.getResolvedPluginContexts() .stream() .filter(p -> p.getPluginId().equals(pluginId)) .findFirst() .orElseThrow(() -> new JadxRuntimeException("Plugin with id '" + pluginId + "' not found")); } @Override public JadxPluginRuntimeData getProviding(String provideId) { return pluginManager.getResolvedPluginContexts() .stream() .filter(p -> p.getPluginInfo().getProvides().equals(provideId)) .findFirst() .orElseThrow(() -> new JadxRuntimeException("Plugin providing '" + provideId + "' not found")); } @SuppressWarnings("unchecked") @Override public

P getInstance(Class

pluginCls) { return pluginManager.getResolvedPluginContexts() .stream() .filter(p -> p.getPluginInstance().getClass().equals(pluginCls)) .map(p -> (P) p.getPluginInstance()) .findFirst() .orElseThrow(() -> new JadxRuntimeException("Plugin class '" + pluginCls + "' not found")); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/PluginContext.java ================================================ package jadx.core.plugins; import java.io.Closeable; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Supplier; import org.jetbrains.annotations.Nullable; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.data.IJadxFiles; import jadx.api.plugins.data.IJadxPlugins; import jadx.api.plugins.data.JadxPluginRuntimeData; import jadx.api.plugins.events.IJadxEvents; import jadx.api.plugins.gui.JadxGuiContext; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.input.data.impl.MergeCodeLoader; import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.options.OptionDescription; import jadx.api.plugins.options.OptionFlag; import jadx.api.plugins.pass.JadxPass; import jadx.api.plugins.resources.IResourcesLoader; import jadx.core.plugins.files.JadxFilesData; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.zip.ZipReader; public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData, Comparable { private final JadxDecompiler decompiler; private final JadxPluginsData pluginsData; private final JadxPlugin plugin; private final JadxPluginInfo pluginInfo; private final ClassLoader pluginClassLoader; private AppContext appContext; private final List codeInputs = new ArrayList<>(); private @Nullable JadxPluginOptions options; private @Nullable Supplier inputsHashSupplier; private boolean initialized; PluginContext(JadxDecompiler decompiler, JadxPluginsData pluginsData, JadxPlugin plugin) { this.decompiler = decompiler; this.pluginsData = pluginsData; this.plugin = plugin; this.pluginInfo = plugin.getPluginInfo(); this.pluginClassLoader = plugin.getClass().getClassLoader(); } public void init() { classLoaderWrap(() -> { plugin.init(this); initialized = true; }); } public void unload() { if (initialized) { classLoaderWrap(plugin::unload); } } public void classLoaderWrap(Runnable task) { Thread thread = Thread.currentThread(); ClassLoader prevClassLoader = thread.getContextClassLoader(); thread.setContextClassLoader(pluginClassLoader); try { task.run(); } finally { thread.setContextClassLoader(prevClassLoader); } } @Override public boolean isInitialized() { return initialized; } @Override public JadxArgs getArgs() { return decompiler.getArgs(); } @Override public JadxDecompiler getDecompiler() { return decompiler; } @Override public void addPass(JadxPass pass) { decompiler.addCustomPass(pass); } @Override public void addCodeInput(JadxCodeInput codeInput) { this.codeInputs.add(codeInput); } @Override public List getCodeInputs() { return codeInputs; } @Override public void registerOptions(JadxPluginOptions options) { try { this.options = Objects.requireNonNull(options); options.setOptions(getArgs().getPluginOptions()); } catch (Exception e) { throw new JadxRuntimeException("Failed to apply options for plugin: " + getPluginId(), e); } } @Override public void registerInputsHashSupplier(Supplier supplier) { this.inputsHashSupplier = supplier; } @Override public String getInputsHash() { if (inputsHashSupplier == null) { return defaultOptionsHash(); } try { return inputsHashSupplier.get(); } catch (Exception e) { throw new JadxRuntimeException("Failed to get inputs hash for plugin: " + getPluginId(), e); } } private String defaultOptionsHash() { if (options == null) { return ""; } Map allOptions = getArgs().getPluginOptions(); StringBuilder sb = new StringBuilder(); for (OptionDescription optDesc : options.getOptionsDescriptions()) { if (!optDesc.getFlags().contains(OptionFlag.NOT_CHANGING_CODE)) { sb.append(':').append(allOptions.get(optDesc.name())); } } return FileUtils.md5Sum(sb.toString()); } @Override public IJadxEvents events() { return decompiler.events(); } @Override public IResourcesLoader getResourcesLoader() { return decompiler.getResourcesLoader(); } public AppContext getAppContext() { return appContext; } public void setAppContext(AppContext appContext) { this.appContext = appContext; } @Override public @Nullable JadxGuiContext getGuiContext() { return appContext.getGuiContext(); } @Override public JadxPlugin getPluginInstance() { return plugin; } @Override public JadxPluginInfo getPluginInfo() { return pluginInfo; } @Override public String getPluginId() { return pluginInfo.getPluginId(); } @Override public @Nullable JadxPluginOptions getOptions() { return options; } @Override public IJadxPlugins plugins() { return pluginsData; } @Override public IJadxFiles files() { return new JadxFilesData(pluginInfo, appContext.getFilesGetter()); } @Override public ICodeLoader loadCodeFiles(List files, @Nullable Closeable closeable) { return new MergeCodeLoader( Utils.collectionMap(codeInputs, codeInput -> codeInput.loadFiles(files)), closeable); } @Override public ZipReader getZipReader() { return decompiler.getZipReader(); } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof PluginContext)) { return false; } return this.getPluginId().equals(((PluginContext) other).getPluginId()); } @Override public int hashCode() { return getPluginId().hashCode(); } @Override public int compareTo(PluginContext other) { return this.getPluginId().compareTo(other.getPluginId()); } @Override public String toString() { return getPluginId(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/events/JadxEventsImpl.java ================================================ package jadx.core.plugins.events; import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.events.IJadxEvent; import jadx.api.plugins.events.IJadxEvents; import jadx.api.plugins.events.JadxEventType; import jadx.core.Consts; public class JadxEventsImpl implements IJadxEvents { private static final Logger LOG = LoggerFactory.getLogger(JadxEventsImpl.class); private final JadxEventsManager manager = new JadxEventsManager(); @Override public void send(IJadxEvent event) { if (Consts.DEBUG_EVENTS) { LOG.debug("Sending event: {}", event); } manager.send(event); } @Override public void addListener(JadxEventType eventType, Consumer listener) { manager.addListener(eventType, listener); if (Consts.DEBUG_EVENTS) { LOG.debug("add listener for: {}, stats: {}", eventType, manager.listenersDebugStats()); } } @Override public void removeListener(JadxEventType eventType, Consumer listener) { manager.removeListener(eventType, listener); if (Consts.DEBUG_EVENTS) { LOG.debug("remove listener for: {}, stats: {}", eventType, manager.listenersDebugStats()); } } @Override public void reset() { manager.reset(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/events/JadxEventsManager.java ================================================ package jadx.core.plugins.events; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import jadx.api.plugins.events.IJadxEvent; import jadx.api.plugins.events.JadxEventType; /** * Handle events sending and receiving */ public class JadxEventsManager { private final Map, List>> listeners = new IdentityHashMap<>(); private final ExecutorService eventsThreadPool; public JadxEventsManager() { // TODO: allow to change threading strategy this.eventsThreadPool = Executors.newSingleThreadExecutor(makeThreadFactory()); } @SuppressWarnings("unchecked") public synchronized void addListener(JadxEventType eventType, Consumer listener) { listeners.computeIfAbsent(eventType, et -> new ArrayList<>()) .add((Consumer) listener); } public synchronized boolean removeListener(JadxEventType eventType, Consumer listener) { List> eventListeners = listeners.get(eventType); if (eventListeners != null) { return eventListeners.remove(listener); } return false; } public synchronized void send(IJadxEvent event) { List> consumers = listeners.get(event.getType()); if (consumers != null) { for (Consumer consumer : consumers) { eventsThreadPool.execute(() -> consumer.accept(event)); } } } public synchronized void reset() { listeners.clear(); } private static ThreadFactory makeThreadFactory() { return new ThreadFactory() { private final AtomicInteger threadNumber = new AtomicInteger(0); @Override public Thread newThread(@NotNull Runnable r) { return new Thread(r, "jadx-events-thread-" + threadNumber.incrementAndGet()); } }; } public String listenersDebugStats() { return listeners.entrySet() .stream() .filter(p -> !p.getValue().isEmpty()) .map(p -> p.getKey() + ":" + p.getValue().size()) .collect(Collectors.joining(", ", "[", "]")); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/files/IJadxFilesGetter.java ================================================ package jadx.core.plugins.files; import java.nio.file.Path; public interface IJadxFilesGetter { Path getConfigDir(); Path getCacheDir(); Path getTempDir(); } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/files/JadxFilesData.java ================================================ package jadx.core.plugins.files; import java.nio.file.Path; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.data.IJadxFiles; import jadx.core.utils.files.FileUtils; public class JadxFilesData implements IJadxFiles { private static final String PLUGINS_DATA_DIR = "plugins-data"; private final JadxPluginInfo pluginInfo; private final IJadxFilesGetter filesGetter; public JadxFilesData(JadxPluginInfo pluginInfo, IJadxFilesGetter filesGetter) { this.pluginInfo = pluginInfo; this.filesGetter = filesGetter; } @Override public Path getPluginCacheDir() { return toPluginPath(filesGetter.getCacheDir()); } @Override public Path getPluginConfigDir() { return toPluginPath(filesGetter.getConfigDir()); } @Override public Path getPluginTempDir() { return toPluginPath(filesGetter.getTempDir()); } private Path toPluginPath(Path dir) { Path dirPath = dir.resolve(PLUGINS_DATA_DIR).resolve(pluginInfo.getPluginId()); FileUtils.makeDirs(dirPath); return dirPath; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/files/SingleDirFilesGetter.java ================================================ package jadx.core.plugins.files; import java.nio.file.Path; import jadx.core.utils.files.FileUtils; /** * Use single directory for all jadx files */ public class SingleDirFilesGetter implements IJadxFilesGetter { private final Path baseDir; public SingleDirFilesGetter(Path baseDir) { this.baseDir = baseDir; } @Override public Path getConfigDir() { return makeSubDir("config"); } @Override public Path getCacheDir() { return makeSubDir("cache"); } @Override public Path getTempDir() { return makeSubDir("temp"); } private Path makeSubDir(String subDir) { Path dir = baseDir.resolve(subDir); FileUtils.makeDirs(dir); return dir; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/files/TempFilesGetter.java ================================================ package jadx.core.plugins.files; import java.nio.file.Files; import java.nio.file.Path; import jadx.core.utils.files.FileUtils; public class TempFilesGetter implements IJadxFilesGetter { public static final TempFilesGetter INSTANCE = new TempFilesGetter(); private static final class TempRootHolder { public static final Path TEMP_ROOT_DIR; static { try { TEMP_ROOT_DIR = Files.createTempDirectory("jadx-temp-"); TEMP_ROOT_DIR.toFile().deleteOnExit(); } catch (Exception e) { throw new RuntimeException("Failed to create temp directory", e); } } } private TempFilesGetter() { } @Override public Path getConfigDir() { return makeSubDir("config"); } @Override public Path getCacheDir() { return makeSubDir("cache"); } @Override public Path getTempDir() { return makeSubDir("tmp"); } private Path makeSubDir(String subDir) { Path dir = TempRootHolder.TEMP_ROOT_DIR.resolve(subDir); FileUtils.makeDirs(dir); return dir; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/versions/VerifyRequiredVersion.java ================================================ package jadx.core.plugins.versions; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jetbrains.annotations.Nullable; import jadx.core.Jadx; public class VerifyRequiredVersion { public static boolean isJadxCompatible(@Nullable String reqVersionStr) { return new VerifyRequiredVersion().isCompatible(reqVersionStr); } public static void verify(String requiredJadxVersion) { try { parse(requiredJadxVersion); } catch (Exception e) { throw new IllegalArgumentException("Malformed 'requiredJadxVersion': " + e.getMessage(), e); } } private final String jadxVersion; private final boolean unstable; private final boolean dev; public VerifyRequiredVersion() { this(Jadx.getVersion()); } public VerifyRequiredVersion(String jadxVersion) { this.jadxVersion = jadxVersion; this.unstable = jadxVersion.startsWith("r"); this.dev = jadxVersion.equals(Jadx.VERSION_DEV); } public boolean isCompatible(@Nullable String reqVersionStr) { if (reqVersionStr == null || reqVersionStr.isEmpty()) { return true; } RequiredVersionData reqVer = parse(reqVersionStr); if (dev) { // keep version str parsing for verification return true; } if (unstable) { return VersionComparator.checkAndCompare(jadxVersion, reqVer.getUnstableRev()) >= 0; } return VersionComparator.checkAndCompare(jadxVersion, reqVer.getReleaseVer()) >= 0; } public String getJadxVersion() { return jadxVersion; } private static final Pattern REQ_VER_FORMAT = Pattern.compile("(\\d+\\.\\d+\\.\\d+),\\s+(r\\d+)"); private static RequiredVersionData parse(String reqVersionStr) { Matcher matcher = REQ_VER_FORMAT.matcher(reqVersionStr); if (!matcher.matches()) { throw new RuntimeException("Expect format: " + REQ_VER_FORMAT + ", got: " + reqVersionStr); } return new RequiredVersionData(matcher.group(1), matcher.group(2)); } private static final class RequiredVersionData { private final String releaseVer; private final String unstableRev; private RequiredVersionData(String releaseVer, String unstableRev) { this.releaseVer = releaseVer; this.unstableRev = unstableRev; } public String getReleaseVer() { return releaseVer; } public String getUnstableRev() { return unstableRev; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/plugins/versions/VersionComparator.java ================================================ package jadx.core.plugins.versions; public class VersionComparator { private VersionComparator() { } public static int checkAndCompare(String str1, String str2) { return compare(clean(str1), clean(str2)); } private static String clean(String str) { if (str == null || str.isEmpty()) { return ""; } String result = str.trim().toLowerCase(); if (result.startsWith("jadx-gui-")) { result = result.substring(9); } if (result.startsWith("jadx-")) { result = result.substring(5); } if (result.charAt(0) == 'v') { result = result.substring(1); } if (result.charAt(0) == 'r') { result = result.substring(1); int dot = result.indexOf('.'); if (dot != -1) { result = result.substring(0, dot); } } // treat a package version as part of version result = result.replace('-', '.'); return result; } private static int compare(String str1, String str2) { String[] s1 = str1.split("\\."); int l1 = s1.length; String[] s2 = str2.split("\\."); int l2 = s2.length; int i = 0; // skip equals parts while (i < l1 && i < l2) { if (!s1[i].equals(s2[i])) { break; } i++; } // compare first non-equal ordinal number if (i < l1 && i < l2) { return Integer.valueOf(s1[i]).compareTo(Integer.valueOf(s2[i])); } boolean checkFirst = l1 > l2; boolean zeroTail = isZeroTail(checkFirst ? s1 : s2, i); if (zeroTail) { return 0; } return checkFirst ? 1 : -1; } private static boolean isZeroTail(String[] arr, int pos) { for (int i = pos; i < arr.length; i++) { if (Integer.parseInt(arr[i]) != 0) { return false; } } return true; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/BetterName.java ================================================ package jadx.core.utils; import java.util.HashSet; import java.util.Locale; import java.util.Objects; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.deobf.NameMapper; public class BetterName { private static final Logger LOG = LoggerFactory.getLogger(BetterName.class); private static final boolean DEBUG = false; private static final double TOLERANCE = 0.001; /** * Compares two class names and returns the "better" one. * If both names are equally good, {@code firstName} is returned. */ public static String getBetterClassName(String firstName, String secondName) { return getBetterName(firstName, secondName); } /** * Compares two resource names and returns the "better" one. * If both names are equally good, {@code firstName} is returned. */ public static String getBetterResourceName(String firstName, String secondName) { return getBetterName(firstName, secondName); } private static String getBetterName(String firstName, String secondName) { if (Objects.equals(firstName, secondName)) { return firstName; } if (StringUtils.isEmpty(firstName) || StringUtils.isEmpty(secondName)) { return StringUtils.notEmpty(firstName) ? firstName : secondName; } final var firstResult = analyze(firstName); final var secondResult = analyze(secondName); if (firstResult.digitCount != 0 || secondResult.digitCount != 0) { final var firstRatio = (float) firstResult.digitCount / firstResult.length; final var secondRatio = (float) secondResult.digitCount / secondResult.length; if (Math.abs(secondRatio - firstRatio) >= TOLERANCE) { return firstRatio <= secondRatio ? firstName : secondName; } } return firstResult.length >= secondResult.length ? firstName : secondName; } private static AnalyzeResult analyze(String name) { final var result = new AnalyzeResult(); StringUtils.visitCodePoints(name, cp -> { if (Character.isDigit(cp)) { result.digitCount++; } result.length++; }); return result; } private static class AnalyzeResult { private int length; private int digitCount; } /** * @deprecated Use {@link #getBetterClassName(String, String)} or * {@link #getBetterResourceName(String, String)} instead. */ @Deprecated public static String compareAndGet(String first, String second) { if (Objects.equals(first, second)) { return first; } int firstRating = calcRating(first); int secondRating = calcRating(second); boolean firstBetter = firstRating >= secondRating; if (DEBUG) { if (firstBetter) { LOG.debug("Better name: '{}' > '{}' ({} > {})", first, second, firstRating, secondRating); } else { LOG.debug("Better name: '{}' > '{}' ({} > {})", second, first, secondRating, firstRating); } } return firstBetter ? first : second; } /** * @deprecated This function is an implementation detail of deprecated * {@link #compareAndGet(String, String)} and should not be used outside tests. */ @Deprecated public static int calcRating(String str) { int rating = str.length() * 3; rating += differentCharsCount(str) * 20; if (NameMapper.isAllCharsPrintable(str)) { rating += 100; } if (NameMapper.isValidIdentifier(str)) { rating += 50; } if (str.contains("_")) { // rare in obfuscated names rating += 100; } return rating; } @Deprecated private static int differentCharsCount(String str) { String lower = str.toLowerCase(Locale.ROOT); Set chars = new HashSet<>(); StringUtils.visitCodePoints(lower, chars::add); return chars.size(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/BlockInsnPair.java ================================================ package jadx.core.utils; import java.util.Objects; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.InsnNode; public class BlockInsnPair { private final IBlock block; private final InsnNode insn; public BlockInsnPair(IBlock block, InsnNode insn) { this.block = block; this.insn = insn; } public IBlock getBlock() { return block; } public InsnNode getInsn() { return insn; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof BlockInsnPair)) { return false; } BlockInsnPair that = (BlockInsnPair) o; return block.equals(that.block) && insn.equals(that.insn); } @Override public int hashCode() { return Objects.hash(block, insn); } @Override public String toString() { return "BlockInsnPair{" + block + ": " + insn + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/BlockParentContainer.java ================================================ package jadx.core.utils; import java.util.Objects; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IContainer; public class BlockParentContainer { private final IContainer parent; private final IBlock block; public BlockParentContainer(IContainer parent, IBlock block) { this.parent = Objects.requireNonNull(parent); this.block = Objects.requireNonNull(block); } public IBlock getBlock() { return block; } public IContainer getParent() { return parent; } @Override public String toString() { return "BlockParentContainer{" + block + ", parent=" + parent + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/BlockUtils.java ================================================ package jadx.core.utils; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Queue; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.attributes.nodes.PhiListAttr; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.Edge; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.utils.blocks.BlockSet; import jadx.core.utils.blocks.DFSIteration; import jadx.core.utils.exceptions.JadxRuntimeException; public class BlockUtils { private BlockUtils() { } public static BlockNode getBlockByOffset(int offset, Iterable casesBlocks) { for (BlockNode block : casesBlocks) { if (block.getStartOffset() == offset) { return block; } } throw new JadxRuntimeException("Can't find block by offset: " + InsnUtils.formatOffset(offset) + " in list " + casesBlocks); } public static BlockNode selectOther(BlockNode node, List blocks) { List list = blocks; if (list.size() > 2) { list = cleanBlockList(list); } if (list.size() != 2) { throw new JadxRuntimeException("Incorrect nodes count for selectOther: " + node + " in " + list); } BlockNode first = list.get(0); if (first != node) { return first; } else { return list.get(1); } } public static BlockNode selectOtherSafe(BlockNode node, List blocks) { int size = blocks.size(); if (size == 1) { BlockNode first = blocks.get(0); return first != node ? first : null; } if (size == 2) { BlockNode first = blocks.get(0); return first != node ? first : blocks.get(1); } return null; } public static boolean isExceptionHandlerPath(BlockNode b) { if (b.contains(AType.EXC_HANDLER) || b.contains(AFlag.EXC_BOTTOM_SPLITTER) || b.contains(AFlag.REMOVE)) { return true; } if (b.contains(AFlag.SYNTHETIC)) { List s = b.getSuccessors(); return s.size() == 1 && s.get(0).contains(AType.EXC_HANDLER); } return false; } /** * Remove exception handlers from block nodes list */ private static List cleanBlockList(List list) { List ret = new ArrayList<>(list.size()); for (BlockNode block : list) { if (!isExceptionHandlerPath(block)) { ret.add(block); } } return ret; } /** * Remove exception handlers from block nodes bitset */ public static void cleanBitSet(MethodNode mth, BitSet bs) { for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) { BlockNode block = mth.getBasicBlocks().get(i); if (isExceptionHandlerPath(block)) { bs.clear(i); } } } public static boolean isBackEdge(BlockNode from, BlockNode to) { if (to == null) { return false; } if (from.getCleanSuccessors().contains(to)) { return false; // already checked } return from.getSuccessors().contains(to); } public static boolean isFollowBackEdge(BlockNode block) { if (block == null) { return false; } if (block.contains(AFlag.LOOP_START)) { List predecessors = block.getPredecessors(); if (predecessors.size() == 1) { BlockNode loopEndBlock = predecessors.get(0); if (loopEndBlock.contains(AFlag.LOOP_END)) { List loops = loopEndBlock.getAll(AType.LOOP); for (LoopInfo loop : loops) { if (loop.getStart().equals(block) && loop.getEnd().equals(loopEndBlock)) { return true; } } } } } return false; } /** * Check if instruction contains in block (use == for comparison, not equals) */ public static boolean blockContains(BlockNode block, InsnNode insn) { for (InsnNode bi : block.getInstructions()) { if (bi == insn) { return true; } } return false; } public static boolean checkFirstInsn(IBlock block, Predicate predicate) { InsnNode insn = getFirstInsn(block); return insn != null && predicate.test(insn); } public static boolean checkLastInsnType(IBlock block, InsnType expectedType) { InsnNode insn = getLastInsn(block); return insn != null && insn.getType() == expectedType; } public static InsnNode getLastInsnWithType(IBlock block, InsnType expectedType) { InsnNode insn = getLastInsn(block); if (insn != null && insn.getType() == expectedType) { return insn; } return null; } public static int getFirstSourceLine(IBlock block) { for (InsnNode insn : block.getInstructions()) { int line = insn.getSourceLine(); if (line != 0) { return line; } } return 0; } @Nullable public static InsnNode getFirstInsn(@Nullable IBlock block) { if (block == null) { return null; } List insns = block.getInstructions(); if (insns.isEmpty()) { return null; } return insns.get(0); } @Nullable public static InsnNode getLastInsn(@Nullable IBlock block) { if (block == null) { return null; } List insns = block.getInstructions(); if (insns.isEmpty()) { return null; } return insns.get(insns.size() - 1); } public static boolean isExitBlock(MethodNode mth, BlockNode block) { if (block == mth.getExitBlock()) { return true; } return isExitBlock(block); } public static boolean isExitBlock(BlockNode block) { List successors = block.getSuccessors(); if (successors.isEmpty()) { return true; } if (successors.size() == 1) { BlockNode next = successors.get(0); return next.getSuccessors().isEmpty(); } return false; } public static boolean containsExitInsn(IBlock block) { InsnNode lastInsn = BlockUtils.getLastInsn(block); if (lastInsn == null) { return false; } InsnType type = lastInsn.getType(); return type == InsnType.RETURN || type == InsnType.THROW || type == InsnType.BREAK || type == InsnType.CONTINUE; } public static @Nullable BlockNode getBlockByInsn(MethodNode mth, @Nullable InsnNode insn) { return getBlockByInsn(mth, insn, mth.getBasicBlocks()); } public static @Nullable BlockNode getBlockByInsn(MethodNode mth, @Nullable InsnNode insn, List blocks) { if (insn == null) { return null; } if (insn instanceof PhiInsn) { return searchBlockWithPhi(mth, (PhiInsn) insn); } if (insn.contains(AFlag.WRAPPED)) { return getBlockByWrappedInsn(mth, insn); } for (BlockNode bn : blocks) { if (blockContains(bn, insn)) { return bn; } } return null; } public static BlockNode searchBlockWithPhi(MethodNode mth, PhiInsn insn) { for (BlockNode block : mth.getBasicBlocks()) { PhiListAttr phiListAttr = block.get(AType.PHI_LIST); if (phiListAttr != null) { for (PhiInsn phiInsn : phiListAttr.getList()) { if (phiInsn == insn) { return block; } } } } return null; } private static BlockNode getBlockByWrappedInsn(MethodNode mth, InsnNode insn) { for (BlockNode bn : mth.getBasicBlocks()) { for (InsnNode bi : bn.getInstructions()) { if (bi == insn || foundWrappedInsn(bi, insn) != null) { return bn; } } } return null; } public static InsnNode searchInsnParent(MethodNode mth, InsnNode insn) { InsnArg insnArg = searchWrappedInsnParent(mth, insn); if (insnArg == null) { return null; } return insnArg.getParentInsn(); } public static InsnArg searchWrappedInsnParent(MethodNode mth, InsnNode insn) { if (!insn.contains(AFlag.WRAPPED)) { return null; } for (BlockNode bn : mth.getBasicBlocks()) { for (InsnNode bi : bn.getInstructions()) { InsnArg res = foundWrappedInsn(bi, insn); if (res != null) { return res; } } } return null; } private static InsnArg foundWrappedInsn(InsnNode container, InsnNode insn) { for (InsnArg arg : container.getArguments()) { if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); if (wrapInsn == insn) { return arg; } InsnArg res = foundWrappedInsn(wrapInsn, insn); if (res != null) { return res; } } } if (container instanceof TernaryInsn) { return foundWrappedInsnInCondition(((TernaryInsn) container).getCondition(), insn); } return null; } private static InsnArg foundWrappedInsnInCondition(IfCondition cond, InsnNode insn) { if (cond.isCompare()) { IfNode cmpInsn = cond.getCompare().getInsn(); return foundWrappedInsn(cmpInsn, insn); } for (IfCondition nestedCond : cond.getArgs()) { InsnArg res = foundWrappedInsnInCondition(nestedCond, insn); if (res != null) { return res; } } return null; } public static BitSet newBlocksBitSet(MethodNode mth) { return new BitSet(mth.getBasicBlocks().size()); } public static BitSet copyBlocksBitSet(MethodNode mth, BitSet bitSet) { BitSet copy = new BitSet(mth.getBasicBlocks().size()); if (!bitSet.isEmpty()) { copy.or(bitSet); } return copy; } public static BitSet blocksToBitSet(MethodNode mth, Collection blocks) { BitSet bs = newBlocksBitSet(mth); for (BlockNode block : blocks) { bs.set(block.getId()); } return bs; } @Nullable public static BlockNode bitSetToOneBlock(MethodNode mth, BitSet bs) { if (bs == null || bs.cardinality() != 1) { return null; } return mth.getBasicBlocks().get(bs.nextSetBit(0)); } public static List bitSetToBlocks(MethodNode mth, BitSet bs) { if (bs == null || bs == EmptyBitSet.EMPTY) { return Collections.emptyList(); } int size = bs.cardinality(); if (size == 0) { return Collections.emptyList(); } List blocks = new ArrayList<>(size); for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) { BlockNode block = mth.getBasicBlocks().get(i); blocks.add(block); } return blocks; } public static void forEachBlockFromBitSet(MethodNode mth, BitSet bs, Consumer consumer) { if (bs == null || bs == EmptyBitSet.EMPTY || bs.isEmpty()) { return; } List blocks = mth.getBasicBlocks(); for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) { consumer.accept(blocks.get(i)); } } /** * Return first successor which not exception handler and not follow loop back edge */ @Nullable public static BlockNode getNextBlock(BlockNode block) { List s = block.getCleanSuccessors(); return s.isEmpty() ? null : s.get(0); } @Nullable public static BlockNode getPrevBlock(BlockNode block) { List preds = block.getPredecessors(); return preds.size() == 1 ? preds.get(0) : null; } /** * Return successor on path to 'pathEnd' block */ public static BlockNode getNextBlockToPath(BlockNode block, BlockNode pathEnd) { List successors = block.getCleanSuccessors(); if (successors.contains(pathEnd)) { return pathEnd; } Set path = getAllPathsBlocks(block, pathEnd); for (BlockNode s : successors) { if (path.contains(s)) { return s; } } return null; } /** * Return predecessor on path from 'pathStart' block */ public static @Nullable BlockNode getPrevBlockOnPath(MethodNode mth, BlockNode block, BlockNode pathStart) { BlockSet preds = BlockSet.from(mth, block.getPredecessors()); if (preds.contains(pathStart)) { return pathStart; } DFSIteration dfs = new DFSIteration(mth, pathStart, BlockNode::getCleanSuccessors); while (true) { BlockNode next = dfs.next(); if (next == null) { return null; } if (preds.contains(next)) { return next; } } } /** * Visit blocks on any path from start to end. * Only one path will be visited! */ public static boolean visitBlocksOnPath(MethodNode mth, BlockNode start, BlockNode end, Consumer visitor) { visitor.accept(start); if (start == end) { return true; } if (start.getCleanSuccessors().contains(end)) { visitor.accept(end); return true; } // DFS on clean successors BitSet visited = newBlocksBitSet(mth); Deque queue = new ArrayDeque<>(); queue.addLast(start); while (true) { BlockNode current = queue.peekLast(); if (current == null) { return false; } boolean added = false; for (BlockNode next : current.getCleanSuccessors()) { if (next == end) { queue.removeFirst(); // start already visited queue.addLast(next); queue.forEach(visitor); return true; } int id = next.getId(); if (!visited.get(id)) { visited.set(id); queue.addLast(next); added = true; break; } } if (!added) { queue.pollLast(); if (queue.isEmpty()) { return false; } } } } public static List collectAllPredecessors(MethodNode mth, BlockNode startBlock) { List list = new ArrayList<>(mth.getBasicBlocks().size()); Function> nextFunc = BlockNode::getPredecessors; visitDFS(mth, startBlock, nextFunc, list::add); return list; } public static List collectAllSuccessors(MethodNode mth, BlockNode startBlock, boolean clean) { List list = new ArrayList<>(mth.getBasicBlocks().size()); Function> nextFunc = clean ? BlockNode::getCleanSuccessors : BlockNode::getSuccessors; visitDFS(mth, startBlock, nextFunc, list::add); return list; } public static List collectAllSuccessorsUntil(MethodNode mth, BlockNode startBlock, boolean clean, Predicate stopCondition) { List blocks = new ArrayList<>(); collectAllSuccessorsUntil(mth, blocks, startBlock, clean, stopCondition); return blocks; } private static void collectAllSuccessorsUntil(MethodNode mth, List blocks, BlockNode currentBlock, boolean clean, Predicate stopCondition) { if (blocks.contains(currentBlock)) { return; } blocks.add(currentBlock); if (stopCondition.test(currentBlock)) { return; } List successors = clean ? currentBlock.getCleanSuccessors() : currentBlock.getSuccessors(); for (BlockNode successor : successors) { collectAllSuccessorsUntil(mth, blocks, successor, clean, stopCondition); } } @Nullable public static BlockNode getBottomCommonPredecessor(MethodNode mth, List blocks, Set containedBlocks) { return getBottomCommonPredecessor(mth, blocks, containedBlocks, false); } @Nullable public static BlockNode getBottomCommonPredecessor(MethodNode mth, List blocks, Set containedBlocks, boolean addTopBlock) { if (blocks.isEmpty()) { return null; } Set visitedPredecessorsByAll = new HashSet<>(collectAllPredecessors(mth, blocks.get(0))); if (addTopBlock) { BlockNode topBlock = BlockUtils.getBottomBlock(blocks); if (topBlock == null) { // TODO: These nodes are not connected so there will be no common successor ???? // return null; } else { visitedPredecessorsByAll.add(topBlock); } } for (int i = 1; i < blocks.size(); i++) { BlockNode nextBlock = blocks.get(i); List predecessors = collectAllPredecessors(mth, nextBlock); visitedPredecessorsByAll.retainAll(predecessors); } return BlockUtils.getBottomBlock(new ArrayList<>(visitedPredecessorsByAll)); } @Nullable public static BlockNode getTopCommonSuccessor(MethodNode mth, List blocks, boolean cleanOnly) { return getTopCommonSuccessor(mth, blocks, cleanOnly, false); } @Nullable public static BlockNode getTopCommonSuccessor(MethodNode mth, List blocks, boolean cleanOnly, boolean addTopBlock) { if (blocks.isEmpty()) { return null; } Set visitedSuccessorsByAll = new HashSet<>(collectAllSuccessors(mth, blocks.get(0), cleanOnly)); if (addTopBlock) { BlockNode topBlock = BlockUtils.getTopBlock(blocks); if (topBlock == null) { // TODO: These nodes are not connected so there will be no common successor ???? // return null; } else { visitedSuccessorsByAll.add(topBlock); } } for (int i = 1; i < blocks.size(); i++) { BlockNode nextBlock = blocks.get(i); List successors = collectAllSuccessors(mth, nextBlock, cleanOnly); visitedSuccessorsByAll.retainAll(successors); } return BlockUtils.getTopBlock(new ArrayList<>(visitedSuccessorsByAll)); } public static void visitDFS(MethodNode mth, Consumer visitor) { visitDFS(mth, mth.getEnterBlock(), BlockNode::getSuccessors, visitor); } public static void visitReverseDFS(MethodNode mth, Consumer visitor) { visitDFS(mth, mth.getExitBlock(), BlockNode::getPredecessors, visitor); } private static void visitDFS(MethodNode mth, BlockNode startBlock, Function> nextFunc, Consumer visitor) { DFSIteration dfsIteration = new DFSIteration(mth, startBlock, nextFunc); while (true) { BlockNode next = dfsIteration.next(); if (next == null) { return; } visitor.accept(next); } } public static List collectPredecessors(MethodNode mth, BlockNode start, Collection stopBlocks) { BitSet bs = newBlocksBitSet(mth); if (!stopBlocks.isEmpty()) { bs.or(blocksToBitSet(mth, stopBlocks)); } List list = new ArrayList<>(); traversePredecessors(start, bs, block -> { list.add(block); return false; }); return list; } public static void visitPredecessorsUntil(MethodNode mth, BlockNode start, Predicate visitor) { traversePredecessors(start, newBlocksBitSet(mth), visitor); } /** * Up BFS. * To stop return true from predicate */ private static void traversePredecessors(BlockNode start, BitSet visited, Predicate visitor) { Queue queue = new ArrayDeque<>(); queue.add(start); while (true) { BlockNode current = queue.poll(); if (current == null || visitor.test(current)) { return; } for (BlockNode next : current.getPredecessors()) { int id = next.getId(); if (!visited.get(id)) { visited.set(id); queue.add(next); } } } } /** * Collect blocks from all possible execution paths from 'start' to 'end' */ public static Set getAllPathsBlocks(BlockNode start, BlockNode end) { Set set = new HashSet<>(); set.add(start); if (start != end) { addPredecessors(set, end, start); } return set; } /** * Collect blocks from one possible execution path from 'start' to 'end' containing no instructions */ public static List getOneEmptyPath(BlockNode start, BlockNode end) { return collectPathUntil(start, end, false, b -> { return b.getInstructions().isEmpty() || b.equals(end); }); } /** * Collect blocks from one possible execution path from 'start' to 'end' */ public static List getOnePath(BlockNode start, BlockNode end) { return collectPathUntil(start, end, false, b -> true); } private static void addPredecessors(Set set, BlockNode from, BlockNode until) { set.add(from); for (BlockNode pred : from.getPredecessors()) { if (pred != until && !set.contains(pred)) { addPredecessors(set, pred, until); } } } private static boolean traverseSuccessorsUntil(BlockNode from, BlockNode until, BitSet visited, boolean clean) { return traverseSuccessorsUntil(from, until, visited, clean, b -> true); } /** * * Traverse succcessors until a node is found * * @param from the source node to begin traversing * @param until the destination node to halt traversing * @param visited the set of visited blocks so far * @param clean use only clean successors * @param pred a predicate that must be true to traverse a block (until or a reachable dominator * of until must satisfy pred) * @return true if there is a path from `from` to `until` or a dominator of `until` through blocks * that satisfy `pred`, false otherwise */ private static boolean traverseSuccessorsUntil(BlockNode from, BlockNode until, BitSet visited, boolean clean, Predicate pred) { List nodes = clean ? from.getCleanSuccessors() : from.getSuccessors(); for (BlockNode s : nodes) { if (!pred.test(s)) { // Only explore blocks such that the predicate holds continue; } if (s == until) { return true; } if (s == from) { // ignore possible block self loop continue; } int id = s.getPos(); if (!visited.get(id)) { visited.set(id); if (until.isDominator(s)) { return true; } if (traverseSuccessorsUntil(s, until, visited, clean, pred)) { return true; } } } return false; } /** * * Traverse succcessors until a node is found, collecting the path to the node * * @param from the source node to begin traversing * @param until the destination node to halt traversing * @param clean use only clean successors * @param pred a predicate that must be true to traverse a block (until must satisfy pred) * @return the list of blocks satisfying pred on a path between from and until (inclusive), or null * if no such path exists */ public static List collectPathUntil(BlockNode from, BlockNode until, boolean clean, Predicate pred) { List path = internalCollectPathUntil(from, until, new BitSet(), clean, pred); if (path == null) { return path; } path.add(from); Collections.reverse(path); return path; } /** * * Traverse succcessors until a node is found, collecting the path to the node * * @param from the source node to begin traversing * @param until the destination node to halt traversing * @param visited the set of visited blocks so far * @param clean use only clean successors * @param pred a predicate that must be true to traverse a block (until must satisfy pred) * @return the list of blocks satisfying pred on a path between from (exclusive) and until * (inclusive) in reverse order, or null if no such path exists */ private static List internalCollectPathUntil(BlockNode from, BlockNode until, BitSet visited, boolean clean, Predicate pred) { List nodes = clean ? from.getCleanSuccessors() : from.getSuccessors(); for (BlockNode s : nodes) { if (!pred.test(s)) { // Only explore blocks such that the predicate holds continue; } if (s == until) { List path = new ArrayList<>(); path.add(s); return path; } int id = s.getPos(); if (!visited.get(id)) { visited.set(id); List path = internalCollectPathUntil(s, until, visited, clean, pred); if (path != null) { path.add(s); return path; } } } return null; } /** * Search at least one path from startBlocks to end */ public static boolean atLeastOnePathExists(Collection startBlocks, BlockNode end) { for (BlockNode startBlock : startBlocks) { if (isPathExists(startBlock, end)) { return true; } } return false; } /** * Check if exist path from every startBlocks to end */ public static boolean isAllPathExists(Collection startBlocks, BlockNode end) { for (BlockNode startBlock : startBlocks) { if (!isPathExists(startBlock, end)) { return false; } } return true; } public static boolean isPathExists(BlockNode start, BlockNode end) { if (start == end || start.getCleanSuccessors().contains(end)) { return true; } return traverseSuccessorsUntil(start, end, new BitSet(), true); } public static boolean isAnyPathExists(BlockNode start, BlockNode end) { if (start == end || end.isDominator(start) || start.getSuccessors().contains(end)) { return true; } return traverseSuccessorsUntil(start, end, new BitSet(), false); } public static boolean isPathExists(BlockNode start, BlockNode end, Predicate pred) { if (start == end) { return true; } return traverseSuccessorsUntil(start, end, new BitSet(), false, pred); } public static BlockNode getTopBlock(List blocks) { if (blocks.size() == 1) { return blocks.get(0); } for (BlockNode from : blocks) { boolean top = true; for (BlockNode to : blocks) { if (from != to && !isAnyPathExists(from, to)) { top = false; break; } } if (top) { return from; } } return null; } /** * Search last block in control flow graph from input set. */ @Nullable public static BlockNode getBottomBlock(List blocks) { return getBottomBlock(blocks, false); } public static BlockNode getBottomBlock(List blocks, boolean clean) { if (blocks.size() == 1) { return blocks.get(0); } // attempt 1: look for a block dominated by every other block // don't do this if clean, since dominators always consider all successors if (!clean) { for (BlockNode bottomCandidate : blocks) { boolean bottom = true; for (BlockNode from : blocks) { if (bottomCandidate != from && !bottomCandidate.isDominator(from)) { bottom = false; break; } } if (bottom) { return bottomCandidate; } } } // attempt 2: look for a block with a path from every other block for (BlockNode bottomCandidate : blocks) { boolean bottom = true; for (BlockNode from : blocks) { if (clean) { if (bottomCandidate != from && !isPathExists(from, bottomCandidate)) { bottom = false; break; } } else { if (bottomCandidate != from && !isAnyPathExists(from, bottomCandidate)) { bottom = false; break; } } } if (bottom) { return bottomCandidate; } } return null; } public static boolean isOnlyOnePathExists(BlockNode start, BlockNode end) { if (start == end) { return true; } if (!end.isDominator(start)) { return false; } BlockNode currentNode = start; while (currentNode.getCleanSuccessors().size() == 1) { currentNode = currentNode.getCleanSuccessors().get(0); if (currentNode == end) { return true; } } return false; } /** * Search for first node which not dominated by dom, starting from start */ public static BlockNode traverseWhileDominates(BlockNode dom, BlockNode start) { for (BlockNode node : start.getCleanSuccessors()) { if (!node.isDominator(dom)) { return node; } else { BlockNode out = traverseWhileDominates(dom, node); if (out != null) { return out; } } } return null; } /** * Search the lowest common ancestor in dominator tree for input set. */ @Nullable public static BlockNode getCommonDominator(MethodNode mth, List blocks) { BitSet doms = newBlocksBitSet(mth); // collect all dominators from input set doms.set(0, mth.getBasicBlocks().size()); blocks.forEach(b -> doms.and(b.getDoms())); // exclude all dominators of immediate dominator (including self) BitSet combine = newBlocksBitSet(mth); combine.or(doms); forEachBlockFromBitSet(mth, doms, block -> { BlockNode idom = block.getIDom(); if (idom != null) { combine.andNot(idom.getDoms()); combine.clear(idom.getId()); } }); return bitSetToOneBlock(mth, combine); } /** * Return the dominace frontier of an edge - the blocks for which any path to the block must pass * through this edge */ public static BitSet getDomFrontierThroughEdge(Edge edge) { BlockNode target = edge.getTarget(); if (target.getPredecessors().size() > 1) { // If the target node has other incoming edges, the dominance frontier is a single block BitSet dominanceFrontier = new BitSet(); dominanceFrontier.set(target.getPos()); return dominanceFrontier; } else { // Otherwise the dominance frontier is equivalent to the domiance frontier of the target return target.getDomFrontier(); } } /** * Return common cross block for input set. * * @return could be one of the giving blocks. null if cross is a method exit block. */ @Nullable public static BlockNode getPathCross(MethodNode mth, Collection blocks) { BitSet domFrontBS = newBlocksBitSet(mth); BitSet tmpBS = newBlocksBitSet(mth); // store block itself and its domFrontier boolean first = true; for (BlockNode b : blocks) { tmpBS.clear(); tmpBS.set(b.getId()); tmpBS.or(b.getDomFrontier()); if (first) { domFrontBS.or(tmpBS); first = false; } else { domFrontBS.and(tmpBS); } } domFrontBS.clear(mth.getExitBlock().getId()); if (domFrontBS.isEmpty()) { return null; } BlockNode oneBlock = bitSetToOneBlock(mth, domFrontBS); if (oneBlock != null) { return oneBlock; } BitSet excluded = newBlocksBitSet(mth); // exclude method exit and loop start blocks excluded.set(mth.getExitBlock().getId()); // exclude loop start blocks mth.getLoops().forEach(l -> excluded.set(l.getStart().getId())); if (!mth.isNoExceptionHandlers()) { // exclude exception handlers paths mth.getExceptionHandlers().forEach(h -> addExcHandler(mth, h, excluded)); } domFrontBS.andNot(excluded); oneBlock = bitSetToOneBlock(mth, domFrontBS); if (oneBlock != null) { return oneBlock; } BitSet combinedDF = newBlocksBitSet(mth); int k = mth.getBasicBlocks().size(); while (true) { // collect dom frontier blocks from current set until only one block left forEachBlockFromBitSet(mth, domFrontBS, block -> { BitSet domFrontier = block.getDomFrontier(); if (!domFrontier.isEmpty()) { combinedDF.or(domFrontier); } }); combinedDF.andNot(excluded); int cardinality = combinedDF.cardinality(); if (cardinality == 1) { return bitSetToOneBlock(mth, combinedDF); } if (cardinality == 0) { return null; } if (k-- < 0) { mth.addWarnComment("Path cross not found for " + blocks + ", limit reached: " + mth.getBasicBlocks().size()); return null; } // replace domFrontBS with combinedDF domFrontBS.clear(); domFrontBS.or(combinedDF); combinedDF.clear(); } } private static void addExcHandler(MethodNode mth, ExceptionHandler handler, BitSet set) { BlockNode handlerBlock = handler.getHandlerBlock(); if (handlerBlock == null) { mth.addDebugComment("Null handler block in: " + handler); return; } set.set(handlerBlock.getId()); } public static BlockNode getPathCross(MethodNode mth, BlockNode b1, BlockNode b2) { if (b1 == b2) { return b1; } if (b1 == null || b2 == null) { return null; } return getPathCross(mth, Arrays.asList(b1, b2)); } /** * Collect all block dominated by 'dominator', starting from 'start' */ public static List collectBlocksDominatedBy(MethodNode mth, BlockNode dominator, BlockNode start) { List result = new ArrayList<>(); collectWhileDominates(dominator, start, result, newBlocksBitSet(mth), false); return result; } /** * Collect all block dominated by 'dominator', starting from 'start', including exception handlers */ public static Set collectBlocksDominatedByWithExcHandlers(MethodNode mth, BlockNode dominator, BlockNode start) { Set result = new LinkedHashSet<>(); collectWhileDominates(dominator, start, result, newBlocksBitSet(mth), true); return result; } private static void collectWhileDominates(BlockNode dominator, BlockNode child, Collection result, BitSet visited, boolean includeExcHandlers) { if (visited.get(child.getId())) { return; } visited.set(child.getId()); List successors = includeExcHandlers ? child.getSuccessors() : child.getCleanSuccessors(); for (BlockNode node : successors) { if (node.isDominator(dominator)) { result.add(node); collectWhileDominates(dominator, node, result, visited, includeExcHandlers); } } } /** * Visit blocks on path without branching or merging paths. */ public static void visitSinglePath(BlockNode startBlock, Consumer visitor) { if (startBlock == null) { return; } visitor.accept(startBlock); BlockNode next = getNextSinglePathBlock(startBlock); while (next != null) { visitor.accept(next); next = getNextSinglePathBlock(next); } } @Nullable public static BlockNode getNextSinglePathBlock(BlockNode block) { if (block == null || block.getPredecessors().size() > 1) { return null; } List successors = block.getSuccessors(); return successors.size() == 1 ? successors.get(0) : null; } public static List buildSimplePath(BlockNode block) { if (block == null) { return Collections.emptyList(); } List list = new ArrayList<>(); if (block.getCleanSuccessors().size() >= 2) { return Collections.emptyList(); } list.add(block); BlockNode currentBlock = getNextBlock(block); while (currentBlock != null && currentBlock.getCleanSuccessors().size() < 2 && currentBlock.getPredecessors().size() == 1) { list.add(currentBlock); currentBlock = getNextBlock(currentBlock); } return list; } /** * Set 'SKIP' flag for all synthetic predecessors from start block. */ public static void skipPredSyntheticPaths(BlockNode block) { for (BlockNode pred : block.getPredecessors()) { if (pred.contains(AFlag.SYNTHETIC) && !pred.contains(AFlag.EXC_TOP_SPLITTER) && !pred.contains(AFlag.EXC_BOTTOM_SPLITTER) && pred.getInstructions().isEmpty()) { pred.add(AFlag.DONT_GENERATE); skipPredSyntheticPaths(pred); } } } /** * Follow empty blocks and return end of path block (first not empty). * Return start block if no such path. */ public static BlockNode followEmptyPath(BlockNode start) { return followEmptyPath(start, false); } public static BlockNode followEmptyPath(BlockNode start, Boolean reverse) { return followEmptyPath(start, reverse, true); } public static BlockNode followEmptyPath(BlockNode start, Boolean reverse, boolean cleanOnly) { while (true) { BlockNode next = getNextBlockOnEmptyPath(start, reverse, cleanOnly); if (next == null) { return start; } start = next; } } public static List followEmptyUpPathWithinSet(BlockNode start, Collection traversableBlocks) { List results = new LinkedList<>(); followEmptyUpPathWithinSet(results, start, traversableBlocks, new HashSet<>()); return results; } public static void followEmptyUpPathWithinSet(List results, BlockNode start, Collection traversableBlocks, Collection traversedBlocks) { List predecessors = ListUtils.filter(start.getPredecessors(), traversableBlocks::contains); for (BlockNode predecessor : predecessors) { if (!traversableBlocks.contains(predecessor) || traversedBlocks.contains(predecessor)) { continue; } traversedBlocks.add(predecessor); if (predecessor.getInstructions().isEmpty()) { followEmptyUpPathWithinSet(results, start, traversableBlocks, traversedBlocks); } else { results.add(predecessor); } start = predecessor; } } public static void visitBlocksOnEmptyPath(BlockNode start, Consumer visitor) { visitBlocksOnEmptyPath(start, visitor, false); } public static void visitBlocksOnEmptyPath(BlockNode start, Consumer visitor, boolean reverse) { while (true) { BlockNode next = getNextBlockOnEmptyPath(start, reverse); if (next == null) { return; } visitor.accept(next); start = next; } } @Nullable private static BlockNode getNextBlockOnEmptyPath(BlockNode block) { return getNextBlockOnEmptyPath(block, false); } @Nullable private static BlockNode getNextBlockOnEmptyPath(BlockNode block, Boolean reverse) { return getNextBlockOnEmptyPath(block, reverse, true); } @Nullable private static BlockNode getNextBlockOnEmptyPath(BlockNode block, Boolean reverse, boolean cleanOnly) { if (!block.getInstructions().isEmpty() || (!reverse && block.getPredecessors().size() > 1) || (reverse && block.getCleanSuccessors().size() > 1)) { return null; } List nextBlocks = reverse ? block.getPredecessors() : (cleanOnly ? block.getCleanSuccessors() : block.getSuccessors()); if (nextBlocks.size() != 1) { return null; } return nextBlocks.get(0); } /** * Return true if on path from start to end no instructions and no branches. */ public static boolean isEmptySimplePath(BlockNode start, BlockNode end) { if (start == end && start.getInstructions().isEmpty()) { return true; } if (!start.getInstructions().isEmpty() || start.getCleanSuccessors().size() != 1) { return false; } BlockNode block = getNextBlock(start); while (block != null && block != end && block.getCleanSuccessors().size() < 2 && block.getPredecessors().size() == 1 && block.getInstructions().isEmpty()) { block = getNextBlock(block); } return block == end; } /** * Return predecessor of synthetic block or same block otherwise. */ public static BlockNode skipSyntheticPredecessor(BlockNode block) { if (block.isSynthetic() && block.getInstructions().isEmpty() && block.getPredecessors().size() == 1) { return block.getPredecessors().get(0); } return block; } public static boolean isAllBlocksEmpty(List blocks) { if (Utils.isEmpty(blocks)) { return true; } for (BlockNode block : blocks) { if (!block.getInstructions().isEmpty()) { return false; } } return true; } public static List collectAllInsns(List blocks) { List insns = new ArrayList<>(); blocks.forEach(block -> insns.addAll(block.getInstructions())); return insns; } /** * Return limited number of instructions from method. * Return empty list if method contains more than limit. */ public static List collectInsnsWithLimit(List blocks, int limit) { List insns = new ArrayList<>(limit); for (BlockNode block : blocks) { List blockInsns = block.getInstructions(); int blockSize = blockInsns.size(); if (blockSize == 0) { continue; } if (insns.size() + blockSize > limit) { return Collections.emptyList(); } insns.addAll(blockInsns); } return insns; } /** * Return insn if it is only one instruction in this method. Return null otherwise. */ @Nullable public static InsnNode getOnlyOneInsnFromMth(MethodNode mth) { if (mth.isNoCode()) { return null; } InsnNode insn = null; for (BlockNode block : mth.getBasicBlocks()) { List blockInsns = block.getInstructions(); int blockSize = blockInsns.size(); if (blockSize == 0) { continue; } if (blockSize > 1) { return null; } if (insn != null) { return null; } insn = blockInsns.get(0); } return insn; } public static boolean isFirstInsn(MethodNode mth, InsnNode insn) { BlockNode startBlock = followEmptyPath(mth.getEnterBlock()); if (startBlock != null && !startBlock.getInstructions().isEmpty()) { return startBlock.getInstructions().get(0) == insn; } // handle branching with empty blocks BlockNode block = getBlockByInsn(mth, insn); if (block == null) { throw new JadxRuntimeException("Insn not found in method: " + insn); } if (block.getInstructions().get(0) != insn) { return false; } Set allPathsBlocks = getAllPathsBlocks(mth.getEnterBlock(), block); for (BlockNode pathBlock : allPathsBlocks) { if (!pathBlock.getInstructions().isEmpty() && pathBlock != block) { return false; } } return true; } /** * Replace insn by index i in block, * for proper copy attributes, assume attributes are not overlap */ public static void replaceInsn(MethodNode mth, BlockNode block, int i, InsnNode insn) { InsnNode prevInsn = block.getInstructions().get(i); insn.copyAttributesFrom(prevInsn); insn.inheritMetadata(prevInsn); insn.setOffset(prevInsn.getOffset()); block.getInstructions().set(i, insn); RegisterArg result = insn.getResult(); RegisterArg prevResult = prevInsn.getResult(); if (result != null && prevResult != null && result.sameRegAndSVar(prevResult)) { // Don't unbind result for same register. // Unbind will remove arg from PHI and not add it back on rebind. InsnRemover.unbindAllArgs(mth, prevInsn); } else { InsnRemover.unbindInsn(mth, prevInsn); } insn.rebindArgs(); } public static boolean replaceInsn(MethodNode mth, BlockNode block, InsnNode oldInsn, InsnNode newInsn) { List instructions = block.getInstructions(); int size = instructions.size(); for (int i = 0; i < size; i++) { InsnNode instruction = instructions.get(i); if (instruction == oldInsn) { replaceInsn(mth, block, i, newInsn); return true; } } return false; } public static void removeInstructions(List blocks) { for (IBlock block : blocks) { block.getInstructions().clear(); } } public static boolean insertBeforeInsn(BlockNode block, InsnNode insn, InsnNode newInsn) { int index = getInsnIndexInBlock(block, insn); if (index == -1) { return false; } block.getInstructions().add(index, newInsn); return true; } public static boolean insertAfterInsn(BlockNode block, InsnNode insn, InsnNode newInsn) { int index = getInsnIndexInBlock(block, insn); if (index == -1) { return false; } block.getInstructions().add(index + 1, newInsn); return true; } public static int getInsnIndexInBlock(BlockNode block, InsnNode insn) { List instructions = block.getInstructions(); int size = instructions.size(); for (int i = 0; i < size; i++) { if (instructions.get(i) == insn) { return i; } } return -1; } public static boolean replaceInsn(MethodNode mth, InsnNode oldInsn, InsnNode newInsn) { for (BlockNode block : mth.getBasicBlocks()) { if (replaceInsn(mth, block, oldInsn, newInsn)) { return true; } } return false; } public static BlockNode getTopSplitterForHandler(BlockNode handlerBlock) { BlockNode block = getBlockWithFlag(handlerBlock.getPredecessors(), AFlag.EXC_TOP_SPLITTER); if (block == null) { throw new JadxRuntimeException("Can't find top splitter block for handler:" + handlerBlock); } return block; } /** * Return out block of try catch, by finding where try branch meets catch branch. * It traverse domFrontier start from handler block, find the first frontier * whose predecessor is try end. *
* It could return null if they never meets, but this doesn't mean that catch * ends at the method exit. * (see TestSwitchWithTryCatch and ExcHandlersRegionMaker#processExcHandler). */ @Nullable public static BlockNode getTryAndHandlerCrossBlock(MethodNode mth, ExceptionHandler handler) { BlockNode start = handler.getHandlerBlock(); BlockNode topSplitter = BlockUtils.getTopSplitterForHandler(start); List allHandlers = handler.getTryBlock().getHandlers(); List handlerExitsCandidate = new ArrayList<>(BlockUtils.bitSetToBlocks(mth, start.getDomFrontier())); BitSet visited = newBlocksBitSet(mth); while (!handlerExitsCandidate.isEmpty()) { BlockNode frontier = handlerExitsCandidate.remove(0); if (visited.get(frontier.getPos())) { continue; } visited.set(frontier.getPos()); // In some cases, handler's domFrontier is in the half of catch block // instead of the end, so we need to make sure frontier's predecessor // comes from try branch end: // 1. not from handler branch, doesn't exist path from handler to pred // 2. from try branch, exists path from topSplitter to pred // 3. skip method exit for (BlockNode pred : frontier.getPredecessors()) { boolean predFromHandler = allHandlers.stream().anyMatch(h -> isPathExists(h.getHandlerBlock(), pred)); if (!predFromHandler && BlockUtils.isPathExists(topSplitter, pred) && frontier != mth.getExitBlock()) { return frontier; } } // if not found, add this frontier's frontier to candidate list handlerExitsCandidate.addAll(BlockUtils.bitSetToBlocks(mth, frontier.getDomFrontier())); } return null; } @Nullable public static BlockNode getBlockWithFlag(List blocks, AFlag flag) { for (BlockNode block : blocks) { if (block.contains(flag)) { return block; } } return null; } public static @Nullable CatchAttr getCatchAttrForInsn(MethodNode mth, InsnNode insn) { CatchAttr catchAttr = insn.get(AType.EXC_CATCH); if (catchAttr != null) { return catchAttr; } BlockNode block = getBlockByInsn(mth, insn); if (block == null) { return null; } return block.get(AType.EXC_CATCH); } public static boolean isEqualPaths(BlockNode b1, BlockNode b2) { if (b1 == b2) { return true; } if (b1 == null || b2 == null) { return false; } return isEqualReturnBlocks(b1, b2) || isEmptySyntheticPath(b1, b2) || isDuplicateBlockPath(b1, b2); } private static boolean isEmptySyntheticPath(BlockNode b1, BlockNode b2) { BlockNode n1 = followEmptyPath(b1); BlockNode n2 = followEmptyPath(b2); return n1 == n2 || isEqualReturnBlocks(n1, n2); } public static boolean isEqualReturnBlocks(BlockNode b1, BlockNode b2) { if (!b1.isReturnBlock() || !b2.isReturnBlock()) { return false; } List b1Insns = b1.getInstructions(); List b2Insns = b2.getInstructions(); if (b1Insns.size() != 1 || b2Insns.size() != 1) { return false; } InsnNode i1 = b1Insns.get(0); InsnNode i2 = b2Insns.get(0); if (i1.getArgsCount() != i2.getArgsCount()) { return false; } if (i1.getArgsCount() == 0) { return true; } InsnArg firstArg = i1.getArg(0); InsnArg secondArg = i2.getArg(0); if (firstArg.isSameConst(secondArg)) { return true; } if (i1.getSourceLine() != i2.getSourceLine()) { return false; } return firstArg.equals(secondArg); } public static boolean isDuplicateBlockPath(BlockNode first, BlockNode second) { if (first.getSuccessors().size() == 1 && second.getSuccessors().size() == 1 && first.getSuccessors().get(0).equals(second.getSuccessors().get(0))) { return isSameInsnsBlocks(first, second); } return false; } public static boolean isSameInsnsBlocks(BlockNode first, BlockNode second) { List firstInsns = first.getInstructions(); List secondInsns = second.getInstructions(); if (firstInsns.size() != secondInsns.size()) { return false; } int len = firstInsns.size(); for (int i = 0; i < len; i++) { InsnNode firstInsn = firstInsns.get(i); InsnNode secondInsn = secondInsns.get(i); if (!isInsnDeepEquals(firstInsn, secondInsn)) { return false; } } return true; } private static boolean isInsnDeepEquals(InsnNode first, InsnNode second) { if (first == second) { return true; } return first.isSame(second) && Objects.equals(first.getArguments(), second.getArguments()) && resultIsSameReg(first.getResult(), second.getResult()); } private static boolean resultIsSameReg(RegisterArg first, RegisterArg second) { if (first == null || second == null) { return first == second; } return first.getRegNum() == second.getRegNum(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/CacheStorage.java ================================================ package jadx.core.utils; import java.util.Collections; import java.util.Set; public class CacheStorage { private Set rootPkgs = Collections.emptySet(); public Set getRootPkgs() { return rootPkgs; } public void setRootPkgs(Set rootPkgs) { this.rootPkgs = rootPkgs; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/DebugChecks.java ================================================ package jadx.core.utils; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Supplier; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.PhiListAttr; import jadx.core.dex.attributes.nodes.TmpEdgeAttr; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Check invariants and information consistency for blocks, instructions, registers, SSA variables. * These checks are very expensive and executed only in tests. */ public class DebugChecks { private static final Set IGNORE_CHECKS = new HashSet<>(List.of( "PrepareForCodeGen", "RenameVisitor", "DotGraphVisitor")); public static List insertPasses(List passes) { int size = passes.size(); List list = new ArrayList<>(size * 2); for (IDexTreeVisitor pass : passes) { list.add(pass); String name = pass.getName(); if (!IGNORE_CHECKS.contains(name)) { list.add(new DebugChecksPass(name)); } } return list; } public static void runChecksAfterVisitor(MethodNode mth, String visitor) { try { checkMethod(mth); } catch (Exception e) { mth.addError("Debug check failed after visitor: " + visitor, e); } } public static void checkMethod(MethodNode mth) { List basicBlocks = mth.getBasicBlocks(); if (Utils.isEmpty(basicBlocks)) { return; } for (BlockNode block : basicBlocks) { for (InsnNode insn : block.getInstructions()) { checkInsn(mth, block, insn); } } checkSSAVars(mth); quickCheckPhiInsn(mth); // checkPHI(mth); } private static void checkInsn(MethodNode mth, BlockNode block, InsnNode insn) { if (insn.getResult() != null) { checkVar(mth, insn, insn.getResult()); } for (InsnArg arg : insn.getArguments()) { if (arg instanceof RegisterArg) { checkVar(mth, insn, (RegisterArg) arg); } else if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); checkInsn(mth, block, wrapInsn); } } switch (insn.getType()) { case TERNARY: TernaryInsn ternaryInsn = (TernaryInsn) insn; for (RegisterArg arg : ternaryInsn.getCondition().getRegisterArgs()) { checkVar(mth, insn, arg); } break; case IF: IfNode ifNode = (IfNode) insn; if (!ifNode.getThenBlock().equals(ifNode.getElseBlock())) { // exclude temp edges int branches = (int) block.getSuccessors().stream().filter(b -> !hasTmpEdge(block, b)).count(); if (branches != 2) { DebugUtils.dumpRaw(mth, "error"); throw new JadxRuntimeException( "Incorrect if block successors count: " + branches + " (expect 2), block: " + block); } } checkBlock(mth, ifNode.getThenBlock(), () -> "then block in if insn: " + ifNode); checkBlock(mth, ifNode.getElseBlock(), () -> "else block in if insn: " + ifNode); break; } } private static boolean hasTmpEdge(BlockNode start, BlockNode end) { TmpEdgeAttr tmpEdgeAttr = end.get(AType.TMP_EDGE); if (tmpEdgeAttr == null) { return false; } return tmpEdgeAttr.getBlock().equals(start); } private static void checkBlock(MethodNode mth, BlockNode block, Supplier source) { if (!mth.getBasicBlocks().contains(block)) { throw new JadxRuntimeException("Block not registered in method: " + block + " from " + source.get()); } } private static void checkVar(MethodNode mth, InsnNode insn, RegisterArg reg) { checkRegisterArg(mth, reg); SSAVar sVar = reg.getSVar(); if (sVar == null) { if (reg.contains(AFlag.DONT_GENERATE) || insn.contains(AFlag.DONT_GENERATE)) { return; } if (Utils.notEmpty(mth.getSVars())) { throw new JadxRuntimeException("Null SSA var in " + reg + " at " + insn); } return; } if (Utils.indexInListByRef(mth.getSVars(), sVar) == -1) { throw new JadxRuntimeException("SSA var not present in method vars list, var: " + sVar + " from insn: " + insn); } RegisterArg resArg = insn.getResult(); List useList = sVar.getUseList(); if (resArg == reg) { if (sVar.getAssignInsn() != insn) { throw new JadxRuntimeException("Incorrect assign in ssa var: " + sVar + "\n expected: " + sVar.getAssignInsn() + "\n got: " + insn); } } else { if (!Utils.containsInListByRef(useList, reg)) { throw new JadxRuntimeException("Incorrect use list in ssa var: " + sVar + ", register not listed." + "\n insn: " + insn); } } for (RegisterArg useArg : useList) { checkRegisterArg(mth, useArg); } } private static void checkSSAVars(MethodNode mth) { for (SSAVar ssaVar : mth.getSVars()) { RegisterArg assignArg = ssaVar.getAssign(); if (assignArg.contains(AFlag.REMOVE)) { // ignore removed vars continue; } InsnNode assignInsn = assignArg.getParentInsn(); if (assignInsn != null) { if (insnMissing(mth, assignInsn)) { throw new JadxRuntimeException("Insn not found for assign arg in SSAVar: " + ssaVar + ", insn: " + assignInsn); } RegisterArg resArg = assignInsn.getResult(); if (resArg == null) { throw new JadxRuntimeException("SSA assign insn result missing. SSAVar: " + ssaVar + ", insn: " + assignInsn); } SSAVar assignVar = resArg.getSVar(); if (!assignVar.equals(ssaVar)) { throw new JadxRuntimeException("Unexpected SSAVar in assign. " + "Expected: " + ssaVar + ", got: " + assignVar + ", insn: " + assignInsn); } } for (RegisterArg arg : ssaVar.getUseList()) { InsnNode useInsn = arg.getParentInsn(); if (useInsn == null) { throw new JadxRuntimeException("Parent insn can't be null for arg in use list of SSAVar: " + ssaVar); } if (insnMissing(mth, useInsn)) { throw new JadxRuntimeException("Insn not found for use arg for SSAVar: " + ssaVar + ", insn: " + useInsn); } int argIndex = useInsn.getArgIndex(arg); if (argIndex == -1) { throw new JadxRuntimeException("Use arg not found in insn for SSAVar: " + ssaVar + ", insn: " + useInsn); } InsnArg foundArg = useInsn.getArg(argIndex); if (!foundArg.equals(arg)) { throw new JadxRuntimeException( "Incorrect use arg in insn for SSAVar: " + ssaVar + ", insn: " + useInsn + ", arg: " + foundArg); } } } } private static boolean insnMissing(MethodNode mth, InsnNode insn) { if (insn.contains(AFlag.HIDDEN)) { // skip search return false; } BlockNode block = BlockUtils.getBlockByInsn(mth, insn); return block == null; } private static void checkRegisterArg(MethodNode mth, RegisterArg reg) { InsnNode parentInsn = reg.getParentInsn(); if (parentInsn == null) { if (reg.contains(AFlag.METHOD_ARGUMENT)) { return; } throw new JadxRuntimeException("Null parentInsn for reg: " + reg); } if (!parentInsn.contains(AFlag.HIDDEN)) { if (parentInsn.getResult() != reg && !parentInsn.containsArg(reg)) { throw new JadxRuntimeException("Incorrect parentInsn: " + parentInsn + ", must contains arg: " + reg); } BlockNode parentInsnBlock = BlockUtils.getBlockByInsn(mth, parentInsn); if (parentInsnBlock == null) { throw new JadxRuntimeException("Parent insn not found in blocks tree for: " + reg + "\n insn: " + parentInsn); } } } public static void quickCheckPhiInsn(MethodNode mth) { if (mth.getSVars().isEmpty()) { return; } for (BlockNode block : mth.getBasicBlocks()) { PhiListAttr phiListAttr = block.get(AType.PHI_LIST); if (phiListAttr != null) { for (PhiInsn phiInsn : phiListAttr.getList()) { checkPhiArg(mth, phiInsn, phiInsn.getResult(), () -> "result"); int argsCount = phiInsn.getArgsCount(); for (int i = 0; i < argsCount; i++) { int argNum = i; checkPhiArg(mth, phiInsn, phiInsn.getArg(argNum), () -> "arg_" + argNum); } } } } } private static void checkPhiArg(MethodNode mth, PhiInsn phiInsn, RegisterArg arg, Supplier argName) { if (arg == null) { throw new JadxRuntimeException("Null " + argName.get() + " in PHI insn: " + phiInsn); } if (arg.getSVar() == null) { throw new JadxRuntimeException("Null SSA variable in " + argName.get() + " in PHI insn: " + phiInsn); } } private static void checkPHI(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { List phis = new ArrayList<>(); for (InsnNode insn : block.getInstructions()) { if (insn.getType() == InsnType.PHI) { PhiInsn phi = (PhiInsn) insn; phis.add(phi); if (phi.getArgsCount() == 0) { throw new JadxRuntimeException("No args and binds in PHI"); } for (InsnArg arg : insn.getArguments()) { if (arg instanceof RegisterArg) { BlockNode b = phi.getBlockByArg((RegisterArg) arg); if (b == null) { throw new JadxRuntimeException("Predecessor block not found"); } } else { throw new JadxRuntimeException("Not register in phi insn"); } } } } PhiListAttr phiListAttr = block.get(AType.PHI_LIST); if (phiListAttr == null) { if (!phis.isEmpty()) { throw new JadxRuntimeException("Missing PHI list attribute"); } } else { List phiList = phiListAttr.getList(); if (phiList.isEmpty()) { throw new JadxRuntimeException("Empty PHI list attribute"); } if (!phis.containsAll(phiList) || !phiList.containsAll(phis)) { throw new JadxRuntimeException("Instructions not match"); } } } for (SSAVar ssaVar : mth.getSVars()) { for (PhiInsn usedInPhi : ssaVar.getUsedInPhi()) { boolean found = false; for (RegisterArg useArg : ssaVar.getUseList()) { InsnNode parentInsn = useArg.getParentInsn(); if (parentInsn != null && parentInsn == usedInPhi) { found = true; break; } } if (!found) { throw new JadxRuntimeException("Used in phi incorrect"); } } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/DebugChecksPass.java ================================================ package jadx.core.utils; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.utils.exceptions.JadxException; public class DebugChecksPass extends AbstractVisitor { private final String visitorName; public DebugChecksPass(String visitorName) { this.visitorName = visitorName; } @Override public String getName() { return "Checks-for-" + visitorName; } @Override public void visit(MethodNode mth) throws JadxException { if (!mth.contains(AType.JADX_ERROR)) { try { DebugChecks.runChecksAfterVisitor(mth, visitorName); } catch (Exception e) { mth.addError("Check error", e); } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/DebugUtils.java ================================================ package jadx.core.utils; import java.io.File; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeWriter; import jadx.api.impl.SimpleCodeWriter; import jadx.core.codegen.ConditionGen; import jadx.core.codegen.InsnGen; import jadx.core.codegen.MethodGen; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.conditions.IfCondition; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.DotGraphVisitor; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.regions.DepthRegionTraversal; import jadx.core.dex.visitors.regions.TracedRegionVisitor; import jadx.core.utils.exceptions.CodegenException; import jadx.core.utils.exceptions.JadxException; /** * Use these methods only for debug purpose. * CheckStyle will reject usage of this class. */ public class DebugUtils { private static final Logger LOG = LoggerFactory.getLogger(DebugUtils.class); public static final Predicate TEST_MTH_FILTER = mth -> mth.getName().equals("test"); private DebugUtils() { } public static void dump(MethodNode mth) { dump(mth, "dump"); } public static void dumpRaw(MethodNode mth, String desc, Predicate dumpCondition) { if (dumpCondition.test(mth)) { dumpRaw(mth, desc); } } public static void dumpRawTest(MethodNode mth, String desc) { dumpRaw(mth, desc, TEST_MTH_FILTER); } public static void dumpRaw(MethodNode mth, String desc) { File out = new File("test-graph-" + desc + "-tmp"); DotGraphVisitor.dumpRaw().save(out, mth); } public static IDexTreeVisitor dumpRawVisitor(String desc) { return new AbstractVisitor() { @Override public void visit(MethodNode mth) throws JadxException { dumpRaw(mth, desc); } }; } public static IDexTreeVisitor dumpRawVisitor(String desc, Predicate filter) { return new AbstractVisitor() { @Override public void visit(MethodNode mth) { if (filter.test(mth)) { dumpRaw(mth, desc); } } }; } public static IDexTreeVisitor dumpRawTestVisitor(String desc) { return dumpRawVisitor(desc, TEST_MTH_FILTER); } public static void dump(MethodNode mth, String desc) { File out = new File("test-graph-" + desc + "-tmp"); DotGraphVisitor.dump().save(out, mth); DotGraphVisitor.dumpRaw().save(out, mth); DotGraphVisitor.dumpRegions().save(out, mth); } public static void printRegionsWithBlock(MethodNode mth, BlockNode block) { Set regions = new LinkedHashSet<>(); DepthRegionTraversal.traverse(mth, new TracedRegionVisitor() { @Override public void processBlockTraced(MethodNode mth, IBlock container, IRegion currentRegion) { if (block.equals(container)) { regions.add(currentRegion); } } }); LOG.debug(" Found block: {} in regions: {}", block, regions); } public static IDexTreeVisitor printRegionsVisitor() { return new AbstractVisitor() { @Override public void visit(MethodNode mth) throws JadxException { printRegions(mth, true); } }; } public static void printRegions(MethodNode mth) { printRegions(mth, false); } public static void printRegions(MethodNode mth, boolean printInsns) { Region mthRegion = mth.getRegion(); if (mthRegion == null) { return; } printRegion(mth, mthRegion, printInsns); } public static void printRegion(MethodNode mth, IRegion region, boolean printInsns) { ICodeWriter cw = new SimpleCodeWriter(); cw.startLine('|').add(mth.toString()); printRegion(mth, region, cw, "| ", printInsns); LOG.debug("{}{}", '\n', cw.finish().getCodeStr()); } private static void printRegion(MethodNode mth, IRegion region, ICodeWriter cw, String indent, boolean printInsns) { printWithAttributes(cw, indent, region.toString(), region); indent += "| "; printRegionSpecificInfo(cw, indent, mth, region, printInsns); for (IContainer container : region.getSubBlocks()) { if (container instanceof IRegion) { printRegion(mth, (IRegion) container, cw, indent, printInsns); } else { printWithAttributes(cw, indent, container.toString(), container); if (printInsns && container instanceof IBlock) { IBlock block = (IBlock) container; printInsns(mth, cw, indent, block); } } } } private static void printRegionSpecificInfo(ICodeWriter cw, String indent, MethodNode mth, IRegion region, boolean printInsns) { if (region instanceof LoopRegion) { LoopRegion loop = (LoopRegion) region; IfCondition condition = loop.getCondition(); if (printInsns && condition != null) { ConditionGen conditionGen = new ConditionGen(new InsnGen(MethodGen.getFallbackMethodGen(mth), true)); cw.startLine(indent).add("|> "); try { conditionGen.add(cw, condition); } catch (Exception e) { cw.startLine(indent).add(">!! ").add(condition.toString()); } } } } private static void printInsns(MethodNode mth, ICodeWriter cw, String indent, IBlock block) { for (InsnNode insn : block.getInstructions()) { try { MethodGen mg = MethodGen.getFallbackMethodGen(mth); InsnGen ig = new InsnGen(mg, true); ICodeWriter code = new SimpleCodeWriter(); ig.makeInsn(insn, code); String codeStr = code.getCodeStr(); List insnStrings = Stream.of(codeStr.split("\\R")) .filter(StringUtils::notBlank) .map(s -> "|> " + s) .collect(Collectors.toList()); Iterator it = insnStrings.iterator(); while (true) { String insnStr = it.next(); if (it.hasNext()) { cw.startLine(indent).add(insnStr); } else { printWithAttributes(cw, indent, insnStr, insn); break; } } } catch (CodegenException e) { cw.startLine(indent).add(">!! ").add(insn.toString()); } } } private static void printWithAttributes(ICodeWriter cw, String indent, String codeStr, IAttributeNode attrNode) { String str = attrNode.isAttrStorageEmpty() ? codeStr : codeStr + ' ' + attrNode.getAttributesString(); List attrStrings = Stream.of(str.split("\\R")) .filter(StringUtils::notBlank) .collect(Collectors.toList()); Iterator it = attrStrings.iterator(); if (!it.hasNext()) { return; } cw.startLine(indent).add(it.next()); while (it.hasNext()) { cw.startLine(indent).add("|+ ").add(it.next()); } } public static void printMap(Map map, String desc) { LOG.debug("Map {} (size = {}):", desc, map.size()); for (Map.Entry entry : map.entrySet()) { LOG.debug(" {}: {}", entry.getKey(), entry.getValue()); } } public static void printStackTrace(String label) { LOG.debug("StackTrace: {}\n{}", label, Utils.getFullStackTrace(new Exception())); } public static void printMethodOverrideTop(RootNode root) { LOG.debug("Methods override top 10:"); root.getClasses().stream() .flatMap(c -> c.getMethods().stream()) .filter(m -> m.contains(AType.METHOD_OVERRIDE)) .map(m -> m.get(AType.METHOD_OVERRIDE)) .filter(o -> !o.getOverrideList().isEmpty()) .filter(distinctByKey(methodOverrideAttr -> methodOverrideAttr.getRelatedMthNodes().size())) .filter(distinctByKey(MethodOverrideAttr::getRelatedMthNodes)) .sorted(Comparator.comparingInt(o -> -o.getRelatedMthNodes().size())) .limit(10) .forEach(o -> LOG.debug(" {} : {}", o.getRelatedMthNodes().size(), Utils.last(o.getOverrideList()))); } private static Predicate distinctByKey(Function keyExtractor) { Set seen = ConcurrentHashMap.newKeySet(); return t -> seen.add(keyExtractor.apply(t)); } private static Map execTimes; public static void initExecTimes() { execTimes = new ConcurrentHashMap<>(); } public static void mergeExecTimeFromStart(String tag, long startTimeMillis) { mergeExecTime(tag, System.currentTimeMillis() - startTimeMillis); } public static void mergeExecTime(String tag, long execTimeMillis) { execTimes.merge(tag, execTimeMillis, Long::sum); } public static void printExecTimes() { System.out.println("Exec times:"); execTimes.forEach((tag, time) -> System.out.println(" " + tag + ": " + time + "ms")); } public static void printExecTimesWithTotal(long totalMillis) { System.out.println("Exec times: total " + totalMillis + "ms"); execTimes.forEach((tag, time) -> System.out.println(" " + tag + ": " + time + "ms" + String.format(" (%.2f%%)", time * 100. / (double) totalMillis))); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java ================================================ package jadx.core.utils; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.IDecompileScheduler; import jadx.api.JavaClass; import jadx.core.utils.exceptions.JadxRuntimeException; public class DecompilerScheduler implements IDecompileScheduler { private static final Logger LOG = LoggerFactory.getLogger(DecompilerScheduler.class); private static final int MERGED_BATCH_SIZE = 16; private static final boolean DEBUG_BATCHES = false; @Override public List> buildBatches(List classes) { try { long start = System.currentTimeMillis(); List> result = internalBatches(classes); if (LOG.isDebugEnabled()) { LOG.debug("Build decompilation batches in {}ms for {} classes", System.currentTimeMillis() - start, classes.size()); } if (DEBUG_BATCHES) { check(result, classes); } return result; } catch (StackOverflowError | BootstrapMethodError e) { LOG.warn("Stack overflow while building decompile batches, continue with fallback"); } catch (Exception e) { LOG.warn("Build batches failed (continue with fallback)", e); } return buildFallback(classes); } /** * Put classes with many dependencies at the end. * Build batches for dependencies of single class to avoid locking from another thread. */ public List> internalBatches(List classes) { List deps = sumDependencies(classes); Set added = new HashSet<>(classes.size()); Comparator cmpDepSize = Comparator.comparingInt(JavaClass::getTotalDepsCount); List> result = new ArrayList<>(); List mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE); for (DepInfo depInfo : deps) { JavaClass cls = depInfo.getCls(); if (!added.add(cls)) { continue; } int depsSize = cls.getTotalDepsCount(); if (depsSize == 0) { // add classes without dependencies in merged batch mergedBatch.add(cls); if (mergedBatch.size() >= MERGED_BATCH_SIZE) { result.add(mergedBatch); mergedBatch = new ArrayList<>(MERGED_BATCH_SIZE); } } else { List batch = new ArrayList<>(); for (JavaClass dep : cls.getDependencies()) { JavaClass topDep = dep.getTopParentClass(); if (!added.contains(topDep)) { batch.add(topDep); added.add(topDep); } } batch.sort(cmpDepSize); batch.add(cls); result.add(Utils.lockList(batch)); } } if (!mergedBatch.isEmpty()) { result.add(mergedBatch); } if (DEBUG_BATCHES) { dumpBatchesStats(classes, result, deps); } return result; } private static List sumDependencies(List classes) { List deps = new ArrayList<>(classes.size()); for (JavaClass cls : classes) { int count = 0; for (JavaClass dep : cls.getDependencies()) { count += 1 + dep.getTotalDepsCount(); } deps.add(new DepInfo(cls, count)); } Collections.sort(deps); return deps; } private static final class DepInfo implements Comparable { private final JavaClass cls; private final int depsCount; private DepInfo(JavaClass cls, int depsCount) { this.cls = cls; this.depsCount = depsCount; } public JavaClass getCls() { return cls; } public int getDepsCount() { return depsCount; } @Override public int compareTo(@NotNull DecompilerScheduler.DepInfo o) { int deps = Integer.compare(depsCount, o.depsCount); if (deps == 0) { return cls.getClassNode().compareTo(o.cls.getClassNode()); } return deps; } @Override public String toString() { return cls + ":" + depsCount; } } private static List> buildFallback(List classes) { return classes.stream() .sorted(Comparator.comparingInt(c -> c.getClassNode().getTotalDepsCount())) .map(Collections::singletonList) .collect(Collectors.toList()); } private void dumpBatchesStats(List classes, List> result, List deps) { int clsInBatches = result.stream().mapToInt(List::size).sum(); double avg = result.stream().mapToInt(List::size).average().orElse(-1); int maxSingleDeps = classes.stream().mapToInt(JavaClass::getTotalDepsCount).max().orElse(-1); int maxSubDeps = deps.stream().mapToInt(DepInfo::getDepsCount).max().orElse(-1); LOG.info("Batches stats:" + "\n input classes: " + classes.size() + ",\n classes in batches: " + clsInBatches + ",\n batches: " + result.size() + ",\n average batch size: " + String.format("%.2f", avg) + ",\n max single deps count: " + maxSingleDeps + ",\n max sub deps count: " + maxSubDeps); } private static void check(List> result, List classes) { int classInBatches = result.stream().mapToInt(List::size).sum(); if (classes.size() != classInBatches) { throw new JadxRuntimeException( "Incorrect number of classes in result batch: " + classInBatches + ", expected: " + classes.size()); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java ================================================ package jadx.core.utils; import java.io.File; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; import java.util.stream.Collectors; import jadx.api.ICodeWriter; import jadx.api.JavaMethod; import jadx.api.impl.SimpleCodeWriter; import jadx.api.plugins.input.data.IMethodRef; import jadx.core.codegen.MethodGen; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.SwitchRegion; import jadx.core.dex.regions.SynchronizedRegion; import jadx.core.dex.regions.TryCatchRegion; import jadx.core.dex.regions.conditions.IfRegion; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.visitors.SaveCode; import jadx.core.utils.files.FileUtils; import static jadx.core.codegen.MethodGen.FallbackOption.BLOCK_DUMP; public class DotGraphUtils { private static final String NL = "\\l"; private static final String NLQR = Matcher.quoteReplacement(NL); private static final boolean PRINT_DOMINATORS = false; private static final boolean PRINT_DOMINATORS_INFO = false; private static final int MAX_REGION_NAME_LENGTH = 2000; private final ICodeWriter dot = new SimpleCodeWriter(); private final ICodeWriter conn = new SimpleCodeWriter(); private final boolean useRegions; private final boolean rawInsn; // if present, this region and it's children will still be drawn when not in regions mode. private Optional highlightRegion; // flag set when the highlighted region has been processed once, to avoid processing it's children // more than once private boolean processedHighlightRegion = false; public DotGraphUtils(boolean useRegions, boolean rawInsn) { this(useRegions, rawInsn, Optional.empty()); } public DotGraphUtils(boolean useRegions, boolean rawInsn, Optional highlightRegion) { this.useRegions = useRegions; this.rawInsn = rawInsn; this.highlightRegion = highlightRegion; } // The default out directory for the method public static File getOutDir(MethodNode mth) { return mth.root().getArgs().getOutDir(); } // The filename the method cfg would be stored in under the default out directory public File getFullFile(MethodNode mth) { return getFullFile(mth, getOutDir(mth)); } // The filename the method cfg would be stored in under the given out directory public File getFullFile(MethodNode mth, File outDir) { String fileName = StringUtils.escape(mth.getMethodInfo().getShortId()) + (useRegions ? ".regions" : "") + (rawInsn ? ".raw" : "") + ".dot"; File file = outDir.toPath() .resolve(mth.getParentClass().getClassInfo().getAliasFullPath() + "_graphs") .resolve(fileName) .toFile(); file = FileUtils.cutFileName(file); return file; } public void dumpToFile(MethodNode mth) { File dir = getOutDir(mth); dumpToFile(mth, dir); } public void dumpToFile(MethodNode mth, File dir) { String graph = dumpToString(mth); if (graph == null) { return; } File file = getFullFile(mth, dir); SaveCode.save(graph, file); } public String dumpToString(MethodNode mth) { dot.startLine("digraph \"CFG for"); dot.add(escape(mth.getMethodInfo().getFullId())); dot.add("\" {"); BlockNode enterBlock = mth.getEnterBlock(); if (useRegions) { if (mth.getRegion() == null) { return null; } processMethodRegion(mth); } else { List blocks = mth.getBasicBlocks(); if (blocks == null) { InsnNode[] insnArr = mth.getInstructions(); if (insnArr == null) { return null; } BlockNode block = new BlockNode(0, 0, 0); List insnList = block.getInstructions(); for (InsnNode insn : insnArr) { if (insn != null) { insnList.add(insn); } } enterBlock = block; blocks = Collections.singletonList(block); } for (BlockNode block : blocks) { if (processedHighlightRegion && highlightRegion.isPresent() && RegionUtils.isRegionContainsBlock(highlightRegion.get(), block)) { // Don't process blocks in the highlight region if it's already been processed, since processing the // region will already process all it's containing blocks. continue; } processBlock(mth, block); } } dot.startLine("MethodNode[shape=record,label=\"{"); dot.add(escape(mth.getAccessFlags().makeString(true))); dot.add(escape(mth.getReturnType() + " " + mth.getParentClass() + '.' + mth.getName() + '(' + Utils.listToString(mth.getAllArgRegs()) + ") ")); String attrs = attributesString(mth); if (!attrs.isEmpty()) { dot.add(" | ").add(attrs); } dot.add("}\"];"); dot.startLine("MethodNode -> ").add(makeName(enterBlock)).add(';'); dot.add(conn.toString()); dot.startLine('}'); dot.startLine(); return dot.finish().getCodeStr(); } private void processMethodRegion(MethodNode mth) { Set regionsBlocks = new HashSet<>(mth.getBasicBlocks().size()); RegionUtils.getAllRegionBlocks(mth.getRegion(), regionsBlocks); for (ExceptionHandler handler : mth.getExceptionHandlers()) { IContainer handlerRegion = handler.getHandlerRegion(); if (handlerRegion != null) { RegionUtils.getAllRegionBlocks(handlerRegion, regionsBlocks); } } processRegion(mth, mth.getRegion(), regionsBlocks); for (ExceptionHandler h : mth.getExceptionHandlers()) { if (h.getHandlerRegion() != null) { processRegion(mth, h.getHandlerRegion(), regionsBlocks); } } for (BlockNode block : mth.getBasicBlocks()) { if (!regionsBlocks.contains(block)) { processBlock(mth, block, true, false); } } } private void processRegion(MethodNode mth, IContainer region, Set regionsBlocks) { if (region instanceof IRegion) { IRegion r = (IRegion) region; dot.startLine("subgraph " + makeName(region) + " {"); dot.startLine("color = " + getColorForRegion(r)); dot.startLine("label = \"").add(truncateRegionName(r)); dot.add("\";"); dot.startLine("node [shape=record,color=blue];"); for (IContainer c : r.getSubBlocks()) { processRegion(mth, c, regionsBlocks); } dot.startLine('}'); } else if (region instanceof BlockNode) { checkAndFixFloatingBlocks(mth, (BlockNode) region, regionsBlocks); processBlock(mth, (BlockNode) region); } else if (region instanceof IBlock) { processIBlock(mth, (IBlock) region); } } private String getColorForRegion(IRegion region) { if (region instanceof IfRegion) { return "lightgoldenrod3"; } else if (region instanceof LoopRegion) { return "lightpink2"; } else if (region instanceof SwitchRegion) { return "lightsteelblue3"; } else if (region instanceof SynchronizedRegion) { return "mediumpurple3"; } else if (region instanceof TryCatchRegion) { return "olivedrab4"; } else if (region.contains(AType.EXC_HANDLER)) { return "orangered4"; } return "gray"; } private String truncateRegionName(IRegion r) { String regionName = r.toString(); String attrs = attributesString(r); if (!attrs.isEmpty()) { regionName += " | " + attrs; } if (regionName.length() > MAX_REGION_NAME_LENGTH) { regionName = regionName.substring(0, MAX_REGION_NAME_LENGTH); regionName += "..."; } return regionName; } /** * A block is floating if it exists in no regions at all. These are placed in a region that makes * sense for generation of this graph only, because otherwise the generated graph is unreadable. */ private void checkAndFixFloatingBlocks(MethodNode mth, BlockNode block, Set regionBlocks) { if (regionBlocks == null || regionBlocks.isEmpty()) { return; } // Heuristic: place the floating block in the same region as either it's predecessor or successor, // depending on which it has less of. This results in a more readable graph as a block with a single // predecessor will be placed near it. for (BlockNode floating : block.getSuccessors()) { if (!regionBlocks.contains(floating) && floating.getPredecessors().size() <= floating.getSuccessors().size()) { // Set true on the pseudoInRegion to draw the block with a dotted outline and apply a marker to it // to notify that it isn't actually in this region. processBlock(mth, floating, true, true); regionBlocks.add(floating); } } for (BlockNode floating : block.getPredecessors()) { if (!regionBlocks.contains(floating) && floating.getPredecessors().size() > floating.getSuccessors().size()) { processBlock(mth, floating, true, true); regionBlocks.add(floating); } } } private void processBlock(MethodNode mth, BlockNode block) { processBlock(mth, block, false, false); } private void processBlock(MethodNode mth, BlockNode block, boolean error, boolean pseudoInRegion) { if (!processedHighlightRegion && highlightRegion.isPresent() && RegionUtils.isRegionContainsBlock(highlightRegion.get(), block)) { processedHighlightRegion = true; processRegion(mth, highlightRegion.get(), null); return; } boolean isMthStart = block.contains(AFlag.MTH_ENTER_BLOCK); boolean isMthEnd = block.contains(AFlag.MTH_EXIT_BLOCK); if (isMthEnd) { dot.startLine("subgraph { rank = sink; "); } dot.startLine(makeName(block)); dot.add(" [shape=record,"); if (error) { dot.add("color=red,"); } if (pseudoInRegion) { dot.add("style = \"filled,dashed\""); } else { dot.add("style = filled,"); } if (isMthStart || isMthEnd) { dot.add("fillcolor = \"#def3fd\","); } else { dot.add("fillcolor = \"#f8fafb\","); } dot.add("label=\"{"); dot.add(String.valueOf(block.getCId())).add("\\:\\ "); dot.add(InsnUtils.formatOffset(block.getStartOffset())); if (pseudoInRegion) { dot.add("\\nNOT IN ANY REGION"); } String attrs = attributesString(block); if (!attrs.isEmpty()) { dot.add('|').add(attrs); } if (PRINT_DOMINATORS_INFO) { dot.add('|'); dot.startLine("doms: ").add(escape(block.getDoms())); dot.startLine("\\lidom: ").add(escape(block.getIDom())); dot.startLine("\\lpost-doms: ").add(escape(block.getPostDoms())); dot.startLine("\\lpost-idom: ").add(escape(block.getIPostDom())); dot.startLine("\\ldom-f: ").add(escape(block.getDomFrontier())); dot.startLine("\\ldoms-on: ").add(escape(Utils.listToString(block.getDominatesOn()))); dot.startLine("\\l"); } String insns = insertInsns(mth, block); if (!insns.isEmpty()) { dot.add('|').add(insns); } dot.add("}\"];"); if (isMthEnd) { dot.add("};"); } BlockNode falsePath = null; InsnNode lastInsn = BlockUtils.getLastInsn(block); if (lastInsn != null && lastInsn.getType() == InsnType.IF) { falsePath = ((IfNode) lastInsn).getElseBlock(); } for (BlockNode next : block.getSuccessors()) { String style = next == falsePath ? "[style=dashed]" : ""; addEdge(block, next, style); } if (PRINT_DOMINATORS) { for (BlockNode c : block.getDominatesOn()) { conn.startLine(block.getCId() + " -> " + c.getCId() + "[color=green];"); } for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) { conn.startLine("f_" + block.getCId() + " -> f_" + dom.getCId() + "[color=blue];"); } } } private void processIBlock(MethodNode mth, IBlock block) { processIBlock(mth, block, false); } private void processIBlock(MethodNode mth, IBlock block, boolean error) { String attrs = attributesString(block); dot.startLine(makeName(block)); dot.add(" [shape=record,"); if (error) { dot.add("color=red,"); } dot.add("label=\"{"); if (!attrs.isEmpty()) { dot.add(attrs); } String insns = insertInsns(mth, block); if (!insns.isEmpty()) { dot.add('|').add(insns); } dot.add("}\"];"); } private void addEdge(BlockNode from, BlockNode to, String style) { conn.startLine(makeName(from)).add(" -> ").add(makeName(to)); conn.add(style); conn.add(';'); } private String attributesString(IAttributeNode block) { StringBuilder attrs = new StringBuilder(); for (String attr : block.getAttributesStringsList()) { attrs.append(escape(attr)).append(NL); } return attrs.toString(); } private String makeName(IContainer c) { String name; if (c instanceof BlockNode) { name = "Node_" + ((BlockNode) c).getCId(); } else if (c instanceof IBlock) { name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode(); } else { name = "cluster_" + c.getClass().getSimpleName() + '_' + c.hashCode(); } return name; } private String insertInsns(MethodNode mth, IBlock block) { if (rawInsn) { StringBuilder sb = new StringBuilder(); for (InsnNode insn : block.getInstructions()) { sb.append(escape(insn)).append(NL); } return sb.toString(); } else { ICodeWriter code = new SimpleCodeWriter(); List instructions = block.getInstructions(); MethodGen.addFallbackInsns(code, mth, instructions.toArray(new InsnNode[0]), BLOCK_DUMP); // For some reason, instructions here get put through an additional step of unescaping String str = escape(code.newLine().toString()); if (str.startsWith(NL)) { str = str.substring(NL.length()); } return str; } } private String escape(Object obj) { if (obj == null) { return "null"; } return escape(obj.toString()); } private String escape(String string) { return escape(string, NLQR); } private String escape(String string, String newline) { return string .replace("\\", "") // TODO replace \" .replace("/", "\\/") .replace(">", "\\>").replace("<", "\\<") .replace("{", "\\{").replace("}", "\\}") .replace("\"", "\\\"") .replace("-", "\\-") .replace("|", "\\|") .replaceAll("\\R", newline); } // Consistently format names for graphs public static String classFormatName(ClassNode cls, boolean longName) { return classFormatName(cls.getClassInfo(), longName); } public static String classFormatName(ClassInfo cls, boolean longName) { return longName ? cls.getAliasFullName() : cls.getAliasShortName(); } public static String methodFormatName(JavaMethod javaMethod, boolean longName) { return methodFormatName(javaMethod.getMethodNode(), longName); } public static String methodFormatName(MethodNode methodNode, boolean longName) { if (longName) { ClassNode parentClass = methodNode.getParentClass(); List argTypes = methodNode.getArgTypes(); ArgType retType = methodNode.getReturnType(); return classFormatName(parentClass, true) + "." + methodFormatName(methodNode, false) + '(' + Utils.listToString(argTypes, ", ", e -> argTypeFormatName(e, parentClass, true)) + "):" + argTypeFormatName(retType, parentClass, true); } return methodNode.getAlias(); } public static String unresolvedMethodFormatName(IMethodRef methodRef, boolean longName) { String name = methodRef.getName(); if (longName) { String className = methodRef.getParentClassType(); className = Utils.cleanObjectName(className); String returnName = methodRef.getReturnType(); returnName = Utils.smaliNameToJavaName(returnName); List argTypes = methodRef.getArgTypes(); argTypes = argTypes.stream().map(c -> Utils.smaliNameToJavaName(c)).collect(Collectors.toList()); return String.format("%s.%s(%s):%s", className, name, Utils.listToString(argTypes), returnName); } return name; } public static String interfaceFormatName(ArgType iface, ClassNode cls, boolean longName) { ClassInfo ifaceInfo = ClassInfo.fromType(cls.root(), iface); return longName ? ifaceInfo.getAliasFullName() : ifaceInfo.getAliasShortName(); } public static String argTypeFormatName(ArgType arg, ClassNode cls, boolean longName) { if (arg.isObject() && !arg.isGenericType()) { ClassNode superCls = cls.root().resolveClass(arg); if (superCls != null) { return DotGraphUtils.classFormatName(superCls, longName); } } return arg.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/EmptyBitSet.java ================================================ package jadx.core.utils; import java.util.BitSet; public final class EmptyBitSet extends BitSet { private static final long serialVersionUID = -1194884945157778639L; public static final BitSet EMPTY = new EmptyBitSet(); public EmptyBitSet() { super(0); } @Override public int cardinality() { return 0; } @Override public boolean isEmpty() { return true; } @Override public int nextSetBit(int fromIndex) { return -1; } @Override public int length() { return 0; } @Override public int size() { return 0; } @Override public void set(int bitIndex) { throw new UnsupportedOperationException(); } @Override public void set(int bitIndex, boolean value) { throw new UnsupportedOperationException(); } @Override public void set(int fromIndex, int toIndex) { throw new UnsupportedOperationException(); } @Override public void set(int fromIndex, int toIndex, boolean value) { throw new UnsupportedOperationException(); } @Override public boolean get(int bitIndex) { return false; } @Override public BitSet get(int fromIndex, int toIndex) { return EMPTY; } @Override public void and(BitSet set) { throw new UnsupportedOperationException(); } @Override public void or(BitSet set) { throw new UnsupportedOperationException(); } @Override public void xor(BitSet set) { throw new UnsupportedOperationException(); } @Override public void andNot(BitSet set) { throw new UnsupportedOperationException(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/EncodedValueUtils.java ================================================ package jadx.core.utils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.MethodHandleType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.ConstClassNode; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxRuntimeException; public class EncodedValueUtils { /** * Return constant literal from {@code jadx.api.plugins.input.data.annotations.EncodedValue} * * @return LiteralArg, String, ArgType or null */ @Nullable public static Object convertToConstValue(EncodedValue encodedValue) { if (encodedValue == null) { return null; } Object value = encodedValue.getValue(); switch (encodedValue.getType()) { case ENCODED_NULL: return InsnArg.lit(0, ArgType.OBJECT); case ENCODED_BOOLEAN: return Boolean.TRUE.equals(value) ? LiteralArg.litTrue() : LiteralArg.litFalse(); case ENCODED_BYTE: return InsnArg.lit((Byte) value, ArgType.BYTE); case ENCODED_SHORT: return InsnArg.lit((Short) value, ArgType.SHORT); case ENCODED_CHAR: return InsnArg.lit((Character) value, ArgType.CHAR); case ENCODED_INT: return InsnArg.lit((Integer) value, ArgType.INT); case ENCODED_LONG: return InsnArg.lit((Long) value, ArgType.LONG); case ENCODED_FLOAT: return InsnArg.lit(Float.floatToIntBits((Float) value), ArgType.FLOAT); case ENCODED_DOUBLE: return InsnArg.lit(Double.doubleToLongBits((Double) value), ArgType.DOUBLE); case ENCODED_STRING: // noinspection RedundantCast return (String) value; case ENCODED_TYPE: return ArgType.parse((String) value); default: return null; } } public static InsnArg convertToInsnArg(RootNode root, EncodedValue value) { Object obj = value.getValue(); switch (value.getType()) { case ENCODED_NULL: case ENCODED_BYTE: case ENCODED_SHORT: case ENCODED_CHAR: case ENCODED_INT: case ENCODED_LONG: case ENCODED_FLOAT: case ENCODED_DOUBLE: return (InsnArg) convertToConstValue(value); case ENCODED_BOOLEAN: return InsnArg.lit(((Boolean) obj) ? 0 : 1, ArgType.BOOLEAN); case ENCODED_STRING: return InsnArg.wrapArg(new ConstStringNode((String) obj)); case ENCODED_TYPE: return InsnArg.wrapArg(new ConstClassNode(ArgType.parse((String) obj))); case ENCODED_METHOD_TYPE: return InsnArg.wrapArg(buildMethodType(root, (IMethodProto) obj)); case ENCODED_METHOD_HANDLE: return InsnArg.wrapArg(buildMethodHandle(root, (IMethodHandle) obj)); } throw new JadxRuntimeException("Unsupported type for raw invoke-custom: " + value.getType()); } private static InvokeNode buildMethodType(RootNode root, IMethodProto methodProto) { ArgType retType = ArgType.parse(methodProto.getReturnType()); List argTypes = Utils.collectionMap(methodProto.getArgTypes(), ArgType::parse); List callTypes = new ArrayList<>(1 + argTypes.size()); callTypes.add(retType); callTypes.addAll(argTypes); ArgType mthType = ArgType.object("java.lang.invoke.MethodType"); ClassInfo cls = ClassInfo.fromType(root, mthType); MethodInfo mth = MethodInfo.fromDetails(root, cls, "methodType", callTypes, mthType); InvokeNode invoke = new InvokeNode(mth, InvokeType.STATIC, callTypes.size()); for (ArgType type : callTypes) { InsnNode argInsn; if (type.isPrimitive()) { argInsn = new IndexInsnNode(InsnType.SGET, getTypeField(root, type.getPrimitiveType()), 0); } else { argInsn = new ConstClassNode(type); } invoke.addArg(InsnArg.wrapArg(argInsn)); } return invoke; } public static FieldInfo getTypeField(RootNode root, PrimitiveType type) { ArgType boxType = type.getBoxType(); ClassInfo boxCls = ClassInfo.fromType(root, boxType); return FieldInfo.from(root, boxCls, "TYPE", boxType); } /** * Build `MethodHandles.lookup().find{type}(methodCls, methodName, methodType)` */ private static InsnNode buildMethodHandle(RootNode root, IMethodHandle methodHandle) { if (methodHandle.getType().isField()) { // TODO: lookup for field return new ConstStringNode("FIELD:" + methodHandle.getFieldRef()); } IMethodRef methodRef = methodHandle.getMethodRef(); methodRef.load(); ClassInfo lookupCls = ClassInfo.fromName(root, "java.lang.invoke.MethodHandles.Lookup"); MethodInfo findMethod = MethodInfo.fromDetails(root, lookupCls, getFindMethodName(methodHandle.getType()), Arrays.asList(ArgType.CLASS, ArgType.STRING, ArgType.object("java.lang.invoke.MethodType")), ArgType.object("java.lang.invoke.MethodHandle")); InvokeNode invoke = new InvokeNode(findMethod, InvokeType.DIRECT, 4); invoke.addArg(buildLookupArg(root)); invoke.addArg(InsnArg.wrapArg(new ConstClassNode(ArgType.object(methodRef.getParentClassType())))); invoke.addArg(InsnArg.wrapArg(new ConstStringNode(methodRef.getName()))); invoke.addArg(InsnArg.wrapArg(buildMethodType(root, methodRef))); return invoke; } public static InsnArg buildLookupArg(RootNode root) { ArgType lookupType = ArgType.object("java.lang.invoke.MethodHandles.Lookup"); ClassInfo cls = ClassInfo.fromName(root, "java.lang.invoke.MethodHandles"); MethodInfo mth = MethodInfo.fromDetails(root, cls, "lookup", Collections.emptyList(), lookupType); return InsnArg.wrapArg(new InvokeNode(mth, InvokeType.STATIC, 0)); } private static String getFindMethodName(MethodHandleType type) { switch (type) { case INVOKE_STATIC: return "findStatic"; case INVOKE_CONSTRUCTOR: return "findConstructor"; case INVOKE_INSTANCE: case INVOKE_DIRECT: case INVOKE_INTERFACE: return "findVirtual"; default: return "<" + type + '>'; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/ErrorsCounter.java ================================================ package jadx.core.utils; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.nodes.IDexNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.exceptions.JadxOverflowException; public class ErrorsCounter { private static final Logger LOG = LoggerFactory.getLogger(ErrorsCounter.class); private static final boolean PRINT_MTH_SIZE = Consts.DEBUG; private final Set errorNodes = new HashSet<>(); private int errorsCount; private final Set warnNodes = new HashSet<>(); private int warnsCount; public static String error(N node, String warnMsg, Throwable th) { return node.root().getErrorsCounter().addError(node, warnMsg, th); } public static void warning(N node, String warnMsg) { node.root().getErrorsCounter().addWarning(node, warnMsg); } public static String formatMsg(IDexNode node, String msg) { return msg + " in " + node.typeName() + ": " + node + ", file: " + node.getInputFileName(); } private synchronized String addError(N node, String error, @Nullable Throwable e) { errorNodes.add(node); errorsCount++; String msg = formatMsg(node, error); if (PRINT_MTH_SIZE && node instanceof MethodNode) { String mthSize = "[" + ((MethodNode) node).getInsnsCount() + "] "; msg = mthSize + msg; error = mthSize + error; } if (e == null) { LOG.error(msg); } else if (e instanceof StackOverflowError) { LOG.error("{}, error: StackOverflowError", msg); } else if (e instanceof JadxOverflowException) { // don't print full stack trace String details = e.getMessage(); e = new JadxOverflowException(details); if (details == null || details.isEmpty()) { LOG.error("{}", msg); } else { LOG.error("{}, details: {}", msg, details); } } else { LOG.error(msg, e); } node.addAttr(AType.JADX_ERROR, new JadxError(error, e)); return msg; } private synchronized void addWarning(N node, String warn) { warnNodes.add(node); warnsCount++; LOG.warn(formatMsg(node, warn)); } public void printReport() { if (getErrorCount() > 0) { LOG.error("{} errors occurred in following nodes:", getErrorCount()); List errors = new ArrayList<>(errorNodes.size()); for (IAttributeNode node : errorNodes) { String nodeName = node.getClass().getSimpleName().replace("Node", ""); errors.add(nodeName + ": " + node); } Collections.sort(errors); for (String err : errors) { LOG.error(" {}", err); } } if (getWarnsCount() > 0) { LOG.warn("{} warnings in {} nodes", getWarnsCount(), warnNodes.size()); } } public int getErrorCount() { return errorsCount; } public int getWarnsCount() { return warnsCount; } public Set getErrorNodes() { return errorNodes; } public Set getWarnNodes() { return warnNodes; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/FileSignature.java ================================================ package jadx.core.utils; public class FileSignature { private final byte[] signatureBytes; private final String fileType; public FileSignature(String fileType, String signatureHex) { this.fileType = fileType; String[] parts = signatureHex.split(" "); this.signatureBytes = new byte[parts.length]; for (int i = 0; i < parts.length; i++) { if (parts[i].length() != 2) { throw new RuntimeException(signatureHex); } if (!parts[i].equals("??")) { this.signatureBytes[i] = (byte) Integer.parseInt(parts[i], 16); } } } public static boolean matches(FileSignature sig, byte[] data) { if (data.length < sig.signatureBytes.length) { return false; } for (int i = 0; i < sig.signatureBytes.length; i++) { byte b = sig.signatureBytes[i]; if (b != data[i]) { return false; } } return true; } public String getFileType() { return fileType; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/GsonUtils.java ================================================ package jadx.core.utils; import java.lang.reflect.Type; 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.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.Strictness; public class GsonUtils { public static Gson buildGson() { return defaultGsonBuilder().create(); } public static GsonBuilder defaultGsonBuilder() { return new GsonBuilder() .disableJdkUnsafe() .disableInnerClassSerialization() .setStrictness(Strictness.STRICT) .setPrettyPrinting(); } public static InterfaceReplace interfaceReplace(Class replaceCls) { return new InterfaceReplace<>(replaceCls); } public static final class InterfaceReplace implements JsonSerializer, JsonDeserializer { private final Class replaceCls; private InterfaceReplace(Class replaceCls) { this.replaceCls = replaceCls; } @Override public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { return context.deserialize(json, this.replaceCls); } @Override public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) { return context.serialize(src, this.replaceCls); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/ImmutableList.java ================================================ package jadx.core.utils; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.NoSuchElementException; import java.util.Objects; import java.util.RandomAccess; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.UnaryOperator; import org.jetbrains.annotations.NotNull; /** * Simple immutable list implementation * Warning: some methods not implemented! */ public final class ImmutableList implements List, RandomAccess { private final E[] arr; @SuppressWarnings({ "unchecked", "SuspiciousArrayCast" }) public ImmutableList(Collection col) { this((E[]) Objects.requireNonNull(col).toArray()); } public ImmutableList(E[] arr) { this.arr = Objects.requireNonNull(arr); } @Override public int size() { return arr.length; } @Override public boolean isEmpty() { return arr.length == 0; } @Override public E get(int index) { return arr[index]; } @Override public int indexOf(Object o) { int len = arr.length; for (int i = 0; i < len; i++) { E e = arr[i]; if (Objects.equals(e, o)) { return i; } } return -1; } @Override public int lastIndexOf(Object o) { for (int i = arr.length - 1; i > 0; i--) { E e = arr[i]; if (Objects.equals(e, o)) { return i; } } return -1; } @Override public boolean contains(Object o) { return indexOf(o) != -1; } @Override public boolean containsAll(@NotNull Collection c) { for (Object obj : c) { if (!contains(obj)) { return false; } } return true; } @NotNull @Override public Iterator iterator() { return new Iterator() { private final int len = arr.length; private int index = 0; @Override public boolean hasNext() { return index < len; } @Override public E next() { try { return arr[index++]; } catch (IndexOutOfBoundsException e) { throw new NoSuchElementException(e.getMessage()); } } }; } @Override public void forEach(Consumer action) { for (E e : arr) { action.accept(e); } } @NotNull @Override public Object[] toArray() { return Arrays.copyOf(arr, arr.length); } @NotNull @Override @SuppressWarnings("unchecked") public T[] toArray(@NotNull T[] a) { return (T[]) Arrays.copyOf(arr, arr.length); } @Override public boolean add(E e) { throw new UnsupportedOperationException(); } @Override public boolean remove(Object o) { throw new UnsupportedOperationException(); } @Override public boolean addAll(@NotNull Collection c) { throw new UnsupportedOperationException(); } @Override public boolean addAll(int index, @NotNull Collection c) { throw new UnsupportedOperationException(); } @Override public boolean removeAll(@NotNull Collection c) { throw new UnsupportedOperationException(); } @Override public boolean retainAll(@NotNull Collection c) { throw new UnsupportedOperationException(); } @Override public void replaceAll(UnaryOperator operator) { throw new UnsupportedOperationException(); } @Override public void sort(Comparator c) { throw new UnsupportedOperationException(); } @Override public boolean removeIf(Predicate filter) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } @Override public E set(int index, E element) { throw new UnsupportedOperationException(); } @Override public void add(int index, E element) { throw new UnsupportedOperationException(); } @Override public E remove(int index) { throw new UnsupportedOperationException(); } @NotNull @Override public ListIterator listIterator() { throw new UnsupportedOperationException(); } @NotNull @Override public ListIterator listIterator(int index) { throw new UnsupportedOperationException(); } @NotNull @Override public List subList(int fromIndex, int toIndex) { throw new UnsupportedOperationException(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o instanceof ImmutableList) { ImmutableList other = (ImmutableList) o; return Arrays.equals(arr, other.arr); } if (o instanceof List) { List other = (List) o; int size = size(); if (size != other.size()) { return false; } for (int i = 0; i < size; i++) { E e1 = arr[i]; Object e2 = other.get(i); if (!Objects.equals(e1, e2)) { return false; } } return true; } return false; } @Override public int hashCode() { return Arrays.hashCode(arr); } @Override public String toString() { return "ImmutableList{" + Arrays.toString(arr) + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/InsnList.java ================================================ package jadx.core.utils; import java.util.Iterator; import java.util.List; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; public final class InsnList implements Iterable { private final List list; public InsnList(List list) { this.list = list; } public static void remove(List list, InsnNode insn) { for (Iterator iterator = list.iterator(); iterator.hasNext();) { InsnNode next = iterator.next(); if (next == insn) { iterator.remove(); return; } } } public static void remove(BlockNode block, InsnNode insn) { remove(block.getInstructions(), insn); } public static int getIndex(List list, InsnNode insn) { return getIndex(list, insn, 0); } public static int getIndex(List list, InsnNode insn, int startOffset) { int size = list.size(); for (int i = startOffset; i < size; i++) { if (list.get(i) == insn) { return i; } } return -1; } public static boolean contains(List list, InsnNode insn) { return getIndex(list, insn, 0) != -1; } public static boolean contains(List list, InsnNode insn, int startOffset) { return getIndex(list, insn, startOffset) != -1; } public int getIndex(InsnNode insn) { return getIndex(list, insn); } public boolean contains(InsnNode insn) { return getIndex(insn) != -1; } public void remove(InsnNode insn) { remove(list, insn); } public Iterator iterator() { return list.iterator(); } public InsnNode get(int index) { return list.get(index); } public int size() { return list.size(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/InsnRemover.java ================================================ package jadx.core.utils; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.PhiListAttr; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.InsnUtils.isInsnType; import static jadx.core.utils.ListUtils.allMatch; /** * Helper class for correct instructions removing, * can be used while iterating over instructions list */ public class InsnRemover { private final MethodNode mth; private final List toRemove; @Nullable private List instrList; public InsnRemover(MethodNode mth) { this(mth, null); } public InsnRemover(MethodNode mth, BlockNode block) { this.mth = mth; this.toRemove = new ArrayList<>(); if (block != null) { this.instrList = block.getInstructions(); } } public void setBlock(BlockNode block) { this.instrList = block.getInstructions(); } public void addAndUnbind(InsnNode insn) { toRemove.add(insn); unbindInsn(mth, insn); } public void addWithoutUnbind(InsnNode insn) { toRemove.add(insn); } public void perform() { if (toRemove.isEmpty()) { return; } if (instrList == null) { for (InsnNode remInsn : toRemove) { remove(mth, remInsn); } } else { unbindInsns(mth, toRemove); removeAll(instrList, toRemove); } toRemove.clear(); } public void performForBlock(BlockNode block) { if (toRemove.isEmpty()) { return; } instrList = Objects.requireNonNull(block.getInstructions()); unbindInsns(mth, toRemove); removeAll(instrList, toRemove); toRemove.clear(); } public static void unbindInsn(@Nullable MethodNode mth, InsnNode insn) { unbindAllArgs(mth, insn); unbindResult(mth, insn); insn.add(AFlag.DONT_GENERATE); } public static void unbindInsns(@Nullable MethodNode mth, List insns) { // remove all usage first so on result unbind we can remove unused ssa vars insns.forEach(insn -> unbindAllArgs(mth, insn)); insns.forEach(insn -> { unbindResult(mth, insn); insn.add(AFlag.DONT_GENERATE); }); } public static void unbindAllArgs(@Nullable MethodNode mth, InsnNode insn) { for (InsnArg arg : insn.getArguments()) { unbindArgUsage(mth, arg); } if (insn.getType() == InsnType.PHI) { for (InsnArg arg : insn.getArguments()) { if (arg instanceof RegisterArg) { ((RegisterArg) arg).getSVar().updateUsedInPhiList(); } } } insn.add(AFlag.REMOVE); insn.add(AFlag.DONT_GENERATE); } public static void unbindResult(@Nullable MethodNode mth, InsnNode insn) { RegisterArg r = insn.getResult(); if (r == null) { return; } if (mth != null) { SSAVar ssaVar = r.getSVar(); if (ssaVar != null && ssaVar.getAssignInsn() == insn /* can be already reassigned */) { removeSsaVar(mth, ssaVar); } } insn.setResult(null); } private static void removeSsaVar(MethodNode mth, SSAVar ssaVar) { int useCount = ssaVar.getUseCount(); if (useCount == 0) { mth.removeSVar(ssaVar); return; } // check if all usage only in PHI insns if (allMatch(ssaVar.getUseList(), arg -> isInsnType(arg.getParentInsn(), InsnType.PHI))) { for (RegisterArg arg : new ArrayList<>(ssaVar.getUseList())) { InsnNode parentInsn = arg.getParentInsn(); if (parentInsn != null) { ((PhiInsn) parentInsn).removeArg(arg); } } mth.removeSVar(ssaVar); return; } // check if all usage only in not generated instructions if (allMatch(ssaVar.getUseList(), arg -> arg.contains(AFlag.DONT_GENERATE) || (InsnUtils.contains(arg.getParentInsn(), AFlag.DONT_GENERATE)))) { for (RegisterArg arg : ssaVar.getUseList()) { arg.resetSSAVar(); } mth.removeSVar(ssaVar); return; } throw new JadxRuntimeException("Can't remove SSA var: " + ssaVar + ", still in use, count: " + useCount + ", list:\n " + ssaVar.getUseList().stream() .map(arg -> arg + " from " + arg.getParentInsn()) .collect(Collectors.joining("\n "))); } public static void unbindArgUsage(@Nullable MethodNode mth, InsnArg arg) { if (arg instanceof RegisterArg) { RegisterArg reg = (RegisterArg) arg; SSAVar sVar = reg.getSVar(); if (sVar != null) { sVar.removeUse(reg); } } else if (arg instanceof InsnWrapArg) { InsnWrapArg wrap = (InsnWrapArg) arg; unbindInsn(mth, wrap.getWrapInsn()); } } // Don't use 'instrList.removeAll(toRemove)' because it will remove instructions by content // and here can be several instructions with same content private static void removeAll(List insns, List toRemove) { if (toRemove == null || toRemove.isEmpty()) { return; } for (InsnNode rem : toRemove) { int insnsCount = insns.size(); boolean found = false; for (int i = 0; i < insnsCount; i++) { if (insns.get(i) == rem) { insns.remove(i); found = true; break; } } if (!found && Consts.DEBUG_WITH_ERRORS) { throw new JadxRuntimeException("Can't remove insn:" + "\n " + rem + "\n not found in list:" + "\n " + Utils.listToString(insns, "\n ")); } } } public static void remove(MethodNode mth, @Nullable InsnNode insn) { if (insn == null) { return; } if (insn.contains(AFlag.WRAPPED)) { unbindInsn(mth, insn); return; } BlockNode block = BlockUtils.getBlockByInsn(mth, insn); if (block != null) { remove(mth, block, insn); } else { insn.add(AFlag.DONT_GENERATE); mth.addWarnComment("Not found block with instruction: " + insn); } } public static void remove(MethodNode mth, BlockNode block, InsnNode insn) { unbindInsn(mth, insn); removeWithoutUnbind(mth, block, insn); } public static boolean removeWithoutUnbind(MethodNode mth, BlockNode block, InsnNode insn) { // remove by pointer (don't use equals) Iterator it = block.getInstructions().iterator(); while (it.hasNext()) { InsnNode ir = it.next(); if (ir == insn) { it.remove(); return true; } } if (!insn.contains(AFlag.WRAPPED)) { mth.addWarnComment("Failed to remove instruction: " + insn + " from block: " + block); } return false; } public static void removeAllAndUnbind(MethodNode mth, BlockNode block, List insns) { unbindInsns(mth, insns); removeAll(block.getInstructions(), insns); } public static void removeAllAndUnbind(MethodNode mth, IContainer container, List insns) { unbindInsns(mth, insns); RegionUtils.visitBlocks(mth, container, b -> removeAll(b.getInstructions(), insns)); } public static void removeAllAndUnbind(MethodNode mth, List insns) { unbindInsns(mth, insns); for (BlockNode block : mth.getBasicBlocks()) { removeAll(block.getInstructions(), insns); } } public static void removeAllWithoutUnbind(BlockNode block, List insns) { removeAll(block.getInstructions(), insns); } public static void removeAllMarked(MethodNode mth) { InsnRemover insnRemover = new InsnRemover(mth); for (BlockNode blockNode : mth.getBasicBlocks()) { for (InsnNode insn : blockNode.getInstructions()) { if (insn.contains(AFlag.REMOVE)) { insnRemover.addWithoutUnbind(insn); } } insnRemover.setBlock(blockNode); insnRemover.perform(); } } public static void remove(MethodNode mth, BlockNode block, int index) { List instructions = block.getInstructions(); unbindInsn(mth, instructions.get(index)); instructions.remove(index); } public static void delistPhi(MethodNode mth, PhiInsn phiInsn) { for (BlockNode block : mth.getBasicBlocks()) { PhiListAttr phiListAttr = block.get(AType.PHI_LIST); if (phiListAttr != null) { phiListAttr.getList().removeIf(i -> i == phiInsn); } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/InsnUtils.java ================================================ package jadx.core.utils; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.ConstClassNode; import jadx.core.dex.instructions.ConstStringNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; public class InsnUtils { private static final Logger LOG = LoggerFactory.getLogger(InsnUtils.class); private InsnUtils() { } public static String formatOffset(int offset) { if (offset < 0) { return "?"; } return String.format("0x%04x", offset); } public static String insnTypeToString(InsnType type) { return type + " "; } public static String indexToString(Object index) { if (index == null) { return ""; } if (index instanceof String) { return "\"" + index + '"'; } return index.toString(); } /** * Search constant assigned to provided arg. * * @return LiteralArg, String, ArgType or null */ public static Object getConstValueByArg(RootNode root, InsnArg arg) { if (arg.isLiteral()) { return arg; } if (arg.isRegister()) { RegisterArg reg = (RegisterArg) arg; InsnNode parInsn = reg.getAssignInsn(); if (parInsn == null) { return null; } if (parInsn.getType() == InsnType.MOVE) { return getConstValueByArg(root, parInsn.getArg(0)); } return getConstValueByInsn(root, parInsn); } if (arg.isInsnWrap()) { InsnNode insn = ((InsnWrapArg) arg).getWrapInsn(); return getConstValueByInsn(root, insn); } return null; } /** * Return constant value from insn or null if not constant. * * @return LiteralArg, String, ArgType or null */ @Nullable public static Object getConstValueByInsn(RootNode root, InsnNode insn) { switch (insn.getType()) { case CONST: return insn.getArg(0); case CONST_STR: return ((ConstStringNode) insn).getString(); case CONST_CLASS: return ((ConstClassNode) insn).getClsType(); case SGET: FieldInfo f = (FieldInfo) ((IndexInsnNode) insn).getIndex(); FieldNode fieldNode = root.resolveField(f); if (fieldNode == null) { LOG.warn("Field {} not found", f); return null; } EncodedValue constVal = fieldNode.get(JadxAttrType.CONSTANT_VALUE); if (constVal != null) { return EncodedValueUtils.convertToConstValue(constVal); } return null; default: return null; } } @Nullable public static InsnNode searchSingleReturnInsn(MethodNode mth, Predicate test) { if (!mth.isNoCode() && mth.getPreExitBlocks().size() == 1) { return searchInsn(mth, InsnType.RETURN, test); } return null; } /** * Search instruction of specific type and condition in method. * This method support inlined instructions. */ @Nullable public static InsnNode searchInsn(MethodNode mth, InsnType insnType, Predicate test) { if (mth.isNoCode()) { return null; } for (BlockNode block : mth.getBasicBlocks()) { for (InsnNode insn : block.getInstructions()) { InsnNode foundInsn = recursiveInsnCheck(insn, insnType, test); if (foundInsn != null) { return foundInsn; } } } return null; } public static void replaceInsns(MethodNode mth, Function replaceFunction) { for (BlockNode block : mth.getBasicBlocks()) { List insns = block.getInstructions(); int insnsCount = insns.size(); for (int i = 0; i < insnsCount; i++) { InsnNode insn = insns.get(i); replaceInsnsInInsn(mth, insn, replaceFunction); InsnNode replace = replaceFunction.apply(insn); if (replace != null) { BlockUtils.replaceInsn(mth, block, i, replace); } } } } public static void replaceInsnsInInsn(MethodNode mth, InsnNode insn, Function replaceFunction) { int argsCount = insn.getArgsCount(); for (int i = 0; i < argsCount; i++) { InsnArg arg = insn.getArg(i); if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); replaceInsnsInInsn(mth, wrapInsn, replaceFunction); InsnNode replace = replaceFunction.apply(wrapInsn); if (replace != null) { InsnRemover.unbindArgUsage(mth, arg); insn.setArg(i, InsnArg.wrapInsnIntoArg(replace)); } } } } @Nullable public static RegisterArg getRegFromInsn(List regs, InsnType insnType) { for (RegisterArg reg : regs) { InsnNode parentInsn = reg.getParentInsn(); if (parentInsn != null && parentInsn.getType() == insnType) { return reg; } } return null; } private static InsnNode recursiveInsnCheck(InsnNode insn, InsnType insnType, Predicate test) { if (insn.getType() == insnType && test.test(insn)) { return insn; } for (InsnArg arg : insn.getArguments()) { if (arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); InsnNode foundInsn = recursiveInsnCheck(wrapInsn, insnType, test); if (foundInsn != null) { return foundInsn; } } } return null; } @Nullable public static InsnArg getSingleArg(InsnNode insn) { if (insn != null && insn.getArgsCount() == 1) { return insn.getArg(0); } return null; } @Nullable public static InsnNode checkInsnType(@Nullable InsnNode insn, InsnType insnType) { if (insn != null && insn.getType() == insnType) { return insn; } return null; } public static boolean isInsnType(@Nullable InsnNode insn, InsnType insnType) { return insn != null && insn.getType() == insnType; } @Nullable public static InsnNode getWrappedInsn(InsnArg arg) { if (arg != null && arg.isInsnWrap()) { return ((InsnWrapArg) arg).getWrapInsn(); } return null; } public static boolean isWrapped(InsnArg arg, InsnType insnType) { if (arg != null && arg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); return wrapInsn.getType() == insnType; } return false; } public static boolean dontGenerateIfNotUsed(InsnNode insn) { RegisterArg resArg = insn.getResult(); if (resArg != null) { SSAVar ssaVar = resArg.getSVar(); for (RegisterArg arg : ssaVar.getUseList()) { InsnNode parentInsn = arg.getParentInsn(); if (parentInsn != null && !parentInsn.contains(AFlag.DONT_GENERATE)) { return false; } } } insn.add(AFlag.DONT_GENERATE); return true; } public static boolean containsVar(List list, RegisterArg arg) { if (list == null || list.isEmpty()) { return false; } for (InsnArg insnArg : list) { if (insnArg == arg || arg.sameRegAndSVar(insnArg)) { return true; } } return false; } public static boolean containsVar(InsnNode insn, RegisterArg arg) { if (insn == null) { return false; } RegisterArg result = insn.getResult(); if (result != null && result.sameRegAndSVar(arg)) { return true; } if (insn.getArgsCount() == 0) { return false; } for (InsnArg insnArg : insn.getArguments()) { if (containsVar(insnArg, arg)) { return true; } } return false; } public static boolean containsVar(InsnArg insnArg, RegisterArg arg) { if (insnArg.isRegister()) { return ((RegisterArg) insnArg).sameRegAndSVar(arg); } if (insnArg.isInsnWrap()) { InsnNode wrapInsn = ((InsnWrapArg) insnArg).getWrapInsn(); return containsVar(wrapInsn, arg); } return false; } public static boolean contains(InsnNode insn, AFlag flag) { return insn != null && insn.contains(flag); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/ListUtils.java ================================================ package jadx.core.utils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.function.BiPredicate; import java.util.function.Function; import java.util.function.Predicate; import org.jetbrains.annotations.Nullable; public class ListUtils { public static boolean isSingleElement(@Nullable List list, T obj) { if (list == null || list.size() != 1) { return false; } return Objects.equals(list.get(0), obj); } public static boolean unorderedEquals(List first, List second) { if (first.size() != second.size()) { return false; } return first.containsAll(second); } public static boolean orderedEquals(List list1, List list2, BiPredicate comparer) { if (list1 == list2) { return true; } if (list1.size() != list2.size()) { return false; } final Iterator iter1 = list1.iterator(); final Iterator iter2 = list2.iterator(); while (iter1.hasNext() && iter2.hasNext()) { final T item1 = iter1.next(); final U item2 = iter2.next(); if (!comparer.test(item1, item2)) { return false; } } return !iter1.hasNext() && !iter2.hasNext(); } public static List map(Collection list, Function mapFunc) { if (list == null || list.isEmpty()) { return Collections.emptyList(); } List result = new ArrayList<>(list.size()); for (T t : list) { result.add(mapFunc.apply(t)); } return result; } public static T first(List list) { return list.get(0); } public static @Nullable T last(List list) { if (list == null || list.isEmpty()) { return null; } return list.get(list.size() - 1); } public static @Nullable T removeLast(List list) { int size = list.size(); if (size == 0) { return null; } return list.remove(size - 1); } public static > List distinctMergeSortedLists(List first, List second) { if (first.isEmpty()) { return second; } if (second.isEmpty()) { return first; } Set set = new TreeSet<>(first); set.addAll(second); return new ArrayList<>(set); } public static List distinctList(List list) { return new ArrayList<>(new LinkedHashSet<>(list)); } public static List concat(T first, T[] values) { List list = new ArrayList<>(1 + values.length); list.add(first); list.addAll(Arrays.asList(values)); return list; } /** * Replace old element to new one. * Support null and empty immutable list (created by Collections.emptyList()) */ public static List safeReplace(List list, T oldObj, T newObj) { if (list == null || list.isEmpty()) { // immutable empty list List newList = new ArrayList<>(1); newList.add(newObj); return newList; } int idx = list.indexOf(oldObj); if (idx != -1) { list.set(idx, newObj); } else { list.add(newObj); } return list; } public static void safeRemove(List list, T obj) { if (list != null && !list.isEmpty()) { list.remove(obj); } } public static List safeRemoveAndTrim(List list, T obj) { if (list == null || list.isEmpty()) { return list; } if (list.remove(obj)) { if (list.isEmpty()) { return Collections.emptyList(); } } return list; } public static List safeAdd(List list, T obj) { if (list == null || list.isEmpty()) { List newList = new ArrayList<>(1); newList.add(obj); return newList; } list.add(obj); return list; } public static List filter(Collection list, Predicate filter) { if (list == null || list.isEmpty()) { return Collections.emptyList(); } List result = new ArrayList<>(); for (T element : list) { if (filter.test(element)) { result.add(element); } } return result; } /** * Search exactly one element in list by filter * * @return null if found not exactly one element (zero or more than one) */ @Nullable public static T filterOnlyOne(List list, Predicate filter) { if (list == null || list.isEmpty()) { return null; } T found = null; for (T element : list) { if (filter.test(element)) { if (found != null) { // found second return null; } found = element; } } return found; } public static boolean allMatch(Collection list, Predicate test) { if (list == null || list.isEmpty()) { return false; } for (T element : list) { if (!test.test(element)) { return false; } } return true; } public static boolean noneMatch(Collection list, Predicate test) { return !anyMatch(list, test); } public static boolean anyMatch(Collection list, Predicate test) { if (list == null || list.isEmpty()) { return false; } for (T element : list) { if (test.test(element)) { return true; } } return false; } public static List enumerationToList(Enumeration enumeration) { if (enumeration == null || enumeration == Collections.emptyEnumeration()) { return Collections.emptyList(); } List list = new ArrayList<>(); while (enumeration.hasMoreElements()) { list.add(enumeration.nextElement()); } return list; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/Pair.java ================================================ package jadx.core.utils; public class Pair { private final T first; private final T second; public Pair(T first, T second) { this.first = first; this.second = second; } public T getFirst() { return first; } public T getSecond() { return second; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Pair)) { return false; } Pair other = (Pair) o; return first.equals(other.first) && second.equals(other.second); } @Override public int hashCode() { return first.hashCode() + 31 * second.hashCode(); } @Override public String toString() { return "(" + first + ", " + second + ')'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/PassMerge.java ================================================ package jadx.core.utils; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import jadx.api.plugins.pass.JadxPass; import jadx.api.plugins.pass.JadxPassInfo; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.utils.exceptions.JadxRuntimeException; public class PassMerge { private final List visitors; private Set mergePassesNames; private Map namesMap; public PassMerge(List visitors) { this.visitors = visitors; } public void merge(List customPasses, Function wrap) { if (Utils.isEmpty(customPasses)) { return; } List mergePasses = ListUtils.map(customPasses, p -> new MergePass(p, wrap.apply(p), p.getInfo())); linkDeps(mergePasses); mergePasses.sort(new ExtDepsComparator(visitors).thenComparing(InvertedDepsComparator.INSTANCE)); namesMap = new IdentityHashMap<>(); visitors.forEach(p -> namesMap.put(p, p.getName())); mergePasses.forEach(p -> namesMap.put(p.getVisitor(), p.getName())); mergePassesNames = mergePasses.stream().map(MergePass::getName).collect(Collectors.toSet()); for (MergePass mergePass : mergePasses) { int pos = searchInsertPos(mergePass); if (pos == -1) { visitors.add(mergePass.getVisitor()); } else { visitors.add(pos, mergePass.getVisitor()); } } } private int searchInsertPos(MergePass pass) { List runAfter = pass.after(); List runBefore = pass.before(); if (runAfter.isEmpty() && runBefore.isEmpty()) { return -1; // last } if (ListUtils.isSingleElement(runAfter, JadxPassInfo.START)) { return 0; } if (ListUtils.isSingleElement(runBefore, JadxPassInfo.END)) { return -1; } int visitorsCount = visitors.size(); Map namePosMap = new HashMap<>(visitorsCount); for (int i = 0; i < visitorsCount; i++) { namePosMap.put(namesMap.get(visitors.get(i)), i); } int after = -1; for (String name : runAfter) { Integer pos = namePosMap.get(name); if (pos != null) { after = Math.max(after, pos); } else { if (mergePassesNames.contains(name)) { // ignore known passes continue; } throw new JadxRuntimeException("Ordering pass not found: " + name + ", listed in 'runAfter' of pass: " + pass + "\n all passes: " + ListUtils.map(visitors, namesMap::get)); } } int before = Integer.MAX_VALUE; for (String name : runBefore) { Integer pos = namePosMap.get(name); if (pos != null) { before = Math.min(before, pos); } else { if (mergePassesNames.contains(name)) { // ignore known passes continue; } throw new JadxRuntimeException("Ordering pass not found: " + name + ", listed in 'runBefore' of pass: " + pass + "\n all passes: " + ListUtils.map(visitors, namesMap::get)); } } if (before <= after) { throw new JadxRuntimeException("Conflict order requirements for pass: " + pass + "\n run after: " + runAfter + "\n run before: " + runBefore + "\n passes: " + ListUtils.map(visitors, namesMap::get)); } if (after == -1) { if (before == Integer.MAX_VALUE) { // not ordered, put at last return -1; } return before; } int pos = after + 1; return pos >= visitorsCount ? -1 : pos; } private static final class MergePass { private final JadxPass pass; private final IDexTreeVisitor visitor; private final JadxPassInfo info; // copy dep lists for future modifications private final List before; private final List after; private MergePass(JadxPass pass, IDexTreeVisitor visitor, JadxPassInfo info) { this.pass = pass; this.visitor = visitor; this.info = info; this.before = new ArrayList<>(info.runBefore()); this.after = new ArrayList<>(info.runAfter()); } public JadxPass getPass() { return pass; } public IDexTreeVisitor getVisitor() { return visitor; } public String getName() { return info.getName(); } public JadxPassInfo getInfo() { return info; } public List before() { return before; } public List after() { return after; } @Override public String toString() { return info.getName(); } } /** * Make deps double linked */ private static void linkDeps(List mergePasses) { Map map = mergePasses.stream().collect(Collectors.toMap(MergePass::getName, p -> p)); for (MergePass pass : mergePasses) { for (String after : pass.getInfo().runAfter()) { MergePass beforePass = map.get(after); if (beforePass != null) { beforePass.before().add(pass.getName()); } } for (String before : pass.getInfo().runBefore()) { MergePass afterPass = map.get(before); if (afterPass != null) { afterPass.after().add(pass.getName()); } } } } /** * Place passes with visitors dependencies before others. */ private static class ExtDepsComparator implements Comparator { private final Set names; public ExtDepsComparator(List visitors) { this.names = visitors.stream() .map(IDexTreeVisitor::getName) .collect(Collectors.toSet()); } @Override public int compare(MergePass first, MergePass second) { boolean isFirst = containsVisitor(first.before()) || containsVisitor(first.after()); boolean isSecond = containsVisitor(second.before()) || containsVisitor(second.after()); return -Boolean.compare(isFirst, isSecond); } private boolean containsVisitor(List deps) { for (String dep : deps) { if (names.contains(dep)) { return true; } } return false; } } /** * Sort to get inverted dependencies i.e. if pass depends on another place it before. */ private static class InvertedDepsComparator implements Comparator { public static final InvertedDepsComparator INSTANCE = new InvertedDepsComparator(); @Override public int compare(MergePass first, MergePass second) { if (first.before().contains(second.getName()) || first.after().contains(second.getName())) { return 1; } if (second.before().contains(first.getName()) || second.after().contains(first.getName())) { return -1; } return 0; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/RegionUtils.java ================================================ package jadx.core.utils; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttrList; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.attributes.nodes.LoopLabelAttr; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.IBranchRegion; import jadx.core.dex.nodes.IConditionRegion; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.trycatch.TryCatchBlockAttr; import jadx.core.dex.visitors.regions.AbstractRegionVisitor; import jadx.core.dex.visitors.regions.DepthRegionTraversal; import jadx.core.utils.exceptions.JadxRuntimeException; public class RegionUtils { private RegionUtils() { } public static boolean hasExitEdge(IContainer container) { if (container instanceof IBlock) { return BlockUtils.containsExitInsn((IBlock) container); } if (container instanceof IBranchRegion) { // all branches must have exit edge for (IContainer br : ((IBranchRegion) container).getBranches()) { if (br == null || !hasExitEdge(br)) { return false; } } return true; } if (container instanceof IRegion) { IContainer last = Utils.last(((IRegion) container).getSubBlocks()); return last != null && hasExitEdge(last); } throw new JadxRuntimeException(unknownContainerType(container)); } @Nullable public static InsnNode getFirstInsn(IContainer container) { if (container instanceof IBlock) { IBlock block = (IBlock) container; List insnList = block.getInstructions(); if (insnList.isEmpty()) { return null; } return insnList.get(0); } else if (container instanceof IBranchRegion) { return null; } else if (container instanceof IRegion) { IRegion region = (IRegion) container; List blocks = region.getSubBlocks(); if (blocks.isEmpty()) { return null; } return getFirstInsn(blocks.get(0)); } else { throw new JadxRuntimeException(unknownContainerType(container)); } } @Nullable public static IBlock getFirstBlock(IContainer container) { if (container == null) { return null; } if (container instanceof IBlock) { return (IBlock) container; } else if (container instanceof IBranchRegion) { return null; } else if (container instanceof IRegion) { IRegion region = (IRegion) container; List blocks = region.getSubBlocks(); if (blocks.isEmpty()) { return null; } return getFirstBlock(blocks.get(0)); } else { throw new JadxRuntimeException(unknownContainerType(container)); } } public static @Nullable BlockNode getFirstBlockNode(IContainer container) { if (container instanceof IBlock) { if (container instanceof BlockNode) { return (BlockNode) container; } return null; } if (container instanceof IBranchRegion) { return null; } if (container instanceof IRegion) { List blocks = ((IRegion) container).getSubBlocks(); if (blocks.isEmpty()) { return null; } return getFirstBlockNode(blocks.get(0)); } throw new JadxRuntimeException(unknownContainerType(container)); } public static int getFirstSourceLine(IContainer container) { if (container instanceof IBlock) { return BlockUtils.getFirstSourceLine((IBlock) container); } if (container instanceof IConditionRegion) { return ((IConditionRegion) container).getConditionSourceLine(); } if (container instanceof IBranchRegion) { IBranchRegion branchRegion = (IBranchRegion) container; return getFirstSourceLine(branchRegion.getBranches()); } if (container instanceof IRegion) { IRegion region = (IRegion) container; return getFirstSourceLine(region.getSubBlocks()); } return 0; } private static int getFirstSourceLine(List containers) { if (containers.isEmpty()) { return 0; } for (IContainer container : containers) { int line = getFirstSourceLine(container); if (line != 0) { return line; } } return 0; } public static InsnNode getLastInsn(IContainer container) { if (container instanceof IBlock) { IBlock block = (IBlock) container; List insnList = block.getInstructions(); if (insnList.isEmpty()) { return null; } return insnList.get(insnList.size() - 1); } else if (container instanceof IBranchRegion) { return null; } else if (container instanceof IRegion) { IRegion region = (IRegion) container; List blocks = region.getSubBlocks(); if (blocks.isEmpty()) { return null; } return getLastInsn(blocks.get(blocks.size() - 1)); } else { throw new JadxRuntimeException(unknownContainerType(container)); } } public static BlockInsnPair getLastInsnWithBlock(IContainer container) { if (container instanceof IBlock) { IBlock block = (IBlock) container; InsnNode lastInsn = ListUtils.last(block.getInstructions()); if (lastInsn == null) { return null; } return new BlockInsnPair(block, lastInsn); } if (container instanceof IBranchRegion) { List branches = ((IBranchRegion) container).getBranches(); long count = branches.stream().filter(Objects::nonNull).count(); if (count == 1) { // single branch for (IContainer branch : branches) { if (branch != null) { return getLastInsnWithBlock(branch); } } } // several last instructions return null; } if (container instanceof IRegion) { IRegion region = (IRegion) container; List blocks = region.getSubBlocks(); if (blocks.isEmpty()) { return null; } return getLastInsnWithBlock(ListUtils.last(blocks)); } throw new JadxRuntimeException(unknownContainerType(container)); } public static IBlock getLastBlock(IContainer container) { if (container instanceof IBlock) { return (IBlock) container; } else if (container instanceof IBranchRegion) { return null; } else if (container instanceof IRegion) { List blocks = ((IRegion) container).getSubBlocks(); if (blocks.isEmpty()) { return null; } return getLastBlock(blocks.get(blocks.size() - 1)); } else { throw new JadxRuntimeException(unknownContainerType(container)); } } public static boolean isExitBlock(MethodNode mth, IContainer container) { if (container instanceof BlockNode) { return BlockUtils.isExitBlock(mth, (BlockNode) container); } return false; } /** * Return true if last block in region has no successors or jump out insn (return or break) */ public static boolean hasExitBlock(IContainer container) { if (container == null) { return false; } return hasExitBlock(container, container); } private static boolean hasExitBlock(IContainer rootContainer, IContainer container) { if (container instanceof BlockNode) { BlockNode blockNode = (BlockNode) container; if (BlockUtils.isExitBlock(blockNode)) { return true; } return isInsnExitContainer(rootContainer, (IBlock) container); } if (container instanceof IBranchRegion) { IBranchRegion branchRegion = (IBranchRegion) container; return ListUtils.allMatch(branchRegion.getBranches(), RegionUtils::hasExitBlock); } if (container instanceof IBlock) { return isInsnExitContainer(rootContainer, (IBlock) container); } if (container instanceof IRegion) { List blocks = ((IRegion) container).getSubBlocks(); return !blocks.isEmpty() && hasExitBlock(rootContainer, blocks.get(blocks.size() - 1)); } throw new JadxRuntimeException(unknownContainerType(container)); } private static boolean isInsnExitContainer(IContainer rootContainer, IBlock block) { InsnNode lastInsn = BlockUtils.getLastInsn(block); if (lastInsn == null) { return false; } InsnType insnType = lastInsn.getType(); if (insnType == InsnType.RETURN) { return true; } if (insnType == InsnType.THROW) { // check if after throw execution can continue in current container CatchAttr catchAttr = lastInsn.get(AType.EXC_CATCH); if (catchAttr != null) { for (ExceptionHandler handler : catchAttr.getHandlers()) { if (RegionUtils.isRegionContainsBlock(rootContainer, handler.getHandlerBlock())) { return false; } } } return true; } if (insnType == InsnType.BREAK) { AttrList loopInfoAttrList = lastInsn.get(AType.LOOP); if (loopInfoAttrList != null) { for (LoopInfo loopInfo : loopInfoAttrList.getList()) { if (!RegionUtils.isRegionContainsBlock(rootContainer, loopInfo.getStart())) { return true; } } } LoopLabelAttr loopLabelAttr = lastInsn.get(AType.LOOP_LABEL); if (loopLabelAttr != null && !RegionUtils.isRegionContainsBlock(rootContainer, loopLabelAttr.getLoop().getStart())) { return true; } } return false; } public static boolean hasBreakInsn(IContainer container) { if (container instanceof IBlock) { return BlockUtils.checkLastInsnType((IBlock) container, InsnType.BREAK); } else if (container instanceof IRegion) { List blocks = ((IRegion) container).getSubBlocks(); return !blocks.isEmpty() && hasBreakInsn(blocks.get(blocks.size() - 1)); } else { throw new JadxRuntimeException("Unknown container type: " + container); } } public static int insnsCount(IContainer container) { if (container instanceof IBlock) { List insnList = ((IBlock) container).getInstructions(); int count = 0; for (InsnNode insn : insnList) { if (insn.contains(AFlag.DONT_GENERATE)) { continue; } count++; } return count; } if (container instanceof IRegion) { IRegion region = (IRegion) container; int count = 0; for (IContainer block : region.getSubBlocks()) { count += insnsCount(block); } return count; } throw new JadxRuntimeException(unknownContainerType(container)); } public static List collectInsns(MethodNode mth, IContainer container) { List list = new ArrayList<>(); visitBlocks(mth, container, block -> list.addAll(block.getInstructions())); return list; } public static boolean isEmpty(IContainer container) { return !notEmpty(container); } public static boolean notEmpty(@Nullable IContainer container) { if (container == null) { return false; } if (container instanceof IBlock) { List insnList = ((IBlock) container).getInstructions(); for (InsnNode insnNode : insnList) { if (!insnNode.contains(AFlag.DONT_GENERATE)) { return true; } } return false; } if (container instanceof LoopRegion) { return true; } if (container instanceof IRegion) { IRegion region = (IRegion) container; for (IContainer block : region.getSubBlocks()) { if (notEmpty(block)) { return true; } } return false; } throw new JadxRuntimeException(unknownContainerType(container)); } public static void getAllRegionBlocks(IContainer container, Set blocks) { if (container instanceof IBlock) { blocks.add((IBlock) container); } else if (container instanceof IRegion) { IRegion region = (IRegion) container; for (IContainer block : region.getSubBlocks()) { getAllRegionBlocks(block, blocks); } } else { throw new JadxRuntimeException(unknownContainerType(container)); } } public static boolean isRegionContainsBlock(IContainer container, BlockNode block) { if (container instanceof IBlock) { return container == block; } else if (container instanceof IRegion) { IRegion region = (IRegion) container; for (IContainer b : region.getSubBlocks()) { if (isRegionContainsBlock(b, block)) { return true; } } return false; } else { throw new JadxRuntimeException(unknownContainerType(container)); } } public static IContainer getSingleSubBlock(IContainer container) { if (container instanceof Region) { List subBlocks = ((Region) container).getSubBlocks(); if (subBlocks.size() == 1) { return ignoreSimpleRegionWrapper(subBlocks.get(0)); } } return null; } private static IContainer ignoreSimpleRegionWrapper(IContainer container) { while (true) { if (container instanceof Region) { List subBlocks = ((Region) container).getSubBlocks(); if (subBlocks.size() != 1) { return container; } container = subBlocks.get(0); } else { return container; } } } public static List getExcHandlersForRegion(IContainer region) { TryCatchBlockAttr tb = region.get(AType.TRY_BLOCK); if (tb != null) { List list = new ArrayList<>(tb.getHandlersCount()); for (ExceptionHandler eh : tb.getHandlers()) { list.add(eh.getHandlerRegion()); } return list; } return Collections.emptyList(); } public static List getLoopsStartInRegion(MethodNode mth, IRegion r) { List loops = new ArrayList<>(); visitBlocks(mth, r, b -> { if (b.contains(AFlag.LOOP_START)) { loops.addAll(b.getAll(AType.LOOP)); } }); return loops; } private static boolean isRegionContainsExcHandlerRegion(IContainer container, IRegion region) { if (container == region) { return true; } if (container instanceof IRegion) { IRegion r = (IRegion) container; // process sub blocks for (IContainer b : r.getSubBlocks()) { // process try block TryCatchBlockAttr tb = b.get(AType.TRY_BLOCK); if (tb != null && b instanceof IRegion) { for (ExceptionHandler eh : tb.getHandlers()) { if (isRegionContainsRegion(eh.getHandlerRegion(), region)) { return true; } } } if (isRegionContainsRegion(b, region)) { return true; } } } return false; } /** * Check if {@code region} contains in {@code container}. *
* For simple region (not from exception handlers) search in parents * otherwise run recursive search because exception handlers can have several parents */ public static boolean isRegionContainsRegion(IContainer container, IRegion region) { if (container == region) { return true; } if (region == null) { return false; } IRegion parent = region.getParent(); while (container != parent) { if (parent == null) { if (region.contains(AType.EXC_HANDLER)) { return isRegionContainsExcHandlerRegion(container, region); } return false; } region = parent; parent = region.getParent(); } return true; } public static IContainer getBlockContainer(IContainer container, IBlock block) { if (container instanceof IBlock) { return container == block ? container : null; } if (container instanceof IRegion) { IRegion region = (IRegion) container; for (IContainer c : region.getSubBlocks()) { IContainer res = getBlockContainer(c, block); if (res != null) { return res instanceof IBlock ? region : res; } } return null; } throw new JadxRuntimeException(unknownContainerType(container)); } /** * Check if two blocks in same region on same level * TODO: Add 'region' annotation to all blocks to speed up checks */ public static boolean isBlocksInSameRegion(MethodNode mth, BlockNode firstBlock, BlockNode secondBlock) { Region region = mth.getRegion(); if (region == null) { return false; } IContainer firstContainer = getBlockContainer(region, firstBlock); if (firstContainer instanceof IRegion) { if (firstContainer instanceof IBranchRegion) { return false; } List subBlocks = ((IRegion) firstContainer).getSubBlocks(); return subBlocks.contains(secondBlock); } return false; } public static boolean isDominatedBy(BlockNode dom, IContainer cont) { if (dom == cont) { return true; } if (cont instanceof BlockNode) { BlockNode block = (BlockNode) cont; return block.isDominator(dom); } else if (cont instanceof IBlock) { return false; } else if (cont instanceof IRegion) { IRegion region = (IRegion) cont; for (IContainer c : region.getSubBlocks()) { if (!isDominatedBy(dom, c)) { return false; } } return true; } else { throw new JadxRuntimeException(unknownContainerType(cont)); } } public static boolean hasPathThroughBlock(BlockNode block, IContainer cont) { if (block == cont) { return true; } if (cont instanceof BlockNode) { return BlockUtils.isPathExists(block, (BlockNode) cont); } if (cont instanceof IBlock) { return false; } if (cont instanceof IRegion) { IRegion region = (IRegion) cont; for (IContainer c : region.getSubBlocks()) { if (hasPathThroughBlock(block, c)) { return true; } } return false; } throw new JadxRuntimeException(unknownContainerType(cont)); } protected static String unknownContainerType(IContainer container) { if (container == null) { return "Null container variable"; } return "Unknown container type: " + container.getClass(); } public static void visitBlocks(MethodNode mth, IContainer container, Consumer visitor) { DepthRegionTraversal.traverse(mth, container, new AbstractRegionVisitor() { @Override public void processBlock(MethodNode mth, IBlock block) { visitor.accept(block); } }); } public static void visitBlockNodes(MethodNode mth, IContainer container, Consumer visitor) { DepthRegionTraversal.traverse(mth, container, new AbstractRegionVisitor() { @Override public void processBlock(MethodNode mth, IBlock block) { if (block instanceof BlockNode) { visitor.accept((BlockNode) block); } } }); } public static void visitRegions(MethodNode mth, IContainer container, Predicate visitor) { DepthRegionTraversal.traverse(mth, container, new AbstractRegionVisitor() { @Override public boolean enterRegion(MethodNode mth, IRegion region) { return visitor.test(region); } }); } public static @Nullable IContainer getNextContainer(MethodNode mth, IRegion region) { IRegion parent = region.getParent(); List subBlocks = parent.getSubBlocks(); int index = subBlocks.indexOf(region); if (index == -1 || index + 1 >= subBlocks.size()) { return null; } return subBlocks.get(index + 1); } // Add a flag to all blocks in a region public static void addToAll(MethodNode mth, IContainer container, AFlag flag) { RegionUtils.visitBlocks(mth, container, new Consumer() { @Override public void accept(IBlock t) { t.add(flag); } }); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/StringUtils.java ================================================ package jadx.core.utils; import java.text.SimpleDateFormat; import java.util.Date; import java.util.function.IntConsumer; import org.jetbrains.annotations.Nullable; import jadx.api.JadxArgs; import jadx.api.args.IntegerFormat; import jadx.core.deobf.NameMapper; import jadx.core.utils.exceptions.JadxRuntimeException; public class StringUtils { private static final StringUtils DEFAULT_INSTANCE = new StringUtils(new JadxArgs()); private static final String WHITES = " \t\r\n\f\b"; private static final String WORD_SEPARATORS = WHITES + "(\")<,>{}=+-*/|[]\\:;'.`~!#^&"; public static StringUtils getInstance() { return DEFAULT_INSTANCE; } private final boolean escapeUnicode; private final IntegerFormat integerFormat; public StringUtils(JadxArgs args) { this.escapeUnicode = args.isEscapeUnicode(); this.integerFormat = args.getIntegerFormat(); } public IntegerFormat getIntegerFormat() { return integerFormat; } public static void visitCodePoints(String str, IntConsumer visitor) { int len = str.length(); int offset = 0; while (offset < len) { int codePoint = str.codePointAt(offset); visitor.accept(codePoint); offset += Character.charCount(codePoint); } } public String unescapeString(String str) { int len = str.length(); if (len == 0) { return "\"\""; } StringBuilder res = new StringBuilder(); res.append('"'); visitCodePoints(str, codePoint -> processCodePoint(codePoint, res)); res.append('"'); return res.toString(); } private void processCodePoint(int codePoint, StringBuilder res) { String str = getSpecialStringForCodePoint(codePoint); if (str != null) { res.append(str); return; } if (isEscapeNeededForCodePoint(codePoint)) { res.append("\\u").append(String.format("%04x", codePoint)); } else { res.appendCodePoint(codePoint); } } private boolean isEscapeNeededForCodePoint(int codePoint) { if (codePoint < 32) { return true; } if (codePoint < 127) { return false; } if (escapeUnicode) { return true; } return !NameMapper.isPrintableCodePoint(codePoint); } /** * Represent single char the best way possible */ public String unescapeChar(char c, boolean explicitCast) { if (c == '\'') { return "'\\''"; } String str = getSpecialStringForCodePoint(c); if (str != null) { return '\'' + str + '\''; } if (c >= 127 && escapeUnicode) { return String.format("'\\u%04x'", (int) c); } if (NameMapper.isPrintableChar(c)) { return "'" + c + '\''; } String intStr = Integer.toString(c); return explicitCast ? "(char) " + intStr : intStr; } public String unescapeChar(char ch) { return unescapeChar(ch, false); } @Nullable private String getSpecialStringForCodePoint(int c) { switch (c) { case '\n': return "\\n"; case '\r': return "\\r"; case '\t': return "\\t"; case '\b': return "\\b"; case '\f': return "\\f"; case '\'': return "'"; case '"': return "\\\""; case '\\': return "\\\\"; default: return null; } } public static String escape(String str) { int len = str.length(); StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = str.charAt(i); switch (c) { case '.': case '/': case ';': case '$': case ' ': case ',': case '<': sb.append('_'); break; case '[': sb.append('A'); break; case ']': case '>': case '?': case '*': break; default: sb.append(c); break; } } return sb.toString(); } public static String escapeXML(String str) { int len = str.length(); StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = str.charAt(i); String replace = escapeXmlChar(c); if (replace != null) { sb.append(replace); } else { sb.append(c); } } return sb.toString(); } public static String escapeResValue(String str) { int len = str.length(); StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = str.charAt(i); commonEscapeAndAppend(sb, c); } return sb.toString(); } public static String escapeResStrValue(String str) { int len = str.length(); StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = str.charAt(i); switch (c) { case '"': sb.append("\\\""); break; case '\'': sb.append("\\'"); break; default: commonEscapeAndAppend(sb, c); break; } } return sb.toString(); } private static String escapeXmlChar(char c) { if (c <= 0x1F) { return "\\" + (int) c; } switch (c) { case '&': return "&"; case '<': return "<"; case '>': return ">"; case '"': return """; case '\'': return "'"; case '\\': return "\\\\"; default: return null; } } private static String escapeWhiteSpaceChar(char c) { switch (c) { case '\n': return "\\n"; case '\r': return "\\r"; case '\t': return "\\t"; case '\b': return "\\b"; case '\f': return "\\f"; default: return null; } } private static void commonEscapeAndAppend(StringBuilder sb, char c) { String replace = escapeWhiteSpaceChar(c); if (replace == null) { replace = escapeXmlChar(c); } if (replace != null) { sb.append(replace); } else { sb.append(c); } } public static boolean notEmpty(String str) { return str != null && !str.isEmpty(); } public static boolean isEmpty(String str) { return str == null || str.isEmpty(); } public static boolean notBlank(String str) { return notEmpty(str) && !str.trim().isEmpty(); } public static int countMatches(String str, String subStr) { if (str == null || str.isEmpty() || subStr == null || subStr.isEmpty()) { return 0; } int subStrLen = subStr.length(); int count = 0; int idx = 0; while ((idx = str.indexOf(subStr, idx)) != -1) { count++; idx += subStrLen; } return count; } public static boolean containsChar(String str, char ch) { return str.indexOf(ch) != -1; } public static String removeChar(String str, char ch) { int pos = str.indexOf(ch); if (pos == -1) { return str; } StringBuilder sb = new StringBuilder(str.length()); int cur = 0; int next = pos; while (true) { sb.append(str, cur, next); cur = next + 1; next = str.indexOf(ch, cur); if (next == -1) { sb.append(str, cur, str.length()); break; } } return sb.toString(); } /** * returns how many lines does it have between start to pos in content. */ public static int countLinesByPos(String content, int pos, int start) { if (start >= pos) { return 0; } int count = 0; int tempPos = start; do { tempPos = content.indexOf("\n", tempPos); if (tempPos == -1) { break; } if (tempPos >= pos) { break; } count += 1; tempPos += 1; } while (tempPos < content.length()); return count; } /** * returns lines that contain pos to end if end is not -1. */ public static String getLine(String content, int pos, int end) { if (pos >= content.length()) { return ""; } if (end != -1) { if (end > content.length()) { end = content.length() - 1; } } else { end = pos + 1; } // get to line head int headPos = content.lastIndexOf("\n", pos); if (headPos == -1) { headPos = 0; } // get to line end int endPos = content.indexOf("\n", end); if (endPos == -1) { endPos = content.length(); } return content.substring(headPos, endPos); } public static boolean isWhite(char chr) { return WHITES.indexOf(chr) != -1; } public static boolean isWordSeparator(char chr) { return WORD_SEPARATORS.indexOf(chr) != -1; } public static String removeSuffix(String str, String suffix) { if (str.endsWith(suffix)) { return str.substring(0, str.length() - suffix.length()); } return str; } public static @Nullable String getPrefix(String str, String delim) { int idx = str.indexOf(delim); if (idx != -1) { return str.substring(0, idx); } return null; } public static String getDateText() { return new SimpleDateFormat("HH:mm:ss").format(new Date()); } private String formatNumber(long number, int bytesLen, boolean cast) { String numStr; if (integerFormat.isHexadecimal()) { String hexStr = Long.toHexString(number); if (number < 0) { // cut leading 'f' for negative numbers to match number type length int len = hexStr.length(); numStr = "0x" + hexStr.substring(len - bytesLen * 2, len); // force cast, because unsigned negative numbers are bigger // than signed max value allowed by compiler cast = true; } else { numStr = "0x" + hexStr; } } else { numStr = Long.toString(number); } if (bytesLen == 8 && (number == Long.MIN_VALUE || Math.abs(number) >= Integer.MAX_VALUE)) { // force cast for long values bigger than min/max int // to resolve compiler error: "integer number too large" cast = true; } if (cast) { if (bytesLen == 8) { return numStr + 'L'; } return getCastStr(bytesLen) + numStr; } return numStr; } private static String getCastStr(int bytesLen) { switch (bytesLen) { case 1: return "(byte) "; case 2: return "(short) "; case 4: return "(int) "; case 8: return "(long) "; default: throw new JadxRuntimeException("Unexpected number type length: " + bytesLen); } } public String formatByte(long l, boolean cast) { return formatNumber(l, 1, cast); } public String formatShort(long l, boolean cast) { if (integerFormat == IntegerFormat.AUTO) { switch ((short) l) { case Short.MAX_VALUE: return "Short.MAX_VALUE"; case Short.MIN_VALUE: return "Short.MIN_VALUE"; } } return formatNumber(l, 2, cast); } public String formatInteger(long l, boolean cast) { if (integerFormat == IntegerFormat.AUTO) { switch ((int) l) { case Integer.MAX_VALUE: return "Integer.MAX_VALUE"; case Integer.MIN_VALUE: return "Integer.MIN_VALUE"; } } return formatNumber(l, 4, cast); } public String formatLong(long l, boolean cast) { if (integerFormat == IntegerFormat.AUTO) { if (l == Long.MAX_VALUE) { return "Long.MAX_VALUE"; } if (l == Long.MIN_VALUE) { return "Long.MIN_VALUE"; } } return formatNumber(l, 8, cast); } public static String formatDouble(double d) { if (Double.isNaN(d)) { return "Double.NaN"; } if (d == Double.NEGATIVE_INFINITY) { return "Double.NEGATIVE_INFINITY"; } if (d == Double.POSITIVE_INFINITY) { return "Double.POSITIVE_INFINITY"; } if (d == Double.MIN_VALUE) { return "Double.MIN_VALUE"; } if (d == Double.MAX_VALUE) { return "Double.MAX_VALUE"; } if (d == Double.MIN_NORMAL) { return "Double.MIN_NORMAL"; } return Double.toString(d) + 'd'; } public static String formatFloat(float f) { if (Float.isNaN(f)) { return "Float.NaN"; } if (f == Float.NEGATIVE_INFINITY) { return "Float.NEGATIVE_INFINITY"; } if (f == Float.POSITIVE_INFINITY) { return "Float.POSITIVE_INFINITY"; } if (f == Float.MIN_VALUE) { return "Float.MIN_VALUE"; } if (f == Float.MAX_VALUE) { return "Float.MAX_VALUE"; } if (f == Float.MIN_NORMAL) { return "Float.MIN_NORMAL"; } return Float.toString(f) + 'f'; } public static String capitalizeFirstChar(String str) { if (isEmpty(str)) { return str; } return Character.toUpperCase(str.charAt(0)) + str.substring(1); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/Utils.java ================================================ package jadx.core.utils; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeWriter; import jadx.api.JadxDecompiler; import jadx.core.dex.visitors.DepthTraversal; import jadx.core.utils.exceptions.JadxRuntimeException; public class Utils { private static final String JADX_API_PACKAGE = JadxDecompiler.class.getPackage().getName(); private static final String STACKTRACE_STOP_CLS_NAME = DepthTraversal.class.getName(); private Utils() { } public static String cleanObjectName(String obj) { if (obj.charAt(0) == 'L') { int last = obj.length() - 1; if (obj.charAt(last) == ';') { return obj.substring(1, last).replace('/', '.'); } } return obj; } public static String cutObject(String obj) { if (obj.charAt(0) == 'L') { return obj.substring(1, obj.length() - 1); } return obj; } public static String makeQualifiedObjectName(String obj) { return 'L' + obj.replace('.', '/') + ';'; } public static String smaliNameToJavaName(String descString) { if (descString.isEmpty()) { return descString; } String javaName; switch (descString.charAt(0)) { case 'V': javaName = "void"; break; case 'Z': javaName = "boolean"; break; case 'C': javaName = "char"; break; case 'B': javaName = "byte"; break; case 'S': javaName = "short"; break; case 'I': javaName = "int"; break; case 'F': javaName = "float"; break; case 'J': javaName = "long"; break; case 'D': javaName = "double"; break; case 'L': javaName = cleanObjectNameWithInnerClass(descString); break; case '[': javaName = String.format("%s[]", smaliNameToJavaName(descString.substring(1, descString.length()))); break; default: javaName = descString; break; } return javaName; } private static String cleanObjectNameWithInnerClass(String obj) { // Probably can just update the Utils.cleanObjectName method? String result = Utils.cleanObjectName(obj); return result.replace('$', '.'); } public static String javaNameToSmaliName(String descString) { if (descString.isEmpty()) { return descString; } if (descString.endsWith("[]")) { return String.format("[%s", javaNameToSmaliName(descString.substring(0, descString.length() - 2))); } String javaName; switch (descString) { case "void": javaName = "V"; break; case "boolean": javaName = "Z"; break; case "char": javaName = "C"; break; case "byte": javaName = "B"; break; case "short": javaName = "S"; break; case "int": javaName = "I"; break; case "float": javaName = "F"; break; case "long": javaName = "J"; break; case "double": javaName = "D"; break; default: javaName = Utils.makeQualifiedObjectName(descString); break; } return javaName; } @SuppressWarnings("StringRepeatCanBeUsed") public static String strRepeat(String str, int count) { if (count < 1) { return ""; } if (count == 1) { return str; } StringBuilder sb = new StringBuilder(str.length() * count); for (int i = 0; i < count; i++) { sb.append(str); } return sb.toString(); } public static String listToString(Iterable objects) { return listToString(objects, ", "); } public static String listToString(Iterable objects, String joiner) { if (objects == null) { return ""; } return listToString(objects, joiner, Objects::toString); } public static String listToString(Iterable objects, Function toStr) { return listToString(objects, ", ", toStr); } public static String listToString(Iterable objects, String joiner, Function toStr) { StringBuilder sb = new StringBuilder(); listToString(sb, objects, joiner, toStr); return sb.toString(); } public static void listToString(StringBuilder sb, Iterable objects, String joiner) { listToString(sb, objects, joiner, Objects::toString); } public static void listToString(StringBuilder sb, Iterable objects, String joiner, Function toStr) { if (objects == null) { return; } Iterator it = objects.iterator(); if (it.hasNext()) { sb.append(toStr.apply(it.next())); } while (it.hasNext()) { sb.append(joiner).append(toStr.apply(it.next())); } } public static String arrayToStr(T[] arr) { int len = arr == null ? 0 : arr.length; if (len == 0) { return ""; } StringBuilder sb = new StringBuilder(); sb.append(arr[0]); for (int i = 1; i < len; i++) { sb.append(", ").append(arr[i]); } return sb.toString(); } public static String concatStrings(List list) { if (isEmpty(list)) { return ""; } if (list.size() == 1) { return list.get(0); } StringBuilder sb = new StringBuilder(); list.forEach(sb::append); return sb.toString(); } public static String currentStackTrace() { return getStackTrace(new Exception()); } public static String currentStackTrace(int skipFrames) { Exception e = new Exception(); StackTraceElement[] stackTrace = e.getStackTrace(); int len = stackTrace.length; if (skipFrames < len) { e.setStackTrace(Arrays.copyOfRange(stackTrace, skipFrames, len)); } return getStackTrace(e); } public static String getFullStackTrace(Throwable throwable) { return getStackTrace(throwable, false); } public static String getStackTrace(Throwable throwable) { return getStackTrace(throwable, true); } private static String getStackTrace(Throwable throwable, boolean filter) { if (throwable == null) { return ""; } StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw, true); if (filter) { filterRecursive(throwable); } throwable.printStackTrace(pw); return sw.getBuffer().toString(); } public static void appendStackTrace(ICodeWriter code, Throwable throwable) { if (throwable == null) { return; } code.startLine(); OutputStream w = new OutputStream() { @Override public void write(int b) { char c = (char) b; switch (c) { case '\n': code.startLine(); break; case '\r': // ignore break; default: code.add(c); break; } } }; try (PrintWriter pw = new PrintWriter(w, true)) { filterRecursive(throwable); throwable.printStackTrace(pw); pw.flush(); } } private static void filterRecursive(Throwable th) { try { filter(th); } catch (Exception e) { // ignore filter exceptions } Throwable cause = th.getCause(); if (cause != null) { filterRecursive(cause); } } private static void filter(Throwable th) { StackTraceElement[] stackTrace = th.getStackTrace(); int length = stackTrace.length; StackTraceElement prevElement = null; for (int i = 0; i < length; i++) { StackTraceElement stackTraceElement = stackTrace[i]; String clsName = stackTraceElement.getClassName(); if (clsName.equals(STACKTRACE_STOP_CLS_NAME) || clsName.startsWith(JADX_API_PACKAGE) || Objects.equals(prevElement, stackTraceElement)) { th.setStackTrace(Arrays.copyOfRange(stackTrace, 0, i)); return; } prevElement = stackTraceElement; } // stop condition not found -> just cut tail to any jadx class for (int i = length - 1; i >= 0; i--) { String clsName = stackTrace[i].getClassName(); if (clsName.startsWith("jadx.")) { if (clsName.startsWith("jadx.tests.")) { continue; } th.setStackTrace(Arrays.copyOfRange(stackTrace, 0, i)); return; } } } public static List collectionMap(Collection list, Function mapFunc) { if (list == null || list.isEmpty()) { return Collections.emptyList(); } List result = new ArrayList<>(list.size()); for (T t : list) { result.add(mapFunc.apply(t)); } return result; } public static List collectionMapNoNull(Collection list, Function mapFunc) { if (list == null || list.isEmpty()) { return Collections.emptyList(); } List result = new ArrayList<>(list.size()); for (T t : list) { R r = mapFunc.apply(t); if (r != null) { result.add(r); } } return result; } public static boolean containsInListByRef(List list, T element) { if (isEmpty(list)) { return false; } for (T t : list) { if (t == element) { return true; } } return false; } public static int indexInListByRef(List list, T element) { if (list == null || list.isEmpty()) { return -1; } int size = list.size(); for (int i = 0; i < size; i++) { T t = list.get(i); if (t == element) { return i; } } return -1; } public static List lockList(List list) { if (list.isEmpty()) { return Collections.emptyList(); } if (list.size() == 1) { return Collections.singletonList(list.get(0)); } return new ImmutableList<>(list); } /** * Sub list from startIndex (inclusive) to list end */ public static List listTail(List list, int startIndex) { if (startIndex == 0) { return list; } int size = list.size(); if (startIndex >= size) { return Collections.emptyList(); } return list.subList(startIndex, size); } public static List mergeLists(List first, List second) { if (isEmpty(first)) { return second; } if (isEmpty(second)) { return first; } List result = new ArrayList<>(first.size() + second.size()); result.addAll(first); result.addAll(second); return result; } public static Set mergeSets(Set first, Set second) { if (isEmpty(first)) { return second; } if (isEmpty(second)) { return first; } Set result = new HashSet<>(first.size() + second.size()); result.addAll(first); result.addAll(second); return result; } public static Map newConstStringMap(String... parameters) { int len = parameters.length; if (len == 0) { return Collections.emptyMap(); } if (len % 2 != 0) { throw new IllegalArgumentException("Incorrect arguments count: " + len); } Map result = new HashMap<>(len / 2); for (int i = 0; i < len - 1; i += 2) { result.put(parameters[i], parameters[i + 1]); } return Collections.unmodifiableMap(result); } /** * Merge two maps. Return HashMap as result. Second map will override values from first map. */ public static Map mergeMaps(Map first, Map second) { if (isEmpty(first)) { return second; } if (isEmpty(second)) { return first; } Map result = new HashMap<>(first.size() + second.size()); result.putAll(first); result.putAll(second); return result; } /** * Build map from list of values with value to key mapping function *
* Similar to: *
* {@code list.stream().collect(Collectors.toMap(mapKey, Function.identity())); } */ public static Map groupBy(List list, Function mapKey) { Map map = new HashMap<>(list.size()); for (V v : list) { map.put(mapKey.apply(v), v); } return map; } /** * Simple DFS visit for tree (cycles not allowed) */ public static void treeDfsVisit(T root, Function> childrenProvider, Consumer visitor) { multiRootTreeDfsVisit(Collections.singletonList(root), childrenProvider, visitor); } public static void multiRootTreeDfsVisit(List roots, Function> childrenProvider, Consumer visitor) { Deque queue = new ArrayDeque<>(roots); while (true) { T current = queue.pollLast(); if (current == null) { return; } visitor.accept(current); for (T child : childrenProvider.apply(current)) { queue.addLast(child); } } } @Nullable public static T getOne(@Nullable List list) { if (list == null || list.size() != 1) { return null; } return list.get(0); } @Nullable public static T getOne(@Nullable Collection collection) { if (collection == null || collection.size() != 1) { return null; } return collection.iterator().next(); } public static boolean isSetContainsAny(Set inputSet, Set searchKeys) { for (T t : inputSet) { if (searchKeys.contains(t)) { return true; } } return false; } @Nullable public static T first(List list) { if (list.isEmpty()) { return null; } return list.get(0); } @Nullable public static T first(Iterable list) { Iterator it = list.iterator(); if (!it.hasNext()) { return null; } return it.next(); } @Nullable public static T last(List list) { if (list.isEmpty()) { return null; } return list.get(list.size() - 1); } @Nullable public static T last(Iterable list) { Iterator it = list.iterator(); if (!it.hasNext()) { return null; } while (true) { T next = it.next(); if (!it.hasNext()) { return next; } } } public static T getOrElse(@Nullable T obj, T defaultObj) { if (obj == null) { return defaultObj; } return obj; } public static boolean isEmpty(Collection col) { return col == null || col.isEmpty(); } public static boolean notEmpty(Collection col) { return col != null && !col.isEmpty(); } public static boolean isEmpty(Map map) { return map == null || map.isEmpty(); } public static boolean isEmpty(T[] arr) { return arr == null || arr.length == 0; } public static boolean notEmpty(T[] arr) { return arr != null && arr.length != 0; } public static void checkThreadInterrupt() { if (Thread.currentThread().isInterrupted()) { throw new JadxRuntimeException("Thread interrupted"); } } public static ThreadFactory simpleThreadFactory(String name) { return new SimpleThreadFactory(name); } private static final class SimpleThreadFactory implements ThreadFactory { private static final AtomicInteger POOL = new AtomicInteger(0); private static final Thread.UncaughtExceptionHandler EXC_HANDLER = new SimpleUncaughtExceptionHandler(); private final AtomicInteger number = new AtomicInteger(0); private final String name; public SimpleThreadFactory(String name) { this.name = name; } @Override public Thread newThread(@NotNull Runnable r) { Thread thread = new Thread(r, "jadx-" + name + '-' + POOL.incrementAndGet() + '-' + number.incrementAndGet()); thread.setUncaughtExceptionHandler(EXC_HANDLER); return thread; } private static class SimpleUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { private static final Logger LOG = LoggerFactory.getLogger(SimpleUncaughtExceptionHandler.class); @Override public void uncaughtException(Thread thread, Throwable e) { if (e instanceof OutOfMemoryError) { thread.interrupt(); LOG.error("OutOfMemoryError in thread: {}, forcing interrupt", thread.getName()); } else { LOG.error("Uncaught thread exception, thread: {}", thread.getName(), e); } } } } /** * @deprecated env vars shouldn't be used in core modules. * Prefer to parse in `app` (use JadxCommonEnv from 'app-commons') and set in jadx args. */ @Deprecated public static boolean getEnvVarBool(String varName, boolean defValue) { String strValue = System.getenv(varName); if (strValue == null) { return defValue; } return strValue.equalsIgnoreCase("true"); } /** * @deprecated env vars shouldn't be used in core modules. * Prefer to parse in `app` (use JadxCommonEnv from 'app-commons') and set in jadx args. */ @Deprecated public static int getEnvVarInt(String varName, int defValue) { String strValue = System.getenv(varName); if (strValue == null) { return defValue; } return Integer.parseInt(strValue); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/android/AndroidManifestParser.java ================================================ package jadx.core.utils.android; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.EnumSet; import java.util.List; import java.util.Objects; import org.jetbrains.annotations.Nullable; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.api.security.IJadxSecurity; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.ResContainer; public class AndroidManifestParser { private final Document androidManifest; private final @Nullable Document appStrings; private final EnumSet parseAttrs; private final IJadxSecurity security; public AndroidManifestParser(ResourceFile androidManifestRes, EnumSet parseAttrs, IJadxSecurity security) { this(androidManifestRes, null, parseAttrs, security); } public AndroidManifestParser(ResourceFile androidManifestRes, @Nullable ResContainer appStrings, EnumSet parseAttrs, IJadxSecurity security) { this.parseAttrs = parseAttrs; this.security = Objects.requireNonNull(security); this.androidManifest = parseAndroidManifest(androidManifestRes); this.appStrings = parseAppStrings(appStrings); } public boolean isManifestFound() { return androidManifest != null; } @Nullable public static ResourceFile getAndroidManifest(List resources) { return resources.stream() .filter(resourceFile -> resourceFile.getType() == ResourceType.MANIFEST) .findFirst() .orElse(null); } public ApplicationParams parse() { if (!isManifestFound()) { throw new JadxRuntimeException("AndroidManifest.xml is missing"); } return parseAttributes(); } private ApplicationParams parseAttributes() { String applicationLabel = null; Integer minSdkVersion = null; Integer targetSdkVersion = null; Integer compileSdkVersion = null; Integer versionCode = null; String versionName = null; String mainActivity = null; String application = null; @Nullable Element manifest = (Element) androidManifest.getElementsByTagName("manifest").item(0); @Nullable Element usesSdk = (Element) androidManifest.getElementsByTagName("uses-sdk").item(0); if (parseAttrs.contains(AppAttribute.APPLICATION_LABEL)) { applicationLabel = getApplicationLabel(); } if (usesSdk != null) { if (parseAttrs.contains(AppAttribute.MIN_SDK_VERSION)) { minSdkVersion = Integer.valueOf(usesSdk.getAttribute("android:minSdkVersion")); } if (parseAttrs.contains(AppAttribute.TARGET_SDK_VERSION)) { String stringTargetSdk = usesSdk.getAttribute("android:targetSdkVersion"); if (!stringTargetSdk.isEmpty()) { targetSdkVersion = Integer.valueOf(stringTargetSdk); } else { if (minSdkVersion == null) { minSdkVersion = Integer.valueOf(usesSdk.getAttribute("android:minSdkVersion")); } targetSdkVersion = minSdkVersion; } } if (parseAttrs.contains(AppAttribute.COMPILE_SDK_VERSION)) { String stringCompileSdk = usesSdk.getAttribute("android:compileSdkVersion"); if (!stringCompileSdk.isEmpty()) { compileSdkVersion = Integer.valueOf(stringCompileSdk); } else { compileSdkVersion = targetSdkVersion; } } } if (manifest != null) { if (parseAttrs.contains(AppAttribute.VERSION_CODE)) { versionCode = Integer.valueOf(manifest.getAttribute("android:versionCode")); } if (parseAttrs.contains(AppAttribute.VERSION_NAME)) { versionName = manifest.getAttribute("android:versionName"); } } if (parseAttrs.contains(AppAttribute.MAIN_ACTIVITY)) { mainActivity = getMainActivityName(); } if (parseAttrs.contains(AppAttribute.APPLICATION)) { application = getApplicationName(); } return new ApplicationParams(applicationLabel, minSdkVersion, targetSdkVersion, compileSdkVersion, versionCode, versionName, mainActivity, application); } private String getApplicationLabel() { Element application = (Element) androidManifest.getElementsByTagName("application").item(0); if (application.hasAttribute("android:label")) { String appLabelName = application.getAttribute("android:label"); if (appLabelName.startsWith("@string")) { if (appStrings == null) { throw new IllegalArgumentException("APPLICATION_LABEL attribute requires non null appStrings"); } appLabelName = appLabelName.split("/")[1]; NodeList strings = appStrings.getElementsByTagName("string"); for (int i = 0; i < strings.getLength(); i++) { String stringName = strings.item(i) .getAttributes() .getNamedItem("name") .getNodeValue(); if (stringName.equals(appLabelName)) { return strings.item(i).getTextContent(); } } } else { return appLabelName; } } return "UNKNOWN"; } private String getMainActivityName() { String mainActivityName = getMainActivityNameThroughActivityTag(); if (mainActivityName == null) { mainActivityName = getMainActivityNameThroughActivityAliasTag(); } return mainActivityName; } private String getApplicationName() { Element application = (Element) androidManifest.getElementsByTagName("application").item(0); if (application.hasAttribute("android:name")) { return application.getAttribute("android:name"); } return null; } private String getMainActivityNameThroughActivityAliasTag() { NodeList activityAliasNodes = androidManifest.getElementsByTagName("activity-alias"); for (int i = 0; i < activityAliasNodes.getLength(); i++) { Element activityElement = (Element) activityAliasNodes.item(i); if (isMainActivityElement(activityElement)) { return activityElement.getAttribute("android:targetActivity"); } } return null; } private String getMainActivityNameThroughActivityTag() { NodeList activityNodes = androidManifest.getElementsByTagName("activity"); for (int i = 0; i < activityNodes.getLength(); i++) { Element activityElement = (Element) activityNodes.item(i); if (isMainActivityElement(activityElement)) { return activityElement.getAttribute("android:name"); } } return null; } private boolean isMainActivityElement(Element element) { NodeList intentFilterNodes = element.getElementsByTagName("intent-filter"); for (int j = 0; j < intentFilterNodes.getLength(); j++) { Element intentFilterElement = (Element) intentFilterNodes.item(j); NodeList actionNodes = intentFilterElement.getElementsByTagName("action"); NodeList categoryNodes = intentFilterElement.getElementsByTagName("category"); boolean isMainAction = false; boolean isLauncherCategory = false; for (int k = 0; k < actionNodes.getLength(); k++) { Element actionElement = (Element) actionNodes.item(k); String actionName = actionElement.getAttribute("android:name"); if ("android.intent.action.MAIN".equals(actionName)) { isMainAction = true; break; } } for (int k = 0; k < categoryNodes.getLength(); k++) { Element categoryElement = (Element) categoryNodes.item(k); String categoryName = categoryElement.getAttribute("android:name"); if ("android.intent.category.LAUNCHER".equals(categoryName)) { isLauncherCategory = true; break; } } if (isMainAction && isLauncherCategory) { return true; } } return false; } private Document parseXml(String xmlContent) { try (InputStream xmlStream = new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8))) { Document document = security.parseXml(xmlStream); document.getDocumentElement().normalize(); return document; } catch (Exception e) { throw new JadxRuntimeException("Can not parse xml content", e); } } private Document parseAppStrings(ResContainer appStrings) { if (appStrings == null) { return null; } String content = appStrings.getText().getCodeStr(); return parseXml(content); } private Document parseAndroidManifest(ResourceFile androidManifest) { if (androidManifest == null) { return null; } String content = androidManifest.loadContent().getText().getCodeStr(); return parseXml(content); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/android/AndroidResourcesMap.java ================================================ package jadx.core.utils.android; import java.io.InputStream; import java.util.Map; import org.jetbrains.annotations.Nullable; import jadx.core.utils.exceptions.JadxRuntimeException; /** * Store resources id to name mapping */ public class AndroidResourcesMap { private static final Map RES_MAP = loadBundled(); public static @Nullable String getResName(int resId) { return RES_MAP.get(resId); } public static Map getMap() { return RES_MAP; } private static Map loadBundled() { try (InputStream is = AndroidResourcesMap.class.getResourceAsStream("/android/res-map.txt")) { return TextResMapFile.read(is); } catch (Exception e) { throw new JadxRuntimeException("Failed to load android resource file (res-map.txt)", e); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/android/AndroidResourcesUtils.java ================================================ package jadx.core.utils.android; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeWriter; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.core.codegen.ClassGen; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.ConstStorage; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.RootNode; import jadx.core.xmlgen.ResourceStorage; import jadx.core.xmlgen.entry.ResourceEntry; /** * Android resources specific handlers */ public class AndroidResourcesUtils { private static final Logger LOG = LoggerFactory.getLogger(AndroidResourcesUtils.class); private AndroidResourcesUtils() { } public static ClassNode searchAppResClass(RootNode root, ResourceStorage resStorage) { String appPackage = root.getAppPackage(); String fullName = appPackage != null ? appPackage + ".R" : "R"; ClassInfo clsInfo = ClassInfo.fromName(root, fullName); ClassNode resCls = root.resolveClass(clsInfo); if (resCls != null) { addResourceFields(resCls, resStorage, true); return resCls; } LOG.debug("Can't find 'R' class in app package: {}", appPackage); List candidates = root.searchClassByShortName("R"); if (candidates.size() == 1) { ClassNode resClsCandidate = candidates.get(0); addResourceFields(resClsCandidate, resStorage, true); return resClsCandidate; } if (!candidates.isEmpty()) { LOG.info("Found several 'R' class candidates: {}", candidates); } LOG.info("App 'R' class not found, put all resources ids into : '{}'", fullName); ClassNode rCls = ClassNode.addSyntheticClass(root, clsInfo, AccessFlags.PUBLIC | AccessFlags.FINAL); rCls.addInfoComment("This class is generated by JADX"); addResourceFields(rCls, resStorage, false); return rCls; } public static boolean handleAppResField(ICodeWriter code, ClassGen clsGen, ClassInfo declClass) { ClassInfo parentClass = declClass.getParentClass(); if (parentClass != null && parentClass.getShortName().equals("R")) { clsGen.useClass(code, parentClass); code.add('.'); code.add(declClass.getAliasShortName()); return true; } return false; } /** * Force hex format for Android resources ids */ public static boolean isResourceFieldValue(ClassNode cls, ArgType type) { return type.equals(ArgType.INT) && isResourceClass(cls); } public static boolean isResourceClass(ClassNode cls) { ClassNode parentClass = cls.getParentClass(); return parentClass != null && parentClass.getAlias().equals("R"); } private static final class ResClsInfo { private final ClassNode typeCls; private final Map fieldsMap = new HashMap<>(); private ResClsInfo(ClassNode typeCls) { this.typeCls = typeCls; } public ClassNode getTypeCls() { return typeCls; } public Map getFieldsMap() { return fieldsMap; } } private static void addResourceFields(ClassNode resCls, ResourceStorage resStorage, boolean rClsExists) { Map resFieldsMap = fillResFieldsMap(resCls); Map innerClsMap = new TreeMap<>(); if (rClsExists) { for (ClassNode innerClass : resCls.getInnerClasses()) { ResClsInfo innerResCls = new ResClsInfo(innerClass); innerClass.getFields().forEach(field -> innerResCls.getFieldsMap().put(field.getName(), field)); innerClsMap.put(innerClass.getAlias(), innerResCls); } } for (ResourceEntry resource : resStorage.getResources()) { String resTypeName = resource.getTypeName(); String resName = resource.getKeyName().replace('.', '_'); ResClsInfo typeClsInfo = innerClsMap.computeIfAbsent( resTypeName, name -> getClassForResType(resCls, rClsExists, name)); typeClsInfo.getFieldsMap().computeIfAbsent(resName, name -> { ClassNode typeCls = typeClsInfo.getTypeCls(); FieldInfo rFieldInfo = FieldInfo.from(typeCls.root(), typeCls.getClassInfo(), resName, ArgType.INT); FieldNode newResField = new FieldNode(typeCls, rFieldInfo, AccessFlags.PUBLIC | AccessFlags.STATIC | AccessFlags.FINAL); newResField.addAttr(new EncodedValue(EncodedType.ENCODED_INT, resource.getId())); typeCls.addField(newResField); if (rClsExists) { newResField.addInfoComment("Added by JADX"); } return newResField; }); FieldNode fieldNode = resFieldsMap.get(resource.getId()); if (fieldNode != null && !fieldNode.getName().equals(resName) && NameMapper.isValidAndPrintable(resName) && resCls.root().getArgs().isRenameValid()) { fieldNode.add(AFlag.DONT_RENAME); fieldNode.getFieldInfo().setAlias(resName); } } } private static ResClsInfo getClassForResType(ClassNode resCls, boolean rClsExists, String typeName) { RootNode root = resCls.root(); String typeClsFullName = resCls.getClassInfo().makeRawFullName() + '$' + typeName; ClassInfo clsInfo = ClassInfo.fromName(root, typeClsFullName); ClassNode existCls = root.resolveClass(clsInfo); if (existCls != null) { ResClsInfo resClsInfo = new ResClsInfo(existCls); existCls.getFields().forEach(field -> resClsInfo.getFieldsMap().put(field.getName(), field)); return resClsInfo; } ClassNode newTypeCls = ClassNode.addSyntheticClass(root, clsInfo, AccessFlags.PUBLIC | AccessFlags.STATIC | AccessFlags.FINAL); if (rClsExists) { newTypeCls.addInfoComment("Added by JADX"); } return new ResClsInfo(newTypeCls); } @NotNull private static Map fillResFieldsMap(ClassNode resCls) { Map resFieldsMap = new HashMap<>(); ConstStorage constStorage = resCls.root().getConstValues(); constStorage.getGlobalConstFields().forEach((key, field) -> { if (field.getFieldInfo().getType().equals(ArgType.INT) && field instanceof FieldNode && key instanceof Integer) { FieldNode fldNode = (FieldNode) field; AccessInfo accessFlags = fldNode.getAccessFlags(); if (accessFlags.isStatic() && accessFlags.isFinal()) { resFieldsMap.put((Integer) key, fldNode); } } }); return resFieldsMap; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/android/AppAttribute.java ================================================ package jadx.core.utils.android; public enum AppAttribute { APPLICATION_LABEL, MIN_SDK_VERSION, COMPILE_SDK_VERSION, TARGET_SDK_VERSION, VERSION_CODE, VERSION_NAME, MAIN_ACTIVITY, APPLICATION, } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/android/ApplicationParams.java ================================================ package jadx.core.utils.android; import jadx.api.JadxDecompiler; import jadx.api.JavaClass; public class ApplicationParams { private final String applicationLabel; private final Integer minSdkVersion; private final Integer targetSdkVersion; private final Integer compileSdkVersion; private final Integer versionCode; private final String versionName; private final String mainActivity; private final String application; public ApplicationParams(String applicationLabel, Integer minSdkVersion, Integer targetSdkVersion, Integer compileSdkVersion, Integer versionCode, String versionName, String mainActivity, String application) { this.applicationLabel = applicationLabel; this.minSdkVersion = minSdkVersion; this.targetSdkVersion = targetSdkVersion; this.compileSdkVersion = compileSdkVersion; this.versionCode = versionCode; this.versionName = versionName; this.mainActivity = mainActivity; this.application = application; } public String getApplicationName() { return applicationLabel; } public Integer getMinSdkVersion() { return minSdkVersion; } public Integer getTargetSdkVersion() { return targetSdkVersion; } public Integer getCompileSdkVersion() { return compileSdkVersion; } public Integer getVersionCode() { return versionCode; } public String getVersionName() { return versionName; } public String getMainActivity() { return mainActivity; } public JavaClass getMainActivityJavaClass(JadxDecompiler decompiler) { return decompiler.searchJavaClassByAliasFullName(mainActivity); } public String getApplication() { return application; } public JavaClass getApplicationJavaClass(JadxDecompiler decompiler) { return decompiler.searchJavaClassByAliasFullName(application); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/android/DataInputDelegate.java ================================================ /** * Copyright 2014 Ryszard Wiśniewski *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jadx.core.utils.android; import java.io.DataInput; import java.io.IOException; /** * @author Ryszard Wiśniewski "brut.alll@gmail.com" */ public abstract class DataInputDelegate implements DataInput { protected final DataInput mDelegate; public DataInputDelegate(DataInput delegate) { this.mDelegate = delegate; } public int skipBytes(int n) throws IOException { return mDelegate.skipBytes(n); } public int readUnsignedShort() throws IOException { return mDelegate.readUnsignedShort(); } public int readUnsignedByte() throws IOException { return mDelegate.readUnsignedByte(); } public String readUTF() throws IOException { return mDelegate.readUTF(); } public short readShort() throws IOException { return mDelegate.readShort(); } public long readLong() throws IOException { return mDelegate.readLong(); } public String readLine() throws IOException { return mDelegate.readLine(); } public int readInt() throws IOException { return mDelegate.readInt(); } public void readFully(byte[] b, int off, int len) throws IOException { mDelegate.readFully(b, off, len); } public void readFully(byte[] b) throws IOException { mDelegate.readFully(b); } public float readFloat() throws IOException { return mDelegate.readFloat(); } public double readDouble() throws IOException { return mDelegate.readDouble(); } public char readChar() throws IOException { return mDelegate.readChar(); } public byte readByte() throws IOException { return mDelegate.readByte(); } public boolean readBoolean() throws IOException { return mDelegate.readBoolean(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/android/ExtDataInput.java ================================================ /** * Copyright 2014 Ryszard Wiśniewski *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jadx.core.utils.android; import java.io.DataInput; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; /** * @author Ryszard Wiśniewski "brut.alll@gmail.com" */ public class ExtDataInput extends DataInputDelegate { public ExtDataInput(InputStream in) { this((DataInput) new DataInputStream(in)); } public ExtDataInput(DataInput delegate) { super(delegate); } public int[] readIntArray(int length) throws IOException { int[] array = new int[length]; for (int i = 0; i < length; i++) { array[i] = readInt(); } return array; } public void skipInt() throws IOException { skipBytes(4); } public void skipCheckInt(int expected) throws IOException { int got = readInt(); if (got != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); } } public void skipCheckShort(short expected) throws IOException { short got = readShort(); if (got != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); } } public void skipCheckByte(byte expected) throws IOException { byte got = readByte(); if (got != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); } } public void skipCheckChunkTypeInt(int expected, int possible) throws IOException { int got = readInt(); if (got == possible) { skipCheckChunkTypeInt(expected, -1); } else if (got != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); } } /** * The general contract of DataInput doesn't guarantee all the bytes requested will be skipped * and failure can occur for many reasons. We override this to try harder to skip all the bytes * requested (this is similar to DataInputStream's wrapper). */ @Override @SuppressWarnings("InnerAssignment") public final int skipBytes(int n) throws IOException { int total = 0; int cur; while ((total < n) && ((cur = super.skipBytes(n - total)) > 0)) { total += cur; } return total; } public String readNullEndedString(int length, boolean fixed) throws IOException { StringBuilder string = new StringBuilder(16); while (length-- != 0) { short ch = readShort(); if (ch == 0) { break; } string.append((char) ch); } if (fixed) { skipBytes(length * 2); } return string.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/android/Res9patchStreamDecoder.java ================================================ /** * Copyright 2014 Ryszard Wiśniewski *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jadx.core.utils.android; import java.awt.image.BufferedImage; import java.io.DataInput; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.imageio.ImageIO; import org.jetbrains.annotations.Nullable; import jadx.core.utils.exceptions.JadxRuntimeException; /** * @author Ryszard Wiśniewski "brut.alll@gmail.com" */ public class Res9patchStreamDecoder { public boolean decode(InputStream in, OutputStream out) { try { BufferedImage im = ImageIO.read(in); NinePatch np = getNinePatch(in); if (np == null) { return false; } int w = im.getWidth(); int h = im.getHeight(); BufferedImage im2 = new BufferedImage(w + 2, h + 2, BufferedImage.TYPE_INT_ARGB); im2.createGraphics().drawImage(im, 1, 1, w, h, null); drawHLine(im2, h + 1, np.padLeft + 1, w - np.padRight); drawVLine(im2, w + 1, np.padTop + 1, h - np.padBottom); int[] xDivs = np.xDivs; for (int i = 0; i < xDivs.length - 1; i += 2) { drawHLine(im2, 0, xDivs[i] + 1, xDivs[i + 1]); } int[] yDivs = np.yDivs; for (int i = 0; i < yDivs.length - 1; i += 2) { drawVLine(im2, 0, yDivs[i] + 1, yDivs[i + 1]); } ImageIO.write(im2, "png", out); return true; } catch (Exception e) { throw new JadxRuntimeException("9patch image decode error", e); } } @Nullable private NinePatch getNinePatch(InputStream in) throws IOException { ExtDataInput di = new ExtDataInput(in); if (!find9patchChunk(di)) { return null; } return NinePatch.decode(di); } private boolean find9patchChunk(DataInput di) throws IOException { di.skipBytes(8); while (true) { int size; try { size = di.readInt(); } catch (IOException ex) { return false; } if (di.readInt() == NP_CHUNK_TYPE) { return true; } di.skipBytes(size + 4); } } private void drawHLine(BufferedImage im, int y, int x1, int x2) { for (int x = x1; x <= x2; x++) { im.setRGB(x, y, NP_COLOR); } } private void drawVLine(BufferedImage im, int x, int y1, int y2) { for (int y = y1; y <= y2; y++) { im.setRGB(x, y, NP_COLOR); } } private static final int NP_CHUNK_TYPE = 0x6e705463; // npTc private static final int NP_COLOR = 0xff000000; private static class NinePatch { public final int padLeft; public final int padRight; public final int padTop; public final int padBottom; public final int[] xDivs; public final int[] yDivs; public NinePatch(int padLeft, int padRight, int padTop, int padBottom, int[] xDivs, int[] yDivs) { this.padLeft = padLeft; this.padRight = padRight; this.padTop = padTop; this.padBottom = padBottom; this.xDivs = xDivs; this.yDivs = yDivs; } public static NinePatch decode(ExtDataInput di) throws IOException { di.skipBytes(1); byte numXDivs = di.readByte(); byte numYDivs = di.readByte(); di.skipBytes(1); di.skipBytes(8); int padLeft = di.readInt(); int padRight = di.readInt(); int padTop = di.readInt(); int padBottom = di.readInt(); di.skipBytes(4); int[] xDivs = di.readIntArray(numXDivs); int[] yDivs = di.readIntArray(numYDivs); return new NinePatch(padLeft, padRight, padTop, padBottom, xDivs, yDivs); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/android/TextResMapFile.java ================================================ package jadx.core.utils.android; import java.io.BufferedReader; 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.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import jadx.core.utils.exceptions.JadxRuntimeException; public class TextResMapFile { private static final int SPLIT_POS = 8; public static Map read(InputStream is) { try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { Map resMap = new HashMap<>(); while (true) { String line = br.readLine(); if (line == null) { break; } parseLine(resMap, line); } return resMap; } catch (Exception e) { throw new JadxRuntimeException("Failed to read res-map file", e); } } private static void parseLine(Map resMap, String line) { int id = Integer.parseInt(line.substring(0, SPLIT_POS), 16); String name = line.substring(SPLIT_POS + 1); resMap.put(id, name); } public static Map read(Path resMapFile) { try (InputStream in = Files.newInputStream(resMapFile)) { return read(in); } catch (Exception e) { throw new JadxRuntimeException("Failed to read res-map file", e); } } public static void write(Path resMapFile, Map inputResMap) { try { Map resMap = new TreeMap<>(inputResMap); List lines = new ArrayList<>(resMap.size()); for (Map.Entry entry : resMap.entrySet()) { lines.add(String.format("%08x=%s", entry.getKey(), entry.getValue())); } Files.write(resMapFile, lines, StandardCharsets.UTF_8); } catch (Exception e) { throw new JadxRuntimeException("Failed to write res-map file", e); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/blocks/BlockPair.java ================================================ package jadx.core.utils.blocks; import jadx.core.dex.nodes.BlockNode; public class BlockPair { private final BlockNode first; private final BlockNode second; public BlockPair(BlockNode first, BlockNode second) { this.first = first; this.second = second; } public BlockNode getFirst() { return first; } public BlockNode getSecond() { return second; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof BlockPair)) { return false; } BlockPair other = (BlockPair) o; return first.equals(other.first) && second.equals(other.second); } @Override public int hashCode() { return first.hashCode() + 31 * second.hashCode(); } @Override public String toString() { return "(" + first + ", " + second + ')'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/blocks/BlockSet.java ================================================ package jadx.core.utils.blocks; import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Spliterator; import java.util.Spliterators; import java.util.function.Consumer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.EmptyBitSet; /** * BlockNode set implementation based on BitSet. */ public class BlockSet implements Iterable { public static BlockSet empty(MethodNode mth) { return new BlockSet(mth); } public static BlockSet from(MethodNode mth, Collection blocks) { BlockSet newBS = new BlockSet(mth); newBS.addAll(blocks); return newBS; } private final MethodNode mth; private final BitSet bs; public BlockSet(MethodNode mth) { this.mth = mth; this.bs = new BitSet(mth.getBasicBlocks().size()); } public boolean contains(BlockNode block) { return bs.get(block.getPos()); } public void add(BlockNode block) { bs.set(block.getPos()); } public void addAll(Collection blocks) { blocks.forEach(this::add); } public void addAll(BlockSet otherBlockSet) { bs.or(otherBlockSet.bs); } public void remove(BlockNode block) { bs.clear(block.getPos()); } public void remove(Collection blocks) { blocks.forEach(this::remove); } public boolean addChecked(BlockNode block) { int id = block.getPos(); boolean state = bs.get(id); bs.set(id); return state; } public boolean containsAll(List blocks) { for (BlockNode block : blocks) { if (!contains(block)) { return false; } } return true; } public boolean intersects(List blocks) { for (BlockNode block : blocks) { if (contains(block)) { return true; } } return false; } public BlockSet intersect(List blocks) { BlockSet input = from(mth, blocks); BlockSet result = new BlockSet(mth); BitSet resultBS = result.bs; resultBS.or(this.bs); resultBS.and(input.bs); return result; } public boolean isEmpty() { return bs.isEmpty(); } public int size() { return bs.cardinality(); } public void remove() { bs.clear(); } public @Nullable BlockNode getOne() { if (bs.cardinality() == 1) { return mth.getBasicBlocks().get(bs.nextSetBit(0)); } return null; } public BlockNode getFirst() { return mth.getBasicBlocks().get(bs.nextSetBit(0)); } @Override public void forEach(Consumer consumer) { if (bs.isEmpty()) { return; } List blocks = mth.getBasicBlocks(); for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) { consumer.accept(blocks.get(i)); } } @Override public @NotNull Iterator iterator() { return new BlockSetIterator(bs, size(), mth.getBasicBlocks()); } @Override public Spliterator spliterator() { int size = size(); BlockSetIterator iterator = new BlockSetIterator(bs, size, mth.getBasicBlocks()); return Spliterators.spliterator(iterator, size, Spliterator.ORDERED | Spliterator.DISTINCT); } public List toList() { if (bs == null || bs == EmptyBitSet.EMPTY) { return Collections.emptyList(); } int size = bs.cardinality(); if (size == 0) { return Collections.emptyList(); } List mthBlocks = mth.getBasicBlocks(); List blocks = new ArrayList<>(size); for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) { blocks.add(mthBlocks.get(i)); } return blocks; } @Override public String toString() { return toList().toString(); } private static final class BlockSetIterator implements Iterator { private final BitSet bs; private final int size; private final List blocks; private int cursor; private int start; public BlockSetIterator(BitSet bs, int size, List blocks) { this.bs = bs; this.size = size; this.blocks = blocks; } @Override public boolean hasNext() { return cursor != size; } @Override public BlockNode next() { int pos = bs.nextSetBit(start); if (pos == -1) { throw new NoSuchElementException(); } start = pos + 1; cursor++; return blocks.get(pos); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/blocks/DFSIteration.java ================================================ package jadx.core.utils.blocks; import java.util.ArrayDeque; import java.util.Deque; import java.util.List; import java.util.function.Function; import org.jetbrains.annotations.Nullable; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; public class DFSIteration { private final Function> nextFunc; private final Deque queue; private final BlockSet visited; public DFSIteration(MethodNode mth, BlockNode startBlock, Function> next) { nextFunc = next; queue = new ArrayDeque<>(); visited = new BlockSet(mth); queue.addLast(startBlock); visited.add(startBlock); } public @Nullable BlockNode next() { BlockNode current = queue.pollLast(); if (current == null) { return null; } List nextBlocks = nextFunc.apply(current); int count = nextBlocks.size(); for (int i = count - 1; i >= 0; i--) { // to preserve order in queue BlockNode next = nextBlocks.get(i); if (!visited.addChecked(next)) { queue.addLast(next); } } return current; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/exceptions/CodegenException.java ================================================ package jadx.core.utils.exceptions; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; public class CodegenException extends JadxException { private static final long serialVersionUID = 39344288912966824L; public CodegenException(String message) { super(message); } public CodegenException(String message, Throwable cause) { super(message, cause); } public CodegenException(ClassNode mth, String msg) { super(mth, msg, null); } public CodegenException(ClassNode mth, String msg, Throwable th) { super(mth, msg, th); } public CodegenException(MethodNode mth, String msg) { super(mth, msg, null); } public CodegenException(MethodNode mth, String msg, Throwable th) { super(mth, msg, th); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/exceptions/DecodeException.java ================================================ package jadx.core.utils.exceptions; import jadx.core.dex.nodes.MethodNode; public class DecodeException extends JadxException { private static final long serialVersionUID = -6611189094923499636L; public DecodeException(String message) { super(message); } public DecodeException(String message, Throwable cause) { super(message, cause); } public DecodeException(MethodNode mth, String msg) { super(mth, msg, null); } public DecodeException(MethodNode mth, String msg, Throwable th) { super(mth, msg, th); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/exceptions/InvalidDataException.java ================================================ package jadx.core.utils.exceptions; public class InvalidDataException extends JadxRuntimeException { public InvalidDataException(String message) { super(message); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/exceptions/JadxArgsValidateException.java ================================================ package jadx.core.utils.exceptions; public class JadxArgsValidateException extends RuntimeException { private static final long serialVersionUID = -7457621776087311909L; public JadxArgsValidateException(String message) { super(message); } public JadxArgsValidateException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/exceptions/JadxException.java ================================================ package jadx.core.utils.exceptions; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.ErrorsCounter; public class JadxException extends Exception { private static final long serialVersionUID = 3577449089978463557L; public JadxException(String message) { super(message); } public JadxException(String message, Throwable cause) { super(message, cause); } public JadxException(ClassNode cls, String msg, Throwable th) { super(ErrorsCounter.formatMsg(cls, msg), th); } public JadxException(MethodNode mth, String msg, Throwable th) { super(ErrorsCounter.formatMsg(mth, msg), th); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/exceptions/JadxOverflowException.java ================================================ package jadx.core.utils.exceptions; public class JadxOverflowException extends JadxRuntimeException { private static final long serialVersionUID = 2568659798680154204L; public JadxOverflowException(String message) { super(message); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/exceptions/JadxRuntimeException.java ================================================ package jadx.core.utils.exceptions; public class JadxRuntimeException extends RuntimeException { private static final long serialVersionUID = -7410848445429898248L; public JadxRuntimeException() { super(); } public JadxRuntimeException(String message) { super(message); } public JadxRuntimeException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java ================================================ package jadx.core.utils.files; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.function.Predicate; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.plugins.files.IJadxFilesGetter; import jadx.core.utils.ListUtils; import jadx.core.utils.exceptions.JadxRuntimeException; public class FileUtils { private static final Logger LOG = LoggerFactory.getLogger(FileUtils.class); public static final int READ_BUFFER_SIZE = 8 * 1024; private static final int MAX_FILENAME_LENGTH = 128; private static final int MAX_UNIQUE_ID_LENGTH = 3; public static final String JADX_TMP_INSTANCE_PREFIX = "jadx-instance-"; public static final String JADX_TMP_PREFIX = "jadx-tmp-"; private static Path tempRootDir = createTempRootDir(); private FileUtils() { // utility class } public static synchronized Path updateTempRootDir(Path newTempRootDir) { try { makeDirs(newTempRootDir); Path dir = Files.createTempDirectory(newTempRootDir, JADX_TMP_INSTANCE_PREFIX); tempRootDir = dir; dir.toFile().deleteOnExit(); return dir; } catch (Exception e) { throw new JadxRuntimeException("Failed to update temp root directory", e); } } private static Path createTempRootDir() { try { Path dir = Files.createTempDirectory(JADX_TMP_INSTANCE_PREFIX); dir.toFile().deleteOnExit(); return dir; } catch (Exception e) { throw new JadxRuntimeException("Failed to create temp root directory", e); } } public static List listFiles(Path dir) { try (Stream files = Files.list(dir)) { return files.collect(Collectors.toList()); } catch (IOException e) { throw new JadxRuntimeException("Failed to list files in directory: " + dir, e); } } public static List listFiles(Path dir, Predicate filter) { try (Stream files = Files.list(dir)) { return files.filter(filter).collect(Collectors.toList()); } catch (IOException e) { throw new JadxRuntimeException("Failed to list files in directory: " + dir, e); } } public static List expandDirs(List paths) { List files = new ArrayList<>(paths.size()); for (Path path : paths) { if (Files.isDirectory(path)) { expandDir(path, files); } else { files.add(path); } } return files; } private static void expandDir(Path dir, List files) { try (Stream walk = Files.walk(dir, FileVisitOption.FOLLOW_LINKS)) { walk.filter(Files::isRegularFile).forEach(files::add); } catch (Exception e) { LOG.error("Failed to list files in directory: {}", dir, e); } } public static void addFileToJar(JarOutputStream jar, File source, String entryName) throws IOException { try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(source))) { JarEntry entry = new JarEntry(entryName); entry.setTime(source.lastModified()); jar.putNextEntry(entry); copyStream(in, jar); jar.closeEntry(); } } public static void makeDirsForFile(Path path) { if (path != null) { makeDirs(path.toAbsolutePath().getParent().toFile()); } } public static void makeDirsForFile(File file) { if (file != null) { makeDirs(file.getParentFile()); } } private static final Object MKDIR_SYNC = new Object(); public static void makeDirs(@Nullable File dir) { if (dir != null) { synchronized (MKDIR_SYNC) { if (!dir.mkdirs() && !dir.isDirectory()) { throw new JadxRuntimeException("Can't create directory " + dir); } } } } public static void makeDirs(@Nullable Path dir) { if (dir != null) { makeDirs(dir.toFile()); } } public static void deleteFileIfExists(Path filePath) throws IOException { Files.deleteIfExists(filePath); } public static boolean deleteDir(File dir) { deleteDir(dir.toPath()); return true; } public static void deleteDir(Path dir) { deleteDir(dir, false); } public static void deleteDirIfExists(Path dir) { if (Files.exists(dir)) { try { deleteDir(dir); } catch (Exception e) { LOG.error("Failed to delete dir: {}", dir.toAbsolutePath(), e); } } } private static void deleteDir(Path dir, boolean keepRootDir) { try { List files = new ArrayList<>(); List directories = new ArrayList<>(); Files.walkFileTree(dir, Collections.emptySet(), Integer.MAX_VALUE, new SimpleFileVisitor<>() { @Override public @NotNull FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) { files.add(file); return FileVisitResult.CONTINUE; } @Override public @NotNull FileVisitResult postVisitDirectory(@NotNull Path directory, IOException exc) { directories.add(directory); return FileVisitResult.CONTINUE; } }); // delete files in parallel if (!files.isEmpty()) { files.parallelStream().forEach(path -> { try { Files.delete(path); } catch (Exception e) { LOG.warn("Failed to delete file {}", path.toAbsolutePath(), e); } }); } // after all files are deleted, remove empty directories if (keepRootDir) { // root dir always last ListUtils.removeLast(directories); } for (Path directory : directories) { try { Files.delete(directory); } catch (IOException e) { LOG.warn("Failed to delete directory {}", directory.toAbsolutePath(), e); } } } catch (Exception e) { throw new JadxRuntimeException("Failed to delete directory " + dir, e); } } public static void clearTempRootDir() { if (Files.isDirectory(tempRootDir)) { clearDir(tempRootDir); } } public static void clearDir(Path clearDir) { try { deleteDir(clearDir, true); } catch (Exception e) { throw new JadxRuntimeException("Failed to clear directory " + clearDir, e); } } /** * Deprecated. * Migrate to {@link IJadxFilesGetter} from jadx args to get temp dir */ @Deprecated public static Path createTempDir(String prefix) { try { Path dir = Files.createTempDirectory(tempRootDir, prefix); dir.toFile().deleteOnExit(); return dir; } catch (Exception e) { throw new JadxRuntimeException("Failed to create temp directory with suffix: " + prefix, e); } } /** * Deprecated. * Migrate to {@link IJadxFilesGetter} from jadx args to get temp dir */ @Deprecated public static Path createTempFile(String suffix) { try { Path path = Files.createTempFile(tempRootDir, JADX_TMP_PREFIX, suffix); path.toFile().deleteOnExit(); return path; } catch (Exception e) { throw new JadxRuntimeException("Failed to create temp file with suffix: " + suffix, e); } } /** * Deprecated. * Prefer {@link IJadxFilesGetter} from jadx args to get temp dir */ @Deprecated public static Path createTempFileNoDelete(String suffix) { try { return Files.createTempFile(Files.createTempDirectory("jadx-persist"), "jadx-", suffix); } catch (Exception e) { throw new JadxRuntimeException("Failed to create temp file with suffix: " + suffix, e); } } /** * Deprecated. * Migrate to {@link IJadxFilesGetter} from jadx args to get temp dir */ @Deprecated public static Path createTempFileNonPrefixed(String fileName) { try { Path path = Files.createFile(tempRootDir.resolve(fileName)); path.toFile().deleteOnExit(); return path; } catch (Exception e) { throw new JadxRuntimeException("Failed to create non-prefixed temp file: " + fileName, e); } } public static void copyStream(InputStream input, OutputStream output) throws IOException { byte[] buffer = new byte[READ_BUFFER_SIZE]; while (true) { int count = input.read(buffer); if (count == -1) { break; } output.write(buffer, 0, count); } } public static byte[] streamToByteArray(InputStream input) throws IOException { return input.readAllBytes(); } public static String streamToString(InputStream input) throws IOException { return new String(streamToByteArray(input), StandardCharsets.UTF_8); } public static void close(Closeable c) { if (c == null) { return; } try { c.close(); } catch (IOException e) { LOG.error("Close exception for {}", c, e); } } public static void writeFile(Path file, String data) throws IOException { FileUtils.makeDirsForFile(file); Files.writeString(file, data, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } public static void writeFile(Path file, byte[] data) throws IOException { FileUtils.makeDirsForFile(file); Files.write(file, data, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } public static void writeFile(Path file, InputStream is) throws IOException { FileUtils.makeDirsForFile(file); Files.copy(is, file, StandardCopyOption.REPLACE_EXISTING); } public static String readFile(Path textFile) throws IOException { return Files.readString(textFile); } public static boolean renameFile(Path sourcePath, Path targetPath) { try { Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); return true; } catch (NoSuchFileException e) { LOG.error("File to rename not found {}", sourcePath, e); } catch (FileAlreadyExistsException e) { LOG.error("File with that name already exists {}", targetPath, e); } catch (IOException e) { LOG.error("Error renaming file {}", e.getMessage(), e); } return false; } @NotNull public static File prepareFile(File file) { File saveFile = cutFileName(file); makeDirsForFile(saveFile); return saveFile; } public static File cutFileName(File file) { String name = file.getName(); if (name.length() <= MAX_FILENAME_LENGTH) { return file; } String uniqueID = String.valueOf(name.hashCode()); if (uniqueID.length() > MAX_UNIQUE_ID_LENGTH) { uniqueID = uniqueID.substring(0, MAX_UNIQUE_ID_LENGTH); } int dotIndex = name.indexOf('.'); int lengthOfSuffix = name.length() - dotIndex; int cutAt = MAX_FILENAME_LENGTH - lengthOfSuffix - uniqueID.length() - 1; if (cutAt <= 0) { name = name.substring(0, MAX_FILENAME_LENGTH - 1); } else { name = name.substring(0, cutAt) + uniqueID + name.substring(dotIndex); } return new File(file.getParentFile(), name); } private static final byte[] HEX_ARRAY = "0123456789abcdef".getBytes(StandardCharsets.US_ASCII); public static String bytesToHex(byte[] bytes) { if (bytes == null || bytes.length == 0) { return ""; } byte[] hexChars = new byte[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = HEX_ARRAY[v >>> 4]; hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; } return new String(hexChars, StandardCharsets.UTF_8); } /** * Zero padded hex string for first byte */ public static String byteToHex(int value) { int v = value & 0xFF; byte[] hexChars = new byte[] { HEX_ARRAY[v >>> 4], HEX_ARRAY[v & 0x0F] }; return new String(hexChars, StandardCharsets.US_ASCII); } /** * Zero padded hex string for int value */ public static String intToHex(int value) { byte[] hexChars = new byte[8]; int v = value; for (int i = 7; i >= 0; i--) { hexChars[i] = HEX_ARRAY[v & 0x0F]; v >>>= 4; } return new String(hexChars, StandardCharsets.US_ASCII); } private static final byte[] ZIP_FILE_MAGIC = { 0x50, 0x4B, 0x03, 0x04 }; public static boolean isZipFile(File file) { try (InputStream is = new FileInputStream(file)) { int len = ZIP_FILE_MAGIC.length; byte[] headers = new byte[len]; int read = is.read(headers); return read == len && Arrays.equals(headers, ZIP_FILE_MAGIC); } catch (Exception e) { LOG.error("Failed to read zip file: {}", file.getAbsolutePath(), e); return false; } } public static String getPathBaseName(Path file) { String fileName = file.getFileName().toString(); int extEndIndex = fileName.lastIndexOf('.'); if (extEndIndex == -1) { return fileName; } return fileName.substring(0, extEndIndex); } public static boolean hasExtension(Path path, String extension) { String fileName = path.getFileName().toString(); return fileName.toLowerCase().endsWith(extension); } public static File toFile(String path) { if (path == null) { return null; } return new File(path); } public static List toPaths(List files) { return files.stream().map(File::toPath).collect(Collectors.toList()); } public static List toPaths(File[] files) { return Stream.of(files).map(File::toPath).collect(Collectors.toList()); } public static List toPathsWithTrim(File[] files) { return Stream.of(files).map(FileUtils::toPathWithTrim).collect(Collectors.toList()); } public static Path toPathWithTrim(File file) { return toPathWithTrim(file.getPath()); } public static Path toPathWithTrim(String file) { return Path.of(file.trim()); } public static List fileNamesToPaths(List fileNames) { return fileNames.stream().map(Paths::get).collect(Collectors.toList()); } public static List toFiles(List paths) { return paths.stream().map(Path::toFile).collect(Collectors.toList()); } public static String md5Sum(String str) { return md5Sum(str.getBytes(StandardCharsets.UTF_8)); } public static String md5Sum(byte[] data) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(data); return bytesToHex(md.digest()); } catch (Exception e) { throw new JadxRuntimeException("Failed to build hash", e); } } /** * Hash timestamps of input files */ public static String buildInputsHash(List inputPaths) { try (ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream data = new DataOutputStream(bout)) { List inputFiles = FileUtils.expandDirs(inputPaths); Collections.sort(inputFiles); data.write(inputPaths.size()); data.write(inputFiles.size()); for (Path inputFile : inputFiles) { FileTime modifiedTime = Files.getLastModifiedTime(inputFile); data.writeLong(modifiedTime.toMillis()); } return FileUtils.md5Sum(bout.toByteArray()); } catch (Exception e) { throw new JadxRuntimeException("Failed to build hash for inputs", e); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/input/InsnDataUtils.java ================================================ package jadx.core.utils.input; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ICallSite; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.insns.InsnData; import jadx.api.plugins.input.insns.InsnIndexType; import jadx.api.plugins.input.insns.custom.ICustomPayload; public class InsnDataUtils { @Nullable public static ICallSite getCallSite(InsnData insnData) { if (insnData.getIndexType() != InsnIndexType.CALL_SITE) { return null; } ICustomPayload payload = insnData.getPayload(); if (payload != null) { return (ICallSite) payload; } return insnData.getIndexAsCallSite(); } @Nullable public static IMethodRef getMethodRef(InsnData insnData) { if (insnData.getIndexType() != InsnIndexType.METHOD_REF) { return null; } ICustomPayload payload = insnData.getPayload(); if (payload != null) { return (IMethodRef) payload; } return insnData.getIndexAsMethod(); } @Nullable public static IMethodHandle getMethodHandleAt(ICallSite callSite, int argNum) { if (callSite == null) { return null; } List values = callSite.getValues(); if (argNum < values.size()) { EncodedValue encodedValue = values.get(argNum); if (encodedValue.getType() == EncodedType.ENCODED_METHOD_HANDLE) { return (IMethodHandle) encodedValue.getValue(); } } return null; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/log/LogUtils.java ================================================ package jadx.core.utils.log; import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; /** * Escape input from untrusted source before pass to logger. * Suggested by CodeQL: https://codeql.github.com/codeql-query-help/java/java-log-injection/ */ public class LogUtils { /** * We replace everything except alphanumeric characters, underscore, dots, colon, semicolon, comma, * spaces, minus */ private static final Pattern REPLACE_PATTERN = Pattern.compile("[^\\w\\.:;, -]"); public static String escape(String input) { if (input == null) { return "null"; } return REPLACE_PATTERN.matcher(input).replaceAll("."); } public static String escape(byte[] input) { if (input == null) { return "null"; } return escape(new String(input, StandardCharsets.UTF_8)); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/utils/tasks/TaskExecutor.java ================================================ package jadx.core.utils.tasks; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.api.utils.tasks.ITaskExecutor; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; public class TaskExecutor implements ITaskExecutor { private static final Logger LOG = LoggerFactory.getLogger(TaskExecutor.class); private enum ExecType { PARALLEL, SEQUENTIAL, } private static final class ExecStage { private final ExecType type; private final List tasks; private ExecStage(ExecType type, List tasks) { this.type = type; this.tasks = tasks; } public ExecType getType() { return type; } public List getTasks() { return tasks; } } private final List stages = new ArrayList<>(); private final AtomicInteger threadsCount = new AtomicInteger(JadxArgs.DEFAULT_THREADS_COUNT); private final AtomicInteger progress = new AtomicInteger(0); private final AtomicBoolean running = new AtomicBoolean(false); private final AtomicBoolean terminating = new AtomicBoolean(false); private final Object executorSync = new Object(); private @Nullable ExecutorService executor; private int tasksCount = 0; private @Nullable Error terminateError; @Override public void addParallelTasks(List parallelTasks) { if (parallelTasks.isEmpty()) { return; } tasksCount += parallelTasks.size(); stages.add(new ExecStage(ExecType.PARALLEL, parallelTasks)); } @Override public void addSequentialTasks(List seqTasks) { if (seqTasks.isEmpty()) { return; } tasksCount += seqTasks.size(); stages.add(new ExecStage(ExecType.SEQUENTIAL, seqTasks)); } @Override public void addSequentialTask(Runnable seqTask) { addSequentialTasks(Collections.singletonList(seqTask)); } @Override public int getThreadsCount() { return threadsCount.get(); } @Override public void setThreadsCount(int count) { threadsCount.set(count); } @Override public int getTasksCount() { return tasksCount; } @Override public int getProgress() { return progress.get(); } @Override public void execute() { synchronized (executorSync) { if (running.get() || executor != null) { throw new IllegalStateException("Already executing"); } executor = Executors.newFixedThreadPool(1, Utils.simpleThreadFactory("task-s")); running.set(true); terminating.set(false); progress.set(0); executor.execute(this::runStages); } } private void stopExecution() { synchronized (executorSync) { running.set(false); terminating.set(true); if (executor != null) { executor.shutdown(); executor = null; } } } @Override public void awaitTermination() { ExecutorService activeExecutor = executor; if (activeExecutor != null && running.get()) { awaitExecutorTermination(activeExecutor); } Error error = terminateError; if (error != null) { throw error; } } @Override public void terminate() { terminating.set(true); } @SuppressWarnings("DataFlowIssue") private void terminateWithError(Error error) { if (terminating.get()) { return; } terminateError = error; terminate(); executor.shutdownNow(); } @Override public boolean isTerminating() { return terminating.get(); } @Override public boolean isRunning() { return running.get(); } @Override public @Nullable ExecutorService getInternalExecutor() { return executor; } private void runStages() { try { for (ExecStage stage : stages) { int threads = Math.min(stage.getTasks().size(), threadsCount.get()); if (stage.getType() == ExecType.SEQUENTIAL || threads == 1) { for (Runnable task : stage.getTasks()) { wrapTask(task); } } else { ExecutorService parallelExecutor = Executors.newFixedThreadPool( threads, Utils.simpleThreadFactory("task-p")); for (Runnable task : stage.getTasks()) { parallelExecutor.execute(() -> wrapTask(task)); } parallelExecutor.shutdown(); awaitExecutorTermination(parallelExecutor); } if (terminating.get()) { break; } } } finally { stopExecution(); } } private void wrapTask(Runnable task) { if (terminating.get()) { return; } try { task.run(); progress.incrementAndGet(); } catch (Error e) { terminateWithError(e); } catch (Exception e) { LOG.error("Unhandled task exception:", e); } } public static void awaitExecutorTermination(ExecutorService executor) { try { boolean complete = executor.awaitTermination(10, TimeUnit.DAYS); if (!complete) { throw new JadxRuntimeException("Executor timeout"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java ================================================ package jadx.core.xmlgen; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Random; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.ResourcesLoader; import jadx.core.dex.info.ConstStorage; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.StringUtils; import jadx.core.utils.android.AndroidResourcesMap; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.entry.ValuesParser; public class BinaryXMLParser extends CommonBinaryParser { private static final Logger LOG = LoggerFactory.getLogger(BinaryXMLParser.class); private final RootNode rootNode; private final ManifestAttributes manifestAttributes; private final boolean attrNewLine; private final Map resNames; private Map nsMap; private Set nsMapGenerated; private Set definedNamespaces; private final Map tagAttrDeobfNames = new HashMap<>(); private ICodeWriter writer; private BinaryXMLStrings strings; private String currentTag = "ERROR"; private boolean firstElement; private ValuesParser valuesParser; private boolean isLastEnd = true; private boolean isOneLine = true; private int namespaceDepth = 0; private @Nullable int[] resourceIds; private String appPackageName; private Map classNameCache; public BinaryXMLParser(RootNode rootNode) { this.rootNode = rootNode; this.manifestAttributes = rootNode.initManifestAttributes(); this.attrNewLine = !rootNode.getArgs().isSkipXmlPrettyPrint(); try { ConstStorage constStorage = rootNode.getConstValues(); resNames = constStorage.getResourcesNames(); } catch (Exception e) { throw new JadxRuntimeException("BinaryXMLParser init error", e); } } public synchronized ICodeInfo parse(InputStream inputStream) throws IOException { resourceIds = null; is = new ParserStream(inputStream); if (!isBinaryXml()) { return ResourcesLoader.loadToCodeWriter(is); } nsMapGenerated = new HashSet<>(); nsMap = new HashMap<>(); definedNamespaces = new HashSet<>(); writer = rootNode.makeCodeWriter(); writer.add(""); firstElement = true; decode(); nsMap = null; definedNamespaces = null; ICodeInfo codeInfo = writer.finish(); this.classNameCache = null; // reset class name cache return codeInfo; } private boolean isBinaryXml() throws IOException { is.mark(4); int v = is.readInt16(); // version int h = is.readInt16(); // header size // Some APK Manifest.xml the version is 0 if (h == 0x0008) { return true; } is.reset(); return false; } void decode() throws IOException { int size = is.readInt32(); while (is.getPos() < size) { int type = is.readInt16(); switch (type) { case RES_NULL_TYPE: // NullType is just doing nothing break; case RES_STRING_POOL_TYPE: strings = parseStringPoolNoType(); valuesParser = new ValuesParser(strings, resNames); break; case RES_XML_RESOURCE_MAP_TYPE: parseResourceMap(); break; case RES_XML_START_NAMESPACE_TYPE: parseNameSpace(); break; case RES_XML_CDATA_TYPE: parseCData(); break; case RES_XML_END_NAMESPACE_TYPE: parseNameSpaceEnd(); break; case RES_XML_START_ELEMENT_TYPE: parseElement(); break; case RES_XML_END_ELEMENT_TYPE: parseElementEnd(); break; default: if (namespaceDepth == 0) { // skip padding on file end return; } die("Type: 0x" + Integer.toHexString(type) + " not yet implemented"); break; } } } private void parseResourceMap() throws IOException { if (is.readInt16() != 0x8) { die("Header size of resmap is not 8!"); } int size = is.readInt32(); int len = (size - 8) / 4; resourceIds = new int[len]; for (int i = 0; i < len; i++) { resourceIds[i] = is.readInt32(); } } private void parseNameSpace() throws IOException { int headerSize = is.readInt16(); if (headerSize > 0x10) { LOG.warn("Invalid namespace header"); } else if (headerSize < 0x10) { die("NAMESPACE header is not 0x10 big"); } int size = is.readInt32(); if (size > 0x18) { LOG.warn("Invalid namespace size"); } else if (size < 0x18) { die("NAMESPACE header chunk is not 0x18 big"); } int beginLineNumber = is.readInt32(); int comment = is.readInt32(); int beginPrefix = is.readInt32(); int beginURI = is.readInt32(); is.skip(headerSize - 0x10); String nsKey = getString(beginURI); String nsValue = getString(beginPrefix); if (StringUtils.notBlank(nsKey) && !nsMap.containsValue(nsValue)) { nsMap.putIfAbsent(nsKey, nsValue); } namespaceDepth++; } private void parseNameSpaceEnd() throws IOException { int headerSize = is.readInt16(); if (headerSize > 0x10) { LOG.warn("Invalid namespace end"); } else if (headerSize < 0x10) { die("NAMESPACE end is not 0x10 big"); } int dataSize = is.readInt32(); if (dataSize != 0x18) { LOG.warn("Invalid namespace end size"); } int endLineNumber = is.readInt32(); int comment = is.readInt32(); int endPrefix = is.readInt32(); int endURI = is.readInt32(); is.skip(headerSize - 0x10); namespaceDepth--; String nsKey = getString(endURI); String nsValue = getString(endPrefix); if (StringUtils.notBlank(nsKey) && !nsMap.containsValue(nsValue)) { nsMap.putIfAbsent(nsKey, nsValue); } } private void parseCData() throws IOException { if (is.readInt16() != 0x10) { die("CDATA header is not 0x10"); } if (is.readInt32() != 0x1C) { die("CDATA header chunk is not 0x1C"); } int lineNumber = is.readInt32(); is.skip(4); int strIndex = is.readInt32(); String str = getString(strIndex); if (!isLastEnd) { isLastEnd = true; writer.add('>'); } writer.attachSourceLine(lineNumber); String escapedStr = StringUtils.escapeXML(str); writer.add(escapedStr); long size = is.readInt16(); is.skip(size - 2); } private void parseElement() throws IOException { if (firstElement) { firstElement = false; } else { writer.incIndent(); } if (is.readInt16() != 0x10) { die("ELEMENT HEADER SIZE is not 0x10"); } // TODO: Check element chunk size long startPos = is.getPos(); int elementSize = is.readInt32(); int elementBegLineNumber = is.readInt32(); int comment = is.readInt32(); int startNS = is.readInt32(); int startNSName = is.readInt32(); // actually is elementName... if (!isLastEnd && !"ERROR".equals(currentTag)) { writer.add('>'); } isOneLine = true; isLastEnd = false; currentTag = deobfClassName(getString(startNSName)); currentTag = getValidTagAttributeName(currentTag); writer.startLine('<').add(currentTag); writer.attachSourceLine(elementBegLineNumber); int attributeStart = is.readInt16(); if (attributeStart != 0x14) { die("startNS's attributeStart is not 0x14"); } int attributeSize = is.readInt16(); if (attributeSize < 0x14) { die("startNS's attributeSize is less than 0x14"); } int attributeCount = is.readInt16(); int idIndex = is.readInt16(); int classIndex = is.readInt16(); int styleIndex = is.readInt16(); if ("manifest".equals(currentTag) || definedNamespaces.size() != nsMap.size()) { for (Map.Entry entry : nsMap.entrySet()) { if (!definedNamespaces.contains(entry.getKey())) { definedNamespaces.add(entry.getKey()); String nsValue = getValidTagAttributeName(entry.getValue()); writer.add(" xmlns"); if (nsValue != null && !nsValue.trim().isEmpty()) { writer.add(':'); writer.add(nsValue); } writer.add("=\"").add(StringUtils.escapeXML(entry.getKey())).add('"'); } } } Set attrCache = new HashSet<>(); boolean attrNewLine = attributeCount != 1 && this.attrNewLine; for (int i = 0; i < attributeCount; i++) { parseAttribute(i, attrNewLine, attrCache, attributeSize); } long endPos = is.getPos(); if (endPos - startPos + 0x4 < elementSize) { is.skip(elementSize - (endPos - startPos + 0x4)); } } private void parseAttribute(int i, boolean newLine, Set attrCache, int attributeSize) throws IOException { int attributeNS = is.readInt32(); int attributeName = is.readInt32(); int attributeRawValue = is.readInt32(); is.skip(3); int attrValDataType = is.readInt8(); int attrValData = is.readInt32(); is.skip(attributeSize - 0x14); String shortNsName = null; if (attributeNS != -1) { shortNsName = getAttributeNS(attributeNS, newLine); } String attrName = getValidTagAttributeName(getAttributeName(attributeName)); String attrFullName = shortNsName != null ? shortNsName + ":" + attrName : attrName; // do not dump duplicated values if (XmlDeobf.isDuplicatedAttr(attrFullName, attrCache)) { return; } if (newLine) { writer.startLine().addIndent(); } else { writer.add(' '); } writer.add(attrFullName).add("=\""); String decodedAttr = manifestAttributes.decode(attrFullName, attrValData); if (decodedAttr != null) { memorizePackageName(attrName, decodedAttr); if (isDeobfCandidateAttr(attrFullName)) { decodedAttr = deobfClassName(decodedAttr); } attachClassNode(writer, attrName, decodedAttr); writer.add(StringUtils.escapeXML(decodedAttr)); } else { decodeAttribute(attributeNS, attrValDataType, attrValData, attrFullName); } if (shortNsName != null && shortNsName.equals("android")) { if (attrName.equals("pathData")) { rootNode.getGradleInfoStorage().setVectorPathData(true); } else if (attrName.equals("fillType")) { rootNode.getGradleInfoStorage().setVectorFillType(true); } } writer.add('"'); } private String getAttributeNS(int attributeNS, boolean newLine) { String attrUrl = getString(attributeNS); if (attrUrl == null || attrUrl.isEmpty()) { if (isResInternalId(attributeNS)) { return null; } else { attrUrl = ANDROID_NS_URL; } } String attrName = nsMap.get(attrUrl); if (attrName == null) { attrName = generateNameForNS(attrUrl, newLine); } return attrName; } private String generateNameForNS(String attrUrl, boolean newLine) { String attrName; if (ANDROID_NS_URL.equals(attrUrl)) { attrName = ANDROID_NS_VALUE; nsMap.put(ANDROID_NS_URL, attrName); } else { for (int i = 1;; i++) { attrName = "ns" + i; if (!nsMapGenerated.contains(attrName) && !nsMap.containsValue(attrName)) { nsMapGenerated.add(attrName); // do not add generated value to nsMap // because attrUrl might be used in a neighbor element, but never defined break; } } } if (newLine) { writer.startLine().addIndent(); } else { writer.add(' '); } writer.add("xmlns:").add(attrName).add("=\"").add(attrUrl).add("\" "); return attrName; } private String getAttributeName(int id) { // As the outcome of https://github.com/skylot/jadx/issues/1208 // Android seems to favor entries from AndroidResMap and only if // there is no entry uses the values form the XML string pool if (resourceIds != null && 0 <= id && id < resourceIds.length) { int resId = resourceIds[id]; String str = AndroidResourcesMap.getResName(resId); if (str != null) { // cut type before / int typeEnd = str.indexOf('/'); if (typeEnd != -1) { return str.substring(typeEnd + 1); } return str; } } String str = getString(id); if (str == null || str.isEmpty()) { return "NOT_FOUND_0x" + Integer.toHexString(id); } return str; } private String getString(int strId) { if (0 <= strId && strId < strings.size()) { return strings.get(strId); } return "NOT_FOUND_STR_0x" + Integer.toHexString(strId); } private void decodeAttribute(int attributeNS, int attrValDataType, int attrValData, String attrFullName) { if (attrValDataType == TYPE_REFERENCE) { // reference custom processing String resName = resNames.get(attrValData); if (resName != null) { writer.add('@'); if (resName.startsWith("id/")) { writer.add('+'); } writer.add(resName); } else { String androidResName = AndroidResourcesMap.getResName(attrValData); if (androidResName != null) { writer.add("@android:").add(androidResName); } else if (attrValData == 0) { writer.add("@null"); } else { writer.add("0x").add(Integer.toHexString(attrValData)); } } } else { String str; try { str = valuesParser.decodeValue(attrValDataType, attrValData); } catch (JadxRuntimeException e) { LOG.error("Failed to decode attribute value of \"{}\"", attrFullName, e); str = null; } memorizePackageName(attrFullName, str); if (isDeobfCandidateAttr(attrFullName)) { str = deobfClassName(str); } attachClassNode(writer, attrFullName, str); writer.add(str != null ? StringUtils.escapeXML(str) : "null"); } } private void parseElementEnd() throws IOException { if (is.readInt16() != 0x10) { die("ELEMENT END header is not 0x10"); } if (is.readInt32() != 0x18) { die("ELEMENT END header chunk is not 0x18 big"); } int endLineNumber = is.readInt32(); int comment = is.readInt32(); int elementNS = is.readInt32(); int elementNameId = is.readInt32(); String elemName = deobfClassName(getString(elementNameId)); elemName = getValidTagAttributeName(elemName); if (currentTag.equals(elemName) && isOneLine && !isLastEnd) { writer.add("/>"); } else { writer.startLine("'); } isLastEnd = true; if (writer.getIndent() != 0) { writer.decIndent(); } } private String getValidTagAttributeName(String originalName) { if (XMLChar.isValidName(originalName)) { return originalName; } if (tagAttrDeobfNames.containsKey(originalName)) { return tagAttrDeobfNames.get(originalName); } String generated; do { generated = generateTagAttrName(); } while (tagAttrDeobfNames.containsValue(generated)); tagAttrDeobfNames.put(originalName, generated); return generated; } private static String generateTagAttrName() { final int length = 6; Random r = new Random(); StringBuilder sb = new StringBuilder(); for (int i = 1; i <= length; i++) { sb.append((char) (r.nextInt(26) + 'a')); } return sb.toString(); } private void attachClassNode(ICodeWriter writer, String attrFullName, String clsName) { if (!writer.isMetadataSupported()) { return; } if (clsName == null || !attrFullName.equals("android:name")) { return; } String clsFullName; if (clsName.startsWith(".")) { clsFullName = appPackageName + clsName; } else { clsFullName = clsName; } if (classNameCache == null) { classNameCache = rootNode.buildFullAliasClassCache(); } ClassNode classNode = classNameCache.get(clsFullName); if (classNode != null) { writer.attachAnnotation(classNode); } } private String deobfClassName(String className) { String newName = XmlDeobf.deobfClassName(rootNode, className, appPackageName); if (newName != null) { return newName; } return className; } private boolean isDeobfCandidateAttr(String attrFullName) { return "android:name".equals(attrFullName); } private void memorizePackageName(String attrFullName, String attrValue) { if ("manifest".equals(currentTag) && "package".equals(attrFullName)) { appPackageName = attrValue; } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLStrings.java ================================================ package jadx.core.xmlgen; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; import java.util.HashMap; import java.util.Map; public class BinaryXMLStrings { public static final String INVALID_STRING_PLACEHOLDER = "⟨STRING_DECODE_ERROR⟩"; private final int stringCount; private final long stringsStart; private final ByteBuffer buffer; private final boolean isUtf8; // This cache include strings that have been overridden by the deobfuscator. private final Map cache = new HashMap<>(); public BinaryXMLStrings() { stringCount = 0; stringsStart = 0; buffer = ByteBuffer.allocate(0); buffer.order(ByteOrder.LITTLE_ENDIAN); isUtf8 = false; } public BinaryXMLStrings(int stringCount, long stringsStart, byte[] buffer, boolean isUtf8) { this.stringCount = stringCount; this.stringsStart = stringsStart; this.buffer = ByteBuffer.wrap(buffer); this.buffer.order(ByteOrder.LITTLE_ENDIAN); this.isUtf8 = isUtf8; } public String get(int id) { String cached = cache.get(id); if (cached != null) { return cached; } if (id * 4 >= buffer.limit() - 3) { return INVALID_STRING_PLACEHOLDER; } int off = buffer.getInt(id * 4); if (off < 0) { // read unsigned offset value is larger than Integer.MAX_VALUE // In reality this should only happen in obfuscated APKs with invalid offsets return INVALID_STRING_PLACEHOLDER; } long offset = stringsStart + off; String extracted; if (isUtf8) { extracted = extractString8(this.buffer.array(), (int) offset); } else { // don't trust specified string length, read until \0 // stringsOffset can be same for different indexes extracted = extractString16(this.buffer.array(), (int) offset); } cache.put(id, extracted); return extracted; } public void put(int id, String content) { cache.put(id, content); } public int size() { return this.stringCount; } private static String extractString8(byte[] strArray, int offset) { if (offset >= strArray.length) { return INVALID_STRING_PLACEHOLDER; } int start = offset + skipStrLen8(strArray, offset); int len = strArray[start++]; if (len == 0) { return ""; } if ((len & 0x80) != 0) { len = (len & 0x7F) << 8 | strArray[start++] & 0xFF; } byte[] arr = Arrays.copyOfRange(strArray, start, start + len); return new String(arr, ParserStream.STRING_CHARSET_UTF8); } private static String extractString16(byte[] strArray, int offset) { if (offset + 2 >= strArray.length) { return INVALID_STRING_PLACEHOLDER; } int len = strArray.length; int start = offset + skipStrLen16(strArray, offset); int end = start; while (true) { if (end + 1 >= len) { break; } if (strArray[end] == 0 && strArray[end + 1] == 0) { break; } end += 2; } byte[] arr = Arrays.copyOfRange(strArray, start, end); return new String(arr, ParserStream.STRING_CHARSET_UTF16); } private static int skipStrLen8(byte[] strArray, int offset) { return (strArray[offset] & 0x80) == 0 ? 1 : 2; } private static int skipStrLen16(byte[] strArray, int offset) { return (strArray[offset + 1] & 0x80) == 0 ? 2 : 4; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/CommonBinaryParser.java ================================================ package jadx.core.xmlgen; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CommonBinaryParser extends ParserConstants { private static final Logger LOG = LoggerFactory.getLogger(CommonBinaryParser.class); protected ParserStream is; protected BinaryXMLStrings parseStringPool() throws IOException { is.checkInt16(RES_STRING_POOL_TYPE, "String pool expected"); return parseStringPoolNoType(); } protected BinaryXMLStrings parseStringPoolNoType() throws IOException { long start = is.getPos() - 2; int headerSize = is.readInt16(); if (headerSize != 0x1c) { LOG.warn("Unexpected string pool header size: 0x{}, expected: 0x1C", Integer.toHexString(headerSize)); } long size = is.readUInt32(); long chunkEnd = start + size; return parseStringPoolNoSize(start, chunkEnd); } protected BinaryXMLStrings parseStringPoolNoSize(long start, long chunkEnd) throws IOException { int stringCount = is.readInt32(); int styleCount = is.readInt32(); int flags = is.readInt32(); long stringsStart = is.readInt32(); long stylesStart = is.readInt32(); // Correct the offset of actual strings, as the header is already read. stringsStart = stringsStart - (is.getPos() - start); byte[] buffer = is.readInt8Array((int) (chunkEnd - is.getPos())); is.checkPos(chunkEnd, "Expected strings pool end"); return new BinaryXMLStrings( stringCount, stringsStart, buffer, (flags & UTF8_FLAG) != 0); } protected void die(String message) throws IOException { throw new IOException("Decode error: " + message + ", position: 0x" + Long.toHexString(is.getPos())); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/IResTableParser.java ================================================ package jadx.core.xmlgen; import java.io.IOException; import java.io.InputStream; public interface IResTableParser { void decode(InputStream inputStream) throws IOException; ResContainer decodeFiles(); ResourceStorage getResStorage(); BinaryXMLStrings getStrings(); default void setBaseFileName(String fileName) { // optional } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java ================================================ package jadx.core.xmlgen; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import jadx.api.security.IJadxSecurity; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.entry.RawNamedValue; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ValuesParser; // TODO: move to Android specific module! /** * Load and store Android Manifest attributes specification */ public class ManifestAttributes { private static final Logger LOG = LoggerFactory.getLogger(ManifestAttributes.class); private static final String ATTR_XML = "/android/attrs.xml"; private static final String MANIFEST_ATTR_XML = "/android/attrs_manifest.xml"; private enum MAttrType { ENUM, FLAG } private static class MAttr { private final MAttrType type; private final Map values = new LinkedHashMap<>(); public MAttr(MAttrType type) { this.type = type; } public MAttrType getType() { return type; } public Map getValues() { return values; } public void addValue(long key, String value) { values.put(key, value); } @Override public String toString() { return "[" + type + ", " + values + ']'; } } private final IJadxSecurity security; /** * Map containing default Android resource attribute definitions. * Keys are Android attribute names (e.g., "android:layout_width"), * and values are their corresponding {@link MAttr} objects. */ private final Map attrMap = new HashMap<>(); private final Map appAttrMap = new HashMap<>(); public ManifestAttributes(IJadxSecurity security) { this.security = security; parseAll(); } private void parseAll() { parse(loadXML(ATTR_XML)); parse(loadXML(MANIFEST_ATTR_XML)); LOG.debug("Loaded android attributes count: {}", attrMap.size()); } private Document loadXML(String xml) { Document doc; try (InputStream xmlStream = ManifestAttributes.class.getResourceAsStream(xml)) { if (xmlStream == null) { throw new JadxRuntimeException(xml + " not found in classpath"); } doc = security.parseXml(xmlStream); } catch (Exception e) { throw new JadxRuntimeException("Xml load error, file: " + xml, e); } return doc; } private void parse(Document doc) { NodeList nodeList = doc.getChildNodes(); for (int count = 0; count < nodeList.getLength(); count++) { Node node = nodeList.item(count); if (node.getNodeType() == Node.ELEMENT_NODE && node.hasChildNodes()) { parseAttrList(node.getChildNodes()); } } } private void parseAttrList(NodeList nodeList) { for (int count = 0; count < nodeList.getLength(); count++) { Node tempNode = nodeList.item(count); if (tempNode.getNodeType() == Node.ELEMENT_NODE && tempNode.hasAttributes() && tempNode.hasChildNodes()) { String name = null; NamedNodeMap nodeMap = tempNode.getAttributes(); for (int i = 0; i < nodeMap.getLength(); i++) { Node node = nodeMap.item(i); if (node.getNodeName().equals("name")) { name = node.getNodeValue(); break; } } if (name != null && tempNode.getNodeName().equals("attr")) { parseValues(name, tempNode.getChildNodes()); } else { parseAttrList(tempNode.getChildNodes()); } } } } private void parseValues(String name, NodeList nodeList) { MAttr attr = null; for (int count = 0; count < nodeList.getLength(); count++) { Node tempNode = nodeList.item(count); if (tempNode.getNodeType() == Node.ELEMENT_NODE && tempNode.hasAttributes()) { if (attr == null) { if (tempNode.getNodeName().equals("enum")) { attr = new MAttr(MAttrType.ENUM); } else if (tempNode.getNodeName().equals("flag")) { attr = new MAttr(MAttrType.FLAG); } if (attr == null) { return; } attrMap.put("android:" + name, attr); } NamedNodeMap attributes = tempNode.getAttributes(); Node nameNode = attributes.getNamedItem("name"); if (nameNode != null) { Node valueNode = attributes.getNamedItem("value"); if (valueNode != null) { try { long key; String nodeValue = valueNode.getNodeValue(); if (nodeValue.startsWith("0x")) { nodeValue = nodeValue.substring(2); key = Long.parseLong(nodeValue, 16); } else { key = Long.parseLong(nodeValue); } attr.addValue(key, nameNode.getNodeValue()); } catch (NumberFormatException e) { LOG.debug("Failed parse manifest number", e); } } } } } } public String decode(String attrName, long value) { MAttr attr = attrMap.get(attrName); if (attr == null) { if (attrName.contains(":")) { attrName = attrName.split(":", 2)[1]; } attr = appAttrMap.get(attrName); if (attr == null) { return null; } } Map attrValuesMap = attr.getValues(); if (attr.getType() == MAttrType.ENUM) { return attrValuesMap.get(value); } else if (attr.getType() == MAttrType.FLAG) { List flagList = new ArrayList<>(); List attrKeys = new ArrayList<>(attrValuesMap.keySet()); attrKeys.sort((a, b) -> Long.compare(b, a)); // sort descending for (Long key : attrKeys) { String attrValue = attrValuesMap.get(key); if (value == key) { flagList.add(attrValue); break; } else if ((key != 0) && ((value & key) == key)) { flagList.add(attrValue); value ^= key; } } return String.join("|", flagList); } return null; } public void updateAttributes(IResTableParser parser) { appAttrMap.clear(); ResourceStorage resStorage = parser.getResStorage(); ValuesParser vp = new ValuesParser(parser.getStrings(), resStorage.getResourcesNames()); for (ResourceEntry ri : resStorage.getResources()) { if (ri.getProtoValue() != null) { // Aapt proto decoder resolves attributes by itself. continue; } if (ri.getTypeName().equals("attr") && ri.getNamedValues().size() > 1) { RawNamedValue first = ri.getNamedValues().get(0); MAttrType attrTyp; int attrTypeVal = first.getRawValue().getData() & 0xff0000; if (attrTypeVal == ValuesParser.ATTR_TYPE_FLAGS) { attrTyp = MAttrType.FLAG; } else if (attrTypeVal == ValuesParser.ATTR_TYPE_ENUM) { attrTyp = MAttrType.ENUM; } else { continue; } MAttr attr = new MAttr(attrTyp); for (int i = 1; i < ri.getNamedValues().size(); i++) { RawNamedValue rv = ri.getNamedValues().get(i); String value = vp.decodeNameRef(rv.getNameRef()); attr.addValue(rv.getRawValue().getData(), value.startsWith("id.") ? value.substring(3) : value); } appAttrMap.put(ri.getKeyName(), attr); } } } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java ================================================ package jadx.core.xmlgen; import java.util.HashMap; import java.util.Map; public class ParserConstants { protected ParserConstants() { } protected static final String ANDROID_NS_URL = "http://schemas.android.com/apk/res/android"; protected static final String ANDROID_NS_VALUE = "android"; /** * Chunk types as defined in frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h (AOSP) */ protected static final int RES_NULL_TYPE = 0x0000; protected static final int RES_STRING_POOL_TYPE = 0x0001; protected static final int RES_TABLE_TYPE = 0x0002; protected static final int RES_XML_TYPE = 0x0003; protected static final int RES_XML_FIRST_CHUNK_TYPE = 0x0100; protected static final int RES_XML_START_NAMESPACE_TYPE = 0x0100; protected static final int RES_XML_END_NAMESPACE_TYPE = 0x0101; protected static final int RES_XML_START_ELEMENT_TYPE = 0x0102; protected static final int RES_XML_END_ELEMENT_TYPE = 0x0103; protected static final int RES_XML_CDATA_TYPE = 0x0104; protected static final int RES_XML_LAST_CHUNK_TYPE = 0x017f; protected static final int RES_XML_RESOURCE_MAP_TYPE = 0x0180; protected static final int RES_TABLE_PACKAGE_TYPE = 0x0200; // 512 protected static final int RES_TABLE_TYPE_TYPE = 0x0201; // 513 protected static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202; // 514 protected static final int RES_TABLE_TYPE_LIBRARY = 0x0203; // 515 protected static final int RES_TABLE_TYPE_OVERLAY = 0x0204; // 516 protected static final int RES_TABLE_TYPE_OVERLAY_POLICY = 0x0205; // 517 protected static final int RES_TABLE_TYPE_STAGED_ALIAS = 0x0206; // 518 /** * Type constants */ // Contains no data. protected static final int TYPE_NULL = 0x00; // The 'data' holds a ResTable_ref, a reference to another resource table entry. protected static final int TYPE_REFERENCE = 0x01; // The 'data' holds an attribute resource identifier. protected static final int TYPE_ATTRIBUTE = 0x02; // The 'data' holds an index into the containing resource table's global value string pool. protected static final int TYPE_STRING = 0x03; // The 'data' holds a single-precision floating point number. protected static final int TYPE_FLOAT = 0x04; // The 'data' holds a complex number encoding a dimension value, such as "100in". protected static final int TYPE_DIMENSION = 0x05; // The 'data' holds a complex number encoding a fraction of a container. protected static final int TYPE_FRACTION = 0x06; /** * The 'data' holds a dynamic reference, a reference to another resource table entry. * See https://github.com/skylot/jadx/issues/919 */ protected static final int TYPE_DYNAMIC_REFERENCE = 0x07; /** * According to the sources of apktool this type seem to be related to themes * See https://github.com/skylot/jadx/issues/919 */ protected static final int TYPE_DYNAMIC_ATTRIBUTE = 0x08; // Beginning of integer flavors... protected static final int TYPE_FIRST_INT = 0x10; // The 'data' is a raw integer value of the form n..n. protected static final int TYPE_INT_DEC = 0x10; // The 'data' is a raw integer value of the form 0xn..n. protected static final int TYPE_INT_HEX = 0x11; // The 'data' is either 0 or 1, for input "false" or "true" respectively. protected static final int TYPE_INT_BOOLEAN = 0x12; // Beginning of color integer flavors... protected static final int TYPE_FIRST_COLOR_INT = 0x1c; // The 'data' is a raw integer value of the form #aarrggbb. protected static final int TYPE_INT_COLOR_ARGB8 = 0x1c; // The 'data' is a raw integer value of the form #rrggbb. protected static final int TYPE_INT_COLOR_RGB8 = 0x1d; // The 'data' is a raw integer value of the form #argb. protected static final int TYPE_INT_COLOR_ARGB4 = 0x1e; // The 'data' is a raw integer value of the form #rgb. protected static final int TYPE_INT_COLOR_RGB4 = 0x1f; // ...end of integer flavors. protected static final int TYPE_LAST_COLOR_INT = 0x1f; // ...end of integer flavors. protected static final int TYPE_LAST_INT = 0x1f; // Where the unit type information is. This gives us 16 possible // types, as defined below. protected static final int COMPLEX_UNIT_SHIFT = 0; protected static final int COMPLEX_UNIT_MASK = 0xf; // TYPE_DIMENSION: Value is raw pixels. protected static final int COMPLEX_UNIT_PX = 0; // TYPE_DIMENSION: Value is Device Independent Pixels. protected static final int COMPLEX_UNIT_DIP = 1; // TYPE_DIMENSION: Value is a Scaled device independent Pixels. protected static final int COMPLEX_UNIT_SP = 2; // TYPE_DIMENSION: Value is in points. protected static final int COMPLEX_UNIT_PT = 3; // TYPE_DIMENSION: Value is in inches. protected static final int COMPLEX_UNIT_IN = 4; // TYPE_DIMENSION: Value is in millimeters. protected static final int COMPLEX_UNIT_MM = 5; // TYPE_FRACTION: A basic fraction of the overall size. protected static final int COMPLEX_UNIT_FRACTION = 0; // TYPE_FRACTION: A fraction of the parent size. protected static final int COMPLEX_UNIT_FRACTION_PARENT = 1; // Where the radix information is, telling where the decimal place // appears in the mantissa. This give us 4 possible fixed point // representations as defined below. protected static final int COMPLEX_RADIX_SHIFT = 4; protected static final int COMPLEX_RADIX_MASK = 0x3; // The mantissa is an integral number -- i.e., 0xnnnnnn.0 protected static final int COMPLEX_RADIX_23P0 = 0; // The mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn protected static final int COMPLEX_RADIX_16P7 = 1; // The mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn protected static final int COMPLEX_RADIX_8P15 = 2; // The mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn protected static final int COMPLEX_RADIX_0P23 = 3; // Where the actual value is. This gives us 23 bits of // precision. The top bit is the sign. protected static final int COMPLEX_MANTISSA_SHIFT = 8; protected static final int COMPLEX_MANTISSA_MASK = 0xffffff; protected static final double MANTISSA_MULT = 1.0f / (1 << COMPLEX_MANTISSA_SHIFT); protected static final double[] RADIX_MULTS = new double[] { 1.0f * MANTISSA_MULT, 1.0f / (1 << 7) * MANTISSA_MULT, 1.0f / (1 << 15) * MANTISSA_MULT, 1.0f / (1 << 23) * MANTISSA_MULT }; /** * String pool flags */ protected static final int SORTED_FLAG = 1; protected static final int UTF8_FLAG = 1 << 8; /** * ResTable_type */ protected static final int NO_ENTRY = 0xFFFFFFFF; // If set, the entry is sparse, and encodes both the entry ID and offset into each entry, // and a binary search is used to find the key. Only available on platforms >= O. // Mark any types that use this with a v26 qualifier to prevent runtime issues on older // platforms. protected static final int FLAG_SPARSE = 0x01; // If set, the offsets to the entries are encoded in 16-bit, real_offset = offset * 4u // An 16-bit offset of 0xffffu means a NO_ENTRY protected static final int FLAG_OFFSET16 = 0x02; /** * ResTable_entry */ // If set, this is a complex entry, holding a set of name/value mappings. // It is followed by an array of ResTable_map structures. protected static final int FLAG_COMPLEX = 0x0001; // If set, this resource has been declared public, so libraries are allowed to reference it. protected static final int FLAG_PUBLIC = 0x0002; // If set, this is a weak resource and may be overridden by strong resources of the same name/type. // This is only useful during linking with other resource tables. protected static final int FLAG_WEAK = 0x0004; // If set, this is a compact entry with data type and value directly // encoded in the entry, see ResTable_entry::compact protected static final int FLAG_COMPACT = 0x0008; /** * ResTable_map */ protected static final int ATTR_TYPE = makeResInternal(0); // For integral attributes, this is the minimum value it can hold. protected static final int ATTR_MIN = makeResInternal(1); // For integral attributes, this is the maximum value it can hold. protected static final int ATTR_MAX = makeResInternal(2); // Localization of this resource is can be encouraged or required with an aapt flag if this is set protected static final int ATTR_L10N = makeResInternal(3); // for plural support, see android.content.res.PluralRules#attrForQuantity(int) protected static final int ATTR_OTHER = makeResInternal(4); protected static final int ATTR_ZERO = makeResInternal(5); protected static final int ATTR_ONE = makeResInternal(6); protected static final int ATTR_TWO = makeResInternal(7); protected static final int ATTR_FEW = makeResInternal(8); protected static final int ATTR_MANY = makeResInternal(9); protected static final Map PLURALS_MAP; static { PLURALS_MAP = new HashMap<>(); PLURALS_MAP.put(ATTR_OTHER, "other"); PLURALS_MAP.put(ATTR_ZERO, "zero"); PLURALS_MAP.put(ATTR_ONE, "one"); PLURALS_MAP.put(ATTR_TWO, "two"); PLURALS_MAP.put(ATTR_FEW, "few"); PLURALS_MAP.put(ATTR_MANY, "many"); } private static int makeResInternal(int entry) { return 0x01000000 | entry & 0xFFFF; } protected static boolean isResInternalId(int resid) { return (resid & 0xFFFF0000) != 0 && (resid & 0xFF0000) == 0; } // Bit mask of allowed types, for use with ATTR_TYPE. protected static final int ATTR_TYPE_ANY = 0x0000FFFF; // Attribute holds a references to another resource. protected static final int ATTR_TYPE_REFERENCE = 1; // Attribute holds a generic string. protected static final int ATTR_TYPE_STRING = 1 << 1; // Attribute holds an integer value. ATTR_MIN and ATTR_MIN can // optionally specify a constrained range of possible integer values. protected static final int ATTR_TYPE_INTEGER = 1 << 2; // Attribute holds a boolean integer. protected static final int ATTR_TYPE_BOOLEAN = 1 << 3; // Attribute holds a color value. protected static final int ATTR_TYPE_COLOR = 1 << 4; // Attribute holds a floating point value. protected static final int ATTR_TYPE_FLOAT = 1 << 5; // Attribute holds a dimension value, such as "20px". protected static final int ATTR_TYPE_DIMENSION = 1 << 6; // Attribute holds a fraction value, such as "20%". protected static final int ATTR_TYPE_FRACTION = 1 << 7; // Attribute holds an enumeration. The enumeration values are // supplied as additional entries in the map. protected static final int ATTR_TYPE_ENUM = 1 << 16; // Attribute holds a bitmaks of flags. The flag bit values are // supplied as additional entries in the map. protected static final int ATTR_TYPE_FLAGS = 1 << 17; // Enum of localization modes, for use with ATTR_L10N protected static final int ATTR_L10N_NOT_REQUIRED = 0; protected static final int ATTR_L10N_SUGGESTED = 1; } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java ================================================ package jadx.core.xmlgen; import java.io.BufferedInputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.jetbrains.annotations.NotNull; public class ParserStream extends InputStream { protected static final Charset STRING_CHARSET_UTF16 = StandardCharsets.UTF_16LE; protected static final Charset STRING_CHARSET_UTF8 = StandardCharsets.UTF_8; private static final int[] EMPTY_INT_ARRAY = new int[0]; private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; private final InputStream input; private long readPos = 0; private long markPos = 0; public ParserStream(@NotNull InputStream inputStream) { this.input = inputStream.markSupported() ? inputStream : new BufferedInputStream(inputStream); } public long getPos() { return readPos; } public int readInt8() throws IOException { readPos++; return input.read(); } public int readInt16() throws IOException { readPos += 2; int b1 = input.read(); int b2 = input.read(); return (b2 & 0xFF) << 8 | b1 & 0xFF; } public int readInt32() throws IOException { readPos += 4; InputStream in = input; int b1 = in.read(); int b2 = in.read(); int b3 = in.read(); int b4 = in.read(); return b4 << 24 | (b3 & 0xFF) << 16 | (b2 & 0xFF) << 8 | b1 & 0xFF; } public long readUInt32() throws IOException { return readInt32() & 0xFFFFFFFFL; } public String readString16Fixed(int len) throws IOException { String str = new String(readInt8Array(len * 2), STRING_CHARSET_UTF16); return str.trim(); } public int[] readInt32Array(int count) throws IOException { if (count == 0) { return EMPTY_INT_ARRAY; } int[] arr = new int[count]; for (int i = 0; i < count; i++) { arr[i] = readInt32(); } return arr; } public byte[] readInt8Array(int count) throws IOException { if (count == 0) { return EMPTY_BYTE_ARRAY; } readPos += count; byte[] arr = new byte[count]; int pos = input.read(arr, 0, count); while (pos < count) { int read = input.read(arr, pos, count - pos); if (read == -1) { throw new IOException("No data, can't read " + count + " bytes"); } pos += read; } return arr; } @Override public long skip(long count) throws IOException { readPos += count; long pos = input.skip(count); while (pos < count) { long skipped = input.skip(count - pos); if (skipped == 0) { throw new IOException("No data, can't skip " + count + " bytes"); } pos += skipped; } return pos; } public void checkInt8(int expected, String error) throws IOException { int v = readInt8(); if (v != expected) { throwException(error, expected, v); } } public void checkInt16(int expected, String error) throws IOException { int v = readInt16(); if (v != expected) { throwException(error, expected, v); } } private void throwException(String error, int expected, int actual) throws IOException { throw new IOException(error + ", expected: 0x" + Integer.toHexString(expected) + ", actual: 0x" + Integer.toHexString(actual) + ", offset: 0x" + Long.toHexString(getPos())); } public void checkPos(long expectedOffset, String error) throws IOException { if (getPos() != expectedOffset) { throw new IOException(error + ", expected offset: 0x" + Long.toHexString(expectedOffset) + ", actual: 0x" + Long.toHexString(getPos())); } } public void skipToPos(long expectedOffset, String error) throws IOException { long pos = getPos(); if (pos > expectedOffset) { throw new IOException(error + ", expected offset not reachable: 0x" + Long.toHexString(expectedOffset) + ", actual: 0x" + Long.toHexString(getPos())); } if (pos < expectedOffset) { skip(expectedOffset - pos); } checkPos(expectedOffset, error); } @Override public void mark(int len) { if (!input.markSupported()) { throw new RuntimeException("Mark not supported for input stream " + input.getClass()); } input.mark(len); markPos = readPos; } @Override public void reset() throws IOException { input.reset(); readPos = markPos; } public void readFully(byte[] b) throws IOException { readFully(b, 0, b.length); } public void readFully(byte[] b, int off, int len) throws IOException { readPos += len; if (len < 0) { throw new IndexOutOfBoundsException(); } int n = 0; while (n < len) { int count = input.read(b, off + n, len - n); if (count < 0) { throw new EOFException(); } n += count; } } @Override public int read() throws IOException { return input.read(); } @Override public int read(@NotNull byte[] b, int off, int len) throws IOException { return input.read(b, off, len); } @Override public String toString() { return "pos: 0x" + Long.toHexString(readPos); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java ================================================ package jadx.core.xmlgen; import java.io.File; import java.util.Collections; import java.util.List; import java.util.Objects; import org.jetbrains.annotations.NotNull; import jadx.api.ICodeInfo; import jadx.api.ResourceFile; public class ResContainer implements Comparable { public enum DataType { TEXT, DECODED_DATA, RES_LINK, RES_TABLE } private final DataType dataType; private final String name; private final Object data; private final List subFiles; public static ResContainer textResource(String name, ICodeInfo content) { return new ResContainer(name, Collections.emptyList(), content, DataType.TEXT); } public static ResContainer decodedData(String name, byte[] data) { return new ResContainer(name, Collections.emptyList(), data, DataType.DECODED_DATA); } public static ResContainer resourceFileLink(ResourceFile resFile) { return new ResContainer(resFile.getDeobfName(), Collections.emptyList(), resFile, DataType.RES_LINK); } public static ResContainer resourceTable(String name, List subFiles, ICodeInfo rootContent) { return new ResContainer(name, subFiles, rootContent, DataType.RES_TABLE); } private ResContainer(String name, List subFiles, Object data, DataType dataType) { this.name = Objects.requireNonNull(name); this.subFiles = Objects.requireNonNull(subFiles); this.data = Objects.requireNonNull(data); this.dataType = Objects.requireNonNull(dataType); } public String getName() { return name; } public String getFileName() { return name.replace('/', File.separatorChar); } public List getSubFiles() { return subFiles; } public DataType getDataType() { return dataType; } public ICodeInfo getText() { return (ICodeInfo) data; } public byte[] getDecodedData() { return (byte[]) data; } public ResourceFile getResLink() { return (ResourceFile) data; } @Override public int compareTo(@NotNull ResContainer o) { return name.compareTo(o.name); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof ResContainer)) { return false; } ResContainer that = (ResContainer) o; return name.equals(that.name); } @Override public int hashCode() { return name.hashCode(); } @Override public String toString() { return "Res{" + name + ", type=" + dataType + ", subFiles=" + subFiles + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/ResDecoder.java ================================================ package jadx.core.xmlgen; public class ResDecoder { } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/ResNameUtils.java ================================================ package jadx.core.xmlgen; import jadx.core.deobf.NameMapper; import static jadx.core.deobf.NameMapper.*; class ResNameUtils { private ResNameUtils() { } /** * Sanitizes the name so that it can be used as a resource name. * By resource name is meant that: *

    *
  • It can be used by aapt2 as a resource entry name. *
  • It can be converted to a valid R class field name. *
*

* If the {@code name} is already a valid resource name, the method returns it unchanged. * If not, the method creates a valid resource name based on {@code name}, appends the * {@code postfix}, and returns the result. */ static String sanitizeAsResourceName(String name, String postfix, boolean allowNonPrintable) { if (name.isEmpty()) { return postfix; } final StringBuilder sb = new StringBuilder(name.length() + 1); boolean nameChanged = false; int cp = name.codePointAt(0); if (isValidResourceNameStart(cp, allowNonPrintable)) { sb.appendCodePoint(cp); } else { sb.append('_'); nameChanged = true; if (isValidResourceNamePart(cp, allowNonPrintable)) { sb.appendCodePoint(cp); } } for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) { cp = name.codePointAt(i); if (isValidResourceNamePart(cp, allowNonPrintable)) { sb.appendCodePoint(cp); } else { sb.append('_'); nameChanged = true; } } final String sanitizedName = sb.toString(); if (NameMapper.isReserved(sanitizedName)) { nameChanged = true; } return nameChanged ? sanitizedName + postfix : sanitizedName; } /** * Converts the resource name to a field name of the R class. */ static String convertToRFieldName(String resourceName) { return resourceName.replace('.', '_'); } /** * Determines whether the code point may be part of a resource name as the first character (aapt2 + * R class gen). */ private static boolean isValidResourceNameStart(int codePoint, boolean allowNonPrintable) { return (allowNonPrintable || isPrintableAsciiCodePoint(codePoint)) && (isValidAapt2ResourceNameStart(codePoint) && isValidIdentifierStart(codePoint)); } /** * Determines whether the code point may be part of a resource name as other than the first * character * (aapt2 + R class gen). */ private static boolean isValidResourceNamePart(int codePoint, boolean allowNonPrintable) { return (allowNonPrintable || isPrintableAsciiCodePoint(codePoint)) && ((isValidAapt2ResourceNamePart(codePoint) && isValidIdentifierPart(codePoint)) || codePoint == '.'); } /** * Determines whether the code point may be part of a resource name as the first character (aapt2). *

* Source: aapt2/text/Unicode.cpp#L112 */ private static boolean isValidAapt2ResourceNameStart(int codePoint) { return isXidStart(codePoint) || codePoint == '_'; } /** * Determines whether the code point may be part of a resource name as other than the first * character (aapt2). *

* Source: aapt2/text/Unicode.cpp#L118 */ private static boolean isValidAapt2ResourceNamePart(int codePoint) { return isXidContinue(codePoint) || codePoint == '.' || codePoint == '-'; } private static boolean isXidStart(int codePoint) { // TODO: Need to implement a full check if the code point is XID_Start. return codePoint < 0x0370 && Character.isUnicodeIdentifierStart(codePoint); } private static boolean isXidContinue(int codePoint) { // TODO: Need to implement a full check if the code point is XID_Continue. return codePoint < 0x0370 && (Character.isUnicodeIdentifierPart(codePoint) && !Character.isIdentifierIgnorable(codePoint)); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java ================================================ package jadx.core.xmlgen; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.StringJoiner; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.args.ResourceNameSource; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.IFieldInfoRef; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.BetterName; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.entry.EntryConfig; import jadx.core.xmlgen.entry.RawNamedValue; import jadx.core.xmlgen.entry.RawValue; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ValuesParser; public class ResTableBinaryParser extends CommonBinaryParser implements IResTableParser { private static final Logger LOG = LoggerFactory.getLogger(ResTableBinaryParser.class); private static final class PackageChunk { private final int id; private final String name; private final BinaryXMLStrings typeStrings; private final BinaryXMLStrings keyStrings; private PackageChunk(int id, String name, BinaryXMLStrings typeStrings, BinaryXMLStrings keyStrings) { this.id = id; this.name = name; this.typeStrings = typeStrings; this.keyStrings = keyStrings; } public int getId() { return id; } public String getName() { return name; } public BinaryXMLStrings getTypeStrings() { return typeStrings; } public BinaryXMLStrings getKeyStrings() { return keyStrings; } } /** * No renaming, pattern checking or name generation. Required for res-map.txt building */ private final boolean useRawResName; private final RootNode root; private ResourceStorage resStorage; private BinaryXMLStrings strings; private String baseFileName = ""; public ResTableBinaryParser(RootNode root) { this(root, false); } public ResTableBinaryParser(RootNode root, boolean useRawResNames) { this.root = root; this.useRawResName = useRawResNames; } @Override public void setBaseFileName(String fileName) { this.baseFileName = fileName; } @Override public void decode(InputStream inputStream) throws IOException { long start = System.currentTimeMillis(); is = new ParserStream(new BufferedInputStream(inputStream, 32768)); resStorage = new ResourceStorage(root.getArgs().getSecurity()); decodeTableChunk(); resStorage.finish(); if (LOG.isDebugEnabled()) { LOG.debug("Resource table parsed: size: {}, time: {}ms", resStorage.size(), System.currentTimeMillis() - start); } } @Override public ResContainer decodeFiles() { ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); ResXmlGen resGen = new ResXmlGen(resStorage, vp, root.initManifestAttributes()); ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage); List xmlFiles = resGen.makeResourcesXml(root.getArgs()); return ResContainer.resourceTable(baseFileName, xmlFiles, content); } void decodeTableChunk() throws IOException { is.checkInt16(RES_TABLE_TYPE, "Not a table chunk"); is.checkInt16(0x000c, "Unexpected table header size"); int size = is.readInt32(); int pkgCount = is.readInt32(); int pkgNum = 0; while (is.getPos() < size) { long chuckStart = is.getPos(); int type = is.readInt16(); int headerSize = is.readInt16(); long chunkSize = is.readUInt32(); long chunkEnd = chuckStart + chunkSize; switch (type) { case RES_NULL_TYPE: // skip break; case RES_STRING_POOL_TYPE: strings = parseStringPoolNoSize(chuckStart, chunkEnd); break; case RES_TABLE_PACKAGE_TYPE: parsePackage(chuckStart, headerSize, chunkEnd); pkgNum++; break; } is.skipToPos(chunkEnd, "Skip to table chunk end"); } if (pkgNum != pkgCount) { LOG.warn("Unexpected package chunks, read: {}, expected: {}", pkgNum, pkgCount); } } private void parsePackage(long pkgChunkStart, int headerSize, long pkgChunkEnd) throws IOException { if (headerSize < 0x011c) { die("Package header size too small"); return; } int id = is.readInt32(); String name = is.readString16Fixed(128); long typeStringsOffset = pkgChunkStart + is.readInt32(); int lastPublicType = is.readInt32(); long keyStringsOffset = pkgChunkStart + is.readInt32(); int lastPublicKey = is.readInt32(); if (headerSize >= 0x0120) { int typeIdOffset = is.readInt32(); } is.skipToPos(pkgChunkStart + headerSize, "package header end"); BinaryXMLStrings typeStrings = null; if (typeStringsOffset != 0) { is.skipToPos(typeStringsOffset, "Expected typeStrings string pool"); typeStrings = parseStringPool(); } BinaryXMLStrings keyStrings = null; if (keyStringsOffset != 0) { is.skipToPos(keyStringsOffset, "Expected keyStrings string pool"); keyStrings = parseStringPool(); } PackageChunk pkg = new PackageChunk(id, name, typeStrings, keyStrings); resStorage.setAppPackage(name); while (is.getPos() < pkgChunkEnd) { long chunkStart = is.getPos(); int type = is.readInt16(); LOG.trace("res package chunk start at {} type {}", chunkStart, type); switch (type) { case RES_NULL_TYPE: LOG.info("Null chunk type encountered at offset {}", chunkStart); break; case RES_TABLE_TYPE_TYPE: // 0x0201 parseTypeChunk(chunkStart, pkg); break; case RES_TABLE_TYPE_SPEC_TYPE: // 0x0202 parseTypeSpecChunk(chunkStart); break; case RES_TABLE_TYPE_LIBRARY: // 0x0203 parseLibraryTypeChunk(chunkStart); break; case RES_TABLE_TYPE_OVERLAY: // 0x0204 parseOverlayTypeChunk(chunkStart); break; case RES_TABLE_TYPE_OVERLAY_POLICY: // 0x0205 throw new IOException( String.format("Encountered unsupported chunk type RES_TABLE_TYPE_OVERLAY_POLICY at offset 0x%x ", chunkStart)); case RES_TABLE_TYPE_STAGED_ALIAS: // 0x0206 parseStagedAliasChunk(chunkStart); break; default: LOG.warn("Unknown chunk type {} encountered at offset {}", type, chunkStart); } } } @SuppressWarnings("unused") private void parseTypeSpecChunk(long chunkStart) throws IOException { is.checkInt16(0x0010, "Unexpected type spec header size"); int chunkSize = is.readInt32(); long expectedEndPos = chunkStart + chunkSize; int id = is.readInt8(); is.skip(3); int entryCount = is.readInt32(); for (int i = 0; i < entryCount; i++) { int entryFlag = is.readInt32(); } if (is.getPos() != expectedEndPos) { throw new IOException(String.format("Error reading type spec chunk at offset 0x%x", chunkStart)); } } private void parseLibraryTypeChunk(long chunkStart) throws IOException { LOG.trace("parsing library type chunk starting at offset {}", chunkStart); is.checkInt16(12, "Unexpected header size"); int chunkSize = is.readInt32(); long expectedEndPos = chunkStart + chunkSize; int count = is.readInt32(); for (int i = 0; i < count; i++) { int packageId = is.readInt32(); String packageName = is.readString16Fixed(128); LOG.info("Found resource shared library {}, pkgId: {}", packageName, packageId); if (is.getPos() > expectedEndPos) { throw new IOException("reading after chunk end"); } } if (is.getPos() != expectedEndPos) { throw new IOException(String.format("Error reading library chunk at offset 0x%x", chunkStart)); } } /** * Parse an ResTable_type (except for the 2 bytes uint16_t * from ResChunk_header). * * @see ResourceTypes.h */ private void parseTypeChunk(long start, PackageChunk pkg) throws IOException { /* int headerSize = */ is.readInt16(); /* int size = */ long chunkSize = is.readUInt32(); long chunkEnd = start + chunkSize; is.mark((int) chunkSize); // The type identifier this chunk is holding. Type IDs start at 1 (corresponding // to the value of the type bits in a resource identifier). 0 is invalid. int typeId = is.readInt8(); String typeName = pkg.getTypeStrings().get(typeId - 1); int flags = is.readInt8(); boolean isSparse = (flags & FLAG_SPARSE) != 0; boolean isOffset16 = (flags & FLAG_OFFSET16) != 0; is.readInt16(); // ignore reserved value - should be zero but in some apps it is not zero; see #2402 int entryCount = is.readInt32(); long entriesStart = start + is.readInt32(); EntryConfig config = parseConfig(); if (config.isInvalid) { LOG.warn("Invalid config flags detected: {}{}", typeName, config.getQualifiers()); } List offsets = new ArrayList<>(entryCount); if (isSparse) { for (int i = 0; i < entryCount; i++) { int idx = is.readInt16(); int offset = is.readInt16() * 4; // The offset in ResTable_sparseTypeEntry::offset is stored divided by 4. offsets.add(new EntryOffset(idx, offset)); } } else if (isOffset16) { for (int i = 0; i < entryCount; i++) { int offset = is.readInt16(); if (offset != 0xFFFF) { offsets.add(new EntryOffset(i, offset * 4)); } } } else { for (int i = 0; i < entryCount; i++) { offsets.add(new EntryOffset(i, is.readInt32())); } } is.skipToPos(entriesStart, "Failed to skip to entries start"); int ignoredEoc = 0; // ignored entries because they are located after end of chunk Set processedIndices = new HashSet<>(offsets.size() * 2); for (EntryOffset entryOffset : offsets) { int offset = entryOffset.getOffset(); if (offset == NO_ENTRY) { continue; } int index = entryOffset.getIdx(); if (isSparse && !processedIndices.add(index)) { // Sometimes sparse type chunks contain multiple entries with the same index. // If we have processed the index once, we assume can ignore other entries with the same index. continue; } long entryStartOffset = entriesStart + offset; if (entryStartOffset >= chunkEnd) { // Certain resource obfuscated apps like com.facebook.orca have more entries defined // than actually fit into the chunk size -> ignore this entry ignoredEoc++; // LOG.debug("Pos is after chunk end: {} end {}", entryStartOffset, chunkEnd); continue; } if (entryStartOffset < is.getPos()) { // workaround for issue #2343: if the entryStartOffset is located before our current position is.reset(); } is.skipToPos(entryStartOffset, "Expected start of entry " + index); parseEntry(pkg, typeId, index, config.getQualifiers()); } if (ignoredEoc > 0) { // invalid = data offset is after the chunk end LOG.warn("{} entries of type {} has been ignored (invalid offset)", ignoredEoc, typeName); } is.skipToPos(chunkEnd, "End of chunk"); } private static class EntryOffset { private final int idx; private final int offset; private EntryOffset(int idx, int offset) { this.idx = idx; this.offset = offset; } public int getIdx() { return idx; } public int getOffset() { return offset; } @Override public String toString() { return new StringJoiner(", ", EntryOffset.class.getSimpleName() + "[", "]") .add("idx=" + idx) .add("offset=" + offset) .toString(); } } private void parseOverlayTypeChunk(long chunkStart) throws IOException { LOG.trace("parsing overlay type chunk starting at offset {}", chunkStart); // read ResTable_overlayable_header /* headerSize = */ is.readInt16(); // usually 1032 bytes int chunkSize = is.readInt32(); // e.g. 1056 bytes long expectedEndPos = chunkStart + chunkSize; String name = is.readString16Fixed(256); // 512 bytes String actor = is.readString16Fixed(256); // 512 bytes LOG.trace("Overlay header data: name={} actor={}", name, actor); // skip: ResTable_overlayable_policy_header + ResTable_ref * x is.skipToPos(expectedEndPos, "overlay chunk end"); } private void parseStagedAliasChunk(long chunkStart) throws IOException { // read ResTable_staged_alias_header LOG.trace("parsing staged alias chunk starting at offset {}", chunkStart); /* headerSize = */ is.readInt16(); int chunkSize = is.readInt32(); long expectedEndPos = chunkStart + chunkSize; int count = is.readInt32(); for (int i = 0; i < count; i++) { // read ResTable_staged_alias_entry int stagedResId = is.readInt32(); int finalizedResId = is.readInt32(); LOG.debug("Staged alias: stagedResId {} finalizedResId {}", stagedResId, finalizedResId); } is.skipToPos(expectedEndPos, "staged alias chunk end"); } private void parseEntry(PackageChunk pkg, int typeId, int entryId, String config) throws IOException { int size = is.readInt16(); int flags = is.readInt16(); boolean isComplex = (flags & FLAG_COMPLEX) != 0; boolean isCompact = (flags & FLAG_COMPACT) != 0; int key = isCompact ? size : is.readInt32(); if (key == -1) { return; } // resourceID as defined in AOSP make_resid() int resId = pkg.getId() << 24 | typeId << 16 | entryId; String typeName = pkg.getTypeStrings().get(typeId - 1); String origKeyName = pkg.getKeyStrings().get(key); ResourceEntry newResEntry = buildResourceEntry(pkg, config, resId, typeName, origKeyName); if (isCompact) { int dataType = flags >> 8; int data = is.readInt32(); newResEntry.setSimpleValue(new RawValue(dataType, data)); } else if (isComplex || size == 16) { int parentRef = is.readInt32(); int count = is.readInt32(); newResEntry.setParentRef(parentRef); List values = new ArrayList<>(count); for (int i = 0; i < count; i++) { values.add(parseValueMap()); } newResEntry.setNamedValues(values); } else { newResEntry.setSimpleValue(parseValue()); } } private static final ResourceEntry STUB_ENTRY = new ResourceEntry(-1, "stub", "stub", "stub", ""); private ResourceEntry buildResourceEntry(PackageChunk pkg, String config, int resId, String typeName, String origKeyName) { if (!root.getArgs().getSecurity().isValidEntryName(origKeyName)) { // malicious entry, ignore it // can't return null here, return stub without adding it to storage return STUB_ENTRY; } ResourceEntry newResEntry; if (useRawResName) { newResEntry = new ResourceEntry(resId, pkg.getName(), typeName, origKeyName, config); } else { String resName = getResName(resId, origKeyName); newResEntry = new ResourceEntry(resId, pkg.getName(), typeName, resName, config); ResourceEntry prevResEntry = resStorage.searchEntryWithSameName(newResEntry); if (prevResEntry != null) { if (prevResEntry.getId() == newResEntry.getId()) { // Check that every resource (identified by its resource ID) is only processed once. // We should not get resource entries with an identical id. This check is just for safety purposes, // otherwise Jadx can accumulate many GB of RAM as described in issue #2775 because resource names // are extended by every rename operation and are getting longer and longer... LOG.error("ResourceEntries with duplicate resource id found: {} {}", prevResEntry, newResEntry); resName = origKeyName; // use the original name, not the renamed one } newResEntry = newResEntry.copyWithId(resName); // rename also previous entry for consistency ResourceEntry replaceForPrevEntry = prevResEntry.copyWithId(resName); LOG.trace("Resource name collision - renamed to {} and {}", newResEntry.getKeyName(), replaceForPrevEntry.getKeyName()); resStorage.replace(prevResEntry, replaceForPrevEntry); resStorage.addRename(replaceForPrevEntry); } if (!Objects.equals(origKeyName, newResEntry.getKeyName())) { resStorage.addRename(newResEntry); } } resStorage.add(newResEntry); return newResEntry; } private String getResName(int resRef, String origKeyName) { if (this.useRawResName) { return origKeyName; } String renamedKey = resStorage.getRename(resRef); if (renamedKey != null) { return renamedKey; } IFieldInfoRef fldRef = root.getConstValues().getGlobalConstFields().get(resRef); FieldNode constField = fldRef instanceof FieldNode ? (FieldNode) fldRef : null; String newResName = getNewResName(resRef, origKeyName, constField); if (!origKeyName.equals(newResName)) { resStorage.addRename(resRef, newResName); } if (constField != null) { final String newFieldName = ResNameUtils.convertToRFieldName(newResName); constField.rename(newFieldName); constField.add(AFlag.DONT_RENAME); } return newResName; } private String getNewResName(int resRef, String origKeyName, @Nullable FieldNode constField) { String newResName; if (constField == null || constField.getTopParentClass().isSynthetic()) { newResName = origKeyName; } else { newResName = getBetterName(root.getArgs().getResourceNameSource(), origKeyName, constField.getName()); } if (root.getArgs().isRenameValid()) { final boolean allowNonPrintable = !root.getArgs().isRenamePrintable(); newResName = ResNameUtils.sanitizeAsResourceName(newResName, String.format("_res_0x%08x", resRef), allowNonPrintable); } return newResName; } public static String getBetterName(ResourceNameSource nameSource, String resName, String codeName) { switch (nameSource) { case AUTO: return BetterName.getBetterResourceName(resName, codeName); case RESOURCES: return resName; case CODE: return codeName; default: throw new JadxRuntimeException("Unexpected ResourceNameSource value: " + nameSource); } } private RawNamedValue parseValueMap() throws IOException { int nameRef = is.readInt32(); return new RawNamedValue(nameRef, parseValue()); } private RawValue parseValue() throws IOException { is.checkInt16(8, "value size"); is.checkInt8(0, "value res0 not 0"); int dataType = is.readInt8(); int data = is.readInt32(); return new RawValue(dataType, data); } private EntryConfig parseConfig() throws IOException { long start = is.getPos(); int size = is.readInt32(); if (size < 4) { throw new IOException("Config size < 4"); } // Android zero fill this structure and only read the data present var configData = new byte[Math.max(52, size - 4)]; is.readFully(configData, 0, size - 4); var configIs = new ParserStream(new ByteArrayInputStream(configData)); short mcc = (short) configIs.readInt16(); short mnc = (short) configIs.readInt16(); char[] language = unpackLocaleOrRegion((byte) configIs.readInt8(), (byte) configIs.readInt8(), 'a'); char[] country = unpackLocaleOrRegion((byte) configIs.readInt8(), (byte) configIs.readInt8(), '0'); byte orientation = (byte) configIs.readInt8(); byte touchscreen = (byte) configIs.readInt8(); int density = configIs.readInt16(); byte keyboard = (byte) configIs.readInt8(); byte navigation = (byte) configIs.readInt8(); byte inputFlags = (byte) configIs.readInt8(); byte grammaticalInflection = (byte) configIs.readInt8(); short screenWidth = (short) configIs.readInt16(); short screenHeight = (short) configIs.readInt16(); short sdkVersion = (short) configIs.readInt16(); configIs.readInt16(); // minorVersion must always be 0 byte screenLayout = (byte) configIs.readInt8(); byte uiMode = (byte) configIs.readInt8(); short smallestScreenWidthDp = (short) configIs.readInt16(); short screenWidthDp = (short) configIs.readInt16(); short screenHeightDp = (short) configIs.readInt16(); char[] localeScript = readScriptOrVariantChar(4, configIs).toCharArray(); char[] localeVariant = readScriptOrVariantChar(8, configIs).toCharArray(); byte screenLayout2 = (byte) configIs.readInt8(); byte colorMode = (byte) configIs.readInt8(); configIs.readInt16(); // reserved padding is.checkPos(start + size, "Config skip trailing bytes"); return new EntryConfig(mcc, mnc, language, country, orientation, touchscreen, density, keyboard, navigation, inputFlags, grammaticalInflection, screenWidth, screenHeight, sdkVersion, screenLayout, uiMode, smallestScreenWidthDp, screenWidthDp, screenHeightDp, localeScript.length == 0 ? null : localeScript, localeVariant.length == 0 ? null : localeVariant, screenLayout2, colorMode, false, size); } private char[] unpackLocaleOrRegion(byte in0, byte in1, char base) { // check high bit, if so we have a packed 3 letter code if (((in0 >> 7) & 1) == 1) { int first = in1 & 0x1F; int second = ((in1 & 0xE0) >> 5) + ((in0 & 0x03) << 3); int third = (in0 & 0x7C) >> 2; // since this function handles languages & regions, we add the value(s) to the base char // which is usually 'a' or '0' depending on language or region. return new char[] { (char) (first + base), (char) (second + base), (char) (third + base) }; } return new char[] { (char) in0, (char) in1 }; } private String readScriptOrVariantChar(int length) throws IOException { return readScriptOrVariantChar(length, is); } private static String readScriptOrVariantChar(int length, ParserStream ps) throws IOException { long start = ps.getPos(); StringBuilder sb = new StringBuilder(16); for (int i = 0; i < length; i++) { short ch = (short) ps.readInt8(); if (ch == 0) { break; } sb.append((char) ch); } ps.skipToPos(start + length, "readScriptOrVariantChar"); return sb.toString(); } @Override public ResourceStorage getResStorage() { return resStorage; } @Override public BinaryXMLStrings getStrings() { return strings; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParserProvider.java ================================================ package jadx.core.xmlgen; import org.jetbrains.annotations.Nullable; import jadx.api.ResourceFile; import jadx.api.plugins.resources.IResTableParserProvider; import jadx.core.dex.nodes.RootNode; public class ResTableBinaryParserProvider implements IResTableParserProvider { private RootNode root; @Override public void init(RootNode root) { this.root = root; } @Override public @Nullable IResTableParser getParser(ResourceFile resFile) { String fileName = resFile.getOriginalName(); if (!fileName.endsWith(".arsc")) { return null; } return new ResTableBinaryParser(root); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java ================================================ package jadx.core.xmlgen; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.JadxArgs; import jadx.api.impl.SimpleCodeWriter; import jadx.core.utils.StringUtils; import jadx.core.xmlgen.entry.ProtoValue; import jadx.core.xmlgen.entry.RawNamedValue; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ValuesParser; import static jadx.core.xmlgen.ParserConstants.PLURALS_MAP; import static jadx.core.xmlgen.ParserConstants.TYPE_REFERENCE; public class ResXmlGen { /** * Skip only file based resource type */ private static final Set SKIP_RES_TYPES = new HashSet<>(Arrays.asList( "anim", "animator", "font", "id", // skip id type, it is usually auto generated when used this syntax "@+id/my_id" "interpolator", "layout", "menu", "mipmap", "navigation", "raw", "transition", "xml")); private final ResourceStorage resStorage; private final ValuesParser vp; private final ManifestAttributes manifestAttributes; public ResXmlGen(ResourceStorage resStorage, ValuesParser vp, ManifestAttributes manifestAttributes) { this.resStorage = resStorage; this.vp = vp; this.manifestAttributes = manifestAttributes; } public List makeResourcesXml(JadxArgs args) { Map contMap = new HashMap<>(); for (ResourceEntry ri : resStorage.getResources()) { if (SKIP_RES_TYPES.contains(ri.getTypeName())) { continue; } String fn = getFileName(ri); ICodeWriter cw = contMap.get(fn); if (cw == null) { cw = new SimpleCodeWriter(args); cw.add(""); cw.startLine(""); cw.incIndent(); contMap.put(fn, cw); } addValue(cw, ri); } List files = new ArrayList<>(contMap.size()); for (Map.Entry entry : contMap.entrySet()) { String fileName = entry.getKey(); ICodeWriter content = entry.getValue(); content.decIndent(); content.startLine(""); ICodeInfo codeInfo = content.finish(); files.add(ResContainer.textResource(fileName, codeInfo)); } Collections.sort(files); return files; } private void addValue(ICodeWriter cw, ResourceEntry ri) { if (ri.getProtoValue() != null) { ProtoValue protoValue = ri.getProtoValue(); if (protoValue.getValue() != null && protoValue.getNamedValues() == null) { addSimpleValue(cw, ri.getTypeName(), ri.getTypeName(), "name", ri.getKeyName(), protoValue.getValue()); } else { cw.startLine(); cw.add('<').add(ri.getTypeName()).add(' '); String itemTag = "item"; cw.add("name=\"").add(ri.getKeyName()).add('\"'); if (ri.getTypeName().equals("attr") && protoValue.getValue() != null) { cw.add(" format=\"").add(protoValue.getValue()).add('\"'); } if (protoValue.getParent() != null) { cw.add(" parent=\"").add(protoValue.getParent()).add('\"'); } cw.add(">"); cw.incIndent(); for (ProtoValue value : protoValue.getNamedValues()) { addProtoItem(cw, itemTag, ri.getTypeName(), value); } cw.decIndent(); cw.startLine().add("'); } } else if (ri.getSimpleValue() != null) { String valueStr = vp.decodeValue(ri.getSimpleValue()); addSimpleValue(cw, ri.getTypeName(), ri.getTypeName(), "name", ri.getKeyName(), valueStr); } else { boolean skipNamedValues = false; cw.startLine(); cw.add('<').add(ri.getTypeName()).add(" name=\""); String itemTag = "item"; if (ri.getTypeName().equals("attr") && !ri.getNamedValues().isEmpty()) { cw.add(ri.getKeyName()); int type = ri.getNamedValues().get(0).getRawValue().getData(); if ((type & ValuesParser.ATTR_TYPE_ENUM) != 0) { itemTag = "enum"; } else if ((type & ValuesParser.ATTR_TYPE_FLAGS) != 0) { itemTag = "flag"; } String formatValue = XmlGenUtils.getAttrTypeAsString(type); if (formatValue != null) { cw.add("\" format=\"").add(formatValue); } if (ri.getNamedValues().size() > 1) { for (RawNamedValue rv : ri.getNamedValues()) { if (rv.getNameRef() == ParserConstants.ATTR_MIN) { cw.add("\" min=\"").add(String.valueOf(rv.getRawValue().getData())); skipNamedValues = true; } } } } else { cw.add(ri.getKeyName()); } if (ri.getTypeName().equals("style") || ri.getParentRef() != 0) { cw.add("\" parent=\""); if (ri.getParentRef() != 0) { String parent = vp.decodeValue(TYPE_REFERENCE, ri.getParentRef()); cw.add(parent); } } cw.add("\">"); if (!skipNamedValues) { cw.incIndent(); for (RawNamedValue value : ri.getNamedValues()) { addItem(cw, itemTag, ri.getTypeName(), value); } cw.decIndent(); } cw.startLine().add("'); } } private void addProtoItem(ICodeWriter cw, String itemTag, String typeName, ProtoValue protoValue) { String name = protoValue.getName(); String value = protoValue.getValue(); switch (typeName) { case "attr": if (name != null) { addSimpleValue(cw, typeName, itemTag, name, value, ""); } break; case "style": if (name != null) { addSimpleValue(cw, typeName, itemTag, name, "", value); } break; case "plurals": addSimpleValue(cw, typeName, itemTag, "quantity", name, value); break; default: addSimpleValue(cw, typeName, itemTag, null, null, value); break; } } private void addItem(ICodeWriter cw, String itemTag, String typeName, RawNamedValue value) { String nameStr = vp.decodeNameRef(value.getNameRef()); String valueStr = vp.decodeValue(value.getRawValue()); int dataType = value.getRawValue().getDataType(); if (!typeName.equals("attr")) { if (dataType == ParserConstants.TYPE_REFERENCE && (valueStr == null || valueStr.equals("0"))) { valueStr = "@null"; } if (dataType == ParserConstants.TYPE_INT_DEC && nameStr != null) { try { int intVal = Integer.parseInt(valueStr); String newVal = manifestAttributes.decode(nameStr.replace("android:", "").replace("attr.", ""), intVal); if (newVal != null) { valueStr = newVal; } } catch (NumberFormatException e) { // ignore } } if (dataType == ParserConstants.TYPE_INT_HEX && nameStr != null) { try { int intVal = Integer.decode(valueStr); String newVal = manifestAttributes.decode(nameStr.replace("android:", "").replace("attr.", ""), intVal); if (newVal != null) { valueStr = newVal; } } catch (NumberFormatException e) { // ignore } } } switch (typeName) { case "attr": if (nameStr != null) { addSimpleValue(cw, typeName, itemTag, nameStr, valueStr, ""); } break; case "style": if (nameStr != null) { addSimpleValue(cw, typeName, itemTag, nameStr, "", valueStr); } break; case "plurals": final String quantity = PLURALS_MAP.get(value.getNameRef()); addSimpleValue(cw, typeName, itemTag, "quantity", quantity, valueStr); break; default: addSimpleValue(cw, typeName, itemTag, null, null, valueStr); break; } } private void addSimpleValue(ICodeWriter cw, String typeName, String itemTag, String attrName, String attrValue, String valueStr) { if (valueStr == null) { return; } if (valueStr.startsWith("res/")) { // remove duplicated resources. return; } cw.startLine(); cw.add('<').add(itemTag); if (attrName != null && attrValue != null) { if (typeName.equals("attr")) { cw.add(' ').add("name=\"").add(attrName.replace("id.", "")).add("\" value=\"").add(attrValue).add('"'); } else if (typeName.equals("style")) { cw.add(' ').add("name=\"").add(attrName.replace("attr.", "")).add('"'); } else { cw.add(' ').add(attrName).add("=\"").add(attrValue).add('"'); } } if (itemTag.equals("string") && valueStr.contains("%") && StringFormattedCheck.hasMultipleNonPositionalSubstitutions(valueStr)) { cw.add(" formatted=\"false\""); } if (valueStr.isEmpty()) { cw.add(" />"); } else { cw.add('>'); if (itemTag.equals("string") || (typeName.equals("array") && valueStr.charAt(0) != '@')) { cw.add(StringUtils.escapeResStrValue(valueStr)); } else { cw.add(StringUtils.escapeResValue(valueStr)); } cw.add("'); } } private String getFileName(ResourceEntry ri) { StringBuilder sb = new StringBuilder(); String qualifiers = ri.getConfig(); sb.append("res/values"); if (!qualifiers.isEmpty()) { sb.append(qualifiers); } sb.append('/'); sb.append(ri.getTypeName()); if (!ri.getTypeName().endsWith("s")) { sb.append('s'); } sb.append(".xml"); return sb.toString(); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java ================================================ package jadx.core.xmlgen; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import jadx.api.security.IJadxSecurity; import jadx.core.xmlgen.entry.ResourceEntry; public class ResourceStorage { private static final Comparator RES_ENTRY_NAME_COMPARATOR = Comparator .comparing(ResourceEntry::getConfig) .thenComparing(ResourceEntry::getTypeName) .thenComparing(ResourceEntry::getKeyName); private final List list = new ArrayList<>(); private final IJadxSecurity security; private String appPackage; /** * Names in one config and type must be unique */ private final Map uniqNameEntries = new TreeMap<>(RES_ENTRY_NAME_COMPARATOR); /** * Preserve same name for same id across different configs */ private final Map renames = new HashMap<>(); public ResourceStorage(IJadxSecurity security) { this.security = security; } public void add(ResourceEntry resEntry) { list.add(resEntry); uniqNameEntries.put(resEntry, resEntry); } public void replace(ResourceEntry prevResEntry, ResourceEntry newResEntry) { int idx = list.indexOf(prevResEntry); if (idx != -1) { list.set(idx, newResEntry); } // don't remove from unique names so old name stays occupied } public void addRename(ResourceEntry entry) { addRename(entry.getId(), entry.getKeyName()); } public void addRename(int id, String keyName) { renames.put(id, keyName); } public String getRename(int id) { return renames.get(id); } public ResourceEntry searchEntryWithSameName(ResourceEntry resourceEntry) { return uniqNameEntries.get(resourceEntry); } public void finish() { list.sort(Comparator.comparingInt(ResourceEntry::getId)); uniqNameEntries.clear(); renames.clear(); } public int size() { return list.size(); } public Iterable getResources() { return list; } public String getAppPackage() { return appPackage; } public void setAppPackage(String appPackage) { this.appPackage = security.verifyAppPackage(appPackage); } public Map getResourcesNames() { Map map = new HashMap<>(); for (ResourceEntry entry : list) { map.put(entry.getId(), entry.getTypeName() + '/' + entry.getKeyName()); } return map; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java ================================================ package jadx.core.xmlgen; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxDecompiler; import jadx.api.ResourceFile; import jadx.api.ResourcesLoader; import jadx.api.security.IJadxSecurity; import jadx.core.dex.visitors.SaveCode; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; public class ResourcesSaver implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(ResourcesSaver.class); private final ResourceFile resourceFile; private final File outDir; private final IJadxSecurity security; public ResourcesSaver(JadxDecompiler decompiler, File outDir, ResourceFile resourceFile) { this.resourceFile = resourceFile; this.outDir = outDir; this.security = decompiler.getArgs().getSecurity(); } @Override public void run() { try { saveResources(resourceFile.loadContent()); } catch (StackOverflowError | Exception e) { LOG.warn("Failed to save resource: {}", resourceFile.getOriginalName(), e); } } private void saveResources(ResContainer rc) { if (rc == null) { return; } if (rc.getDataType() == ResContainer.DataType.RES_TABLE) { saveToFile(rc, new File(outDir, "res/values/public.xml")); for (ResContainer subFile : rc.getSubFiles()) { saveResources(subFile); } } else { save(rc, outDir); } } private void save(ResContainer rc, File outDir) { File outFile = new File(outDir, rc.getFileName()); if (!security.isInSubDirectory(outDir, outFile)) { LOG.error("Invalid resource name or path traversal attack detected: {}", outFile.getPath()); return; } saveToFile(rc, outFile); } private void saveToFile(ResContainer rc, File outFile) { switch (rc.getDataType()) { case TEXT: case RES_TABLE: SaveCode.save(rc.getText(), outFile); return; case DECODED_DATA: byte[] data = rc.getDecodedData(); FileUtils.makeDirsForFile(outFile); try { Files.write(outFile.toPath(), data); } catch (Exception e) { LOG.warn("Resource '{}' not saved, got exception", rc.getName(), e); } return; case RES_LINK: ResourceFile resFile = rc.getResLink(); FileUtils.makeDirsForFile(outFile); try { saveResourceFile(resFile, outFile); } catch (Exception e) { LOG.warn("Resource '{}' not saved, got exception", rc.getName(), e); } return; default: LOG.warn("Resource '{}' not saved, unknown type", rc.getName()); break; } } private void saveResourceFile(ResourceFile resFile, File outFile) throws JadxException { ResourcesLoader.decodeStream(resFile, (size, is) -> { Path target = outFile.toPath(); try { Files.copy(is, target, StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { Files.deleteIfExists(target); // delete partially written file throw new JadxRuntimeException("Resource file save error", e); } return null; }); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/StringFormattedCheck.java ================================================ package jadx.core.xmlgen; import java.util.ArrayList; import java.util.List; import java.util.Objects; /* * This class contains source code form https://github.com/iBotPeaches/Apktool/ * see: * https://github.com/iBotPeaches/Apktool/blob/master/brut.apktool/apktool-lib/src/main/java/brut/ * androlib/res/xml/ResXmlEncoders.java */ public class StringFormattedCheck { public static boolean hasMultipleNonPositionalSubstitutions(String str) { Duo, List> tuple = findSubstitutions(str, 4); return !tuple.m1.isEmpty() && tuple.m1.size() + tuple.m2.size() > 1; } @SuppressWarnings("checkstyle:ClassTypeParameterName") private static class Duo { public final T1 m1; public final T2 m2; public Duo(T1 t1, T2 t2) { this.m1 = t1; this.m2 = t2; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } @SuppressWarnings("unchecked") final Duo other = (Duo) obj; if (!Objects.equals(this.m1, other.m1)) { return false; } return Objects.equals(this.m2, other.m2); } @Override public int hashCode() { int hash = 3; hash = 71 * hash + (this.m1 != null ? this.m1.hashCode() : 0); hash = 71 * hash + (this.m2 != null ? this.m2.hashCode() : 0); return hash; } } /** * It returns a tuple of: * - a list of offsets of non positional substitutions. non-pos is defined as any "%" which isn't * "%%" nor "%\d+\$" * - a list of offsets of positional substitutions */ @SuppressWarnings({ "checkstyle:NeedBraces", "checkstyle:EmptyStatement" }) private static Duo, List> findSubstitutions(String str, int nonPosMax) { if (nonPosMax == -1) { nonPosMax = Integer.MAX_VALUE; } int pos; int pos2 = 0; List nonPositional = new ArrayList<>(); List positional = new ArrayList<>(); if (str == null) { return new Duo<>(nonPositional, positional); } int length = str.length(); while ((pos = str.indexOf('%', pos2)) != -1) { pos2 = pos + 1; if (pos2 == length) { nonPositional.add(pos); break; } char c = str.charAt(pos2++); if (c == '%') { continue; } if (c >= '0' && c <= '9' && pos2 < length) { while ((c = str.charAt(pos2++)) >= '0' && c <= '9' && pos2 < length) ; if (c == '$') { positional.add(pos); continue; } } nonPositional.add(pos); if (nonPositional.size() >= nonPosMax) { break; } } return new Duo<>(nonPositional, positional); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/XMLChar.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jadx.core.xmlgen; import java.util.Arrays; /** * This class defines the basic XML character properties. The data * in this class can be used to verify that a character is a valid * XML character or if the character is a space, name start, or name * character. *

* A series of convenience methods are supplied to ease the burden * of the developer. Because inlining the checks can improve per * character performance, the tables of character properties are * public. Using the character as an index into the CHARS * array and applying the appropriate mask flag (e.g. * MASK_VALID), yields the same results as calling the * convenience methods. There is one exception: check the comments * for the isValid method for details. * * @author Glenn Marcy, IBM * @author Andy Clark, IBM * @author Eric Ye, IBM * @author Arnaud Le Hors, IBM * @author Michael Glavassevich, IBM * @author Rahul Srivastava, Sun Microsystems Inc. * @version $Id: XMLChar.java 674378 2008-07-07 00:52:45Z mrglavas $ */ public class XMLChar { // // Constants // /** * Character flags. */ private static final byte[] CHARS = new byte[1 << 16]; /** * Valid character mask. */ public static final int MASK_VALID = 0x01; /** * Space character mask. */ public static final int MASK_SPACE = 0x02; /** * Name start character mask. */ public static final int MASK_NAME_START = 0x04; /** * Name character mask. */ public static final int MASK_NAME = 0x08; /** * Pubid character mask. */ public static final int MASK_PUBID = 0x10; /** * Content character mask. Special characters are those that can * be considered the start of markup, such as '<' and '&'. * The various newline characters are considered special as well. * All other valid XML characters can be considered content. *

* This is an optimization for the inner loop of character scanning. */ public static final int MASK_CONTENT = 0x20; /** * NCName start character mask. */ public static final int MASK_NCNAME_START = 0x40; /** * NCName character mask. */ public static final int MASK_NCNAME = 0x80; // // Static initialization // static { // Initializing the Character Flag Array // Code generated by: XMLCharGenerator. CHARS[9] = 35; CHARS[10] = 19; CHARS[13] = 19; CHARS[32] = 51; CHARS[33] = 49; CHARS[34] = 33; Arrays.fill(CHARS, 35, 38, (byte) 49); // Fill 3 of value (byte) 49 CHARS[38] = 1; Arrays.fill(CHARS, 39, 45, (byte) 49); // Fill 6 of value (byte) 49 Arrays.fill(CHARS, 45, 47, (byte) -71); // Fill 2 of value (byte) -71 CHARS[47] = 49; Arrays.fill(CHARS, 48, 58, (byte) -71); // Fill 10 of value (byte) -71 CHARS[58] = 61; CHARS[59] = 49; CHARS[60] = 1; CHARS[61] = 49; CHARS[62] = 33; Arrays.fill(CHARS, 63, 65, (byte) 49); // Fill 2 of value (byte) 49 Arrays.fill(CHARS, 65, 91, (byte) -3); // Fill 26 of value (byte) -3 Arrays.fill(CHARS, 91, 93, (byte) 33); // Fill 2 of value (byte) 33 CHARS[93] = 1; CHARS[94] = 33; CHARS[95] = -3; CHARS[96] = 33; Arrays.fill(CHARS, 97, 123, (byte) -3); // Fill 26 of value (byte) -3 Arrays.fill(CHARS, 123, 183, (byte) 33); // Fill 60 of value (byte) 33 CHARS[183] = -87; Arrays.fill(CHARS, 184, 192, (byte) 33); // Fill 8 of value (byte) 33 Arrays.fill(CHARS, 192, 215, (byte) -19); // Fill 23 of value (byte) -19 CHARS[215] = 33; Arrays.fill(CHARS, 216, 247, (byte) -19); // Fill 31 of value (byte) -19 CHARS[247] = 33; Arrays.fill(CHARS, 248, 306, (byte) -19); // Fill 58 of value (byte) -19 Arrays.fill(CHARS, 306, 308, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 308, 319, (byte) -19); // Fill 11 of value (byte) -19 Arrays.fill(CHARS, 319, 321, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 321, 329, (byte) -19); // Fill 8 of value (byte) -19 CHARS[329] = 33; Arrays.fill(CHARS, 330, 383, (byte) -19); // Fill 53 of value (byte) -19 CHARS[383] = 33; Arrays.fill(CHARS, 384, 452, (byte) -19); // Fill 68 of value (byte) -19 Arrays.fill(CHARS, 452, 461, (byte) 33); // Fill 9 of value (byte) 33 Arrays.fill(CHARS, 461, 497, (byte) -19); // Fill 36 of value (byte) -19 Arrays.fill(CHARS, 497, 500, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 500, 502, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 502, 506, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 506, 536, (byte) -19); // Fill 30 of value (byte) -19 Arrays.fill(CHARS, 536, 592, (byte) 33); // Fill 56 of value (byte) 33 Arrays.fill(CHARS, 592, 681, (byte) -19); // Fill 89 of value (byte) -19 Arrays.fill(CHARS, 681, 699, (byte) 33); // Fill 18 of value (byte) 33 Arrays.fill(CHARS, 699, 706, (byte) -19); // Fill 7 of value (byte) -19 Arrays.fill(CHARS, 706, 720, (byte) 33); // Fill 14 of value (byte) 33 Arrays.fill(CHARS, 720, 722, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 722, 768, (byte) 33); // Fill 46 of value (byte) 33 Arrays.fill(CHARS, 768, 838, (byte) -87); // Fill 70 of value (byte) -87 Arrays.fill(CHARS, 838, 864, (byte) 33); // Fill 26 of value (byte) 33 Arrays.fill(CHARS, 864, 866, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 866, 902, (byte) 33); // Fill 36 of value (byte) 33 CHARS[902] = -19; CHARS[903] = -87; Arrays.fill(CHARS, 904, 907, (byte) -19); // Fill 3 of value (byte) -19 CHARS[907] = 33; CHARS[908] = -19; CHARS[909] = 33; Arrays.fill(CHARS, 910, 930, (byte) -19); // Fill 20 of value (byte) -19 CHARS[930] = 33; Arrays.fill(CHARS, 931, 975, (byte) -19); // Fill 44 of value (byte) -19 CHARS[975] = 33; Arrays.fill(CHARS, 976, 983, (byte) -19); // Fill 7 of value (byte) -19 Arrays.fill(CHARS, 983, 986, (byte) 33); // Fill 3 of value (byte) 33 CHARS[986] = -19; CHARS[987] = 33; CHARS[988] = -19; CHARS[989] = 33; CHARS[990] = -19; CHARS[991] = 33; CHARS[992] = -19; CHARS[993] = 33; Arrays.fill(CHARS, 994, 1012, (byte) -19); // Fill 18 of value (byte) -19 Arrays.fill(CHARS, 1012, 1025, (byte) 33); // Fill 13 of value (byte) 33 Arrays.fill(CHARS, 1025, 1037, (byte) -19); // Fill 12 of value (byte) -19 CHARS[1037] = 33; Arrays.fill(CHARS, 1038, 1104, (byte) -19); // Fill 66 of value (byte) -19 CHARS[1104] = 33; Arrays.fill(CHARS, 1105, 1117, (byte) -19); // Fill 12 of value (byte) -19 CHARS[1117] = 33; Arrays.fill(CHARS, 1118, 1154, (byte) -19); // Fill 36 of value (byte) -19 CHARS[1154] = 33; Arrays.fill(CHARS, 1155, 1159, (byte) -87); // Fill 4 of value (byte) -87 Arrays.fill(CHARS, 1159, 1168, (byte) 33); // Fill 9 of value (byte) 33 Arrays.fill(CHARS, 1168, 1221, (byte) -19); // Fill 53 of value (byte) -19 Arrays.fill(CHARS, 1221, 1223, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 1223, 1225, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 1225, 1227, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 1227, 1229, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 1229, 1232, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 1232, 1260, (byte) -19); // Fill 28 of value (byte) -19 Arrays.fill(CHARS, 1260, 1262, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 1262, 1270, (byte) -19); // Fill 8 of value (byte) -19 Arrays.fill(CHARS, 1270, 1272, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 1272, 1274, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 1274, 1329, (byte) 33); // Fill 55 of value (byte) 33 Arrays.fill(CHARS, 1329, 1367, (byte) -19); // Fill 38 of value (byte) -19 Arrays.fill(CHARS, 1367, 1369, (byte) 33); // Fill 2 of value (byte) 33 CHARS[1369] = -19; Arrays.fill(CHARS, 1370, 1377, (byte) 33); // Fill 7 of value (byte) 33 Arrays.fill(CHARS, 1377, 1415, (byte) -19); // Fill 38 of value (byte) -19 Arrays.fill(CHARS, 1415, 1425, (byte) 33); // Fill 10 of value (byte) 33 Arrays.fill(CHARS, 1425, 1442, (byte) -87); // Fill 17 of value (byte) -87 CHARS[1442] = 33; Arrays.fill(CHARS, 1443, 1466, (byte) -87); // Fill 23 of value (byte) -87 CHARS[1466] = 33; Arrays.fill(CHARS, 1467, 1470, (byte) -87); // Fill 3 of value (byte) -87 CHARS[1470] = 33; CHARS[1471] = -87; CHARS[1472] = 33; Arrays.fill(CHARS, 1473, 1475, (byte) -87); // Fill 2 of value (byte) -87 CHARS[1475] = 33; CHARS[1476] = -87; Arrays.fill(CHARS, 1477, 1488, (byte) 33); // Fill 11 of value (byte) 33 Arrays.fill(CHARS, 1488, 1515, (byte) -19); // Fill 27 of value (byte) -19 Arrays.fill(CHARS, 1515, 1520, (byte) 33); // Fill 5 of value (byte) 33 Arrays.fill(CHARS, 1520, 1523, (byte) -19); // Fill 3 of value (byte) -19 Arrays.fill(CHARS, 1523, 1569, (byte) 33); // Fill 46 of value (byte) 33 Arrays.fill(CHARS, 1569, 1595, (byte) -19); // Fill 26 of value (byte) -19 Arrays.fill(CHARS, 1595, 1600, (byte) 33); // Fill 5 of value (byte) 33 CHARS[1600] = -87; Arrays.fill(CHARS, 1601, 1611, (byte) -19); // Fill 10 of value (byte) -19 Arrays.fill(CHARS, 1611, 1619, (byte) -87); // Fill 8 of value (byte) -87 Arrays.fill(CHARS, 1619, 1632, (byte) 33); // Fill 13 of value (byte) 33 Arrays.fill(CHARS, 1632, 1642, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 1642, 1648, (byte) 33); // Fill 6 of value (byte) 33 CHARS[1648] = -87; Arrays.fill(CHARS, 1649, 1720, (byte) -19); // Fill 71 of value (byte) -19 Arrays.fill(CHARS, 1720, 1722, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 1722, 1727, (byte) -19); // Fill 5 of value (byte) -19 CHARS[1727] = 33; Arrays.fill(CHARS, 1728, 1743, (byte) -19); // Fill 15 of value (byte) -19 CHARS[1743] = 33; Arrays.fill(CHARS, 1744, 1748, (byte) -19); // Fill 4 of value (byte) -19 CHARS[1748] = 33; CHARS[1749] = -19; Arrays.fill(CHARS, 1750, 1765, (byte) -87); // Fill 15 of value (byte) -87 Arrays.fill(CHARS, 1765, 1767, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 1767, 1769, (byte) -87); // Fill 2 of value (byte) -87 CHARS[1769] = 33; Arrays.fill(CHARS, 1770, 1774, (byte) -87); // Fill 4 of value (byte) -87 Arrays.fill(CHARS, 1774, 1776, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 1776, 1786, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 1786, 2305, (byte) 33); // Fill 519 of value (byte) 33 Arrays.fill(CHARS, 2305, 2308, (byte) -87); // Fill 3 of value (byte) -87 CHARS[2308] = 33; Arrays.fill(CHARS, 2309, 2362, (byte) -19); // Fill 53 of value (byte) -19 Arrays.fill(CHARS, 2362, 2364, (byte) 33); // Fill 2 of value (byte) 33 CHARS[2364] = -87; CHARS[2365] = -19; Arrays.fill(CHARS, 2366, 2382, (byte) -87); // Fill 16 of value (byte) -87 Arrays.fill(CHARS, 2382, 2385, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 2385, 2389, (byte) -87); // Fill 4 of value (byte) -87 Arrays.fill(CHARS, 2389, 2392, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 2392, 2402, (byte) -19); // Fill 10 of value (byte) -19 Arrays.fill(CHARS, 2402, 2404, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 2404, 2406, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2406, 2416, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 2416, 2433, (byte) 33); // Fill 17 of value (byte) 33 Arrays.fill(CHARS, 2433, 2436, (byte) -87); // Fill 3 of value (byte) -87 CHARS[2436] = 33; Arrays.fill(CHARS, 2437, 2445, (byte) -19); // Fill 8 of value (byte) -19 Arrays.fill(CHARS, 2445, 2447, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2447, 2449, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 2449, 2451, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2451, 2473, (byte) -19); // Fill 22 of value (byte) -19 CHARS[2473] = 33; Arrays.fill(CHARS, 2474, 2481, (byte) -19); // Fill 7 of value (byte) -19 CHARS[2481] = 33; CHARS[2482] = -19; Arrays.fill(CHARS, 2483, 2486, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 2486, 2490, (byte) -19); // Fill 4 of value (byte) -19 Arrays.fill(CHARS, 2490, 2492, (byte) 33); // Fill 2 of value (byte) 33 CHARS[2492] = -87; CHARS[2493] = 33; Arrays.fill(CHARS, 2494, 2501, (byte) -87); // Fill 7 of value (byte) -87 Arrays.fill(CHARS, 2501, 2503, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2503, 2505, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 2505, 2507, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2507, 2510, (byte) -87); // Fill 3 of value (byte) -87 Arrays.fill(CHARS, 2510, 2519, (byte) 33); // Fill 9 of value (byte) 33 CHARS[2519] = -87; Arrays.fill(CHARS, 2520, 2524, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 2524, 2526, (byte) -19); // Fill 2 of value (byte) -19 CHARS[2526] = 33; Arrays.fill(CHARS, 2527, 2530, (byte) -19); // Fill 3 of value (byte) -19 Arrays.fill(CHARS, 2530, 2532, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 2532, 2534, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2534, 2544, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 2544, 2546, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 2546, 2562, (byte) 33); // Fill 16 of value (byte) 33 CHARS[2562] = -87; Arrays.fill(CHARS, 2563, 2565, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2565, 2571, (byte) -19); // Fill 6 of value (byte) -19 Arrays.fill(CHARS, 2571, 2575, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 2575, 2577, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 2577, 2579, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2579, 2601, (byte) -19); // Fill 22 of value (byte) -19 CHARS[2601] = 33; Arrays.fill(CHARS, 2602, 2609, (byte) -19); // Fill 7 of value (byte) -19 CHARS[2609] = 33; Arrays.fill(CHARS, 2610, 2612, (byte) -19); // Fill 2 of value (byte) -19 CHARS[2612] = 33; Arrays.fill(CHARS, 2613, 2615, (byte) -19); // Fill 2 of value (byte) -19 CHARS[2615] = 33; Arrays.fill(CHARS, 2616, 2618, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 2618, 2620, (byte) 33); // Fill 2 of value (byte) 33 CHARS[2620] = -87; CHARS[2621] = 33; Arrays.fill(CHARS, 2622, 2627, (byte) -87); // Fill 5 of value (byte) -87 Arrays.fill(CHARS, 2627, 2631, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 2631, 2633, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 2633, 2635, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2635, 2638, (byte) -87); // Fill 3 of value (byte) -87 Arrays.fill(CHARS, 2638, 2649, (byte) 33); // Fill 11 of value (byte) 33 Arrays.fill(CHARS, 2649, 2653, (byte) -19); // Fill 4 of value (byte) -19 CHARS[2653] = 33; CHARS[2654] = -19; Arrays.fill(CHARS, 2655, 2662, (byte) 33); // Fill 7 of value (byte) 33 Arrays.fill(CHARS, 2662, 2674, (byte) -87); // Fill 12 of value (byte) -87 Arrays.fill(CHARS, 2674, 2677, (byte) -19); // Fill 3 of value (byte) -19 Arrays.fill(CHARS, 2677, 2689, (byte) 33); // Fill 12 of value (byte) 33 Arrays.fill(CHARS, 2689, 2692, (byte) -87); // Fill 3 of value (byte) -87 CHARS[2692] = 33; Arrays.fill(CHARS, 2693, 2700, (byte) -19); // Fill 7 of value (byte) -19 CHARS[2700] = 33; CHARS[2701] = -19; CHARS[2702] = 33; Arrays.fill(CHARS, 2703, 2706, (byte) -19); // Fill 3 of value (byte) -19 CHARS[2706] = 33; Arrays.fill(CHARS, 2707, 2729, (byte) -19); // Fill 22 of value (byte) -19 CHARS[2729] = 33; Arrays.fill(CHARS, 2730, 2737, (byte) -19); // Fill 7 of value (byte) -19 CHARS[2737] = 33; Arrays.fill(CHARS, 2738, 2740, (byte) -19); // Fill 2 of value (byte) -19 CHARS[2740] = 33; Arrays.fill(CHARS, 2741, 2746, (byte) -19); // Fill 5 of value (byte) -19 Arrays.fill(CHARS, 2746, 2748, (byte) 33); // Fill 2 of value (byte) 33 CHARS[2748] = -87; CHARS[2749] = -19; Arrays.fill(CHARS, 2750, 2758, (byte) -87); // Fill 8 of value (byte) -87 CHARS[2758] = 33; Arrays.fill(CHARS, 2759, 2762, (byte) -87); // Fill 3 of value (byte) -87 CHARS[2762] = 33; Arrays.fill(CHARS, 2763, 2766, (byte) -87); // Fill 3 of value (byte) -87 Arrays.fill(CHARS, 2766, 2784, (byte) 33); // Fill 18 of value (byte) 33 CHARS[2784] = -19; Arrays.fill(CHARS, 2785, 2790, (byte) 33); // Fill 5 of value (byte) 33 Arrays.fill(CHARS, 2790, 2800, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 2800, 2817, (byte) 33); // Fill 17 of value (byte) 33 Arrays.fill(CHARS, 2817, 2820, (byte) -87); // Fill 3 of value (byte) -87 CHARS[2820] = 33; Arrays.fill(CHARS, 2821, 2829, (byte) -19); // Fill 8 of value (byte) -19 Arrays.fill(CHARS, 2829, 2831, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2831, 2833, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 2833, 2835, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2835, 2857, (byte) -19); // Fill 22 of value (byte) -19 CHARS[2857] = 33; Arrays.fill(CHARS, 2858, 2865, (byte) -19); // Fill 7 of value (byte) -19 CHARS[2865] = 33; Arrays.fill(CHARS, 2866, 2868, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 2868, 2870, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2870, 2874, (byte) -19); // Fill 4 of value (byte) -19 Arrays.fill(CHARS, 2874, 2876, (byte) 33); // Fill 2 of value (byte) 33 CHARS[2876] = -87; CHARS[2877] = -19; Arrays.fill(CHARS, 2878, 2884, (byte) -87); // Fill 6 of value (byte) -87 Arrays.fill(CHARS, 2884, 2887, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 2887, 2889, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 2889, 2891, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 2891, 2894, (byte) -87); // Fill 3 of value (byte) -87 Arrays.fill(CHARS, 2894, 2902, (byte) 33); // Fill 8 of value (byte) 33 Arrays.fill(CHARS, 2902, 2904, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 2904, 2908, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 2908, 2910, (byte) -19); // Fill 2 of value (byte) -19 CHARS[2910] = 33; Arrays.fill(CHARS, 2911, 2914, (byte) -19); // Fill 3 of value (byte) -19 Arrays.fill(CHARS, 2914, 2918, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 2918, 2928, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 2928, 2946, (byte) 33); // Fill 18 of value (byte) 33 Arrays.fill(CHARS, 2946, 2948, (byte) -87); // Fill 2 of value (byte) -87 CHARS[2948] = 33; Arrays.fill(CHARS, 2949, 2955, (byte) -19); // Fill 6 of value (byte) -19 Arrays.fill(CHARS, 2955, 2958, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 2958, 2961, (byte) -19); // Fill 3 of value (byte) -19 CHARS[2961] = 33; Arrays.fill(CHARS, 2962, 2966, (byte) -19); // Fill 4 of value (byte) -19 Arrays.fill(CHARS, 2966, 2969, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 2969, 2971, (byte) -19); // Fill 2 of value (byte) -19 CHARS[2971] = 33; CHARS[2972] = -19; CHARS[2973] = 33; Arrays.fill(CHARS, 2974, 2976, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 2976, 2979, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 2979, 2981, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 2981, 2984, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 2984, 2987, (byte) -19); // Fill 3 of value (byte) -19 Arrays.fill(CHARS, 2987, 2990, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 2990, 2998, (byte) -19); // Fill 8 of value (byte) -19 CHARS[2998] = 33; Arrays.fill(CHARS, 2999, 3002, (byte) -19); // Fill 3 of value (byte) -19 Arrays.fill(CHARS, 3002, 3006, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 3006, 3011, (byte) -87); // Fill 5 of value (byte) -87 Arrays.fill(CHARS, 3011, 3014, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 3014, 3017, (byte) -87); // Fill 3 of value (byte) -87 CHARS[3017] = 33; Arrays.fill(CHARS, 3018, 3022, (byte) -87); // Fill 4 of value (byte) -87 Arrays.fill(CHARS, 3022, 3031, (byte) 33); // Fill 9 of value (byte) 33 CHARS[3031] = -87; Arrays.fill(CHARS, 3032, 3047, (byte) 33); // Fill 15 of value (byte) 33 Arrays.fill(CHARS, 3047, 3056, (byte) -87); // Fill 9 of value (byte) -87 Arrays.fill(CHARS, 3056, 3073, (byte) 33); // Fill 17 of value (byte) 33 Arrays.fill(CHARS, 3073, 3076, (byte) -87); // Fill 3 of value (byte) -87 CHARS[3076] = 33; Arrays.fill(CHARS, 3077, 3085, (byte) -19); // Fill 8 of value (byte) -19 CHARS[3085] = 33; Arrays.fill(CHARS, 3086, 3089, (byte) -19); // Fill 3 of value (byte) -19 CHARS[3089] = 33; Arrays.fill(CHARS, 3090, 3113, (byte) -19); // Fill 23 of value (byte) -19 CHARS[3113] = 33; Arrays.fill(CHARS, 3114, 3124, (byte) -19); // Fill 10 of value (byte) -19 CHARS[3124] = 33; Arrays.fill(CHARS, 3125, 3130, (byte) -19); // Fill 5 of value (byte) -19 Arrays.fill(CHARS, 3130, 3134, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 3134, 3141, (byte) -87); // Fill 7 of value (byte) -87 CHARS[3141] = 33; Arrays.fill(CHARS, 3142, 3145, (byte) -87); // Fill 3 of value (byte) -87 CHARS[3145] = 33; Arrays.fill(CHARS, 3146, 3150, (byte) -87); // Fill 4 of value (byte) -87 Arrays.fill(CHARS, 3150, 3157, (byte) 33); // Fill 7 of value (byte) 33 Arrays.fill(CHARS, 3157, 3159, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 3159, 3168, (byte) 33); // Fill 9 of value (byte) 33 Arrays.fill(CHARS, 3168, 3170, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 3170, 3174, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 3174, 3184, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 3184, 3202, (byte) 33); // Fill 18 of value (byte) 33 Arrays.fill(CHARS, 3202, 3204, (byte) -87); // Fill 2 of value (byte) -87 CHARS[3204] = 33; Arrays.fill(CHARS, 3205, 3213, (byte) -19); // Fill 8 of value (byte) -19 CHARS[3213] = 33; Arrays.fill(CHARS, 3214, 3217, (byte) -19); // Fill 3 of value (byte) -19 CHARS[3217] = 33; Arrays.fill(CHARS, 3218, 3241, (byte) -19); // Fill 23 of value (byte) -19 CHARS[3241] = 33; Arrays.fill(CHARS, 3242, 3252, (byte) -19); // Fill 10 of value (byte) -19 CHARS[3252] = 33; Arrays.fill(CHARS, 3253, 3258, (byte) -19); // Fill 5 of value (byte) -19 Arrays.fill(CHARS, 3258, 3262, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 3262, 3269, (byte) -87); // Fill 7 of value (byte) -87 CHARS[3269] = 33; Arrays.fill(CHARS, 3270, 3273, (byte) -87); // Fill 3 of value (byte) -87 CHARS[3273] = 33; Arrays.fill(CHARS, 3274, 3278, (byte) -87); // Fill 4 of value (byte) -87 Arrays.fill(CHARS, 3278, 3285, (byte) 33); // Fill 7 of value (byte) 33 Arrays.fill(CHARS, 3285, 3287, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 3287, 3294, (byte) 33); // Fill 7 of value (byte) 33 CHARS[3294] = -19; CHARS[3295] = 33; Arrays.fill(CHARS, 3296, 3298, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 3298, 3302, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 3302, 3312, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 3312, 3330, (byte) 33); // Fill 18 of value (byte) 33 Arrays.fill(CHARS, 3330, 3332, (byte) -87); // Fill 2 of value (byte) -87 CHARS[3332] = 33; Arrays.fill(CHARS, 3333, 3341, (byte) -19); // Fill 8 of value (byte) -19 CHARS[3341] = 33; Arrays.fill(CHARS, 3342, 3345, (byte) -19); // Fill 3 of value (byte) -19 CHARS[3345] = 33; Arrays.fill(CHARS, 3346, 3369, (byte) -19); // Fill 23 of value (byte) -19 CHARS[3369] = 33; Arrays.fill(CHARS, 3370, 3386, (byte) -19); // Fill 16 of value (byte) -19 Arrays.fill(CHARS, 3386, 3390, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 3390, 3396, (byte) -87); // Fill 6 of value (byte) -87 Arrays.fill(CHARS, 3396, 3398, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 3398, 3401, (byte) -87); // Fill 3 of value (byte) -87 CHARS[3401] = 33; Arrays.fill(CHARS, 3402, 3406, (byte) -87); // Fill 4 of value (byte) -87 Arrays.fill(CHARS, 3406, 3415, (byte) 33); // Fill 9 of value (byte) 33 CHARS[3415] = -87; Arrays.fill(CHARS, 3416, 3424, (byte) 33); // Fill 8 of value (byte) 33 Arrays.fill(CHARS, 3424, 3426, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 3426, 3430, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 3430, 3440, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 3440, 3585, (byte) 33); // Fill 145 of value (byte) 33 Arrays.fill(CHARS, 3585, 3631, (byte) -19); // Fill 46 of value (byte) -19 CHARS[3631] = 33; CHARS[3632] = -19; CHARS[3633] = -87; Arrays.fill(CHARS, 3634, 3636, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 3636, 3643, (byte) -87); // Fill 7 of value (byte) -87 Arrays.fill(CHARS, 3643, 3648, (byte) 33); // Fill 5 of value (byte) 33 Arrays.fill(CHARS, 3648, 3654, (byte) -19); // Fill 6 of value (byte) -19 Arrays.fill(CHARS, 3654, 3663, (byte) -87); // Fill 9 of value (byte) -87 CHARS[3663] = 33; Arrays.fill(CHARS, 3664, 3674, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 3674, 3713, (byte) 33); // Fill 39 of value (byte) 33 Arrays.fill(CHARS, 3713, 3715, (byte) -19); // Fill 2 of value (byte) -19 CHARS[3715] = 33; CHARS[3716] = -19; Arrays.fill(CHARS, 3717, 3719, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 3719, 3721, (byte) -19); // Fill 2 of value (byte) -19 CHARS[3721] = 33; CHARS[3722] = -19; Arrays.fill(CHARS, 3723, 3725, (byte) 33); // Fill 2 of value (byte) 33 CHARS[3725] = -19; Arrays.fill(CHARS, 3726, 3732, (byte) 33); // Fill 6 of value (byte) 33 Arrays.fill(CHARS, 3732, 3736, (byte) -19); // Fill 4 of value (byte) -19 CHARS[3736] = 33; Arrays.fill(CHARS, 3737, 3744, (byte) -19); // Fill 7 of value (byte) -19 CHARS[3744] = 33; Arrays.fill(CHARS, 3745, 3748, (byte) -19); // Fill 3 of value (byte) -19 CHARS[3748] = 33; CHARS[3749] = -19; CHARS[3750] = 33; CHARS[3751] = -19; Arrays.fill(CHARS, 3752, 3754, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 3754, 3756, (byte) -19); // Fill 2 of value (byte) -19 CHARS[3756] = 33; Arrays.fill(CHARS, 3757, 3759, (byte) -19); // Fill 2 of value (byte) -19 CHARS[3759] = 33; CHARS[3760] = -19; CHARS[3761] = -87; Arrays.fill(CHARS, 3762, 3764, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 3764, 3770, (byte) -87); // Fill 6 of value (byte) -87 CHARS[3770] = 33; Arrays.fill(CHARS, 3771, 3773, (byte) -87); // Fill 2 of value (byte) -87 CHARS[3773] = -19; Arrays.fill(CHARS, 3774, 3776, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 3776, 3781, (byte) -19); // Fill 5 of value (byte) -19 CHARS[3781] = 33; CHARS[3782] = -87; CHARS[3783] = 33; Arrays.fill(CHARS, 3784, 3790, (byte) -87); // Fill 6 of value (byte) -87 Arrays.fill(CHARS, 3790, 3792, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 3792, 3802, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 3802, 3864, (byte) 33); // Fill 62 of value (byte) 33 Arrays.fill(CHARS, 3864, 3866, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 3866, 3872, (byte) 33); // Fill 6 of value (byte) 33 Arrays.fill(CHARS, 3872, 3882, (byte) -87); // Fill 10 of value (byte) -87 Arrays.fill(CHARS, 3882, 3893, (byte) 33); // Fill 11 of value (byte) 33 CHARS[3893] = -87; CHARS[3894] = 33; CHARS[3895] = -87; CHARS[3896] = 33; CHARS[3897] = -87; Arrays.fill(CHARS, 3898, 3902, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 3902, 3904, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 3904, 3912, (byte) -19); // Fill 8 of value (byte) -19 CHARS[3912] = 33; Arrays.fill(CHARS, 3913, 3946, (byte) -19); // Fill 33 of value (byte) -19 Arrays.fill(CHARS, 3946, 3953, (byte) 33); // Fill 7 of value (byte) 33 Arrays.fill(CHARS, 3953, 3973, (byte) -87); // Fill 20 of value (byte) -87 CHARS[3973] = 33; Arrays.fill(CHARS, 3974, 3980, (byte) -87); // Fill 6 of value (byte) -87 Arrays.fill(CHARS, 3980, 3984, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 3984, 3990, (byte) -87); // Fill 6 of value (byte) -87 CHARS[3990] = 33; CHARS[3991] = -87; CHARS[3992] = 33; Arrays.fill(CHARS, 3993, 4014, (byte) -87); // Fill 21 of value (byte) -87 Arrays.fill(CHARS, 4014, 4017, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 4017, 4024, (byte) -87); // Fill 7 of value (byte) -87 CHARS[4024] = 33; CHARS[4025] = -87; Arrays.fill(CHARS, 4026, 4256, (byte) 33); // Fill 230 of value (byte) 33 Arrays.fill(CHARS, 4256, 4294, (byte) -19); // Fill 38 of value (byte) -19 Arrays.fill(CHARS, 4294, 4304, (byte) 33); // Fill 10 of value (byte) 33 Arrays.fill(CHARS, 4304, 4343, (byte) -19); // Fill 39 of value (byte) -19 Arrays.fill(CHARS, 4343, 4352, (byte) 33); // Fill 9 of value (byte) 33 CHARS[4352] = -19; CHARS[4353] = 33; Arrays.fill(CHARS, 4354, 4356, (byte) -19); // Fill 2 of value (byte) -19 CHARS[4356] = 33; Arrays.fill(CHARS, 4357, 4360, (byte) -19); // Fill 3 of value (byte) -19 CHARS[4360] = 33; CHARS[4361] = -19; CHARS[4362] = 33; Arrays.fill(CHARS, 4363, 4365, (byte) -19); // Fill 2 of value (byte) -19 CHARS[4365] = 33; Arrays.fill(CHARS, 4366, 4371, (byte) -19); // Fill 5 of value (byte) -19 Arrays.fill(CHARS, 4371, 4412, (byte) 33); // Fill 41 of value (byte) 33 CHARS[4412] = -19; CHARS[4413] = 33; CHARS[4414] = -19; CHARS[4415] = 33; CHARS[4416] = -19; Arrays.fill(CHARS, 4417, 4428, (byte) 33); // Fill 11 of value (byte) 33 CHARS[4428] = -19; CHARS[4429] = 33; CHARS[4430] = -19; CHARS[4431] = 33; CHARS[4432] = -19; Arrays.fill(CHARS, 4433, 4436, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 4436, 4438, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 4438, 4441, (byte) 33); // Fill 3 of value (byte) 33 CHARS[4441] = -19; Arrays.fill(CHARS, 4442, 4447, (byte) 33); // Fill 5 of value (byte) 33 Arrays.fill(CHARS, 4447, 4450, (byte) -19); // Fill 3 of value (byte) -19 CHARS[4450] = 33; CHARS[4451] = -19; CHARS[4452] = 33; CHARS[4453] = -19; CHARS[4454] = 33; CHARS[4455] = -19; CHARS[4456] = 33; CHARS[4457] = -19; Arrays.fill(CHARS, 4458, 4461, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 4461, 4463, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 4463, 4466, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 4466, 4468, (byte) -19); // Fill 2 of value (byte) -19 CHARS[4468] = 33; CHARS[4469] = -19; Arrays.fill(CHARS, 4470, 4510, (byte) 33); // Fill 40 of value (byte) 33 CHARS[4510] = -19; Arrays.fill(CHARS, 4511, 4520, (byte) 33); // Fill 9 of value (byte) 33 CHARS[4520] = -19; Arrays.fill(CHARS, 4521, 4523, (byte) 33); // Fill 2 of value (byte) 33 CHARS[4523] = -19; Arrays.fill(CHARS, 4524, 4526, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 4526, 4528, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 4528, 4535, (byte) 33); // Fill 7 of value (byte) 33 Arrays.fill(CHARS, 4535, 4537, (byte) -19); // Fill 2 of value (byte) -19 CHARS[4537] = 33; CHARS[4538] = -19; CHARS[4539] = 33; Arrays.fill(CHARS, 4540, 4547, (byte) -19); // Fill 7 of value (byte) -19 Arrays.fill(CHARS, 4547, 4587, (byte) 33); // Fill 40 of value (byte) 33 CHARS[4587] = -19; Arrays.fill(CHARS, 4588, 4592, (byte) 33); // Fill 4 of value (byte) 33 CHARS[4592] = -19; Arrays.fill(CHARS, 4593, 4601, (byte) 33); // Fill 8 of value (byte) 33 CHARS[4601] = -19; Arrays.fill(CHARS, 4602, 7680, (byte) 33); // Fill 3078 of value (byte) 33 Arrays.fill(CHARS, 7680, 7836, (byte) -19); // Fill 156 of value (byte) -19 Arrays.fill(CHARS, 7836, 7840, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 7840, 7930, (byte) -19); // Fill 90 of value (byte) -19 Arrays.fill(CHARS, 7930, 7936, (byte) 33); // Fill 6 of value (byte) 33 Arrays.fill(CHARS, 7936, 7958, (byte) -19); // Fill 22 of value (byte) -19 Arrays.fill(CHARS, 7958, 7960, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 7960, 7966, (byte) -19); // Fill 6 of value (byte) -19 Arrays.fill(CHARS, 7966, 7968, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 7968, 8006, (byte) -19); // Fill 38 of value (byte) -19 Arrays.fill(CHARS, 8006, 8008, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 8008, 8014, (byte) -19); // Fill 6 of value (byte) -19 Arrays.fill(CHARS, 8014, 8016, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 8016, 8024, (byte) -19); // Fill 8 of value (byte) -19 CHARS[8024] = 33; CHARS[8025] = -19; CHARS[8026] = 33; CHARS[8027] = -19; CHARS[8028] = 33; CHARS[8029] = -19; CHARS[8030] = 33; Arrays.fill(CHARS, 8031, 8062, (byte) -19); // Fill 31 of value (byte) -19 Arrays.fill(CHARS, 8062, 8064, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 8064, 8117, (byte) -19); // Fill 53 of value (byte) -19 CHARS[8117] = 33; Arrays.fill(CHARS, 8118, 8125, (byte) -19); // Fill 7 of value (byte) -19 CHARS[8125] = 33; CHARS[8126] = -19; Arrays.fill(CHARS, 8127, 8130, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 8130, 8133, (byte) -19); // Fill 3 of value (byte) -19 CHARS[8133] = 33; Arrays.fill(CHARS, 8134, 8141, (byte) -19); // Fill 7 of value (byte) -19 Arrays.fill(CHARS, 8141, 8144, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 8144, 8148, (byte) -19); // Fill 4 of value (byte) -19 Arrays.fill(CHARS, 8148, 8150, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 8150, 8156, (byte) -19); // Fill 6 of value (byte) -19 Arrays.fill(CHARS, 8156, 8160, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 8160, 8173, (byte) -19); // Fill 13 of value (byte) -19 Arrays.fill(CHARS, 8173, 8178, (byte) 33); // Fill 5 of value (byte) 33 Arrays.fill(CHARS, 8178, 8181, (byte) -19); // Fill 3 of value (byte) -19 CHARS[8181] = 33; Arrays.fill(CHARS, 8182, 8189, (byte) -19); // Fill 7 of value (byte) -19 Arrays.fill(CHARS, 8189, 8400, (byte) 33); // Fill 211 of value (byte) 33 Arrays.fill(CHARS, 8400, 8413, (byte) -87); // Fill 13 of value (byte) -87 Arrays.fill(CHARS, 8413, 8417, (byte) 33); // Fill 4 of value (byte) 33 CHARS[8417] = -87; Arrays.fill(CHARS, 8418, 8486, (byte) 33); // Fill 68 of value (byte) 33 CHARS[8486] = -19; Arrays.fill(CHARS, 8487, 8490, (byte) 33); // Fill 3 of value (byte) 33 Arrays.fill(CHARS, 8490, 8492, (byte) -19); // Fill 2 of value (byte) -19 Arrays.fill(CHARS, 8492, 8494, (byte) 33); // Fill 2 of value (byte) 33 CHARS[8494] = -19; Arrays.fill(CHARS, 8495, 8576, (byte) 33); // Fill 81 of value (byte) 33 Arrays.fill(CHARS, 8576, 8579, (byte) -19); // Fill 3 of value (byte) -19 Arrays.fill(CHARS, 8579, 12293, (byte) 33); // Fill 3714 of value (byte) 33 CHARS[12293] = -87; CHARS[12294] = 33; CHARS[12295] = -19; Arrays.fill(CHARS, 12296, 12321, (byte) 33); // Fill 25 of value (byte) 33 Arrays.fill(CHARS, 12321, 12330, (byte) -19); // Fill 9 of value (byte) -19 Arrays.fill(CHARS, 12330, 12336, (byte) -87); // Fill 6 of value (byte) -87 CHARS[12336] = 33; Arrays.fill(CHARS, 12337, 12342, (byte) -87); // Fill 5 of value (byte) -87 Arrays.fill(CHARS, 12342, 12353, (byte) 33); // Fill 11 of value (byte) 33 Arrays.fill(CHARS, 12353, 12437, (byte) -19); // Fill 84 of value (byte) -19 Arrays.fill(CHARS, 12437, 12441, (byte) 33); // Fill 4 of value (byte) 33 Arrays.fill(CHARS, 12441, 12443, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 12443, 12445, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 12445, 12447, (byte) -87); // Fill 2 of value (byte) -87 Arrays.fill(CHARS, 12447, 12449, (byte) 33); // Fill 2 of value (byte) 33 Arrays.fill(CHARS, 12449, 12539, (byte) -19); // Fill 90 of value (byte) -19 CHARS[12539] = 33; Arrays.fill(CHARS, 12540, 12543, (byte) -87); // Fill 3 of value (byte) -87 Arrays.fill(CHARS, 12543, 12549, (byte) 33); // Fill 6 of value (byte) 33 Arrays.fill(CHARS, 12549, 12589, (byte) -19); // Fill 40 of value (byte) -19 Arrays.fill(CHARS, 12589, 19968, (byte) 33); // Fill 7379 of value (byte) 33 Arrays.fill(CHARS, 19968, 40870, (byte) -19); // Fill 20902 of value (byte) -19 Arrays.fill(CHARS, 40870, 44032, (byte) 33); // Fill 3162 of value (byte) 33 Arrays.fill(CHARS, 44032, 55204, (byte) -19); // Fill 11172 of value (byte) -19 Arrays.fill(CHARS, 55204, 55296, (byte) 33); // Fill 92 of value (byte) 33 Arrays.fill(CHARS, 57344, 65534, (byte) 33); // Fill 8190 of value (byte) 33 } // () // // Public static methods // /** * Returns true if the specified character is a supplemental character. * * @param c The character to check. */ public static boolean isSupplemental(int c) { return (c >= 0x10000 && c <= 0x10FFFF); } /** * Returns true the supplemental character corresponding to the given * surrogates. * * @param h The high surrogate. * @param l The low surrogate. */ public static int supplemental(char h, char l) { return (h - 0xD800) * 0x400 + (l - 0xDC00) + 0x10000; } /** * Returns the high surrogate of a supplemental character * * @param c The supplemental character to "split". */ public static char highSurrogate(int c) { return (char) (((c - 0x00010000) >> 10) + 0xD800); } /** * Returns the low surrogate of a supplemental character * * @param c The supplemental character to "split". */ public static char lowSurrogate(int c) { return (char) (((c - 0x00010000) & 0x3FF) + 0xDC00); } /** * Returns whether the given character is a high surrogate * * @param c The character to check. */ public static boolean isHighSurrogate(int c) { return (0xD800 <= c && c <= 0xDBFF); } /** * Returns whether the given character is a low surrogate * * @param c The character to check. */ public static boolean isLowSurrogate(int c) { return (0xDC00 <= c && c <= 0xDFFF); } /** * Returns true if the specified character is valid. This method * also checks the surrogate character range from 0x10000 to 0x10FFFF. *

* If the program chooses to apply the mask directly to the * CHARS array, then they are responsible for checking * the surrogate character range. * * @param c The character to check. */ public static boolean isValid(int c) { return (c < 0x10000 && (CHARS[c] & MASK_VALID) != 0) || (0x10000 <= c && c <= 0x10FFFF); } // isValid(int):boolean /** * Returns true if the specified character is invalid. * * @param c The character to check. */ public static boolean isInvalid(int c) { return !isValid(c); } // isInvalid(int):boolean /** * Returns true if the specified character can be considered content. * * @param c The character to check. */ public static boolean isContent(int c) { return (c < 0x10000 && (CHARS[c] & MASK_CONTENT) != 0) || (0x10000 <= c && c <= 0x10FFFF); } // isContent(int):boolean /** * Returns true if the specified character can be considered markup. * Markup characters include '<', '&', and '%'. * * @param c The character to check. */ public static boolean isMarkup(int c) { return c == '<' || c == '&' || c == '%'; } // isMarkup(int):boolean /** * Returns true if the specified character is a space character * as defined by production [3] in the XML 1.0 specification. * * @param c The character to check. */ public static boolean isSpace(int c) { return c <= 0x20 && (CHARS[c] & MASK_SPACE) != 0; } // isSpace(int):boolean /** * Returns true if the specified character is a valid name start * character as defined by production [5] in the XML 1.0 * specification. * * @param c The character to check. */ public static boolean isNameStart(int c) { return c < 0x10000 && (CHARS[c] & MASK_NAME_START) != 0; } // isNameStart(int):boolean /** * Returns true if the specified character is a valid name * character as defined by production [4] in the XML 1.0 * specification. * * @param c The character to check. */ public static boolean isName(int c) { return c < 0x10000 && (CHARS[c] & MASK_NAME) != 0; } // isName(int):boolean /** * Returns true if the specified character is a valid NCName start * character as defined by production [4] in Namespaces in XML * recommendation. * * @param c The character to check. */ public static boolean isNCNameStart(int c) { return c < 0x10000 && (CHARS[c] & MASK_NCNAME_START) != 0; } // isNCNameStart(int):boolean /** * Returns true if the specified character is a valid NCName * character as defined by production [5] in Namespaces in XML * recommendation. * * @param c The character to check. */ public static boolean isNCName(int c) { return c < 0x10000 && (CHARS[c] & MASK_NCNAME) != 0; } // isNCName(int):boolean /** * Returns true if the specified character is a valid Pubid * character as defined by production [13] in the XML 1.0 * specification. * * @param c The character to check. */ public static boolean isPubid(int c) { return c < 0x10000 && (CHARS[c] & MASK_PUBID) != 0; } // isPubid(int):boolean /* * [5] Name ::= (Letter | '_' | ':') (NameChar)* */ /** * Check to see if a string is a valid Name according to [5] * in the XML 1.0 Recommendation * * @param name string to check * @return true if name is a valid Name */ public static boolean isValidName(String name) { final int length = name.length(); if (length == 0) { return false; } char ch = name.charAt(0); if (!isNameStart(ch)) { return false; } for (int i = 1; i < length; ++i) { ch = name.charAt(i); if (!isName(ch)) { return false; } } return true; } // isValidName(String):boolean /* * from the namespace rec * [4] NCName ::= (Letter | '_') (NCNameChar)* */ /** * Check to see if a string is a valid NCName according to [4] * from the XML Namespaces 1.0 Recommendation * * @param ncName string to check * @return true if name is a valid NCName */ public static boolean isValidNCName(String ncName) { final int length = ncName.length(); if (length == 0) { return false; } char ch = ncName.charAt(0); if (!isNCNameStart(ch)) { return false; } for (int i = 1; i < length; ++i) { ch = ncName.charAt(i); if (!isNCName(ch)) { return false; } } return true; } // isValidNCName(String):boolean /* * [7] Nmtoken ::= (NameChar)+ */ /** * Check to see if a string is a valid Nmtoken according to [7] * in the XML 1.0 Recommendation * * @param nmtoken string to check * @return true if nmtoken is a valid Nmtoken */ public static boolean isValidNmtoken(String nmtoken) { final int length = nmtoken.length(); if (length == 0) { return false; } for (int i = 0; i < length; ++i) { char ch = nmtoken.charAt(i); if (!isName(ch)) { return false; } } return true; } // isValidName(String):boolean // encodings /** * Returns true if the encoding name is a valid IANA encoding. * This method does not verify that there is a decoder available * for this encoding, only that the characters are valid for an * IANA encoding name. * * @param ianaEncoding The IANA encoding name. */ public static boolean isValidIANAEncoding(String ianaEncoding) { if (ianaEncoding != null) { int length = ianaEncoding.length(); if (length > 0) { char c = ianaEncoding.charAt(0); if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { for (int i = 1; i < length; i++) { c = ianaEncoding.charAt(i); if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '.' && c != '_' && c != '-') { return false; } } return true; } } } return false; } // isValidIANAEncoding(String):boolean /** * Returns true if the encoding name is a valid Java encoding. * This method does not verify that there is a decoder available * for this encoding, only that the characters are valid for an * Java encoding name. * * @param javaEncoding The Java encoding name. */ public static boolean isValidJavaEncoding(String javaEncoding) { if (javaEncoding != null) { int length = javaEncoding.length(); if (length > 0) { for (int i = 1; i < length; i++) { char c = javaEncoding.charAt(i); if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '.' && c != '_' && c != '-') { return false; } } return true; } } return false; } // isValidIANAEncoding(String):boolean // other methods /** * Trims space characters as defined by production [3] in * the XML 1.0 specification from both ends of the given string. * * @param value the string to be trimmed * @return the given string with the space characters trimmed * from both ends */ public static String trim(String value) { int start; int end; final int lengthMinusOne = value.length() - 1; for (start = 0; start <= lengthMinusOne; ++start) { if (!isSpace(value.charAt(start))) { break; } } for (end = lengthMinusOne; end >= start; --end) { if (!isSpace(value.charAt(end))) { break; } } if (start == 0 && end == lengthMinusOne) { return value; } if (start > lengthMinusOne) { return ""; } return value.substring(start, end + 1); } // trim(String):String } // class XMLChar ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/XmlDeobf.java ================================================ package jadx.core.xmlgen; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.RootNode; /* * Modifies android:name attributes and xml tags which were changed during deobfuscation */ public class XmlDeobf { private XmlDeobf() { } @Nullable public static String deobfClassName(RootNode root, String potentialClassName, String packageName) { if (potentialClassName.indexOf('.') == -1) { return null; } if (packageName != null && potentialClassName.startsWith(".")) { potentialClassName = packageName + potentialClassName; } ArgType clsType = ArgType.object(potentialClassName); ClassInfo classInfo = root.getInfoStorage().getCls(clsType); if (classInfo == null) { // unknown class reference return null; } return classInfo.getAliasFullName(); } public static boolean isDuplicatedAttr(String attrFullName, Set attrCache) { return !attrCache.add(attrFullName); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/XmlGenUtils.java ================================================ package jadx.core.xmlgen; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.text.NumberFormat; import java.util.HashSet; import java.util.Locale; import java.util.Set; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ValuesParser; public class XmlGenUtils { private XmlGenUtils() { } public static byte[] readData(InputStream i) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] data = new byte[16384]; int read; while ((read = i.read(data, 0, data.length)) != -1) { buffer.write(data, 0, read); } return buffer.toByteArray(); } public static ICodeInfo makeXmlDump(ICodeWriter writer, ResourceStorage resStorage) { writer.add(""); writer.startLine(""); writer.incIndent(); Set addedValues = new HashSet<>(); for (ResourceEntry ri : resStorage.getResources()) { if (addedValues.add(ri.getTypeName() + '.' + ri.getKeyName())) { String format = String.format("", ri.getTypeName(), ri.getKeyName(), ri.getId()); writer.startLine(format); } } writer.decIndent(); writer.startLine(""); return writer.finish(); } public static String decodeComplex(int data, boolean isFraction) { double value = (data & ParserConstants.COMPLEX_MANTISSA_MASK << ParserConstants.COMPLEX_MANTISSA_SHIFT) * ParserConstants.RADIX_MULTS[data >> ParserConstants.COMPLEX_RADIX_SHIFT & ParserConstants.COMPLEX_RADIX_MASK]; int unitType = data & ParserConstants.COMPLEX_UNIT_MASK; String unit; if (isFraction) { value *= 100; switch (unitType) { case ParserConstants.COMPLEX_UNIT_FRACTION: unit = "%"; break; case ParserConstants.COMPLEX_UNIT_FRACTION_PARENT: unit = "%p"; break; default: unit = "?f" + Integer.toHexString(unitType); } } else { switch (unitType) { case ParserConstants.COMPLEX_UNIT_PX: unit = "px"; break; case ParserConstants.COMPLEX_UNIT_DIP: unit = "dp"; break; case ParserConstants.COMPLEX_UNIT_SP: unit = "sp"; break; case ParserConstants.COMPLEX_UNIT_PT: unit = "pt"; break; case ParserConstants.COMPLEX_UNIT_IN: unit = "in"; break; case ParserConstants.COMPLEX_UNIT_MM: unit = "mm"; break; default: unit = "?d" + Integer.toHexString(unitType); } } return doubleToString(value) + unit; } public static String doubleToString(double value) { if (Double.compare(value, Math.floor(value)) == 0 && !Double.isInfinite(value)) { return Integer.toString((int) value); } // remove trailing zeroes NumberFormat f = NumberFormat.getInstance(Locale.ROOT); f.setMaximumFractionDigits(4); f.setMinimumIntegerDigits(1); return f.format(value); } public static String floatToString(float value) { return doubleToString(value); } public static String getAttrTypeAsString(int type) { String s = ""; if ((type & ValuesParser.ATTR_TYPE_REFERENCE) != 0) { s += "|reference"; } if ((type & ValuesParser.ATTR_TYPE_STRING) != 0) { s += "|string"; } if ((type & ValuesParser.ATTR_TYPE_INTEGER) != 0) { s += "|integer"; } if ((type & ValuesParser.ATTR_TYPE_BOOLEAN) != 0) { s += "|boolean"; } if ((type & ValuesParser.ATTR_TYPE_COLOR) != 0) { s += "|color"; } if ((type & ValuesParser.ATTR_TYPE_FLOAT) != 0) { s += "|float"; } if ((type & ValuesParser.ATTR_TYPE_DIMENSION) != 0) { s += "|dimension"; } if ((type & ValuesParser.ATTR_TYPE_FRACTION) != 0) { s += "|fraction"; } if (s.isEmpty()) { return null; } return s.substring(1); } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java ================================================ /** * Copyright (C) 2018 Ryszard Wiśniewski * Copyright (C) 2018 Connor Tumbleson *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jadx.core.xmlgen.entry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Original source code can be found * here */ public class EntryConfig { private static final Logger LOG = LoggerFactory.getLogger(EntryConfig.class); public final short mcc; public final short mnc; public final char[] language; public final char[] region; public final byte orientation; public final byte touchscreen; public final int density; public final byte keyboard; public final byte navigation; public final byte inputFlags; public final byte grammaticalInflection; public final short screenWidth; public final short screenHeight; public final short sdkVersion; public final byte screenLayout; public final byte uiMode; public final short smallestScreenWidthDp; public final short screenWidthDp; public final short screenHeightDp; private final char[] localeScript; private final char[] localeVariant; private final byte screenLayout2; private final byte colorMode; public final boolean isInvalid; private final String mQualifiers; private final int size; public EntryConfig(short mcc, short mnc, char[] language, char[] region, byte orientation, byte touchscreen, int density, byte keyboard, byte navigation, byte inputFlags, byte grammaticalInflection, short screenWidth, short screenHeight, short sdkVersion, byte screenLayout, byte uiMode, short smallestScreenWidthDp, short screenWidthDp, short screenHeightDp, char[] localeScript, char[] localeVariant, byte screenLayout2, byte colorMode, boolean isInvalid, int size) { if (orientation < 0 || orientation > 3) { LOG.warn("Invalid orientation value: {}", orientation); orientation = 0; isInvalid = true; } if (touchscreen < 0 || touchscreen > 3) { LOG.warn("Invalid touchscreen value: {}", touchscreen); touchscreen = 0; isInvalid = true; } if (density < -1) { LOG.warn("Invalid density value: {}", density); density = 0; isInvalid = true; } if (keyboard < 0 || keyboard > 3) { LOG.warn("Invalid keyboard value: {}", keyboard); keyboard = 0; isInvalid = true; } if (navigation < 0 || navigation > 4) { LOG.warn("Invalid navigation value: {}", navigation); navigation = 0; isInvalid = true; } if (localeScript != null && localeScript.length != 0) { if (localeScript[0] == '\00') { localeScript = null; } } else { localeScript = null; } if (localeVariant != null && localeVariant.length != 0) { if (localeVariant[0] == '\00') { localeVariant = null; } } else { localeVariant = null; } this.mcc = mcc; this.mnc = mnc; this.language = language; this.region = region; this.orientation = orientation; this.touchscreen = touchscreen; this.density = density; this.keyboard = keyboard; this.navigation = navigation; this.inputFlags = inputFlags; this.grammaticalInflection = grammaticalInflection; this.screenWidth = screenWidth; this.screenHeight = screenHeight; this.sdkVersion = sdkVersion; this.screenLayout = screenLayout; this.uiMode = uiMode; this.smallestScreenWidthDp = smallestScreenWidthDp; this.screenWidthDp = screenWidthDp; this.screenHeightDp = screenHeightDp; this.localeScript = localeScript; this.localeVariant = localeVariant; this.screenLayout2 = screenLayout2; this.colorMode = colorMode; this.isInvalid = isInvalid; this.size = size; mQualifiers = generateQualifiers(); } public String getQualifiers() { return mQualifiers; } private String generateQualifiers() { StringBuilder ret = new StringBuilder(); if (mcc != 0) { ret.append("-mcc").append(String.format("%03d", mcc)); if (mnc != MNC_ZERO) { if (mnc != 0) { ret.append("-mnc"); if (size <= 32) { if (mnc > 0 && mnc < 10) { ret.append(String.format("%02d", mnc)); } else { ret.append(String.format("%03d", mnc)); } } else { ret.append(mnc); } } } else { ret.append("-mnc00"); } } else { if (mnc != 0) { ret.append("-mnc").append(mnc); } } ret.append(getLocaleString()); switch (grammaticalInflection) { case GRAMMATICAL_GENDER_NEUTER: ret.append("-neuter"); break; case GRAMMATICAL_GENDER_FEMININE: ret.append("-feminine"); break; case GRAMMATICAL_GENDER_MASCULINE: ret.append("-masculine"); break; } switch (screenLayout & MASK_LAYOUTDIR) { case SCREENLAYOUT_LAYOUTDIR_RTL: ret.append("-ldrtl"); break; case SCREENLAYOUT_LAYOUTDIR_LTR: ret.append("-ldltr"); break; } if (smallestScreenWidthDp != 0) { ret.append("-sw").append(smallestScreenWidthDp).append("dp"); } if (screenWidthDp != 0) { ret.append("-w").append(screenWidthDp).append("dp"); } if (screenHeightDp != 0) { ret.append("-h").append(screenHeightDp).append("dp"); } switch (screenLayout & MASK_SCREENSIZE) { case SCREENSIZE_SMALL: ret.append("-small"); break; case SCREENSIZE_NORMAL: ret.append("-normal"); break; case SCREENSIZE_LARGE: ret.append("-large"); break; case SCREENSIZE_XLARGE: ret.append("-xlarge"); break; } switch (screenLayout & MASK_SCREENLONG) { case SCREENLONG_YES: ret.append("-long"); break; case SCREENLONG_NO: ret.append("-notlong"); break; } switch (screenLayout2 & MASK_SCREENROUND) { case SCREENLAYOUT_ROUND_NO: ret.append("-notround"); break; case SCREENLAYOUT_ROUND_YES: ret.append("-round"); break; } switch (colorMode & COLOR_HDR_MASK) { case COLOR_HDR_YES: ret.append("-highdr"); break; case COLOR_HDR_NO: ret.append("-lowdr"); break; } switch (colorMode & COLOR_WIDE_MASK) { case COLOR_WIDE_YES: ret.append("-widecg"); break; case COLOR_WIDE_NO: ret.append("-nowidecg"); break; } switch (orientation) { case ORIENTATION_PORT: ret.append("-port"); break; case ORIENTATION_LAND: ret.append("-land"); break; case ORIENTATION_SQUARE: ret.append("-square"); break; } switch (uiMode & MASK_UI_MODE_TYPE) { case UI_MODE_TYPE_CAR: ret.append("-car"); break; case UI_MODE_TYPE_DESK: ret.append("-desk"); break; case UI_MODE_TYPE_TELEVISION: ret.append("-television"); break; case UI_MODE_TYPE_SMALLUI: ret.append("-smallui"); break; case UI_MODE_TYPE_MEDIUMUI: ret.append("-mediumui"); break; case UI_MODE_TYPE_LARGEUI: ret.append("-largeui"); break; case UI_MODE_TYPE_GODZILLAUI: ret.append("-godzillaui"); break; case UI_MODE_TYPE_HUGEUI: ret.append("-hugeui"); break; case UI_MODE_TYPE_APPLIANCE: ret.append("-appliance"); break; case UI_MODE_TYPE_WATCH: ret.append("-watch"); break; case UI_MODE_TYPE_VR_HEADSET: ret.append("-vrheadset"); break; } switch (uiMode & MASK_UI_MODE_NIGHT) { case UI_MODE_NIGHT_YES: ret.append("-night"); break; case UI_MODE_NIGHT_NO: ret.append("-notnight"); break; } switch (density) { case DENSITY_DEFAULT: break; case DENSITY_LOW: ret.append("-ldpi"); break; case DENSITY_MEDIUM: ret.append("-mdpi"); break; case DENSITY_HIGH: ret.append("-hdpi"); break; case DENSITY_TV: ret.append("-tvdpi"); break; case DENSITY_XHIGH: ret.append("-xhdpi"); break; case DENSITY_XXHIGH: ret.append("-xxhdpi"); break; case DENSITY_XXXHIGH: ret.append("-xxxhdpi"); break; case DENSITY_ANY: ret.append("-anydpi"); break; case DENSITY_NONE: ret.append("-nodpi"); break; default: ret.append('-').append(density).append("dpi"); } switch (touchscreen) { case TOUCHSCREEN_NOTOUCH: ret.append("-notouch"); break; case TOUCHSCREEN_STYLUS: ret.append("-stylus"); break; case TOUCHSCREEN_FINGER: ret.append("-finger"); break; } switch (inputFlags & MASK_KEYSHIDDEN) { case KEYSHIDDEN_NO: ret.append("-keysexposed"); break; case KEYSHIDDEN_YES: ret.append("-keyshidden"); break; case KEYSHIDDEN_SOFT: ret.append("-keyssoft"); break; } switch (keyboard) { case KEYBOARD_NOKEYS: ret.append("-nokeys"); break; case KEYBOARD_QWERTY: ret.append("-qwerty"); break; case KEYBOARD_12KEY: ret.append("-12key"); break; } switch (inputFlags & MASK_NAVHIDDEN) { case NAVHIDDEN_NO: ret.append("-navexposed"); break; case NAVHIDDEN_YES: ret.append("-navhidden"); break; } switch (navigation) { case NAVIGATION_NONAV: ret.append("-nonav"); break; case NAVIGATION_DPAD: ret.append("-dpad"); break; case NAVIGATION_TRACKBALL: ret.append("-trackball"); break; case NAVIGATION_WHEEL: ret.append("-wheel"); break; } if (screenWidth != 0 && screenHeight != 0) { if (screenWidth > screenHeight) { ret.append(String.format("-%dx%d", screenWidth, screenHeight)); } else { ret.append(String.format("-%dx%d", screenHeight, screenWidth)); } } if (sdkVersion > 0 && sdkVersion >= getNaturalSdkVersionRequirement()) { ret.append("-v").append(sdkVersion); } if (isInvalid) { ret.append("-ERR").append(sErrCounter++); } return ret.toString(); } private short getNaturalSdkVersionRequirement() { if ((uiMode & MASK_UI_MODE_TYPE) == UI_MODE_TYPE_VR_HEADSET || (colorMode & COLOR_WIDE_MASK) != 0 || ((colorMode & COLOR_HDR_MASK) != 0)) { return SDK_OREO; } if ((screenLayout2 & MASK_SCREENROUND) != 0) { return SDK_MNC; } if (density == DENSITY_ANY) { return SDK_LOLLIPOP; } if (smallestScreenWidthDp != 0 || screenWidthDp != 0 || screenHeightDp != 0) { return SDK_HONEYCOMB_MR2; } if ((uiMode & (MASK_UI_MODE_TYPE | MASK_UI_MODE_NIGHT)) != UI_MODE_NIGHT_ANY) { return SDK_FROYO; } if ((screenLayout & (MASK_SCREENSIZE | MASK_SCREENLONG)) != SCREENSIZE_ANY || density != DENSITY_DEFAULT) { return SDK_DONUT; } return 0; } private String getLocaleString() { StringBuilder sb = new StringBuilder(); // check for old style non BCP47 tags // allows values-xx-rXX, values-xx, values-xxx-rXX // denies values-xxx, anything else if (localeVariant == null && localeScript == null && (region[0] != '\00' || language[0] != '\00') && region.length != 3) { sb.append('-').append(language); if (region[0] != '\00') { sb.append("-r").append(region); } } else { // BCP47 if (language[0] == '\00' && region[0] == '\00') { return sb.toString(); // early return, no language or region } sb.append("-b+"); if (language[0] != '\00') { sb.append(language); } if (localeScript != null && localeScript.length == 4) { sb.append('+').append(localeScript); } if ((region.length == 2 || region.length == 3) && region[0] != '\00') { sb.append('+').append(region); } if (localeVariant != null && localeVariant.length >= 5) { sb.append('+').append(toUpper(localeVariant)); } } return sb.toString(); } private String toUpper(char[] character) { StringBuilder sb = new StringBuilder(); for (char ch : character) { sb.append(Character.toUpperCase(ch)); } return sb.toString(); } @Override public String toString() { return !getQualifiers().isEmpty() ? getQualifiers() : "[DEFAULT]"; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final EntryConfig other = (EntryConfig) obj; return this.mQualifiers.equals(other.mQualifiers); } @Override public int hashCode() { int hash = 17; hash = 31 * hash + this.mQualifiers.hashCode(); return hash; } // TODO: Dirty static hack. This counter should be a part of ResPackage, // but it would be hard right now and this feature is very rarely used. private static int sErrCounter = 0; public static final byte SDK_BASE = 1; public static final byte SDK_BASE_1_1 = 2; public static final byte SDK_CUPCAKE = 3; public static final byte SDK_DONUT = 4; public static final byte SDK_ECLAIR = 5; public static final byte SDK_ECLAIR_0_1 = 6; public static final byte SDK_ECLAIR_MR1 = 7; public static final byte SDK_FROYO = 8; public static final byte SDK_GINGERBREAD = 9; public static final byte SDK_GINGERBREAD_MR1 = 10; public static final byte SDK_HONEYCOMB = 11; public static final byte SDK_HONEYCOMB_MR1 = 12; public static final byte SDK_HONEYCOMB_MR2 = 13; public static final byte SDK_ICE_CREAM_SANDWICH = 14; public static final byte SDK_ICE_CREAM_SANDWICH_MR1 = 15; public static final byte SDK_JELLY_BEAN = 16; public static final byte SDK_JELLY_BEAN_MR1 = 17; public static final byte SDK_JELLY_BEAN_MR2 = 18; public static final byte SDK_KITKAT = 19; public static final byte SDK_LOLLIPOP = 21; public static final byte SDK_LOLLIPOP_MR1 = 22; public static final byte SDK_MNC = 23; public static final byte SDK_NOUGAT = 24; public static final byte SDK_NOUGAT_MR1 = 25; public static final byte SDK_OREO = 26; public static final byte SDK_OREO_MR1 = 27; public static final byte SDK_P = 28; public static final byte ORIENTATION_ANY = 0; public static final byte ORIENTATION_PORT = 1; public static final byte ORIENTATION_LAND = 2; public static final byte ORIENTATION_SQUARE = 3; public static final byte TOUCHSCREEN_ANY = 0; public static final byte TOUCHSCREEN_NOTOUCH = 1; public static final byte TOUCHSCREEN_STYLUS = 2; public static final byte TOUCHSCREEN_FINGER = 3; public static final int DENSITY_DEFAULT = 0; public static final int DENSITY_LOW = 120; public static final int DENSITY_MEDIUM = 160; public static final int DENSITY_400 = 190; public static final int DENSITY_TV = 213; public static final int DENSITY_HIGH = 240; public static final int DENSITY_XHIGH = 320; public static final int DENSITY_XXHIGH = 480; public static final int DENSITY_XXXHIGH = 640; public static final int DENSITY_ANY = 0xFFFE; public static final int DENSITY_NONE = 0xFFFF; public static final int MNC_ZERO = -1; public static final short MASK_LAYOUTDIR = 0xc0; public static final short SCREENLAYOUT_LAYOUTDIR_ANY = 0x00; public static final short SCREENLAYOUT_LAYOUTDIR_LTR = 0x40; public static final short SCREENLAYOUT_LAYOUTDIR_RTL = 0x80; public static final short SCREENLAYOUT_LAYOUTDIR_SHIFT = 0x06; public static final short MASK_SCREENROUND = 0x03; public static final short SCREENLAYOUT_ROUND_ANY = 0; public static final short SCREENLAYOUT_ROUND_NO = 0x1; public static final short SCREENLAYOUT_ROUND_YES = 0x2; public static final byte KEYBOARD_ANY = 0; public static final byte KEYBOARD_NOKEYS = 1; public static final byte KEYBOARD_QWERTY = 2; public static final byte KEYBOARD_12KEY = 3; public static final byte NAVIGATION_ANY = 0; public static final byte NAVIGATION_NONAV = 1; public static final byte NAVIGATION_DPAD = 2; public static final byte NAVIGATION_TRACKBALL = 3; public static final byte NAVIGATION_WHEEL = 4; public static final byte MASK_KEYSHIDDEN = 0x3; public static final byte KEYSHIDDEN_ANY = 0x0; public static final byte KEYSHIDDEN_NO = 0x1; public static final byte KEYSHIDDEN_YES = 0x2; public static final byte KEYSHIDDEN_SOFT = 0x3; public static final byte MASK_NAVHIDDEN = 0xc; public static final byte NAVHIDDEN_ANY = 0x0; public static final byte NAVHIDDEN_NO = 0x4; public static final byte NAVHIDDEN_YES = 0x8; public static final byte MASK_SCREENSIZE = 0x0f; public static final byte SCREENSIZE_ANY = 0x00; public static final byte SCREENSIZE_SMALL = 0x01; public static final byte SCREENSIZE_NORMAL = 0x02; public static final byte SCREENSIZE_LARGE = 0x03; public static final byte SCREENSIZE_XLARGE = 0x04; public static final byte MASK_SCREENLONG = 0x30; public static final byte SCREENLONG_ANY = 0x00; public static final byte SCREENLONG_NO = 0x10; public static final byte SCREENLONG_YES = 0x20; public static final byte MASK_UI_MODE_TYPE = 0x0f; public static final byte UI_MODE_TYPE_ANY = 0x00; public static final byte UI_MODE_TYPE_NORMAL = 0x01; public static final byte UI_MODE_TYPE_DESK = 0x02; public static final byte UI_MODE_TYPE_CAR = 0x03; public static final byte UI_MODE_TYPE_TELEVISION = 0x04; public static final byte UI_MODE_TYPE_APPLIANCE = 0x05; public static final byte UI_MODE_TYPE_WATCH = 0x06; public static final byte UI_MODE_TYPE_VR_HEADSET = 0x07; // start - miui public static final byte UI_MODE_TYPE_GODZILLAUI = 0x0b; public static final byte UI_MODE_TYPE_SMALLUI = 0x0c; public static final byte UI_MODE_TYPE_MEDIUMUI = 0x0d; public static final byte UI_MODE_TYPE_LARGEUI = 0x0e; public static final byte UI_MODE_TYPE_HUGEUI = 0x0f; // end - miui public static final byte MASK_UI_MODE_NIGHT = 0x30; public static final byte UI_MODE_NIGHT_ANY = 0x00; public static final byte UI_MODE_NIGHT_NO = 0x10; public static final byte UI_MODE_NIGHT_YES = 0x20; public static final byte COLOR_HDR_MASK = 0xC; public static final byte COLOR_HDR_NO = 0x4; public static final byte COLOR_HDR_SHIFT = 0x2; public static final byte COLOR_HDR_UNDEFINED = 0x0; public static final byte COLOR_HDR_YES = 0x8; public static final byte COLOR_UNDEFINED = 0x0; public static final byte COLOR_WIDE_UNDEFINED = 0x0; public static final byte COLOR_WIDE_NO = 0x1; public static final byte COLOR_WIDE_YES = 0x2; public static final byte COLOR_WIDE_MASK = 0x3; public static final byte GRAMMATICAL_GENDER_ANY = 0; public static final byte GRAMMATICAL_GENDER_NEUTER = 1; public static final byte GRAMMATICAL_GENDER_FEMININE = 2; public static final byte GRAMMATICAL_GENDER_MASCULINE = 3; } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/entry/ProtoValue.java ================================================ package jadx.core.xmlgen.entry; import java.util.List; public class ProtoValue { private String parent; private String name; private String value; private int type; private List namedValues; public ProtoValue(String value) { this.value = value; } public ProtoValue() { } public int getType() { return type; } public ProtoValue setType(int type) { this.type = type; return this; } public String getValue() { return value; } public String getParent() { return parent; } public ProtoValue setParent(String parent) { this.parent = parent; return this; } public ProtoValue setName(String name) { this.name = name; return this; } public String getName() { return name; } public ProtoValue setNamedValues(List namedValues) { this.namedValues = namedValues; return this; } public List getNamedValues() { return namedValues; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/entry/RawNamedValue.java ================================================ package jadx.core.xmlgen.entry; public class RawNamedValue { private final int nameRef; private final RawValue rawValue; public RawNamedValue(int nameRef, RawValue rawValue) { this.nameRef = nameRef; this.rawValue = rawValue; } public int getNameRef() { return nameRef; } public RawValue getRawValue() { return rawValue; } @Override public String toString() { return "RawNamedValue{nameRef=" + nameRef + ", rawValue=" + rawValue + '}'; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/entry/RawValue.java ================================================ package jadx.core.xmlgen.entry; public final class RawValue { private final int dataType; private final int data; public RawValue(int dataType, int data) { this.dataType = dataType; this.data = data; } public int getDataType() { return dataType; } public int getData() { return data; } @Override public String toString() { return "RawValue: type=0x" + Integer.toHexString(dataType) + ", value=" + data; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/entry/ResourceEntry.java ================================================ package jadx.core.xmlgen.entry; import java.util.List; public final class ResourceEntry { private final int id; private final String pkgName; private final String typeName; private final String keyName; private final String config; private int parentRef; private ProtoValue protoValue; private RawValue simpleValue; private List namedValues; public ResourceEntry(int id, String pkgName, String typeName, String keyName, String config) { this.id = id; this.pkgName = pkgName; this.typeName = typeName; this.keyName = keyName; this.config = config; } public ResourceEntry copy(String newKeyName) { ResourceEntry copy = new ResourceEntry(id, pkgName, typeName, newKeyName, config); copy.parentRef = this.parentRef; copy.protoValue = this.protoValue; copy.simpleValue = this.simpleValue; copy.namedValues = this.namedValues; return copy; } public ResourceEntry copyWithId(String resName) { return copy(String.format("%s_res_0x%08x", resName, id)); } /** * 32 bit resource ID as defined in AOSP. * *

    *
  1. Package ID (8 bit)
  2. *
  3. Type ID (8 bit)
  4. *
  5. Entry ID (16 bit)
  6. *
* * See make_resid() in ResourceUtils.h * * @return resource ID */ public int getId() { return id; } public String getPkgName() { return pkgName; } public String getTypeName() { return typeName; } public String getKeyName() { return keyName; } public String getConfig() { return config; } public void setParentRef(int parentRef) { this.parentRef = parentRef; } public int getParentRef() { return parentRef; } public ProtoValue getProtoValue() { return protoValue; } public void setProtoValue(ProtoValue protoValue) { this.protoValue = protoValue; } public RawValue getSimpleValue() { return simpleValue; } public void setSimpleValue(RawValue simpleValue) { this.simpleValue = simpleValue; } public void setNamedValues(List namedValues) { this.namedValues = namedValues; } public List getNamedValues() { return namedValues; } @Override public String toString() { return " 0x" + Integer.toHexString(id) + " (" + id + ')' + config + " = " + typeName + '.' + keyName; } } ================================================ FILE: jadx-core/src/main/java/jadx/core/xmlgen/entry/ValuesParser.java ================================================ package jadx.core.xmlgen.entry; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.android.AndroidResourcesMap; import jadx.core.xmlgen.BinaryXMLStrings; import jadx.core.xmlgen.ParserConstants; import jadx.core.xmlgen.XmlGenUtils; public class ValuesParser extends ParserConstants { private static final Logger LOG = LoggerFactory.getLogger(ValuesParser.class); private final BinaryXMLStrings strings; private final Map resMap; public ValuesParser(BinaryXMLStrings strings, Map resMap) { this.strings = strings; this.resMap = resMap; } @Nullable public String getSimpleValueString(ResourceEntry ri) { ProtoValue protoValue = ri.getProtoValue(); if (protoValue != null) { return protoValue.getValue(); } RawValue simpleValue = ri.getSimpleValue(); if (simpleValue == null) { return null; } return decodeValue(simpleValue); } @Nullable public String getValueString(ResourceEntry ri) { ProtoValue protoValue = ri.getProtoValue(); if (protoValue != null) { if (protoValue.getValue() != null) { return protoValue.getValue(); } List values = protoValue.getNamedValues(); List strList = new ArrayList<>(values.size()); for (ProtoValue value : values) { if (value.getName() == null) { strList.add(value.getValue()); } else { strList.add(value.getName() + '=' + value.getValue()); } } return strList.toString(); } RawValue simpleValue = ri.getSimpleValue(); if (simpleValue != null) { return decodeValue(simpleValue); } List namedValues = ri.getNamedValues(); List strList = new ArrayList<>(namedValues.size()); for (RawNamedValue value : namedValues) { String nameStr = decodeNameRef(value.getNameRef()); String valueStr = decodeValue(value.getRawValue()); if (nameStr == null) { strList.add(valueStr); } else { strList.add(nameStr + '=' + valueStr); } } return strList.toString(); } @Nullable public String decodeValue(RawValue value) { int dataType = value.getDataType(); int data = value.getData(); return decodeValue(dataType, data); } @Nullable public String decodeValue(int dataType, int data) { switch (dataType) { case TYPE_NULL: return null; case TYPE_STRING: return strings.get(data); case TYPE_INT_DEC: return Integer.toString(data); case TYPE_INT_HEX: return "0x" + Integer.toHexString(data); case TYPE_INT_BOOLEAN: return data == 0 ? "false" : "true"; case TYPE_FLOAT: return XmlGenUtils.floatToString(Float.intBitsToFloat(data)); case TYPE_INT_COLOR_ARGB8: return String.format("#%08x", data); case TYPE_INT_COLOR_RGB8: return String.format("#%06x", data & 0xFFFFFF); case TYPE_INT_COLOR_ARGB4: return String.format("#%04x", data & 0xFFFF); case TYPE_INT_COLOR_RGB4: return String.format("#%03x", data & 0xFFF); case TYPE_DYNAMIC_REFERENCE: case TYPE_REFERENCE: { String ri = resMap.get(data); if (ri == null) { String androidRi = AndroidResourcesMap.getResName(data); if (androidRi != null) { return "@android:" + androidRi; } if (data == 0) { return "0"; } return "?unknown_ref: " + Integer.toHexString(data); } return '@' + ri; } case TYPE_ATTRIBUTE: { String ri = resMap.get(data); if (ri == null) { String androidRi = AndroidResourcesMap.getResName(data); if (androidRi != null) { return "?android:" + androidRi; } return "?unknown_attr_ref: " + Integer.toHexString(data); } return '?' + ri; } case TYPE_DIMENSION: return XmlGenUtils.decodeComplex(data, false); case TYPE_FRACTION: return XmlGenUtils.decodeComplex(data, true); case TYPE_DYNAMIC_ATTRIBUTE: LOG.warn("Data type TYPE_DYNAMIC_ATTRIBUTE not yet supported: {}", data); return " TYPE_DYNAMIC_ATTRIBUTE: " + data; default: LOG.warn("Unknown data type: 0x{} {}", Integer.toHexString(dataType), data); return " ?0x" + Integer.toHexString(dataType) + ' ' + data; } } public String decodeNameRef(int nameRef) { int ref = nameRef; if (isResInternalId(nameRef)) { ref = nameRef & ATTR_TYPE_ANY; if (ref == 0) { return null; } } String ri = resMap.get(ref); if (ri != null) { return ri.replace('/', '.'); } else { String androidRi = AndroidResourcesMap.getResName(ref); if (androidRi != null) { return "android:" + androidRi.replace('/', '.'); } } return "?0x" + Integer.toHexString(nameRef); } } ================================================ FILE: jadx-core/src/main/resources/android/attrs.xml ================================================ ================================================ FILE: jadx-core/src/main/resources/android/attrs_manifest.xml ================================================ ================================================ FILE: jadx-core/src/main/resources/android/res-map.txt ================================================ 01010000=attr/theme 01010001=attr/label 01010002=attr/icon 01010003=attr/name 01010004=attr/manageSpaceActivity 01010005=attr/allowClearUserData 01010006=attr/permission 01010007=attr/readPermission 01010008=attr/writePermission 01010009=attr/protectionLevel 0101000a=attr/permissionGroup 0101000b=attr/sharedUserId 0101000c=attr/hasCode 0101000d=attr/persistent 0101000e=attr/enabled 0101000f=attr/debuggable 01010010=attr/exported 01010011=attr/process 01010012=attr/taskAffinity 01010013=attr/multiprocess 01010014=attr/finishOnTaskLaunch 01010015=attr/clearTaskOnLaunch 01010016=attr/stateNotNeeded 01010017=attr/excludeFromRecents 01010018=attr/authorities 01010019=attr/syncable 0101001a=attr/initOrder 0101001b=attr/grantUriPermissions 0101001c=attr/priority 0101001d=attr/launchMode 0101001e=attr/screenOrientation 0101001f=attr/configChanges 01010020=attr/description 01010021=attr/targetPackage 01010022=attr/handleProfiling 01010023=attr/functionalTest 01010024=attr/value 01010025=attr/resource 01010026=attr/mimeType 01010027=attr/scheme 01010028=attr/host 01010029=attr/port 0101002a=attr/path 0101002b=attr/pathPrefix 0101002c=attr/pathPattern 0101002d=attr/action 0101002e=attr/data 0101002f=attr/targetClass 01010030=attr/colorForeground 01010031=attr/colorBackground 01010032=attr/backgroundDimAmount 01010033=attr/disabledAlpha 01010034=attr/textAppearance 01010035=attr/textAppearanceInverse 01010036=attr/textColorPrimary 01010037=attr/textColorPrimaryDisableOnly 01010038=attr/textColorSecondary 01010039=attr/textColorPrimaryInverse 0101003a=attr/textColorSecondaryInverse 0101003b=attr/textColorPrimaryNoDisable 0101003c=attr/textColorSecondaryNoDisable 0101003d=attr/textColorPrimaryInverseNoDisable 0101003e=attr/textColorSecondaryInverseNoDisable 0101003f=attr/textColorHintInverse 01010040=attr/textAppearanceLarge 01010041=attr/textAppearanceMedium 01010042=attr/textAppearanceSmall 01010043=attr/textAppearanceLargeInverse 01010044=attr/textAppearanceMediumInverse 01010045=attr/textAppearanceSmallInverse 01010046=attr/textCheckMark 01010047=attr/textCheckMarkInverse 01010048=attr/buttonStyle 01010049=attr/buttonStyleSmall 0101004a=attr/buttonStyleInset 0101004b=attr/buttonStyleToggle 0101004c=attr/galleryItemBackground 0101004d=attr/listPreferredItemHeight 0101004e=attr/expandableListPreferredItemPaddingLeft 0101004f=attr/expandableListPreferredChildPaddingLeft 01010050=attr/expandableListPreferredItemIndicatorLeft 01010051=attr/expandableListPreferredItemIndicatorRight 01010052=attr/expandableListPreferredChildIndicatorLeft 01010053=attr/expandableListPreferredChildIndicatorRight 01010054=attr/windowBackground 01010055=attr/windowFrame 01010056=attr/windowNoTitle 01010057=attr/windowIsFloating 01010058=attr/windowIsTranslucent 01010059=attr/windowContentOverlay 0101005a=attr/windowTitleSize 0101005b=attr/windowTitleStyle 0101005c=attr/windowTitleBackgroundStyle 0101005d=attr/alertDialogStyle 0101005e=attr/panelBackground 0101005f=attr/panelFullBackground 01010060=attr/panelColorForeground 01010061=attr/panelColorBackground 01010062=attr/panelTextAppearance 01010063=attr/scrollbarSize 01010064=attr/scrollbarThumbHorizontal 01010065=attr/scrollbarThumbVertical 01010066=attr/scrollbarTrackHorizontal 01010067=attr/scrollbarTrackVertical 01010068=attr/scrollbarAlwaysDrawHorizontalTrack 01010069=attr/scrollbarAlwaysDrawVerticalTrack 0101006a=attr/absListViewStyle 0101006b=attr/autoCompleteTextViewStyle 0101006c=attr/checkboxStyle 0101006d=attr/dropDownListViewStyle 0101006e=attr/editTextStyle 0101006f=attr/expandableListViewStyle 01010070=attr/galleryStyle 01010071=attr/gridViewStyle 01010072=attr/imageButtonStyle 01010073=attr/imageWellStyle 01010074=attr/listViewStyle 01010075=attr/listViewWhiteStyle 01010076=attr/popupWindowStyle 01010077=attr/progressBarStyle 01010078=attr/progressBarStyleHorizontal 01010079=attr/progressBarStyleSmall 0101007a=attr/progressBarStyleLarge 0101007b=attr/seekBarStyle 0101007c=attr/ratingBarStyle 0101007d=attr/ratingBarStyleSmall 0101007e=attr/radioButtonStyle 0101007f=attr/scrollbarStyle 01010080=attr/scrollViewStyle 01010081=attr/spinnerStyle 01010082=attr/starStyle 01010083=attr/tabWidgetStyle 01010084=attr/textViewStyle 01010085=attr/webViewStyle 01010086=attr/dropDownItemStyle 01010087=attr/spinnerDropDownItemStyle 01010088=attr/dropDownHintAppearance 01010089=attr/spinnerItemStyle 0101008a=attr/mapViewStyle 0101008b=attr/preferenceScreenStyle 0101008c=attr/preferenceCategoryStyle 0101008d=attr/preferenceInformationStyle 0101008e=attr/preferenceStyle 0101008f=attr/checkBoxPreferenceStyle 01010090=attr/yesNoPreferenceStyle 01010091=attr/dialogPreferenceStyle 01010092=attr/editTextPreferenceStyle 01010093=attr/ringtonePreferenceStyle 01010094=attr/preferenceLayoutChild 01010095=attr/textSize 01010096=attr/typeface 01010097=attr/textStyle 01010098=attr/textColor 01010099=attr/textColorHighlight 0101009a=attr/textColorHint 0101009b=attr/textColorLink 0101009c=attr/state_focused 0101009d=attr/state_window_focused 0101009e=attr/state_enabled 0101009f=attr/state_checkable 010100a0=attr/state_checked 010100a1=attr/state_selected 010100a2=attr/state_active 010100a3=attr/state_single 010100a4=attr/state_first 010100a5=attr/state_middle 010100a6=attr/state_last 010100a7=attr/state_pressed 010100a8=attr/state_expanded 010100a9=attr/state_empty 010100aa=attr/state_above_anchor 010100ab=attr/ellipsize 010100ac=attr/x 010100ad=attr/y 010100ae=attr/windowAnimationStyle 010100af=attr/gravity 010100b0=attr/autoLink 010100b1=attr/linksClickable 010100b2=attr/entries 010100b3=attr/layout_gravity 010100b4=attr/windowEnterAnimation 010100b5=attr/windowExitAnimation 010100b6=attr/windowShowAnimation 010100b7=attr/windowHideAnimation 010100b8=attr/activityOpenEnterAnimation 010100b9=attr/activityOpenExitAnimation 010100ba=attr/activityCloseEnterAnimation 010100bb=attr/activityCloseExitAnimation 010100bc=attr/taskOpenEnterAnimation 010100bd=attr/taskOpenExitAnimation 010100be=attr/taskCloseEnterAnimation 010100bf=attr/taskCloseExitAnimation 010100c0=attr/taskToFrontEnterAnimation 010100c1=attr/taskToFrontExitAnimation 010100c2=attr/taskToBackEnterAnimation 010100c3=attr/taskToBackExitAnimation 010100c4=attr/orientation 010100c5=attr/keycode 010100c6=attr/fullDark 010100c7=attr/topDark 010100c8=attr/centerDark 010100c9=attr/bottomDark 010100ca=attr/fullBright 010100cb=attr/topBright 010100cc=attr/centerBright 010100cd=attr/bottomBright 010100ce=attr/bottomMedium 010100cf=attr/centerMedium 010100d0=attr/id 010100d1=attr/tag 010100d2=attr/scrollX 010100d3=attr/scrollY 010100d4=attr/background 010100d5=attr/padding 010100d6=attr/paddingLeft 010100d7=attr/paddingTop 010100d8=attr/paddingRight 010100d9=attr/paddingBottom 010100da=attr/focusable 010100db=attr/focusableInTouchMode 010100dc=attr/visibility 010100dd=attr/fitsSystemWindows 010100de=attr/scrollbars 010100df=attr/fadingEdge 010100e0=attr/fadingEdgeLength 010100e1=attr/nextFocusLeft 010100e2=attr/nextFocusRight 010100e3=attr/nextFocusUp 010100e4=attr/nextFocusDown 010100e5=attr/clickable 010100e6=attr/longClickable 010100e7=attr/saveEnabled 010100e8=attr/drawingCacheQuality 010100e9=attr/duplicateParentState 010100ea=attr/clipChildren 010100eb=attr/clipToPadding 010100ec=attr/layoutAnimation 010100ed=attr/animationCache 010100ee=attr/persistentDrawingCache 010100ef=attr/alwaysDrawnWithCache 010100f0=attr/addStatesFromChildren 010100f1=attr/descendantFocusability 010100f2=attr/layout 010100f3=attr/inflatedId 010100f4=attr/layout_width 010100f5=attr/layout_height 010100f6=attr/layout_margin 010100f7=attr/layout_marginLeft 010100f8=attr/layout_marginTop 010100f9=attr/layout_marginRight 010100fa=attr/layout_marginBottom 010100fb=attr/listSelector 010100fc=attr/drawSelectorOnTop 010100fd=attr/stackFromBottom 010100fe=attr/scrollingCache 010100ff=attr/textFilterEnabled 01010100=attr/transcriptMode 01010101=attr/cacheColorHint 01010102=attr/dial 01010103=attr/hand_hour 01010104=attr/hand_minute 01010105=attr/format 01010106=attr/checked 01010107=attr/button 01010108=attr/checkMark 01010109=attr/foreground 0101010a=attr/measureAllChildren 0101010b=attr/groupIndicator 0101010c=attr/childIndicator 0101010d=attr/indicatorLeft 0101010e=attr/indicatorRight 0101010f=attr/childIndicatorLeft 01010110=attr/childIndicatorRight 01010111=attr/childDivider 01010112=attr/animationDuration 01010113=attr/spacing 01010114=attr/horizontalSpacing 01010115=attr/verticalSpacing 01010116=attr/stretchMode 01010117=attr/columnWidth 01010118=attr/numColumns 01010119=attr/src 0101011a=attr/antialias 0101011b=attr/filter 0101011c=attr/dither 0101011d=attr/scaleType 0101011e=attr/adjustViewBounds 0101011f=attr/maxWidth 01010120=attr/maxHeight 01010121=attr/tint 01010122=attr/baselineAlignBottom 01010123=attr/cropToPadding 01010124=attr/textOn 01010125=attr/textOff 01010126=attr/baselineAligned 01010127=attr/baselineAlignedChildIndex 01010128=attr/weightSum 01010129=attr/divider 0101012a=attr/dividerHeight 0101012b=attr/choiceMode 0101012c=attr/itemTextAppearance 0101012d=attr/horizontalDivider 0101012e=attr/verticalDivider 0101012f=attr/headerBackground 01010130=attr/itemBackground 01010131=attr/itemIconDisabledAlpha 01010132=attr/rowHeight 01010133=attr/maxRows 01010134=attr/maxItemsPerRow 01010135=attr/moreIcon 01010136=attr/max 01010137=attr/progress 01010138=attr/secondaryProgress 01010139=attr/indeterminate 0101013a=attr/indeterminateOnly 0101013b=attr/indeterminateDrawable 0101013c=attr/progressDrawable 0101013d=attr/indeterminateDuration 0101013e=attr/indeterminateBehavior 0101013f=attr/minWidth 01010140=attr/minHeight 01010141=attr/interpolator 01010142=attr/thumb 01010143=attr/thumbOffset 01010144=attr/numStars 01010145=attr/rating 01010146=attr/stepSize 01010147=attr/isIndicator 01010148=attr/checkedButton 01010149=attr/stretchColumns 0101014a=attr/shrinkColumns 0101014b=attr/collapseColumns 0101014c=attr/layout_column 0101014d=attr/layout_span 0101014e=attr/bufferType 0101014f=attr/text 01010150=attr/hint 01010151=attr/textScaleX 01010152=attr/cursorVisible 01010153=attr/maxLines 01010154=attr/lines 01010155=attr/height 01010156=attr/minLines 01010157=attr/maxEms 01010158=attr/ems 01010159=attr/width 0101015a=attr/minEms 0101015b=attr/scrollHorizontally 0101015c=attr/password 0101015d=attr/singleLine 0101015e=attr/selectAllOnFocus 0101015f=attr/includeFontPadding 01010160=attr/maxLength 01010161=attr/shadowColor 01010162=attr/shadowDx 01010163=attr/shadowDy 01010164=attr/shadowRadius 01010165=attr/numeric 01010166=attr/digits 01010167=attr/phoneNumber 01010168=attr/inputMethod 01010169=attr/capitalize 0101016a=attr/autoText 0101016b=attr/editable 0101016c=attr/freezesText 0101016d=attr/drawableTop 0101016e=attr/drawableBottom 0101016f=attr/drawableLeft 01010170=attr/drawableRight 01010171=attr/drawablePadding 01010172=attr/completionHint 01010173=attr/completionHintView 01010174=attr/completionThreshold 01010175=attr/dropDownSelector 01010176=attr/popupBackground 01010177=attr/inAnimation 01010178=attr/outAnimation 01010179=attr/flipInterval 0101017a=attr/fillViewport 0101017b=attr/prompt 0101017c=attr/startYear 0101017d=attr/endYear 0101017e=attr/mode 0101017f=attr/layout_x 01010180=attr/layout_y 01010181=attr/layout_weight 01010182=attr/layout_toLeftOf 01010183=attr/layout_toRightOf 01010184=attr/layout_above 01010185=attr/layout_below 01010186=attr/layout_alignBaseline 01010187=attr/layout_alignLeft 01010188=attr/layout_alignTop 01010189=attr/layout_alignRight 0101018a=attr/layout_alignBottom 0101018b=attr/layout_alignParentLeft 0101018c=attr/layout_alignParentTop 0101018d=attr/layout_alignParentRight 0101018e=attr/layout_alignParentBottom 0101018f=attr/layout_centerInParent 01010190=attr/layout_centerHorizontal 01010191=attr/layout_centerVertical 01010192=attr/layout_alignWithParentIfMissing 01010193=attr/layout_scale 01010194=attr/visible 01010195=attr/variablePadding 01010196=attr/constantSize 01010197=attr/oneshot 01010198=attr/duration 01010199=attr/drawable 0101019a=attr/shape 0101019b=attr/innerRadiusRatio 0101019c=attr/thicknessRatio 0101019d=attr/startColor 0101019e=attr/endColor 0101019f=attr/useLevel 010101a0=attr/angle 010101a1=attr/type 010101a2=attr/centerX 010101a3=attr/centerY 010101a4=attr/gradientRadius 010101a5=attr/color 010101a6=attr/dashWidth 010101a7=attr/dashGap 010101a8=attr/radius 010101a9=attr/topLeftRadius 010101aa=attr/topRightRadius 010101ab=attr/bottomLeftRadius 010101ac=attr/bottomRightRadius 010101ad=attr/left 010101ae=attr/top 010101af=attr/right 010101b0=attr/bottom 010101b1=attr/minLevel 010101b2=attr/maxLevel 010101b3=attr/fromDegrees 010101b4=attr/toDegrees 010101b5=attr/pivotX 010101b6=attr/pivotY 010101b7=attr/insetLeft 010101b8=attr/insetRight 010101b9=attr/insetTop 010101ba=attr/insetBottom 010101bb=attr/shareInterpolator 010101bc=attr/fillBefore 010101bd=attr/fillAfter 010101be=attr/startOffset 010101bf=attr/repeatCount 010101c0=attr/repeatMode 010101c1=attr/zAdjustment 010101c2=attr/fromXScale 010101c3=attr/toXScale 010101c4=attr/fromYScale 010101c5=attr/toYScale 010101c6=attr/fromXDelta 010101c7=attr/toXDelta 010101c8=attr/fromYDelta 010101c9=attr/toYDelta 010101ca=attr/fromAlpha 010101cb=attr/toAlpha 010101cc=attr/delay 010101cd=attr/animation 010101ce=attr/animationOrder 010101cf=attr/columnDelay 010101d0=attr/rowDelay 010101d1=attr/direction 010101d2=attr/directionPriority 010101d3=attr/factor 010101d4=attr/cycles 010101d5=attr/searchMode 010101d6=attr/searchSuggestAuthority 010101d7=attr/searchSuggestPath 010101d8=attr/searchSuggestSelection 010101d9=attr/searchSuggestIntentAction 010101da=attr/searchSuggestIntentData 010101db=attr/queryActionMsg 010101dc=attr/suggestActionMsg 010101dd=attr/suggestActionMsgColumn 010101de=attr/menuCategory 010101df=attr/orderInCategory 010101e0=attr/checkableBehavior 010101e1=attr/title 010101e2=attr/titleCondensed 010101e3=attr/alphabeticShortcut 010101e4=attr/numericShortcut 010101e5=attr/checkable 010101e6=attr/selectable 010101e7=attr/orderingFromXml 010101e8=attr/key 010101e9=attr/summary 010101ea=attr/order 010101eb=attr/widgetLayout 010101ec=attr/dependency 010101ed=attr/defaultValue 010101ee=attr/shouldDisableView 010101ef=attr/summaryOn 010101f0=attr/summaryOff 010101f1=attr/disableDependentsState 010101f2=attr/dialogTitle 010101f3=attr/dialogMessage 010101f4=attr/dialogIcon 010101f5=attr/positiveButtonText 010101f6=attr/negativeButtonText 010101f7=attr/dialogLayout 010101f8=attr/entryValues 010101f9=attr/ringtoneType 010101fa=attr/showDefault 010101fb=attr/showSilent 010101fc=attr/scaleWidth 010101fd=attr/scaleHeight 010101fe=attr/scaleGravity 010101ff=attr/ignoreGravity 01010200=attr/foregroundGravity 01010201=attr/tileMode 01010202=attr/targetActivity 01010203=attr/alwaysRetainTaskState 01010204=attr/allowTaskReparenting 01010205=attr/searchButtonText 01010206=attr/colorForegroundInverse 01010207=attr/textAppearanceButton 01010208=attr/listSeparatorTextViewStyle 01010209=attr/streamType 0101020a=attr/clipOrientation 0101020b=attr/centerColor 0101020c=attr/minSdkVersion 0101020d=attr/windowFullscreen 0101020e=attr/unselectedAlpha 0101020f=attr/progressBarStyleSmallTitle 01010210=attr/ratingBarStyleIndicator 01010211=attr/apiKey 01010212=attr/textColorTertiary 01010213=attr/textColorTertiaryInverse 01010214=attr/listDivider 01010215=attr/soundEffectsEnabled 01010216=attr/keepScreenOn 01010217=attr/lineSpacingExtra 01010218=attr/lineSpacingMultiplier 01010219=attr/listChoiceIndicatorSingle 0101021a=attr/listChoiceIndicatorMultiple 0101021b=attr/versionCode 0101021c=attr/versionName 0101021d=attr/marqueeRepeatLimit 0101021e=attr/windowNoDisplay 0101021f=attr/backgroundDimEnabled 01010220=attr/inputType 01010221=attr/isDefault 01010222=attr/windowDisablePreview 01010223=attr/privateImeOptions 01010224=attr/editorExtras 01010225=attr/settingsActivity 01010226=attr/fastScrollEnabled 01010227=attr/reqTouchScreen 01010228=attr/reqKeyboardType 01010229=attr/reqHardKeyboard 0101022a=attr/reqNavigation 0101022b=attr/windowSoftInputMode 0101022c=attr/imeFullscreenBackground 0101022d=attr/noHistory 0101022e=attr/headerDividersEnabled 0101022f=attr/footerDividersEnabled 01010230=attr/candidatesTextStyleSpans 01010231=attr/smoothScrollbar 01010232=attr/reqFiveWayNav 01010233=attr/keyBackground 01010234=attr/keyTextSize 01010235=attr/labelTextSize 01010236=attr/keyTextColor 01010237=attr/keyPreviewLayout 01010238=attr/keyPreviewOffset 01010239=attr/keyPreviewHeight 0101023a=attr/verticalCorrection 0101023b=attr/popupLayout 0101023c=attr/state_long_pressable 0101023d=attr/keyWidth 0101023e=attr/keyHeight 0101023f=attr/horizontalGap 01010240=attr/verticalGap 01010241=attr/rowEdgeFlags 01010242=attr/codes 01010243=attr/popupKeyboard 01010244=attr/popupCharacters 01010245=attr/keyEdgeFlags 01010246=attr/isModifier 01010247=attr/isSticky 01010248=attr/isRepeatable 01010249=attr/iconPreview 0101024a=attr/keyOutputText 0101024b=attr/keyLabel 0101024c=attr/keyIcon 0101024d=attr/keyboardMode 0101024e=attr/isScrollContainer 0101024f=attr/fillEnabled 01010250=attr/updatePeriodMillis 01010251=attr/initialLayout 01010252=attr/voiceSearchMode 01010253=attr/voiceLanguageModel 01010254=attr/voicePromptText 01010255=attr/voiceLanguage 01010256=attr/voiceMaxResults 01010257=attr/bottomOffset 01010258=attr/topOffset 01010259=attr/allowSingleTap 0101025a=attr/handle 0101025b=attr/content 0101025c=attr/animateOnClick 0101025d=attr/configure 0101025e=attr/hapticFeedbackEnabled 0101025f=attr/innerRadius 01010260=attr/thickness 01010261=attr/sharedUserLabel 01010262=attr/dropDownWidth 01010263=attr/dropDownAnchor 01010264=attr/imeOptions 01010265=attr/imeActionLabel 01010266=attr/imeActionId 01010267=attr/textColorPrimaryActivated 01010268=attr/imeExtractEnterAnimation 01010269=attr/imeExtractExitAnimation 0101026a=attr/tension 0101026b=attr/extraTension 0101026c=attr/anyDensity 0101026d=attr/searchSuggestThreshold 0101026e=attr/includeInGlobalSearch 0101026f=attr/onClick 01010270=attr/targetSdkVersion 01010271=attr/maxSdkVersion 01010272=attr/testOnly 01010273=attr/contentDescription 01010274=attr/gestureStrokeWidth 01010275=attr/gestureColor 01010276=attr/uncertainGestureColor 01010277=attr/fadeOffset 01010278=attr/fadeDuration 01010279=attr/gestureStrokeType 0101027a=attr/gestureStrokeLengthThreshold 0101027b=attr/gestureStrokeSquarenessThreshold 0101027c=attr/gestureStrokeAngleThreshold 0101027d=attr/eventsInterceptionEnabled 0101027e=attr/fadeEnabled 0101027f=attr/backupAgent 01010280=attr/allowBackup 01010281=attr/glEsVersion 01010282=attr/queryAfterZeroResults 01010283=attr/dropDownHeight 01010284=attr/smallScreens 01010285=attr/normalScreens 01010286=attr/largeScreens 01010287=attr/progressBarStyleInverse 01010288=attr/progressBarStyleSmallInverse 01010289=attr/progressBarStyleLargeInverse 0101028a=attr/searchSettingsDescription 0101028b=attr/textColorPrimaryInverseDisableOnly 0101028c=attr/autoUrlDetect 0101028d=attr/resizeable 0101028e=attr/required 0101028f=attr/accountType 01010290=attr/contentAuthority 01010291=attr/userVisible 01010292=attr/windowShowWallpaper 01010293=attr/wallpaperOpenEnterAnimation 01010294=attr/wallpaperOpenExitAnimation 01010295=attr/wallpaperCloseEnterAnimation 01010296=attr/wallpaperCloseExitAnimation 01010297=attr/wallpaperIntraOpenEnterAnimation 01010298=attr/wallpaperIntraOpenExitAnimation 01010299=attr/wallpaperIntraCloseEnterAnimation 0101029a=attr/wallpaperIntraCloseExitAnimation 0101029b=attr/supportsUploading 0101029c=attr/killAfterRestore 0101029d=attr/restoreNeedsApplication 0101029e=attr/smallIcon 0101029f=attr/accountPreferences 010102a0=attr/textAppearanceSearchResultSubtitle 010102a1=attr/textAppearanceSearchResultTitle 010102a2=attr/summaryColumn 010102a3=attr/detailColumn 010102a4=attr/detailSocialSummary 010102a5=attr/thumbnail 010102a6=attr/detachWallpaper 010102a7=attr/finishOnCloseSystemDialogs 010102a8=attr/scrollbarFadeDuration 010102a9=attr/scrollbarDefaultDelayBeforeFade 010102aa=attr/fadeScrollbars 010102ab=attr/colorBackgroundCacheHint 010102ac=attr/dropDownHorizontalOffset 010102ad=attr/dropDownVerticalOffset 010102ae=attr/quickContactBadgeStyleWindowSmall 010102af=attr/quickContactBadgeStyleWindowMedium 010102b0=attr/quickContactBadgeStyleWindowLarge 010102b1=attr/quickContactBadgeStyleSmallWindowSmall 010102b2=attr/quickContactBadgeStyleSmallWindowMedium 010102b3=attr/quickContactBadgeStyleSmallWindowLarge 010102b4=attr/author 010102b5=attr/autoStart 010102b6=attr/expandableListViewWhiteStyle 010102b7=attr/installLocation 010102b8=attr/vmSafeMode 010102b9=attr/webTextViewStyle 010102ba=attr/restoreAnyVersion 010102bb=attr/tabStripLeft 010102bc=attr/tabStripRight 010102bd=attr/tabStripEnabled 010102be=attr/logo 010102bf=attr/xlargeScreens 010102c0=attr/immersive 010102c1=attr/overScrollMode 010102c2=attr/overScrollHeader 010102c3=attr/overScrollFooter 010102c4=attr/filterTouchesWhenObscured 010102c5=attr/textSelectHandleLeft 010102c6=attr/textSelectHandleRight 010102c7=attr/textSelectHandle 010102c8=attr/textSelectHandleWindowStyle 010102c9=attr/popupAnimationStyle 010102ca=attr/screenSize 010102cb=attr/screenDensity 010102cc=attr/allContactsName 010102cd=attr/windowActionBar 010102ce=attr/actionBarStyle 010102cf=attr/navigationMode 010102d0=attr/displayOptions 010102d1=attr/subtitle 010102d2=attr/customNavigationLayout 010102d3=attr/hardwareAccelerated 010102d4=attr/measureWithLargestChild 010102d5=attr/animateFirstView 010102d6=attr/dropDownSpinnerStyle 010102d7=attr/actionDropDownStyle 010102d8=attr/actionButtonStyle 010102d9=attr/showAsAction 010102da=attr/previewImage 010102db=attr/actionModeBackground 010102dc=attr/actionModeCloseDrawable 010102dd=attr/windowActionModeOverlay 010102de=attr/valueFrom 010102df=attr/valueTo 010102e0=attr/valueType 010102e1=attr/propertyName 010102e2=attr/ordering 010102e3=attr/fragment 010102e4=attr/windowActionBarOverlay 010102e5=attr/fragmentOpenEnterAnimation 010102e6=attr/fragmentOpenExitAnimation 010102e7=attr/fragmentCloseEnterAnimation 010102e8=attr/fragmentCloseExitAnimation 010102e9=attr/fragmentFadeEnterAnimation 010102ea=attr/fragmentFadeExitAnimation 010102eb=attr/actionBarSize 010102ec=attr/imeSubtypeLocale 010102ed=attr/imeSubtypeMode 010102ee=attr/imeSubtypeExtraValue 010102ef=attr/splitMotionEvents 010102f0=attr/listChoiceBackgroundIndicator 010102f1=attr/spinnerMode 010102f2=attr/animateLayoutChanges 010102f3=attr/actionBarTabStyle 010102f4=attr/actionBarTabBarStyle 010102f5=attr/actionBarTabTextStyle 010102f6=attr/actionOverflowButtonStyle 010102f7=attr/actionModeCloseButtonStyle 010102f8=attr/titleTextStyle 010102f9=attr/subtitleTextStyle 010102fa=attr/iconifiedByDefault 010102fb=attr/actionLayout 010102fc=attr/actionViewClass 010102fd=attr/activatedBackgroundIndicator 010102fe=attr/state_activated 010102ff=attr/listPopupWindowStyle 01010300=attr/popupMenuStyle 01010301=attr/textAppearanceLargePopupMenu 01010302=attr/textAppearanceSmallPopupMenu 01010303=attr/breadCrumbTitle 01010304=attr/breadCrumbShortTitle 01010305=attr/listDividerAlertDialog 01010306=attr/textColorAlertDialogListItem 01010307=attr/loopViews 01010308=attr/dialogTheme 01010309=attr/alertDialogTheme 0101030a=attr/dividerVertical 0101030b=attr/homeAsUpIndicator 0101030c=attr/enterFadeDuration 0101030d=attr/exitFadeDuration 0101030e=attr/selectableItemBackground 0101030f=attr/autoAdvanceViewId 01010310=attr/useIntrinsicSizeAsMinimum 01010311=attr/actionModeCutDrawable 01010312=attr/actionModeCopyDrawable 01010313=attr/actionModePasteDrawable 01010314=attr/textEditPasteWindowLayout 01010315=attr/textEditNoPasteWindowLayout 01010316=attr/textIsSelectable 01010317=attr/windowEnableSplitTouch 01010318=attr/indeterminateProgressStyle 01010319=attr/progressBarPadding 0101031a=attr/animationResolution 0101031b=attr/state_accelerated 0101031c=attr/baseline 0101031d=attr/homeLayout 0101031e=attr/opacity 0101031f=attr/alpha 01010320=attr/transformPivotX 01010321=attr/transformPivotY 01010322=attr/translationX 01010323=attr/translationY 01010324=attr/scaleX 01010325=attr/scaleY 01010326=attr/rotation 01010327=attr/rotationX 01010328=attr/rotationY 01010329=attr/showDividers 0101032a=attr/dividerPadding 0101032b=attr/borderlessButtonStyle 0101032c=attr/dividerHorizontal 0101032d=attr/itemPadding 0101032e=attr/buttonBarStyle 0101032f=attr/buttonBarButtonStyle 01010330=attr/segmentedButtonStyle 01010331=attr/staticWallpaperPreview 01010332=attr/allowParallelSyncs 01010333=attr/isAlwaysSyncable 01010334=attr/verticalScrollbarPosition 01010335=attr/fastScrollAlwaysVisible 01010336=attr/fastScrollThumbDrawable 01010337=attr/fastScrollPreviewBackgroundLeft 01010338=attr/fastScrollPreviewBackgroundRight 01010339=attr/fastScrollTrackDrawable 0101033a=attr/fastScrollOverlayPosition 0101033b=attr/customTokens 0101033c=attr/nextFocusForward 0101033d=attr/firstDayOfWeek 0101033e=attr/showWeekNumber 0101033f=attr/minDate 01010340=attr/maxDate 01010341=attr/shownWeekCount 01010342=attr/selectedWeekBackgroundColor 01010343=attr/focusedMonthDateColor 01010344=attr/unfocusedMonthDateColor 01010345=attr/weekNumberColor 01010346=attr/weekSeparatorLineColor 01010347=attr/selectedDateVerticalBar 01010348=attr/weekDayTextAppearance 01010349=attr/dateTextAppearance 0101034a=attr/solidColor 0101034b=attr/spinnersShown 0101034c=attr/calendarViewShown 0101034d=attr/state_multiline 0101034e=attr/detailsElementBackground 0101034f=attr/textColorHighlightInverse 01010350=attr/textColorLinkInverse 01010351=attr/editTextColor 01010352=attr/editTextBackground 01010353=attr/horizontalScrollViewStyle 01010354=attr/layerType 01010355=attr/alertDialogIcon 01010356=attr/windowMinWidthMajor 01010357=attr/windowMinWidthMinor 01010358=attr/queryHint 01010359=attr/fastScrollTextColor 0101035a=attr/largeHeap 0101035b=attr/windowCloseOnTouchOutside 0101035c=attr/datePickerStyle 0101035d=attr/calendarViewStyle 0101035e=attr/textEditSidePasteWindowLayout 0101035f=attr/textEditSideNoPasteWindowLayout 01010360=attr/actionMenuTextAppearance 01010361=attr/actionMenuTextColor 01010362=attr/textCursorDrawable 01010363=attr/resizeMode 01010364=attr/requiresSmallestWidthDp 01010365=attr/compatibleWidthLimitDp 01010366=attr/largestWidthLimitDp 01010367=attr/state_hovered 01010368=attr/state_drag_can_accept 01010369=attr/state_drag_hovered 0101036a=attr/stopWithTask 0101036b=attr/switchTextOn 0101036c=attr/switchTextOff 0101036d=attr/switchPreferenceStyle 0101036e=attr/switchTextAppearance 0101036f=attr/track 01010370=attr/switchMinWidth 01010371=attr/switchPadding 01010372=attr/thumbTextPadding 01010373=attr/textSuggestionsWindowStyle 01010374=attr/textEditSuggestionItemLayout 01010375=attr/rowCount 01010376=attr/rowOrderPreserved 01010377=attr/columnCount 01010378=attr/columnOrderPreserved 01010379=attr/useDefaultMargins 0101037a=attr/alignmentMode 0101037b=attr/layout_row 0101037c=attr/layout_rowSpan 0101037d=attr/layout_columnSpan 0101037e=attr/actionModeSelectAllDrawable 0101037f=attr/isAuxiliary 01010380=attr/accessibilityEventTypes 01010381=attr/packageNames 01010382=attr/accessibilityFeedbackType 01010383=attr/notificationTimeout 01010384=attr/accessibilityFlags 01010385=attr/canRetrieveWindowContent 01010386=attr/listPreferredItemHeightLarge 01010387=attr/listPreferredItemHeightSmall 01010388=attr/actionBarSplitStyle 01010389=attr/actionProviderClass 0101038a=attr/backgroundStacked 0101038b=attr/backgroundSplit 0101038c=attr/textAllCaps 0101038d=attr/colorPressedHighlight 0101038e=attr/colorLongPressedHighlight 0101038f=attr/colorFocusedHighlight 01010390=attr/colorActivatedHighlight 01010391=attr/colorMultiSelectHighlight 01010392=attr/drawableStart 01010393=attr/drawableEnd 01010394=attr/actionModeStyle 01010395=attr/minResizeWidth 01010396=attr/minResizeHeight 01010397=attr/actionBarWidgetTheme 01010398=attr/uiOptions 01010399=attr/subtypeLocale 0101039a=attr/subtypeExtraValue 0101039b=attr/actionBarDivider 0101039c=attr/actionBarItemBackground 0101039d=attr/actionModeSplitBackground 0101039e=attr/textAppearanceListItem 0101039f=attr/textAppearanceListItemSmall 010103a0=attr/targetDescriptions 010103a1=attr/directionDescriptions 010103a2=attr/overridesImplicitlyEnabledSubtype 010103a3=attr/listPreferredItemPaddingLeft 010103a4=attr/listPreferredItemPaddingRight 010103a5=attr/requiresFadingEdge 010103a6=attr/publicKey 010103a7=attr/parentActivityName 010103a8=attr/textColorSecondaryActivated 010103a9=attr/isolatedProcess 010103aa=attr/importantForAccessibility 010103ab=attr/keyboardLayout 010103ac=attr/fontFamily 010103ad=attr/mediaRouteButtonStyle 010103ae=attr/mediaRouteTypes 010103af=attr/supportsRtl 010103b0=attr/textDirection 010103b1=attr/textAlignment 010103b2=attr/layoutDirection 010103b3=attr/paddingStart 010103b4=attr/paddingEnd 010103b5=attr/layout_marginStart 010103b6=attr/layout_marginEnd 010103b7=attr/layout_toStartOf 010103b8=attr/layout_toEndOf 010103b9=attr/layout_alignStart 010103ba=attr/layout_alignEnd 010103bb=attr/layout_alignParentStart 010103bc=attr/layout_alignParentEnd 010103bd=attr/listPreferredItemPaddingStart 010103be=attr/listPreferredItemPaddingEnd 010103bf=attr/singleUser 010103c0=attr/presentationTheme 010103c1=attr/subtypeId 010103c2=attr/initialKeyguardLayout 010103c3=attr/textColorSearchUrl 010103c4=attr/widgetCategory 010103c5=attr/permissionGroupFlags 010103c6=attr/labelFor 010103c7=attr/permissionFlags 010103c8=attr/checkedTextViewStyle 010103c9=attr/showOnLockScreen 010103ca=attr/format12Hour 010103cb=attr/format24Hour 010103cc=attr/timeZone 010103cd=attr/mipMap 010103ce=attr/mirrorForRtl 010103cf=attr/windowOverscan 010103d0=attr/requiredForAllUsers 010103d1=attr/indicatorStart 010103d2=attr/indicatorEnd 010103d3=attr/childIndicatorStart 010103d4=attr/childIndicatorEnd 010103d5=attr/restrictedAccountType 010103d6=attr/requiredAccountType 010103d7=attr/canRequestTouchExplorationMode 010103d8=attr/canRequestEnhancedWebAccessibility 010103d9=attr/canRequestFilterKeyEvents 010103da=attr/layoutMode 010103db=attr/keySet 010103dc=attr/targetId 010103dd=attr/fromScene 010103de=attr/toScene 010103df=attr/transition 010103e0=attr/transitionOrdering 010103e1=attr/fadingMode 010103e2=attr/startDelay 010103e3=attr/ssp 010103e4=attr/sspPrefix 010103e5=attr/sspPattern 010103e6=attr/addPrintersActivity 010103e7=attr/vendor 010103e8=attr/category 010103e9=attr/isAsciiCapable 010103ea=attr/autoMirrored 010103eb=attr/supportsSwitchingToNextInputMethod 010103ec=attr/requireDeviceUnlock 010103ed=attr/apduServiceBanner 010103ee=attr/accessibilityLiveRegion 010103ef=attr/windowTranslucentStatus 010103f0=attr/windowTranslucentNavigation 010103f1=attr/advancedPrintOptionsActivity 010103f2=attr/banner 010103f3=attr/windowSwipeToDismiss 010103f4=attr/isGame 010103f5=attr/allowEmbedded 010103f6=attr/setupActivity 010103f7=attr/fastScrollStyle 010103f8=attr/windowContentTransitions 010103f9=attr/windowContentTransitionManager 010103fa=attr/translationZ 010103fb=attr/tintMode 010103fc=attr/controlX1 010103fd=attr/controlY1 010103fe=attr/controlX2 010103ff=attr/controlY2 01010400=attr/transitionName 01010401=attr/transitionGroup 01010402=attr/viewportWidth 01010403=attr/viewportHeight 01010404=attr/fillColor 01010405=attr/pathData 01010406=attr/strokeColor 01010407=attr/strokeWidth 01010408=attr/trimPathStart 01010409=attr/trimPathEnd 0101040a=attr/trimPathOffset 0101040b=attr/strokeLineCap 0101040c=attr/strokeLineJoin 0101040d=attr/strokeMiterLimit 0101040e=attr/searchWidgetCorpusItemBackground 0101040f=attr/textAppearanceEasyCorrectSuggestion 01010410=attr/textAppearanceMisspelledSuggestion 01010411=attr/textAppearanceAutoCorrectionSuggestion 01010412=attr/textUnderlineColor 01010413=attr/textUnderlineThickness 01010414=attr/errorMessageBackground 01010415=attr/errorMessageAboveBackground 01010416=attr/searchResultListItemHeight 01010417=attr/dropdownListPreferredItemHeight 01010418=attr/windowBackgroundFallback 01010419=attr/windowActionBarFullscreenDecorLayout 0101041a=attr/alertDialogButtonGroupStyle 0101041b=attr/alertDialogCenterButtons 0101041c=attr/panelMenuIsCompact 0101041d=attr/panelMenuListWidth 0101041e=attr/panelMenuListTheme 0101041f=attr/gestureOverlayViewStyle 01010420=attr/quickContactBadgeOverlay 01010421=attr/fragmentBreadCrumbsStyle 01010422=attr/numberPickerStyle 01010423=attr/activityChooserViewStyle 01010424=attr/actionModePopupWindowStyle 01010425=attr/preferenceActivityStyle 01010426=attr/preferenceFragmentStyle 01010427=attr/preferencePanelStyle 01010428=attr/preferenceHeaderPanelStyle 01010429=attr/colorControlNormal 0101042a=attr/colorControlActivated 0101042b=attr/colorButtonNormal 0101042c=attr/colorControlHighlight 0101042d=attr/persistableMode 0101042e=attr/titleTextAppearance 0101042f=attr/subtitleTextAppearance 01010430=attr/slideEdge 01010431=attr/actionBarTheme 01010432=attr/textAppearanceListItemSecondary 01010433=attr/colorPrimary 01010434=attr/colorPrimaryDark 01010435=attr/colorAccent 01010436=attr/nestedScrollingEnabled 01010437=attr/windowEnterTransition 01010438=attr/windowExitTransition 01010439=attr/windowSharedElementEnterTransition 0101043a=attr/windowSharedElementExitTransition 0101043b=attr/windowAllowReturnTransitionOverlap 0101043c=attr/windowAllowEnterTransitionOverlap 0101043d=attr/sessionService 0101043e=attr/stackViewStyle 0101043f=attr/switchStyle 01010440=attr/elevation 01010441=attr/excludeId 01010442=attr/excludeClass 01010443=attr/hideOnContentScroll 01010444=attr/actionOverflowMenuStyle 01010445=attr/documentLaunchMode 01010446=attr/maxRecents 01010447=attr/autoRemoveFromRecents 01010448=attr/stateListAnimator 01010449=attr/toId 0101044a=attr/fromId 0101044b=attr/reversible 0101044c=attr/splitTrack 0101044d=attr/targetName 0101044e=attr/excludeName 0101044f=attr/matchOrder 01010450=attr/windowDrawsSystemBarBackgrounds 01010451=attr/statusBarColor 01010452=attr/navigationBarColor 01010453=attr/contentInsetStart 01010454=attr/contentInsetEnd 01010455=attr/contentInsetLeft 01010456=attr/contentInsetRight 01010457=attr/paddingMode 01010458=attr/layout_rowWeight 01010459=attr/layout_columnWeight 0101045a=attr/translateX 0101045b=attr/translateY 0101045c=attr/selectableItemBackgroundBorderless 0101045d=attr/elegantTextHeight 0101045e=attr/searchKeyphraseId 0101045f=attr/searchKeyphrase 01010460=attr/searchKeyphraseSupportedLocales 01010461=attr/windowTransitionBackgroundFadeDuration 01010462=attr/overlapAnchor 01010463=attr/progressTint 01010464=attr/progressTintMode 01010465=attr/progressBackgroundTint 01010466=attr/progressBackgroundTintMode 01010467=attr/secondaryProgressTint 01010468=attr/secondaryProgressTintMode 01010469=attr/indeterminateTint 0101046a=attr/indeterminateTintMode 0101046b=attr/backgroundTint 0101046c=attr/backgroundTintMode 0101046d=attr/foregroundTint 0101046e=attr/foregroundTintMode 0101046f=attr/buttonTint 01010470=attr/buttonTintMode 01010471=attr/thumbTint 01010472=attr/thumbTintMode 01010473=attr/fullBackupOnly 01010474=attr/propertyXName 01010475=attr/propertyYName 01010476=attr/relinquishTaskIdentity 01010477=attr/tileModeX 01010478=attr/tileModeY 01010479=attr/actionModeShareDrawable 0101047a=attr/actionModeFindDrawable 0101047b=attr/actionModeWebSearchDrawable 0101047c=attr/transitionVisibilityMode 0101047d=attr/minimumHorizontalAngle 0101047e=attr/minimumVerticalAngle 0101047f=attr/maximumAngle 01010480=attr/searchViewStyle 01010481=attr/closeIcon 01010482=attr/goIcon 01010483=attr/searchIcon 01010484=attr/voiceIcon 01010485=attr/commitIcon 01010486=attr/suggestionRowLayout 01010487=attr/queryBackground 01010488=attr/submitBackground 01010489=attr/buttonBarPositiveButtonStyle 0101048a=attr/buttonBarNeutralButtonStyle 0101048b=attr/buttonBarNegativeButtonStyle 0101048c=attr/popupElevation 0101048d=attr/actionBarPopupTheme 0101048e=attr/multiArch 0101048f=attr/touchscreenBlocksFocus 01010490=attr/windowElevation 01010491=attr/launchTaskBehindTargetAnimation 01010492=attr/launchTaskBehindSourceAnimation 01010493=attr/restrictionType 01010494=attr/dayOfWeekBackground 01010495=attr/dayOfWeekTextAppearance 01010496=attr/headerMonthTextAppearance 01010497=attr/headerDayOfMonthTextAppearance 01010498=attr/headerYearTextAppearance 01010499=attr/yearListItemTextAppearance 0101049a=attr/yearListSelectorColor 0101049b=attr/calendarTextColor 0101049c=attr/recognitionService 0101049d=attr/timePickerStyle 0101049e=attr/timePickerDialogTheme 0101049f=attr/headerTimeTextAppearance 010104a0=attr/headerAmPmTextAppearance 010104a1=attr/numbersTextColor 010104a2=attr/numbersBackgroundColor 010104a3=attr/numbersSelectorColor 010104a4=attr/amPmTextColor 010104a5=attr/amPmBackgroundColor 010104a6=attr/searchKeyphraseRecognitionFlags 010104a7=attr/checkMarkTint 010104a8=attr/checkMarkTintMode 010104a9=attr/popupTheme 010104aa=attr/toolbarStyle 010104ab=attr/windowClipToOutline 010104ac=attr/datePickerDialogTheme 010104ad=attr/showText 010104ae=attr/windowReturnTransition 010104af=attr/windowReenterTransition 010104b0=attr/windowSharedElementReturnTransition 010104b1=attr/windowSharedElementReenterTransition 010104b2=attr/resumeWhilePausing 010104b3=attr/datePickerMode 010104b4=attr/timePickerMode 010104b5=attr/inset 010104b6=attr/letterSpacing 010104b7=attr/fontFeatureSettings 010104b8=attr/outlineProvider 010104b9=attr/contentAgeHint 010104ba=attr/country 010104bb=attr/windowSharedElementsUseOverlay 010104bc=attr/reparent 010104bd=attr/reparentWithOverlay 010104be=attr/ambientShadowAlpha 010104bf=attr/spotShadowAlpha 010104c0=attr/navigationIcon 010104c1=attr/navigationContentDescription 010104c2=attr/fragmentExitTransition 010104c3=attr/fragmentEnterTransition 010104c4=attr/fragmentSharedElementEnterTransition 010104c5=attr/fragmentReturnTransition 010104c6=attr/fragmentSharedElementReturnTransition 010104c7=attr/fragmentReenterTransition 010104c8=attr/fragmentAllowEnterTransitionOverlap 010104c9=attr/fragmentAllowReturnTransitionOverlap 010104ca=attr/patternPathData 010104cb=attr/strokeAlpha 010104cc=attr/fillAlpha 010104cd=attr/windowActivityTransitions 010104ce=attr/colorEdgeEffect 010104cf=attr/resizeClip 010104d0=attr/collapseContentDescription 010104d1=attr/accessibilityTraversalBefore 010104d2=attr/accessibilityTraversalAfter 010104d3=attr/dialogPreferredPadding 010104d4=attr/searchHintIcon 010104d5=attr/revisionCode 010104d6=attr/drawableTint 010104d7=attr/drawableTintMode 010104d8=attr/fraction 010104d9=attr/trackTint 010104da=attr/trackTintMode 010104db=attr/start 010104dc=attr/end 010104dd=attr/breakStrategy 010104de=attr/hyphenationFrequency 010104df=attr/allowUndo 010104e0=attr/windowLightStatusBar 010104e1=attr/numbersInnerTextColor 010104e2=attr/colorBackgroundFloating 010104e3=attr/titleTextColor 010104e4=attr/subtitleTextColor 010104e5=attr/thumbPosition 010104e6=attr/scrollIndicators 010104e7=attr/contextClickable 010104e8=attr/fingerprintAuthDrawable 010104e9=attr/logoDescription 010104ea=attr/extractNativeLibs 010104eb=attr/fullBackupContent 010104ec=attr/usesCleartextTraffic 010104ed=attr/lockTaskMode 010104ee=attr/autoVerify 010104ef=attr/showForAllUsers 010104f0=attr/supportsAssist 010104f1=attr/supportsLaunchVoiceAssistFromKeyguard 010104f2=attr/listMenuViewStyle 010104f3=attr/subMenuArrow 010104f4=attr/defaultWidth 010104f5=attr/defaultHeight 010104f6=attr/resizeableActivity 010104f7=attr/supportsPictureInPicture 010104f8=attr/titleMargin 010104f9=attr/titleMarginStart 010104fa=attr/titleMarginEnd 010104fb=attr/titleMarginTop 010104fc=attr/titleMarginBottom 010104fd=attr/maxButtonHeight 010104fe=attr/buttonGravity 010104ff=attr/collapseIcon 01010500=attr/level 01010501=attr/contextPopupMenuStyle 01010502=attr/textAppearancePopupMenuHeader 01010503=attr/windowBackgroundFallback 01010504=attr/defaultToDeviceProtectedStorage 01010505=attr/directBootAware 01010506=attr/preferenceFragmentStyle 01010507=attr/canControlMagnification 01010508=attr/languageTag 01010509=attr/pointerIcon 0101050a=attr/tickMark 0101050b=attr/tickMarkTint 0101050c=attr/tickMarkTintMode 0101050d=attr/canPerformGestures 0101050e=attr/externalService 0101050f=attr/supportsLocalInteraction 01010510=attr/startX 01010511=attr/startY 01010512=attr/endX 01010513=attr/endY 01010514=attr/offset 01010515=attr/use32bitAbi 01010516=attr/bitmap 01010517=attr/hotSpotX 01010518=attr/hotSpotY 01010519=attr/version 0101051a=attr/backupInForeground 0101051b=attr/countDown 0101051c=attr/canRecord 0101051d=attr/tunerCount 0101051e=attr/fillType 0101051f=attr/popupEnterTransition 01010520=attr/popupExitTransition 01010521=attr/forceHasOverlappingRendering 01010522=attr/contentInsetStartWithNavigation 01010523=attr/contentInsetEndWithActions 01010524=attr/numberPickerStyle 01010525=attr/enableVrMode 01010526=attr/hash 01010527=attr/networkSecurityConfig 01010528=attr/shortcutId 01010529=attr/shortcutShortLabel 0101052a=attr/shortcutLongLabel 0101052b=attr/shortcutDisabledMessage 0101052c=attr/roundIcon 0101052d=attr/contextUri 0101052e=attr/contextDescription 0101052f=attr/showMetadataInPreview 01010530=attr/colorSecondary 01010531=attr/visibleToInstantApps 01010532=attr/font 01010533=attr/fontWeight 01010534=attr/tooltipText 01010535=attr/autoSizeTextType 01010536=attr/autoSizeStepGranularity 01010537=attr/autoSizePresetSizes 01010538=attr/autoSizeMinTextSize 01010539=attr/min 0101053a=attr/rotationAnimation 0101053b=attr/layout_marginHorizontal 0101053c=attr/layout_marginVertical 0101053d=attr/paddingHorizontal 0101053e=attr/paddingVertical 0101053f=attr/fontStyle 01010540=attr/keyboardNavigationCluster 01010541=attr/targetProcesses 01010542=attr/nextClusterForward 01010543=attr/colorError 01010544=attr/focusedByDefault 01010545=attr/appCategory 01010546=attr/autoSizeMaxTextSize 01010547=attr/recreateOnConfigChanges 01010548=attr/certDigest 01010549=attr/splitName 0101054a=attr/colorMode 0101054b=attr/isolatedSplits 0101054c=attr/targetSandboxVersion 0101054d=attr/canRequestFingerprintGestures 0101054e=attr/alphabeticModifiers 0101054f=attr/numericModifiers 01010550=attr/fontProviderAuthority 01010551=attr/fontProviderQuery 01010552=attr/primaryContentAlpha 01010553=attr/secondaryContentAlpha 01010554=attr/requiredFeature 01010555=attr/requiredNotFeature 01010556=attr/autofillHints 01010557=attr/fontProviderPackage 01010558=attr/importantForAutofill 01010559=attr/recycleEnabled 0101055a=attr/isStatic 0101055b=attr/isFeatureSplit 0101055c=attr/singleLineTitle 0101055d=attr/fontProviderCerts 0101055e=attr/iconTint 0101055f=attr/iconTintMode 01010560=attr/maxAspectRatio 01010561=attr/iconSpaceReserved 01010562=attr/defaultFocusHighlightEnabled 01010563=attr/persistentWhenFeatureAvailable 01010564=attr/windowSplashscreenContent 01010565=attr/requiredSystemPropertyName 01010566=attr/requiredSystemPropertyValue 01010567=attr/justificationMode 01010568=attr/autofilledHighlight 01010569=attr/showWhenLocked 0101056a=attr/turnScreenOn 0101056b=attr/classLoader 0101056c=attr/windowLightNavigationBar 0101056d=attr/navigationBarDividerColor 0101056e=attr/cantSaveState 0101056f=attr/ttcIndex 01010570=attr/fontVariationSettings 01010571=attr/dialogCornerRadius 01010572=attr/compileSdkVersion 01010573=attr/compileSdkVersionCodename 01010574=attr/screenReaderFocusable 01010575=attr/buttonCornerRadius 01010576=attr/versionCodeMajor 01010577=attr/versionMajor 01010578=attr/isVrOnly 01010579=attr/widgetFeatures 0101057a=attr/appComponentFactory 0101057b=attr/fallbackLineSpacing 0101057c=attr/accessibilityPaneTitle 0101057d=attr/firstBaselineToTopHeight 0101057e=attr/lastBaselineToBottomHeight 0101057f=attr/lineHeight 01010580=attr/accessibilityHeading 01010581=attr/outlineSpotShadowColor 01010582=attr/outlineAmbientShadowColor 01010583=attr/maxLongVersionCode 01010584=attr/userRestriction 01010585=attr/textFontWeight 01010586=attr/windowLayoutInDisplayCutoutMode 01010587=attr/packageType 01010588=attr/opticalInsetLeft 01010589=attr/opticalInsetTop 0101058a=attr/opticalInsetRight 0101058b=attr/opticalInsetBottom 0101058c=attr/forceDarkAllowed 0101058d=attr/supportsAmbientMode 0101058e=attr/usesNonSdkApi 0101058f=attr/nonInteractiveUiTimeout 01010590=attr/isLightTheme 01010591=attr/isSplitRequired 01010592=attr/textLocale 01010593=attr/settingsSliceUri 01010594=attr/shell 01010595=attr/interactiveUiTimeout 01010596=attr/supportsMultipleDisplays 01010597=attr/useAppZygote 01010598=attr/selectionDividerHeight 01010599=attr/foregroundServiceType 0101059a=attr/hasFragileUserData 0101059b=attr/minAspectRatio 0101059c=attr/inheritShowWhenLocked 0101059d=attr/zygotePreloadName 0101059e=attr/useEmbeddedDex 0101059f=attr/forceUriPermissions 01010600=attr/allowClearUserDataOnFailedRestore 01010601=attr/allowAudioPlaybackCapture 01010602=attr/secureElementName 01010603=attr/requestLegacyExternalStorage 01010604=attr/enforceStatusBarContrast 01010605=attr/enforceNavigationBarContrast 01010606=attr/identifier 01010607=attr/importantForContentCapture 01010608=attr/forceQueryable 01010609=attr/resourcesMap 0101060a=attr/animatedImageDrawable 0101060b=attr/htmlDescription 0101060c=attr/preferMinimalPostProcessing 0101060d=attr/supportsInlineSuggestions 0101060e=attr/crossProfile 0101060f=attr/canTakeScreenshot 01010610=attr/sdkVersion 01010611=attr/minExtensionVersion 01010612=attr/allowNativeHeapPointerTagging 01010613=attr/autoRevokePermissions 01010614=attr/preserveLegacyExternalStorage 01010615=attr/mimeGroup 01010616=attr/gwpAsanMode 01010617=attr/rollbackDataPolicy 01010618=attr/allowClickWhenDisabled 01010619=attr/windowLayoutAffinity 0101061a=attr/canPauseRecording 0101061b=attr/windowBlurBehindRadius 0101061c=attr/windowBlurBehindEnabled 0101061d=attr/requireDeviceScreenOn 0101061e=attr/pathSuffix 0101061f=attr/sspSuffix 01010620=attr/pathAdvancedPattern 01010621=attr/sspAdvancedPattern 01010622=attr/fontProviderSystemFontFamily 01010623=attr/hand_second 01010624=attr/memtagMode 01010625=attr/nativeHeapZeroInitialized 01010626=attr/hotwordDetectionService 01010627=attr/previewLayout 01010628=attr/clipToOutline 0101062a=attr/knownCerts 0101062b=attr/windowBackgroundBlurRadius 0101062c=attr/windowSplashScreenBackground 0101062d=attr/windowSplashScreenAnimatedIcon 0101062e=attr/windowSplashScreenAnimationDuration 0101062f=attr/windowSplashScreenBrandingImage 01010630=attr/windowSplashScreenIconBackgroundColor 01010631=attr/splashScreenTheme 01010632=attr/maxResizeWidth 01010633=attr/maxResizeHeight 01010634=attr/targetCellWidth 01010635=attr/targetCellHeight 01010636=attr/dialTint 01010637=attr/dialTintMode 01010638=attr/hand_hourTint 01010639=attr/hand_hourTintMode 0101063a=attr/hand_minuteTint 0101063b=attr/hand_minuteTintMode 0101063c=attr/hand_secondTint 0101063d=attr/hand_secondTintMode 0101063e=attr/dataExtractionRules 0101063f=attr/passwordsActivity 01010640=attr/selectableAsDefault 01010641=attr/isAccessibilityTool 01010642=attr/attributionTags 01010643=attr/suppressesSpellChecker 01010644=attr/usesPermissionFlags 01010645=attr/requestRawExternalStorageAccess 01010646=attr/playHomeTransitionSound 01010647=attr/lStar 01010648=attr/showInInputMethodPicker 01010649=attr/effectColor 0101064a=attr/requestForegroundServiceExemption 0101064b=attr/attributionsAreUserVisible 0101064c=attr/shouldUseDefaultUnfoldTransition 0101064d=attr/sharedUserMaxSdkVersion 0101064e=attr/requiredSplitTypes 0101064f=attr/splitTypes 01010650=attr/canDisplayOnRemoteDevices 01010651=attr/supportedTypes 01010652=attr/resetEnabledSettingsOnAppDataCleared 01010653=attr/supportsStylusHandwriting 01010654=attr/showClockAndComplications 01010655=attr/gameSessionService 01010656=attr/supportsBatteryGameMode 01010657=attr/supportsPerformanceGameMode 01010658=attr/allowGameAngleDriver 01010659=attr/allowGameDownscaling 0101065a=attr/allowGameFpsOverride 0101065b=attr/localeConfig 0101065c=attr/showBackdrop 0101065d=attr/preferKeepClear 0101065e=attr/autoHandwritingEnabled 0101065f=attr/fromExtendLeft 01010660=attr/fromExtendTop 01010661=attr/fromExtendRight 01010662=attr/fromExtendBottom 01010663=attr/toExtendLeft 01010664=attr/toExtendTop 01010665=attr/toExtendRight 01010666=attr/toExtendBottom 01010667=attr/tileService 01010668=attr/windowSplashScreenBehavior 01010669=attr/allowUntrustedActivityEmbedding 0101066a=attr/knownActivityEmbeddingCerts 0101066b=attr/intro 0101066c=attr/enableOnBackInvokedCallback 0101066d=attr/supportsInlineSuggestionsWithTouchExploration 0101066e=attr/lineBreakStyle 0101066f=attr/lineBreakWordStyle 01010670=attr/maxDrawableWidth 01010671=attr/maxDrawableHeight 01010672=attr/backdropColor 01010673=attr/handwritingBoundsOffsetLeft 01010674=attr/handwritingBoundsOffsetTop 01010675=attr/handwritingBoundsOffsetRight 01010676=attr/handwritingBoundsOffsetBottom 01010677=attr/accessibilityDataSensitive 01010678=attr/enableTextStylingShortcuts 01010679=attr/requiredDisplayCategory 0101067a=attr/visualQueryDetectionService 0101067b=attr/physicalKeyboardHintLanguageTag 0101067c=attr/physicalKeyboardHintLayoutType 0101067d=attr/allowSharedIsolatedProcess 0101067e=attr/keyboardLocale 0101067f=attr/keyboardLayoutType 01010680=attr/allowUpdateOwnership 01010681=attr/isCredential 01010682=attr/searchResultHighlightColor 01010683=attr/focusedSearchResultHighlightColor 01010684=attr/stylusHandwritingSettingsActivity 01010685=attr/windowNoMoveAnimation 01010686=attr/settingsSubtitle 01010687=attr/capability 01010688=attr/defaultLocale 01010689=attr/isVirtualDeviceOnly 0101068c=attr/featureFlag 0101068d=attr/systemUserOnly 0101068e=attr/allow 0101068f=attr/query 01010690=attr/queryPrefix 01010691=attr/queryPattern 01010692=attr/queryAdvancedPattern 01010693=attr/querySuffix 01010694=attr/fragmentPrefix 01010695=attr/fragmentPattern 01010696=attr/fragmentAdvancedPattern 01010697=attr/fragmentSuffix 01010698=attr/useBoundsForWidth 01010699=attr/autoTransact 0101069a=attr/windowOptOutEdgeToEdgeEnforcement 0101069b=attr/requireContentUriPermissionFromCaller 0101069d=attr/useLocalePreferredLineHeightForMinimum 0101069e=attr/contentSensitivity 0101069f=attr/supportsConnectionlessStylusHandwriting 010106a0=attr/shouldDefaultToObserveMode 010106a1=attr/allowCrossUidActivitySwitchFromBelow 010106a2=attr/shiftDrawingOffsetForStartOverhang 010106a3=attr/windowIsFrameRatePowerSavingsBalanced 010106a4=attr/adServiceTypes 010106a5=attr/languageSettingsActivity 010106a6=attr/dreamCategory 010106a7=attr/backgroundPermission 010106a8=attr/supplementalDescription 010106a9=attr/intentMatchingFlags 010106aa=attr/layoutLabel 010106ab=attr/pageSizeCompat 010106ac=attr/wantsRoleHolderPriority 01020000=id/background 01020001=id/checkbox 01020002=id/content 01020003=id/edit 01020004=id/empty 01020005=id/hint 01020006=id/icon 01020007=id/icon1 01020008=id/icon2 01020009=id/input 0102000a=id/list 0102000b=id/message 0102000c=id/primary 0102000d=id/progress 0102000e=id/selectedIcon 0102000f=id/secondaryProgress 01020010=id/summary 01020011=id/tabcontent 01020012=id/tabhost 01020013=id/tabs 01020014=id/text1 01020015=id/text2 01020016=id/title 01020017=id/toggle 01020018=id/widget_frame 01020019=id/button1 0102001a=id/button2 0102001b=id/button3 0102001c=id/extractArea 0102001d=id/candidatesArea 0102001e=id/inputArea 0102001f=id/selectAll 01020020=id/cut 01020021=id/copy 01020022=id/paste 01020023=id/copyUrl 01020024=id/switchInputMethod 01020025=id/inputExtractEditText 01020026=id/keyboardView 01020027=id/closeButton 01020028=id/startSelectingText 01020029=id/stopSelectingText 0102002a=id/addToDictionary 0102002b=id/custom 0102002c=id/home 0102002d=id/selectTextMode 0102002e=id/mask 0102002f=id/statusBarBackground 01020030=id/navigationBarBackground 01020031=id/pasteAsPlainText 01020032=id/undo 01020033=id/redo 01020034=id/replaceText 01020035=id/shareText 01020036=id/accessibilityActionShowOnScreen 01020037=id/accessibilityActionScrollToPosition 01020038=id/accessibilityActionScrollUp 01020039=id/accessibilityActionScrollLeft 0102003a=id/accessibilityActionScrollDown 0102003b=id/accessibilityActionScrollRight 0102003c=id/accessibilityActionContextClick 0102003d=id/accessibilityActionSetProgress 0102003e=id/icon_frame 0102003f=id/list_container 01020040=id/switch_widget 01020041=id/textAssist 01020042=id/accessibilityActionMoveWindow 01020043=id/autofill 01020044=id/accessibilityActionShowTooltip 01020045=id/accessibilityActionHideTooltip 01020046=id/accessibilityActionPageUp 01020047=id/accessibilityActionPageDown 01020048=id/accessibilityActionPageLeft 01020049=id/accessibilityActionPageRight 0102004a=id/accessibilityActionPressAndHold 0102004b=id/accessibilitySystemActionBack 0102004c=id/accessibilitySystemActionHome 0102004d=id/accessibilitySystemActionRecents 0102004e=id/accessibilitySystemActionNotifications 0102004f=id/accessibilitySystemActionQuickSettings 01020050=id/accessibilitySystemActionPowerDialog 01020051=id/accessibilitySystemActionToggleSplitScreen 01020052=id/accessibilitySystemActionLockScreen 01020053=id/accessibilitySystemActionTakeScreenshot 01020054=id/accessibilityActionImeEnter 01020055=id/accessibilityActionDragStart 01020056=id/accessibilityActionDragDrop 01020057=id/accessibilityActionDragCancel 01020058=id/accessibilityActionShowTextSuggestions 01020059=id/inputExtractAction 0102005a=id/inputExtractAccessories 0102005b=id/bold 0102005c=id/italic 0102005d=id/underline 0102005e=id/accessibilityActionScrollInDirection 0102005f=id/ALT 01020060=id/CAPS_LOCK 01020061=id/CTRL 01020062=id/FUNCTION 01020063=id/KEYCODE_0 01020064=id/KEYCODE_1 01020065=id/KEYCODE_11 01020066=id/KEYCODE_12 01020067=id/KEYCODE_2 01020068=id/KEYCODE_3 01020069=id/KEYCODE_3D_MODE 0102006a=id/KEYCODE_4 0102006b=id/KEYCODE_5 0102006c=id/KEYCODE_6 0102006d=id/KEYCODE_7 0102006e=id/KEYCODE_8 0102006f=id/KEYCODE_9 01020070=id/KEYCODE_A 01020071=id/KEYCODE_ALL_APPS 01020072=id/KEYCODE_ALT_LEFT 01020073=id/KEYCODE_ALT_RIGHT 01020074=id/KEYCODE_APOSTROPHE 01020075=id/KEYCODE_APP_SWITCH 01020076=id/KEYCODE_ASSIST 01020077=id/KEYCODE_AT 01020078=id/KEYCODE_AVR_INPUT 01020079=id/KEYCODE_AVR_POWER 0102007a=id/KEYCODE_B 0102007b=id/KEYCODE_BACK 0102007c=id/KEYCODE_BACKSLASH 0102007d=id/KEYCODE_BOOKMARK 0102007e=id/KEYCODE_BREAK 0102007f=id/KEYCODE_BRIGHTNESS_DOWN 01020080=id/KEYCODE_BRIGHTNESS_UP 01020081=id/KEYCODE_BUTTON_1 01020082=id/KEYCODE_BUTTON_10 01020083=id/KEYCODE_BUTTON_11 01020084=id/KEYCODE_BUTTON_12 01020085=id/KEYCODE_BUTTON_13 01020086=id/KEYCODE_BUTTON_14 01020087=id/KEYCODE_BUTTON_15 01020088=id/KEYCODE_BUTTON_16 01020089=id/KEYCODE_BUTTON_2 0102008a=id/KEYCODE_BUTTON_3 0102008b=id/KEYCODE_BUTTON_4 0102008c=id/KEYCODE_BUTTON_5 0102008d=id/KEYCODE_BUTTON_6 0102008e=id/KEYCODE_BUTTON_7 0102008f=id/KEYCODE_BUTTON_8 01020090=id/KEYCODE_BUTTON_9 01020091=id/KEYCODE_BUTTON_A 01020092=id/KEYCODE_BUTTON_B 01020093=id/KEYCODE_BUTTON_C 01020094=id/KEYCODE_BUTTON_L1 01020095=id/KEYCODE_BUTTON_L2 01020096=id/KEYCODE_BUTTON_MODE 01020097=id/KEYCODE_BUTTON_R1 01020098=id/KEYCODE_BUTTON_R2 01020099=id/KEYCODE_BUTTON_SELECT 0102009a=id/KEYCODE_BUTTON_START 0102009b=id/KEYCODE_BUTTON_THUMBL 0102009c=id/KEYCODE_BUTTON_THUMBR 0102009d=id/KEYCODE_BUTTON_X 0102009e=id/KEYCODE_BUTTON_Y 0102009f=id/KEYCODE_BUTTON_Z 010200a0=id/KEYCODE_C 010200a1=id/KEYCODE_CALCULATOR 010200a2=id/KEYCODE_CALENDAR 010200a3=id/KEYCODE_CALL 010200a4=id/KEYCODE_CAMERA 010200a5=id/KEYCODE_CAPS_LOCK 010200a6=id/KEYCODE_CAPTIONS 010200a7=id/KEYCODE_CHANNEL_DOWN 010200a8=id/KEYCODE_CHANNEL_UP 010200a9=id/KEYCODE_CLEAR 010200aa=id/KEYCODE_COMMA 010200ab=id/KEYCODE_CONTACTS 010200ac=id/KEYCODE_COPY 010200ad=id/KEYCODE_CTRL_LEFT 010200ae=id/KEYCODE_CTRL_RIGHT 010200af=id/KEYCODE_CUT 010200b0=id/KEYCODE_D 010200b1=id/KEYCODE_DEL 010200b2=id/KEYCODE_DEMO_APP_1 010200b3=id/KEYCODE_DEMO_APP_2 010200b4=id/KEYCODE_DEMO_APP_3 010200b5=id/KEYCODE_DEMO_APP_4 010200b6=id/KEYCODE_DPAD_CENTER 010200b7=id/KEYCODE_DPAD_DOWN 010200b8=id/KEYCODE_DPAD_DOWN_LEFT 010200b9=id/KEYCODE_DPAD_DOWN_RIGHT 010200ba=id/KEYCODE_DPAD_LEFT 010200bb=id/KEYCODE_DPAD_RIGHT 010200bc=id/KEYCODE_DPAD_UP 010200bd=id/KEYCODE_DPAD_UP_LEFT 010200be=id/KEYCODE_DPAD_UP_RIGHT 010200bf=id/KEYCODE_DVR 010200c0=id/KEYCODE_E 010200c1=id/KEYCODE_EISU 010200c2=id/KEYCODE_EMOJI_PICKER 010200c3=id/KEYCODE_ENDCALL 010200c4=id/KEYCODE_ENTER 010200c5=id/KEYCODE_ENVELOPE 010200c6=id/KEYCODE_EQUALS 010200c7=id/KEYCODE_ESCAPE 010200c8=id/KEYCODE_EXPLORER 010200c9=id/KEYCODE_F 010200ca=id/KEYCODE_F1 010200cb=id/KEYCODE_F10 010200cc=id/KEYCODE_F11 010200cd=id/KEYCODE_F12 010200ce=id/KEYCODE_F2 010200cf=id/KEYCODE_F3 010200d0=id/KEYCODE_F4 010200d1=id/KEYCODE_F5 010200d2=id/KEYCODE_F6 010200d3=id/KEYCODE_F7 010200d4=id/KEYCODE_F8 010200d5=id/KEYCODE_F9 010200d6=id/KEYCODE_FEATURED_APP_1 010200d7=id/KEYCODE_FEATURED_APP_2 010200d8=id/KEYCODE_FEATURED_APP_3 010200d9=id/KEYCODE_FEATURED_APP_4 010200da=id/KEYCODE_FOCUS 010200db=id/KEYCODE_FORWARD 010200dc=id/KEYCODE_FORWARD_DEL 010200dd=id/KEYCODE_FUNCTION 010200de=id/KEYCODE_G 010200df=id/KEYCODE_GRAVE 010200e0=id/KEYCODE_GUIDE 010200e1=id/KEYCODE_H 010200e2=id/KEYCODE_HEADSETHOOK 010200e3=id/KEYCODE_HELP 010200e4=id/KEYCODE_HENKAN 010200e5=id/KEYCODE_HOME 010200e6=id/KEYCODE_I 010200e7=id/KEYCODE_INFO 010200e8=id/KEYCODE_INSERT 010200e9=id/KEYCODE_J 010200ea=id/KEYCODE_K 010200eb=id/KEYCODE_KANA 010200ec=id/KEYCODE_KATAKANA_HIRAGANA 010200ed=id/KEYCODE_KEYBOARD_BACKLIGHT_DOWN 010200ee=id/KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE 010200ef=id/KEYCODE_KEYBOARD_BACKLIGHT_UP 010200f0=id/KEYCODE_L 010200f1=id/KEYCODE_LANGUAGE_SWITCH 010200f2=id/KEYCODE_LAST_CHANNEL 010200f3=id/KEYCODE_LEFT_BRACKET 010200f4=id/KEYCODE_M 010200f5=id/KEYCODE_MACRO_1 010200f6=id/KEYCODE_MACRO_2 010200f7=id/KEYCODE_MACRO_3 010200f8=id/KEYCODE_MACRO_4 010200f9=id/KEYCODE_MANNER_MODE 010200fa=id/KEYCODE_MEDIA_AUDIO_TRACK 010200fb=id/KEYCODE_MEDIA_CLOSE 010200fc=id/KEYCODE_MEDIA_EJECT 010200fd=id/KEYCODE_MEDIA_FAST_FORWARD 010200fe=id/KEYCODE_MEDIA_NEXT 010200ff=id/KEYCODE_MEDIA_PAUSE 01020100=id/KEYCODE_MEDIA_PLAY 01020101=id/KEYCODE_MEDIA_PLAY_PAUSE 01020102=id/KEYCODE_MEDIA_PREVIOUS 01020103=id/KEYCODE_MEDIA_RECORD 01020104=id/KEYCODE_MEDIA_REWIND 01020105=id/KEYCODE_MEDIA_SKIP_BACKWARD 01020106=id/KEYCODE_MEDIA_SKIP_FORWARD 01020107=id/KEYCODE_MEDIA_SLEEP 01020108=id/KEYCODE_MEDIA_STEP_BACKWARD 01020109=id/KEYCODE_MEDIA_STEP_FORWARD 0102010a=id/KEYCODE_MEDIA_STOP 0102010b=id/KEYCODE_MEDIA_TOP_MENU 0102010c=id/KEYCODE_MEDIA_WAKEUP 0102010d=id/KEYCODE_MENU 0102010e=id/KEYCODE_META_LEFT 0102010f=id/KEYCODE_META_RIGHT 01020110=id/KEYCODE_MINUS 01020111=id/KEYCODE_MOVE_END 01020112=id/KEYCODE_MOVE_HOME 01020113=id/KEYCODE_MUHENKAN 01020114=id/KEYCODE_MUSIC 01020115=id/KEYCODE_MUTE 01020116=id/KEYCODE_N 01020117=id/KEYCODE_NAVIGATE_IN 01020118=id/KEYCODE_NAVIGATE_NEXT 01020119=id/KEYCODE_NAVIGATE_OUT 0102011a=id/KEYCODE_NAVIGATE_PREVIOUS 0102011b=id/KEYCODE_NOTIFICATION 0102011c=id/KEYCODE_NUM 0102011d=id/KEYCODE_NUMPAD_0 0102011e=id/KEYCODE_NUMPAD_1 0102011f=id/KEYCODE_NUMPAD_2 01020120=id/KEYCODE_NUMPAD_3 01020121=id/KEYCODE_NUMPAD_4 01020122=id/KEYCODE_NUMPAD_5 01020123=id/KEYCODE_NUMPAD_6 01020124=id/KEYCODE_NUMPAD_7 01020125=id/KEYCODE_NUMPAD_8 01020126=id/KEYCODE_NUMPAD_9 01020127=id/KEYCODE_NUMPAD_ADD 01020128=id/KEYCODE_NUMPAD_COMMA 01020129=id/KEYCODE_NUMPAD_DIVIDE 0102012a=id/KEYCODE_NUMPAD_DOT 0102012b=id/KEYCODE_NUMPAD_ENTER 0102012c=id/KEYCODE_NUMPAD_EQUALS 0102012d=id/KEYCODE_NUMPAD_LEFT_PAREN 0102012e=id/KEYCODE_NUMPAD_MULTIPLY 0102012f=id/KEYCODE_NUMPAD_RIGHT_PAREN 01020130=id/KEYCODE_NUMPAD_SUBTRACT 01020131=id/KEYCODE_NUM_LOCK 01020132=id/KEYCODE_O 01020133=id/KEYCODE_P 01020134=id/KEYCODE_PAGE_DOWN 01020135=id/KEYCODE_PAGE_UP 01020136=id/KEYCODE_PAIRING 01020137=id/KEYCODE_PASTE 01020138=id/KEYCODE_PERIOD 01020139=id/KEYCODE_PICTSYMBOLS 0102013a=id/KEYCODE_PLUS 0102013b=id/KEYCODE_POUND 0102013c=id/KEYCODE_POWER 0102013d=id/KEYCODE_PROFILE_SWITCH 0102013e=id/KEYCODE_PROG_BLUE 0102013f=id/KEYCODE_PROG_GRED 01020140=id/KEYCODE_PROG_GREEN 01020141=id/KEYCODE_PROG_YELLOW 01020142=id/KEYCODE_Q 01020143=id/KEYCODE_R 01020144=id/KEYCODE_RECENT_APPS 01020145=id/KEYCODE_REFRESH 01020146=id/KEYCODE_RIGHT_BRACKET 01020147=id/KEYCODE_RO 01020148=id/KEYCODE_S 01020149=id/KEYCODE_SCREENSHOT 0102014a=id/KEYCODE_SCROLL_LOCK 0102014b=id/KEYCODE_SEARCH 0102014c=id/KEYCODE_SEMICOLON 0102014d=id/KEYCODE_SETTINGS 0102014e=id/KEYCODE_SHIFT_LEFT 0102014f=id/KEYCODE_SHIFT_RIGHT 01020150=id/KEYCODE_SLASH 01020151=id/KEYCODE_SOFT_LEFT 01020152=id/KEYCODE_SOFT_RIGHT 01020153=id/KEYCODE_SOFT_SLEEP 01020154=id/KEYCODE_SPACE 01020155=id/KEYCODE_STAR 01020156=id/KEYCODE_STB_INPUT 01020157=id/KEYCODE_STB_POWER 01020158=id/KEYCODE_STEM_1 01020159=id/KEYCODE_STEM_2 0102015a=id/KEYCODE_STEM_3 0102015b=id/KEYCODE_STEM_PRIMARY 0102015c=id/KEYCODE_STYLUS_BUTTON_PRIMARY 0102015d=id/KEYCODE_STYLUS_BUTTON_SECONDARY 0102015e=id/KEYCODE_STYLUS_BUTTON_TAIL 0102015f=id/KEYCODE_STYLUS_BUTTON_TERTIARY 01020160=id/KEYCODE_SWITCH_CHARSET 01020161=id/KEYCODE_SYM 01020162=id/KEYCODE_SYSRQ 01020163=id/KEYCODE_SYSTEM_NAVIGATION_DOWN 01020164=id/KEYCODE_SYSTEM_NAVIGATION_LEFT 01020165=id/KEYCODE_SYSTEM_NAVIGATION_RIGHT 01020166=id/KEYCODE_SYSTEM_NAVIGATION_UP 01020167=id/KEYCODE_T 01020168=id/KEYCODE_TAB 01020169=id/KEYCODE_THUMBS_DOWN 0102016a=id/KEYCODE_THUMBS_UP 0102016b=id/KEYCODE_TV 0102016c=id/KEYCODE_TV_ANTENNA_CABLE 0102016d=id/KEYCODE_TV_AUDIO_DESCRIPTION 0102016e=id/KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN 0102016f=id/KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP 01020170=id/KEYCODE_TV_CONTENTS_MENU 01020171=id/KEYCODE_TV_DATA_SERVICE 01020172=id/KEYCODE_TV_INPUT 01020173=id/KEYCODE_TV_INPUT_COMPONENT_1 01020174=id/KEYCODE_TV_INPUT_COMPONENT_2 01020175=id/KEYCODE_TV_INPUT_COMPOSITE_1 01020176=id/KEYCODE_TV_INPUT_COMPOSITE_2 01020177=id/KEYCODE_TV_INPUT_HDMI_1 01020178=id/KEYCODE_TV_INPUT_HDMI_2 01020179=id/KEYCODE_TV_INPUT_HDMI_3 0102017a=id/KEYCODE_TV_INPUT_HDMI_4 0102017b=id/KEYCODE_TV_INPUT_VGA_1 0102017c=id/KEYCODE_TV_MEDIA_CONTEXT_MENU 0102017d=id/KEYCODE_TV_NETWORK 0102017e=id/KEYCODE_TV_NUMBER_ENTRY 0102017f=id/KEYCODE_TV_POWER 01020180=id/KEYCODE_TV_RADIO_SERVICE 01020181=id/KEYCODE_TV_SATELLITE 01020182=id/KEYCODE_TV_SATELLITE_BS 01020183=id/KEYCODE_TV_SATELLITE_CS 01020184=id/KEYCODE_TV_SATELLITE_SERVICE 01020185=id/KEYCODE_TV_TELETEXT 01020186=id/KEYCODE_TV_TERRESTRIAL_ANALOG 01020187=id/KEYCODE_TV_TERRESTRIAL_DIGITAL 01020188=id/KEYCODE_TV_TIMER_PROGRAMMING 01020189=id/KEYCODE_TV_ZOOM_MODE 0102018a=id/KEYCODE_U 0102018b=id/KEYCODE_UNKNOWN 0102018c=id/KEYCODE_V 0102018d=id/KEYCODE_VIDEO_APP_1 0102018e=id/KEYCODE_VIDEO_APP_2 0102018f=id/KEYCODE_VIDEO_APP_3 01020190=id/KEYCODE_VIDEO_APP_4 01020191=id/KEYCODE_VIDEO_APP_5 01020192=id/KEYCODE_VIDEO_APP_6 01020193=id/KEYCODE_VIDEO_APP_7 01020194=id/KEYCODE_VIDEO_APP_8 01020195=id/KEYCODE_VOICE_ASSIST 01020196=id/KEYCODE_VOLUME_DOWN 01020197=id/KEYCODE_VOLUME_MUTE 01020198=id/KEYCODE_VOLUME_UP 01020199=id/KEYCODE_W 0102019a=id/KEYCODE_WINDOW 0102019b=id/KEYCODE_X 0102019c=id/KEYCODE_Y 0102019d=id/KEYCODE_YEN 0102019e=id/KEYCODE_Z 0102019f=id/KEYCODE_ZENKAKU_HANKAKU 010201a0=id/KEYCODE_ZOOM_IN 010201a1=id/KEYCODE_ZOOM_OUT 010201a2=id/META 010201a3=id/NUM_LOCK 010201a4=id/SCROLL_LOCK 010201a5=id/SHIFT 010201a6=id/SYM 010201a7=id/aboveThumb 010201a8=id/accessibility 010201a9=id/accessibilityActionClickOnClickableSpan 010201aa=id/accessibility_autoclick_button_group_container 010201ab=id/accessibility_autoclick_double_click_button 010201ac=id/accessibility_autoclick_double_click_layout 010201ad=id/accessibility_autoclick_drag_button 010201ae=id/accessibility_autoclick_drag_layout 010201af=id/accessibility_autoclick_left_click_button 010201b0=id/accessibility_autoclick_left_click_layout 010201b1=id/accessibility_autoclick_pause_button 010201b2=id/accessibility_autoclick_pause_layout 010201b3=id/accessibility_autoclick_position_button 010201b4=id/accessibility_autoclick_position_layout 010201b5=id/accessibility_autoclick_right_click_button 010201b6=id/accessibility_autoclick_right_click_layout 010201b7=id/accessibility_autoclick_scroll_button 010201b8=id/accessibility_autoclick_scroll_layout 010201b9=id/accessibility_autoclick_scroll_panel 010201ba=id/accessibility_autoclick_type_panel 010201bb=id/accessibility_button_chooser_grid 010201bc=id/accessibility_button_prompt 010201bd=id/accessibility_button_prompt_prologue 010201be=id/accessibility_button_target_icon 010201bf=id/accessibility_button_target_label 010201c0=id/accessibility_controlScreen_description 010201c1=id/accessibility_controlScreen_icon 010201c2=id/accessibility_controlScreen_title 010201c3=id/accessibility_magnification_thumbnail_view 010201c4=id/accessibility_performAction_description 010201c5=id/accessibility_performAction_icon 010201c6=id/accessibility_performAction_title 010201c7=id/accessibility_permissionDialog_description 010201c8=id/accessibility_permissionDialog_icon 010201c9=id/accessibility_permissionDialog_title 010201ca=id/accessibility_permission_enable_allow_button 010201cb=id/accessibility_permission_enable_deny_button 010201cc=id/accessibility_permission_enable_uninstall_button 010201cd=id/accessibility_shortcut_target_checkbox 010201ce=id/accessibility_shortcut_target_icon 010201cf=id/accessibility_shortcut_target_label 010201d0=id/accessibility_shortcut_target_status 010201d1=id/accountPreferences 010201d2=id/account_name 010201d3=id/account_row_icon 010201d4=id/account_row_text 010201d5=id/account_type 010201d6=id/action0 010201d7=id/action1 010201d8=id/action2 010201d9=id/action3 010201da=id/action4 010201db=id/actionDone 010201dc=id/actionGo 010201dd=id/actionNext 010201de=id/actionNone 010201df=id/actionPrevious 010201e0=id/actionSearch 010201e1=id/actionSend 010201e2=id/actionUnspecified 010201e3=id/action_bar 010201e4=id/action_bar_container 010201e5=id/action_bar_spinner 010201e6=id/action_bar_subtitle 010201e7=id/action_bar_title 010201e8=id/action_context_bar 010201e9=id/action_divider 010201ea=id/action_menu_divider 010201eb=id/action_menu_presenter 010201ec=id/action_mode_bar 010201ed=id/action_mode_bar_stub 010201ee=id/action_mode_close_button 010201ef=id/actions 010201f0=id/actions_container 010201f1=id/actions_container_layout 010201f2=id/activity_chooser_view_content 010201f3=id/add 010201f4=id/addToDictionaryButton 010201f5=id/adjustNothing 010201f6=id/adjustPan 010201f7=id/adjustResize 010201f8=id/adjustUnspecified 010201f9=id/aerr_app_info 010201fa=id/aerr_close 010201fb=id/aerr_mute 010201fc=id/aerr_report 010201fd=id/aerr_restart 010201fe=id/aerr_wait 010201ff=id/afterDescendants 01020200=id/alarm 01020201=id/alertTitle 01020202=id/alerted_icon 01020203=id/alias 01020204=id/alignBounds 01020205=id/alignMargins 01020206=id/all 01020207=id/all_scroll 01020208=id/allowNullAction 01020209=id/allow_button 0102020a=id/allowed 0102020b=id/alternate_expand_target 0102020c=id/alternative 0102020d=id/always 0102020e=id/alwaysScroll 0102020f=id/alwaysUse 01020210=id/amPm 01020211=id/am_label 01020212=id/am_pm_spinner 01020213=id/ampm_layout 01020214=id/animation 01020215=id/animator 01020216=id/anyRtl 01020217=id/appPredictor 01020218=id/app_name_divider 01020219=id/app_name_text 0102021a=id/app_ops 0102021b=id/appop 0102021c=id/arc 0102021d=id/arrow 0102021e=id/ask_checkbox 0102021f=id/assertive 01020220=id/assetsPaths 01020221=id/async 01020222=id/atThumb 01020223=id/audio 01020224=id/authtoken_type 01020225=id/auto 01020226=id/auto_fit 01020227=id/autofill_button_bar_spacer 01020228=id/autofill_dataset_footer 01020229=id/autofill_dataset_header 0102022a=id/autofill_dataset_icon 0102022b=id/autofill_dataset_list 0102022c=id/autofill_dataset_picker 0102022d=id/autofill_dataset_title 0102022e=id/autofill_dialog_container 0102022f=id/autofill_dialog_header 01020230=id/autofill_dialog_list 01020231=id/autofill_dialog_no 01020232=id/autofill_dialog_picker 01020233=id/autofill_dialog_yes 01020234=id/autofill_save 01020235=id/autofill_save_button_bar 01020236=id/autofill_save_custom_subtitle 01020237=id/autofill_save_icon 01020238=id/autofill_save_no 01020239=id/autofill_save_title 0102023a=id/autofill_save_yes 0102023b=id/autofill_service_icon 0102023c=id/autofill_sheet_divider 0102023d=id/autofill_sheet_scroll_view 0102023e=id/autofill_sheet_scroll_view_space 0102023f=id/azerty 01020240=id/back_button 01020241=id/balanced 01020242=id/beforeDescendants 01020243=id/beginning 01020244=id/behind 01020245=id/bevel 01020246=id/big_picture 01020247=id/big_text 01020248=id/blocksDescendants 01020249=id/body 0102024a=id/bool 0102024b=id/bottom 0102024c=id/bottom_to_top 0102024d=id/bounds 0102024e=id/breadcrumb_section 0102024f=id/bubble_button 01020250=id/bundle 01020251=id/bundle_array 01020252=id/butt 01020253=id/button0 01020254=id/button4 01020255=id/button5 01020256=id/button6 01020257=id/button7 01020258=id/buttonPanel 01020259=id/button_always 0102025a=id/button_bar 0102025b=id/button_bar_container 0102025c=id/button_once 0102025d=id/button_open 0102025e=id/buttons 0102025f=id/by_common 01020260=id/by_common_header 01020261=id/by_org 01020262=id/by_org_header 01020263=id/by_org_unit 01020264=id/by_org_unit_header 01020265=id/calendar 01020266=id/calendar_view 01020267=id/camera 01020268=id/cancel 01020269=id/cell 0102026a=id/center 0102026b=id/centerCrop 0102026c=id/centerInside 0102026d=id/center_horizontal 0102026e=id/center_vertical 0102026f=id/challenge 01020270=id/characterPicker 01020271=id/characters 01020272=id/check 01020273=id/checked 01020274=id/choice 01020275=id/chooser_action_row 01020276=id/chooser_copy_button 01020277=id/chooser_edit_button 01020278=id/chooser_header 01020279=id/chooser_nearby_button 0102027a=id/chooser_row 0102027b=id/chooser_row_text_option 0102027c=id/chronometer 0102027d=id/clamp 0102027e=id/clearDefaultHint 0102027f=id/clipBounds 01020280=id/clip_children_set_tag 01020281=id/clip_children_tag 01020282=id/clip_horizontal 01020283=id/clip_to_padding_tag 01020284=id/clip_vertical 01020285=id/clock 01020286=id/close_button 01020287=id/close_button_pill_colorized_layer 01020288=id/colemak 01020289=id/collapseActionView 0102028a=id/collapsing 0102028b=id/colorMode 0102028c=id/colorType 0102028d=id/column 0102028e=id/columnWidth 0102028f=id/companion 01020290=id/compat_checkbox 01020291=id/configuration_optional 01020292=id/configurator 01020293=id/connectedDevice 01020294=id/container 01020295=id/contentPanel 01020296=id/content_preview_container 01020297=id/content_preview_file_area 01020298=id/content_preview_file_icon 01020299=id/content_preview_file_layout 0102029a=id/content_preview_file_thumbnail 0102029b=id/content_preview_filename 0102029c=id/content_preview_image_1_large 0102029d=id/content_preview_image_2_large 0102029e=id/content_preview_image_2_small 0102029f=id/content_preview_image_3_small 010202a0=id/content_preview_image_area 010202a1=id/content_preview_text 010202a2=id/content_preview_text_area 010202a3=id/content_preview_text_layout 010202a4=id/content_preview_thumbnail 010202a5=id/content_preview_title 010202a6=id/content_preview_title_layout 010202a7=id/context_menu 010202a8=id/controlScreen_description 010202a9=id/controlScreen_icon 010202aa=id/controlScreen_title 010202ab=id/conversation_face_pile 010202ac=id/conversation_face_pile_bottom 010202ad=id/conversation_face_pile_bottom_background 010202ae=id/conversation_face_pile_top 010202af=id/conversation_header 010202b0=id/conversation_icon 010202b1=id/conversation_icon_badge 010202b2=id/conversation_icon_badge_bg 010202b3=id/conversation_icon_badge_ring 010202b4=id/conversation_icon_container 010202b5=id/conversation_image_message_container 010202b6=id/conversation_text 010202b7=id/costsMoney 010202b8=id/cross_task_transition 010202b9=id/crossfade 010202ba=id/crosshair 010202bb=id/current_scene 010202bc=id/customPanel 010202bd=id/cycle 010202be=id/dangerous 010202bf=id/dataSync 010202c0=id/date 010202c1=id/datePicker 010202c2=id/date_picker_day_picker 010202c3=id/date_picker_header 010202c4=id/date_picker_header_date 010202c5=id/date_picker_header_year 010202c6=id/date_picker_year_picker 010202c7=id/datetime 010202c8=id/day 010202c9=id/day_names 010202ca=id/day_picker_view_pager 010202cb=id/decimal 010202cc=id/decor_content_parent 010202cd=id/decrement 010202ce=id/default 010202cf=id/defaultPosition 010202d0=id/default_activity_button 010202d1=id/default_loading_view 010202d2=id/deleteButton 010202d3=id/density 010202d4=id/deny_button 010202d5=id/description 010202d6=id/development 010202d7=id/dialog 010202d8=id/dialog_item 010202d9=id/disableHome 010202da=id/disabled 010202db=id/disallowed 010202dc=id/discouraged 010202dd=id/divider 010202de=id/dpad 010202df=id/drag 010202e0=id/dropdown 010202e1=id/dvorak 010202e2=id/edit_query 010202e3=id/editable 010202e4=id/edittext_container 010202e5=id/eight 010202e6=id/email 010202e7=id/enabled 010202e8=id/end 010202e9=id/enforceIntentFilter 010202ea=id/evenOdd 010202eb=id/exclude 010202ec=id/excludeDescendants 010202ed=id/expandChallengeHandle 010202ee=id/expand_activities_button 010202ef=id/expand_button 010202f0=id/expand_button_a11y_container 010202f1=id/expand_button_and_content_container 010202f2=id/expand_button_container 010202f3=id/expand_button_icon 010202f4=id/expand_button_number 010202f5=id/expand_button_pill 010202f6=id/expand_button_pill_colorized_layer 010202f7=id/expand_button_touch_container 010202f8=id/expanded_menu 010202f9=id/expires_on 010202fa=id/expires_on_header 010202fb=id/extended 010202fc=id/fade_in 010202fd=id/fade_in_out 010202fe=id/fade_out 010202ff=id/feedback 01020300=id/feedbackAllMask 01020301=id/feedbackAudible 01020302=id/feedbackGeneric 01020303=id/feedbackHaptic 01020304=id/feedbackSpoken 01020305=id/feedbackVisual 01020306=id/ffwd 01020307=id/fill 01020308=id/fillInIntent 01020309=id/fill_horizontal 0102030a=id/fill_parent 0102030b=id/fill_vertical 0102030c=id/find 0102030d=id/find_next 0102030e=id/find_prev 0102030f=id/finger 01020310=id/fingerprints 01020311=id/firstStrong 01020312=id/firstStrongLtr 01020313=id/firstStrongRtl 01020314=id/fitCenter 01020315=id/fitEnd 01020316=id/fitStart 01020317=id/fitXY 01020318=id/five 01020319=id/flagDefault 0102031a=id/flagEnableAccessibilityVolume 0102031b=id/flagForceAscii 0102031c=id/flagIncludeNotImportantViews 0102031d=id/flagInputMethodEditor 0102031e=id/flagNavigateNext 0102031f=id/flagNavigatePrevious 01020320=id/flagNoAccessoryAction 01020321=id/flagNoEnterAction 01020322=id/flagNoExtractUi 01020323=id/flagNoFullscreen 01020324=id/flagNoPersonalizedLearning 01020325=id/flagReportViewIds 01020326=id/flagRequestAccessibilityButton 01020327=id/flagRequestEnhancedWebAccessibility 01020328=id/flagRequestFilterKeyEvents 01020329=id/flagRequestFingerprintGestures 0102032a=id/flagRequestMultiFingerGestures 0102032b=id/flagRequestShortcutWarningDialogSpokenFeedback 0102032c=id/flagRequestTouchExplorationMode 0102032d=id/flagRetrieveInteractiveWindows 0102032e=id/flagSendMotionEvents 0102032f=id/flagServiceHandlesDoubleTap 01020330=id/floatType 01020331=id/floating 01020332=id/floating_popup_container 01020333=id/floating_toolbar_menu_item_image 01020334=id/floating_toolbar_menu_item_image_button 01020335=id/floating_toolbar_menu_item_text 01020336=id/fontScale 01020337=id/fontWeightAdjustment 01020338=id/four 01020339=id/frame 0102033a=id/full 0102033b=id/fullFast 0102033c=id/fullSensor 0102033d=id/fullUser 0102033e=id/fullscreenArea 0102033f=id/future 01020340=id/game 01020341=id/gone 01020342=id/grab 01020343=id/grabbing 01020344=id/grammaticalGender 01020345=id/grant_credentials_permission_message_footer 01020346=id/grant_credentials_permission_message_header 01020347=id/gravity 01020348=id/group_divider 01020349=id/group_message_container 0102034a=id/hand 0102034b=id/handwriting 0102034c=id/hardRestricted 0102034d=id/hard_keyboard_section 0102034e=id/hard_keyboard_switch 0102034f=id/hardware 01020350=id/hdpi 01020351=id/hdr 01020352=id/header_text 01020353=id/header_text_divider 01020354=id/header_text_secondary 01020355=id/header_text_secondary_divider 01020356=id/headers 01020357=id/health 01020358=id/help 01020359=id/hidden 0102035a=id/hide_from_picker 0102035b=id/high 0102035c=id/high_quality 0102035d=id/holo 0102035e=id/homeAsUp 0102035f=id/home_panel 01020360=id/home_screen 01020361=id/horizontal 01020362=id/horizontal_double_arrow 01020363=id/hour 01020364=id/hour_label_holder 01020365=id/hours 01020366=id/icon_badge 01020367=id/icon_menu 01020368=id/icon_menu_presenter 01020369=id/icon_preferred 0102036a=id/ifContentScrolls 0102036b=id/ifRoom 0102036c=id/if_whitelisted 0102036d=id/image 0102036e=id/imageView 0102036f=id/immersive_cling_description 01020370=id/immersive_cling_icon 01020371=id/immersive_cling_title 01020372=id/immutablyRestricted 01020373=id/inbox_text0 01020374=id/inbox_text1 01020375=id/inbox_text2 01020376=id/inbox_text3 01020377=id/inbox_text4 01020378=id/inbox_text5 01020379=id/inbox_text6 0102037a=id/incidentReportApprover 0102037b=id/include 0102037c=id/increment 0102037d=id/index 0102037e=id/indicator 0102037f=id/infinite 01020380=id/inherit 01020381=id/input_block 01020382=id/input_header 01020383=id/input_hour 01020384=id/input_method_nav_back 01020385=id/input_method_nav_buttons 01020386=id/input_method_nav_center_group 01020387=id/input_method_nav_ends_group 01020388=id/input_method_nav_home_handle 01020389=id/input_method_nav_horizontal 0102038a=id/input_method_nav_ime_switcher 0102038b=id/input_method_nav_inflater 0102038c=id/input_method_navigation_bar_view 0102038d=id/input_minute 0102038e=id/input_mode 0102038f=id/input_separator 01020390=id/insertion_handle 01020391=id/inside 01020392=id/insideInset 01020393=id/insideOverlay 01020394=id/installer 01020395=id/installerExemptIgnored 01020396=id/instant 01020397=id/intType 01020398=id/integer 01020399=id/inter_character 0102039a=id/inter_word 0102039b=id/internal 0102039c=id/internalEmpty 0102039d=id/internalOnly 0102039e=id/intoExisting 0102039f=id/invisible 010203a0=id/issued_on 010203a1=id/issued_on_header 010203a2=id/issued_to_header 010203a3=id/item 010203a4=id/item_touch_helper_previous_elevation 010203a5=id/jumpcut 010203a6=id/keyboard 010203a7=id/keyboardHidden 010203a8=id/keyguard 010203a9=id/keyguard_click_area 010203aa=id/keyguard_message_area 010203ab=id/knownSigner 010203ac=id/label 010203ad=id/label_error 010203ae=id/label_hour 010203af=id/label_minute 010203b0=id/landscape 010203b1=id/language_item 010203b2=id/language_picker_header 010203b3=id/language_picker_item 010203b4=id/large 010203b5=id/launchRecognizer 010203b6=id/launchWebSearch 010203b7=id/layoutDirection 010203b8=id/ldpi 010203b9=id/left 010203ba=id/leftPanel 010203bb=id/leftSpacer 010203bc=id/left_icon 010203bd=id/left_to_right 010203be=id/line 010203bf=id/line1 010203c0=id/linear 010203c1=id/listContainer 010203c2=id/listMode 010203c3=id/list_footer 010203c4=id/list_item 010203c5=id/list_menu_presenter 010203c6=id/liveAudio 010203c7=id/locale 010203c8=id/locale_native 010203c9=id/locale_search_menu 010203ca=id/locale_secondary 010203cb=id/location 010203cc=id/lock_screen 010203cd=id/locked 010203ce=id/loose 010203cf=id/low 010203d0=id/low_light 010203d1=id/ltr 010203d2=id/map 010203d3=id/maps 010203d4=id/marquee 010203d5=id/marquee_forever 010203d6=id/match_parent 010203d7=id/matches 010203d8=id/material 010203d9=id/matrix 010203da=id/mcc 010203db=id/mdpi 010203dc=id/mediaPlayback 010203dd=id/mediaProcessing 010203de=id/mediaProjection 010203df=id/media_actions 010203e0=id/media_route_extended_settings_button 010203e1=id/media_route_list 010203e2=id/media_route_progress_bar 010203e3=id/media_route_volume_layout 010203e4=id/media_route_volume_slider 010203e5=id/mediacontroller_progress 010203e6=id/medium 010203e7=id/menu 010203e8=id/message_icon 010203e9=id/message_icon_container 010203ea=id/message_name 010203eb=id/message_text 010203ec=id/messaging_group_content_container 010203ed=id/messaging_group_icon_container 010203ee=id/messaging_group_sending_progress 010203ef=id/messaging_group_sending_progress_container 010203f0=id/mic 010203f1=id/micro 010203f2=id/microphone 010203f3=id/middle 010203f4=id/midpoint 010203f5=id/miniresolver_info_section 010203f6=id/miniresolver_info_section_icon 010203f7=id/miniresolver_info_section_text 010203f8=id/minute 010203f9=id/minutes 010203fa=id/mirror 010203fb=id/miter 010203fc=id/mnc 010203fd=id/modeLarge 010203fe=id/modeMedium 010203ff=id/modeSmall 01020400=id/mode_in 01020401=id/mode_normal 01020402=id/mode_out 01020403=id/module 01020404=id/monospace 01020405=id/month 01020406=id/month_name 01020407=id/month_view 01020408=id/multi-select 01020409=id/multiple 0102040a=id/multipleChoice 0102040b=id/multipleChoiceModal 0102040c=id/multiply 0102040d=id/music 0102040e=id/navigation 0102040f=id/nest 01020410=id/never 01020411=id/neverForLocation 01020412=id/new_app_action 01020413=id/new_app_description 01020414=id/new_app_icon 01020415=id/news 01020416=id/next 01020417=id/next_button 01020418=id/nine 01020419=id/no 0102041a=id/noExcludeDescendants 0102041b=id/noHideDescendants 0102041c=id/no_applications_message 0102041d=id/no_drop 0102041e=id/no_permissions 0102041f=id/nokeys 01020420=id/nonZero 01020421=id/nonav 01020422=id/none 01020423=id/normal 01020424=id/normalFast 01020425=id/nosensor 01020426=id/notSensitive 01020427=id/not_keyguard 01020428=id/notification 01020429=id/notification_action_index_tag 0102042a=id/notification_action_list_margin_target 0102042b=id/notification_custom_view_index_tag 0102042c=id/notification_header 0102042d=id/notification_headerless_view_column 0102042e=id/notification_headerless_view_row 0102042f=id/notification_main_column 01020430=id/notification_material_reply_container 01020431=id/notification_material_reply_progress 01020432=id/notification_material_reply_text_1 01020433=id/notification_material_reply_text_1_container 01020434=id/notification_material_reply_text_2 01020435=id/notification_material_reply_text_3 01020436=id/notification_media_content 01020437=id/notification_messaging 01020438=id/notification_progress_end_icon 01020439=id/notification_progress_start_icon 0102043a=id/notification_top_line 0102043b=id/notouch 0102043c=id/number 0102043d=id/numberDecimal 0102043e=id/numberPassword 0102043f=id/numberSigned 01020440=id/numberpicker_input 01020441=id/oem 01020442=id/off 01020443=id/ok 01020444=id/old_app_action 01020445=id/old_app_icon 01020446=id/on 01020447=id/one 01020448=id/oneLine 01020449=id/opaque 0102044a=id/open_cross_profile 0102044b=id/opticalBounds 0102044c=id/option1 0102044d=id/option2 0102044e=id/option3 0102044f=id/orientation 01020450=id/original_app_icon 01020451=id/original_message 01020452=id/outsideInset 01020453=id/outsideOverlay 01020454=id/oval 01020455=id/overflow 01020456=id/overflow_menu_presenter 01020457=id/overlay 01020458=id/overlay_display_window_texture 01020459=id/overlay_display_window_title 0102045a=id/package_icon 0102045b=id/package_label 0102045c=id/packages_list 0102045d=id/paddedBounds 0102045e=id/pageDeleteDropTarget 0102045f=id/parentMatrix 01020460=id/parentPanel 01020461=id/past 01020462=id/pathType 01020463=id/pause 01020464=id/pending_intent_tag 01020465=id/performAction_description 01020466=id/performAction_icon 01020467=id/performAction_title 01020468=id/perm_icon 01020469=id/perm_money_icon 0102046a=id/perm_money_label 0102046b=id/perm_name 0102046c=id/permissionDialog_description 0102046d=id/permission_group 0102046e=id/permission_icon 0102046f=id/permission_list 01020470=id/perms_list 01020471=id/persistAcrossReboots 01020472=id/persistNever 01020473=id/persistRootOnly 01020474=id/personalInfo 01020475=id/phishing_alert 01020476=id/phone 01020477=id/phoneCall 01020478=id/phrase 01020479=id/pickers 0102047a=id/pin_cancel_button 0102047b=id/pin_confirm_text 0102047c=id/pin_error_message 0102047d=id/pin_message 0102047e=id/pin_new_text 0102047f=id/pin_ok_button 01020480=id/pin_text 01020481=id/placeholder 01020482=id/pm_label 01020483=id/polite 01020484=id/popup_submenu_presenter 01020485=id/portrait 01020486=id/pre23 01020487=id/preferExternal 01020488=id/prefs 01020489=id/prefs_container 0102048a=id/prefs_frame 0102048b=id/preinstalled 0102048c=id/pressed 0102048d=id/prev 0102048e=id/privileged 0102048f=id/productivity 01020490=id/profile_badge 01020491=id/profile_button 01020492=id/profile_pager 01020493=id/profile_tabhost 01020494=id/progressContainer 01020495=id/progress_circular 01020496=id/progress_dialog_message 01020497=id/progress_horizontal 01020498=id/progress_number 01020499=id/progress_percent 0102049a=id/queryRewriteFromData 0102049b=id/queryRewriteFromText 0102049c=id/qwerty 0102049d=id/qwertz 0102049e=id/radial 0102049f=id/radial_picker 010204a0=id/radio 010204a1=id/radio_power 010204a2=id/random 010204a3=id/read 010204a4=id/readAndWrite 010204a5=id/readOrWrite 010204a6=id/reask_hint 010204a7=id/recents 010204a8=id/reconfigurable 010204a9=id/rectangle 010204aa=id/remoteMessaging 010204ab=id/remoteViewsMetricsId 010204ac=id/remote_checked_change_listener_tag 010204ad=id/remote_input 010204ae=id/remote_input_progress 010204af=id/remote_input_send 010204b0=id/remote_input_tag 010204b1=id/remote_input_text 010204b2=id/remote_views_next_child 010204b3=id/remote_views_override_id 010204b4=id/remote_views_stable_id 010204b5=id/removed 010204b6=id/repeat 010204b7=id/replace_app_icon 010204b8=id/replace_message 010204b9=id/reply_action_container 010204ba=id/resolver_button_bar_divider 010204bb=id/resolver_empty_state 010204bc=id/resolver_empty_state_button 010204bd=id/resolver_empty_state_container 010204be=id/resolver_empty_state_icon 010204bf=id/resolver_empty_state_progress 010204c0=id/resolver_empty_state_subtitle 010204c1=id/resolver_empty_state_title 010204c2=id/resolver_list 010204c3=id/resolver_tab_divider 010204c4=id/resourcesUnused 010204c5=id/restart 010204c6=id/restore 010204c7=id/retailDemo 010204c8=id/retain 010204c9=id/reverse 010204ca=id/reverseLandscape 010204cb=id/reversePortrait 010204cc=id/rew 010204cd=id/right 010204ce=id/rightSpacer 010204cf=id/right_container 010204d0=id/right_icon 010204d1=id/right_to_left 010204d2=id/ring 010204d3=id/ringtone 010204d4=id/role 010204d5=id/rotate 010204d6=id/round 010204d7=id/row 010204d8=id/rowTypeId 010204d9=id/rtl 010204da=id/runtime 010204db=id/sans 010204dc=id/scene_layoutid_cache 010204dd=id/screen 010204de=id/screenLayout 010204df=id/screenSize 010204e0=id/scrim 010204e1=id/scrollView 010204e2=id/scroll_down 010204e3=id/scroll_down_layout 010204e4=id/scroll_exit 010204e5=id/scroll_exit_layout 010204e6=id/scroll_left 010204e7=id/scroll_left_layout 010204e8=id/scroll_right 010204e9=id/scroll_right_layout 010204ea=id/scroll_up 010204eb=id/scroll_up_layout 010204ec=id/scrolling 010204ed=id/seamless 010204ee=id/search_app_icon 010204ef=id/search_badge 010204f0=id/search_bar 010204f1=id/search_button 010204f2=id/search_close_btn 010204f3=id/search_edit_frame 010204f4=id/search_go_btn 010204f5=id/search_mag_icon 010204f6=id/search_plate 010204f7=id/search_src_text 010204f8=id/search_view 010204f9=id/search_voice_btn 010204fa=id/searchbox 010204fb=id/secondary 010204fc=id/seekbar 010204fd=id/select_all 010204fe=id/select_dialog_listview 010204ff=id/selection_end_handle 01020500=id/selection_start_handle 01020501=id/sensitive 01020502=id/sensor 01020503=id/sensorLandscape 01020504=id/sensorPortrait 01020505=id/sentences 01020506=id/separator 01020507=id/sequential 01020508=id/sequentially 01020509=id/serial_number 0102050a=id/serial_number_header 0102050b=id/serif 0102050c=id/setup 0102050d=id/seven 0102050e=id/sha1_fingerprint 0102050f=id/sha1_fingerprint_header 01020510=id/sha256_fingerprint 01020511=id/sha256_fingerprint_header 01020512=id/share 01020513=id/shortEdges 01020514=id/shortService 01020515=id/shortcut 01020516=id/shortcuts_container 01020517=id/shortest 01020518=id/showCustom 01020519=id/showHome 0102051a=id/showSearchIconAsBadge 0102051b=id/showSearchLabelAsBadge 0102051c=id/showTitle 0102051d=id/showVoiceSearchButton 0102051e=id/signature 0102051f=id/signatureOrSystem 01020520=id/signed 01020521=id/silent 01020522=id/simple 01020523=id/single 01020524=id/singleChoice 01020525=id/singleInstance 01020526=id/singleInstancePerTask 01020527=id/singleTask 01020528=id/singleTop 01020529=id/six 0102052a=id/skip_button 0102052b=id/small 0102052c=id/smallIcon 0102052d=id/smallestScreenSize 0102052e=id/smart_reply_container 0102052f=id/sms_short_code_coins_icon 01020530=id/sms_short_code_confirm_message 01020531=id/sms_short_code_detail_layout 01020532=id/sms_short_code_detail_message 01020533=id/sms_short_code_remember_choice_checkbox 01020534=id/sms_short_code_remember_choice_text 01020535=id/sms_short_code_remember_undo_instruction 01020536=id/snooze_button 01020537=id/social 01020538=id/softRestricted 01020539=id/software 0102053a=id/spacer 0102053b=id/spacingWidth 0102053c=id/spacingWidthUniform 0102053d=id/spannable 0102053e=id/specialUse 0102053f=id/spinner 01020540=id/splashscreen 01020541=id/splashscreen_branding_view 01020542=id/splashscreen_icon_view 01020543=id/splitActionBarWhenNarrow 01020544=id/split_action_bar 01020545=id/square 01020546=id/src_atop 01020547=id/src_in 01020548=id/src_over 01020549=id/stack 0102054a=id/standard 0102054b=id/start 0102054c=id/stateAlwaysHidden 0102054d=id/stateAlwaysVisible 0102054e=id/stateHidden 0102054f=id/stateUnchanged 01020550=id/stateUnspecified 01020551=id/stateVisible 01020552=id/status 01020553=id/status_bar_latest_event_content 01020554=id/strict 01020555=id/string 01020556=id/stub 01020557=id/stylus 01020558=id/sub_color 01020559=id/sub_name 0102055a=id/sub_number 0102055b=id/sub_short_number 0102055c=id/submenuarrow 0102055d=id/submit_area 0102055e=id/suggestionContainer 0102055f=id/suggestionWindowContainer 01020560=id/surfaceView 01020561=id/sweep 01020562=id/switch_new 01020563=id/switch_old 01020564=id/sync 01020565=id/system 01020566=id/systemExempted 01020567=id/system_language_view 01020568=id/system_locale_subtitle 01020569=id/tabMode 0102056a=id/tabs_container 0102056b=id/tag_alpha_animator 0102056c=id/tag_is_first_layout 0102056d=id/tag_keep_when_showing_left_icon 0102056e=id/tag_layout_top 0102056f=id/tag_margin_end_when_icon_gone 01020570=id/tag_margin_end_when_icon_visible 01020571=id/tag_top_animator 01020572=id/tag_top_override 01020573=id/tag_uses_right_icon_drawable 01020574=id/text 01020575=id/textAutoComplete 01020576=id/textAutoCorrect 01020577=id/textCapCharacters 01020578=id/textCapSentences 01020579=id/textCapWords 0102057a=id/textClassifier 0102057b=id/textEmailAddress 0102057c=id/textEmailSubject 0102057d=id/textEnableTextConversionSuggestions 0102057e=id/textEnd 0102057f=id/textFilter 01020580=id/textImeMultiLine 01020581=id/textLongMessage 01020582=id/textMultiLine 01020583=id/textNoSuggestions 01020584=id/textPassword 01020585=id/textPersonName 01020586=id/textPhonetic 01020587=id/textPostalAddress 01020588=id/textShortMessage 01020589=id/textSpacerNoButtons 0102058a=id/textSpacerNoTitle 0102058b=id/textStart 0102058c=id/textUri 0102058d=id/textVisiblePassword 0102058e=id/textWebEditText 0102058f=id/textWebEmailAddress 01020590=id/textWebPassword 01020591=id/text_layout 01020592=id/textureView 01020593=id/three 01020594=id/time 01020595=id/timePicker 01020596=id/timePickerLayout 01020597=id/time_current 01020598=id/time_divider 01020599=id/time_header 0102059a=id/time_layout 0102059b=id/titleDivider 0102059c=id/titleDividerNoCustom 0102059d=id/titleDividerTop 0102059e=id/title_container 0102059f=id/title_separator 010205a0=id/title_template 010205a1=id/to_common 010205a2=id/to_common_header 010205a3=id/to_org 010205a4=id/to_org_header 010205a5=id/to_org_unit 010205a6=id/to_org_unit_header 010205a7=id/together 010205a8=id/toggle_mode 010205a9=id/top 010205aa=id/topPanel 010205ab=id/top_label 010205ac=id/top_left_diagonal_double_arrow 010205ad=id/top_right_diagonal_double_arrow 010205ae=id/top_to_bottom 010205af=id/touchscreen 010205b0=id/trackball 010205b1=id/transitionPosition 010205b2=id/transitionTransform 010205b3=id/transition_overlay_view_tag 010205b4=id/translucent 010205b5=id/transparent 010205b6=id/turkish_f 010205b7=id/turkish_q 010205b8=id/turn_off_screen 010205b9=id/twelvekey 010205ba=id/two 010205bb=id/twoLine 010205bc=id/typeAllMask 010205bd=id/typeAnnouncement 010205be=id/typeAssistReadingContext 010205bf=id/typeContextClicked 010205c0=id/typeGestureDetectionEnd 010205c1=id/typeGestureDetectionStart 010205c2=id/typeNotificationStateChanged 010205c3=id/typeTouchExplorationGestureEnd 010205c4=id/typeTouchExplorationGestureStart 010205c5=id/typeTouchInteractionEnd 010205c6=id/typeTouchInteractionStart 010205c7=id/typeViewAccessibilityFocusCleared 010205c8=id/typeViewAccessibilityFocused 010205c9=id/typeViewClicked 010205ca=id/typeViewFocused 010205cb=id/typeViewHoverEnter 010205cc=id/typeViewHoverExit 010205cd=id/typeViewLongClicked 010205ce=id/typeViewScrolled 010205cf=id/typeViewSelected 010205d0=id/typeViewTextChanged 010205d1=id/typeViewTextSelectionChanged 010205d2=id/typeViewTextTraversedAtMovementGranularity 010205d3=id/typeWindowContentChanged 010205d4=id/typeWindowStateChanged 010205d5=id/typeWindowsChanged 010205d6=id/uiMode 010205d7=id/unchecked 010205d8=id/undefined 010205d9=id/uniform 010205da=id/unpressed 010205db=id/unspecified 010205dc=id/up 010205dd=id/useLogo 010205de=id/use_same_profile_browser 010205df=id/user 010205e0=id/userIdentification 010205e1=id/userLandscape 010205e2=id/userPortrait 010205e3=id/userSwitcher 010205e4=id/validity_header 010205e5=id/value 010205e6=id/vendorPrivileged 010205e7=id/verification_divider 010205e8=id/verification_icon 010205e9=id/verification_text 010205ea=id/verifier 010205eb=id/vertical 010205ec=id/vertical_double_arrow 010205ed=id/vertical_text 010205ee=id/video 010205ef=id/viewEnd 010205f0=id/viewStart 010205f1=id/visible 010205f2=id/voice 010205f3=id/voiceTrigger 010205f4=id/wait 010205f5=id/web 010205f6=id/websearch 010205f7=id/webview 010205f8=id/wheel 010205f9=id/wideColorGamut 010205fa=id/widget 010205fb=id/widgets 010205fc=id/wipe 010205fd=id/withText 010205fe=id/words 010205ff=id/work_widget_app_icon 01020600=id/work_widget_badge_icon 01020601=id/workman 01020602=id/wrap_content 01020603=id/write 01020604=id/xhdpi 01020605=id/xlarge 01020606=id/xxhdpi 01020607=id/xxxhdpi 01020608=id/year 01020609=id/yes 0102060a=id/yesExcludeDescendants 0102060b=id/zero 0102060c=id/zoomControls 0102060d=id/zoomIn 0102060e=id/zoomMagnify 0102060f=id/zoomOut 01020610=id/zoom_fit_page 01020611=id/zoom_in 01020612=id/zoom_out 01020613=id/zoom_page_overview 01030000=style/Animation 01030001=style/Animation.Activity 01030002=style/Animation.Dialog 01030003=style/Animation.Translucent 01030004=style/Animation.Toast 01030005=style/Theme 01030006=style/Theme.NoTitleBar 01030007=style/Theme.NoTitleBar.Fullscreen 01030008=style/Theme.Black 01030009=style/Theme.Black.NoTitleBar 0103000a=style/Theme.Black.NoTitleBar.Fullscreen 0103000b=style/Theme.Dialog 0103000c=style/Theme.Light 0103000d=style/Theme.Light.NoTitleBar 0103000e=style/Theme.Light.NoTitleBar.Fullscreen 0103000f=style/Theme.Translucent 01030010=style/Theme.Translucent.NoTitleBar 01030011=style/Theme.Translucent.NoTitleBar.Fullscreen 01030012=style/Widget 01030013=style/Widget.AbsListView 01030014=style/Widget.Button 01030015=style/Widget.Button.Inset 01030016=style/Widget.Button.Small 01030017=style/Widget.Button.Toggle 01030018=style/Widget.CompoundButton 01030019=style/Widget.CompoundButton.CheckBox 0103001a=style/Widget.CompoundButton.RadioButton 0103001b=style/Widget.CompoundButton.Star 0103001c=style/Widget.ProgressBar 0103001d=style/Widget.ProgressBar.Large 0103001e=style/Widget.ProgressBar.Small 0103001f=style/Widget.ProgressBar.Horizontal 01030020=style/Widget.SeekBar 01030021=style/Widget.RatingBar 01030022=style/Widget.TextView 01030023=style/Widget.EditText 01030024=style/Widget.ExpandableListView 01030025=style/Widget.ImageWell 01030026=style/Widget.ImageButton 01030027=style/Widget.AutoCompleteTextView 01030028=style/Widget.Spinner 01030029=style/Widget.TextView.PopupMenu 0103002a=style/Widget.TextView.SpinnerItem 0103002b=style/Widget.DropDownItem 0103002c=style/Widget.DropDownItem.Spinner 0103002d=style/Widget.ScrollView 0103002e=style/Widget.ListView 0103002f=style/Widget.ListView.White 01030030=style/Widget.ListView.DropDown 01030031=style/Widget.ListView.Menu 01030032=style/Widget.GridView 01030033=style/Widget.WebView 01030034=style/Widget.TabWidget 01030035=style/Widget.Gallery 01030036=style/Widget.PopupWindow 01030037=style/MediaButton 01030038=style/MediaButton.Previous 01030039=style/MediaButton.Next 0103003a=style/MediaButton.Play 0103003b=style/MediaButton.Ffwd 0103003c=style/MediaButton.Rew 0103003d=style/MediaButton.Pause 0103003e=style/TextAppearance 0103003f=style/TextAppearance.Inverse 01030040=style/TextAppearance.Theme 01030041=style/TextAppearance.DialogWindowTitle 01030042=style/TextAppearance.Large 01030043=style/TextAppearance.Large.Inverse 01030044=style/TextAppearance.Medium 01030045=style/TextAppearance.Medium.Inverse 01030046=style/TextAppearance.Small 01030047=style/TextAppearance.Small.Inverse 01030048=style/TextAppearance.Theme.Dialog 01030049=style/TextAppearance.Widget 0103004a=style/TextAppearance.Widget.Button 0103004b=style/TextAppearance.Widget.IconMenu.Item 0103004c=style/TextAppearance.Widget.EditText 0103004d=style/TextAppearance.Widget.TabWidget 0103004e=style/TextAppearance.Widget.TextView 0103004f=style/TextAppearance.Widget.TextView.PopupMenu 01030050=style/TextAppearance.Widget.DropDownHint 01030051=style/TextAppearance.Widget.DropDownItem 01030052=style/TextAppearance.Widget.TextView.SpinnerItem 01030053=style/TextAppearance.WindowTitle 01030054=style/Theme.InputMethod 01030055=style/Theme.NoDisplay 01030056=style/Animation.InputMethod 01030057=style/Widget.KeyboardView 01030058=style/ButtonBar 01030059=style/Theme.Panel 0103005a=style/Theme.Light.Panel 0103005b=style/Widget.ProgressBar.Inverse 0103005c=style/Widget.ProgressBar.Large.Inverse 0103005d=style/Widget.ProgressBar.Small.Inverse 0103005e=style/Theme.Wallpaper 0103005f=style/Theme.Wallpaper.NoTitleBar 01030060=style/Theme.Wallpaper.NoTitleBar.Fullscreen 01030061=style/Theme.WallpaperSettings 01030062=style/Theme.Light.WallpaperSettings 01030063=style/TextAppearance.SearchResult.Title 01030064=style/TextAppearance.SearchResult.Subtitle 01030065=style/TextAppearance.StatusBar.Title 01030066=style/TextAppearance.StatusBar.Icon 01030067=style/TextAppearance.StatusBar.EventContent 01030068=style/TextAppearance.StatusBar.EventContent.Title 01030069=style/Theme.WithActionBar 0103006a=style/Theme.NoTitleBar.OverlayActionModes 0103006b=style/Theme.Holo 0103006c=style/Theme.Holo.NoActionBar 0103006d=style/Theme.Holo.NoActionBar.Fullscreen 0103006e=style/Theme.Holo.Light 0103006f=style/Theme.Holo.Dialog 01030070=style/Theme.Holo.Dialog.MinWidth 01030071=style/Theme.Holo.Dialog.NoActionBar 01030072=style/Theme.Holo.Dialog.NoActionBar.MinWidth 01030073=style/Theme.Holo.Light.Dialog 01030074=style/Theme.Holo.Light.Dialog.MinWidth 01030075=style/Theme.Holo.Light.Dialog.NoActionBar 01030076=style/Theme.Holo.Light.Dialog.NoActionBar.MinWidth 01030077=style/Theme.Holo.DialogWhenLarge 01030078=style/Theme.Holo.DialogWhenLarge.NoActionBar 01030079=style/Theme.Holo.Light.DialogWhenLarge 0103007a=style/Theme.Holo.Light.DialogWhenLarge.NoActionBar 0103007b=style/Theme.Holo.Panel 0103007c=style/Theme.Holo.Light.Panel 0103007d=style/Theme.Holo.Wallpaper 0103007e=style/Theme.Holo.Wallpaper.NoTitleBar 0103007f=style/Theme.Holo.InputMethod 01030080=style/TextAppearance.Widget.PopupMenu.Large 01030081=style/TextAppearance.Widget.PopupMenu.Small 01030082=style/Widget.ActionBar 01030083=style/Widget.Spinner.DropDown 01030084=style/Widget.ActionButton 01030085=style/Widget.ListPopupWindow 01030086=style/Widget.PopupMenu 01030087=style/Widget.ActionButton.Overflow 01030088=style/Widget.ActionButton.CloseMode 01030089=style/Widget.FragmentBreadCrumbs 0103008a=style/Widget.Holo 0103008b=style/Widget.Holo.Button 0103008c=style/Widget.Holo.Button.Small 0103008d=style/Widget.Holo.Button.Inset 0103008e=style/Widget.Holo.Button.Toggle 0103008f=style/Widget.Holo.TextView 01030090=style/Widget.Holo.AutoCompleteTextView 01030091=style/Widget.Holo.CompoundButton.CheckBox 01030092=style/Widget.Holo.ListView.DropDown 01030093=style/Widget.Holo.EditText 01030094=style/Widget.Holo.ExpandableListView 01030095=style/Widget.Holo.GridView 01030096=style/Widget.Holo.ImageButton 01030097=style/Widget.Holo.ListView 01030098=style/Widget.Holo.PopupWindow 01030099=style/Widget.Holo.ProgressBar 0103009a=style/Widget.Holo.ProgressBar.Horizontal 0103009b=style/Widget.Holo.ProgressBar.Small 0103009c=style/Widget.Holo.ProgressBar.Small.Title 0103009d=style/Widget.Holo.ProgressBar.Large 0103009e=style/Widget.Holo.SeekBar 0103009f=style/Widget.Holo.RatingBar 010300a0=style/Widget.Holo.RatingBar.Indicator 010300a1=style/Widget.Holo.RatingBar.Small 010300a2=style/Widget.Holo.CompoundButton.RadioButton 010300a3=style/Widget.Holo.ScrollView 010300a4=style/Widget.Holo.HorizontalScrollView 010300a5=style/Widget.Holo.Spinner 010300a6=style/Widget.Holo.CompoundButton.Star 010300a7=style/Widget.Holo.TabWidget 010300a8=style/Widget.Holo.WebTextView 010300a9=style/Widget.Holo.WebView 010300aa=style/Widget.Holo.DropDownItem 010300ab=style/Widget.Holo.DropDownItem.Spinner 010300ac=style/Widget.Holo.TextView.SpinnerItem 010300ad=style/Widget.Holo.ListPopupWindow 010300ae=style/Widget.Holo.PopupMenu 010300af=style/Widget.Holo.ActionButton 010300b0=style/Widget.Holo.ActionButton.Overflow 010300b1=style/Widget.Holo.ActionButton.TextButton 010300b2=style/Widget.Holo.ActionMode 010300b3=style/Widget.Holo.ActionButton.CloseMode 010300b4=style/Widget.Holo.ActionBar 010300b5=style/Widget.Holo.Light 010300b6=style/Widget.Holo.Light.Button 010300b7=style/Widget.Holo.Light.Button.Small 010300b8=style/Widget.Holo.Light.Button.Inset 010300b9=style/Widget.Holo.Light.Button.Toggle 010300ba=style/Widget.Holo.Light.TextView 010300bb=style/Widget.Holo.Light.AutoCompleteTextView 010300bc=style/Widget.Holo.Light.CompoundButton.CheckBox 010300bd=style/Widget.Holo.Light.ListView.DropDown 010300be=style/Widget.Holo.Light.EditText 010300bf=style/Widget.Holo.Light.ExpandableListView 010300c0=style/Widget.Holo.Light.GridView 010300c1=style/Widget.Holo.Light.ImageButton 010300c2=style/Widget.Holo.Light.ListView 010300c3=style/Widget.Holo.Light.PopupWindow 010300c4=style/Widget.Holo.Light.ProgressBar 010300c5=style/Widget.Holo.Light.ProgressBar.Horizontal 010300c6=style/Widget.Holo.Light.ProgressBar.Small 010300c7=style/Widget.Holo.Light.ProgressBar.Small.Title 010300c8=style/Widget.Holo.Light.ProgressBar.Large 010300c9=style/Widget.Holo.Light.ProgressBar.Inverse 010300ca=style/Widget.Holo.Light.ProgressBar.Small.Inverse 010300cb=style/Widget.Holo.Light.ProgressBar.Large.Inverse 010300cc=style/Widget.Holo.Light.SeekBar 010300cd=style/Widget.Holo.Light.RatingBar 010300ce=style/Widget.Holo.Light.RatingBar.Indicator 010300cf=style/Widget.Holo.Light.RatingBar.Small 010300d0=style/Widget.Holo.Light.CompoundButton.RadioButton 010300d1=style/Widget.Holo.Light.ScrollView 010300d2=style/Widget.Holo.Light.HorizontalScrollView 010300d3=style/Widget.Holo.Light.Spinner 010300d4=style/Widget.Holo.Light.CompoundButton.Star 010300d5=style/Widget.Holo.Light.TabWidget 010300d6=style/Widget.Holo.Light.WebTextView 010300d7=style/Widget.Holo.Light.WebView 010300d8=style/Widget.Holo.Light.DropDownItem 010300d9=style/Widget.Holo.Light.DropDownItem.Spinner 010300da=style/Widget.Holo.Light.TextView.SpinnerItem 010300db=style/Widget.Holo.Light.ListPopupWindow 010300dc=style/Widget.Holo.Light.PopupMenu 010300dd=style/Widget.Holo.Light.ActionButton 010300de=style/Widget.Holo.Light.ActionButton.Overflow 010300df=style/Widget.Holo.Light.ActionMode 010300e0=style/Widget.Holo.Light.ActionButton.CloseMode 010300e1=style/Widget.Holo.Light.ActionBar 010300e2=style/Widget.Holo.Button.Borderless 010300e3=style/Widget.Holo.Tab 010300e4=style/Widget.Holo.Light.Tab 010300e5=style/Holo.ButtonBar 010300e6=style/Holo.Light.ButtonBar 010300e7=style/Holo.ButtonBar.AlertDialog 010300e8=style/Holo.Light.ButtonBar.AlertDialog 010300e9=style/Holo.SegmentedButton 010300ea=style/Holo.Light.SegmentedButton 010300eb=style/Widget.CalendarView 010300ec=style/Widget.Holo.CalendarView 010300ed=style/Widget.Holo.Light.CalendarView 010300ee=style/Widget.DatePicker 010300ef=style/Widget.Holo.DatePicker 010300f0=style/Theme.Holo.Light.NoActionBar 010300f1=style/Theme.Holo.Light.NoActionBar.Fullscreen 010300f2=style/Widget.ActionBar.TabView 010300f3=style/Widget.ActionBar.TabText 010300f4=style/Widget.ActionBar.TabBar 010300f5=style/Widget.Holo.ActionBar.TabView 010300f6=style/Widget.Holo.ActionBar.TabText 010300f7=style/Widget.Holo.ActionBar.TabBar 010300f8=style/Widget.Holo.Light.ActionBar.TabView 010300f9=style/Widget.Holo.Light.ActionBar.TabText 010300fa=style/Widget.Holo.Light.ActionBar.TabBar 010300fb=style/TextAppearance.Holo 010300fc=style/TextAppearance.Holo.Inverse 010300fd=style/TextAppearance.Holo.Large 010300fe=style/TextAppearance.Holo.Large.Inverse 010300ff=style/TextAppearance.Holo.Medium 01030100=style/TextAppearance.Holo.Medium.Inverse 01030101=style/TextAppearance.Holo.Small 01030102=style/TextAppearance.Holo.Small.Inverse 01030103=style/TextAppearance.Holo.SearchResult.Title 01030104=style/TextAppearance.Holo.SearchResult.Subtitle 01030105=style/TextAppearance.Holo.Widget 01030106=style/TextAppearance.Holo.Widget.Button 01030107=style/TextAppearance.Holo.Widget.IconMenu.Item 01030108=style/TextAppearance.Holo.Widget.TabWidget 01030109=style/TextAppearance.Holo.Widget.TextView 0103010a=style/TextAppearance.Holo.Widget.TextView.PopupMenu 0103010b=style/TextAppearance.Holo.Widget.DropDownHint 0103010c=style/TextAppearance.Holo.Widget.DropDownItem 0103010d=style/TextAppearance.Holo.Widget.TextView.SpinnerItem 0103010e=style/TextAppearance.Holo.Widget.EditText 0103010f=style/TextAppearance.Holo.Widget.PopupMenu 01030110=style/TextAppearance.Holo.Widget.PopupMenu.Large 01030111=style/TextAppearance.Holo.Widget.PopupMenu.Small 01030112=style/TextAppearance.Holo.Widget.ActionBar.Title 01030113=style/TextAppearance.Holo.Widget.ActionBar.Subtitle 01030114=style/TextAppearance.Holo.Widget.ActionMode.Title 01030115=style/TextAppearance.Holo.Widget.ActionMode.Subtitle 01030116=style/TextAppearance.Holo.WindowTitle 01030117=style/TextAppearance.Holo.DialogWindowTitle 01030118=style/TextAppearance.SuggestionHighlight 01030119=style/Theme.Holo.Light.DarkActionBar 0103011a=style/Widget.Holo.Button.Borderless.Small 0103011b=style/Widget.Holo.Light.Button.Borderless.Small 0103011c=style/TextAppearance.Holo.Widget.ActionBar.Title.Inverse 0103011d=style/TextAppearance.Holo.Widget.ActionBar.Subtitle.Inverse 0103011e=style/TextAppearance.Holo.Widget.ActionMode.Title.Inverse 0103011f=style/TextAppearance.Holo.Widget.ActionMode.Subtitle.Inverse 01030120=style/TextAppearance.Holo.Widget.ActionBar.Menu 01030121=style/Widget.Holo.ActionBar.Solid 01030122=style/Widget.Holo.Light.ActionBar.Solid 01030123=style/Widget.Holo.Light.ActionBar.Solid.Inverse 01030124=style/Widget.Holo.Light.ActionBar.TabBar.Inverse 01030125=style/Widget.Holo.Light.ActionBar.TabView.Inverse 01030126=style/Widget.Holo.Light.ActionBar.TabText.Inverse 01030127=style/Widget.Holo.Light.ActionMode.Inverse 01030128=style/Theme.DeviceDefault 01030129=style/Theme.DeviceDefault.NoActionBar 0103012a=style/Theme.DeviceDefault.NoActionBar.Fullscreen 0103012b=style/Theme.DeviceDefault.Light 0103012c=style/Theme.DeviceDefault.Light.NoActionBar 0103012d=style/Theme.DeviceDefault.Light.NoActionBar.Fullscreen 0103012e=style/Theme.DeviceDefault.Dialog 0103012f=style/Theme.DeviceDefault.Dialog.MinWidth 01030130=style/Theme.DeviceDefault.Dialog.NoActionBar 01030131=style/Theme.DeviceDefault.Dialog.NoActionBar.MinWidth 01030132=style/Theme.DeviceDefault.Light.Dialog 01030133=style/Theme.DeviceDefault.Light.Dialog.MinWidth 01030134=style/Theme.DeviceDefault.Light.Dialog.NoActionBar 01030135=style/Theme.DeviceDefault.Light.Dialog.NoActionBar.MinWidth 01030136=style/Theme.DeviceDefault.DialogWhenLarge 01030137=style/Theme.DeviceDefault.DialogWhenLarge.NoActionBar 01030138=style/Theme.DeviceDefault.Light.DialogWhenLarge 01030139=style/Theme.DeviceDefault.Light.DialogWhenLarge.NoActionBar 0103013a=style/Theme.DeviceDefault.Panel 0103013b=style/Theme.DeviceDefault.Light.Panel 0103013c=style/Theme.DeviceDefault.Wallpaper 0103013d=style/Theme.DeviceDefault.Wallpaper.NoTitleBar 0103013e=style/Theme.DeviceDefault.InputMethod 0103013f=style/Theme.DeviceDefault.Light.DarkActionBar 01030140=style/Widget.DeviceDefault 01030141=style/Widget.DeviceDefault.Button 01030142=style/Widget.DeviceDefault.Button.Small 01030143=style/Widget.DeviceDefault.Button.Inset 01030144=style/Widget.DeviceDefault.Button.Toggle 01030145=style/Widget.DeviceDefault.Button.Borderless.Small 01030146=style/Widget.DeviceDefault.TextView 01030147=style/Widget.DeviceDefault.AutoCompleteTextView 01030148=style/Widget.DeviceDefault.CompoundButton.CheckBox 01030149=style/Widget.DeviceDefault.ListView.DropDown 0103014a=style/Widget.DeviceDefault.EditText 0103014b=style/Widget.DeviceDefault.ExpandableListView 0103014c=style/Widget.DeviceDefault.GridView 0103014d=style/Widget.DeviceDefault.ImageButton 0103014e=style/Widget.DeviceDefault.ListView 0103014f=style/Widget.DeviceDefault.PopupWindow 01030150=style/Widget.DeviceDefault.ProgressBar 01030151=style/Widget.DeviceDefault.ProgressBar.Horizontal 01030152=style/Widget.DeviceDefault.ProgressBar.Small 01030153=style/Widget.DeviceDefault.ProgressBar.Small.Title 01030154=style/Widget.DeviceDefault.ProgressBar.Large 01030155=style/Widget.DeviceDefault.SeekBar 01030156=style/Widget.DeviceDefault.RatingBar 01030157=style/Widget.DeviceDefault.RatingBar.Indicator 01030158=style/Widget.DeviceDefault.RatingBar.Small 01030159=style/Widget.DeviceDefault.CompoundButton.RadioButton 0103015a=style/Widget.DeviceDefault.ScrollView 0103015b=style/Widget.DeviceDefault.HorizontalScrollView 0103015c=style/Widget.DeviceDefault.Spinner 0103015d=style/Widget.DeviceDefault.CompoundButton.Star 0103015e=style/Widget.DeviceDefault.TabWidget 0103015f=style/Widget.DeviceDefault.WebTextView 01030160=style/Widget.DeviceDefault.WebView 01030161=style/Widget.DeviceDefault.DropDownItem 01030162=style/Widget.DeviceDefault.DropDownItem.Spinner 01030163=style/Widget.DeviceDefault.TextView.SpinnerItem 01030164=style/Widget.DeviceDefault.ListPopupWindow 01030165=style/Widget.DeviceDefault.PopupMenu 01030166=style/Widget.DeviceDefault.ActionButton 01030167=style/Widget.DeviceDefault.ActionButton.Overflow 01030168=style/Widget.DeviceDefault.ActionButton.TextButton 01030169=style/Widget.DeviceDefault.ActionMode 0103016a=style/Widget.DeviceDefault.ActionButton.CloseMode 0103016b=style/Widget.DeviceDefault.ActionBar 0103016c=style/Widget.DeviceDefault.Button.Borderless 0103016d=style/Widget.DeviceDefault.Tab 0103016e=style/Widget.DeviceDefault.CalendarView 0103016f=style/Widget.DeviceDefault.DatePicker 01030170=style/Widget.DeviceDefault.ActionBar.TabView 01030171=style/Widget.DeviceDefault.ActionBar.TabText 01030172=style/Widget.DeviceDefault.ActionBar.TabBar 01030173=style/Widget.DeviceDefault.ActionBar.Solid 01030174=style/Widget.DeviceDefault.Light 01030175=style/Widget.DeviceDefault.Light.Button 01030176=style/Widget.DeviceDefault.Light.Button.Small 01030177=style/Widget.DeviceDefault.Light.Button.Inset 01030178=style/Widget.DeviceDefault.Light.Button.Toggle 01030179=style/Widget.DeviceDefault.Light.Button.Borderless.Small 0103017a=style/Widget.DeviceDefault.Light.TextView 0103017b=style/Widget.DeviceDefault.Light.AutoCompleteTextView 0103017c=style/Widget.DeviceDefault.Light.CompoundButton.CheckBox 0103017d=style/Widget.DeviceDefault.Light.ListView.DropDown 0103017e=style/Widget.DeviceDefault.Light.EditText 0103017f=style/Widget.DeviceDefault.Light.ExpandableListView 01030180=style/Widget.DeviceDefault.Light.GridView 01030181=style/Widget.DeviceDefault.Light.ImageButton 01030182=style/Widget.DeviceDefault.Light.ListView 01030183=style/Widget.DeviceDefault.Light.PopupWindow 01030184=style/Widget.DeviceDefault.Light.ProgressBar 01030185=style/Widget.DeviceDefault.Light.ProgressBar.Horizontal 01030186=style/Widget.DeviceDefault.Light.ProgressBar.Small 01030187=style/Widget.DeviceDefault.Light.ProgressBar.Small.Title 01030188=style/Widget.DeviceDefault.Light.ProgressBar.Large 01030189=style/Widget.DeviceDefault.Light.ProgressBar.Inverse 0103018a=style/Widget.DeviceDefault.Light.ProgressBar.Small.Inverse 0103018b=style/Widget.DeviceDefault.Light.ProgressBar.Large.Inverse 0103018c=style/Widget.DeviceDefault.Light.SeekBar 0103018d=style/Widget.DeviceDefault.Light.RatingBar 0103018e=style/Widget.DeviceDefault.Light.RatingBar.Indicator 0103018f=style/Widget.DeviceDefault.Light.RatingBar.Small 01030190=style/Widget.DeviceDefault.Light.CompoundButton.RadioButton 01030191=style/Widget.DeviceDefault.Light.ScrollView 01030192=style/Widget.DeviceDefault.Light.HorizontalScrollView 01030193=style/Widget.DeviceDefault.Light.Spinner 01030194=style/Widget.DeviceDefault.Light.CompoundButton.Star 01030195=style/Widget.DeviceDefault.Light.TabWidget 01030196=style/Widget.DeviceDefault.Light.WebTextView 01030197=style/Widget.DeviceDefault.Light.WebView 01030198=style/Widget.DeviceDefault.Light.DropDownItem 01030199=style/Widget.DeviceDefault.Light.DropDownItem.Spinner 0103019a=style/Widget.DeviceDefault.Light.TextView.SpinnerItem 0103019b=style/Widget.DeviceDefault.Light.ListPopupWindow 0103019c=style/Widget.DeviceDefault.Light.PopupMenu 0103019d=style/Widget.DeviceDefault.Light.Tab 0103019e=style/Widget.DeviceDefault.Light.CalendarView 0103019f=style/Widget.DeviceDefault.Light.ActionButton 010301a0=style/Widget.DeviceDefault.Light.ActionButton.Overflow 010301a1=style/Widget.DeviceDefault.Light.ActionMode 010301a2=style/Widget.DeviceDefault.Light.ActionButton.CloseMode 010301a3=style/Widget.DeviceDefault.Light.ActionBar 010301a4=style/Widget.DeviceDefault.Light.ActionBar.TabView 010301a5=style/Widget.DeviceDefault.Light.ActionBar.TabText 010301a6=style/Widget.DeviceDefault.Light.ActionBar.TabBar 010301a7=style/Widget.DeviceDefault.Light.ActionBar.Solid 010301a8=style/Widget.DeviceDefault.Light.ActionBar.Solid.Inverse 010301a9=style/Widget.DeviceDefault.Light.ActionBar.TabBar.Inverse 010301aa=style/Widget.DeviceDefault.Light.ActionBar.TabView.Inverse 010301ab=style/Widget.DeviceDefault.Light.ActionBar.TabText.Inverse 010301ac=style/Widget.DeviceDefault.Light.ActionMode.Inverse 010301ad=style/TextAppearance.DeviceDefault 010301ae=style/TextAppearance.DeviceDefault.Inverse 010301af=style/TextAppearance.DeviceDefault.Large 010301b0=style/TextAppearance.DeviceDefault.Large.Inverse 010301b1=style/TextAppearance.DeviceDefault.Medium 010301b2=style/TextAppearance.DeviceDefault.Medium.Inverse 010301b3=style/TextAppearance.DeviceDefault.Small 010301b4=style/TextAppearance.DeviceDefault.Small.Inverse 010301b5=style/TextAppearance.DeviceDefault.SearchResult.Title 010301b6=style/TextAppearance.DeviceDefault.SearchResult.Subtitle 010301b7=style/TextAppearance.DeviceDefault.WindowTitle 010301b8=style/TextAppearance.DeviceDefault.DialogWindowTitle 010301b9=style/TextAppearance.DeviceDefault.Widget 010301ba=style/TextAppearance.DeviceDefault.Widget.Button 010301bb=style/TextAppearance.DeviceDefault.Widget.IconMenu.Item 010301bc=style/TextAppearance.DeviceDefault.Widget.TabWidget 010301bd=style/TextAppearance.DeviceDefault.Widget.TextView 010301be=style/TextAppearance.DeviceDefault.Widget.TextView.PopupMenu 010301bf=style/TextAppearance.DeviceDefault.Widget.DropDownHint 010301c0=style/TextAppearance.DeviceDefault.Widget.DropDownItem 010301c1=style/TextAppearance.DeviceDefault.Widget.TextView.SpinnerItem 010301c2=style/TextAppearance.DeviceDefault.Widget.EditText 010301c3=style/TextAppearance.DeviceDefault.Widget.PopupMenu 010301c4=style/TextAppearance.DeviceDefault.Widget.PopupMenu.Large 010301c5=style/TextAppearance.DeviceDefault.Widget.PopupMenu.Small 010301c6=style/TextAppearance.DeviceDefault.Widget.ActionBar.Title 010301c7=style/TextAppearance.DeviceDefault.Widget.ActionBar.Subtitle 010301c8=style/TextAppearance.DeviceDefault.Widget.ActionMode.Title 010301c9=style/TextAppearance.DeviceDefault.Widget.ActionMode.Subtitle 010301ca=style/TextAppearance.DeviceDefault.Widget.ActionBar.Title.Inverse 010301cb=style/TextAppearance.DeviceDefault.Widget.ActionBar.Subtitle.Inverse 010301cc=style/TextAppearance.DeviceDefault.Widget.ActionMode.Title.Inverse 010301cd=style/TextAppearance.DeviceDefault.Widget.ActionMode.Subtitle.Inverse 010301ce=style/TextAppearance.DeviceDefault.Widget.ActionBar.Menu 010301cf=style/DeviceDefault.ButtonBar 010301d0=style/DeviceDefault.ButtonBar.AlertDialog 010301d1=style/DeviceDefault.SegmentedButton 010301d2=style/DeviceDefault.Light.ButtonBar 010301d3=style/DeviceDefault.Light.ButtonBar.AlertDialog 010301d4=style/DeviceDefault.Light.SegmentedButton 010301d5=style/Widget.Holo.MediaRouteButton 010301d6=style/Widget.Holo.Light.MediaRouteButton 010301d7=style/Widget.DeviceDefault.MediaRouteButton 010301d8=style/Widget.DeviceDefault.Light.MediaRouteButton 010301d9=style/Widget.Holo.CheckedTextView 010301da=style/Widget.Holo.Light.CheckedTextView 010301db=style/Widget.DeviceDefault.CheckedTextView 010301dc=style/Widget.DeviceDefault.Light.CheckedTextView 010301dd=style/Theme.Holo.NoActionBar.Overscan 010301de=style/Theme.Holo.Light.NoActionBar.Overscan 010301df=style/Theme.DeviceDefault.NoActionBar.Overscan 010301e0=style/Theme.DeviceDefault.Light.NoActionBar.Overscan 010301e1=style/Theme.Holo.NoActionBar.TranslucentDecor 010301e2=style/Theme.Holo.Light.NoActionBar.TranslucentDecor 010301e3=style/Theme.DeviceDefault.NoActionBar.TranslucentDecor 010301e4=style/Theme.DeviceDefault.Light.NoActionBar.TranslucentDecor 010301e5=style/Widget.FastScroll 010301e6=style/Widget.StackView 010301e7=style/Widget.Toolbar 010301e8=style/Widget.Toolbar.Button.Navigation 010301e9=style/Widget.DeviceDefault.FastScroll 010301ea=style/Widget.DeviceDefault.StackView 010301eb=style/Widget.DeviceDefault.Light.FastScroll 010301ec=style/Widget.DeviceDefault.Light.StackView 010301ed=style/TextAppearance.Material 010301ee=style/TextAppearance.Material.Button 010301ef=style/TextAppearance.Material.Body2 010301f0=style/TextAppearance.Material.Body1 010301f1=style/TextAppearance.Material.Caption 010301f2=style/TextAppearance.Material.DialogWindowTitle 010301f3=style/TextAppearance.Material.Display4 010301f4=style/TextAppearance.Material.Display3 010301f5=style/TextAppearance.Material.Display2 010301f6=style/TextAppearance.Material.Display1 010301f7=style/TextAppearance.Material.Headline 010301f8=style/TextAppearance.Material.Inverse 010301f9=style/TextAppearance.Material.Large 010301fa=style/TextAppearance.Material.Large.Inverse 010301fb=style/TextAppearance.Material.Medium 010301fc=style/TextAppearance.Material.Medium.Inverse 010301fd=style/TextAppearance.Material.Menu 010301fe=style/TextAppearance.Material.Notification 010301ff=style/TextAppearance.Material.Notification.Emphasis 01030200=style/TextAppearance.Material.Notification.Info 01030201=style/TextAppearance.Material.Notification.Line2 01030202=style/TextAppearance.Material.Notification.Time 01030203=style/TextAppearance.Material.Notification.Title 01030204=style/TextAppearance.Material.SearchResult.Subtitle 01030205=style/TextAppearance.Material.SearchResult.Title 01030206=style/TextAppearance.Material.Small 01030207=style/TextAppearance.Material.Small.Inverse 01030208=style/TextAppearance.Material.Subhead 01030209=style/TextAppearance.Material.Title 0103020a=style/TextAppearance.Material.WindowTitle 0103020b=style/TextAppearance.Material.Widget 0103020c=style/TextAppearance.Material.Widget.ActionBar.Menu 0103020d=style/TextAppearance.Material.Widget.ActionBar.Subtitle 0103020e=style/TextAppearance.Material.Widget.ActionBar.Subtitle.Inverse 0103020f=style/TextAppearance.Material.Widget.ActionBar.Title 01030210=style/TextAppearance.Material.Widget.ActionBar.Title.Inverse 01030211=style/TextAppearance.Material.Widget.ActionMode.Subtitle 01030212=style/TextAppearance.Material.Widget.ActionMode.Subtitle.Inverse 01030213=style/TextAppearance.Material.Widget.ActionMode.Title 01030214=style/TextAppearance.Material.Widget.ActionMode.Title.Inverse 01030215=style/TextAppearance.Material.Widget.Button 01030216=style/TextAppearance.Material.Widget.DropDownHint 01030217=style/TextAppearance.Material.Widget.DropDownItem 01030218=style/TextAppearance.Material.Widget.EditText 01030219=style/TextAppearance.Material.Widget.IconMenu.Item 0103021a=style/TextAppearance.Material.Widget.PopupMenu 0103021b=style/TextAppearance.Material.Widget.PopupMenu.Large 0103021c=style/TextAppearance.Material.Widget.PopupMenu.Small 0103021d=style/TextAppearance.Material.Widget.TabWidget 0103021e=style/TextAppearance.Material.Widget.TextView 0103021f=style/TextAppearance.Material.Widget.TextView.PopupMenu 01030220=style/TextAppearance.Material.Widget.TextView.SpinnerItem 01030221=style/TextAppearance.Material.Widget.Toolbar.Subtitle 01030222=style/TextAppearance.Material.Widget.Toolbar.Title 01030223=style/Theme.DeviceDefault.Settings 01030224=style/Theme.Material 01030225=style/Theme.Material.Dialog 01030226=style/Theme.Material.Dialog.Alert 01030227=style/Theme.Material.Dialog.MinWidth 01030228=style/Theme.Material.Dialog.NoActionBar 01030229=style/Theme.Material.Dialog.NoActionBar.MinWidth 0103022a=style/Theme.Material.Dialog.Presentation 0103022b=style/Theme.Material.DialogWhenLarge 0103022c=style/Theme.Material.DialogWhenLarge.NoActionBar 0103022d=style/Theme.Material.InputMethod 0103022e=style/Theme.Material.NoActionBar 0103022f=style/Theme.Material.NoActionBar.Fullscreen 01030230=style/Theme.Material.NoActionBar.Overscan 01030231=style/Theme.Material.NoActionBar.TranslucentDecor 01030232=style/Theme.Material.Panel 01030233=style/Theme.Material.Settings 01030234=style/Theme.Material.Voice 01030235=style/Theme.Material.Wallpaper 01030236=style/Theme.Material.Wallpaper.NoTitleBar 01030237=style/Theme.Material.Light 01030238=style/Theme.Material.Light.DarkActionBar 01030239=style/Theme.Material.Light.Dialog 0103023a=style/Theme.Material.Light.Dialog.Alert 0103023b=style/Theme.Material.Light.Dialog.MinWidth 0103023c=style/Theme.Material.Light.Dialog.NoActionBar 0103023d=style/Theme.Material.Light.Dialog.NoActionBar.MinWidth 0103023e=style/Theme.Material.Light.Dialog.Presentation 0103023f=style/Theme.Material.Light.DialogWhenLarge 01030240=style/Theme.Material.Light.DialogWhenLarge.NoActionBar 01030241=style/Theme.Material.Light.NoActionBar 01030242=style/Theme.Material.Light.NoActionBar.Fullscreen 01030243=style/Theme.Material.Light.NoActionBar.Overscan 01030244=style/Theme.Material.Light.NoActionBar.TranslucentDecor 01030245=style/Theme.Material.Light.Panel 01030246=style/Theme.Material.Light.Voice 01030247=style/ThemeOverlay 01030248=style/ThemeOverlay.Material 01030249=style/ThemeOverlay.Material.ActionBar 0103024a=style/ThemeOverlay.Material.Light 0103024b=style/ThemeOverlay.Material.Dark 0103024c=style/ThemeOverlay.Material.Dark.ActionBar 0103024d=style/Widget.Material 0103024e=style/Widget.Material.ActionBar 0103024f=style/Widget.Material.ActionBar.Solid 01030250=style/Widget.Material.ActionBar.TabBar 01030251=style/Widget.Material.ActionBar.TabText 01030252=style/Widget.Material.ActionBar.TabView 01030253=style/Widget.Material.ActionButton 01030254=style/Widget.Material.ActionButton.CloseMode 01030255=style/Widget.Material.ActionButton.Overflow 01030256=style/Widget.Material.ActionMode 01030257=style/Widget.Material.AutoCompleteTextView 01030258=style/Widget.Material.Button 01030259=style/Widget.Material.Button.Borderless 0103025a=style/Widget.Material.Button.Borderless.Colored 0103025b=style/Widget.Material.Button.Borderless.Small 0103025c=style/Widget.Material.Button.Inset 0103025d=style/Widget.Material.Button.Small 0103025e=style/Widget.Material.Button.Toggle 0103025f=style/Widget.Material.ButtonBar 01030260=style/Widget.Material.ButtonBar.AlertDialog 01030261=style/Widget.Material.CalendarView 01030262=style/Widget.Material.CheckedTextView 01030263=style/Widget.Material.CompoundButton.CheckBox 01030264=style/Widget.Material.CompoundButton.RadioButton 01030265=style/Widget.Material.CompoundButton.Star 01030266=style/Widget.Material.DatePicker 01030267=style/Widget.Material.DropDownItem 01030268=style/Widget.Material.DropDownItem.Spinner 01030269=style/Widget.Material.EditText 0103026a=style/Widget.Material.ExpandableListView 0103026b=style/Widget.Material.FastScroll 0103026c=style/Widget.Material.GridView 0103026d=style/Widget.Material.HorizontalScrollView 0103026e=style/Widget.Material.ImageButton 0103026f=style/Widget.Material.ListPopupWindow 01030270=style/Widget.Material.ListView 01030271=style/Widget.Material.ListView.DropDown 01030272=style/Widget.Material.MediaRouteButton 01030273=style/Widget.Material.PopupMenu 01030274=style/Widget.Material.PopupMenu.Overflow 01030275=style/Widget.Material.PopupWindow 01030276=style/Widget.Material.ProgressBar 01030277=style/Widget.Material.ProgressBar.Horizontal 01030278=style/Widget.Material.ProgressBar.Large 01030279=style/Widget.Material.ProgressBar.Small 0103027a=style/Widget.Material.ProgressBar.Small.Title 0103027b=style/Widget.Material.RatingBar 0103027c=style/Widget.Material.RatingBar.Indicator 0103027d=style/Widget.Material.RatingBar.Small 0103027e=style/Widget.Material.ScrollView 0103027f=style/Widget.Material.SearchView 01030280=style/Widget.Material.SeekBar 01030281=style/Widget.Material.SegmentedButton 01030282=style/Widget.Material.StackView 01030283=style/Widget.Material.Spinner 01030284=style/Widget.Material.Spinner.Underlined 01030285=style/Widget.Material.Tab 01030286=style/Widget.Material.TabWidget 01030287=style/Widget.Material.TextView 01030288=style/Widget.Material.TextView.SpinnerItem 01030289=style/Widget.Material.TimePicker 0103028a=style/Widget.Material.Toolbar 0103028b=style/Widget.Material.Toolbar.Button.Navigation 0103028c=style/Widget.Material.WebTextView 0103028d=style/Widget.Material.WebView 0103028e=style/Widget.Material.Light 0103028f=style/Widget.Material.Light.ActionBar 01030290=style/Widget.Material.Light.ActionBar.Solid 01030291=style/Widget.Material.Light.ActionBar.TabBar 01030292=style/Widget.Material.Light.ActionBar.TabText 01030293=style/Widget.Material.Light.ActionBar.TabView 01030294=style/Widget.Material.Light.ActionButton 01030295=style/Widget.Material.Light.ActionButton.CloseMode 01030296=style/Widget.Material.Light.ActionButton.Overflow 01030297=style/Widget.Material.Light.ActionMode 01030298=style/Widget.Material.Light.AutoCompleteTextView 01030299=style/Widget.Material.Light.Button 0103029a=style/Widget.Material.Light.Button.Borderless 0103029b=style/Widget.Material.Light.Button.Borderless.Colored 0103029c=style/Widget.Material.Light.Button.Borderless.Small 0103029d=style/Widget.Material.Light.Button.Inset 0103029e=style/Widget.Material.Light.Button.Small 0103029f=style/Widget.Material.Light.Button.Toggle 010302a0=style/Widget.Material.Light.ButtonBar 010302a1=style/Widget.Material.Light.ButtonBar.AlertDialog 010302a2=style/Widget.Material.Light.CalendarView 010302a3=style/Widget.Material.Light.CheckedTextView 010302a4=style/Widget.Material.Light.CompoundButton.CheckBox 010302a5=style/Widget.Material.Light.CompoundButton.RadioButton 010302a6=style/Widget.Material.Light.CompoundButton.Star 010302a7=style/Widget.Material.Light.DatePicker 010302a8=style/Widget.Material.Light.DropDownItem 010302a9=style/Widget.Material.Light.DropDownItem.Spinner 010302aa=style/Widget.Material.Light.EditText 010302ab=style/Widget.Material.Light.ExpandableListView 010302ac=style/Widget.Material.Light.FastScroll 010302ad=style/Widget.Material.Light.GridView 010302ae=style/Widget.Material.Light.HorizontalScrollView 010302af=style/Widget.Material.Light.ImageButton 010302b0=style/Widget.Material.Light.ListPopupWindow 010302b1=style/Widget.Material.Light.ListView 010302b2=style/Widget.Material.Light.ListView.DropDown 010302b3=style/Widget.Material.Light.MediaRouteButton 010302b4=style/Widget.Material.Light.PopupMenu 010302b5=style/Widget.Material.Light.PopupMenu.Overflow 010302b6=style/Widget.Material.Light.PopupWindow 010302b7=style/Widget.Material.Light.ProgressBar 010302b8=style/Widget.Material.Light.ProgressBar.Horizontal 010302b9=style/Widget.Material.Light.ProgressBar.Inverse 010302ba=style/Widget.Material.Light.ProgressBar.Large 010302bb=style/Widget.Material.Light.ProgressBar.Large.Inverse 010302bc=style/Widget.Material.Light.ProgressBar.Small 010302bd=style/Widget.Material.Light.ProgressBar.Small.Inverse 010302be=style/Widget.Material.Light.ProgressBar.Small.Title 010302bf=style/Widget.Material.Light.RatingBar 010302c0=style/Widget.Material.Light.RatingBar.Indicator 010302c1=style/Widget.Material.Light.RatingBar.Small 010302c2=style/Widget.Material.Light.ScrollView 010302c3=style/Widget.Material.Light.SearchView 010302c4=style/Widget.Material.Light.SeekBar 010302c5=style/Widget.Material.Light.SegmentedButton 010302c6=style/Widget.Material.Light.StackView 010302c7=style/Widget.Material.Light.Spinner 010302c8=style/Widget.Material.Light.Spinner.Underlined 010302c9=style/Widget.Material.Light.Tab 010302ca=style/Widget.Material.Light.TabWidget 010302cb=style/Widget.Material.Light.TextView 010302cc=style/Widget.Material.Light.TextView.SpinnerItem 010302cd=style/Widget.Material.Light.TimePicker 010302ce=style/Widget.Material.Light.WebTextView 010302cf=style/Widget.Material.Light.WebView 010302d0=style/Theme.Leanback.FormWizard 010302d1=style/Theme.DeviceDefault.Dialog.Alert 010302d2=style/Theme.DeviceDefault.Light.Dialog.Alert 010302d3=style/Widget.Material.Button.Colored 010302d4=style/TextAppearance.Material.Widget.Button.Inverse 010302d5=style/Theme.Material.Light.LightStatusBar 010302d6=style/ThemeOverlay.Material.Dialog 010302d7=style/ThemeOverlay.Material.Dialog.Alert 010302d8=style/Theme.Material.Light.DialogWhenLarge.DarkActionBar 010302d9=style/Widget.Material.SeekBar.Discrete 010302da=style/Widget.Material.CompoundButton.Switch 010302db=style/Widget.Material.Light.CompoundButton.Switch 010302dc=style/Widget.Material.NumberPicker 010302dd=style/Widget.Material.Light.NumberPicker 010302de=style/TextAppearance.Material.Widget.Button.Colored 010302df=style/TextAppearance.Material.Widget.Button.Borderless.Colored 010302e0=style/Widget.DeviceDefault.Button.Colored 010302e1=style/Widget.DeviceDefault.Button.Borderless.Colored 010302e2=style/Theme.DeviceDefault.DocumentsUI 010302e3=style/Theme.DeviceDefault.DayNight 010302e4=style/ThemeOverlay.DeviceDefault.Accent.DayNight 010302e5=style/TextAppearance.DeviceDefault.Headline 010302e6=style/AccessibilityAutoclickPanelButtonLayoutStyle 010302e7=style/AccessibilityAutoclickPanelImageButtonStyle 010302e8=style/AccessibilityAutoclickScrollPanelButtonLayoutStyle 010302e9=style/AccessibilityAutoclickScrollPanelImageButtonStyle 010302ea=style/AccessibilityButtonChooserDialog 010302eb=style/AccessibilityDialog 010302ec=style/AccessibilityDialogButton 010302ed=style/AccessibilityDialogButtonBarSpace 010302ee=style/AccessibilityDialogButtonList 010302ef=style/AccessibilityDialogDescription 010302f0=style/AccessibilityDialogIcon 010302f1=style/AccessibilityDialogPermissionDescription 010302f2=style/AccessibilityDialogPermissionTitle 010302f3=style/AccessibilityDialogServiceIcon 010302f4=style/AccessibilityDialogTitle 010302f5=style/ActiveWallpaperSettings 010302f6=style/AlertDialog 010302f7=style/AlertDialog.DeviceDefault 010302f8=style/AlertDialog.DeviceDefault.Light 010302f9=style/AlertDialog.Holo 010302fa=style/AlertDialog.Holo.Light 010302fb=style/AlertDialog.Leanback 010302fc=style/AlertDialog.Leanback.Light 010302fd=style/AlertDialog.Material 010302fe=style/AlertDialog.Material.Light 010302ff=style/AlertDialog.Material3 01030300=style/AlertDialogEmergencyButtonStyle 01030301=style/AlertDialogWithEmergencyButton 01030302=style/Animation.DeviceDefault.Activity 01030303=style/Animation.DeviceDefault.Activity.Resolver 01030304=style/Animation.DeviceDefault.Dialog 01030305=style/Animation.DropDownDown 01030306=style/Animation.DropDownUp 01030307=style/Animation.Holo 01030308=style/Animation.Holo.Activity 01030309=style/Animation.Holo.Dialog 0103030a=style/Animation.ImmersiveModeConfirmation 0103030b=style/Animation.InputMethodFancy 0103030c=style/Animation.LockScreen 0103030d=style/Animation.Material 0103030e=style/Animation.Material.Activity 0103030f=style/Animation.Material.Dialog 01030310=style/Animation.Material.Popup 01030311=style/Animation.OptionsPanel 01030312=style/Animation.PopupWindow 01030313=style/Animation.PopupWindow.ActionMode 01030314=style/Animation.RecentApplications 01030315=style/Animation.SearchBar 01030316=style/Animation.SubMenuPanel 01030317=style/Animation.TextSelectHandle 01030318=style/Animation.Tooltip 01030319=style/Animation.TypingFilter 0103031a=style/Animation.TypingFilterRestore 0103031b=style/Animation.VoiceActivity 0103031c=style/Animation.VoiceInteractionSession 0103031d=style/Animation.VolumePanel 0103031e=style/Animation.Wallpaper 0103031f=style/Animation.ZoomButtons 01030320=style/AutofillDatasetPicker 01030321=style/AutofillHalfScreenAnimation 01030322=style/AutofillHalfSheetButton 01030323=style/AutofillHalfSheetDivider 01030324=style/AutofillHalfSheetOutlinedButton 01030325=style/AutofillHalfSheetTonalButton 01030326=style/AutofillSaveAnimation 01030327=style/AutofillSaveUiTitle 01030328=style/BaseErrorDialog.DeviceDefault 01030329=style/BasePreferenceFragment 0103032a=style/CarDialogButtonText 0103032b=style/CarDialogMessageText 0103032c=style/DatePickerDialog.Material 0103032d=style/DialogWindowTitle 0103032e=style/DialogWindowTitle.DeviceDefault 0103032f=style/DialogWindowTitle.DeviceDefault.Light 01030330=style/DialogWindowTitle.Holo 01030331=style/DialogWindowTitle.Holo.Light 01030332=style/DialogWindowTitle.Material 01030333=style/DialogWindowTitle.Material.Light 01030334=style/DialogWindowTitleBackground 01030335=style/DialogWindowTitleBackground.Material 01030336=style/DialogWindowTitleBackground.Material.Light 01030337=style/GrantCredentialsPermissionActivity 01030338=style/Holo 01030339=style/Holo.Light 0103033a=style/InputMethodSwitchDialogStyle 0103033b=style/InputMethodSwitchHardKeyboardSwitch 0103033c=style/InputMethodSwitchHardKeyboardText 0103033d=style/LargePointer 0103033e=style/Material 0103033f=style/Material.Light 01030340=style/Notification.Header 01030341=style/NotificationAction 01030342=style/NotificationEmphasizedAction 01030343=style/NotificationTombstoneAction 01030344=style/Pointer 01030345=style/PointerIconVectorStyleFillBlack 01030346=style/PointerIconVectorStyleFillBlue 01030347=style/PointerIconVectorStyleFillGreen 01030348=style/PointerIconVectorStyleFillPink 01030349=style/PointerIconVectorStyleFillPurple 0103034a=style/PointerIconVectorStyleFillRed 0103034b=style/PointerIconVectorStyleStrokeBlack 0103034c=style/PointerIconVectorStyleStrokeNone 0103034d=style/PointerIconVectorStyleStrokeWhite 0103034e=style/Preference 0103034f=style/Preference.Category 01030350=style/Preference.CheckBoxPreference 01030351=style/Preference.DeviceDefault 01030352=style/Preference.DeviceDefault.Category 01030353=style/Preference.DeviceDefault.CheckBoxPreference 01030354=style/Preference.DeviceDefault.DialogPreference 01030355=style/Preference.DeviceDefault.DialogPreference.EditTextPreference 01030356=style/Preference.DeviceDefault.DialogPreference.YesNoPreference 01030357=style/Preference.DeviceDefault.Information 01030358=style/Preference.DeviceDefault.PreferenceScreen 01030359=style/Preference.DeviceDefault.RingtonePreference 0103035a=style/Preference.DeviceDefault.SeekBarPreference 0103035b=style/Preference.DeviceDefault.SwitchPreference 0103035c=style/Preference.DialogPreference 0103035d=style/Preference.DialogPreference.EditTextPreference 0103035e=style/Preference.DialogPreference.SeekBarPreference 0103035f=style/Preference.DialogPreference.YesNoPreference 01030360=style/Preference.Holo 01030361=style/Preference.Holo.Category 01030362=style/Preference.Holo.CheckBoxPreference 01030363=style/Preference.Holo.DialogPreference 01030364=style/Preference.Holo.DialogPreference.EditTextPreference 01030365=style/Preference.Holo.DialogPreference.YesNoPreference 01030366=style/Preference.Holo.Information 01030367=style/Preference.Holo.PreferenceScreen 01030368=style/Preference.Holo.RingtonePreference 01030369=style/Preference.Holo.SeekBarPreference 0103036a=style/Preference.Holo.SwitchPreference 0103036b=style/Preference.Information 0103036c=style/Preference.Material 0103036d=style/Preference.Material.BasePreferenceScreen 0103036e=style/Preference.Material.Category 0103036f=style/Preference.Material.CheckBoxPreference 01030370=style/Preference.Material.DialogPreference 01030371=style/Preference.Material.DialogPreference.EditTextPreference 01030372=style/Preference.Material.DialogPreference.SeekBarPreference 01030373=style/Preference.Material.DialogPreference.YesNoPreference 01030374=style/Preference.Material.Information 01030375=style/Preference.Material.PreferenceScreen 01030376=style/Preference.Material.RingtonePreference 01030377=style/Preference.Material.SeekBarPreference 01030378=style/Preference.Material.SwitchPreference 01030379=style/Preference.PreferenceScreen 0103037a=style/Preference.RingtonePreference 0103037b=style/Preference.SeekBarPreference 0103037c=style/Preference.SwitchPreference 0103037d=style/PreferenceActivity 0103037e=style/PreferenceActivity.Material 0103037f=style/PreferenceFragment 01030380=style/PreferenceFragment.Holo 01030381=style/PreferenceFragment.Material 01030382=style/PreferenceFragmentList 01030383=style/PreferenceFragmentList.Material 01030384=style/PreferenceHeaderList 01030385=style/PreferenceHeaderList.Material 01030386=style/PreferenceHeaderPanel 01030387=style/PreferenceHeaderPanel.Material 01030388=style/PreferencePanel 01030389=style/PreferencePanel.Dialog 0103038a=style/PreferencePanel.Material 0103038b=style/PreferencePanel.Material.Dialog 0103038c=style/PreviewWallpaperSettings 0103038d=style/ProgressDialogMessage 0103038e=style/SegmentedButton 0103038f=style/TextAppearance.AlertDialog.Body1 01030390=style/TextAppearance.AlertDialog.Title 01030391=style/TextAppearance.AutoCorrectionSuggestion 01030392=style/TextAppearance.DeviceDefault.Body1 01030393=style/TextAppearance.DeviceDefault.Body2 01030394=style/TextAppearance.DeviceDefault.Caption 01030395=style/TextAppearance.DeviceDefault.Display1 01030396=style/TextAppearance.DeviceDefault.ListItem 01030397=style/TextAppearance.DeviceDefault.ListItemSecondary 01030398=style/TextAppearance.DeviceDefault.Notification 01030399=style/TextAppearance.DeviceDefault.Notification.BigTitle 0103039a=style/TextAppearance.DeviceDefault.Notification.Info 0103039b=style/TextAppearance.DeviceDefault.Notification.Reply 0103039c=style/TextAppearance.DeviceDefault.Notification.Time 0103039d=style/TextAppearance.DeviceDefault.Notification.Title 0103039e=style/TextAppearance.DeviceDefault.Subhead 0103039f=style/TextAppearance.DeviceDefault.Title 010303a0=style/TextAppearance.DeviceDefault.Widget.Button.Borderless.Colored 010303a1=style/TextAppearance.DeviceDefault.Widget.Switch 010303a2=style/TextAppearance.DeviceDefault.Widget.Toolbar.Subtitle 010303a3=style/TextAppearance.DeviceDefault.Widget.Toolbar.Title 010303a4=style/TextAppearance.EasyCorrectSuggestion 010303a5=style/TextAppearance.GrammarErrorSuggestion 010303a6=style/TextAppearance.Holo.CalendarViewWeekDayView 010303a7=style/TextAppearance.Holo.Light 010303a8=style/TextAppearance.Holo.Light.CalendarViewWeekDayView 010303a9=style/TextAppearance.Holo.Light.DialogWindowTitle 010303aa=style/TextAppearance.Holo.Light.Inverse 010303ab=style/TextAppearance.Holo.Light.Large 010303ac=style/TextAppearance.Holo.Light.Large.Inverse 010303ad=style/TextAppearance.Holo.Light.Medium 010303ae=style/TextAppearance.Holo.Light.Medium.Inverse 010303af=style/TextAppearance.Holo.Light.SearchResult 010303b0=style/TextAppearance.Holo.Light.SearchResult.Subtitle 010303b1=style/TextAppearance.Holo.Light.SearchResult.Title 010303b2=style/TextAppearance.Holo.Light.Small 010303b3=style/TextAppearance.Holo.Light.Small.Inverse 010303b4=style/TextAppearance.Holo.Light.Widget 010303b5=style/TextAppearance.Holo.Light.Widget.ActionMode.Subtitle 010303b6=style/TextAppearance.Holo.Light.Widget.ActionMode.Title 010303b7=style/TextAppearance.Holo.Light.Widget.Button 010303b8=style/TextAppearance.Holo.Light.Widget.DropDownHint 010303b9=style/TextAppearance.Holo.Light.Widget.EditText 010303ba=style/TextAppearance.Holo.Light.Widget.PopupMenu 010303bb=style/TextAppearance.Holo.Light.Widget.PopupMenu.Large 010303bc=style/TextAppearance.Holo.Light.Widget.PopupMenu.Small 010303bd=style/TextAppearance.Holo.Light.Widget.Switch 010303be=style/TextAppearance.Holo.Light.WindowTitle 010303bf=style/TextAppearance.Holo.SearchResult 010303c0=style/TextAppearance.Holo.SuggestionHighlight 010303c1=style/TextAppearance.Holo.Widget.ActionMode 010303c2=style/TextAppearance.Holo.Widget.Switch 010303c3=style/TextAppearance.Large.Inverse.NumberPickerInputText 010303c4=style/TextAppearance.Leanback.FormWizard 010303c5=style/TextAppearance.Leanback.FormWizard.Large 010303c6=style/TextAppearance.Leanback.FormWizard.ListItem 010303c7=style/TextAppearance.Leanback.FormWizard.Medium 010303c8=style/TextAppearance.Leanback.FormWizard.Small 010303c9=style/TextAppearance.Material.DatePicker.DateLabel 010303ca=style/TextAppearance.Material.DatePicker.List.YearLabel 010303cb=style/TextAppearance.Material.DatePicker.List.YearLabel.Activated 010303cc=style/TextAppearance.Material.DatePicker.YearLabel 010303cd=style/TextAppearance.Material.ListItem 010303ce=style/TextAppearance.Material.ListItemSecondary 010303cf=style/TextAppearance.Material.Menu.Inverse 010303d0=style/TextAppearance.Material.Notification.BigTitle 010303d1=style/TextAppearance.Material.Notification.Reply 010303d2=style/TextAppearance.Material.NumberPicker 010303d3=style/TextAppearance.Material.SearchResult 010303d4=style/TextAppearance.Material.Subhead.Inverse 010303d5=style/TextAppearance.Material.TextSuggestionHighlight 010303d6=style/TextAppearance.Material.TimePicker.AmPmLabel 010303d7=style/TextAppearance.Material.TimePicker.InputField 010303d8=style/TextAppearance.Material.TimePicker.InputHeader 010303d9=style/TextAppearance.Material.TimePicker.PromptLabel 010303da=style/TextAppearance.Material.TimePicker.TimeLabel 010303db=style/TextAppearance.Material.Title.Inverse 010303dc=style/TextAppearance.Material.Widget.ActionBar.Menu.Inverse 010303dd=style/TextAppearance.Material.Widget.ActionMode 010303de=style/TextAppearance.Material.Widget.Calendar.Day 010303df=style/TextAppearance.Material.Widget.Calendar.DayOfWeek 010303e0=style/TextAppearance.Material.Widget.Calendar.Month 010303e1=style/TextAppearance.Material.Widget.PopupMenu.Header 010303e2=style/TextAppearance.Material.Widget.Switch 010303e3=style/TextAppearance.MisspelledSuggestion 010303e4=style/TextAppearance.SearchResult 010303e5=style/TextAppearance.SlidingTabActive 010303e6=style/TextAppearance.SlidingTabNormal 010303e7=style/TextAppearance.Small.CalendarViewWeekDayView 010303e8=style/TextAppearance.StatusBar 010303e9=style/TextAppearance.StatusBar.EventContent.Emphasis 010303ea=style/TextAppearance.StatusBar.EventContent.Info 010303eb=style/TextAppearance.StatusBar.EventContent.Line2 010303ec=style/TextAppearance.StatusBar.EventContent.Time 010303ed=style/TextAppearance.StatusBar.Ticker 010303ee=style/TextAppearance.Suggestion 010303ef=style/TextAppearance.Toast 010303f0=style/TextAppearance.Tooltip 010303f1=style/TextAppearance.Watch 010303f2=style/TextAppearance.Watch.AppErrorDialog 010303f3=style/TextAppearance.Watch.AppErrorDialog.Item 010303f4=style/TextAppearance.Watch.BaseErrorDialog 010303f5=style/TextAppearance.Watch.BaseErrorDialog.Title 010303f6=style/TextAppearance.Widget.ActionBar.Subtitle 010303f7=style/TextAppearance.Widget.ActionBar.Title 010303f8=style/TextAppearance.Widget.ActionMode.Subtitle 010303f9=style/TextAppearance.Widget.ActionMode.Title 010303fa=style/TextAppearance.Widget.Material3.Button 010303fb=style/TextAppearance.Widget.Material3.Button.Filled 010303fc=style/TextAppearance.Widget.PopupMenu 010303fd=style/TextAppearance.Widget.Toolbar.Subtitle 010303fe=style/TextAppearance.Widget.Toolbar.Title 010303ff=style/Theme.DeviceDefault.Autofill 01030400=style/Theme.DeviceDefault.Autofill.Light 01030401=style/Theme.DeviceDefault.Autofill.Save 01030402=style/Theme.DeviceDefault.AutofillHalfScreenDialogButton 01030403=style/Theme.DeviceDefault.AutofillHalfScreenDialogList 01030404=style/Theme.DeviceDefault.Chooser 01030405=style/Theme.DeviceDefault.Dialog.Alert.DayNight 01030406=style/Theme.DeviceDefault.Dialog.AppError 01030407=style/Theme.DeviceDefault.Dialog.FixedSize 01030408=style/Theme.DeviceDefault.Dialog.NoActionBar.FixedSize 01030409=style/Theme.DeviceDefault.Dialog.NoFrame 0103040a=style/Theme.DeviceDefault.Dialog.Presentation 0103040b=style/Theme.DeviceDefault.InputMethodSwitcherDialog 0103040c=style/Theme.DeviceDefault.Light.Autofill 0103040d=style/Theme.DeviceDefault.Light.Autofill.Save 0103040e=style/Theme.DeviceDefault.Light.Dialog.Alert.UserSwitchingDialog 0103040f=style/Theme.DeviceDefault.Light.Dialog.FixedSize 01030410=style/Theme.DeviceDefault.Light.Dialog.NoActionBar.FixedSize 01030411=style/Theme.DeviceDefault.Light.Dialog.Presentation 01030412=style/Theme.DeviceDefault.Light.SearchBar 01030413=style/Theme.DeviceDefault.Light.Voice 01030414=style/Theme.DeviceDefault.Notification 01030415=style/Theme.DeviceDefault.Resolver 01030416=style/Theme.DeviceDefault.ResolverCommon 01030417=style/Theme.DeviceDefault.SearchBar 01030418=style/Theme.DeviceDefault.Settings.BaseDialog 01030419=style/Theme.DeviceDefault.Settings.CompactMenu 0103041a=style/Theme.DeviceDefault.Settings.Dark.NoActionBar 0103041b=style/Theme.DeviceDefault.Settings.Dialog 0103041c=style/Theme.DeviceDefault.Settings.Dialog.Alert 0103041d=style/Theme.DeviceDefault.Settings.Dialog.NoActionBar 0103041e=style/Theme.DeviceDefault.Settings.Dialog.Presentation 0103041f=style/Theme.DeviceDefault.Settings.DialogBase 01030420=style/Theme.DeviceDefault.Settings.DialogWhenLarge 01030421=style/Theme.DeviceDefault.Settings.DialogWhenLarge.NoActionBar 01030422=style/Theme.DeviceDefault.Settings.NoActionBar 01030423=style/Theme.DeviceDefault.Settings.SearchBar 01030424=style/Theme.DeviceDefault.System 01030425=style/Theme.DeviceDefault.System.Dialog 01030426=style/Theme.DeviceDefault.System.Dialog.Alert 01030427=style/Theme.DeviceDefault.SystemUI 01030428=style/Theme.DeviceDefault.SystemUI.Dialog 01030429=style/Theme.DeviceDefault.VoiceInteractionSession 0103042a=style/Theme.DeviceDefaultBase 0103042b=style/Theme.Dialog.Alert 0103042c=style/Theme.Dialog.Confirmation 0103042d=style/Theme.Dialog.NoFrame 0103042e=style/Theme.Dialog.RecentApplications 0103042f=style/Theme.Dream 01030430=style/Theme.ExpandedMenu 01030431=style/Theme.GameSessionTrampoline 01030432=style/Theme.GlobalSearchBar 01030433=style/Theme.Holo.CompactMenu 01030434=style/Theme.Holo.Dialog.Alert 01030435=style/Theme.Holo.Dialog.BaseAlert 01030436=style/Theme.Holo.Dialog.FixedSize 01030437=style/Theme.Holo.Dialog.NoActionBar.FixedSize 01030438=style/Theme.Holo.Dialog.NoFrame 01030439=style/Theme.Holo.Dialog.Presentation 0103043a=style/Theme.Holo.Light.CompactMenu 0103043b=style/Theme.Holo.Light.Dialog.Alert 0103043c=style/Theme.Holo.Light.Dialog.BaseAlert 0103043d=style/Theme.Holo.Light.Dialog.FixedSize 0103043e=style/Theme.Holo.Light.Dialog.NoActionBar.FixedSize 0103043f=style/Theme.Holo.Light.Dialog.Presentation 01030440=style/Theme.Holo.Light.SearchBar 01030441=style/Theme.Holo.SearchBar 01030442=style/Theme.IconMenu 01030443=style/Theme.Leanback.Dialog 01030444=style/Theme.Leanback.Dialog.Alert 01030445=style/Theme.Leanback.Dialog.AppError 01030446=style/Theme.Leanback.Dialog.Confirmation 01030447=style/Theme.Leanback.Resolver 01030448=style/Theme.Leanback.Settings.Dialog 01030449=style/Theme.Leanback.Settings.Dialog.Alert 0103044a=style/Theme.Material.BaseDialog 0103044b=style/Theme.Material.CompactMenu 0103044c=style/Theme.Material.Dialog.BaseAlert 0103044d=style/Theme.Material.Dialog.FixedSize 0103044e=style/Theme.Material.Dialog.NoActionBar.FixedSize 0103044f=style/Theme.Material.Dialog.NoFrame 01030450=style/Theme.Material.Light.BaseDialog 01030451=style/Theme.Material.Light.CompactMenu 01030452=style/Theme.Material.Light.Dialog.BaseAlert 01030453=style/Theme.Material.Light.Dialog.FixedSize 01030454=style/Theme.Material.Light.Dialog.NoActionBar.FixedSize 01030455=style/Theme.Material.Light.SearchBar 01030456=style/Theme.Material.Notification 01030457=style/Theme.Material.SearchBar 01030458=style/Theme.Material.Settings.BaseDialog 01030459=style/Theme.Material.Settings.CompactMenu 0103045a=style/Theme.Material.Settings.Dialog 0103045b=style/Theme.Material.Settings.Dialog.Alert 0103045c=style/Theme.Material.Settings.Dialog.BaseAlert 0103045d=style/Theme.Material.Settings.Dialog.Presentation 0103045e=style/Theme.Material.Settings.DialogWhenLarge 0103045f=style/Theme.Material.Settings.DialogWhenLarge.NoActionBar 01030460=style/Theme.Material.Settings.NoActionBar 01030461=style/Theme.Material.Settings.SearchBar 01030462=style/Theme.Material.VoiceInteractionSession 01030463=style/Theme.SearchBar 01030464=style/Theme.Toast 01030465=style/Theme.VoiceInteractionSession 01030466=style/ThemeOverlay.DeviceDefault 01030467=style/ThemeOverlay.DeviceDefault.Accent 01030468=style/ThemeOverlay.DeviceDefault.Accent.Light 01030469=style/ThemeOverlay.DeviceDefault.ActionBar 0103046a=style/ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent 0103046b=style/ThemeOverlay.DeviceDefault.Popup.Light 0103046c=style/ThemeOverlay.Material.BaseDialog 0103046d=style/ThemeOverlay.Material.Dialog.DatePicker 0103046e=style/ThemeOverlay.Material.Dialog.TimePicker 0103046f=style/TimePickerDialog.Material 01030470=style/VectorPointer 01030471=style/Widget.ActionMode 01030472=style/Widget.ActivityChooserView 01030473=style/Widget.Button.Transparent 01030474=style/Widget.CheckedTextView 01030475=style/Widget.CompoundButton.Switch 01030476=style/Widget.DeviceDefault.AbsListView 01030477=style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog 01030478=style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3 01030479=style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3.Confirm 0103047a=style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3.Negative 0103047b=style/Widget.DeviceDefault.CompoundButton.Switch 0103047c=style/Widget.DeviceDefault.ExpandableListView.White 0103047d=style/Widget.DeviceDefault.FragmentBreadCrumbs 0103047e=style/Widget.DeviceDefault.Gallery 0103047f=style/Widget.DeviceDefault.GestureOverlayView 01030480=style/Widget.DeviceDefault.ImageWell 01030481=style/Widget.DeviceDefault.KeyboardView 01030482=style/Widget.DeviceDefault.Light.AbsListView 01030483=style/Widget.DeviceDefault.Light.Button.Borderless 01030484=style/Widget.DeviceDefault.Light.DatePicker 01030485=style/Widget.DeviceDefault.Light.ExpandableListView.White 01030486=style/Widget.DeviceDefault.Light.FragmentBreadCrumbs 01030487=style/Widget.DeviceDefault.Light.Gallery 01030488=style/Widget.DeviceDefault.Light.GestureOverlayView 01030489=style/Widget.DeviceDefault.Light.ImageWell 0103048a=style/Widget.DeviceDefault.Light.ListView.White 0103048b=style/Widget.DeviceDefault.Light.NumberPicker 0103048c=style/Widget.DeviceDefault.Light.PopupWindow.ActionMode 0103048d=style/Widget.DeviceDefault.Light.Spinner.DropDown 0103048e=style/Widget.DeviceDefault.Light.Spinner.DropDown.ActionBar 0103048f=style/Widget.DeviceDefault.Light.TextView.ListSeparator 01030490=style/Widget.DeviceDefault.Light.TimePicker 01030491=style/Widget.DeviceDefault.ListView.White 01030492=style/Widget.DeviceDefault.Notification.MessagingName 01030493=style/Widget.DeviceDefault.Notification.MessagingText 01030494=style/Widget.DeviceDefault.Notification.Text 01030495=style/Widget.DeviceDefault.NumberPicker 01030496=style/Widget.DeviceDefault.PopupWindow.ActionMode 01030497=style/Widget.DeviceDefault.PreferenceFrameLayout 01030498=style/Widget.DeviceDefault.ProgressBar.Inverse 01030499=style/Widget.DeviceDefault.ProgressBar.Large.Inverse 0103049a=style/Widget.DeviceDefault.ProgressBar.Small.Inverse 0103049b=style/Widget.DeviceDefault.QuickContactBadge.WindowLarge 0103049c=style/Widget.DeviceDefault.QuickContactBadge.WindowMedium 0103049d=style/Widget.DeviceDefault.QuickContactBadge.WindowSmall 0103049e=style/Widget.DeviceDefault.QuickContactBadgeSmall.WindowLarge 0103049f=style/Widget.DeviceDefault.QuickContactBadgeSmall.WindowMedium 010304a0=style/Widget.DeviceDefault.QuickContactBadgeSmall.WindowSmall 010304a1=style/Widget.DeviceDefault.Resolver.TabWidget 010304a2=style/Widget.DeviceDefault.Spinner.DropDown 010304a3=style/Widget.DeviceDefault.Spinner.DropDown.ActionBar 010304a4=style/Widget.DeviceDefault.TextSelectHandle 010304a5=style/Widget.DeviceDefault.TextView.ListSeparator 010304a6=style/Widget.DeviceDefault.TimePicker 010304a7=style/Widget.DeviceDefault.Toolbar 010304a8=style/Widget.ExpandableListView.White 010304a9=style/Widget.GenericQuickContactBadge 010304aa=style/Widget.GestureOverlayView 010304ab=style/Widget.GestureOverlayView.White 010304ac=style/Widget.Holo.AbsListView 010304ad=style/Widget.Holo.ActivityChooserView 010304ae=style/Widget.Holo.ButtonBar 010304af=style/Widget.Holo.ButtonBar.Button 010304b0=style/Widget.Holo.CompoundButton 010304b1=style/Widget.Holo.CompoundButton.Switch 010304b2=style/Widget.Holo.ExpandableListView.White 010304b3=style/Widget.Holo.FastScroll 010304b4=style/Widget.Holo.FragmentBreadCrumbs 010304b5=style/Widget.Holo.Gallery 010304b6=style/Widget.Holo.GestureOverlayView 010304b7=style/Widget.Holo.ImageWell 010304b8=style/Widget.Holo.KeyboardView 010304b9=style/Widget.Holo.Light.AbsListView 010304ba=style/Widget.Holo.Light.ActivityChooserView 010304bb=style/Widget.Holo.Light.Button.Borderless 010304bc=style/Widget.Holo.Light.CompoundButton.Switch 010304bd=style/Widget.Holo.Light.DatePicker 010304be=style/Widget.Holo.Light.ExpandableListView.White 010304bf=style/Widget.Holo.Light.FastScroll 010304c0=style/Widget.Holo.Light.FragmentBreadCrumbs 010304c1=style/Widget.Holo.Light.Gallery 010304c2=style/Widget.Holo.Light.GestureOverlayView 010304c3=style/Widget.Holo.Light.ImageWell 010304c4=style/Widget.Holo.Light.KeyboardView 010304c5=style/Widget.Holo.Light.ListView.White 010304c6=style/Widget.Holo.Light.NumberPicker 010304c7=style/Widget.Holo.Light.PopupWindow.ActionMode 010304c8=style/Widget.Holo.Light.QuickContactBadge.WindowLarge 010304c9=style/Widget.Holo.Light.QuickContactBadge.WindowMedium 010304ca=style/Widget.Holo.Light.QuickContactBadge.WindowSmall 010304cb=style/Widget.Holo.Light.QuickContactBadgeSmall.WindowLarge 010304cc=style/Widget.Holo.Light.QuickContactBadgeSmall.WindowMedium 010304cd=style/Widget.Holo.Light.QuickContactBadgeSmall.WindowSmall 010304ce=style/Widget.Holo.Light.SearchView 010304cf=style/Widget.Holo.Light.Spinner.DropDown 010304d0=style/Widget.Holo.Light.Spinner.DropDown.ActionBar 010304d1=style/Widget.Holo.Light.StackView 010304d2=style/Widget.Holo.Light.TextSelectHandle 010304d3=style/Widget.Holo.Light.TextView.ListSeparator 010304d4=style/Widget.Holo.Light.TimePicker 010304d5=style/Widget.Holo.ListView.White 010304d6=style/Widget.Holo.NumberPicker 010304d7=style/Widget.Holo.PopupWindow.ActionMode 010304d8=style/Widget.Holo.PreferenceFrameLayout 010304d9=style/Widget.Holo.ProgressBar.Inverse 010304da=style/Widget.Holo.ProgressBar.Large.Inverse 010304db=style/Widget.Holo.ProgressBar.Small.Inverse 010304dc=style/Widget.Holo.QuickContactBadge.WindowLarge 010304dd=style/Widget.Holo.QuickContactBadge.WindowMedium 010304de=style/Widget.Holo.QuickContactBadge.WindowSmall 010304df=style/Widget.Holo.QuickContactBadgeSmall.WindowLarge 010304e0=style/Widget.Holo.QuickContactBadgeSmall.WindowMedium 010304e1=style/Widget.Holo.QuickContactBadgeSmall.WindowSmall 010304e2=style/Widget.Holo.SearchView 010304e3=style/Widget.Holo.Spinner.DropDown 010304e4=style/Widget.Holo.Spinner.DropDown.ActionBar 010304e5=style/Widget.Holo.StackView 010304e6=style/Widget.Holo.SuggestionButton 010304e7=style/Widget.Holo.SuggestionItem 010304e8=style/Widget.Holo.TabText 010304e9=style/Widget.Holo.TextSelectHandle 010304ea=style/Widget.Holo.TextView.ListSeparator 010304eb=style/Widget.Holo.TimePicker 010304ec=style/Widget.HorizontalScrollView 010304ed=style/Widget.Leanback.Button 010304ee=style/Widget.Leanback.Button.ButtonBar 010304ef=style/Widget.Leanback.Button.ButtonBarGravityStart 010304f0=style/Widget.Leanback.ButtonBar 010304f1=style/Widget.Leanback.DatePicker 010304f2=style/Widget.Leanback.NumberPicker 010304f3=style/Widget.Leanback.TimePicker 010304f4=style/Widget.LockPatternView 010304f5=style/Widget.Magnifier 010304f6=style/Widget.Material.AbsListView 010304f7=style/Widget.Material.ActivityChooserView 010304f8=style/Widget.Material.Button.ButtonBar.AlertDialog 010304f9=style/Widget.Material.CompoundButton 010304fa=style/Widget.Material.ContextPopupMenu 010304fb=style/Widget.Material.ExpandableListView.White 010304fc=style/Widget.Material.FragmentBreadCrumbs 010304fd=style/Widget.Material.Gallery 010304fe=style/Widget.Material.GestureOverlayView 010304ff=style/Widget.Material.ImageWell 01030500=style/Widget.Material.KeyboardView 01030501=style/Widget.Material.Light.AbsListView 01030502=style/Widget.Material.Light.ActivityChooserView 01030503=style/Widget.Material.Light.Button.ButtonBar.AlertDialog 01030504=style/Widget.Material.Light.CompoundButton 01030505=style/Widget.Material.Light.ExpandableListView.White 01030506=style/Widget.Material.Light.FragmentBreadCrumbs 01030507=style/Widget.Material.Light.Gallery 01030508=style/Widget.Material.Light.GestureOverlayView 01030509=style/Widget.Material.Light.ImageWell 0103050a=style/Widget.Material.Light.KeyboardView 0103050b=style/Widget.Material.Light.ListView.White 0103050c=style/Widget.Material.Light.PopupWindow.ActionMode 0103050d=style/Widget.Material.Light.QuickContactBadge.WindowLarge 0103050e=style/Widget.Material.Light.QuickContactBadge.WindowMedium 0103050f=style/Widget.Material.Light.QuickContactBadge.WindowSmall 01030510=style/Widget.Material.Light.QuickContactBadgeSmall.WindowLarge 01030511=style/Widget.Material.Light.QuickContactBadgeSmall.WindowMedium 01030512=style/Widget.Material.Light.QuickContactBadgeSmall.WindowSmall 01030513=style/Widget.Material.Light.SearchView.ActionBar 01030514=style/Widget.Material.Light.Spinner.DropDown 01030515=style/Widget.Material.Light.Spinner.DropDown.ActionBar 01030516=style/Widget.Material.Light.TextSelectHandle 01030517=style/Widget.Material.Light.TextView.ListSeparator 01030518=style/Widget.Material.ListMenuView 01030519=style/Widget.Material.ListView.White 0103051a=style/Widget.Material.Notification.MessagingName 0103051b=style/Widget.Material.Notification.MessagingText 0103051c=style/Widget.Material.Notification.NotificationProgressBar 0103051d=style/Widget.Material.Notification.ProgressBar 0103051e=style/Widget.Material.Notification.Text 0103051f=style/Widget.Material.PopupWindow.ActionMode 01030520=style/Widget.Material.PreferenceFrameLayout 01030521=style/Widget.Material.ProgressBar.Inverse 01030522=style/Widget.Material.ProgressBar.Large.Inverse 01030523=style/Widget.Material.ProgressBar.Small.Inverse 01030524=style/Widget.Material.QuickContactBadge.WindowLarge 01030525=style/Widget.Material.QuickContactBadge.WindowMedium 01030526=style/Widget.Material.QuickContactBadge.WindowSmall 01030527=style/Widget.Material.QuickContactBadgeSmall.WindowLarge 01030528=style/Widget.Material.QuickContactBadgeSmall.WindowMedium 01030529=style/Widget.Material.QuickContactBadgeSmall.WindowSmall 0103052a=style/Widget.Material.Resolver.Tab 0103052b=style/Widget.Material.SearchView.ActionBar 0103052c=style/Widget.Material.Spinner.DropDown 0103052d=style/Widget.Material.Spinner.DropDown.ActionBar 0103052e=style/Widget.Material.SuggestionButton 0103052f=style/Widget.Material.SuggestionItem 01030530=style/Widget.Material.TabText 01030531=style/Widget.Material.TextSelectHandle 01030532=style/Widget.Material.TextView.ListSeparator 01030533=style/Widget.Material3 01030534=style/Widget.Material3.Button 01030535=style/Widget.Material3.Button.Filled 01030536=style/Widget.Material3.Button.FilledTonal 01030537=style/Widget.Material3.Button.Outlined 01030538=style/Widget.Material3.Button.Text 01030539=style/Widget.NumberPicker 0103053a=style/Widget.PreferenceFrameLayout 0103053b=style/Widget.ProgressBar.Small.Title 0103053c=style/Widget.QuickContactBadge 0103053d=style/Widget.QuickContactBadge.WindowLarge 0103053e=style/Widget.QuickContactBadge.WindowMedium 0103053f=style/Widget.QuickContactBadge.WindowSmall 01030540=style/Widget.QuickContactBadgeSmall 01030541=style/Widget.QuickContactBadgeSmall.WindowLarge 01030542=style/Widget.QuickContactBadgeSmall.WindowMedium 01030543=style/Widget.QuickContactBadgeSmall.WindowSmall 01030544=style/Widget.RatingBar.Indicator 01030545=style/Widget.RatingBar.Small 01030546=style/Widget.TextSelectHandle 01030547=style/Widget.TextView.ListSeparator 01030548=style/Widget.TextView.ListSeparator.White 01030549=style/Widget.TimePicker 0103054a=style/Widget.WebTextView 0103054b=style/WindowAnimationStyle.Leanback.Setup 0103054c=style/WindowTitle 0103054d=style/WindowTitle.DeviceDefault 0103054e=style/WindowTitle.Holo 0103054f=style/WindowTitle.Material 01030550=style/WindowTitleBackground 01030551=style/WindowTitleBackground.DeviceDefault 01030552=style/WindowTitleBackground.Holo 01030553=style/WindowTitleBackground.Material 01030554=style/ZoomControls 01030555=style/aerr_list_item 01040000=string/cancel 01040001=string/copy 01040002=string/copyUrl 01040003=string/cut 01040004=string/defaultVoiceMailAlphaTag 01040005=string/defaultMsisdnAlphaTag 01040006=string/emptyPhoneNumber 01040007=string/httpErrorBadUrl 01040008=string/httpErrorUnsupportedScheme 01040009=string/no 0104000a=string/ok 0104000b=string/paste 0104000c=string/search_go 0104000d=string/selectAll 0104000e=string/unknownName 0104000f=string/untitled 01040010=string/VideoView_error_button 01040011=string/VideoView_error_text_unknown 01040012=string/VideoView_error_title 01040013=string/yes 01040014=string/dialog_alert_title 01040015=string/VideoView_error_text_invalid_progressive_playback 01040016=string/selectTextMode 01040017=string/status_bar_notification_info_overflow 01040018=string/fingerprint_icon_content_description 01040019=string/paste_as_plain_text 0104001a=string/autofill 0104001b=string/config_helpPackageNameKey 0104001c=string/config_helpPackageNameValue 0104001d=string/config_helpIntentExtraKey 0104001e=string/config_helpIntentNameKey 0104001f=string/config_feedbackIntentExtraKey 01040020=string/config_feedbackIntentNameKey 01040021=string/config_defaultAssistant 01040022=string/config_defaultBrowser 01040023=string/config_defaultDialer 01040024=string/config_defaultSms 01040025=string/config_defaultCallRedirection 01040026=string/config_defaultCallScreening 01040027=string/config_systemGallery 01040028=string/config_systemAutomotiveCluster 01040029=string/config_systemAutomotiveProjection 0104002a=string/config_systemShell 0104002b=string/config_systemContacts 0104002c=string/config_customMediaKeyDispatcher 0104002d=string/config_customMediaSessionPolicyProvider 0104002e=string/config_systemSpeechRecognizer 0104002f=string/config_systemWifiCoexManager 01040030=string/config_systemWellbeing 01040031=string/config_systemTelevisionNotificationHandler 01040032=string/config_systemUiIntelligence 01040033=string/config_systemAmbientAudioIntelligence 01040034=string/config_systemAudioIntelligence 01040035=string/config_systemNotificationIntelligence 01040036=string/config_systemTextIntelligence 01040037=string/config_systemVisualIntelligence 01040038=string/config_systemActivityRecognizer 01040039=string/config_systemCompanionDeviceProvider 0104003a=string/config_systemUi 0104003b=string/config_defaultRingtoneVibrationSound 0104003c=string/config_systemSupervision 0104003d=string/config_devicePolicyManagement 0104003e=string/config_systemAppProtectionService 0104003f=string/config_systemAutomotiveCalendarSyncManager 01040040=string/config_defaultAutomotiveNavigation 01040041=string/safety_protection_display_text 01040042=string/config_systemSettingsIntelligence 01040043=string/config_systemBluetoothStack 01040044=string/config_systemWearHealthService 01040045=string/config_defaultNotes 01040046=string/config_systemFinancedDeviceController 01040047=string/config_systemCallStreaming 01040048=string/config_defaultRetailDemo 01040049=string/config_defaultWallet 0104004a=string/config_systemDependencyInstaller 0104004b=string/config_systemVendorIntelligence 0104004c=string/BaMmi 0104004d=string/CLIRDefaultOffNextCallOff 0104004e=string/CLIRDefaultOffNextCallOn 0104004f=string/CLIRDefaultOnNextCallOff 01040050=string/CLIRDefaultOnNextCallOn 01040051=string/CLIRPermanent 01040052=string/CfMmi 01040053=string/ClipMmi 01040054=string/ClirMmi 01040055=string/CndMmi 01040056=string/CnipMmi 01040057=string/CnirMmi 01040058=string/ColpMmi 01040059=string/ColrMmi 0104005a=string/CwMmi 0104005b=string/DndMmi 0104005c=string/EmergencyCallWarningSummary 0104005d=string/EmergencyCallWarningTitle 0104005e=string/Midnight 0104005f=string/NetworkPreferenceSwitchSummary 01040060=string/NetworkPreferenceSwitchTitle 01040061=string/Noon 01040062=string/PERSOSUBSTATE_RUIM_CORPORATE_ENTRY 01040063=string/PERSOSUBSTATE_RUIM_CORPORATE_ERROR 01040064=string/PERSOSUBSTATE_RUIM_CORPORATE_IN_PROGRESS 01040065=string/PERSOSUBSTATE_RUIM_CORPORATE_PUK_ENTRY 01040066=string/PERSOSUBSTATE_RUIM_CORPORATE_PUK_ERROR 01040067=string/PERSOSUBSTATE_RUIM_CORPORATE_PUK_IN_PROGRESS 01040068=string/PERSOSUBSTATE_RUIM_CORPORATE_PUK_SUCCESS 01040069=string/PERSOSUBSTATE_RUIM_CORPORATE_SUCCESS 0104006a=string/PERSOSUBSTATE_RUIM_HRPD_ENTRY 0104006b=string/PERSOSUBSTATE_RUIM_HRPD_ERROR 0104006c=string/PERSOSUBSTATE_RUIM_HRPD_IN_PROGRESS 0104006d=string/PERSOSUBSTATE_RUIM_HRPD_PUK_ENTRY 0104006e=string/PERSOSUBSTATE_RUIM_HRPD_PUK_ERROR 0104006f=string/PERSOSUBSTATE_RUIM_HRPD_PUK_IN_PROGRESS 01040070=string/PERSOSUBSTATE_RUIM_HRPD_PUK_SUCCESS 01040071=string/PERSOSUBSTATE_RUIM_HRPD_SUCCESS 01040072=string/PERSOSUBSTATE_RUIM_NETWORK1_ENTRY 01040073=string/PERSOSUBSTATE_RUIM_NETWORK1_ERROR 01040074=string/PERSOSUBSTATE_RUIM_NETWORK1_IN_PROGRESS 01040075=string/PERSOSUBSTATE_RUIM_NETWORK1_PUK_ENTRY 01040076=string/PERSOSUBSTATE_RUIM_NETWORK1_PUK_ERROR 01040077=string/PERSOSUBSTATE_RUIM_NETWORK1_PUK_IN_PROGRESS 01040078=string/PERSOSUBSTATE_RUIM_NETWORK1_PUK_SUCCESS 01040079=string/PERSOSUBSTATE_RUIM_NETWORK1_SUCCESS 0104007a=string/PERSOSUBSTATE_RUIM_NETWORK2_ENTRY 0104007b=string/PERSOSUBSTATE_RUIM_NETWORK2_ERROR 0104007c=string/PERSOSUBSTATE_RUIM_NETWORK2_IN_PROGRESS 0104007d=string/PERSOSUBSTATE_RUIM_NETWORK2_PUK_ENTRY 0104007e=string/PERSOSUBSTATE_RUIM_NETWORK2_PUK_ERROR 0104007f=string/PERSOSUBSTATE_RUIM_NETWORK2_PUK_IN_PROGRESS 01040080=string/PERSOSUBSTATE_RUIM_NETWORK2_PUK_SUCCESS 01040081=string/PERSOSUBSTATE_RUIM_NETWORK2_SUCCESS 01040082=string/PERSOSUBSTATE_RUIM_RUIM_ENTRY 01040083=string/PERSOSUBSTATE_RUIM_RUIM_ERROR 01040084=string/PERSOSUBSTATE_RUIM_RUIM_IN_PROGRESS 01040085=string/PERSOSUBSTATE_RUIM_RUIM_PUK_ENTRY 01040086=string/PERSOSUBSTATE_RUIM_RUIM_PUK_ERROR 01040087=string/PERSOSUBSTATE_RUIM_RUIM_PUK_IN_PROGRESS 01040088=string/PERSOSUBSTATE_RUIM_RUIM_PUK_SUCCESS 01040089=string/PERSOSUBSTATE_RUIM_RUIM_SUCCESS 0104008a=string/PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_ENTRY 0104008b=string/PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_ERROR 0104008c=string/PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_IN_PROGRESS 0104008d=string/PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_PUK_ENTRY 0104008e=string/PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_PUK_ERROR 0104008f=string/PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_PUK_IN_PROGRESS 01040090=string/PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_PUK_SUCCESS 01040091=string/PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_SUCCESS 01040092=string/PERSOSUBSTATE_SIM_CORPORATE_ENTRY 01040093=string/PERSOSUBSTATE_SIM_CORPORATE_ERROR 01040094=string/PERSOSUBSTATE_SIM_CORPORATE_IN_PROGRESS 01040095=string/PERSOSUBSTATE_SIM_CORPORATE_PUK_ENTRY 01040096=string/PERSOSUBSTATE_SIM_CORPORATE_PUK_ERROR 01040097=string/PERSOSUBSTATE_SIM_CORPORATE_PUK_IN_PROGRESS 01040098=string/PERSOSUBSTATE_SIM_CORPORATE_PUK_SUCCESS 01040099=string/PERSOSUBSTATE_SIM_CORPORATE_SUCCESS 0104009a=string/PERSOSUBSTATE_SIM_ICCID_ENTRY 0104009b=string/PERSOSUBSTATE_SIM_ICCID_ERROR 0104009c=string/PERSOSUBSTATE_SIM_ICCID_IN_PROGRESS 0104009d=string/PERSOSUBSTATE_SIM_ICCID_SUCCESS 0104009e=string/PERSOSUBSTATE_SIM_IMPI_ENTRY 0104009f=string/PERSOSUBSTATE_SIM_IMPI_ERROR 010400a0=string/PERSOSUBSTATE_SIM_IMPI_IN_PROGRESS 010400a1=string/PERSOSUBSTATE_SIM_IMPI_SUCCESS 010400a2=string/PERSOSUBSTATE_SIM_NETWORK_ENTRY 010400a3=string/PERSOSUBSTATE_SIM_NETWORK_ERROR 010400a4=string/PERSOSUBSTATE_SIM_NETWORK_IN_PROGRESS 010400a5=string/PERSOSUBSTATE_SIM_NETWORK_PUK_ENTRY 010400a6=string/PERSOSUBSTATE_SIM_NETWORK_PUK_ERROR 010400a7=string/PERSOSUBSTATE_SIM_NETWORK_PUK_IN_PROGRESS 010400a8=string/PERSOSUBSTATE_SIM_NETWORK_PUK_SUCCESS 010400a9=string/PERSOSUBSTATE_SIM_NETWORK_SUBSET_ENTRY 010400aa=string/PERSOSUBSTATE_SIM_NETWORK_SUBSET_ERROR 010400ab=string/PERSOSUBSTATE_SIM_NETWORK_SUBSET_IN_PROGRESS 010400ac=string/PERSOSUBSTATE_SIM_NETWORK_SUBSET_PUK_ENTRY 010400ad=string/PERSOSUBSTATE_SIM_NETWORK_SUBSET_PUK_ERROR 010400ae=string/PERSOSUBSTATE_SIM_NETWORK_SUBSET_PUK_IN_PROGRESS 010400af=string/PERSOSUBSTATE_SIM_NETWORK_SUBSET_PUK_SUCCESS 010400b0=string/PERSOSUBSTATE_SIM_NETWORK_SUBSET_SUCCESS 010400b1=string/PERSOSUBSTATE_SIM_NETWORK_SUCCESS 010400b2=string/PERSOSUBSTATE_SIM_NS_SP_ENTRY 010400b3=string/PERSOSUBSTATE_SIM_NS_SP_ERROR 010400b4=string/PERSOSUBSTATE_SIM_NS_SP_IN_PROGRESS 010400b5=string/PERSOSUBSTATE_SIM_NS_SP_SUCCESS 010400b6=string/PERSOSUBSTATE_SIM_SERVICE_PROVIDER_ENTRY 010400b7=string/PERSOSUBSTATE_SIM_SERVICE_PROVIDER_ERROR 010400b8=string/PERSOSUBSTATE_SIM_SERVICE_PROVIDER_IN_PROGRESS 010400b9=string/PERSOSUBSTATE_SIM_SERVICE_PROVIDER_PUK_ENTRY 010400ba=string/PERSOSUBSTATE_SIM_SERVICE_PROVIDER_PUK_ERROR 010400bb=string/PERSOSUBSTATE_SIM_SERVICE_PROVIDER_PUK_IN_PROGRESS 010400bc=string/PERSOSUBSTATE_SIM_SERVICE_PROVIDER_PUK_SUCCESS 010400bd=string/PERSOSUBSTATE_SIM_SERVICE_PROVIDER_SUCCESS 010400be=string/PERSOSUBSTATE_SIM_SIM_ENTRY 010400bf=string/PERSOSUBSTATE_SIM_SIM_ERROR 010400c0=string/PERSOSUBSTATE_SIM_SIM_IN_PROGRESS 010400c1=string/PERSOSUBSTATE_SIM_SIM_PUK_ENTRY 010400c2=string/PERSOSUBSTATE_SIM_SIM_PUK_ERROR 010400c3=string/PERSOSUBSTATE_SIM_SIM_PUK_IN_PROGRESS 010400c4=string/PERSOSUBSTATE_SIM_SIM_PUK_SUCCESS 010400c5=string/PERSOSUBSTATE_SIM_SIM_SUCCESS 010400c6=string/PERSOSUBSTATE_SIM_SPN_ENTRY 010400c7=string/PERSOSUBSTATE_SIM_SPN_ERROR 010400c8=string/PERSOSUBSTATE_SIM_SPN_IN_PROGRESS 010400c9=string/PERSOSUBSTATE_SIM_SPN_SUCCESS 010400ca=string/PERSOSUBSTATE_SIM_SP_EHPLMN_ENTRY 010400cb=string/PERSOSUBSTATE_SIM_SP_EHPLMN_ERROR 010400cc=string/PERSOSUBSTATE_SIM_SP_EHPLMN_IN_PROGRESS 010400cd=string/PERSOSUBSTATE_SIM_SP_EHPLMN_SUCCESS 010400ce=string/PinMmi 010400cf=string/PwdMmi 010400d0=string/RestrictedOnAllVoiceTitle 010400d1=string/RestrictedOnDataTitle 010400d2=string/RestrictedOnEmergencyTitle 010400d3=string/RestrictedOnNormalTitle 010400d4=string/RestrictedStateContent 010400d5=string/RestrictedStateContentMsimTemplate 010400d6=string/RuacMmi 010400d7=string/SetupCallDefault 010400d8=string/ThreeWCMmi 010400d9=string/accept 010400da=string/accessibility_autoclick_double_click 010400db=string/accessibility_autoclick_drag 010400dc=string/accessibility_autoclick_left_click 010400dd=string/accessibility_autoclick_pause 010400de=string/accessibility_autoclick_position 010400df=string/accessibility_autoclick_right_click 010400e0=string/accessibility_autoclick_scroll 010400e1=string/accessibility_autoclick_scroll_down 010400e2=string/accessibility_autoclick_scroll_exit 010400e3=string/accessibility_autoclick_scroll_left 010400e4=string/accessibility_autoclick_scroll_panel_title 010400e5=string/accessibility_autoclick_scroll_right 010400e6=string/accessibility_autoclick_scroll_up 010400e7=string/accessibility_autoclick_type_settings_panel_title 010400e8=string/accessibility_binding_label 010400e9=string/accessibility_button_instructional_text 010400ea=string/accessibility_button_prompt_text 010400eb=string/accessibility_dialog_button_allow 010400ec=string/accessibility_dialog_button_deny 010400ed=string/accessibility_dialog_button_uninstall 010400ee=string/accessibility_dialog_touch_filtered_warning 010400ef=string/accessibility_edit_shortcut_menu_button_title 010400f0=string/accessibility_edit_shortcut_menu_volume_title 010400f1=string/accessibility_enable_service_title 010400f2=string/accessibility_gesture_3finger_instructional_text 010400f3=string/accessibility_gesture_3finger_prompt_text 010400f4=string/accessibility_gesture_instructional_text 010400f5=string/accessibility_gesture_prompt_text 010400f6=string/accessibility_label_clone_profile 010400f7=string/accessibility_label_communal_profile 010400f8=string/accessibility_label_managed_profile 010400f9=string/accessibility_label_private_profile 010400fa=string/accessibility_magnification_chooser_text 010400fb=string/accessibility_select_shortcut_menu_title 010400fc=string/accessibility_service_action_perform_description 010400fd=string/accessibility_service_action_perform_title 010400fe=string/accessibility_service_screen_control_description 010400ff=string/accessibility_service_screen_control_title 01040100=string/accessibility_service_warning_description 01040101=string/accessibility_shortcut_disabling_service 01040102=string/accessibility_shortcut_enabling_service 01040103=string/accessibility_shortcut_menu_item_status_off 01040104=string/accessibility_shortcut_menu_item_status_on 01040105=string/accessibility_shortcut_multiple_service_list 01040106=string/accessibility_shortcut_multiple_service_warning 01040107=string/accessibility_shortcut_multiple_service_warning_title 01040108=string/accessibility_shortcut_off 01040109=string/accessibility_shortcut_on 0104010a=string/accessibility_shortcut_single_service_warning 0104010b=string/accessibility_shortcut_single_service_warning_title 0104010c=string/accessibility_shortcut_spoken_feedback 0104010d=string/accessibility_shortcut_toogle_warning 0104010e=string/accessibility_shortcut_warning_dialog_title 0104010f=string/accessibility_system_action_back_label 01040110=string/accessibility_system_action_dismiss_notification_shade 01040111=string/accessibility_system_action_dpad_center_label 01040112=string/accessibility_system_action_dpad_down_label 01040113=string/accessibility_system_action_dpad_left_label 01040114=string/accessibility_system_action_dpad_right_label 01040115=string/accessibility_system_action_dpad_up_label 01040116=string/accessibility_system_action_hardware_a11y_shortcut_label 01040117=string/accessibility_system_action_headset_hook_label 01040118=string/accessibility_system_action_home_label 01040119=string/accessibility_system_action_lock_screen_label 0104011a=string/accessibility_system_action_media_play_pause_label 0104011b=string/accessibility_system_action_menu_label 0104011c=string/accessibility_system_action_notifications_label 0104011d=string/accessibility_system_action_on_screen_a11y_shortcut_chooser_label 0104011e=string/accessibility_system_action_on_screen_a11y_shortcut_label 0104011f=string/accessibility_system_action_power_dialog_label 01040120=string/accessibility_system_action_quick_settings_label 01040121=string/accessibility_system_action_recents_label 01040122=string/accessibility_system_action_screenshot_label 01040123=string/accessibility_uncheck_legacy_item_warning 01040124=string/action_bar_home_description 01040125=string/action_bar_home_description_format 01040126=string/action_bar_home_subtitle_description_format 01040127=string/action_bar_up_description 01040128=string/action_menu_overflow_description 01040129=string/action_mode_done 0104012a=string/activity_chooser_view_dialog_title_default 0104012b=string/activity_chooser_view_see_all 0104012c=string/activity_list_empty 0104012d=string/activity_resolver_use_always 0104012e=string/activity_resolver_use_once 0104012f=string/activity_resolver_work_profiles_support 01040130=string/activitychooserview_choose_application 01040131=string/activitychooserview_choose_application_error 01040132=string/adb_active_notification_message 01040133=string/adb_active_notification_title 01040134=string/adb_debugging_notification_channel_tv 01040135=string/adbwifi_active_notification_message 01040136=string/adbwifi_active_notification_title 01040137=string/addToDictionary 01040138=string/add_account_button_label 01040139=string/add_account_label 0104013a=string/aerr_application 0104013b=string/aerr_application_repeated 0104013c=string/aerr_close 0104013d=string/aerr_close_app 0104013e=string/aerr_mute 0104013f=string/aerr_process 01040140=string/aerr_process_repeated 01040141=string/aerr_report 01040142=string/aerr_restart 01040143=string/aerr_wait 01040144=string/alert_windows_notification_channel_group_name 01040145=string/alert_windows_notification_channel_name 01040146=string/alert_windows_notification_message 01040147=string/alert_windows_notification_title 01040148=string/alert_windows_notification_turn_off_action 01040149=string/all_apps_group_a11y_title 0104014a=string/allow 0104014b=string/alternate_eri_file 0104014c=string/alternative_face_setup_notification_content 0104014d=string/alternative_fp_setup_notification_content 0104014e=string/alternative_unlock_setup_notification_title 0104014f=string/alwaysUse 01040150=string/android_preparing_apk 01040151=string/android_start_title 01040152=string/android_system_label 01040153=string/android_upgrading_complete 01040154=string/android_upgrading_notification_title 01040155=string/android_upgrading_starting_apps 01040156=string/android_upgrading_title 01040157=string/anr_activity_application 01040158=string/anr_activity_process 01040159=string/anr_application_process 0104015a=string/anr_process 0104015b=string/anr_title 0104015c=string/app_blocked_message 0104015d=string/app_blocked_title 0104015e=string/app_category_accessibility 0104015f=string/app_category_audio 01040160=string/app_category_game 01040161=string/app_category_image 01040162=string/app_category_maps 01040163=string/app_category_news 01040164=string/app_category_productivity 01040165=string/app_category_social 01040166=string/app_category_video 01040167=string/app_info 01040168=string/app_not_found 01040169=string/app_running_notification_text 0104016a=string/app_running_notification_title 0104016b=string/app_streaming_blocked_message 0104016c=string/app_streaming_blocked_message_for_fingerprint_dialog 0104016d=string/app_streaming_blocked_message_for_permission_request 0104016e=string/app_streaming_blocked_message_for_settings_dialog 0104016f=string/app_streaming_blocked_title 01040170=string/app_streaming_blocked_title_for_camera_dialog 01040171=string/app_streaming_blocked_title_for_fingerprint_dialog 01040172=string/app_streaming_blocked_title_for_microphone_dialog 01040173=string/app_streaming_blocked_title_for_permission_dialog 01040174=string/app_streaming_blocked_title_for_playstore_dialog 01040175=string/app_streaming_blocked_title_for_settings_dialog 01040176=string/app_suspended_default_message 01040177=string/app_suspended_more_details 01040178=string/app_suspended_title 01040179=string/app_suspended_unsuspend_message 0104017a=string/app_upgrading_toast 0104017b=string/as_app_forced_to_restricted_bucket 0104017c=string/auto_data_switch_content 0104017d=string/auto_data_switch_title 0104017e=string/autoclick_feature_name 0104017f=string/autofill_continue_yes 01040180=string/autofill_error_cannot_autofill 01040181=string/autofill_picker_accessibility_title 01040182=string/autofill_picker_no_suggestions 01040183=string/autofill_picker_some_suggestions 01040184=string/autofill_save_accessibility_title 01040185=string/autofill_save_never 01040186=string/autofill_save_no 01040187=string/autofill_save_notnow 01040188=string/autofill_save_title 01040189=string/autofill_save_title_with_2types 0104018a=string/autofill_save_title_with_3types 0104018b=string/autofill_save_title_with_type 0104018c=string/autofill_save_type_address 0104018d=string/autofill_save_type_credit_card 0104018e=string/autofill_save_type_debit_card 0104018f=string/autofill_save_type_email_address 01040190=string/autofill_save_type_generic_card 01040191=string/autofill_save_type_password 01040192=string/autofill_save_type_payment_card 01040193=string/autofill_save_type_username 01040194=string/autofill_save_yes 01040195=string/autofill_update_title 01040196=string/autofill_update_title_with_2types 01040197=string/autofill_update_title_with_3types 01040198=string/autofill_update_title_with_type 01040199=string/autofill_update_yes 0104019a=string/autofill_window_title 0104019b=string/back_button_label 0104019c=string/badPin 0104019d=string/badPuk 0104019e=string/battery_saver_charged_notification_summary 0104019f=string/battery_saver_description 010401a0=string/battery_saver_description_with_learn_more 010401a1=string/battery_saver_notification_channel_name 010401a2=string/battery_saver_off_notification_title 010401a3=string/beforeOneMonthDurationPast 010401a4=string/bg_user_sound_notification_button_mute 010401a5=string/bg_user_sound_notification_button_switch_user 010401a6=string/bg_user_sound_notification_message 010401a7=string/bg_user_sound_notification_title_alarm 010401a8=string/biometric_app_setting_name 010401a9=string/biometric_dangling_notification_action_not_now 010401aa=string/biometric_dangling_notification_action_set_up 010401ab=string/biometric_dialog_default_subtitle 010401ac=string/biometric_dialog_default_title 010401ad=string/biometric_error_canceled 010401ae=string/biometric_error_device_not_secured 010401af=string/biometric_error_generic 010401b0=string/biometric_error_hw_unavailable 010401b1=string/biometric_error_user_canceled 010401b2=string/biometric_face_not_recognized 010401b3=string/biometric_not_recognized 010401b4=string/biometric_or_screen_lock_app_setting_name 010401b5=string/biometric_or_screen_lock_dialog_default_subtitle 010401b6=string/bluetooth_a2dp_audio_route_id 010401b7=string/bluetooth_a2dp_audio_route_name 010401b8=string/bluetooth_airplane_mode_toast 010401b9=string/bugreport_countdown 010401ba=string/bugreport_message 010401bb=string/bugreport_option_full_summary 010401bc=string/bugreport_option_full_title 010401bd=string/bugreport_option_interactive_summary 010401be=string/bugreport_option_interactive_title 010401bf=string/bugreport_screenshot_failure_toast 010401c0=string/bugreport_screenshot_success_toast 010401c1=string/bugreport_status 010401c2=string/bugreport_title 010401c3=string/byteShort 010401c4=string/call_notification_answer_action 010401c5=string/call_notification_answer_video_action 010401c6=string/call_notification_decline_action 010401c7=string/call_notification_hang_up_action 010401c8=string/call_notification_incoming_text 010401c9=string/call_notification_ongoing_text 010401ca=string/call_notification_screening_text 010401cb=string/candidates_style 010401cc=string/capability_desc_canCaptureFingerprintGestures 010401cd=string/capability_desc_canControlMagnification 010401ce=string/capability_desc_canPerformGestures 010401cf=string/capability_desc_canRequestFilterKeyEvents 010401d0=string/capability_desc_canRequestTouchExploration 010401d1=string/capability_desc_canRetrieveWindowContent 010401d2=string/capability_desc_canTakeScreenshot 010401d3=string/capability_title_canCaptureFingerprintGestures 010401d4=string/capability_title_canControlMagnification 010401d5=string/capability_title_canPerformGestures 010401d6=string/capability_title_canRequestFilterKeyEvents 010401d7=string/capability_title_canRequestTouchExploration 010401d8=string/capability_title_canRetrieveWindowContent 010401d9=string/capability_title_canTakeScreenshot 010401da=string/capital_off 010401db=string/capital_on 010401dc=string/car_loading_profile 010401dd=string/car_mode_disable_notification_message 010401de=string/car_mode_disable_notification_title 010401df=string/carrier_app_notification_text 010401e0=string/carrier_app_notification_title 010401e1=string/cfTemplateForwarded 010401e2=string/cfTemplateForwardedTime 010401e3=string/cfTemplateNotForwarded 010401e4=string/cfTemplateRegistered 010401e5=string/cfTemplateRegisteredTime 010401e6=string/checked 010401e7=string/chooseActivity 010401e8=string/chooseUsbActivity 010401e9=string/choose_account_label 010401ea=string/chooser_all_apps_button_label 010401eb=string/chooser_no_direct_share_targets 010401ec=string/chooser_wallpaper 010401ed=string/clearDefaultHintMsg 010401ee=string/clone_profile_label_badge 010401ef=string/close_button_text 010401f0=string/color_correction_feature_name 010401f1=string/color_inversion_feature_name 010401f2=string/common_last_name_prefixes 010401f3=string/common_name 010401f4=string/common_name_conjunctions 010401f5=string/common_name_prefixes 010401f6=string/common_name_suffixes 010401f7=string/concurrent_display_notification_active_content 010401f8=string/concurrent_display_notification_active_title 010401f9=string/concurrent_display_notification_name 010401fa=string/concurrent_display_notification_power_save_content 010401fb=string/concurrent_display_notification_power_save_title 010401fc=string/concurrent_display_notification_thermal_content 010401fd=string/concurrent_display_notification_thermal_title 010401fe=string/condition_provider_service_binding_label 010401ff=string/conference_call 01040200=string/config_UsbDeviceConnectionHandling_component 01040201=string/config_accountTypeToKeepFirstAccount 01040202=string/config_activityRecognitionHardwarePackageName 01040203=string/config_ambientContextEventArrayExtraKey 01040204=string/config_ambientContextPackageNameExtraKey 01040205=string/config_appsAuthorizedForSharedAccounts 01040206=string/config_appsNotReportingCrashes 01040207=string/config_bandwidthEstimateSource 01040208=string/config_batterySaverDeviceSpecificConfig 01040209=string/config_batterySaverScheduleProvider 0104020a=string/config_batterymeterBoltPath 0104020b=string/config_batterymeterErrorPerimeterPath 0104020c=string/config_batterymeterFillMask 0104020d=string/config_batterymeterPerimeterPath 0104020e=string/config_batterymeterPowersavePath 0104020f=string/config_biometric_prompt_ui_package 01040210=string/config_bodyFontFamily 01040211=string/config_bodyFontFamilyMedium 01040212=string/config_cameraLaunchGestureSensorStringType 01040213=string/config_cameraLiftTriggerSensorStringType 01040214=string/config_cameraShutterSound 01040215=string/config_carrierAppInstallDialogComponent 01040216=string/config_chooseAccountActivity 01040217=string/config_chooseTypeAndAccountActivity 01040218=string/config_chooserActivity 01040219=string/config_clockFontFamily 0104021a=string/config_companionDeviceManagerPackage 0104021b=string/config_controlsPackage 0104021c=string/config_credentialManagerReceiverComponent 0104021d=string/config_customAdbPublicKeyConfirmationComponent 0104021e=string/config_customAdbPublicKeyConfirmationSecondaryUserComponent 0104021f=string/config_customAdbWifiNetworkConfirmationComponent 01040220=string/config_customAdbWifiNetworkConfirmationSecondaryUserComponent 01040221=string/config_customCountryDetector 01040222=string/config_customResolverActivity 01040223=string/config_customVpnAlwaysOnDisconnectedDialogComponent 01040224=string/config_customVpnConfirmDialogComponent 01040225=string/config_dataUsageSummaryComponent 01040226=string/config_datause_iface 01040227=string/config_defaultAccessibilityNotificationSound 01040228=string/config_defaultAccessibilityService 01040229=string/config_defaultAmbientContextConsentComponent 0104022a=string/config_defaultAmbientContextDetectionService 0104022b=string/config_defaultAppPredictionService 0104022c=string/config_defaultAssistantAccessComponent 0104022d=string/config_defaultAttentionService 0104022e=string/config_defaultAugmentedAutofillService 0104022f=string/config_defaultAutofillService 01040230=string/config_defaultBugReportHandlerApp 01040231=string/config_defaultCaptivePortalLoginPackageName 01040232=string/config_defaultContentCaptureService 01040233=string/config_defaultContentProtectionService 01040234=string/config_defaultContentSuggestionsService 01040235=string/config_defaultContextualSearchEnabled 01040236=string/config_defaultContextualSearchKey 01040237=string/config_defaultContextualSearchLegacyEnabled 01040238=string/config_defaultContextualSearchPackageName 01040239=string/config_defaultCredentialManagerAutofillService 0104023a=string/config_defaultCredentialManagerHybridService 0104023b=string/config_defaultDisplayCompatHostActivity 0104023c=string/config_defaultDndAccessPackages 0104023d=string/config_defaultDndDeniedPackages 0104023e=string/config_defaultDockManagerPackageName 0104023f=string/config_defaultFieldClassificationService 01040240=string/config_defaultHealthConnectApp 01040241=string/config_defaultLaunchOnPrivateDisplayRouterActivity 01040242=string/config_defaultListenerAccessPackages 01040243=string/config_defaultModuleMetadataProvider 01040244=string/config_defaultMusicRecognitionService 01040245=string/config_defaultNearbyFastPairSettingsDevicesComponent 01040246=string/config_defaultNearbySharingComponent 01040247=string/config_defaultNearbySharingSliceUri 01040248=string/config_defaultNetworkRecommendationProviderPackage 01040249=string/config_defaultNetworkScorerPackageName 0104024a=string/config_defaultOnDeviceSpeechRecognitionService 0104024b=string/config_defaultProfcollectReportUploaderAction 0104024c=string/config_defaultProfcollectReportUploaderApp 0104024d=string/config_defaultQrCodeComponent 0104024e=string/config_defaultRotationResolverService 0104024f=string/config_defaultSearchSelectorPackageName 01040250=string/config_defaultSearchUiService 01040251=string/config_defaultSelectToSpeakService 01040252=string/config_defaultShutdownVibrationFile 01040253=string/config_defaultSmartspaceService 01040254=string/config_defaultSupervisionProfileOwnerComponent 01040255=string/config_defaultSystemCaptionsManagerService 01040256=string/config_defaultTextClassifierPackage 01040257=string/config_defaultTranslationService 01040258=string/config_defaultTrustAgent 01040259=string/config_defaultWallpaperEffectsGenerationService 0104025a=string/config_defaultWearableSensingConsentComponent 0104025b=string/config_defaultWearableSensingService 0104025c=string/config_defaultWellbeingPackage 0104025d=string/config_default_dns_server 0104025e=string/config_deviceConfiguratorPackageName 0104025f=string/config_devicePolicyManagementUpdater 01040260=string/config_deviceProvisioningPackage 01040261=string/config_deviceSpecificAudioService 01040262=string/config_deviceSpecificDevicePolicyManagerService 01040263=string/config_deviceSpecificDeviceStatePolicyProvider 01040264=string/config_deviceSpecificDisplayAreaPolicyProvider 01040265=string/config_deviceSpecificInputMethodManagerService 01040266=string/config_displayLightSensorType 01040267=string/config_displayWhiteBalanceColorTemperatureSensorName 01040268=string/config_display_features 01040269=string/config_doublePressOnPowerTargetActivity 0104026a=string/config_doubleTouchGestureEnableFile 0104026b=string/config_dozeComponent 0104026c=string/config_dozeDoubleTapSensorType 0104026d=string/config_dozeLongPressSensorType 0104026e=string/config_dozeTapSensorType 0104026f=string/config_dozeUdfpsLongPressSensorType 01040270=string/config_dreamsDefaultComponent 01040271=string/config_emergency_call_number 01040272=string/config_emergency_dialer_package 01040273=string/config_ethernet_iface_regex 01040274=string/config_ethernet_tcp_buffers 01040275=string/config_extensionFallbackPackageName 01040276=string/config_extensionFallbackServiceName 01040277=string/config_factoryResetPackage 01040278=string/config_fallbackCredentialManagerDialogComponent 01040279=string/config_fingerprintFrrTargetComponent 0104027a=string/config_foldedArea 0104027b=string/config_forceVoiceInteractionServicePackage 0104027c=string/config_fusedLocationProviderPackageName 0104027d=string/config_geocoderProviderPackageName 0104027e=string/config_geofenceProviderPackageName 0104027f=string/config_globalAppSearchDataQuerierPackage 01040280=string/config_gnssAssistanceProviderPackageName 01040281=string/config_gnssLocationProviderPackageName 01040282=string/config_hapticFeedbackCustomizationFile 01040283=string/config_hdmiCecActiveSourceLostActivity 01040284=string/config_hdmiCecSetMenuLanguageActivity 01040285=string/config_headlineFontFamily 01040286=string/config_headlineFontFamilyMedium 01040287=string/config_headlineFontFeatureSettings 01040288=string/config_healthConnectMigratorPackageName 01040289=string/config_help_url_action_disabled_by_advanced_protection 0104028a=string/config_iccHotswapPromptForRestartDialogComponent 0104028b=string/config_icon_mask 0104028c=string/config_inCallNotificationSound 0104028d=string/config_incidentReportApproverPackage 0104028e=string/config_inputEventCompatProcessorOverrideClassName 0104028f=string/config_intrusionDetectionEventTransport 01040290=string/config_isoImagePath 01040291=string/config_keyguardComponent 01040292=string/config_mainBuiltInDisplayCutout 01040293=string/config_mainBuiltInDisplayCutoutRectApproximation 01040294=string/config_mainDisplayShape 01040295=string/config_managed_provisioning_package 01040296=string/config_mediaProjectionPermissionDialogComponent 01040297=string/config_misprovisionedBrandValue 01040298=string/config_misprovisionedDeviceModel 01040299=string/config_mms_user_agent 0104029a=string/config_mms_user_agent_profile_url 0104029b=string/config_mobile_hotspot_provision_app_no_ui 0104029c=string/config_mobile_hotspot_provision_response 0104029d=string/config_mt_sms_polling_text 0104029e=string/config_networkLocationProviderPackageName 0104029f=string/config_networkOverLimitComponent 010402a0=string/config_notificationAccessConfirmationActivity 010402a1=string/config_notificationHandlerPackage 010402a2=string/config_oemCredentialManagerDialogComponent 010402a3=string/config_oem_enabled_satellite_s2cell_file 010402a4=string/config_oem_enabled_satellite_sos_handover_app 010402a5=string/config_onDeviceIntelligenceModelLoadedBroadcastKey 010402a6=string/config_onDeviceIntelligenceModelUnloadedBroadcastKey 010402a7=string/config_overrideComponentUiPackage 010402a8=string/config_packagedKeyboardName 010402a9=string/config_pdp_reject_dialog_title 010402aa=string/config_pdp_reject_multi_conn_to_same_pdn_not_allowed 010402ab=string/config_pdp_reject_service_not_subscribed 010402ac=string/config_pdp_reject_user_authentication_failed 010402ad=string/config_persistentDataPackageName 010402ae=string/config_platformVpnConfirmDialogComponent 010402af=string/config_pluginsProviderJarPath 010402b0=string/config_pointing_ui_class 010402b1=string/config_pointing_ui_package 010402b2=string/config_populationDensityProviderPackageName 010402b3=string/config_powerSaveModeChangedListenerPackage 010402b4=string/config_powerStatsThrottlePeriods 010402b5=string/config_primaryLocationTimeZoneProviderPackageName 010402b6=string/config_primaryShortPressTargetActivity 010402b7=string/config_qualified_networks_service_class 010402b8=string/config_qualified_networks_service_package 010402b9=string/config_quickPickupSensorType 010402ba=string/config_radio_access_family 010402bb=string/config_rawContactsLocalAccountName 010402bc=string/config_rawContactsLocalAccountType 010402bd=string/config_rearDisplayPhysicalAddress 010402be=string/config_recentsComponentName 010402bf=string/config_retailDemoPackage 010402c0=string/config_retailDemoPackageSignature 010402c1=string/config_satellite_carrier_roaming_esos_provisioned_class 010402c2=string/config_satellite_carrier_roaming_non_emergency_session_class 010402c3=string/config_satellite_demo_mode_sos_intent_action 010402c4=string/config_satellite_emergency_handover_intent_action 010402c5=string/config_satellite_gateway_service_package 010402c6=string/config_satellite_nidd_apn_name 010402c7=string/config_satellite_service_package 010402c8=string/config_satellite_sim_plmn_identifier 010402c9=string/config_satellite_sim_spn_identifier 010402ca=string/config_satellite_test_with_esp_replies_intent_action 010402cb=string/config_screenshotAppClipsServiceComponent 010402cc=string/config_screenshotErrorReceiverComponent 010402cd=string/config_screenshotServiceComponent 010402ce=string/config_searchKeyTargetActivity 010402cf=string/config_secondaryBuiltInDisplayCutout 010402d0=string/config_secondaryBuiltInDisplayCutoutRectApproximation 010402d1=string/config_secondaryDisplayShape 010402d2=string/config_secondaryHomePackage 010402d3=string/config_secondaryLocationTimeZoneProviderPackageName 010402d4=string/config_sensorStateChangedActivity 010402d5=string/config_sensorUseStartedActivity 010402d6=string/config_sensorUseStartedActivity_hwToggle 010402d7=string/config_servicesExtensionPackage 010402d8=string/config_sharedConnectivityServiceIntentAction 010402d9=string/config_sharedConnectivityServicePackage 010402da=string/config_signalAttributionPath 010402db=string/config_slicePermissionComponent 010402dc=string/config_somnambulatorComponent 010402dd=string/config_supervisedUserCreationPackage 010402de=string/config_systemGameService 010402df=string/config_systemImageEditor 010402e0=string/config_systemTelevisionRemoteService 010402e1=string/config_systemUIServiceComponent 010402e2=string/config_tcp_buffers 010402e3=string/config_tvRemoteServicePackage 010402e4=string/config_usbAccessoryUriActivity 010402e5=string/config_usbConfirmActivity 010402e6=string/config_usbContaminantActivity 010402e7=string/config_usbPermissionActivity 010402e8=string/config_usbResolverActivity 010402e9=string/config_useragentprofile_url 010402ea=string/config_vendorColorModesRestoreHint 010402eb=string/config_wallpaperCropperPackage 010402ec=string/config_wallpaperManagerServiceName 010402ed=string/config_wearMediaControlsPackage 010402ee=string/config_wearMediaSessionsPackage 010402ef=string/config_wearRemoteIntentAction 010402f0=string/config_wearServiceComponent 010402f1=string/config_wearSysUiMainActivity 010402f2=string/config_wearSysUiPackage 010402f3=string/config_wearableAmbientContextEventArrayExtraKey 010402f4=string/config_wearableAmbientContextPackageNameExtraKey 010402f5=string/config_wifi_tether_enable 010402f6=string/config_wimaxManagerClassname 010402f7=string/config_wimaxNativeLibLocation 010402f8=string/config_wimaxServiceClassname 010402f9=string/config_wimaxServiceJarLocation 010402fa=string/config_wimaxStateTrackerClassname 010402fb=string/config_wlan_data_service_class 010402fc=string/config_wlan_data_service_package 010402fd=string/config_wlan_network_service_class 010402fe=string/config_wlan_network_service_package 010402ff=string/config_work_badge_path_24 01040300=string/config_wwan_data_service_class 01040301=string/config_wwan_data_service_package 01040302=string/config_wwan_network_service_class 01040303=string/config_wwan_network_service_package 01040304=string/confirm_battery_saver 01040305=string/connected_display_thermally_unavailable_notification_content 01040306=string/connected_display_unavailable_notification_content 01040307=string/connected_display_unavailable_notification_title 01040308=string/console_running_notification_message 01040309=string/console_running_notification_title 0104030a=string/contentServiceSync 0104030b=string/contentServiceSyncNotificationTitle 0104030c=string/contentServiceTooManyDeletesNotificationDesc 0104030d=string/content_description_collapsed 0104030e=string/content_description_expanded 0104030f=string/content_description_sliding_handle 01040310=string/conversation_single_line_image_placeholder 01040311=string/conversation_single_line_name_display 01040312=string/conversation_title_fallback_group_chat 01040313=string/conversation_title_fallback_one_to_one 01040314=string/country_detector 01040315=string/country_selection_title 01040316=string/create_contact_using 01040317=string/crossSimFormat_spn 01040318=string/crossSimFormat_spn_cross_sim_calling 01040319=string/csd_dose_reached_warning 0104031a=string/csd_momentary_exposure_warning 0104031b=string/data_saver_description 0104031c=string/data_saver_enable_button 0104031d=string/data_saver_enable_title 0104031e=string/data_usage_limit_body 0104031f=string/data_usage_limit_snoozed_body 01040320=string/data_usage_mobile_limit_snoozed_title 01040321=string/data_usage_mobile_limit_title 01040322=string/data_usage_rapid_app_body 01040323=string/data_usage_rapid_body 01040324=string/data_usage_rapid_title 01040325=string/data_usage_restricted_body 01040326=string/data_usage_restricted_title 01040327=string/data_usage_warning_body 01040328=string/data_usage_warning_title 01040329=string/data_usage_wifi_limit_snoozed_title 0104032a=string/data_usage_wifi_limit_title 0104032b=string/date_and_time 0104032c=string/date_picker_day_of_week_typeface 0104032d=string/date_picker_day_typeface 0104032e=string/date_picker_decrement_day_button 0104032f=string/date_picker_decrement_month_button 01040330=string/date_picker_decrement_year_button 01040331=string/date_picker_dialog_title 01040332=string/date_picker_increment_day_button 01040333=string/date_picker_increment_month_button 01040334=string/date_picker_increment_year_button 01040335=string/date_picker_mode 01040336=string/date_picker_month_typeface 01040337=string/date_picker_next_month_button 01040338=string/date_picker_prev_month_button 01040339=string/date_time 0104033a=string/date_time_done 0104033b=string/date_time_set 0104033c=string/day 0104033d=string/days 0104033e=string/db_default_journal_mode 0104033f=string/db_default_sync_mode 01040340=string/db_wal_sync_mode 01040341=string/decline 01040342=string/decline_remote_bugreport_action 01040343=string/default_audio_route_category_name 01040344=string/default_audio_route_id 01040345=string/default_audio_route_name 01040346=string/default_audio_route_name_dock_speakers 01040347=string/default_audio_route_name_external_device 01040348=string/default_audio_route_name_headphones 01040349=string/default_audio_route_name_usb 0104034a=string/default_browser 0104034b=string/default_card_name 0104034c=string/default_notification_channel_label 0104034d=string/default_sms_application 0104034e=string/default_wallpaper_component 0104034f=string/delete 01040350=string/deleteText 01040351=string/deleted_key 01040352=string/demo_restarting_message 01040353=string/demo_starting_message 01040354=string/deny 01040355=string/deprecated_abi_message 01040356=string/deprecated_target_sdk_app_store 01040357=string/deprecated_target_sdk_message 01040358=string/description_target_unlock_tablet 01040359=string/device_ownership_relinquished 0104035a=string/device_policy_manager_service 0104035b=string/device_state_notification_settings_button 0104035c=string/device_state_notification_turn_off_button 0104035d=string/device_storage_monitor_notification_channel 0104035e=string/device_unlock_notification_name 0104035f=string/dial_number_using 01040360=string/disable_accessibility_shortcut 01040361=string/dismiss_action 01040362=string/display_manager_built_in_display_name 01040363=string/display_manager_hdmi_display_name 01040364=string/display_manager_overlay_display_name 01040365=string/display_manager_overlay_display_secure_suffix 01040366=string/display_manager_overlay_display_title 01040367=string/display_rotation_camera_compat_toast_after_rotation 01040368=string/display_rotation_camera_compat_toast_in_multi_window 01040369=string/dlg_ok 0104036a=string/done_accessibility_shortcut_menu_button 0104036b=string/done_label 0104036c=string/dream_accessibility_action_click 0104036d=string/dream_preview_title 0104036e=string/dump_heap_notification 0104036f=string/dump_heap_notification_detail 01040370=string/dump_heap_ready_notification 01040371=string/dump_heap_ready_text 01040372=string/dump_heap_system_text 01040373=string/dump_heap_text 01040374=string/dump_heap_title 01040375=string/duration_days_medium 01040376=string/duration_days_medium_future 01040377=string/duration_days_medium_past 01040378=string/duration_days_relative 01040379=string/duration_days_relative_future 0104037a=string/duration_days_shortest 0104037b=string/duration_days_shortest_future 0104037c=string/duration_days_shortest_past 0104037d=string/duration_hours_medium 0104037e=string/duration_hours_medium_future 0104037f=string/duration_hours_medium_past 01040380=string/duration_hours_relative 01040381=string/duration_hours_relative_future 01040382=string/duration_hours_shortest 01040383=string/duration_hours_shortest_future 01040384=string/duration_hours_shortest_past 01040385=string/duration_minutes_medium 01040386=string/duration_minutes_medium_future 01040387=string/duration_minutes_medium_past 01040388=string/duration_minutes_relative 01040389=string/duration_minutes_relative_future 0104038a=string/duration_minutes_shortest 0104038b=string/duration_minutes_shortest_future 0104038c=string/duration_minutes_shortest_past 0104038d=string/duration_years_medium 0104038e=string/duration_years_medium_future 0104038f=string/duration_years_medium_past 01040390=string/duration_years_relative 01040391=string/duration_years_relative_future 01040392=string/duration_years_shortest 01040393=string/duration_years_shortest_future 01040394=string/duration_years_shortest_past 01040395=string/dynamic_mode_notification_channel_name 01040396=string/dynamic_mode_notification_summary 01040397=string/dynamic_mode_notification_summary_v2 01040398=string/dynamic_mode_notification_title 01040399=string/dynamic_mode_notification_title_v2 0104039a=string/editTextMenuTitle 0104039b=string/edit_accessibility_shortcut_menu_button 0104039c=string/elapsed_time_short_format_h_mm_ss 0104039d=string/elapsed_time_short_format_mm_ss 0104039e=string/emailTypeCustom 0104039f=string/emailTypeHome 010403a0=string/emailTypeMobile 010403a1=string/emailTypeOther 010403a2=string/emailTypeWork 010403a3=string/emergency_call_dialog_number_for_display 010403a4=string/emergency_calling_do_not_show_again 010403a5=string/emergency_calls_only 010403a6=string/enablePin 010403a7=string/enable_explore_by_touch_warning_message 010403a8=string/enable_explore_by_touch_warning_title 010403a9=string/error_handwriting_unsupported 010403aa=string/error_handwriting_unsupported_password 010403ab=string/error_message_change_not_allowed 010403ac=string/error_message_title 010403ad=string/etws_primary_default_message_earthquake 010403ae=string/etws_primary_default_message_earthquake_and_tsunami 010403af=string/etws_primary_default_message_others 010403b0=string/etws_primary_default_message_test 010403b1=string/etws_primary_default_message_tsunami 010403b2=string/eventTypeAnniversary 010403b3=string/eventTypeBirthday 010403b4=string/eventTypeCustom 010403b5=string/eventTypeOther 010403b6=string/expand_action_accessibility 010403b7=string/expand_button_content_description_collapsed 010403b8=string/expand_button_content_description_expanded 010403b9=string/expires_on 010403ba=string/ext_media_badremoval_notification_message 010403bb=string/ext_media_badremoval_notification_title 010403bc=string/ext_media_browse_action 010403bd=string/ext_media_checking_notification_message 010403be=string/ext_media_checking_notification_title 010403bf=string/ext_media_init_action 010403c0=string/ext_media_missing_message 010403c1=string/ext_media_missing_title 010403c2=string/ext_media_move_failure_message 010403c3=string/ext_media_move_failure_title 010403c4=string/ext_media_move_specific_title 010403c5=string/ext_media_move_success_message 010403c6=string/ext_media_move_success_title 010403c7=string/ext_media_move_title 010403c8=string/ext_media_new_notification_message 010403c9=string/ext_media_new_notification_title 010403ca=string/ext_media_nomedia_notification_message 010403cb=string/ext_media_nomedia_notification_title 010403cc=string/ext_media_ready_notification_message 010403cd=string/ext_media_seamless_action 010403ce=string/ext_media_status_bad_removal 010403cf=string/ext_media_status_checking 010403d0=string/ext_media_status_ejecting 010403d1=string/ext_media_status_formatting 010403d2=string/ext_media_status_missing 010403d3=string/ext_media_status_mounted 010403d4=string/ext_media_status_mounted_ro 010403d5=string/ext_media_status_removed 010403d6=string/ext_media_status_unmountable 010403d7=string/ext_media_status_unmounted 010403d8=string/ext_media_status_unsupported 010403d9=string/ext_media_unmount_action 010403da=string/ext_media_unmountable_notification_message 010403db=string/ext_media_unmountable_notification_title 010403dc=string/ext_media_unmounting_notification_message 010403dd=string/ext_media_unmounting_notification_title 010403de=string/ext_media_unsupported_notification_message 010403df=string/ext_media_unsupported_notification_title 010403e0=string/extract_edit_menu_button 010403e1=string/face_acquired_dark_glasses_detected 010403e2=string/face_acquired_dark_glasses_detected_alt 010403e3=string/face_acquired_insufficient 010403e4=string/face_acquired_mouth_covering_detected 010403e5=string/face_acquired_mouth_covering_detected_alt 010403e6=string/face_acquired_not_detected 010403e7=string/face_acquired_obscured 010403e8=string/face_acquired_pan_too_extreme 010403e9=string/face_acquired_poor_gaze 010403ea=string/face_acquired_recalibrate 010403eb=string/face_acquired_recalibrate_alt 010403ec=string/face_acquired_roll_too_extreme 010403ed=string/face_acquired_sensor_dirty 010403ee=string/face_acquired_tilt_too_extreme 010403ef=string/face_acquired_too_bright 010403f0=string/face_acquired_too_close 010403f1=string/face_acquired_too_dark 010403f2=string/face_acquired_too_different 010403f3=string/face_acquired_too_far 010403f4=string/face_acquired_too_high 010403f5=string/face_acquired_too_left 010403f6=string/face_acquired_too_low 010403f7=string/face_acquired_too_much_motion 010403f8=string/face_acquired_too_right 010403f9=string/face_acquired_too_similar 010403fa=string/face_app_setting_name 010403fb=string/face_authenticated_confirmation_required 010403fc=string/face_authenticated_no_confirmation_required 010403fd=string/face_dangling_notification_msg 010403fe=string/face_dangling_notification_title 010403ff=string/face_dialog_default_subtitle 01040400=string/face_error_canceled 01040401=string/face_error_hw_not_available 01040402=string/face_error_hw_not_present 01040403=string/face_error_lockout 01040404=string/face_error_lockout_permanent 01040405=string/face_error_lockout_screen_lock 01040406=string/face_error_no_space 01040407=string/face_error_not_enrolled 01040408=string/face_error_security_update_required 01040409=string/face_error_timeout 0104040a=string/face_error_unable_to_process 0104040b=string/face_error_user_canceled 0104040c=string/face_error_vendor_unknown 0104040d=string/face_icon_content_description 0104040e=string/face_name_template 0104040f=string/face_or_screen_lock_app_setting_name 01040410=string/face_or_screen_lock_dialog_default_subtitle 01040411=string/face_recalibrate_notification_content 01040412=string/face_recalibrate_notification_name 01040413=string/face_recalibrate_notification_title 01040414=string/face_sensor_privacy_enabled 01040415=string/faceunlock_multiple_failures 01040416=string/factory_reset_message 01040417=string/factory_reset_warning 01040418=string/factorytest_failed 01040419=string/factorytest_no_action 0104041a=string/factorytest_not_system 0104041b=string/factorytest_reboot 0104041c=string/failed_to_copy_to_clipboard 0104041d=string/fallback_wallpaper_component 0104041e=string/fast_scroll_alphabet 0104041f=string/fast_scroll_numeric_alphabet 01040420=string/fcComplete 01040421=string/fcError 01040422=string/fileSizeSuffix 01040423=string/file_count 01040424=string/find 01040425=string/find_next 01040426=string/find_on_page 01040427=string/find_previous 01040428=string/fingerprint_acquired_already_enrolled 01040429=string/fingerprint_acquired_imager_dirty 0104042a=string/fingerprint_acquired_imager_dirty_alt 0104042b=string/fingerprint_acquired_immobile 0104042c=string/fingerprint_acquired_insufficient 0104042d=string/fingerprint_acquired_partial 0104042e=string/fingerprint_acquired_power_press 0104042f=string/fingerprint_acquired_too_bright 01040430=string/fingerprint_acquired_too_fast 01040431=string/fingerprint_acquired_too_slow 01040432=string/fingerprint_acquired_try_adjusting 01040433=string/fingerprint_app_setting_name 01040434=string/fingerprint_authenticated 01040435=string/fingerprint_dangling_notification_msg_1 01040436=string/fingerprint_dangling_notification_msg_2 01040437=string/fingerprint_dangling_notification_msg_all_deleted_1 01040438=string/fingerprint_dangling_notification_msg_all_deleted_2 01040439=string/fingerprint_dangling_notification_title 0104043a=string/fingerprint_dialog_default_subtitle 0104043b=string/fingerprint_dialog_use_fingerprint_instead 0104043c=string/fingerprint_error_bad_calibration 0104043d=string/fingerprint_error_canceled 0104043e=string/fingerprint_error_hw_not_available 0104043f=string/fingerprint_error_hw_not_present 01040440=string/fingerprint_error_lockout 01040441=string/fingerprint_error_lockout_permanent 01040442=string/fingerprint_error_no_fingerprints 01040443=string/fingerprint_error_no_space 01040444=string/fingerprint_error_not_match 01040445=string/fingerprint_error_power_pressed 01040446=string/fingerprint_error_security_update_required 01040447=string/fingerprint_error_timeout 01040448=string/fingerprint_error_unable_to_process 01040449=string/fingerprint_error_user_canceled 0104044a=string/fingerprint_error_vendor_unknown 0104044b=string/fingerprint_frr_notification_msg 0104044c=string/fingerprint_frr_notification_title 0104044d=string/fingerprint_loe_notification_msg 0104044e=string/fingerprint_name_template 0104044f=string/fingerprint_or_screen_lock_app_setting_name 01040450=string/fingerprint_or_screen_lock_dialog_default_subtitle 01040451=string/fingerprint_recalibrate_notification_content 01040452=string/fingerprint_recalibrate_notification_name 01040453=string/fingerprint_recalibrate_notification_title 01040454=string/fingerprint_udfps_error_not_match 01040455=string/fingerprints 01040456=string/floating_toolbar_close_overflow_description 01040457=string/floating_toolbar_open_overflow_description 01040458=string/font_family_body_1_material 01040459=string/font_family_body_2_material 0104045a=string/font_family_button_material 0104045b=string/font_family_caption_material 0104045c=string/font_family_display_1_material 0104045d=string/font_family_display_2_material 0104045e=string/font_family_display_3_material 0104045f=string/font_family_display_4_material 01040460=string/font_family_headline_material 01040461=string/font_family_menu_material 01040462=string/font_family_subhead_material 01040463=string/font_family_title_material 01040464=string/force_close 01040465=string/foreground_service_app_in_background 01040466=string/foreground_service_apps_in_background 01040467=string/foreground_service_multiple_separator 01040468=string/foreground_service_tap_for_details 01040469=string/forward_intent_to_owner 0104046a=string/forward_intent_to_work 0104046b=string/fp_power_button_bp_message 0104046c=string/fp_power_button_bp_negative_button 0104046d=string/fp_power_button_bp_positive_button 0104046e=string/fp_power_button_bp_title 0104046f=string/fp_power_button_enrollment_button_text 01040470=string/fp_power_button_enrollment_message 01040471=string/fp_power_button_enrollment_title 01040472=string/gadget_host_error_inflating 01040473=string/geofencing_service 01040474=string/global_action_assist 01040475=string/global_action_bug_report 01040476=string/global_action_emergency 01040477=string/global_action_lock 01040478=string/global_action_lockdown 01040479=string/global_action_logout 0104047a=string/global_action_power_off 0104047b=string/global_action_power_options 0104047c=string/global_action_restart 0104047d=string/global_action_screenshot 0104047e=string/global_action_settings 0104047f=string/global_action_silent_mode_off_status 01040480=string/global_action_silent_mode_on_status 01040481=string/global_action_standby 01040482=string/global_action_toggle_silent_mode 01040483=string/global_action_voice_assist 01040484=string/global_actions 01040485=string/global_actions_airplane_mode_off_status 01040486=string/global_actions_airplane_mode_on_status 01040487=string/global_actions_toggle_airplane_mode 01040488=string/gnss_service 01040489=string/gnss_time_update_service 0104048a=string/gpsNotifMessage 0104048b=string/gpsNotifTicker 0104048c=string/gpsNotifTitle 0104048d=string/gpsVerifNo 0104048e=string/gpsVerifYes 0104048f=string/grant_credentials_permission_message_footer 01040490=string/grant_credentials_permission_message_header 01040491=string/grant_permissions_header_text 01040492=string/granularity_label_character 01040493=string/granularity_label_line 01040494=string/granularity_label_link 01040495=string/granularity_label_word 01040496=string/gsm_alphabet_default_charset 01040497=string/guest_name 01040498=string/hardware 01040499=string/harmful_app_warning_open_anyway 0104049a=string/harmful_app_warning_title 0104049b=string/harmful_app_warning_uninstall 0104049c=string/hearing_aids_feature_name 0104049d=string/hearing_device_notification_settings_button 0104049e=string/hearing_device_notification_switch_button 0104049f=string/hearing_device_status_active 010404a0=string/hearing_device_status_connected 010404a1=string/hearing_device_status_disconnected 010404a2=string/hearing_device_status_loading 010404a3=string/hearing_device_switch_hearing_mic_notification_text 010404a4=string/hearing_device_switch_hearing_mic_notification_title 010404a5=string/hearing_device_switch_phone_mic_notification_text 010404a6=string/hearing_device_switch_phone_mic_notification_title 010404a7=string/heavy_weight_notification 010404a8=string/heavy_weight_notification_detail 010404a9=string/heavy_weight_switcher_text 010404aa=string/heavy_weight_switcher_title 010404ab=string/hour 010404ac=string/hour_picker_description 010404ad=string/hours 010404ae=string/httpError 010404af=string/httpErrorAuth 010404b0=string/httpErrorConnect 010404b1=string/httpErrorFailedSslHandshake 010404b2=string/httpErrorFile 010404b3=string/httpErrorFileNotFound 010404b4=string/httpErrorIO 010404b5=string/httpErrorLookup 010404b6=string/httpErrorOk 010404b7=string/httpErrorProxyAuth 010404b8=string/httpErrorRedirectLoop 010404b9=string/httpErrorTimeout 010404ba=string/httpErrorTooManyRequests 010404bb=string/httpErrorUnsupportedAuthScheme 010404bc=string/icu_abbrev_wday_month_day_no_year 010404bd=string/identity_check_settings_action 010404be=string/identity_check_settings_package_name 010404bf=string/imProtocolAim 010404c0=string/imProtocolCustom 010404c1=string/imProtocolGoogleTalk 010404c2=string/imProtocolIcq 010404c3=string/imProtocolJabber 010404c4=string/imProtocolMsn 010404c5=string/imProtocolNetMeeting 010404c6=string/imProtocolQq 010404c7=string/imProtocolSkype 010404c8=string/imProtocolYahoo 010404c9=string/imTypeCustom 010404ca=string/imTypeHome 010404cb=string/imTypeOther 010404cc=string/imTypeWork 010404cd=string/image_wallpaper_component 010404ce=string/ime_action_default 010404cf=string/ime_action_done 010404d0=string/ime_action_go 010404d1=string/ime_action_next 010404d2=string/ime_action_previous 010404d3=string/ime_action_search 010404d4=string/ime_action_send 010404d5=string/imei 010404d6=string/immersive_cling_description 010404d7=string/immersive_cling_positive 010404d8=string/immersive_cling_title 010404d9=string/importance_from_person 010404da=string/importance_from_user 010404db=string/in_progress 010404dc=string/indeterminate_progress_01 010404dd=string/indeterminate_progress_02 010404de=string/indeterminate_progress_03 010404df=string/indeterminate_progress_04 010404e0=string/indeterminate_progress_05 010404e1=string/indeterminate_progress_06 010404e2=string/indeterminate_progress_07 010404e3=string/indeterminate_progress_08 010404e4=string/indeterminate_progress_09 010404e5=string/indeterminate_progress_10 010404e6=string/indeterminate_progress_11 010404e7=string/indeterminate_progress_12 010404e8=string/indeterminate_progress_13 010404e9=string/indeterminate_progress_14 010404ea=string/indeterminate_progress_15 010404eb=string/indeterminate_progress_16 010404ec=string/indeterminate_progress_17 010404ed=string/indeterminate_progress_18 010404ee=string/indeterminate_progress_19 010404ef=string/indeterminate_progress_20 010404f0=string/indeterminate_progress_21 010404f1=string/indeterminate_progress_22 010404f2=string/indeterminate_progress_23 010404f3=string/indeterminate_progress_24 010404f4=string/indeterminate_progress_25 010404f5=string/indeterminate_progress_26 010404f6=string/indeterminate_progress_27 010404f7=string/indeterminate_progress_28 010404f8=string/indeterminate_progress_29 010404f9=string/indeterminate_progress_30 010404fa=string/indeterminate_progress_31 010404fb=string/indeterminate_progress_32 010404fc=string/indeterminate_progress_33 010404fd=string/indeterminate_progress_34 010404fe=string/indeterminate_progress_35 010404ff=string/indeterminate_progress_36 01040500=string/indeterminate_progress_37 01040501=string/indeterminate_progress_38 01040502=string/indeterminate_progress_39 01040503=string/indeterminate_progress_40 01040504=string/indeterminate_progress_41 01040505=string/indeterminate_progress_42 01040506=string/indeterminate_progress_43 01040507=string/indeterminate_progress_44 01040508=string/indeterminate_progress_45 01040509=string/indeterminate_progress_46 0104050a=string/indeterminate_progress_47 0104050b=string/indeterminate_progress_48 0104050c=string/indeterminate_progress_49 0104050d=string/indeterminate_progress_50 0104050e=string/indeterminate_progress_51 0104050f=string/indeterminate_progress_52 01040510=string/indeterminate_progress_53 01040511=string/indeterminate_progress_54 01040512=string/indeterminate_progress_55 01040513=string/indeterminate_progress_56 01040514=string/indeterminate_progress_57 01040515=string/indeterminate_progress_58 01040516=string/indeterminate_progress_59 01040517=string/indeterminate_progress_60 01040518=string/indeterminate_progress_background 01040519=string/inputMethod 0104051a=string/input_method_binding_label 0104051b=string/input_method_ime_switch_button_desc 0104051c=string/input_method_ime_switch_long_click_action_desc 0104051d=string/input_method_language_settings 0104051e=string/input_method_nav_back_button_desc 0104051f=string/input_method_switcher_settings_button 01040520=string/install_carrier_app_notification_button 01040521=string/install_carrier_app_notification_text 01040522=string/install_carrier_app_notification_text_app_name 01040523=string/install_carrier_app_notification_title 01040524=string/invalidPin 01040525=string/invalidPuk 01040526=string/issued_by 01040527=string/issued_on 01040528=string/issued_to 01040529=string/js_dialog_before_unload 0104052a=string/js_dialog_before_unload_negative_button 0104052b=string/js_dialog_before_unload_positive_button 0104052c=string/js_dialog_before_unload_title 0104052d=string/js_dialog_title 0104052e=string/js_dialog_title_default 0104052f=string/key_warning_state 01040530=string/keyboard_layout_notification_more_than_three_selected_message 01040531=string/keyboard_layout_notification_multiple_selected_message 01040532=string/keyboard_layout_notification_multiple_selected_title 01040533=string/keyboard_layout_notification_one_selected_message 01040534=string/keyboard_layout_notification_selected_title 01040535=string/keyboard_layout_notification_three_selected_message 01040536=string/keyboard_layout_notification_two_selected_message 01040537=string/keyboard_shortcut_group_applications 01040538=string/keyboard_shortcut_group_applications_browser 01040539=string/keyboard_shortcut_group_applications_calculator 0104053a=string/keyboard_shortcut_group_applications_calendar 0104053b=string/keyboard_shortcut_group_applications_contacts 0104053c=string/keyboard_shortcut_group_applications_email 0104053d=string/keyboard_shortcut_group_applications_maps 0104053e=string/keyboard_shortcut_group_applications_music 0104053f=string/keyboard_shortcut_group_applications_sms 01040540=string/keyboardview_keycode_alt 01040541=string/keyboardview_keycode_cancel 01040542=string/keyboardview_keycode_delete 01040543=string/keyboardview_keycode_done 01040544=string/keyboardview_keycode_enter 01040545=string/keyboardview_keycode_mode_change 01040546=string/keyboardview_keycode_shift 01040547=string/keygaurd_accessibility_media_controls 01040548=string/keyguard_accessibility_add_widget 01040549=string/keyguard_accessibility_camera 0104054a=string/keyguard_accessibility_expand_lock_area 0104054b=string/keyguard_accessibility_face_unlock 0104054c=string/keyguard_accessibility_password_unlock 0104054d=string/keyguard_accessibility_pattern_area 0104054e=string/keyguard_accessibility_pattern_unlock 0104054f=string/keyguard_accessibility_pin_unlock 01040550=string/keyguard_accessibility_sim_pin_unlock 01040551=string/keyguard_accessibility_sim_puk_unlock 01040552=string/keyguard_accessibility_slide_area 01040553=string/keyguard_accessibility_slide_unlock 01040554=string/keyguard_accessibility_status 01040555=string/keyguard_accessibility_unlock_area_collapsed 01040556=string/keyguard_accessibility_unlock_area_expanded 01040557=string/keyguard_accessibility_user_selector 01040558=string/keyguard_accessibility_widget 01040559=string/keyguard_accessibility_widget_changed 0104055a=string/keyguard_accessibility_widget_deleted 0104055b=string/keyguard_accessibility_widget_empty_slot 0104055c=string/keyguard_accessibility_widget_reorder_end 0104055d=string/keyguard_accessibility_widget_reorder_start 0104055e=string/keyguard_label_text 0104055f=string/keyguard_password_enter_password_code 01040560=string/keyguard_password_enter_pin_code 01040561=string/keyguard_password_enter_pin_password_code 01040562=string/keyguard_password_enter_pin_prompt 01040563=string/keyguard_password_enter_puk_code 01040564=string/keyguard_password_enter_puk_prompt 01040565=string/keyguard_password_entry_touch_hint 01040566=string/keyguard_password_wrong_pin_code 01040567=string/kg_enter_confirm_pin_hint 01040568=string/kg_failed_attempts_almost_at_login 01040569=string/kg_failed_attempts_almost_at_wipe 0104056a=string/kg_failed_attempts_now_wiping 0104056b=string/kg_forgot_pattern_button_text 0104056c=string/kg_invalid_confirm_pin_hint 0104056d=string/kg_invalid_puk 0104056e=string/kg_invalid_sim_pin_hint 0104056f=string/kg_invalid_sim_puk_hint 01040570=string/kg_login_account_recovery_hint 01040571=string/kg_login_checking_password 01040572=string/kg_login_instructions 01040573=string/kg_login_invalid_input 01040574=string/kg_login_password_hint 01040575=string/kg_login_submit_button 01040576=string/kg_login_too_many_attempts 01040577=string/kg_login_username_hint 01040578=string/kg_password_instructions 01040579=string/kg_password_wrong_pin_code 0104057a=string/kg_pattern_instructions 0104057b=string/kg_pin_instructions 0104057c=string/kg_puk_enter_pin_hint 0104057d=string/kg_puk_enter_puk_hint 0104057e=string/kg_reordering_delete_drop_target_text 0104057f=string/kg_sim_pin_instructions 01040580=string/kg_sim_unlock_progress_dialog_message 01040581=string/kg_text_message_separator 01040582=string/kg_too_many_failed_password_attempts_dialog_message 01040583=string/kg_too_many_failed_pattern_attempts_dialog_message 01040584=string/kg_too_many_failed_pin_attempts_dialog_message 01040585=string/kg_wrong_password 01040586=string/kg_wrong_pattern 01040587=string/kg_wrong_pin 01040588=string/language_picker_regions_section_suggested 01040589=string/language_picker_section_all 0104058a=string/language_picker_section_suggested 0104058b=string/language_picker_section_suggested_bilingual 0104058c=string/language_selection_title 0104058d=string/last_month 0104058e=string/last_num_days 0104058f=string/launchBrowserDefault 01040590=string/launch_warning_original 01040591=string/launch_warning_replace 01040592=string/launch_warning_title 01040593=string/leave_accessibility_shortcut_on 01040594=string/loading 01040595=string/locale_replacement 01040596=string/locale_search_menu 01040597=string/location_changed_notification_text 01040598=string/location_changed_notification_title 01040599=string/location_service 0104059a=string/lock_pattern_view_aspect 0104059b=string/lock_to_app_unlock_password 0104059c=string/lock_to_app_unlock_pattern 0104059d=string/lock_to_app_unlock_pin 0104059e=string/lockscreen_access_pattern_area 0104059f=string/lockscreen_access_pattern_cell_added 010405a0=string/lockscreen_access_pattern_cell_added_verbose 010405a1=string/lockscreen_access_pattern_cleared 010405a2=string/lockscreen_access_pattern_detected 010405a3=string/lockscreen_access_pattern_start 010405a4=string/lockscreen_carrier_default 010405a5=string/lockscreen_emergency_call 010405a6=string/lockscreen_failed_attempts_almost_at_wipe 010405a7=string/lockscreen_failed_attempts_almost_glogin 010405a8=string/lockscreen_failed_attempts_now_wiping 010405a9=string/lockscreen_forgot_pattern_button_text 010405aa=string/lockscreen_glogin_account_recovery_hint 010405ab=string/lockscreen_glogin_checking_password 010405ac=string/lockscreen_glogin_forgot_pattern 010405ad=string/lockscreen_glogin_instructions 010405ae=string/lockscreen_glogin_invalid_input 010405af=string/lockscreen_glogin_password_hint 010405b0=string/lockscreen_glogin_submit_button 010405b1=string/lockscreen_glogin_too_many_attempts 010405b2=string/lockscreen_glogin_username_hint 010405b3=string/lockscreen_instructions_when_pattern_disabled 010405b4=string/lockscreen_instructions_when_pattern_enabled 010405b5=string/lockscreen_missing_sim_instructions 010405b6=string/lockscreen_missing_sim_instructions_long 010405b7=string/lockscreen_missing_sim_message 010405b8=string/lockscreen_missing_sim_message_short 010405b9=string/lockscreen_network_locked_message 010405ba=string/lockscreen_password_wrong 010405bb=string/lockscreen_pattern_correct 010405bc=string/lockscreen_pattern_instructions 010405bd=string/lockscreen_pattern_wrong 010405be=string/lockscreen_permanent_disabled_sim_instructions 010405bf=string/lockscreen_permanent_disabled_sim_message_short 010405c0=string/lockscreen_return_to_call 010405c1=string/lockscreen_screen_locked 010405c2=string/lockscreen_sim_locked_message 010405c3=string/lockscreen_sim_puk_locked_instructions 010405c4=string/lockscreen_sim_puk_locked_message 010405c5=string/lockscreen_sim_unlock_progress_dialog_message 010405c6=string/lockscreen_sound_off_label 010405c7=string/lockscreen_sound_on_label 010405c8=string/lockscreen_storage_locked 010405c9=string/lockscreen_too_many_failed_attempts_countdown 010405ca=string/lockscreen_too_many_failed_attempts_dialog_message 010405cb=string/lockscreen_too_many_failed_password_attempts_dialog_message 010405cc=string/lockscreen_too_many_failed_pin_attempts_dialog_message 010405cd=string/lockscreen_transport_ffw_description 010405ce=string/lockscreen_transport_next_description 010405cf=string/lockscreen_transport_pause_description 010405d0=string/lockscreen_transport_play_description 010405d1=string/lockscreen_transport_prev_description 010405d2=string/lockscreen_transport_rew_description 010405d3=string/lockscreen_transport_stop_description 010405d4=string/lockscreen_unlock_label 010405d5=string/low_internal_storage_view_text 010405d6=string/low_internal_storage_view_text_no_boot 010405d7=string/low_internal_storage_view_title 010405d8=string/low_memory 010405d9=string/managed_profile_app_label 010405da=string/managed_profile_label 010405db=string/managed_profile_label_badge 010405dc=string/managed_profile_label_badge_2 010405dd=string/managed_profile_label_badge_3 010405de=string/matches_found 010405df=string/maximize_button_text 010405e0=string/me 010405e1=string/media_route_button_content_description 010405e2=string/media_route_chooser_extended_settings 010405e3=string/media_route_chooser_searching 010405e4=string/media_route_chooser_title 010405e5=string/media_route_chooser_title_for_remote_display 010405e6=string/media_route_controller_disconnect 010405e7=string/media_route_status_available 010405e8=string/media_route_status_connecting 010405e9=string/media_route_status_in_use 010405ea=string/media_route_status_not_available 010405eb=string/media_route_status_scanning 010405ec=string/mediasize_chinese_om_dai_pa_kai 010405ed=string/mediasize_chinese_om_jurro_ku_kai 010405ee=string/mediasize_chinese_om_pa_kai 010405ef=string/mediasize_chinese_prc_1 010405f0=string/mediasize_chinese_prc_10 010405f1=string/mediasize_chinese_prc_16k 010405f2=string/mediasize_chinese_prc_2 010405f3=string/mediasize_chinese_prc_3 010405f4=string/mediasize_chinese_prc_4 010405f5=string/mediasize_chinese_prc_5 010405f6=string/mediasize_chinese_prc_6 010405f7=string/mediasize_chinese_prc_7 010405f8=string/mediasize_chinese_prc_8 010405f9=string/mediasize_chinese_prc_9 010405fa=string/mediasize_chinese_roc_16k 010405fb=string/mediasize_chinese_roc_8k 010405fc=string/mediasize_iso_a0 010405fd=string/mediasize_iso_a1 010405fe=string/mediasize_iso_a10 010405ff=string/mediasize_iso_a2 01040600=string/mediasize_iso_a3 01040601=string/mediasize_iso_a4 01040602=string/mediasize_iso_a5 01040603=string/mediasize_iso_a6 01040604=string/mediasize_iso_a7 01040605=string/mediasize_iso_a8 01040606=string/mediasize_iso_a9 01040607=string/mediasize_iso_b0 01040608=string/mediasize_iso_b1 01040609=string/mediasize_iso_b10 0104060a=string/mediasize_iso_b2 0104060b=string/mediasize_iso_b3 0104060c=string/mediasize_iso_b4 0104060d=string/mediasize_iso_b5 0104060e=string/mediasize_iso_b6 0104060f=string/mediasize_iso_b7 01040610=string/mediasize_iso_b8 01040611=string/mediasize_iso_b9 01040612=string/mediasize_iso_c0 01040613=string/mediasize_iso_c1 01040614=string/mediasize_iso_c10 01040615=string/mediasize_iso_c2 01040616=string/mediasize_iso_c3 01040617=string/mediasize_iso_c4 01040618=string/mediasize_iso_c5 01040619=string/mediasize_iso_c6 0104061a=string/mediasize_iso_c7 0104061b=string/mediasize_iso_c8 0104061c=string/mediasize_iso_c9 0104061d=string/mediasize_japanese_chou2 0104061e=string/mediasize_japanese_chou3 0104061f=string/mediasize_japanese_chou4 01040620=string/mediasize_japanese_hagaki 01040621=string/mediasize_japanese_jis_b0 01040622=string/mediasize_japanese_jis_b1 01040623=string/mediasize_japanese_jis_b10 01040624=string/mediasize_japanese_jis_b2 01040625=string/mediasize_japanese_jis_b3 01040626=string/mediasize_japanese_jis_b4 01040627=string/mediasize_japanese_jis_b5 01040628=string/mediasize_japanese_jis_b6 01040629=string/mediasize_japanese_jis_b7 0104062a=string/mediasize_japanese_jis_b8 0104062b=string/mediasize_japanese_jis_b9 0104062c=string/mediasize_japanese_jis_exec 0104062d=string/mediasize_japanese_kahu 0104062e=string/mediasize_japanese_kaku2 0104062f=string/mediasize_japanese_l 01040630=string/mediasize_japanese_oufuku 01040631=string/mediasize_japanese_you4 01040632=string/mediasize_na_ansi_c 01040633=string/mediasize_na_ansi_d 01040634=string/mediasize_na_ansi_e 01040635=string/mediasize_na_ansi_f 01040636=string/mediasize_na_arch_a 01040637=string/mediasize_na_arch_b 01040638=string/mediasize_na_arch_c 01040639=string/mediasize_na_arch_d 0104063a=string/mediasize_na_arch_e 0104063b=string/mediasize_na_arch_e1 0104063c=string/mediasize_na_foolscap 0104063d=string/mediasize_na_gvrnmt_letter 0104063e=string/mediasize_na_index_3x5 0104063f=string/mediasize_na_index_4x6 01040640=string/mediasize_na_index_5x8 01040641=string/mediasize_na_junior_legal 01040642=string/mediasize_na_ledger 01040643=string/mediasize_na_legal 01040644=string/mediasize_na_letter 01040645=string/mediasize_na_monarch 01040646=string/mediasize_na_quarto 01040647=string/mediasize_na_super_b 01040648=string/mediasize_na_tabloid 01040649=string/mediasize_unknown_landscape 0104064a=string/mediasize_unknown_portrait 0104064b=string/meid 0104064c=string/menu_alt_shortcut_label 0104064d=string/menu_ctrl_shortcut_label 0104064e=string/menu_delete_shortcut_label 0104064f=string/menu_enter_shortcut_label 01040650=string/menu_function_shortcut_label 01040651=string/menu_meta_shortcut_label 01040652=string/menu_shift_shortcut_label 01040653=string/menu_space_shortcut_label 01040654=string/menu_sym_shortcut_label 01040655=string/mic_access_off_toast 01040656=string/mic_access_on_toast 01040657=string/midnight 01040658=string/mime_type_apk 01040659=string/mime_type_audio 0104065a=string/mime_type_audio_ext 0104065b=string/mime_type_compressed 0104065c=string/mime_type_compressed_ext 0104065d=string/mime_type_document 0104065e=string/mime_type_document_ext 0104065f=string/mime_type_folder 01040660=string/mime_type_generic 01040661=string/mime_type_generic_ext 01040662=string/mime_type_image 01040663=string/mime_type_image_ext 01040664=string/mime_type_presentation 01040665=string/mime_type_presentation_ext 01040666=string/mime_type_spreadsheet 01040667=string/mime_type_spreadsheet_ext 01040668=string/mime_type_video 01040669=string/mime_type_video_ext 0104066a=string/miniresolver_call 0104066b=string/miniresolver_call_in_work 0104066c=string/miniresolver_call_information 0104066d=string/miniresolver_open_in_personal 0104066e=string/miniresolver_open_in_work 0104066f=string/miniresolver_open_work 01040670=string/miniresolver_private_space_messages_information 01040671=string/miniresolver_private_space_phone_information 01040672=string/miniresolver_sms_information 01040673=string/miniresolver_switch 01040674=string/miniresolver_switch_to_work 01040675=string/miniresolver_use_personal_browser 01040676=string/miniresolver_use_work_browser 01040677=string/minute 01040678=string/minute_picker_description 01040679=string/minutes 0104067a=string/mismatchPin 0104067b=string/mmcc_authentication_reject 0104067c=string/mmcc_authentication_reject_msim_template 0104067d=string/mmcc_illegal_me 0104067e=string/mmcc_illegal_me_msim_template 0104067f=string/mmcc_illegal_ms 01040680=string/mmcc_illegal_ms_msim_template 01040681=string/mmcc_imsi_unknown_in_hlr 01040682=string/mmcc_imsi_unknown_in_hlr_msim_template 01040683=string/mmiComplete 01040684=string/mmiError 01040685=string/mmiErrorNotSupported 01040686=string/mmiErrorWhileRoaming 01040687=string/mmiFdnError 01040688=string/mobile_no_internet 01040689=string/mobile_provisioning_apn 0104068a=string/mobile_provisioning_url 0104068b=string/month_day_year 0104068c=string/more_item_label 0104068d=string/mte_override_notification_message 0104068e=string/mte_override_notification_title 0104068f=string/music_recognition_manager_service 01040690=string/muted_by 01040691=string/nas_upgrade_notification_content 01040692=string/nas_upgrade_notification_disable_action 01040693=string/nas_upgrade_notification_enable_action 01040694=string/nas_upgrade_notification_learn_more_action 01040695=string/nas_upgrade_notification_learn_more_content 01040696=string/nas_upgrade_notification_title 01040697=string/needPuk 01040698=string/needPuk2 01040699=string/negative_duration 0104069a=string/network_available_sign_in 0104069b=string/network_available_sign_in_detailed 0104069c=string/network_logging_notification_text 0104069d=string/network_logging_notification_title 0104069e=string/network_partial_connectivity 0104069f=string/network_partial_connectivity_detailed 010406a0=string/network_switch_metered 010406a1=string/network_switch_metered_detail 010406a2=string/network_switch_metered_toast 010406a3=string/network_switch_type_name_unknown 010406a4=string/new_app_action 010406a5=string/new_app_description 010406a6=string/new_sms_notification_content 010406a7=string/new_sms_notification_title 010406a8=string/news_notification_channel_label 010406a9=string/next_button_label 010406aa=string/noApplications 010406ab=string/no_file_chosen 010406ac=string/no_matches 010406ad=string/no_permissions 010406ae=string/no_recent_tasks 010406af=string/noon 010406b0=string/not_checked 010406b1=string/not_selected 010406b2=string/notification_action_check_bg_apps 010406b3=string/notification_alerted_content_description 010406b4=string/notification_app_name_settings 010406b5=string/notification_app_name_system 010406b6=string/notification_appops_camera_active 010406b7=string/notification_appops_microphone_active 010406b8=string/notification_appops_overlay_active 010406b9=string/notification_channel_abusive_bg_apps 010406ba=string/notification_channel_accessibility_hearing_device 010406bb=string/notification_channel_accessibility_magnification 010406bc=string/notification_channel_accessibility_security_policy 010406bd=string/notification_channel_account 010406be=string/notification_channel_alerts 010406bf=string/notification_channel_call_forward 010406c0=string/notification_channel_car_mode 010406c1=string/notification_channel_developer 010406c2=string/notification_channel_developer_important 010406c3=string/notification_channel_device_admin 010406c4=string/notification_channel_display 010406c5=string/notification_channel_emergency_callback 010406c6=string/notification_channel_foreground_service 010406c7=string/notification_channel_heavy_weight_app 010406c8=string/notification_channel_mobile_data_status 010406c9=string/notification_channel_network_alert 010406ca=string/notification_channel_network_alerts 010406cb=string/notification_channel_network_available 010406cc=string/notification_channel_network_status 010406cd=string/notification_channel_physical_keyboard 010406ce=string/notification_channel_retail_mode 010406cf=string/notification_channel_security 010406d0=string/notification_channel_sim 010406d1=string/notification_channel_sim_high_prio 010406d2=string/notification_channel_sms 010406d3=string/notification_channel_system_changes 010406d4=string/notification_channel_system_time 010406d5=string/notification_channel_updates 010406d6=string/notification_channel_usb 010406d7=string/notification_channel_voice_mail 010406d8=string/notification_channel_vpn 010406d9=string/notification_channel_wfc 010406da=string/notification_compact_heads_up_reply 010406db=string/notification_content_abusive_bg_apps 010406dc=string/notification_content_long_running_fgs 010406dd=string/notification_feedback_indicator 010406de=string/notification_feedback_indicator_alerted 010406df=string/notification_feedback_indicator_demoted 010406e0=string/notification_feedback_indicator_promoted 010406e1=string/notification_feedback_indicator_silenced 010406e2=string/notification_header_divider_symbol 010406e3=string/notification_header_divider_symbol_with_spaces 010406e4=string/notification_hidden_text 010406e5=string/notification_history_title_placeholder 010406e6=string/notification_inbox_ellipsis 010406e7=string/notification_listener_binding_label 010406e8=string/notification_messaging_title_template 010406e9=string/notification_phishing_alert_content_description 010406ea=string/notification_ranker_binding_label 010406eb=string/notification_reply_button_accessibility 010406ec=string/notification_title 010406ed=string/notification_title_abusive_bg_apps 010406ee=string/notification_title_long_running_fgs 010406ef=string/notification_verified_content_description 010406f0=string/notification_work_profile_content_description 010406f1=string/now_string_shortest 010406f2=string/number_picker_decrement_button 010406f3=string/number_picker_increment_button 010406f4=string/number_picker_increment_scroll_action 010406f5=string/number_picker_increment_scroll_mode 010406f6=string/old_app_action 010406f7=string/older 010406f8=string/oneMonthDurationPast 010406f9=string/one_handed_mode_feature_name 010406fa=string/orgTypeCustom 010406fb=string/orgTypeOther 010406fc=string/orgTypeWork 010406fd=string/org_name 010406fe=string/org_unit 010406ff=string/other_networks_no_internet 01040700=string/owner_name 01040701=string/package_deleted_device_owner 01040702=string/package_installed_device_owner 01040703=string/package_updated_device_owner 01040704=string/page_size_compat_apk_and_elf_warning 01040705=string/page_size_compat_apk_warning 01040706=string/page_size_compat_elf_warning 01040707=string/passwordIncorrect 01040708=string/password_keyboard_label_alpha_key 01040709=string/password_keyboard_label_alt_key 0104070a=string/password_keyboard_label_symbol_key 0104070b=string/pasted_from_clipboard 0104070c=string/peerTtyModeFull 0104070d=string/peerTtyModeHco 0104070e=string/peerTtyModeOff 0104070f=string/peerTtyModeVco 01040710=string/perm_costs_money 01040711=string/permdesc_acceptHandovers 01040712=string/permdesc_accessBackgroundLocation 01040713=string/permdesc_accessCoarseLocation 01040714=string/permdesc_accessDrmCertificates 01040715=string/permdesc_accessFineLocation 01040716=string/permdesc_accessHiddenProfile 01040717=string/permdesc_accessImsCallService 01040718=string/permdesc_accessLastKnownCellId 01040719=string/permdesc_accessLocationExtraCommands 0104071a=string/permdesc_accessNetworkConditions 0104071b=string/permdesc_accessNetworkState 0104071c=string/permdesc_accessNotifications 0104071d=string/permdesc_accessWifiState 0104071e=string/permdesc_accessWimaxState 0104071f=string/permdesc_access_notification_policy 01040720=string/permdesc_activityRecognition 01040721=string/permdesc_addVoicemail 01040722=string/permdesc_answerPhoneCalls 01040723=string/permdesc_audioWrite 01040724=string/permdesc_backgroundCamera 01040725=string/permdesc_bindCarrierMessagingService 01040726=string/permdesc_bindCarrierServices 01040727=string/permdesc_bindCellBroadcastService 01040728=string/permdesc_bindConditionProviderService 01040729=string/permdesc_bindDreamService 0104072a=string/permdesc_bindNotificationListenerService 0104072b=string/permdesc_bind_connection_service 0104072c=string/permdesc_bind_incall_service 0104072d=string/permdesc_bluetooth 0104072e=string/permdesc_bluetoothAdmin 0104072f=string/permdesc_bluetooth_advertise 01040730=string/permdesc_bluetooth_connect 01040731=string/permdesc_bluetooth_scan 01040732=string/permdesc_bodySensors 01040733=string/permdesc_bodySensors_background 01040734=string/permdesc_broadcastSticky 01040735=string/permdesc_callCompanionApp 01040736=string/permdesc_callPhone 01040737=string/permdesc_camera 01040738=string/permdesc_cameraHeadlessSystemUser 01040739=string/permdesc_cameraOpenCloseListener 0104073a=string/permdesc_changeNetworkState 0104073b=string/permdesc_changeTetherState 0104073c=string/permdesc_changeWifiMulticastState 0104073d=string/permdesc_changeWifiState 0104073e=string/permdesc_changeWimaxState 0104073f=string/permdesc_companionProfileWatch 01040740=string/permdesc_connection_manager 01040741=string/permdesc_control_incall_experience 01040742=string/permdesc_createNetworkSockets 01040743=string/permdesc_deliverCompanionMessages 01040744=string/permdesc_detectScreenCapture 01040745=string/permdesc_disableKeyguard 01040746=string/permdesc_enableCarMode 01040747=string/permdesc_exemptFromAudioRecordRestrictions 01040748=string/permdesc_expandStatusBar 01040749=string/permdesc_eye_tracking_coarse 0104074a=string/permdesc_eye_tracking_fine 0104074b=string/permdesc_face_tracking 0104074c=string/permdesc_foregroundService 0104074d=string/permdesc_foregroundServiceCamera 0104074e=string/permdesc_foregroundServiceConnectedDevice 0104074f=string/permdesc_foregroundServiceDataSync 01040750=string/permdesc_foregroundServiceFileManagement 01040751=string/permdesc_foregroundServiceHealth 01040752=string/permdesc_foregroundServiceLocation 01040753=string/permdesc_foregroundServiceMediaPlayback 01040754=string/permdesc_foregroundServiceMediaProcessing 01040755=string/permdesc_foregroundServiceMediaProjection 01040756=string/permdesc_foregroundServiceMicrophone 01040757=string/permdesc_foregroundServicePhoneCall 01040758=string/permdesc_foregroundServiceRemoteMessaging 01040759=string/permdesc_foregroundServiceSpecialUse 0104075a=string/permdesc_foregroundServiceSystemExempted 0104075b=string/permdesc_fullScreenIntent 0104075c=string/permdesc_getAccounts 0104075d=string/permdesc_getPackageSize 0104075e=string/permdesc_getTasks 0104075f=string/permdesc_hand_tracking 01040760=string/permdesc_handoverStatus 01040761=string/permdesc_head_tracking 01040762=string/permdesc_hideOverlayWindows 01040763=string/permdesc_highSamplingRateSensors 01040764=string/permdesc_imagesWrite 01040765=string/permdesc_install_shortcut 01040766=string/permdesc_invokeCarrierSetup 01040767=string/permdesc_killBackgroundProcesses 01040768=string/permdesc_manageFingerprint 01040769=string/permdesc_manageNetworkPolicy 0104076a=string/permdesc_manageOngoingCalls 0104076b=string/permdesc_manageOwnCalls 0104076c=string/permdesc_manageProfileAndDeviceOwners 0104076d=string/permdesc_mediaLocation 0104076e=string/permdesc_modifyAudioSettings 0104076f=string/permdesc_modifyNetworkAccounting 01040770=string/permdesc_nearby_wifi_devices 01040771=string/permdesc_nfc 01040772=string/permdesc_nfcTransactionEvent 01040773=string/permdesc_observeCompanionDevicePresence 01040774=string/permdesc_persistentActivity 01040775=string/permdesc_postNotification 01040776=string/permdesc_preferredPaymentInfo 01040777=string/permdesc_processOutgoingCalls 01040778=string/permdesc_queryAllPackages 01040779=string/permdesc_ranging 0104077a=string/permdesc_readBasicPhoneState 0104077b=string/permdesc_readCalendar 0104077c=string/permdesc_readCallLog 0104077d=string/permdesc_readCellBroadcasts 0104077e=string/permdesc_readContacts 0104077f=string/permdesc_readInstallSessions 01040780=string/permdesc_readMediaAudio 01040781=string/permdesc_readMediaImages 01040782=string/permdesc_readMediaVideo 01040783=string/permdesc_readNetworkUsageHistory 01040784=string/permdesc_readPhoneNumbers 01040785=string/permdesc_readPhoneState 01040786=string/permdesc_readSms 01040787=string/permdesc_readSyncSettings 01040788=string/permdesc_readSyncStats 01040789=string/permdesc_readVisualUserSelect 0104078a=string/permdesc_receiveBootCompleted 0104078b=string/permdesc_receiveMms 0104078c=string/permdesc_receiveSms 0104078d=string/permdesc_receiveWapPush 0104078e=string/permdesc_recordAudio 0104078f=string/permdesc_recordBackgroundAudio 01040790=string/permdesc_register_call_provider 01040791=string/permdesc_register_sim_subscription 01040792=string/permdesc_removeDrmCertificates 01040793=string/permdesc_reorderTasks 01040794=string/permdesc_requestDeletePackages 01040795=string/permdesc_requestIgnoreBatteryOptimizations 01040796=string/permdesc_requestInstallPackages 01040797=string/permdesc_requestPasswordComplexity 01040798=string/permdesc_route_media_output 01040799=string/permdesc_runInBackground 0104079a=string/permdesc_scene_understanding_coarse 0104079b=string/permdesc_scene_understanding_fine 0104079c=string/permdesc_schedule_exact_alarm 0104079d=string/permdesc_sdcardRead 0104079e=string/permdesc_sdcardWrite 0104079f=string/permdesc_sendSms 010407a0=string/permdesc_setAlarm 010407a1=string/permdesc_setInputCalibration 010407a2=string/permdesc_setTimeZone 010407a3=string/permdesc_setWallpaper 010407a4=string/permdesc_setWallpaperHints 010407a5=string/permdesc_sim_communication 010407a6=string/permdesc_startForegroundServicesFromBackground 010407a7=string/permdesc_startReviewPermissionDecisions 010407a8=string/permdesc_startViewAppFeatures 010407a9=string/permdesc_startViewPermissionUsage 010407aa=string/permdesc_statusBar 010407ab=string/permdesc_statusBarService 010407ac=string/permdesc_subscribedFeedsRead 010407ad=string/permdesc_systemAlertWindow 010407ae=string/permdesc_systemCamera 010407af=string/permdesc_transmitIr 010407b0=string/permdesc_turnScreenOn 010407b1=string/permdesc_uninstall_shortcut 010407b2=string/permdesc_updatePackagesWithoutUserAction 010407b3=string/permdesc_useBiometric 010407b4=string/permdesc_useDataInBackground 010407b5=string/permdesc_useFingerprint 010407b6=string/permdesc_use_exact_alarm 010407b7=string/permdesc_use_sip 010407b8=string/permdesc_uwb_ranging 010407b9=string/permdesc_vibrate 010407ba=string/permdesc_vibrator_state 010407bb=string/permdesc_videoWrite 010407bc=string/permdesc_wakeLock 010407bd=string/permdesc_writeCalendar 010407be=string/permdesc_writeCallLog 010407bf=string/permdesc_writeContacts 010407c0=string/permdesc_writeSettings 010407c1=string/permdesc_writeSyncSettings 010407c2=string/permdesc_writeVerificationStateE2eeContactKeys 010407c3=string/permdesc_xr_tracking_in_background 010407c4=string/permgroupdesc_activityRecognition 010407c5=string/permgroupdesc_calendar 010407c6=string/permgroupdesc_calllog 010407c7=string/permgroupdesc_camera 010407c8=string/permgroupdesc_contacts 010407c9=string/permgroupdesc_location 010407ca=string/permgroupdesc_microphone 010407cb=string/permgroupdesc_nearby_devices 010407cc=string/permgroupdesc_notifications 010407cd=string/permgroupdesc_phone 010407ce=string/permgroupdesc_readMediaAural 010407cf=string/permgroupdesc_readMediaVisual 010407d0=string/permgroupdesc_sensors 010407d1=string/permgroupdesc_sms 010407d2=string/permgroupdesc_storage 010407d3=string/permgroupdesc_xr_tracking 010407d4=string/permgroupdesc_xr_tracking_sensitive 010407d5=string/permgrouplab_activityRecognition 010407d6=string/permgrouplab_calendar 010407d7=string/permgrouplab_calllog 010407d8=string/permgrouplab_camera 010407d9=string/permgrouplab_contacts 010407da=string/permgrouplab_location 010407db=string/permgrouplab_microphone 010407dc=string/permgrouplab_nearby_devices 010407dd=string/permgrouplab_notifications 010407de=string/permgrouplab_phone 010407df=string/permgrouplab_readMediaAural 010407e0=string/permgrouplab_readMediaVisual 010407e1=string/permgrouplab_sensors 010407e2=string/permgrouplab_sms 010407e3=string/permgrouplab_storage 010407e4=string/permgrouplab_xr_tracking 010407e5=string/permgrouplab_xr_tracking_sensitive 010407e6=string/permission_request_notification_for_app_with_subtitle 010407e7=string/permission_request_notification_title 010407e8=string/permission_request_notification_with_subtitle 010407e9=string/permlab_acceptHandover 010407ea=string/permlab_accessBackgroundLocation 010407eb=string/permlab_accessCoarseLocation 010407ec=string/permlab_accessDrmCertificates 010407ed=string/permlab_accessFineLocation 010407ee=string/permlab_accessHiddenProfile 010407ef=string/permlab_accessImsCallService 010407f0=string/permlab_accessLastKnownCellId 010407f1=string/permlab_accessLocationExtraCommands 010407f2=string/permlab_accessNetworkConditions 010407f3=string/permlab_accessNetworkState 010407f4=string/permlab_accessNotifications 010407f5=string/permlab_accessWifiState 010407f6=string/permlab_accessWimaxState 010407f7=string/permlab_access_notification_policy 010407f8=string/permlab_activityRecognition 010407f9=string/permlab_addVoicemail 010407fa=string/permlab_answerPhoneCalls 010407fb=string/permlab_audioWrite 010407fc=string/permlab_backgroundCamera 010407fd=string/permlab_bindCarrierMessagingService 010407fe=string/permlab_bindCarrierServices 010407ff=string/permlab_bindCellBroadcastService 01040800=string/permlab_bindConditionProviderService 01040801=string/permlab_bindDreamService 01040802=string/permlab_bindNotificationListenerService 01040803=string/permlab_bind_connection_service 01040804=string/permlab_bind_incall_service 01040805=string/permlab_bluetooth 01040806=string/permlab_bluetoothAdmin 01040807=string/permlab_bluetooth_advertise 01040808=string/permlab_bluetooth_connect 01040809=string/permlab_bluetooth_scan 0104080a=string/permlab_bodySensors 0104080b=string/permlab_bodySensors_background 0104080c=string/permlab_broadcastSticky 0104080d=string/permlab_callCompanionApp 0104080e=string/permlab_callPhone 0104080f=string/permlab_camera 01040810=string/permlab_cameraHeadlessSystemUser 01040811=string/permlab_cameraOpenCloseListener 01040812=string/permlab_changeNetworkState 01040813=string/permlab_changeTetherState 01040814=string/permlab_changeWifiMulticastState 01040815=string/permlab_changeWifiState 01040816=string/permlab_changeWimaxState 01040817=string/permlab_companionProfileWatch 01040818=string/permlab_connection_manager 01040819=string/permlab_control_incall_experience 0104081a=string/permlab_createNetworkSockets 0104081b=string/permlab_deliverCompanionMessages 0104081c=string/permlab_detectScreenCapture 0104081d=string/permlab_disableKeyguard 0104081e=string/permlab_enableCarMode 0104081f=string/permlab_exemptFromAudioRecordRestrictions 01040820=string/permlab_expandStatusBar 01040821=string/permlab_eye_tracking_coarse 01040822=string/permlab_eye_tracking_fine 01040823=string/permlab_face_tracking 01040824=string/permlab_foregroundService 01040825=string/permlab_foregroundServiceCamera 01040826=string/permlab_foregroundServiceConnectedDevice 01040827=string/permlab_foregroundServiceDataSync 01040828=string/permlab_foregroundServiceFileManagement 01040829=string/permlab_foregroundServiceHealth 0104082a=string/permlab_foregroundServiceLocation 0104082b=string/permlab_foregroundServiceMediaPlayback 0104082c=string/permlab_foregroundServiceMediaProcessing 0104082d=string/permlab_foregroundServiceMediaProjection 0104082e=string/permlab_foregroundServiceMicrophone 0104082f=string/permlab_foregroundServicePhoneCall 01040830=string/permlab_foregroundServiceRemoteMessaging 01040831=string/permlab_foregroundServiceSpecialUse 01040832=string/permlab_foregroundServiceSystemExempted 01040833=string/permlab_fullScreenIntent 01040834=string/permlab_getAccounts 01040835=string/permlab_getPackageSize 01040836=string/permlab_getTasks 01040837=string/permlab_hand_tracking 01040838=string/permlab_handoverStatus 01040839=string/permlab_head_tracking 0104083a=string/permlab_hideOverlayWindows 0104083b=string/permlab_highSamplingRateSensors 0104083c=string/permlab_imagesWrite 0104083d=string/permlab_install_shortcut 0104083e=string/permlab_invokeCarrierSetup 0104083f=string/permlab_killBackgroundProcesses 01040840=string/permlab_manageFingerprint 01040841=string/permlab_manageNetworkPolicy 01040842=string/permlab_manageOngoingCalls 01040843=string/permlab_manageOwnCalls 01040844=string/permlab_manageProfileAndDeviceOwners 01040845=string/permlab_mediaLocation 01040846=string/permlab_modifyAudioSettings 01040847=string/permlab_modifyNetworkAccounting 01040848=string/permlab_nearby_wifi_devices 01040849=string/permlab_nfc 0104084a=string/permlab_nfcTransactionEvent 0104084b=string/permlab_observeCompanionDevicePresence 0104084c=string/permlab_persistentActivity 0104084d=string/permlab_postNotification 0104084e=string/permlab_preferredPaymentInfo 0104084f=string/permlab_processOutgoingCalls 01040850=string/permlab_queryAllPackages 01040851=string/permlab_ranging 01040852=string/permlab_readBasicPhoneState 01040853=string/permlab_readCalendar 01040854=string/permlab_readCallLog 01040855=string/permlab_readCellBroadcasts 01040856=string/permlab_readContacts 01040857=string/permlab_readInstallSessions 01040858=string/permlab_readMediaAudio 01040859=string/permlab_readMediaImages 0104085a=string/permlab_readMediaVideo 0104085b=string/permlab_readNetworkUsageHistory 0104085c=string/permlab_readPhoneNumbers 0104085d=string/permlab_readPhoneState 0104085e=string/permlab_readSms 0104085f=string/permlab_readSyncSettings 01040860=string/permlab_readSyncStats 01040861=string/permlab_readVisualUserSelect 01040862=string/permlab_receiveBootCompleted 01040863=string/permlab_receiveMms 01040864=string/permlab_receiveSms 01040865=string/permlab_receiveWapPush 01040866=string/permlab_recordAudio 01040867=string/permlab_recordBackgroundAudio 01040868=string/permlab_register_call_provider 01040869=string/permlab_register_sim_subscription 0104086a=string/permlab_removeDrmCertificates 0104086b=string/permlab_reorderTasks 0104086c=string/permlab_requestDeletePackages 0104086d=string/permlab_requestIgnoreBatteryOptimizations 0104086e=string/permlab_requestInstallPackages 0104086f=string/permlab_requestPasswordComplexity 01040870=string/permlab_route_media_output 01040871=string/permlab_runInBackground 01040872=string/permlab_scene_understanding_coarse 01040873=string/permlab_scene_understanding_fine 01040874=string/permlab_schedule_exact_alarm 01040875=string/permlab_sdcardRead 01040876=string/permlab_sdcardWrite 01040877=string/permlab_sendSms 01040878=string/permlab_setAlarm 01040879=string/permlab_setInputCalibration 0104087a=string/permlab_setTimeZone 0104087b=string/permlab_setWallpaper 0104087c=string/permlab_setWallpaperHints 0104087d=string/permlab_sim_communication 0104087e=string/permlab_startForegroundServicesFromBackground 0104087f=string/permlab_startReviewPermissionDecisions 01040880=string/permlab_startViewAppFeatures 01040881=string/permlab_startViewPermissionUsage 01040882=string/permlab_statusBar 01040883=string/permlab_statusBarService 01040884=string/permlab_subscribedFeedsRead 01040885=string/permlab_systemAlertWindow 01040886=string/permlab_systemCamera 01040887=string/permlab_transmitIr 01040888=string/permlab_turnScreenOn 01040889=string/permlab_uninstall_shortcut 0104088a=string/permlab_updatePackagesWithoutUserAction 0104088b=string/permlab_useBiometric 0104088c=string/permlab_useDataInBackground 0104088d=string/permlab_useFingerprint 0104088e=string/permlab_use_exact_alarm 0104088f=string/permlab_use_sip 01040890=string/permlab_uwb_ranging 01040891=string/permlab_vibrate 01040892=string/permlab_videoWrite 01040893=string/permlab_wakeLock 01040894=string/permlab_writeCalendar 01040895=string/permlab_writeCallLog 01040896=string/permlab_writeContacts 01040897=string/permlab_writeSettings 01040898=string/permlab_writeSyncSettings 01040899=string/permlab_writeVerificationStateE2eeContactKeys 0104089a=string/permlab_xr_tracking_in_background 0104089b=string/perms_description_app 0104089c=string/perms_new_perm_prefix 0104089d=string/personal_apps_suspended_turn_profile_on 0104089e=string/personal_apps_suspension_soon_text 0104089f=string/personal_apps_suspension_text 010408a0=string/personal_apps_suspension_title 010408a1=string/phoneTypeAssistant 010408a2=string/phoneTypeCallback 010408a3=string/phoneTypeCar 010408a4=string/phoneTypeCompanyMain 010408a5=string/phoneTypeCustom 010408a6=string/phoneTypeFaxHome 010408a7=string/phoneTypeFaxWork 010408a8=string/phoneTypeHome 010408a9=string/phoneTypeIsdn 010408aa=string/phoneTypeMain 010408ab=string/phoneTypeMms 010408ac=string/phoneTypeMobile 010408ad=string/phoneTypeOther 010408ae=string/phoneTypeOtherFax 010408af=string/phoneTypePager 010408b0=string/phoneTypeRadio 010408b1=string/phoneTypeTelex 010408b2=string/phoneTypeTtyTdd 010408b3=string/phoneTypeWork 010408b4=string/phoneTypeWorkMobile 010408b5=string/phoneTypeWorkPager 010408b6=string/pin_specific_target 010408b7=string/pin_target 010408b8=string/policydesc_disableCamera 010408b9=string/policydesc_disableKeyguardFeatures 010408ba=string/policydesc_encryptedStorage 010408bb=string/policydesc_expirePassword 010408bc=string/policydesc_forceLock 010408bd=string/policydesc_limitPassword 010408be=string/policydesc_resetPassword 010408bf=string/policydesc_setGlobalProxy 010408c0=string/policydesc_watchLogin 010408c1=string/policydesc_watchLogin_secondaryUser 010408c2=string/policydesc_wipeData 010408c3=string/policydesc_wipeData_secondaryUser 010408c4=string/policylab_disableCamera 010408c5=string/policylab_disableKeyguardFeatures 010408c6=string/policylab_encryptedStorage 010408c7=string/policylab_expirePassword 010408c8=string/policylab_forceLock 010408c9=string/policylab_limitPassword 010408ca=string/policylab_resetPassword 010408cb=string/policylab_setGlobalProxy 010408cc=string/policylab_watchLogin 010408cd=string/policylab_wipeData 010408ce=string/policylab_wipeData_secondaryUser 010408cf=string/popup_window_default_title 010408d0=string/postalTypeCustom 010408d1=string/postalTypeHome 010408d2=string/postalTypeOther 010408d3=string/postalTypeWork 010408d4=string/power_dialog 010408d5=string/power_off 010408d6=string/prefs_bugreport 010408d7=string/prepend_shortcut_label 010408d8=string/preposition_for_date 010408d9=string/preposition_for_time 010408da=string/preposition_for_year 010408db=string/print_service_installed_message 010408dc=string/print_service_installed_title 010408dd=string/printing_disabled_by 010408de=string/private_dns_broken_detailed 010408df=string/private_profile_label_badge 010408e0=string/private_space_biometric_prompt_title 010408e1=string/private_space_deleted_by_admin 010408e2=string/private_space_deleted_by_admin_details 010408e3=string/private_space_set_up_screen_lock_for_reset 010408e4=string/private_space_set_up_screen_lock_message 010408e5=string/profile_encrypted_detail 010408e6=string/profile_encrypted_message 010408e7=string/profile_encrypted_title 010408e8=string/profile_label_clone 010408e9=string/profile_label_communal 010408ea=string/profile_label_private 010408eb=string/profile_label_supervising 010408ec=string/profile_label_test 010408ed=string/profile_label_work 010408ee=string/profile_label_work_2 010408ef=string/profile_label_work_3 010408f0=string/progress_erasing 010408f1=string/prohibit_manual_network_selection_in_gobal_mode 010408f2=string/promotional_notification_channel_label 010408f3=string/quick_contacts_not_available 010408f4=string/radial_numbers_typeface 010408f5=string/rating_label 010408f6=string/reason_service_unavailable 010408f7=string/reason_unknown 010408f8=string/reboot_safemode_confirm 010408f9=string/reboot_safemode_title 010408fa=string/reboot_to_reset_message 010408fb=string/reboot_to_reset_title 010408fc=string/reboot_to_update_package 010408fd=string/reboot_to_update_prepare 010408fe=string/reboot_to_update_reboot 010408ff=string/reboot_to_update_title 01040900=string/recent_tasks_title 01040901=string/recs_notification_channel_label 01040902=string/redacted_notification_action_title 01040903=string/redacted_notification_message 01040904=string/redo 01040905=string/reduce_bright_colors_feature_name 01040906=string/region_picker_section_all 01040907=string/region_picker_section_suggested_bilingual 01040908=string/relationTypeAssistant 01040909=string/relationTypeBrother 0104090a=string/relationTypeChild 0104090b=string/relationTypeCustom 0104090c=string/relationTypeDomesticPartner 0104090d=string/relationTypeFather 0104090e=string/relationTypeFriend 0104090f=string/relationTypeManager 01040910=string/relationTypeMother 01040911=string/relationTypeParent 01040912=string/relationTypePartner 01040913=string/relationTypeReferredBy 01040914=string/relationTypeRelative 01040915=string/relationTypeSister 01040916=string/relationTypeSpouse 01040917=string/relative_time 01040918=string/replace 01040919=string/report 0104091a=string/reset 0104091b=string/resolver_cant_access_personal_apps_explanation 0104091c=string/resolver_cant_access_work_apps_explanation 0104091d=string/resolver_cant_share_with_personal_apps_explanation 0104091e=string/resolver_cant_share_with_work_apps_explanation 0104091f=string/resolver_cross_profile_blocked 01040920=string/resolver_no_personal_apps_available 01040921=string/resolver_no_work_apps_available 01040922=string/resolver_personal_tab 01040923=string/resolver_personal_tab_accessibility 01040924=string/resolver_switch_on_work 01040925=string/resolver_turn_on_work_apps 01040926=string/resolver_work_tab 01040927=string/resolver_work_tab_accessibility 01040928=string/restr_pin_confirm_pin 01040929=string/restr_pin_create_pin 0104092a=string/restr_pin_enter_admin_pin 0104092b=string/restr_pin_enter_new_pin 0104092c=string/restr_pin_enter_old_pin 0104092d=string/restr_pin_enter_pin 0104092e=string/restr_pin_error_doesnt_match 0104092f=string/restr_pin_error_too_short 01040930=string/restr_pin_incorrect 01040931=string/restr_pin_try_later 01040932=string/review_notification_settings_dismiss 01040933=string/review_notification_settings_remind_me_action 01040934=string/review_notification_settings_text 01040935=string/review_notification_settings_title 01040936=string/revoke 01040937=string/ringtone_default 01040938=string/ringtone_default_with_actual 01040939=string/ringtone_picker_title 0104093a=string/ringtone_picker_title_alarm 0104093b=string/ringtone_picker_title_notification 0104093c=string/ringtone_silent 0104093d=string/ringtone_unknown 0104093e=string/roamingText0 0104093f=string/roamingText1 01040940=string/roamingText10 01040941=string/roamingText11 01040942=string/roamingText12 01040943=string/roamingText2 01040944=string/roamingText3 01040945=string/roamingText4 01040946=string/roamingText5 01040947=string/roamingText6 01040948=string/roamingText7 01040949=string/roamingText8 0104094a=string/roamingText9 0104094b=string/roamingTextSearching 0104094c=string/safeMode 0104094d=string/safe_media_volume_warning 0104094e=string/sans_serif 0104094f=string/satellite_access_config_file 01040950=string/satellite_manual_selection_state_popup_cancel 01040951=string/satellite_manual_selection_state_popup_message 01040952=string/satellite_manual_selection_state_popup_ok 01040953=string/satellite_manual_selection_state_popup_title 01040954=string/satellite_messaging_available_notification_summary 01040955=string/satellite_messaging_available_notification_title 01040956=string/satellite_messaging_location_disabled_notification_summary 01040957=string/satellite_messaging_location_disabled_notification_title 01040958=string/satellite_messaging_not_in_allowed_region_notification_summary 01040959=string/satellite_messaging_not_in_allowed_region_notification_title 0104095a=string/satellite_messaging_not_provisioned_notification_summary 0104095b=string/satellite_messaging_not_provisioned_notification_title 0104095c=string/satellite_messaging_not_supported_notification_summary 0104095d=string/satellite_messaging_not_supported_notification_title 0104095e=string/satellite_messaging_unsupported_default_sms_app_notification_summary 0104095f=string/satellite_messaging_unsupported_default_sms_app_notification_title 01040960=string/satellite_notification_how_it_works 01040961=string/satellite_notification_manual_summary 01040962=string/satellite_notification_manual_title 01040963=string/satellite_notification_open_message 01040964=string/satellite_notification_summary 01040965=string/satellite_notification_summary_with_data 01040966=string/satellite_notification_title 01040967=string/satellite_sos_available_notification_summary 01040968=string/satellite_sos_available_notification_title 01040969=string/satellite_sos_location_disabled_notification_summary 0104096a=string/satellite_sos_location_disabled_notification_title 0104096b=string/satellite_sos_not_in_allowed_region_notification_summary 0104096c=string/satellite_sos_not_in_allowed_region_notification_title 0104096d=string/satellite_sos_not_provisioned_notification_summary 0104096e=string/satellite_sos_not_provisioned_notification_title 0104096f=string/satellite_sos_not_supported_notification_summary 01040970=string/satellite_sos_not_supported_notification_title 01040971=string/satellite_sos_unsupported_default_sms_app_notification_summary 01040972=string/satellite_sos_unsupported_default_sms_app_notification_title 01040973=string/scCellularNetworkSecurityLearnMore 01040974=string/scCellularNetworkSecuritySummary 01040975=string/scCellularNetworkSecurityTitle 01040976=string/scIdentifierDisclosureIssueSummary 01040977=string/scIdentifierDisclosureIssueSummaryNotification 01040978=string/scIdentifierDisclosureIssueTitle 01040979=string/scNullCipherIssueActionGotIt 0104097a=string/scNullCipherIssueActionLearnMore 0104097b=string/scNullCipherIssueActionSettings 0104097c=string/scNullCipherIssueEncryptedSummary 0104097d=string/scNullCipherIssueEncryptedTitle 0104097e=string/scNullCipherIssueNonEncryptedSummary 0104097f=string/scNullCipherIssueNonEncryptedSummaryNotification 01040980=string/scNullCipherIssueNonEncryptedTitle 01040981=string/screen_compat_mode_hint 01040982=string/screen_compat_mode_scale 01040983=string/screen_compat_mode_show 01040984=string/screen_lock 01040985=string/screen_lock_app_setting_name 01040986=string/screen_lock_dialog_default_subtitle 01040987=string/screen_not_shared_sensitive_content 01040988=string/screenshot_edit 01040989=string/search_hint 0104098a=string/search_language_hint 0104098b=string/searchview_description_clear 0104098c=string/searchview_description_query 0104098d=string/searchview_description_search 0104098e=string/searchview_description_submit 0104098f=string/searchview_description_voice 01040990=string/second 01040991=string/seconds 01040992=string/select_character 01040993=string/select_day 01040994=string/select_hours 01040995=string/select_input_method 01040996=string/select_keyboard_layout_notification_message 01040997=string/select_keyboard_layout_notification_title 01040998=string/select_minutes 01040999=string/select_multiple_keyboards_layout_notification_title 0104099a=string/select_year 0104099b=string/selected 0104099c=string/sendText 0104099d=string/sending 0104099e=string/sensor_notification_service 0104099f=string/sensor_privacy_notification_channel_label 010409a0=string/sensor_privacy_start_use_camera_notification_content_title 010409a1=string/sensor_privacy_start_use_dialog_turn_on_button 010409a2=string/sensor_privacy_start_use_mic_notification_content_title 010409a3=string/sensor_privacy_start_use_notification_content_text 010409a4=string/serial_number 010409a5=string/serviceClassData 010409a6=string/serviceClassDataAsync 010409a7=string/serviceClassDataSync 010409a8=string/serviceClassFAX 010409a9=string/serviceClassPAD 010409aa=string/serviceClassPacket 010409ab=string/serviceClassSMS 010409ac=string/serviceClassVoice 010409ad=string/serviceDisabled 010409ae=string/serviceEnabled 010409af=string/serviceEnabledFor 010409b0=string/serviceErased 010409b1=string/serviceNotProvisioned 010409b2=string/serviceRegistered 010409b3=string/set_up_screen_lock_action_label 010409b4=string/set_up_screen_lock_title 010409b5=string/sha1_fingerprint 010409b6=string/sha256_fingerprint 010409b7=string/share 010409b8=string/share_action_provider_share_with 010409b9=string/share_remote_bugreport_action 010409ba=string/share_remote_bugreport_notification_message_finished 010409bb=string/share_remote_bugreport_notification_title 010409bc=string/shareactionprovider_share_with 010409bd=string/shareactionprovider_share_with_application 010409be=string/sharing_remote_bugreport_notification_title 010409bf=string/shortcut_disabled_reason_unknown 010409c0=string/shortcut_group_a11y_title 010409c1=string/shortcut_restore_not_supported 010409c2=string/shortcut_restore_signature_mismatch 010409c3=string/shortcut_restore_unknown_issue 010409c4=string/shortcut_restored_on_lower_version 010409c5=string/show_ime 010409c6=string/shutdown_confirm 010409c7=string/shutdown_confirm_question 010409c8=string/shutdown_progress 010409c9=string/silent_mode 010409ca=string/silent_mode_ring 010409cb=string/silent_mode_silent 010409cc=string/silent_mode_vibrate 010409cd=string/sim_added_message 010409ce=string/sim_added_title 010409cf=string/sim_done_button 010409d0=string/sim_removed_message 010409d1=string/sim_removed_title 010409d2=string/sim_restart_button 010409d3=string/sipAddressTypeCustom 010409d4=string/sipAddressTypeHome 010409d5=string/sipAddressTypeOther 010409d6=string/sipAddressTypeWork 010409d7=string/skip_button_label 010409d8=string/slice_more_content 010409d9=string/slices_permission_request 010409da=string/sms_control_message 010409db=string/sms_control_no 010409dc=string/sms_control_title 010409dd=string/sms_control_yes 010409de=string/sms_premium_short_code_details 010409df=string/sms_short_code_confirm_allow 010409e0=string/sms_short_code_confirm_always_allow 010409e1=string/sms_short_code_confirm_deny 010409e2=string/sms_short_code_confirm_message 010409e3=string/sms_short_code_confirm_never_allow 010409e4=string/sms_short_code_details 010409e5=string/sms_short_code_remember_choice 010409e6=string/sms_short_code_remember_undo_instruction 010409e7=string/smv_application 010409e8=string/smv_process 010409e9=string/social_notification_channel_label 010409ea=string/splash_screen_view_branding_description 010409eb=string/splash_screen_view_icon_description 010409ec=string/ssl_ca_cert_noti_by_administrator 010409ed=string/ssl_ca_cert_noti_by_unknown 010409ee=string/ssl_ca_cert_noti_managed 010409ef=string/ssl_ca_cert_warning 010409f0=string/ssl_certificate 010409f1=string/ssl_certificate_is_valid 010409f2=string/status_bar_airplane 010409f3=string/status_bar_alarm_clock 010409f4=string/status_bar_battery 010409f5=string/status_bar_bluetooth 010409f6=string/status_bar_call_strength 010409f7=string/status_bar_camera 010409f8=string/status_bar_cast 010409f9=string/status_bar_cdma_eri 010409fa=string/status_bar_clock 010409fb=string/status_bar_connected_display 010409fc=string/status_bar_data_connection 010409fd=string/status_bar_data_saver 010409fe=string/status_bar_ethernet 010409ff=string/status_bar_headset 01040a00=string/status_bar_hotspot 01040a01=string/status_bar_ime 01040a02=string/status_bar_location 01040a03=string/status_bar_managed_profile 01040a04=string/status_bar_microphone 01040a05=string/status_bar_mobile 01040a06=string/status_bar_mute 01040a07=string/status_bar_nfc 01040a08=string/status_bar_no_calling 01040a09=string/status_bar_oem_satellite 01040a0a=string/status_bar_phone_evdo_signal 01040a0b=string/status_bar_phone_signal 01040a0c=string/status_bar_rotate 01040a0d=string/status_bar_screen_record 01040a0e=string/status_bar_secure 01040a0f=string/status_bar_sensors_off 01040a10=string/status_bar_speakerphone 01040a11=string/status_bar_stacked_mobile 01040a12=string/status_bar_sync_active 01040a13=string/status_bar_sync_failing 01040a14=string/status_bar_tty 01040a15=string/status_bar_volume 01040a16=string/status_bar_vpn 01040a17=string/status_bar_wifi 01040a18=string/status_bar_zen 01040a19=string/stk_cc_ss_to_dial 01040a1a=string/stk_cc_ss_to_dial_video 01040a1b=string/stk_cc_ss_to_ss 01040a1c=string/stk_cc_ss_to_ussd 01040a1d=string/stk_cc_ussd_to_dial 01040a1e=string/stk_cc_ussd_to_dial_video 01040a1f=string/stk_cc_ussd_to_ss 01040a20=string/stk_cc_ussd_to_ussd 01040a21=string/storage_internal 01040a22=string/storage_sd_card 01040a23=string/storage_sd_card_label 01040a24=string/storage_usb 01040a25=string/storage_usb_drive 01040a26=string/storage_usb_drive_label 01040a27=string/submit 01040a28=string/suggested_apps_group_a11y_title 01040a29=string/supervised_user_creation_label 01040a2a=string/suspended_widget_accessibility 01040a2b=string/sync_binding_label 01040a2c=string/sync_do_nothing 01040a2d=string/sync_really_delete 01040a2e=string/sync_too_many_deletes 01040a2f=string/sync_too_many_deletes_desc 01040a30=string/sync_undo_deletes 01040a31=string/system_error_manufacturer 01040a32=string/system_error_wipe_data 01040a33=string/system_locale_title 01040a34=string/system_ui_date_pattern 01040a35=string/taking_remote_bugreport_notification_title 01040a36=string/test_harness_mode_notification_message 01040a37=string/test_harness_mode_notification_title 01040a38=string/textSelectionCABTitle 01040a39=string/time_of_day 01040a3a=string/time_picker_decrement_hour_button 01040a3b=string/time_picker_decrement_minute_button 01040a3c=string/time_picker_decrement_set_am_button 01040a3d=string/time_picker_dialog_title 01040a3e=string/time_picker_header_text 01040a3f=string/time_picker_hour_label 01040a40=string/time_picker_increment_hour_button 01040a41=string/time_picker_increment_minute_button 01040a42=string/time_picker_increment_set_pm_button 01040a43=string/time_picker_input_error 01040a44=string/time_picker_minute_label 01040a45=string/time_picker_mode 01040a46=string/time_picker_prompt_label 01040a47=string/time_picker_radial_mode_description 01040a48=string/time_picker_text_input_mode_description 01040a49=string/time_placeholder 01040a4a=string/time_zone_change_notification_body 01040a4b=string/time_zone_change_notification_title 01040a4c=string/toolbar_collapse_description 01040a4d=string/tooltip_popup_title 01040a4e=string/turn_off_radio 01040a4f=string/turn_on_magnification_settings_action 01040a50=string/turn_on_radio 01040a51=string/tutorial_double_tap_to_zoom_message_short 01040a52=string/twilight_service 01040a53=string/ui_translation_accessibility_translated_text 01040a54=string/ui_translation_accessibility_translation_finished 01040a55=string/unarchival_session_app_label 01040a56=string/undo 01040a57=string/unpin_specific_target 01040a58=string/unpin_target 01040a59=string/unread_convo_overflow 01040a5a=string/unsupported_compile_sdk_check_update 01040a5b=string/unsupported_compile_sdk_message 01040a5c=string/unsupported_compile_sdk_show 01040a5d=string/unsupported_display_size_message 01040a5e=string/unsupported_display_size_show 01040a5f=string/upload_file 01040a60=string/usb_accessory_notification_title 01040a61=string/usb_apm_usb_plugged_in_when_locked_notification_text 01040a62=string/usb_apm_usb_plugged_in_when_locked_notification_title 01040a63=string/usb_apm_usb_suspicious_activity_notification_text 01040a64=string/usb_apm_usb_suspicious_activity_notification_title 01040a65=string/usb_charging_notification_title 01040a66=string/usb_contaminant_detected_message 01040a67=string/usb_contaminant_detected_title 01040a68=string/usb_contaminant_not_detected_message 01040a69=string/usb_contaminant_not_detected_title 01040a6a=string/usb_device_resolve_prompt_warn 01040a6b=string/usb_midi_notification_title 01040a6c=string/usb_midi_peripheral_manufacturer_name 01040a6d=string/usb_midi_peripheral_name 01040a6e=string/usb_midi_peripheral_product_name 01040a6f=string/usb_mtp_launch_notification_description 01040a70=string/usb_mtp_launch_notification_title 01040a71=string/usb_mtp_notification_title 01040a72=string/usb_notification_message 01040a73=string/usb_power_notification_message 01040a74=string/usb_ptp_notification_title 01040a75=string/usb_supplying_notification_title 01040a76=string/usb_tether_notification_title 01040a77=string/usb_unsupported_audio_accessory_message 01040a78=string/usb_unsupported_audio_accessory_title 01040a79=string/usb_uvc_notification_title 01040a7a=string/use_a_different_app 01040a7b=string/user_creation_account_exists 01040a7c=string/user_creation_adding 01040a7d=string/user_logging_out_message 01040a7e=string/user_owner_app_label 01040a7f=string/user_owner_label 01040a81=string/user_switching_message 01040a82=string/validity_period 01040a83=string/vdm_camera_access_denied 01040a84=string/vdm_pip_blocked 01040a85=string/vdm_secure_window 01040a86=string/vendor_required_attestation_revocation_list_url 01040a87=string/view_and_control_notification_content 01040a88=string/view_and_control_notification_title 01040a89=string/volume_alarm 01040a8a=string/volume_bluetooth_call 01040a8b=string/volume_call 01040a8c=string/volume_dialog_ringer_guidance_silent 01040a8d=string/volume_dialog_ringer_guidance_vibrate 01040a8e=string/volume_icon_description_bluetooth 01040a8f=string/volume_icon_description_incall 01040a90=string/volume_icon_description_media 01040a91=string/volume_icon_description_notification 01040a92=string/volume_icon_description_ringer 01040a93=string/volume_music 01040a94=string/volume_music_hint_playing_through_bluetooth 01040a95=string/volume_music_hint_silent_ringtone_selected 01040a96=string/volume_notification 01040a97=string/volume_ringtone 01040a98=string/volume_unknown 01040a99=string/vpn_lockdown_config 01040a9a=string/vpn_lockdown_connected 01040a9b=string/vpn_lockdown_connecting 01040a9c=string/vpn_lockdown_disconnected 01040a9d=string/vpn_lockdown_error 01040a9e=string/vpn_text 01040a9f=string/vpn_text_long 01040aa0=string/vpn_title 01040aa1=string/vpn_title_long 01040aa2=string/vr_listener_binding_label 01040aa3=string/wait 01040aa4=string/wallpaper_binding_label 01040aa5=string/webpage_unresponsive 01040aa6=string/websearch 01040aa7=string/week 01040aa8=string/weeks 01040aa9=string/wfcRegErrorTitle 01040aaa=string/wfcSpnFormat 01040aab=string/wfcSpnFormat_spn 01040aac=string/wfcSpnFormat_spn_vowifi 01040aad=string/wfcSpnFormat_spn_wifi 01040aae=string/wfcSpnFormat_spn_wifi_calling 01040aaf=string/wfcSpnFormat_spn_wifi_calling_vo_hyphen 01040ab0=string/wfcSpnFormat_spn_wlan_call 01040ab1=string/wfcSpnFormat_vowifi 01040ab2=string/wfcSpnFormat_wifi 01040ab3=string/wfcSpnFormat_wifi_call 01040ab4=string/wfcSpnFormat_wifi_calling 01040ab5=string/wfcSpnFormat_wifi_calling_bar_spn 01040ab6=string/wfcSpnFormat_wifi_calling_wo_hyphen 01040ab7=string/wfcSpnFormat_wlan_call 01040ab8=string/wfc_mode_cellular_preferred_summary 01040ab9=string/wfc_mode_wifi_only_summary 01040aba=string/wfc_mode_wifi_preferred_summary 01040abb=string/whichApplication 01040abc=string/whichApplicationLabel 01040abd=string/whichApplicationNamed 01040abe=string/whichEditApplication 01040abf=string/whichEditApplicationLabel 01040ac0=string/whichEditApplicationNamed 01040ac1=string/whichGiveAccessToApplicationLabel 01040ac2=string/whichHomeApplication 01040ac3=string/whichHomeApplicationLabel 01040ac4=string/whichHomeApplicationNamed 01040ac5=string/whichImageCaptureApplication 01040ac6=string/whichImageCaptureApplicationLabel 01040ac7=string/whichImageCaptureApplicationNamed 01040ac8=string/whichOpenHostLinksWith 01040ac9=string/whichOpenHostLinksWithApp 01040aca=string/whichOpenLinksWith 01040acb=string/whichOpenLinksWithApp 01040acc=string/whichSendApplication 01040acd=string/whichSendApplicationLabel 01040ace=string/whichSendApplicationNamed 01040acf=string/whichSendToApplication 01040ad0=string/whichSendToApplicationLabel 01040ad1=string/whichSendToApplicationNamed 01040ad2=string/whichViewApplication 01040ad3=string/whichViewApplicationLabel 01040ad4=string/whichViewApplicationNamed 01040ad5=string/widget_default_class_name 01040ad6=string/widget_default_package_name 01040ad7=string/wifi_available_sign_in 01040ad8=string/wifi_calling_off_summary 01040ad9=string/wifi_no_internet 01040ada=string/wifi_no_internet_detailed 01040adb=string/window_magnification_prompt_content 01040adc=string/window_magnification_prompt_title 01040add=string/wireless_display_route_description 01040ade=string/work_mode_emergency_call_button 01040adf=string/work_mode_off_title 01040ae0=string/work_mode_turn_on 01040ae1=string/work_profile_deleted 01040ae2=string/work_profile_deleted_description_dpm_wipe 01040ae3=string/work_profile_deleted_details 01040ae4=string/work_profile_deleted_reason_maximum_password_failure 01040ae5=string/work_profile_telephony_paused_text 01040ae6=string/work_profile_telephony_paused_title 01040ae7=string/work_profile_telephony_paused_turn_on_button 01040ae8=string/write_fail_reason_cancelled 01040ae9=string/write_fail_reason_cannot_write 01040aea=string/wrong_hsum_configuration_notification_message 01040aeb=string/wrong_hsum_configuration_notification_title 01040aec=string/year 01040aed=string/years 01040aee=string/zen_mode_alarm 01040aef=string/zen_mode_default_events_name 01040af0=string/zen_mode_default_every_night_name 01040af1=string/zen_mode_default_weekends_name 01040af2=string/zen_mode_default_weeknights_name 01040af3=string/zen_mode_downtime_feature_name 01040af4=string/zen_mode_duration_hours 01040af5=string/zen_mode_duration_hours_short 01040af6=string/zen_mode_duration_hours_summary 01040af7=string/zen_mode_duration_hours_summary_short 01040af8=string/zen_mode_duration_minutes 01040af9=string/zen_mode_duration_minutes_short 01040afa=string/zen_mode_duration_minutes_summary 01040afb=string/zen_mode_duration_minutes_summary_short 01040afc=string/zen_mode_feature_name 01040afd=string/zen_mode_forever 01040afe=string/zen_mode_forever_dnd 01040aff=string/zen_mode_implicit_activated 01040b00=string/zen_mode_implicit_deactivated 01040b01=string/zen_mode_implicit_name 01040b02=string/zen_mode_implicit_trigger_description 01040b03=string/zen_mode_rule_name_combination 01040b04=string/zen_mode_trigger_event_calendar_any 01040b05=string/zen_mode_trigger_summary_combined 01040b06=string/zen_mode_trigger_summary_divider_text 01040b07=string/zen_mode_trigger_summary_range_symbol_combination 01040b08=string/zen_mode_trigger_summary_range_words 01040b09=string/zen_mode_until 01040b0a=string/zen_mode_until_next_day 01050000=dimen/app_icon_size 01050001=dimen/thumbnail_height 01050002=dimen/thumbnail_width 01050003=dimen/dialog_min_width_major 01050004=dimen/dialog_min_width_minor 01050005=dimen/notification_large_icon_width 01050006=dimen/notification_large_icon_height 01050007=dimen/config_restrictedIconSize 01050008=dimen/system_app_widget_background_radius 01050009=dimen/system_app_widget_inner_radius 0105000a=dimen/config_viewConfigurationHandwritingGestureLineMargin 0105000b=dimen/__removed_system_app_widget_internal_padding 0105000c=dimen/accessibility_autoclick_scroll_panel_button_size 0105000d=dimen/accessibility_autoclick_type_panel_button_size 0105000e=dimen/accessibility_autoclick_type_panel_button_spacing 0105000f=dimen/accessibility_autoclick_type_panel_divider_height 01050010=dimen/accessibility_autoclick_type_panel_divider_width 01050011=dimen/accessibility_focus_highlight_stroke_width 01050012=dimen/accessibility_fullscreen_magnification_gesture_edge_slop 01050013=dimen/accessibility_icon_foreground_padding_ratio 01050014=dimen/accessibility_magnification_indicator_width 01050015=dimen/accessibility_magnification_thumbnail_container_stroke_width 01050016=dimen/accessibility_magnification_thumbnail_padding 01050017=dimen/accessibility_touch_slop 01050018=dimen/accessibility_window_magnifier_min_size 01050019=dimen/action_bar_button_margin 0105001a=dimen/action_bar_button_max_width 0105001b=dimen/action_bar_content_inset_material 0105001c=dimen/action_bar_content_inset_with_nav 0105001d=dimen/action_bar_default_height 0105001e=dimen/action_bar_default_height_material 0105001f=dimen/action_bar_default_padding_end_material 01050020=dimen/action_bar_default_padding_start_material 01050021=dimen/action_bar_elevation_material 01050022=dimen/action_bar_icon_vertical_padding 01050023=dimen/action_bar_icon_vertical_padding_material 01050024=dimen/action_bar_margin 01050025=dimen/action_bar_margin_end 01050026=dimen/action_bar_margin_start 01050027=dimen/action_bar_overflow_padding_end_material 01050028=dimen/action_bar_overflow_padding_start_material 01050029=dimen/action_bar_stacked_max_height 0105002a=dimen/action_bar_stacked_tab_max_width 0105002b=dimen/action_bar_subtitle_bottom_margin 0105002c=dimen/action_bar_subtitle_bottom_margin_material 0105002d=dimen/action_bar_subtitle_text_size 0105002e=dimen/action_bar_subtitle_top_margin 0105002f=dimen/action_bar_subtitle_top_margin_material 01050030=dimen/action_bar_title_text_size 01050031=dimen/action_bar_toggle_internal_padding 01050032=dimen/action_button_min_height_material 01050033=dimen/action_button_min_width 01050034=dimen/action_button_min_width_material 01050035=dimen/action_button_min_width_overflow_material 01050036=dimen/activity_chooser_popup_min_width 01050037=dimen/activity_embedding_divider_handle_height 01050038=dimen/activity_embedding_divider_handle_height_pressed 01050039=dimen/activity_embedding_divider_handle_radius 0105003a=dimen/activity_embedding_divider_handle_radius_pressed 0105003b=dimen/activity_embedding_divider_handle_width 0105003c=dimen/activity_embedding_divider_handle_width_pressed 0105003d=dimen/activity_embedding_divider_touch_target_height 0105003e=dimen/activity_embedding_divider_touch_target_width 0105003f=dimen/aerr_list_item_height 01050040=dimen/aerr_padding_list_bottom 01050041=dimen/aerr_padding_list_top 01050042=dimen/alertDialog_material_bottom_margin 01050043=dimen/alertDialog_material_letter_spacing_body_1 01050044=dimen/alertDialog_material_letter_spacing_title 01050045=dimen/alertDialog_material_line_height_body_1 01050046=dimen/alertDialog_material_line_height_title 01050047=dimen/alertDialog_material_side_margin 01050048=dimen/alertDialog_material_side_margin_body 01050049=dimen/alertDialog_material_side_margin_title 0105004a=dimen/alertDialog_material_text_size_body_1 0105004b=dimen/alertDialog_material_text_size_title 0105004c=dimen/alertDialog_material_top_margin 0105004d=dimen/alert_dialog_button_bar_height 0105004e=dimen/alert_dialog_button_bar_width 0105004f=dimen/alert_dialog_round_padding 01050050=dimen/alert_dialog_title_height 01050051=dimen/ambient_shadow_alpha 01050052=dimen/app_header_height 01050053=dimen/autofill_button_bar_spacer_height 01050054=dimen/autofill_button_bar_spacer_width 01050055=dimen/autofill_dataset_picker_max_height 01050056=dimen/autofill_dataset_picker_max_width 01050057=dimen/autofill_dialog_corner_radius 01050058=dimen/autofill_dialog_icon_max_height 01050059=dimen/autofill_dialog_icon_size 0105005a=dimen/autofill_dialog_max_width 0105005b=dimen/autofill_dialog_offset 0105005c=dimen/autofill_elevation 0105005d=dimen/autofill_save_button_bar_padding 0105005e=dimen/autofill_save_custom_subtitle_max_height 0105005f=dimen/autofill_save_icon_max_height 01050060=dimen/autofill_save_icon_size 01050061=dimen/autofill_save_inner_padding 01050062=dimen/autofill_save_outer_margin 01050063=dimen/autofill_save_outer_top_padding 01050064=dimen/autofill_save_scroll_view_top_margin 01050065=dimen/autofill_save_title_start_padding 01050066=dimen/back_progress_non_linear_factor 01050067=dimen/base_error_dialog_bottom_padding 01050068=dimen/base_error_dialog_contents_padding 01050069=dimen/base_error_dialog_padding 0105006a=dimen/base_error_dialog_top_padding 0105006b=dimen/btn_drawable_padding 0105006c=dimen/btn_horizontal_edge_padding 0105006d=dimen/btn_lineHeight 0105006e=dimen/btn_material_height 0105006f=dimen/btn_material_width 01050070=dimen/btn_textSize 01050071=dimen/button_bar_layout_end_padding 01050072=dimen/button_bar_layout_start_padding 01050073=dimen/button_bar_layout_top_padding 01050074=dimen/button_elevation_material 01050075=dimen/button_end_margin 01050076=dimen/button_inset_horizontal_material 01050077=dimen/button_inset_vertical_material 01050078=dimen/button_layout_height 01050079=dimen/button_padding_horizontal_material 0105007a=dimen/button_padding_vertical_material 0105007b=dimen/button_pressed_z_material 0105007c=dimen/call_notification_collapsible_indent 0105007d=dimen/call_notification_system_action_min_width 0105007e=dimen/car_action1_size 0105007f=dimen/car_action2_size 01050080=dimen/car_activity_resolver_corner_radius 01050081=dimen/car_activity_resolver_list_item_height 01050082=dimen/car_activity_resolver_list_max_height 01050083=dimen/car_activity_resolver_width 01050084=dimen/car_app_bar_height 01050085=dimen/car_body1_size 01050086=dimen/car_body2_size 01050087=dimen/car_body3_size 01050088=dimen/car_body4_size 01050089=dimen/car_body5_size 0105008a=dimen/car_borderless_button_horizontal_padding 0105008b=dimen/car_button_height 0105008c=dimen/car_button_horizontal_padding 0105008d=dimen/car_button_min_width 0105008e=dimen/car_button_radius 0105008f=dimen/car_card_action_bar_height 01050090=dimen/car_card_header_height 01050091=dimen/car_dialog_action_bar_height 01050092=dimen/car_double_line_list_item_height 01050093=dimen/car_headline1_size 01050094=dimen/car_headline2_size 01050095=dimen/car_headline3_size 01050096=dimen/car_headline4_size 01050097=dimen/car_icon_size 01050098=dimen/car_keyline_1 01050099=dimen/car_keyline_1_keyline_3_diff 0105009a=dimen/car_keyline_2 0105009b=dimen/car_keyline_3 0105009c=dimen/car_keyline_4 0105009d=dimen/car_label1_size 0105009e=dimen/car_label2_size 0105009f=dimen/car_large_avatar_badge_size 010500a0=dimen/car_large_avatar_size 010500a1=dimen/car_list_divider_height 010500a2=dimen/car_margin 010500a3=dimen/car_padding_0 010500a4=dimen/car_padding_1 010500a5=dimen/car_padding_2 010500a6=dimen/car_padding_3 010500a7=dimen/car_padding_4 010500a8=dimen/car_padding_5 010500a9=dimen/car_padding_6 010500aa=dimen/car_pill_button_size 010500ab=dimen/car_preference_category_icon_size 010500ac=dimen/car_preference_icon_size 010500ad=dimen/car_preference_row_vertical_margin 010500ae=dimen/car_primary_icon_size 010500af=dimen/car_progress_bar_height 010500b0=dimen/car_radius_1 010500b1=dimen/car_radius_2 010500b2=dimen/car_radius_3 010500b3=dimen/car_radius_5 010500b4=dimen/car_secondary_icon_size 010500b5=dimen/car_seekbar_height 010500b6=dimen/car_seekbar_padding 010500b7=dimen/car_seekbar_text_overlap 010500b8=dimen/car_seekbar_thumb_size 010500b9=dimen/car_seekbar_thumb_stroke 010500ba=dimen/car_single_line_list_item_height 010500bb=dimen/car_switch_thumb_margin_size 010500bc=dimen/car_switch_thumb_size 010500bd=dimen/car_switch_track_margin_size 010500be=dimen/car_textview_fading_edge_length 010500bf=dimen/car_title2_size 010500c0=dimen/car_title_size 010500c1=dimen/car_touch_target_size 010500c2=dimen/car_touch_target_size_minus_one 010500c3=dimen/cascading_menus_min_smallest_width 010500c4=dimen/chooser_action_button_icon_size 010500c5=dimen/chooser_badge_size 010500c6=dimen/chooser_corner_radius 010500c7=dimen/chooser_direct_share_label_placeholder_max_width 010500c8=dimen/chooser_edge_margin_normal 010500c9=dimen/chooser_edge_margin_thin 010500ca=dimen/chooser_grid_padding 010500cb=dimen/chooser_header_scroll_elevation 010500cc=dimen/chooser_icon_size 010500cd=dimen/chooser_max_collapsed_height 010500ce=dimen/chooser_preview_image_border 010500cf=dimen/chooser_preview_image_font_size 010500d0=dimen/chooser_preview_image_max_dimen 010500d1=dimen/chooser_preview_width 010500d2=dimen/chooser_row_text_option_translate 010500d3=dimen/chooser_view_spacing 010500d4=dimen/chooser_width 010500d5=dimen/circular_display_mask_thickness 010500d6=dimen/config_alertDialogSelectionScrollOffset 010500d7=dimen/config_am_pssToRssThresholdModifier 010500d8=dimen/config_ambiguousGestureMultiplier 010500d9=dimen/config_appTransitionAnimationDurationScaleDefault 010500da=dimen/config_autoKeyboardBrightnessSmoothingConstant 010500db=dimen/config_backGestureInset 010500dc=dimen/config_batterySaver_full_adjustBrightnessFactor 010500dd=dimen/config_bottomDialogCornerRadius 010500de=dimen/config_buttonCornerRadius 010500df=dimen/config_closeToSquareDisplayMaxAspectRatio 010500e0=dimen/config_defaultBinderHeavyHitterAutoSamplerThreshold 010500e1=dimen/config_defaultBinderHeavyHitterWatcherThreshold 010500e2=dimen/config_dialogCornerRadius 010500e3=dimen/config_displayWhiteBalanceBrightnessFilterIntercept 010500e4=dimen/config_displayWhiteBalanceColorTemperatureFilterIntercept 010500e5=dimen/config_displayWhiteBalanceHighLightAmbientColorTemperature 010500e6=dimen/config_displayWhiteBalanceHighLightAmbientColorTemperatureStrong 010500e7=dimen/config_displayWhiteBalanceLowLightAmbientColorTemperature 010500e8=dimen/config_displayWhiteBalanceLowLightAmbientColorTemperatureStrong 010500e9=dimen/config_fixedOrientationLetterboxAspectRatio 010500ea=dimen/config_hapticChannelMaxVibrationAmplitude 010500eb=dimen/config_highResTaskSnapshotScale 010500ec=dimen/config_horizontalScrollFactor 010500ed=dimen/config_hoverTapSlop 010500ee=dimen/config_inCallNotificationVolume 010500ef=dimen/config_keyboardHapticFeedbackFixedAmplitude 010500f0=dimen/config_letterboxBackgroundWallaperDarkScrimAlpha 010500f1=dimen/config_letterboxBackgroundWallpaperBlurRadius 010500f2=dimen/config_letterboxBookModePositionMultiplier 010500f3=dimen/config_letterboxDefaultMinAspectRatioForUnresizableApps 010500f4=dimen/config_letterboxHorizontalPositionMultiplier 010500f5=dimen/config_letterboxTabletopModePositionMultiplier 010500f6=dimen/config_letterboxThinLetterboxHeightDp 010500f7=dimen/config_letterboxThinLetterboxWidthDp 010500f8=dimen/config_letterboxVerticalPositionMultiplier 010500f9=dimen/config_lowResTaskSnapshotScale 010500fa=dimen/config_mediaMetadataBitmapMaxSize 010500fb=dimen/config_minPercentageMultiWindowSupportHeight 010500fc=dimen/config_minPercentageMultiWindowSupportWidth 010500fd=dimen/config_minScalingSpan 010500fe=dimen/config_minScalingTouchMajor 010500ff=dimen/config_minScrollbarTouchTarget 01050100=dimen/config_pictureInPictureExpandedHorizontalHeight 01050101=dimen/config_pictureInPictureExpandedVerticalWidth 01050102=dimen/config_pictureInPictureMaxAspectRatio 01050103=dimen/config_pictureInPictureMinAspectRatio 01050104=dimen/config_prefDialogWidth 01050105=dimen/config_preferredHyphenationFrequency 01050106=dimen/config_progressBarCornerRadius 01050107=dimen/config_qsTileStrokeWidthActive 01050108=dimen/config_qsTileStrokeWidthInactive 01050109=dimen/config_resActivitySnapshotScale 0105010a=dimen/config_rotaryEncoderAxisScrollTickInterval 0105010b=dimen/config_screenBrightnessDimFloat 0105010c=dimen/config_screenBrightnessDozeFloat 0105010d=dimen/config_screenBrightnessMinimumDimAmountFloat 0105010e=dimen/config_screenBrightnessSettingDefaultFloat 0105010f=dimen/config_screenBrightnessSettingMaximumFloat 01050110=dimen/config_screenBrightnessSettingMinimumFloat 01050111=dimen/config_screen_magnification_scaling_threshold 01050112=dimen/config_scrollFactor 01050113=dimen/config_scrollFriction 01050114=dimen/config_scrollbarSize 01050115=dimen/config_signalCutoutHeightFraction 01050116=dimen/config_signalCutoutWidthFraction 01050117=dimen/config_verticalScrollFactor 01050118=dimen/config_viewConfigurationHandwritingSlop 01050119=dimen/config_viewConfigurationHoverSlop 0105011a=dimen/config_viewConfigurationTouchSlop 0105011b=dimen/config_viewMaxFlingVelocity 0105011c=dimen/config_viewMaxRotaryEncoderFlingVelocity 0105011d=dimen/config_viewMinFlingVelocity 0105011e=dimen/config_viewMinRotaryEncoderFlingVelocity 0105011f=dimen/config_wallpaperDimAmount 01050120=dimen/config_wallpaperMaxScale 01050121=dimen/config_wallpaperMinScale 01050122=dimen/config_wearMaterial3_bottomDialogCornerRadius 01050123=dimen/config_wearMaterial3_buttonCornerRadius 01050124=dimen/config_windowManagerCameraCompatAspectRatio 01050125=dimen/content_rect_bottom_clip_allowance 01050126=dimen/control_corner_material 01050127=dimen/control_inset_material 01050128=dimen/control_padding_material 01050129=dimen/controls_thumbnail_image_max_height 0105012a=dimen/controls_thumbnail_image_max_width 0105012b=dimen/conversation_avatar_size 0105012c=dimen/conversation_avatar_size_group_expanded 0105012d=dimen/conversation_badge_protrusion 0105012e=dimen/conversation_badge_protrusion_group_expanded 0105012f=dimen/conversation_badge_protrusion_group_expanded_face_pile 01050130=dimen/conversation_compact_face_pile_avatar_size 01050131=dimen/conversation_compact_face_pile_protection_width 01050132=dimen/conversation_compact_face_pile_size 01050133=dimen/conversation_content_start 01050134=dimen/conversation_expand_button_height 01050135=dimen/conversation_face_pile_avatar_size 01050136=dimen/conversation_face_pile_avatar_size_group_expanded 01050137=dimen/conversation_face_pile_protection_width 01050138=dimen/conversation_face_pile_protection_width_expanded 01050139=dimen/conversation_header_expanded_padding_end 0105013a=dimen/conversation_icon_circle_start 0105013b=dimen/conversation_icon_container_top_padding 0105013c=dimen/conversation_icon_container_top_padding_small_avatar 0105013d=dimen/conversation_icon_size_badged 0105013e=dimen/conversation_image_start_margin 0105013f=dimen/cross_profile_apps_thumbnail_size 01050140=dimen/date_picker_date_label_size 01050141=dimen/date_picker_day_height 01050142=dimen/date_picker_day_of_week_height 01050143=dimen/date_picker_day_of_week_text_size 01050144=dimen/date_picker_day_selector_radius 01050145=dimen/date_picker_day_text_size 01050146=dimen/date_picker_day_width 01050147=dimen/date_picker_month_height 01050148=dimen/date_picker_month_text_size 01050149=dimen/date_picker_year_label_size 0105014a=dimen/datepicker_component_width 0105014b=dimen/datepicker_dialog_width 0105014c=dimen/datepicker_header_height 0105014d=dimen/datepicker_header_text_size 0105014e=dimen/datepicker_list_year_activated_label_size 0105014f=dimen/datepicker_list_year_label_size 01050150=dimen/datepicker_selected_date_day_size 01050151=dimen/datepicker_selected_date_month_size 01050152=dimen/datepicker_selected_date_year_size 01050153=dimen/datepicker_view_animator_height 01050154=dimen/datepicker_year_label_height 01050155=dimen/day_picker_button_margin_top 01050156=dimen/day_picker_padding_horizontal 01050157=dimen/day_picker_padding_top 01050158=dimen/default_app_widget_padding_bottom 01050159=dimen/default_app_widget_padding_left 0105015a=dimen/default_app_widget_padding_right 0105015b=dimen/default_app_widget_padding_top 0105015c=dimen/default_background_blur_radius 0105015d=dimen/default_gap 0105015e=dimen/default_magnifier_corner_radius 0105015f=dimen/default_magnifier_elevation 01050160=dimen/default_magnifier_height 01050161=dimen/default_magnifier_horizontal_offset 01050162=dimen/default_magnifier_vertical_offset 01050163=dimen/default_magnifier_width 01050164=dimen/default_magnifier_zoom 01050165=dimen/default_minimal_size_resizable_task 01050166=dimen/desktop_view_default_header_height 01050167=dimen/dialog_btn_confirm_height 01050168=dimen/dialog_btn_confirm_width 01050169=dimen/dialog_btn_negative_height 0105016a=dimen/dialog_btn_negative_width 0105016b=dimen/dialog_corner_radius 0105016c=dimen/dialog_fixed_height_major 0105016d=dimen/dialog_fixed_height_minor 0105016e=dimen/dialog_fixed_width_major 0105016f=dimen/dialog_fixed_width_minor 01050170=dimen/dialog_list_padding_bottom_no_buttons 01050171=dimen/dialog_list_padding_top_no_title 01050172=dimen/dialog_no_title_padding_top 01050173=dimen/dialog_padding 01050174=dimen/dialog_padding_material 01050175=dimen/dialog_padding_top_material 01050176=dimen/dialog_title_divider_material 01050177=dimen/disabled_alpha_device_default 01050178=dimen/disabled_alpha_leanback_formwizard 01050179=dimen/disabled_alpha_material_dark 0105017a=dimen/disabled_alpha_material_light 0105017b=dimen/disabled_alpha_wear_material3 0105017c=dimen/display_cutout_touchable_region_size 0105017d=dimen/docked_stack_divider_insets 0105017e=dimen/docked_stack_divider_thickness 0105017f=dimen/docked_stack_minimize_thickness 01050180=dimen/dropdownitem_icon_width 01050181=dimen/dropdownitem_text_padding_left 01050182=dimen/dropdownitem_text_padding_right 01050183=dimen/edit_text_inset_bottom_material 01050184=dimen/edit_text_inset_horizontal_material 01050185=dimen/edit_text_inset_top_material 01050186=dimen/emphasized_button_stroke_width 01050187=dimen/expanded_group_conversation_message_padding 01050188=dimen/face_unlock_height 01050189=dimen/fast_scroller_minimum_touch_target 0105018a=dimen/floating_toolbar_height 0105018b=dimen/floating_toolbar_horizontal_margin 0105018c=dimen/floating_toolbar_icon_text_spacing 0105018d=dimen/floating_toolbar_maximum_overflow_height 0105018e=dimen/floating_toolbar_menu_button_minimum_width 0105018f=dimen/floating_toolbar_menu_button_side_padding 01050190=dimen/floating_toolbar_menu_image_button_vertical_padding 01050191=dimen/floating_toolbar_menu_image_button_width 01050192=dimen/floating_toolbar_menu_image_width 01050193=dimen/floating_toolbar_minimum_overflow_height 01050194=dimen/floating_toolbar_overflow_image_button_width 01050195=dimen/floating_toolbar_overflow_side_padding 01050196=dimen/floating_toolbar_preferred_width 01050197=dimen/floating_toolbar_text_size 01050198=dimen/floating_toolbar_vertical_margin 01050199=dimen/floating_window_margin_bottom 0105019a=dimen/floating_window_margin_left 0105019b=dimen/floating_window_margin_right 0105019c=dimen/floating_window_margin_top 0105019d=dimen/floating_window_z 0105019e=dimen/glowpadview_target_placement_radius 0105019f=dimen/handwriting_bounds_offset_bottom 010501a0=dimen/handwriting_bounds_offset_left 010501a1=dimen/handwriting_bounds_offset_right 010501a2=dimen/handwriting_bounds_offset_top 010501a3=dimen/harmful_app_icon_name_padding 010501a4=dimen/harmful_app_icon_size 010501a5=dimen/harmful_app_message_line_spacing_modifier 010501a6=dimen/harmful_app_message_padding_bottom 010501a7=dimen/harmful_app_message_padding_left 010501a8=dimen/harmful_app_message_padding_right 010501a9=dimen/harmful_app_name_padding_bottom 010501aa=dimen/harmful_app_name_padding_left 010501ab=dimen/harmful_app_name_padding_right 010501ac=dimen/harmful_app_name_padding_top 010501ad=dimen/harmful_app_padding_top 010501ae=dimen/highlight_alpha_material_colored 010501af=dimen/highlight_alpha_material_dark 010501b0=dimen/highlight_alpha_material_light 010501b1=dimen/image_margin_start 010501b2=dimen/image_size 010501b3=dimen/immersive_mode_cling_width 010501b4=dimen/importance_ring_anim_max_stroke_width 010501b5=dimen/importance_ring_size 010501b6=dimen/importance_ring_stroke_width 010501b7=dimen/indeterminate_progress_alpha_01 010501b8=dimen/indeterminate_progress_alpha_02 010501b9=dimen/indeterminate_progress_alpha_03 010501ba=dimen/indeterminate_progress_alpha_04 010501bb=dimen/indeterminate_progress_alpha_05 010501bc=dimen/indeterminate_progress_alpha_06 010501bd=dimen/indeterminate_progress_alpha_07 010501be=dimen/indeterminate_progress_alpha_08 010501bf=dimen/indeterminate_progress_alpha_09 010501c0=dimen/indeterminate_progress_alpha_10 010501c1=dimen/indeterminate_progress_alpha_11 010501c2=dimen/indeterminate_progress_alpha_12 010501c3=dimen/indeterminate_progress_alpha_13 010501c4=dimen/indeterminate_progress_alpha_14 010501c5=dimen/indeterminate_progress_alpha_15 010501c6=dimen/indeterminate_progress_alpha_16 010501c7=dimen/indeterminate_progress_alpha_17 010501c8=dimen/indeterminate_progress_alpha_18 010501c9=dimen/indeterminate_progress_alpha_19 010501ca=dimen/indeterminate_progress_alpha_20 010501cb=dimen/indeterminate_progress_alpha_21 010501cc=dimen/indeterminate_progress_alpha_22 010501cd=dimen/indeterminate_progress_alpha_23 010501ce=dimen/indeterminate_progress_alpha_24 010501cf=dimen/indeterminate_progress_alpha_25 010501d0=dimen/indeterminate_progress_alpha_26 010501d1=dimen/indeterminate_progress_alpha_27 010501d2=dimen/indeterminate_progress_alpha_28 010501d3=dimen/indeterminate_progress_alpha_29 010501d4=dimen/indeterminate_progress_alpha_30 010501d5=dimen/indeterminate_progress_alpha_31 010501d6=dimen/indeterminate_progress_alpha_32 010501d7=dimen/indeterminate_progress_alpha_33 010501d8=dimen/indeterminate_progress_alpha_34 010501d9=dimen/indeterminate_progress_alpha_35 010501da=dimen/indeterminate_progress_alpha_36 010501db=dimen/indeterminate_progress_alpha_37 010501dc=dimen/indeterminate_progress_alpha_38 010501dd=dimen/indeterminate_progress_alpha_39 010501de=dimen/indeterminate_progress_alpha_40 010501df=dimen/indeterminate_progress_alpha_41 010501e0=dimen/indeterminate_progress_alpha_42 010501e1=dimen/indeterminate_progress_alpha_43 010501e2=dimen/indeterminate_progress_alpha_44 010501e3=dimen/indeterminate_progress_alpha_45 010501e4=dimen/indeterminate_progress_alpha_46 010501e5=dimen/indeterminate_progress_alpha_47 010501e6=dimen/indeterminate_progress_alpha_48 010501e7=dimen/indeterminate_progress_alpha_49 010501e8=dimen/indeterminate_progress_alpha_50 010501e9=dimen/indeterminate_progress_alpha_51 010501ea=dimen/indeterminate_progress_alpha_52 010501eb=dimen/indeterminate_progress_alpha_53 010501ec=dimen/indeterminate_progress_alpha_54 010501ed=dimen/indeterminate_progress_alpha_55 010501ee=dimen/indeterminate_progress_alpha_56 010501ef=dimen/indeterminate_progress_alpha_57 010501f0=dimen/indeterminate_progress_alpha_58 010501f1=dimen/indeterminate_progress_alpha_59 010501f2=dimen/indeterminate_progress_alpha_60 010501f3=dimen/input_extract_action_button_height 010501f4=dimen/input_extract_action_button_width 010501f5=dimen/input_extract_action_icon_padding 010501f6=dimen/input_method_divider_height 010501f7=dimen/input_method_nav_content_padding 010501f8=dimen/input_method_nav_key_button_ripple_max_width 010501f9=dimen/input_method_navigation_key_padding 010501fa=dimen/input_method_navigation_key_width 010501fb=dimen/item_touch_helper_max_drag_scroll_per_frame 010501fc=dimen/item_touch_helper_swipe_escape_max_velocity 010501fd=dimen/item_touch_helper_swipe_escape_velocity 010501fe=dimen/keyguard_avatar_frame_shadow_radius 010501ff=dimen/keyguard_avatar_frame_stroke_width 01050200=dimen/keyguard_avatar_name_size 01050201=dimen/keyguard_avatar_size 01050202=dimen/keyguard_lockscreen_clock_font_size 01050203=dimen/keyguard_lockscreen_outerring_diameter 01050204=dimen/keyguard_lockscreen_pin_margin_left 01050205=dimen/keyguard_lockscreen_status_line_clockfont_bottom_margin 01050206=dimen/keyguard_lockscreen_status_line_clockfont_top_margin 01050207=dimen/keyguard_lockscreen_status_line_font_right_margin 01050208=dimen/keyguard_lockscreen_status_line_font_size 01050209=dimen/keyguard_muliuser_selector_margin 0105020a=dimen/keyguard_pattern_unlock_clock_font_size 0105020b=dimen/keyguard_pattern_unlock_status_line_font_size 0105020c=dimen/kg_clock_top_margin 0105020d=dimen/kg_edge_swipe_region_size 0105020e=dimen/kg_emergency_button_shift 0105020f=dimen/kg_key_horizontal_gap 01050210=dimen/kg_key_vertical_gap 01050211=dimen/kg_pin_key_height 01050212=dimen/kg_runway_lights_height 01050213=dimen/kg_runway_lights_top_margin 01050214=dimen/kg_runway_lights_vertical_padding 01050215=dimen/kg_secure_padding_height 01050216=dimen/kg_security_panel_height 01050217=dimen/kg_security_view_height 01050218=dimen/kg_small_widget_height 01050219=dimen/kg_squashed_layout_threshold 0105021a=dimen/kg_status_clock_font_size 0105021b=dimen/kg_status_date_font_size 0105021c=dimen/kg_status_line_font_right_margin 0105021d=dimen/kg_status_line_font_size 0105021e=dimen/kg_widget_pager_bottom_padding 0105021f=dimen/kg_widget_pager_horizontal_padding 01050220=dimen/kg_widget_pager_top_padding 01050221=dimen/kg_widget_view_height 01050222=dimen/kg_widget_view_width 01050223=dimen/leanback_button_height 01050224=dimen/leanback_button_padding_horizontal 01050225=dimen/leanback_button_padding_vertical 01050226=dimen/leanback_button_radius 01050227=dimen/leanback_dialog_corner_radius 01050228=dimen/leanback_setup_alpha_activity_in_bkg_end 01050229=dimen/leanback_setup_alpha_activity_in_bkg_start 0105022a=dimen/leanback_setup_alpha_activity_out_bkg_end 0105022b=dimen/leanback_setup_alpha_activity_out_bkg_start 0105022c=dimen/leanback_setup_alpha_animiation_max_opacity 0105022d=dimen/leanback_setup_alpha_animiation_min_opacity 0105022e=dimen/leanback_setup_alpha_backward_in_content_end 0105022f=dimen/leanback_setup_alpha_backward_in_content_start 01050230=dimen/leanback_setup_alpha_backward_out_content_end 01050231=dimen/leanback_setup_alpha_backward_out_content_start 01050232=dimen/leanback_setup_alpha_forward_in_content_end 01050233=dimen/leanback_setup_alpha_forward_in_content_start 01050234=dimen/leanback_setup_alpha_forward_out_content_end 01050235=dimen/leanback_setup_alpha_forward_out_content_start 01050236=dimen/leanback_setup_translation_backward_out_content_end 01050237=dimen/leanback_setup_translation_backward_out_content_end_v4 01050238=dimen/leanback_setup_translation_backward_out_content_start 01050239=dimen/leanback_setup_translation_backward_out_content_start_v4 0105023a=dimen/leanback_setup_translation_content_cliff 0105023b=dimen/leanback_setup_translation_content_resting_point 0105023c=dimen/leanback_setup_translation_forward_in_content_end 0105023d=dimen/leanback_setup_translation_forward_in_content_end_v4 0105023e=dimen/leanback_setup_translation_forward_in_content_start 0105023f=dimen/leanback_setup_translation_forward_in_content_start_v4 01050240=dimen/light_radius 01050241=dimen/light_y 01050242=dimen/light_z 01050243=dimen/list_item_padding_end_material 01050244=dimen/list_item_padding_horizontal_material 01050245=dimen/list_item_padding_start_material 01050246=dimen/list_menu_item_icon_max_width 01050247=dimen/loader_horizontal_min_height_watch 01050248=dimen/loader_horizontal_min_width_watch 01050249=dimen/lock_pattern_dot_hit_factor 0105024a=dimen/lock_pattern_dot_line_width 0105024b=dimen/lock_pattern_dot_size 0105024c=dimen/lock_pattern_dot_size_activated 0105024d=dimen/lock_pattern_fade_away_gradient_width 0105024e=dimen/media_notification_action_button_size 0105024f=dimen/media_notification_actions_padding_bottom 01050250=dimen/media_notification_expanded_image_margin_bottom 01050251=dimen/media_notification_expanded_image_max_size 01050252=dimen/media_notification_header_height 01050253=dimen/message_progress_dialog_bottom_padding 01050254=dimen/message_progress_dialog_end_padding 01050255=dimen/message_progress_dialog_letter_spacing 01050256=dimen/message_progress_dialog_start_padding 01050257=dimen/message_progress_dialog_text_size 01050258=dimen/message_progress_dialog_top_padding 01050259=dimen/messaging_avatar_size 0105025a=dimen/messaging_group_sending_progress_size 0105025b=dimen/messaging_group_singleline_sender_padding_end 0105025c=dimen/messaging_image_extra_spacing 0105025d=dimen/messaging_image_max_height 0105025e=dimen/messaging_image_min_size 0105025f=dimen/messaging_image_rounding 01050260=dimen/messaging_layout_icon_padding_start 01050261=dimen/min_xlarge_screen_width 01050262=dimen/navigation_bar_frame_height 01050263=dimen/navigation_bar_frame_height_landscape 01050264=dimen/navigation_bar_gesture_height 01050265=dimen/navigation_bar_gesture_larger_height 01050266=dimen/navigation_bar_height 01050267=dimen/navigation_bar_height_car_mode 01050268=dimen/navigation_bar_height_landscape 01050269=dimen/navigation_bar_height_landscape_car_mode 0105026a=dimen/navigation_bar_height_portrait 0105026b=dimen/navigation_bar_width 0105026c=dimen/navigation_bar_width_car_mode 0105026d=dimen/navigation_edge_action_progress_threshold 0105026e=dimen/notification_2025_action_list_height 0105026f=dimen/notification_2025_action_list_margin_bottom 01050270=dimen/notification_2025_action_list_min_height 01050271=dimen/notification_2025_action_text_size 01050272=dimen/notification_2025_actions_icon_size 01050273=dimen/notification_2025_actions_margin_start 01050274=dimen/notification_2025_additional_margin 01050275=dimen/notification_2025_badge_baseline 01050276=dimen/notification_2025_badge_margin 01050277=dimen/notification_2025_badge_size 01050278=dimen/notification_2025_content_margin_start 01050279=dimen/notification_2025_content_margin_top 0105027a=dimen/notification_2025_content_min_height 0105027b=dimen/notification_2025_conversation_icon_badge_padding 0105027c=dimen/notification_2025_conversation_icon_badge_position 0105027d=dimen/notification_2025_conversation_icon_badge_size 0105027e=dimen/notification_2025_expand_button_horizontal_icon_padding 0105027f=dimen/notification_2025_expand_button_icon_size 01050280=dimen/notification_2025_expand_button_pill_height 01050281=dimen/notification_2025_expand_button_pill_width 01050282=dimen/notification_2025_expand_button_reduced_end_padding 01050283=dimen/notification_2025_expand_button_right_icon_spacing 01050284=dimen/notification_2025_expand_button_vertical_icon_padding 01050285=dimen/notification_2025_face_pile_avatar_size 01050286=dimen/notification_2025_header_height 01050287=dimen/notification_2025_icon_circle_padding 01050288=dimen/notification_2025_icon_circle_size 01050289=dimen/notification_2025_left_icon_size 0105028a=dimen/notification_2025_margin 0105028b=dimen/notification_2025_media_actions_margin_start 0105028c=dimen/notification_2025_messaging_spacing 0105028d=dimen/notification_2025_min_height 0105028e=dimen/notification_2025_reduced_margin 0105028f=dimen/notification_2025_right_icon_content_margin 01050290=dimen/notification_2025_right_icon_expanded_margin_end 01050291=dimen/notification_2025_right_icon_vertical_margin 01050292=dimen/notification_2025_smart_reply_container_margin 01050293=dimen/notification_action_button_radius 01050294=dimen/notification_action_disabled_alpha 01050295=dimen/notification_action_disabled_container_alpha 01050296=dimen/notification_action_disabled_content_alpha 01050297=dimen/notification_action_emphasized_height 01050298=dimen/notification_action_list_height 01050299=dimen/notification_action_list_margin_top 0105029a=dimen/notification_actions_collapsed_priority_width 0105029b=dimen/notification_actions_icon_drawable_size 0105029c=dimen/notification_actions_icon_size 0105029d=dimen/notification_actions_padding_start 0105029e=dimen/notification_alerted_size 0105029f=dimen/notification_badge_size 010502a0=dimen/notification_big_picture_max_height 010502a1=dimen/notification_big_picture_max_height_low_ram 010502a2=dimen/notification_big_picture_max_width 010502a3=dimen/notification_big_picture_max_width_low_ram 010502a4=dimen/notification_big_title_text_size 010502a5=dimen/notification_close_button_padding 010502a6=dimen/notification_close_button_size 010502a7=dimen/notification_collapsed_height_with_summarization 010502a8=dimen/notification_content_margin 010502a9=dimen/notification_content_margin_end 010502aa=dimen/notification_content_margin_start 010502ab=dimen/notification_content_margin_top 010502ac=dimen/notification_conversation_header_separating_margin 010502ad=dimen/notification_custom_view_max_image_height 010502ae=dimen/notification_custom_view_max_image_height_low_ram 010502af=dimen/notification_custom_view_max_image_width 010502b0=dimen/notification_custom_view_max_image_width_low_ram 010502b1=dimen/notification_expand_button_icon_padding 010502b2=dimen/notification_expand_button_pill_height 010502b3=dimen/notification_feedback_size 010502b4=dimen/notification_grayscale_icon_max_size 010502b5=dimen/notification_header_app_name_margin_start 010502b6=dimen/notification_header_background_height 010502b7=dimen/notification_header_expand_icon_size 010502b8=dimen/notification_header_height 010502b9=dimen/notification_header_icon_size 010502ba=dimen/notification_header_icon_size_ambient 010502bb=dimen/notification_header_margin_bottom 010502bc=dimen/notification_header_padding_bottom 010502bd=dimen/notification_header_padding_top 010502be=dimen/notification_header_separating_margin 010502bf=dimen/notification_header_shrink_hide_width 010502c0=dimen/notification_header_shrink_min_width 010502c1=dimen/notification_header_touchable_height 010502c2=dimen/notification_headerless_line_height 010502c3=dimen/notification_headerless_margin_oneline 010502c4=dimen/notification_headerless_margin_twoline 010502c5=dimen/notification_headerless_min_height 010502c6=dimen/notification_heading_margin_end 010502c7=dimen/notification_icon_circle_padding 010502c8=dimen/notification_icon_circle_size 010502c9=dimen/notification_icon_circle_start 010502ca=dimen/notification_inbox_item_top_padding 010502cb=dimen/notification_left_icon_size 010502cc=dimen/notification_left_icon_start 010502cd=dimen/notification_messaging_spacing 010502ce=dimen/notification_messaging_spacing_conversation_group 010502cf=dimen/notification_min_height 010502d0=dimen/notification_person_icon_max_size 010502d1=dimen/notification_person_icon_max_size_low_ram 010502d2=dimen/notification_phishing_alert_size 010502d3=dimen/notification_progress_bar_height 010502d4=dimen/notification_progress_icon_size 010502d5=dimen/notification_progress_margin_horizontal 010502d6=dimen/notification_progress_margin_top 010502d7=dimen/notification_progress_points_corner_radius 010502d8=dimen/notification_progress_points_inset 010502d9=dimen/notification_progress_points_radius 010502da=dimen/notification_progress_segPoint_gap 010502db=dimen/notification_progress_segSeg_gap 010502dc=dimen/notification_progress_segments_corner_radius 010502dd=dimen/notification_progress_segments_faded_height 010502de=dimen/notification_progress_segments_height 010502df=dimen/notification_progress_segments_min_width 010502e0=dimen/notification_progress_tracker_height 010502e1=dimen/notification_progress_tracker_width 010502e2=dimen/notification_right_icon_big_margin_top 010502e3=dimen/notification_right_icon_content_margin 010502e4=dimen/notification_right_icon_headerless_margin 010502e5=dimen/notification_right_icon_size 010502e6=dimen/notification_right_icon_size_low_ram 010502e7=dimen/notification_secondary_text_disabled_alpha 010502e8=dimen/notification_small_icon_size 010502e9=dimen/notification_small_icon_size_low_ram 010502ea=dimen/notification_subtext_size 010502eb=dimen/notification_text_height 010502ec=dimen/notification_text_margin_top 010502ed=dimen/notification_text_size 010502ee=dimen/notification_title_text_size 010502ef=dimen/notification_top_pad 010502f0=dimen/notification_top_pad_large_text 010502f1=dimen/notification_top_pad_large_text_narrow 010502f2=dimen/notification_top_pad_narrow 010502f3=dimen/notification_verification_icon_size 010502f4=dimen/password_keyboard_height 010502f5=dimen/password_keyboard_horizontalGap 010502f6=dimen/password_keyboard_key_height_alpha 010502f7=dimen/password_keyboard_key_height_numeric 010502f8=dimen/password_keyboard_spacebar_vertical_correction 010502f9=dimen/password_keyboard_verticalGap 010502fa=dimen/picker_bottom_margin 010502fb=dimen/picker_top_margin 010502fc=dimen/pip_minimized_visible_size 010502fd=dimen/popup_enter_animation_from_y_delta 010502fe=dimen/popup_exit_animation_to_y_delta 010502ff=dimen/preference_breadcrumb_paddingLeft 01050300=dimen/preference_breadcrumb_paddingRight 01050301=dimen/preference_breadcrumbs_padding_end_material 01050302=dimen/preference_breadcrumbs_padding_start_material 01050303=dimen/preference_child_padding_side 01050304=dimen/preference_fragment_padding_bottom 01050305=dimen/preference_fragment_padding_side 01050306=dimen/preference_fragment_padding_side_material 01050307=dimen/preference_fragment_padding_vertical_material 01050308=dimen/preference_icon_minWidth 01050309=dimen/preference_item_padding_inner 0105030a=dimen/preference_item_padding_side 0105030b=dimen/preference_screen_bottom_margin 0105030c=dimen/preference_screen_header_padding_side 0105030d=dimen/preference_screen_header_padding_side_material 0105030e=dimen/preference_screen_header_vertical_padding 0105030f=dimen/preference_screen_header_vertical_padding_material 01050310=dimen/preference_screen_side_margin 01050311=dimen/preference_screen_side_margin_material 01050312=dimen/preference_screen_side_margin_negative 01050313=dimen/preference_screen_side_margin_negative_material 01050314=dimen/preference_screen_top_margin 01050315=dimen/preference_widget_width 01050316=dimen/primary_content_alpha_device_default 01050317=dimen/primary_content_alpha_material_dark 01050318=dimen/primary_content_alpha_material_light 01050319=dimen/primary_content_alpha_wear_material3 0105031a=dimen/progress_bar_corner_material 0105031b=dimen/progress_bar_height 0105031c=dimen/progress_bar_height_material 0105031d=dimen/progress_bar_size_large 0105031e=dimen/progress_bar_size_medium 0105031f=dimen/progress_bar_size_small 01050320=dimen/progressbar_elevation 01050321=dimen/progressbar_inner_radius_ratio 01050322=dimen/progressbar_thickness 01050323=dimen/quick_qs_offset_height 01050324=dimen/resolver_badge_size 01050325=dimen/resolver_button_bar_spacing 01050326=dimen/resolver_edge_margin 01050327=dimen/resolver_elevation 01050328=dimen/resolver_empty_state_container_padding_bottom 01050329=dimen/resolver_empty_state_container_padding_top 0105032a=dimen/resolver_empty_state_height 0105032b=dimen/resolver_empty_state_height_with_tabs 0105032c=dimen/resolver_icon_margin 0105032d=dimen/resolver_icon_size 0105032e=dimen/resolver_max_collapsed_height 0105032f=dimen/resolver_max_collapsed_height_with_default 01050330=dimen/resolver_max_collapsed_height_with_default_with_tabs 01050331=dimen/resolver_max_collapsed_height_with_tabs 01050332=dimen/resolver_max_width 01050333=dimen/resolver_profile_tab_margin 01050334=dimen/resolver_small_margin 01050335=dimen/resolver_tab_text_size 01050336=dimen/resolver_title_padding_bottom 01050337=dimen/restricted_icon_size_material 01050338=dimen/round_scrollbar_width 01050339=dimen/rounded_corner_content_padding 0105033a=dimen/rounded_corner_radius 0105033b=dimen/rounded_corner_radius_adjustment 0105033c=dimen/rounded_corner_radius_bottom 0105033d=dimen/rounded_corner_radius_bottom_adjustment 0105033e=dimen/rounded_corner_radius_top 0105033f=dimen/rounded_corner_radius_top_adjustment 01050340=dimen/screen_percentage_0416 01050341=dimen/screen_percentage_05 01050342=dimen/screen_percentage_052 01050343=dimen/screen_percentage_10 01050344=dimen/screen_percentage_12 01050345=dimen/screen_percentage_15 01050346=dimen/screen_percentage_3646 01050347=dimen/search_view_preferred_height 01050348=dimen/search_view_preferred_width 01050349=dimen/secondary_content_alpha_device_default 0105034a=dimen/secondary_content_alpha_material_dark 0105034b=dimen/secondary_content_alpha_material_light 0105034c=dimen/secondary_rounded_corner_radius 0105034d=dimen/secondary_rounded_corner_radius_adjustment 0105034e=dimen/secondary_rounded_corner_radius_bottom 0105034f=dimen/secondary_rounded_corner_radius_bottom_adjustment 01050350=dimen/secondary_rounded_corner_radius_top 01050351=dimen/secondary_rounded_corner_radius_top_adjustment 01050352=dimen/secondary_waterfall_display_bottom_edge_size 01050353=dimen/secondary_waterfall_display_left_edge_size 01050354=dimen/secondary_waterfall_display_right_edge_size 01050355=dimen/secondary_waterfall_display_top_edge_size 01050356=dimen/seekbar_thumb_exclusion_max_size 01050357=dimen/seekbar_track_background_height_material 01050358=dimen/seekbar_track_progress_height_material 01050359=dimen/select_dialog_drawable_padding_start_material 0105035a=dimen/select_dialog_padding_start_material 0105035b=dimen/slice_icon_size 0105035c=dimen/slice_padding 0105035d=dimen/slice_shortcut_size 0105035e=dimen/snooze_and_bubble_gone_padding_end 0105035f=dimen/spot_shadow_alpha 01050360=dimen/starting_surface_default_icon_size 01050361=dimen/starting_surface_icon_size 01050362=dimen/status_bar_content_number_size 01050363=dimen/status_bar_edge_ignore 01050364=dimen/status_bar_height 01050365=dimen/status_bar_height_default 01050366=dimen/status_bar_height_landscape 01050367=dimen/status_bar_height_portrait 01050368=dimen/status_bar_icon_size 01050369=dimen/status_bar_icon_size_sp 0105036a=dimen/status_bar_system_icon_intrinsic_size 0105036b=dimen/status_bar_system_icon_size 0105036c=dimen/subtitle_corner_radius 0105036d=dimen/subtitle_outline_width 0105036e=dimen/subtitle_shadow_offset 0105036f=dimen/subtitle_shadow_radius 01050370=dimen/system_gestures_distance_threshold 01050371=dimen/system_gestures_start_threshold 01050372=dimen/task_height_of_minimized_mode 01050373=dimen/taskbar_frame_height 01050374=dimen/text_edit_floating_toolbar_elevation 01050375=dimen/text_edit_floating_toolbar_margin 01050376=dimen/text_handle_min_size 01050377=dimen/text_line_spacing_multiplier_material 01050378=dimen/text_size_body_1_material 01050379=dimen/text_size_body_2_material 0105037a=dimen/text_size_button_material 0105037b=dimen/text_size_caption_material 0105037c=dimen/text_size_display_1_material 0105037d=dimen/text_size_display_2_material 0105037e=dimen/text_size_display_3_material 0105037f=dimen/text_size_display_4_material 01050380=dimen/text_size_headline_material 01050381=dimen/text_size_large_material 01050382=dimen/text_size_medium_material 01050383=dimen/text_size_menu_header_material 01050384=dimen/text_size_menu_material 01050385=dimen/text_size_small_material 01050386=dimen/text_size_subhead_material 01050387=dimen/text_size_subtitle_material_toolbar 01050388=dimen/text_size_title_material 01050389=dimen/text_size_title_material_toolbar 0105038a=dimen/text_view_end_margin 0105038b=dimen/text_view_start_margin 0105038c=dimen/textview_error_popup_default_width 0105038d=dimen/timepicker_am_top_padding 0105038e=dimen/timepicker_ampm_horizontal_padding 0105038f=dimen/timepicker_ampm_label_size 01050390=dimen/timepicker_center_dot_radius 01050391=dimen/timepicker_edit_text_size 01050392=dimen/timepicker_header_height 01050393=dimen/timepicker_left_side_width 01050394=dimen/timepicker_pm_top_padding 01050395=dimen/timepicker_radial_picker_dimen 01050396=dimen/timepicker_radial_picker_horizontal_margin 01050397=dimen/timepicker_radial_picker_top_margin 01050398=dimen/timepicker_selector_dot_radius 01050399=dimen/timepicker_selector_radius 0105039a=dimen/timepicker_selector_stroke 0105039b=dimen/timepicker_separator_padding 0105039c=dimen/timepicker_text_inset_inner 0105039d=dimen/timepicker_text_inset_normal 0105039e=dimen/timepicker_text_size_inner 0105039f=dimen/timepicker_text_size_normal 010503a0=dimen/timepicker_time_label_size 010503a1=dimen/toast_elevation 010503a2=dimen/toast_text_size 010503a3=dimen/toast_width 010503a4=dimen/toast_y_offset 010503a5=dimen/tooltip_corner_radius 010503a6=dimen/tooltip_corner_radius_material 010503a7=dimen/tooltip_font_size 010503a8=dimen/tooltip_font_size_material 010503a9=dimen/tooltip_horizontal_padding 010503aa=dimen/tooltip_horizontal_padding_material 010503ab=dimen/tooltip_margin 010503ac=dimen/tooltip_precise_anchor_extra_offset 010503ad=dimen/tooltip_precise_anchor_threshold 010503ae=dimen/tooltip_vertical_padding 010503af=dimen/tooltip_vertical_padding_material 010503b0=dimen/tooltip_y_offset_non_touch 010503b1=dimen/tooltip_y_offset_touch 010503b2=dimen/user_icon_size 010503b3=dimen/waterfall_display_bottom_edge_size 010503b4=dimen/waterfall_display_left_edge_size 010503b5=dimen/waterfall_display_right_edge_size 010503b6=dimen/waterfall_display_top_edge_size 01060000=color/darker_gray 01060001=color/primary_text_dark 01060002=color/primary_text_dark_nodisable 01060003=color/primary_text_light 01060004=color/primary_text_light_nodisable 01060005=color/secondary_text_dark 01060006=color/secondary_text_dark_nodisable 01060007=color/secondary_text_light 01060008=color/secondary_text_light_nodisable 01060009=color/tab_indicator_text 0106000a=color/widget_edittext_dark 0106000b=color/white 0106000c=color/black 0106000d=color/transparent 0106000e=color/background_dark 0106000f=color/background_light 01060010=color/tertiary_text_dark 01060011=color/tertiary_text_light 01060012=color/holo_blue_light 01060013=color/holo_blue_dark 01060014=color/holo_green_light 01060015=color/holo_green_dark 01060016=color/holo_red_light 01060017=color/holo_red_dark 01060018=color/holo_orange_light 01060019=color/holo_orange_dark 0106001a=color/holo_purple 0106001b=color/holo_blue_bright 0106001c=color/system_notification_accent_color 0106001d=color/system_neutral1_0 0106001e=color/system_neutral1_10 0106001f=color/system_neutral1_50 01060020=color/system_neutral1_100 01060021=color/system_neutral1_200 01060022=color/system_neutral1_300 01060023=color/system_neutral1_400 01060024=color/system_neutral1_500 01060025=color/system_neutral1_600 01060026=color/system_neutral1_700 01060027=color/system_neutral1_800 01060028=color/system_neutral1_900 01060029=color/system_neutral1_1000 0106002a=color/system_neutral2_0 0106002b=color/system_neutral2_10 0106002c=color/system_neutral2_50 0106002d=color/system_neutral2_100 0106002e=color/system_neutral2_200 0106002f=color/system_neutral2_300 01060030=color/system_neutral2_400 01060031=color/system_neutral2_500 01060032=color/system_neutral2_600 01060033=color/system_neutral2_700 01060034=color/system_neutral2_800 01060035=color/system_neutral2_900 01060036=color/system_neutral2_1000 01060037=color/system_accent1_0 01060038=color/system_accent1_10 01060039=color/system_accent1_50 0106003a=color/system_accent1_100 0106003b=color/system_accent1_200 0106003c=color/system_accent1_300 0106003d=color/system_accent1_400 0106003e=color/system_accent1_500 0106003f=color/system_accent1_600 01060040=color/system_accent1_700 01060041=color/system_accent1_800 01060042=color/system_accent1_900 01060043=color/system_accent1_1000 01060044=color/system_accent2_0 01060045=color/system_accent2_10 01060046=color/system_accent2_50 01060047=color/system_accent2_100 01060048=color/system_accent2_200 01060049=color/system_accent2_300 0106004a=color/system_accent2_400 0106004b=color/system_accent2_500 0106004c=color/system_accent2_600 0106004d=color/system_accent2_700 0106004e=color/system_accent2_800 0106004f=color/system_accent2_900 01060050=color/system_accent2_1000 01060051=color/system_accent3_0 01060052=color/system_accent3_10 01060053=color/system_accent3_50 01060054=color/system_accent3_100 01060055=color/system_accent3_200 01060056=color/system_accent3_300 01060057=color/system_accent3_400 01060058=color/system_accent3_500 01060059=color/system_accent3_600 0106005a=color/system_accent3_700 0106005b=color/system_accent3_800 0106005c=color/system_accent3_900 0106005d=color/system_accent3_1000 0106005e=color/system_primary_container_light 0106005f=color/system_on_primary_container_light 01060060=color/system_primary_light 01060061=color/system_on_primary_light 01060062=color/system_secondary_container_light 01060063=color/system_on_secondary_container_light 01060064=color/system_secondary_light 01060065=color/system_on_secondary_light 01060066=color/system_tertiary_container_light 01060067=color/system_on_tertiary_container_light 01060068=color/system_tertiary_light 01060069=color/system_on_tertiary_light 0106006a=color/system_background_light 0106006b=color/system_on_background_light 0106006c=color/system_surface_light 0106006d=color/system_on_surface_light 0106006e=color/system_surface_container_low_light 0106006f=color/system_surface_container_lowest_light 01060070=color/system_surface_container_light 01060071=color/system_surface_container_high_light 01060072=color/system_surface_container_highest_light 01060073=color/system_surface_bright_light 01060074=color/system_surface_dim_light 01060075=color/system_surface_variant_light 01060076=color/system_on_surface_variant_light 01060077=color/system_outline_light 01060078=color/system_error_light 01060079=color/system_on_error_light 0106007a=color/system_error_container_light 0106007b=color/system_on_error_container_light 0106007c=color/system_control_activated_light 0106007d=color/system_control_normal_light 0106007e=color/system_control_highlight_light 0106007f=color/system_text_primary_inverse_light 01060080=color/system_text_secondary_and_tertiary_inverse_light 01060081=color/system_text_primary_inverse_disable_only_light 01060082=color/system_text_secondary_and_tertiary_inverse_disabled_light 01060083=color/system_text_hint_inverse_light 01060084=color/system_palette_key_color_primary_light 01060085=color/system_palette_key_color_secondary_light 01060086=color/system_palette_key_color_tertiary_light 01060087=color/system_palette_key_color_neutral_light 01060088=color/system_palette_key_color_neutral_variant_light 01060089=color/system_primary_container_dark 0106008a=color/system_on_primary_container_dark 0106008b=color/system_primary_dark 0106008c=color/system_on_primary_dark 0106008d=color/system_secondary_container_dark 0106008e=color/system_on_secondary_container_dark 0106008f=color/system_secondary_dark 01060090=color/system_on_secondary_dark 01060091=color/system_tertiary_container_dark 01060092=color/system_on_tertiary_container_dark 01060093=color/system_tertiary_dark 01060094=color/system_on_tertiary_dark 01060095=color/system_background_dark 01060096=color/system_on_background_dark 01060097=color/system_surface_dark 01060098=color/system_on_surface_dark 01060099=color/system_surface_container_low_dark 0106009a=color/system_surface_container_lowest_dark 0106009b=color/system_surface_container_dark 0106009c=color/system_surface_container_high_dark 0106009d=color/system_surface_container_highest_dark 0106009e=color/system_surface_bright_dark 0106009f=color/system_surface_dim_dark 010600a0=color/system_surface_variant_dark 010600a1=color/system_on_surface_variant_dark 010600a2=color/system_outline_dark 010600a3=color/system_error_dark 010600a4=color/system_on_error_dark 010600a5=color/system_error_container_dark 010600a6=color/system_on_error_container_dark 010600a7=color/system_control_activated_dark 010600a8=color/system_control_normal_dark 010600a9=color/system_control_highlight_dark 010600aa=color/system_text_primary_inverse_dark 010600ab=color/system_text_secondary_and_tertiary_inverse_dark 010600ac=color/system_text_primary_inverse_disable_only_dark 010600ad=color/system_text_secondary_and_tertiary_inverse_disabled_dark 010600ae=color/system_text_hint_inverse_dark 010600af=color/system_palette_key_color_primary_dark 010600b0=color/system_palette_key_color_secondary_dark 010600b1=color/system_palette_key_color_tertiary_dark 010600b2=color/system_palette_key_color_neutral_dark 010600b3=color/system_palette_key_color_neutral_variant_dark 010600b4=color/system_primary_fixed 010600b5=color/system_primary_fixed_dim 010600b6=color/system_on_primary_fixed 010600b7=color/system_on_primary_fixed_variant 010600b8=color/system_secondary_fixed 010600b9=color/system_secondary_fixed_dim 010600ba=color/system_on_secondary_fixed 010600bb=color/system_on_secondary_fixed_variant 010600bc=color/system_tertiary_fixed 010600bd=color/system_tertiary_fixed_dim 010600be=color/system_on_tertiary_fixed 010600bf=color/system_on_tertiary_fixed_variant 010600c0=color/system_outline_variant_light 010600c1=color/system_outline_variant_dark 010600c2=color/system_surface_disabled 010600c3=color/system_on_surface_disabled 010600c4=color/system_outline_disabled 010600c5=color/system_error_0 010600c6=color/system_error_10 010600c7=color/system_error_50 010600c8=color/system_error_100 010600c9=color/system_error_200 010600ca=color/system_error_300 010600cb=color/system_error_400 010600cc=color/system_error_500 010600cd=color/system_error_600 010600ce=color/system_error_700 010600cf=color/system_error_800 010600d0=color/system_error_900 010600d1=color/system_error_1000 010600d2=color/GM2_grey_800 010600d3=color/SIM_color_blue 010600d4=color/SIM_color_cyan 010600d5=color/SIM_color_green 010600d6=color/SIM_color_orange 010600d7=color/SIM_color_pink 010600d8=color/SIM_color_purple 010600d9=color/SIM_dark_mode_color_blue 010600da=color/SIM_dark_mode_color_cyan 010600db=color/SIM_dark_mode_color_green 010600dc=color/SIM_dark_mode_color_orange 010600dd=color/SIM_dark_mode_color_pink 010600de=color/SIM_dark_mode_color_purple 010600df=color/accent_device_default 010600e0=color/accent_device_default_dark 010600e1=color/accent_device_default_light 010600e2=color/accent_material_dark 010600e3=color/accent_material_light 010600e4=color/accent_primary_device_default 010600e5=color/accent_primary_variant_dark_device_default 010600e6=color/accent_primary_variant_light_device_default 010600e7=color/accent_secondary_device_default 010600e8=color/accent_secondary_variant_dark_device_default 010600e9=color/accent_secondary_variant_light_device_default 010600ea=color/accent_tertiary_device_default 010600eb=color/accent_tertiary_variant_dark_device_default 010600ec=color/accent_tertiary_variant_light_device_default 010600ed=color/accessibility_autoclick_background 010600ee=color/accessibility_color_inversion_background 010600ef=color/accessibility_daltonizer_background 010600f0=color/accessibility_feature_background 010600f1=color/accessibility_focus_highlight_color 010600f2=color/accessibility_magnification_background 010600f3=color/accessibility_magnification_thumbnail_background_color 010600f4=color/accessibility_magnification_thumbnail_container_background_color 010600f5=color/accessibility_magnification_thumbnail_container_stroke_color 010600f6=color/accessibility_magnification_thumbnail_stroke_color 010600f7=color/activity_embedding_divider_color 010600f8=color/activity_embedding_divider_color_pressed 010600f9=color/autofill_background_material_dark 010600fa=color/autofill_background_material_light 010600fb=color/autofilled_highlight 010600fc=color/background_cache_hint_selector_device_default 010600fd=color/background_cache_hint_selector_holo_dark 010600fe=color/background_cache_hint_selector_holo_light 010600ff=color/background_cache_hint_selector_material_dark 01060100=color/background_cache_hint_selector_material_light 01060101=color/background_device_default_dark 01060102=color/background_device_default_light 01060103=color/background_floating_device_default_dark 01060104=color/background_floating_device_default_light 01060105=color/background_floating_material_dark 01060106=color/background_floating_material_light 01060107=color/background_holo_dark 01060108=color/background_holo_light 01060109=color/background_leanback_dark 0106010a=color/background_material_dark 0106010b=color/background_material_light 0106010c=color/bright_foreground_dark 0106010d=color/bright_foreground_dark_disabled 0106010e=color/bright_foreground_dark_inverse 0106010f=color/bright_foreground_disabled_holo_dark 01060110=color/bright_foreground_disabled_holo_light 01060111=color/bright_foreground_holo_dark 01060112=color/bright_foreground_holo_light 01060113=color/bright_foreground_inverse_holo_dark 01060114=color/bright_foreground_inverse_holo_light 01060115=color/bright_foreground_light 01060116=color/bright_foreground_light_disabled 01060117=color/bright_foreground_light_inverse 01060118=color/btn_colored_background_material 01060119=color/btn_colored_borderless_text_material 0106011a=color/btn_colored_text_material 0106011b=color/btn_default_material_dark 0106011c=color/btn_default_material_light 0106011d=color/btn_leanback_color 0106011e=color/btn_leanback_focused 0106011f=color/btn_leanback_text_color 01060120=color/btn_leanback_unfocused 01060121=color/btn_material_filled_background_color_watch 01060122=color/btn_material_filled_content_color_watch 01060123=color/btn_material_filled_tonal_background_color_watch 01060124=color/btn_material_filled_tonal_content_color_watch 01060125=color/btn_material_outlined_background_color_watch 01060126=color/btn_text_leanback_focused 01060127=color/btn_text_leanback_unfocused 01060128=color/btn_watch_default_dark 01060129=color/button_material_dark 0106012a=color/button_material_light 0106012b=color/button_normal_device_default_dark 0106012c=color/call_notification_answer_color 0106012d=color/call_notification_decline_color 0106012e=color/car_accent 0106012f=color/car_accent_dark 01060130=color/car_accent_light 01060131=color/car_action1 01060132=color/car_action1_dark 01060133=color/car_action1_light 01060134=color/car_background 01060135=color/car_blue_100 01060136=color/car_blue_200 01060137=color/car_blue_300 01060138=color/car_blue_400 01060139=color/car_blue_50 0106013a=color/car_blue_500 0106013b=color/car_blue_600 0106013c=color/car_blue_700 0106013d=color/car_blue_800 0106013e=color/car_blue_900 0106013f=color/car_blue_grey_800 01060140=color/car_blue_grey_900 01060141=color/car_body1 01060142=color/car_body1_dark 01060143=color/car_body1_light 01060144=color/car_body2 01060145=color/car_body2_dark 01060146=color/car_body2_light 01060147=color/car_body3 01060148=color/car_body3_dark 01060149=color/car_body3_light 0106014a=color/car_body4 0106014b=color/car_body4_dark 0106014c=color/car_body4_light 0106014d=color/car_borderless_button_text_color 0106014e=color/car_button_text_color 0106014f=color/car_card 01060150=color/car_card_dark 01060151=color/car_card_light 01060152=color/car_card_ripple_background 01060153=color/car_card_ripple_background_dark 01060154=color/car_card_ripple_background_inverse 01060155=color/car_card_ripple_background_light 01060156=color/car_colorPrimary 01060157=color/car_colorPrimaryDark 01060158=color/car_colorSecondary 01060159=color/car_cyan_100 0106015a=color/car_cyan_200 0106015b=color/car_cyan_300 0106015c=color/car_cyan_400 0106015d=color/car_cyan_50 0106015e=color/car_cyan_500 0106015f=color/car_cyan_600 01060160=color/car_cyan_700 01060161=color/car_cyan_800 01060162=color/car_cyan_900 01060163=color/car_dark_blue_grey_1000 01060164=color/car_dark_blue_grey_600 01060165=color/car_dark_blue_grey_700 01060166=color/car_dark_blue_grey_800 01060167=color/car_dark_blue_grey_900 01060168=color/car_green_100 01060169=color/car_green_200 0106016a=color/car_green_300 0106016b=color/car_green_400 0106016c=color/car_green_50 0106016d=color/car_green_500 0106016e=color/car_green_600 0106016f=color/car_green_700 01060170=color/car_green_800 01060171=color/car_green_900 01060172=color/car_grey_100 01060173=color/car_grey_1000 01060174=color/car_grey_200 01060175=color/car_grey_300 01060176=color/car_grey_400 01060177=color/car_grey_50 01060178=color/car_grey_500 01060179=color/car_grey_600 0106017a=color/car_grey_700 0106017b=color/car_grey_746 0106017c=color/car_grey_772 0106017d=color/car_grey_800 0106017e=color/car_grey_846 0106017f=color/car_grey_868 01060180=color/car_grey_900 01060181=color/car_grey_928 01060182=color/car_grey_958 01060183=color/car_grey_972 01060184=color/car_headline1 01060185=color/car_headline1_dark 01060186=color/car_headline1_light 01060187=color/car_headline2 01060188=color/car_headline2_dark 01060189=color/car_headline2_light 0106018a=color/car_headline3 0106018b=color/car_headline3_dark 0106018c=color/car_headline3_light 0106018d=color/car_headline4 0106018e=color/car_headline4_dark 0106018f=color/car_headline4_light 01060190=color/car_highlight 01060191=color/car_highlight_dark 01060192=color/car_highlight_light 01060193=color/car_keyboard_divider_line 01060194=color/car_keyboard_text_primary_color 01060195=color/car_keyboard_text_secondary_color 01060196=color/car_light_blue_300 01060197=color/car_light_blue_500 01060198=color/car_light_blue_600 01060199=color/car_light_blue_700 0106019a=color/car_light_blue_800 0106019b=color/car_light_blue_900 0106019c=color/car_list_divider 0106019d=color/car_list_divider_dark 0106019e=color/car_list_divider_light 0106019f=color/car_orange_100 010601a0=color/car_orange_200 010601a1=color/car_orange_300 010601a2=color/car_orange_400 010601a3=color/car_orange_50 010601a4=color/car_orange_500 010601a5=color/car_orange_600 010601a6=color/car_orange_700 010601a7=color/car_orange_800 010601a8=color/car_orange_900 010601a9=color/car_pink_100 010601aa=color/car_pink_200 010601ab=color/car_pink_300 010601ac=color/car_pink_400 010601ad=color/car_pink_50 010601ae=color/car_pink_500 010601af=color/car_pink_600 010601b0=color/car_pink_700 010601b1=color/car_pink_800 010601b2=color/car_pink_900 010601b3=color/car_purple_100 010601b4=color/car_purple_200 010601b5=color/car_purple_300 010601b6=color/car_purple_400 010601b7=color/car_purple_50 010601b8=color/car_purple_500 010601b9=color/car_purple_600 010601ba=color/car_purple_700 010601bb=color/car_purple_800 010601bc=color/car_purple_900 010601bd=color/car_red_100 010601be=color/car_red_200 010601bf=color/car_red_300 010601c0=color/car_red_400 010601c1=color/car_red_50 010601c2=color/car_red_500 010601c3=color/car_red_500a 010601c4=color/car_red_600 010601c5=color/car_red_700 010601c6=color/car_red_800 010601c7=color/car_red_900 010601c8=color/car_red_a700 010601c9=color/car_scrollbar_thumb 010601ca=color/car_scrollbar_thumb_dark 010601cb=color/car_scrollbar_thumb_inverse 010601cc=color/car_scrollbar_thumb_light 010601cd=color/car_seekbar_track_background 010601ce=color/car_seekbar_track_background_dark 010601cf=color/car_seekbar_track_background_light 010601d0=color/car_seekbar_track_secondary_progress 010601d1=color/car_switch 010601d2=color/car_switch_track 010601d3=color/car_teal_100 010601d4=color/car_teal_200 010601d5=color/car_teal_300 010601d6=color/car_teal_400 010601d7=color/car_teal_50 010601d8=color/car_teal_500 010601d9=color/car_teal_600 010601da=color/car_teal_700 010601db=color/car_teal_800 010601dc=color/car_teal_900 010601dd=color/car_tint 010601de=color/car_tint_dark 010601df=color/car_tint_inverse 010601e0=color/car_tint_light 010601e1=color/car_title 010601e2=color/car_title2 010601e3=color/car_title2_dark 010601e4=color/car_title2_light 010601e5=color/car_title_dark 010601e6=color/car_title_light 010601e7=color/car_toast_background 010601e8=color/car_user_switcher_user_image_bgcolor 010601e9=color/car_user_switcher_user_image_fgcolor 010601ea=color/car_white_1000 010601eb=color/car_yellow_100 010601ec=color/car_yellow_200 010601ed=color/car_yellow_300 010601ee=color/car_yellow_400 010601ef=color/car_yellow_50 010601f0=color/car_yellow_500 010601f1=color/car_yellow_600 010601f2=color/car_yellow_700 010601f3=color/car_yellow_800 010601f4=color/car_yellow_900 010601f5=color/chooser_gradient_background 010601f6=color/chooser_gradient_highlight 010601f7=color/chooser_row_divider 010601f8=color/config_defaultNotificationColor 010601f9=color/config_letterboxBackgroundColor 010601fa=color/config_progress_background_tint 010601fb=color/control_checkable_material 010601fc=color/control_default_material 010601fd=color/control_highlight_material 010601fe=color/conversation_important_highlight 010601ff=color/customColorBrandA 01060200=color/customColorBrandB 01060201=color/customColorBrandC 01060202=color/customColorBrandD 01060203=color/customColorClockHour 01060204=color/customColorClockMinute 01060205=color/customColorClockSecond 01060206=color/customColorOnShadeActive 01060207=color/customColorOnShadeActiveVariant 01060208=color/customColorOnShadeInactive 01060209=color/customColorOnShadeInactiveVariant 0106020a=color/customColorOnThemeApp 0106020b=color/customColorOverviewBackground 0106020c=color/customColorShadeActive 0106020d=color/customColorShadeDisabled 0106020e=color/customColorShadeInactive 0106020f=color/customColorThemeApp 01060210=color/customColorThemeAppRing 01060211=color/customColorThemeNotif 01060212=color/customColorUnderSurface 01060213=color/customColorWeatherTemp 01060214=color/customColorWidgetBackground 01060215=color/datepicker_default_circle_background_color_material_dark 01060216=color/datepicker_default_circle_background_color_material_light 01060217=color/datepicker_default_disabled_text_color_material_dark 01060218=color/datepicker_default_disabled_text_color_material_light 01060219=color/datepicker_default_header_dayofweek_background_color_material_dark 0106021a=color/datepicker_default_header_dayofweek_background_color_material_light 0106021b=color/datepicker_default_header_selector_background_material_dark 0106021c=color/datepicker_default_header_selector_background_material_light 0106021d=color/datepicker_default_normal_text_color_material_dark 0106021e=color/datepicker_default_normal_text_color_material_light 0106021f=color/datepicker_default_pressed_text_color_material_dark 01060220=color/datepicker_default_pressed_text_color_material_light 01060221=color/datepicker_default_selected_text_color_material_dark 01060222=color/datepicker_default_selected_text_color_material_light 01060223=color/datepicker_default_view_animator_color_material_dark 01060224=color/datepicker_default_view_animator_color_material_light 01060225=color/decor_button_dark_color 01060226=color/decor_button_light_color 01060227=color/decor_view_status_guard 01060228=color/decor_view_status_guard_light 01060229=color/default_magnifier_color_overlay 0106022a=color/dim_foreground_dark 0106022b=color/dim_foreground_dark_disabled 0106022c=color/dim_foreground_dark_inverse 0106022d=color/dim_foreground_dark_inverse_disabled 0106022e=color/dim_foreground_disabled_holo_dark 0106022f=color/dim_foreground_disabled_holo_light 01060230=color/dim_foreground_holo_dark 01060231=color/dim_foreground_holo_light 01060232=color/dim_foreground_inverse_disabled_holo_dark 01060233=color/dim_foreground_inverse_disabled_holo_light 01060234=color/dim_foreground_inverse_holo_dark 01060235=color/dim_foreground_inverse_holo_light 01060236=color/dim_foreground_light 01060237=color/dim_foreground_light_disabled 01060238=color/dim_foreground_light_inverse 01060239=color/dim_foreground_light_inverse_disabled 0106023a=color/edge_effect_device_default_dark 0106023b=color/edge_effect_device_default_light 0106023c=color/error_color_device_default_dark 0106023d=color/error_color_device_default_light 0106023e=color/error_color_material_dark 0106023f=color/error_color_material_light 01060240=color/facelock_spotlight_mask 01060241=color/floating_popup_divider_dark 01060242=color/floating_popup_divider_light 01060243=color/foreground_device_default_dark 01060244=color/foreground_device_default_light 01060245=color/foreground_material_dark 01060246=color/foreground_material_light 01060247=color/global_actions_container_background 01060248=color/group_button_dialog_focused_holo_dark 01060249=color/group_button_dialog_focused_holo_light 0106024a=color/group_button_dialog_pressed_holo_dark 0106024b=color/group_button_dialog_pressed_holo_light 0106024c=color/highlighted_text_dark 0106024d=color/highlighted_text_holo_dark 0106024e=color/highlighted_text_holo_light 0106024f=color/highlighted_text_light 01060250=color/highlighted_text_material 01060251=color/hint_foreground_dark 01060252=color/hint_foreground_holo_dark 01060253=color/hint_foreground_holo_light 01060254=color/hint_foreground_light 01060255=color/hint_foreground_material_dark 01060256=color/hint_foreground_material_light 01060257=color/holo_button_normal 01060258=color/holo_button_pressed 01060259=color/holo_control_activated 0106025a=color/holo_control_normal 0106025b=color/holo_gray_bright 0106025c=color/holo_gray_light 0106025d=color/holo_light_button_normal 0106025e=color/holo_light_button_pressed 0106025f=color/holo_light_control_activated 01060260=color/holo_light_control_normal 01060261=color/holo_light_primary 01060262=color/holo_light_primary_dark 01060263=color/holo_primary 01060264=color/holo_primary_dark 01060265=color/input_method_switch_on_item 01060266=color/instant_app_badge 01060267=color/keyguard_avatar_frame_color 01060268=color/keyguard_avatar_frame_pressed_color 01060269=color/keyguard_avatar_frame_shadow_color 0106026a=color/keyguard_avatar_nick_color 0106026b=color/keyguard_text_color_decline 0106026c=color/keyguard_text_color_normal 0106026d=color/keyguard_text_color_soundoff 0106026e=color/keyguard_text_color_soundon 0106026f=color/keyguard_text_color_unlock 01060270=color/kg_multi_user_text_active 01060271=color/kg_multi_user_text_inactive 01060272=color/kg_widget_pager_gradient 01060273=color/language_picker_item_selected_bg 01060274=color/language_picker_item_selected_indicator 01060275=color/language_picker_item_selected_stroke 01060276=color/language_picker_item_text_color 01060277=color/language_picker_item_text_color_secondary 01060278=color/language_picker_item_text_color_secondary_selected 01060279=color/language_picker_item_text_color_selected 0106027a=color/legacy_button_normal 0106027b=color/legacy_button_pressed 0106027c=color/legacy_control_activated 0106027d=color/legacy_control_normal 0106027e=color/legacy_green 0106027f=color/legacy_light_button_normal 01060280=color/legacy_light_button_pressed 01060281=color/legacy_light_control_activated 01060282=color/legacy_light_control_normal 01060283=color/legacy_light_primary 01060284=color/legacy_light_primary_dark 01060285=color/legacy_long_pressed_highlight 01060286=color/legacy_orange 01060287=color/legacy_pressed_highlight 01060288=color/legacy_primary 01060289=color/legacy_primary_dark 0106028a=color/legacy_selected_highlight 0106028b=color/lighter_gray 0106028c=color/link_text_dark 0106028d=color/link_text_holo_dark 0106028e=color/link_text_holo_light 0106028f=color/link_text_light 01060290=color/list_divider_color_dark 01060291=color/list_divider_color_light 01060292=color/list_divider_opacity_device_default_dark 01060293=color/list_divider_opacity_device_default_light 01060294=color/list_divider_opacity_material 01060295=color/list_highlight_material 01060296=color/loading_gradient_background_color_dark 01060297=color/loading_gradient_background_color_light 01060298=color/loading_gradient_highlight_color_dark 01060299=color/loading_gradient_highlight_color_light 0106029a=color/lock_pattern_view_regular_color 0106029b=color/lock_pattern_view_success_color 0106029c=color/lockscreen_clock_am_pm 0106029d=color/lockscreen_clock_background 0106029e=color/lockscreen_clock_foreground 0106029f=color/lockscreen_owner_info 010602a0=color/materialColorBackground 010602a1=color/materialColorControlActivated 010602a2=color/materialColorControlHighlight 010602a3=color/materialColorControlNormal 010602a4=color/materialColorError 010602a5=color/materialColorErrorContainer 010602a6=color/materialColorInverseOnSurface 010602a7=color/materialColorInversePrimary 010602a8=color/materialColorInverseSurface 010602a9=color/materialColorOnBackground 010602aa=color/materialColorOnError 010602ab=color/materialColorOnErrorContainer 010602ac=color/materialColorOnPrimary 010602ad=color/materialColorOnPrimaryContainer 010602ae=color/materialColorOnPrimaryFixed 010602af=color/materialColorOnPrimaryFixedVariant 010602b0=color/materialColorOnSecondary 010602b1=color/materialColorOnSecondaryContainer 010602b2=color/materialColorOnSecondaryFixed 010602b3=color/materialColorOnSecondaryFixedVariant 010602b4=color/materialColorOnSurface 010602b5=color/materialColorOnSurfaceVariant 010602b6=color/materialColorOnTertiary 010602b7=color/materialColorOnTertiaryContainer 010602b8=color/materialColorOnTertiaryFixed 010602b9=color/materialColorOnTertiaryFixedVariant 010602ba=color/materialColorOutline 010602bb=color/materialColorOutlineVariant 010602bc=color/materialColorPaletteKeyColorNeutral 010602bd=color/materialColorPaletteKeyColorNeutralVariant 010602be=color/materialColorPaletteKeyColorPrimary 010602bf=color/materialColorPaletteKeyColorSecondary 010602c0=color/materialColorPaletteKeyColorTertiary 010602c1=color/materialColorPrimary 010602c2=color/materialColorPrimaryContainer 010602c3=color/materialColorPrimaryFixed 010602c4=color/materialColorPrimaryFixedDim 010602c5=color/materialColorScrim 010602c6=color/materialColorSecondary 010602c7=color/materialColorSecondaryContainer 010602c8=color/materialColorSecondaryFixed 010602c9=color/materialColorSecondaryFixedDim 010602ca=color/materialColorShadow 010602cb=color/materialColorSurface 010602cc=color/materialColorSurfaceBright 010602cd=color/materialColorSurfaceContainer 010602ce=color/materialColorSurfaceContainerHigh 010602cf=color/materialColorSurfaceContainerHighest 010602d0=color/materialColorSurfaceContainerLow 010602d1=color/materialColorSurfaceContainerLowest 010602d2=color/materialColorSurfaceDim 010602d3=color/materialColorSurfaceTint 010602d4=color/materialColorSurfaceVariant 010602d5=color/materialColorTertiary 010602d6=color/materialColorTertiaryContainer 010602d7=color/materialColorTertiaryFixed 010602d8=color/materialColorTertiaryFixedDim 010602d9=color/materialColorTextHintInverse 010602da=color/materialColorTextPrimaryInverse 010602db=color/materialColorTextPrimaryInverseDisableOnly 010602dc=color/materialColorTextSecondaryAndTertiaryInverse 010602dd=color/materialColorTextSecondaryAndTertiaryInverseDisabled 010602de=color/material_blue_grey_200 010602df=color/material_blue_grey_700 010602e0=color/material_blue_grey_800 010602e1=color/material_blue_grey_900 010602e2=color/material_blue_grey_950 010602e3=color/material_deep_teal_100 010602e4=color/material_deep_teal_200 010602e5=color/material_deep_teal_300 010602e6=color/material_deep_teal_500 010602e7=color/material_grey_100 010602e8=color/material_grey_200 010602e9=color/material_grey_300 010602ea=color/material_grey_50 010602eb=color/material_grey_600 010602ec=color/material_grey_800 010602ed=color/material_grey_850 010602ee=color/material_grey_900 010602ef=color/material_red_A100 010602f0=color/material_red_A700 010602f1=color/micro_text_light 010602f2=color/navigation_bar_compatible 010602f3=color/navigation_bar_default 010602f4=color/navigation_bar_divider_device_default_settings 010602f5=color/notification_action_button_text_color 010602f6=color/notification_action_list 010602f7=color/notification_action_list_background_color 010602f8=color/notification_close_button_state_tint 010602f9=color/notification_default_color 010602fa=color/notification_default_color_current 010602fb=color/notification_default_color_dark 010602fc=color/notification_default_color_light 010602fd=color/notification_expand_button_state_tint 010602fe=color/notification_primary_text_color_current 010602ff=color/notification_primary_text_color_dark 01060300=color/notification_primary_text_color_light 01060301=color/notification_progress_background_color 01060302=color/notification_secondary_text_color_current 01060303=color/notification_secondary_text_color_dark 01060304=color/notification_secondary_text_color_light 01060305=color/overview_background 01060306=color/overview_background_dark 01060307=color/perms_dangerous_grp_color 01060308=color/perms_dangerous_perm_color 01060309=color/personal_apps_suspension_notification_color 0106030a=color/primary_dark_device_default_dark 0106030b=color/primary_dark_device_default_light 0106030c=color/primary_dark_device_default_settings 0106030d=color/primary_dark_device_default_settings_light 0106030e=color/primary_dark_material_dark 0106030f=color/primary_dark_material_light 01060310=color/primary_dark_material_light_light_status_bar 01060311=color/primary_dark_material_settings 01060312=color/primary_dark_material_settings_light 01060313=color/primary_device_default_dark 01060314=color/primary_device_default_light 01060315=color/primary_device_default_settings 01060316=color/primary_device_default_settings_light 01060317=color/primary_material_dark 01060318=color/primary_material_light 01060319=color/primary_material_settings 0106031a=color/primary_material_settings_light 0106031b=color/primary_text_dark_disable_only 0106031c=color/primary_text_dark_focused 0106031d=color/primary_text_default_material_dark 0106031e=color/primary_text_default_material_light 0106031f=color/primary_text_disable_only_holo_dark 01060320=color/primary_text_disable_only_holo_light 01060321=color/primary_text_disable_only_material_dark 01060322=color/primary_text_disable_only_material_light 01060323=color/primary_text_focused_holo_dark 01060324=color/primary_text_holo_dark 01060325=color/primary_text_holo_light 01060326=color/primary_text_inverse_when_activated_material 01060327=color/primary_text_leanback_dark 01060328=color/primary_text_leanback_formwizard_dark 01060329=color/primary_text_leanback_formwizard_default_dark 0106032a=color/primary_text_light_disable_only 0106032b=color/primary_text_material_dark 0106032c=color/primary_text_material_light 0106032d=color/primary_text_nodisable_holo_dark 0106032e=color/primary_text_nodisable_holo_light 0106032f=color/primary_text_secondary_when_activated_material 01060330=color/primary_text_secondary_when_activated_material_inverse 01060331=color/profile_badge_1 01060332=color/profile_badge_1_dark 01060333=color/profile_badge_2 01060334=color/profile_badge_2_dark 01060335=color/profile_badge_3 01060336=color/profile_badge_3_dark 01060337=color/quaternary_device_default_settings 01060338=color/quaternary_material_settings 01060339=color/ratingbar_background_material 0106033a=color/red 0106033b=color/resolver_accent_ripple 0106033c=color/resolver_button_text 0106033d=color/resolver_profile_tab_selected_bg 0106033e=color/resolver_profile_tab_text 0106033f=color/resolver_text_color_secondary_dark 01060340=color/ripple_material_dark 01060341=color/ripple_material_light 01060342=color/safe_mode_text 01060343=color/search_url_text 01060344=color/search_url_text_holo 01060345=color/search_url_text_material_dark 01060346=color/search_url_text_material_light 01060347=color/search_url_text_normal 01060348=color/search_url_text_pressed 01060349=color/search_url_text_selected 0106034a=color/search_widget_corpus_item_background 0106034b=color/secondary_device_default_settings 0106034c=color/secondary_device_default_settings_light 0106034d=color/secondary_material_settings 0106034e=color/secondary_material_settings_light 0106034f=color/secondary_text_default_material_dark 01060350=color/secondary_text_default_material_light 01060351=color/secondary_text_holo_dark 01060352=color/secondary_text_holo_light 01060353=color/secondary_text_inverse_when_activated_material 01060354=color/secondary_text_leanback_dark 01060355=color/secondary_text_material_dark 01060356=color/secondary_text_material_light 01060357=color/secondary_text_nodisable_holo_dark 01060358=color/secondary_text_nodisable_holo_light 01060359=color/secondary_text_nofocus 0106035a=color/seekbar_track_progress_material 0106035b=color/shadow 0106035c=color/side_fps_button_color 0106035d=color/side_fps_text_color 0106035e=color/side_fps_toast_background 0106035f=color/sliding_tab_text_color_active 01060360=color/sliding_tab_text_color_shadow 01060361=color/suggestion_highlight_text 01060362=color/surface_dark 01060363=color/surface_effect_0 01060364=color/surface_effect_0_color 01060365=color/surface_effect_1 01060366=color/surface_effect_1_color 01060367=color/surface_effect_2 01060368=color/surface_effect_2_color 01060369=color/surface_effect_3 0106036a=color/surface_effect_3_color 0106036b=color/surface_header_dark 0106036c=color/surface_header_light 0106036d=color/surface_highlight_dark 0106036e=color/surface_highlight_light 0106036f=color/surface_light 01060370=color/surface_variant_dark 01060371=color/surface_variant_light 01060372=color/switch_thumb_disabled_material_dark 01060373=color/switch_thumb_disabled_material_light 01060374=color/switch_thumb_material_dark 01060375=color/switch_thumb_material_light 01060376=color/switch_thumb_normal_material_dark 01060377=color/switch_thumb_normal_material_light 01060378=color/switch_thumb_watch_default_dark 01060379=color/switch_track_material 0106037a=color/switch_track_watch_default_dark 0106037b=color/system_bar_background_semi_transparent 0106037c=color/system_brand_a_dark 0106037d=color/system_brand_a_light 0106037e=color/system_brand_b_dark 0106037f=color/system_brand_b_light 01060380=color/system_brand_c_dark 01060381=color/system_brand_c_light 01060382=color/system_brand_d_dark 01060383=color/system_brand_d_light 01060384=color/system_clock_hour_dark 01060385=color/system_clock_hour_light 01060386=color/system_clock_minute_dark 01060387=color/system_clock_minute_light 01060388=color/system_clock_second_dark 01060389=color/system_clock_second_light 0106038a=color/system_on_shade_active_dark 0106038b=color/system_on_shade_active_light 0106038c=color/system_on_shade_active_variant_dark 0106038d=color/system_on_shade_active_variant_light 0106038e=color/system_on_shade_inactive_dark 0106038f=color/system_on_shade_inactive_light 01060390=color/system_on_shade_inactive_variant_dark 01060391=color/system_on_shade_inactive_variant_light 01060392=color/system_on_theme_app_dark 01060393=color/system_on_theme_app_light 01060394=color/system_overview_background_dark 01060395=color/system_overview_background_light 01060396=color/system_shade_active_dark 01060397=color/system_shade_active_light 01060398=color/system_shade_disabled_dark 01060399=color/system_shade_disabled_light 0106039a=color/system_shade_inactive_dark 0106039b=color/system_shade_inactive_light 0106039c=color/system_theme_app_dark 0106039d=color/system_theme_app_light 0106039e=color/system_theme_app_ring_dark 0106039f=color/system_theme_app_ring_light 010603a0=color/system_theme_notif_dark 010603a1=color/system_theme_notif_light 010603a2=color/system_under_surface_dark 010603a3=color/system_under_surface_light 010603a4=color/system_weather_temp_dark 010603a5=color/system_weather_temp_light 010603a6=color/system_widget_background_dark 010603a7=color/system_widget_background_light 010603a8=color/tab_highlight_material 010603a9=color/tab_indicator_material 010603aa=color/tab_indicator_text_material 010603ab=color/tab_indicator_text_v4 010603ac=color/tertiary_device_default_settings 010603ad=color/tertiary_material_settings 010603ae=color/tertiary_text_holo_dark 010603af=color/tertiary_text_holo_light 010603b0=color/text_color_on_accent_device_default 010603b1=color/text_color_primary 010603b2=color/text_color_primary_device_default_dark 010603b3=color/text_color_primary_device_default_light 010603b4=color/text_color_secondary 010603b5=color/text_color_secondary_device_default_dark 010603b6=color/text_color_secondary_device_default_light 010603b7=color/text_color_tertiary_device_default_dark 010603b8=color/text_color_tertiary_device_default_light 010603b9=color/timepicker_default_ampm_selected_background_color_material 010603ba=color/timepicker_default_ampm_unselected_background_color_material 010603bb=color/timepicker_default_background_material 010603bc=color/timepicker_default_numbers_background_color_material 010603bd=color/timepicker_default_selector_color_material 010603be=color/timepicker_default_text_color_material 010603bf=color/tooltip_background_dark 010603c0=color/tooltip_background_light 010603c1=color/user_icon_1 010603c2=color/user_icon_2 010603c3=color/user_icon_3 010603c4=color/user_icon_4 010603c5=color/user_icon_5 010603c6=color/user_icon_6 010603c7=color/user_icon_7 010603c8=color/user_icon_8 010603c9=color/user_icon_default_gray 010603ca=color/user_icon_default_white 010603cb=color/white_disabled_material 01070000=array/emailAddressTypes 01070001=array/imProtocols 01070002=array/organizationTypes 01070003=array/phoneTypes 01070004=array/postalAddressTypes 01070005=array/config_keySystemUuidMapping 01070006=array/config_optionalIpSecAlgorithms 01070007=array/carrier_properties 01070008=array/cloneable_apps 01070009=array/common_nicknames 0107000a=array/config_allowedGlobalInstantAppSettings 0107000b=array/config_allowedSecureInstantAppSettings 0107000c=array/config_allowedSystemInstantAppSettings 0107000d=array/config_ambientBrighteningThresholds 0107000e=array/config_ambientDarkeningThresholds 0107000f=array/config_ambientThresholdLevels 01070010=array/config_ambientThresholdsOfPeakRefreshRate 01070011=array/config_angleAllowList 01070012=array/config_autoBrightnessButtonBacklightValues 01070013=array/config_autoBrightnessDisplayValuesNits 01070014=array/config_autoBrightnessDisplayValuesNitsIdle 01070015=array/config_autoBrightnessLcdBacklightValues 01070016=array/config_autoBrightnessLcdBacklightValues_doze 01070017=array/config_autoBrightnessLevels 01070018=array/config_autoBrightnessLevelsIdle 01070019=array/config_autoKeyboardBacklightBrightnessValues 0107001a=array/config_autoKeyboardBacklightDecreaseLuxThreshold 0107001b=array/config_autoKeyboardBacklightIncreaseLuxThreshold 0107001c=array/config_autoRotationTiltTolerance 0107001d=array/config_autoTimeSourcesPriority 0107001e=array/config_availableColorModes 0107001f=array/config_availableEMValueOptions 01070020=array/config_backGestureInsetScales 01070021=array/config_backupHealthConnectDataAndSettingsKnownSigners 01070022=array/config_batteryPackageTypeService 01070023=array/config_batteryPackageTypeSystem 01070024=array/config_bg_current_drain_high_threshold_to_bg_restricted 01070025=array/config_bg_current_drain_high_threshold_to_restricted_bucket 01070026=array/config_bg_current_drain_threshold_to_bg_restricted 01070027=array/config_bg_current_drain_threshold_to_restricted_bucket 01070028=array/config_biometric_protected_package_names 01070029=array/config_biometric_sensors 0107002a=array/config_brightnessThresholdsOfPeakRefreshRate 0107002b=array/config_builtInDisplayIsRoundArray 0107002c=array/config_callBarringMMI 0107002d=array/config_callBarringMMI_for_ims 0107002e=array/config_cameraPrivacyLightAlsLuxThresholds 0107002f=array/config_cameraPrivacyLightColors 01070030=array/config_cdma_dun_supported_types 01070031=array/config_cdma_home_system 01070032=array/config_cdma_international_roaming_indicators 01070033=array/config_cell_retries_per_error_code 01070034=array/config_clockTickVibePattern 01070035=array/config_companionDeviceCerts 01070036=array/config_companionDevicePackages 01070037=array/config_companionPermSyncEnabledCerts 01070038=array/config_companionPermSyncEnabledPackages 01070039=array/config_concurrentDisplayDeviceStates 0107003a=array/config_convert_to_emergency_number_map 0107003b=array/config_defaultAllowlistLaunchOnPrivateDisplayPackages 0107003c=array/config_defaultAmbientContextServices 0107003d=array/config_defaultCloudSearchServices 0107003e=array/config_defaultFirstUserRestrictions 0107003f=array/config_defaultImperceptibleKillingExemptionPkgs 01070040=array/config_defaultImperceptibleKillingExemptionProcStates 01070041=array/config_defaultNotificationVibePattern 01070042=array/config_defaultNotificationVibeWaveform 01070043=array/config_defaultPinnerServiceFiles 01070044=array/config_default_vm_number 01070045=array/config_deviceSpecificSystemServices 01070046=array/config_deviceStatesAvailableForAppRequests 01070047=array/config_deviceStatesOnWhichToSleep 01070048=array/config_deviceStatesOnWhichToWakeUp 01070049=array/config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis 0107004a=array/config_deviceTabletopRotations 0107004b=array/config_device_state_postures 0107004c=array/config_disabledDreamComponents 0107004d=array/config_disabledUntilUsedPreinstalledImes 0107004e=array/config_displayCompositionColorModes 0107004f=array/config_displayCompositionColorSpaces 01070050=array/config_displayCutoutApproximationRectArray 01070051=array/config_displayCutoutPathArray 01070052=array/config_displayCutoutSideOverrideArray 01070053=array/config_displayShapeArray 01070054=array/config_displayUniqueIdArray 01070055=array/config_displayWhiteBalanceAmbientColorTemperatures 01070056=array/config_displayWhiteBalanceBaseThresholds 01070057=array/config_displayWhiteBalanceDecreaseThresholds 01070058=array/config_displayWhiteBalanceDisplayColorTemperatures 01070059=array/config_displayWhiteBalanceDisplayNominalWhite 0107005a=array/config_displayWhiteBalanceDisplayPrimaries 0107005b=array/config_displayWhiteBalanceDisplayRangeMinimums 0107005c=array/config_displayWhiteBalanceDisplaySteps 0107005d=array/config_displayWhiteBalanceHighLightAmbientBiases 0107005e=array/config_displayWhiteBalanceHighLightAmbientBiasesStrong 0107005f=array/config_displayWhiteBalanceHighLightAmbientBrightnesses 01070060=array/config_displayWhiteBalanceHighLightAmbientBrightnessesStrong 01070061=array/config_displayWhiteBalanceIncreaseThresholds 01070062=array/config_displayWhiteBalanceLowLightAmbientBiases 01070063=array/config_displayWhiteBalanceLowLightAmbientBiasesStrong 01070064=array/config_displayWhiteBalanceLowLightAmbientBrightnesses 01070065=array/config_displayWhiteBalanceLowLightAmbientBrightnessesStrong 01070066=array/config_displayWhiteBalanceStrongAmbientColorTemperatures 01070067=array/config_displayWhiteBalanceStrongDisplayColorTemperatures 01070068=array/config_display_no_service_when_sim_unready 01070069=array/config_dockExtconStateMapping 0107006a=array/config_doubleClickVibePattern 0107006b=array/config_dozeTapSensorPostureMapping 0107006c=array/config_dropboxLowPriorityTags 0107006d=array/config_emergency_iso_country_codes 0107006e=array/config_enabledCredentialProviderService 0107006f=array/config_ephemeralResolverPackage 01070070=array/config_ethernet_interfaces 01070071=array/config_face_acquire_biometricprompt_ignorelist 01070072=array/config_face_acquire_enroll_ignorelist 01070073=array/config_face_acquire_keyguard_ignorelist 01070074=array/config_face_acquire_vendor_biometricprompt_ignorelist 01070075=array/config_face_acquire_vendor_enroll_ignorelist 01070076=array/config_face_acquire_vendor_keyguard_ignorelist 01070077=array/config_fillBuiltInDisplayCutoutArray 01070078=array/config_foldedDeviceStates 01070079=array/config_fontManagerServiceCerts 0107007a=array/config_forceQueryablePackages 0107007b=array/config_forceSlowJpegModeList 0107007c=array/config_force_cellular_transport_capabilities 0107007d=array/config_globalActionsList 0107007e=array/config_gnssParameters 0107007f=array/config_halfFoldedDeviceStates 01070080=array/config_healthConnectMigrationKnownSigners 01070081=array/config_healthConnectRestoreKnownSigners 01070082=array/config_hideWhenDisabled_packageNames 01070083=array/config_highAmbientBrightnessThresholdsOfFixedRefreshRate 01070084=array/config_highDisplayBrightnessThresholdsOfFixedRefreshRate 01070085=array/config_highRefreshRateBlacklist 01070086=array/config_integrityRuleProviderPackages 01070087=array/config_keep_warming_services 01070088=array/config_localNotStealTopFocusDisplayPorts 01070089=array/config_localPrivateDisplayPorts 0107008a=array/config_locationDriverAssistancePackageNames 0107008b=array/config_locationExtraPackageNames 0107008c=array/config_locationProviderPackageNames 0107008d=array/config_loggable_dream_prefixes 0107008e=array/config_longPressOnPowerDurationSettings 0107008f=array/config_longPressVibePattern 01070090=array/config_lteDbmThresholds 01070091=array/config_mainBuiltInDisplayCutoutSideOverride 01070092=array/config_mainBuiltInDisplayWaterfallCutout 01070093=array/config_mappedColorModes 01070094=array/config_maskBuiltInDisplayCutoutArray 01070095=array/config_minimumBrightnessCurveLux 01070096=array/config_minimumBrightnessCurveNits 01070097=array/config_mobile_hotspot_provision_app 01070098=array/config_mobile_tcp_buffers 01070099=array/config_networkNotifySwitches 0107009a=array/config_networkSupportedKeepaliveCount 0107009b=array/config_network_type_tcp_buffers 0107009c=array/config_nightDisplayColorTemperatureCoefficients 0107009d=array/config_nightDisplayColorTemperatureCoefficientsNative 0107009e=array/config_nonPreemptibleInputMethods 0107009f=array/config_notificationDefaultUnsupportedAdjustments 010700a0=array/config_notificationFallbackVibePattern 010700a1=array/config_notificationFallbackVibeWaveform 010700a2=array/config_notificationMsgPkgsAllowedAsConvos 010700a3=array/config_notificationSignalExtractors 010700a4=array/config_ntpServers 010700a5=array/config_oemUsbModeOverride 010700a6=array/config_oem_enabled_satellite_country_codes 010700a7=array/config_openDeviceStates 010700a8=array/config_packagesExemptFromSuspension 010700a9=array/config_perDeviceStateRotationLockDefaults 010700aa=array/config_primaryCredentialProviderService 010700ab=array/config_priorityOnlyDndExemptPackages 010700ac=array/config_profcollectOnCameraOpenedSkipPackages 010700ad=array/config_protectedNetworks 010700ae=array/config_rawContactsEligibleDefaultAccountTypes 010700af=array/config_rearDisplayDeviceStates 010700b0=array/config_reduceBrightColorsCoefficients 010700b1=array/config_reduceBrightColorsCoefficientsNonlinear 010700b2=array/config_requestVibrationParamsForUsages 010700b3=array/config_restoreHealthConnectDataAndSettingsKnownSigners 010700b4=array/config_restrictedImagesServices 010700b5=array/config_ringtoneEffectUris 010700b6=array/config_roundedCornerBottomRadiusAdjustmentArray 010700b7=array/config_roundedCornerBottomRadiusArray 010700b8=array/config_roundedCornerRadiusAdjustmentArray 010700b9=array/config_roundedCornerRadiusArray 010700ba=array/config_roundedCornerTopRadiusAdjustmentArray 010700bb=array/config_roundedCornerTopRadiusArray 010700bc=array/config_safeModeEnabledVibePattern 010700bd=array/config_satellite_providers 010700be=array/config_screenBrighteningThresholds 010700bf=array/config_screenBrightnessBacklight 010700c0=array/config_screenBrightnessNits 010700c1=array/config_screenDarkeningThresholds 010700c2=array/config_screenThresholdLevels 010700c3=array/config_secondaryBuiltInDisplayCutoutSideOverride 010700c4=array/config_secondaryBuiltInDisplayWaterfallCutout 010700c5=array/config_securityStatePackages 010700c6=array/config_serialPorts 010700c7=array/config_serviceStateLocationAllowedPackages 010700c8=array/config_setContactsDefaultAccountKnownSigners 010700c9=array/config_sfps_enroll_stage_thresholds 010700ca=array/config_sfps_sensor_props 010700cb=array/config_sharedLibrariesLoadedAfterApp 010700cc=array/config_smallAreaDetectionAllowlist 010700cd=array/config_sms_enabled_locking_shift_tables 010700ce=array/config_sms_enabled_single_shift_tables 010700cf=array/config_statusBarIcons 010700d0=array/config_supportedDreamComplications 010700d1=array/config_supported_cellular_usage_settings 010700d2=array/config_system_condition_providers 010700d3=array/config_telephonyEuiccDeviceCapabilities 010700d4=array/config_telephonyHardware 010700d5=array/config_testLocationProviders 010700d6=array/config_tether_bluetooth_regexs 010700d7=array/config_tether_dhcp_range 010700d8=array/config_tether_upstream_types 010700d9=array/config_tether_usb_regexs 010700da=array/config_tether_wifi_regexs 010700db=array/config_toastCrossUserPackages 010700dc=array/config_trustedAccessibilityServices 010700dd=array/config_tvExternalInputLoggingDeviceBrandNames 010700de=array/config_tvExternalInputLoggingDeviceOnScreenDisplayNames 010700df=array/config_twoDigitNumberPattern 010700e0=array/config_udfps_enroll_stage_thresholds 010700e1=array/config_udfps_sensor_props 010700e2=array/config_udfps_touch_detection_options 010700e3=array/config_unsupported_network_capabilities 010700e4=array/config_usbHostDenylist 010700e5=array/config_verizon_satellite_enabled_tagids 010700e6=array/config_virtualKeyVibePattern 010700e7=array/config_vvmSmsFilterRegexes 010700e8=array/config_waterfallCutoutArray 010700e9=array/config_wearActivityModeRadios 010700ea=array/crossSimSpnFormats 010700eb=array/cross_profile_apps 010700ec=array/default_wallpaper_component_per_device_color 010700ed=array/demo_device_provisioning_known_signers 010700ee=array/device_state_notification_active_contents 010700ef=array/device_state_notification_active_titles 010700f0=array/device_state_notification_names 010700f1=array/device_state_notification_power_save_contents 010700f2=array/device_state_notification_power_save_titles 010700f3=array/device_state_notification_state_identifiers 010700f4=array/device_state_notification_thermal_contents 010700f5=array/device_state_notification_thermal_titles 010700f6=array/dial_string_replace 010700f7=array/disallowed_apps_managed_device 010700f8=array/disallowed_apps_managed_profile 010700f9=array/disallowed_apps_managed_user 010700fa=array/face_acquired_vendor 010700fb=array/face_error_vendor 010700fc=array/fingerprint_acquired_vendor 010700fd=array/fingerprint_error_vendor 010700fe=array/imAddressTypes 010700ff=array/maps_starting_lat_lng 01070100=array/maps_starting_zoom 01070101=array/networkAttributes 01070102=array/network_switch_type_name 01070103=array/networks_not_clear_data 01070104=array/no_ems_support_sim_operators 01070105=array/non_removable_euicc_slots 01070106=array/pause_wallpaper_render_when_state_change 01070107=array/policy_exempt_apps 01070108=array/preloaded_color_state_lists 01070109=array/preloaded_drawables 0107010a=array/radioAttributes 0107010b=array/required_apps_managed_device 0107010c=array/required_apps_managed_profile 0107010d=array/required_apps_managed_user 0107010e=array/resolver_target_actions_pin 0107010f=array/resolver_target_actions_unpin 01070110=array/sim_colors 01070111=array/special_locale_codes 01070112=array/special_locale_names 01070113=array/stoppable_fgs_system_apps 01070114=array/supported_locales 01070115=array/theming_defaults 01070116=array/unloggable_phone_numbers 01070117=array/vendor_cross_profile_apps 01070118=array/vendor_disallowed_apps_managed_device 01070119=array/vendor_disallowed_apps_managed_profile 0107011a=array/vendor_disallowed_apps_managed_user 0107011b=array/vendor_policy_exempt_apps 0107011c=array/vendor_required_apps_managed_device 0107011d=array/vendor_required_apps_managed_profile 0107011e=array/vendor_required_apps_managed_user 0107011f=array/vendor_required_attestation_certificates 01070120=array/vendor_stoppable_fgs_system_apps 01070121=array/wfcOperatorErrorAlertMessages 01070122=array/wfcOperatorErrorNotificationMessages 01070123=array/wfcSpnFormats 01070124=array/wifi_known_signers 01080000=drawable/alert_dark_frame 01080001=drawable/alert_light_frame 01080002=drawable/arrow_down_float 01080003=drawable/arrow_up_float 01080004=drawable/btn_default 01080005=drawable/btn_default_small 01080006=drawable/btn_dropdown 01080007=drawable/btn_minus 01080008=drawable/btn_plus 01080009=drawable/btn_radio 0108000a=drawable/btn_star 0108000b=drawable/btn_star_big_off 0108000c=drawable/btn_star_big_on 0108000d=drawable/button_onoff_indicator_on 0108000e=drawable/button_onoff_indicator_off 0108000f=drawable/checkbox_off_background 01080010=drawable/checkbox_on_background 01080011=drawable/dialog_frame 01080012=drawable/divider_horizontal_bright 01080013=drawable/divider_horizontal_textfield 01080014=drawable/divider_horizontal_dark 01080015=drawable/divider_horizontal_dim_dark 01080016=drawable/edit_text 01080017=drawable/btn_dialog 01080018=drawable/editbox_background 01080019=drawable/editbox_background_normal 0108001a=drawable/editbox_dropdown_dark_frame 0108001b=drawable/editbox_dropdown_light_frame 0108001c=drawable/gallery_thumb 0108001d=drawable/ic_delete 0108001e=drawable/ic_lock_idle_charging 0108001f=drawable/ic_lock_idle_lock 01080020=drawable/ic_lock_idle_low_battery 01080021=drawable/ic_media_ff 01080022=drawable/ic_media_next 01080023=drawable/ic_media_pause 01080024=drawable/ic_media_play 01080025=drawable/ic_media_previous 01080026=drawable/ic_media_rew 01080027=drawable/ic_dialog_alert 01080028=drawable/ic_dialog_dialer 01080029=drawable/ic_dialog_email 0108002a=drawable/ic_dialog_map 0108002b=drawable/ic_input_add 0108002c=drawable/ic_input_delete 0108002d=drawable/ic_input_get 0108002e=drawable/ic_lock_idle_alarm 0108002f=drawable/ic_lock_lock 01080030=drawable/ic_lock_power_off 01080031=drawable/ic_lock_silent_mode 01080032=drawable/ic_lock_silent_mode_off 01080033=drawable/ic_menu_add 01080034=drawable/ic_menu_agenda 01080035=drawable/ic_menu_always_landscape_portrait 01080036=drawable/ic_menu_call 01080037=drawable/ic_menu_camera 01080038=drawable/ic_menu_close_clear_cancel 01080039=drawable/ic_menu_compass 0108003a=drawable/ic_menu_crop 0108003b=drawable/ic_menu_day 0108003c=drawable/ic_menu_delete 0108003d=drawable/ic_menu_directions 0108003e=drawable/ic_menu_edit 0108003f=drawable/ic_menu_gallery 01080040=drawable/ic_menu_help 01080041=drawable/ic_menu_info_details 01080042=drawable/ic_menu_manage 01080043=drawable/ic_menu_mapmode 01080044=drawable/ic_menu_month 01080045=drawable/ic_menu_more 01080046=drawable/ic_menu_my_calendar 01080047=drawable/ic_menu_mylocation 01080048=drawable/ic_menu_myplaces 01080049=drawable/ic_menu_preferences 0108004a=drawable/ic_menu_recent_history 0108004b=drawable/ic_menu_report_image 0108004c=drawable/ic_menu_revert 0108004d=drawable/ic_menu_rotate 0108004e=drawable/ic_menu_save 0108004f=drawable/ic_menu_search 01080050=drawable/ic_menu_send 01080051=drawable/ic_menu_set_as 01080052=drawable/ic_menu_share 01080053=drawable/ic_menu_slideshow 01080054=drawable/ic_menu_today 01080055=drawable/ic_menu_upload 01080056=drawable/ic_menu_upload_you_tube 01080057=drawable/ic_menu_view 01080058=drawable/ic_menu_week 01080059=drawable/ic_menu_zoom 0108005a=drawable/ic_notification_clear_all 0108005b=drawable/ic_notification_overlay 0108005c=drawable/ic_partial_secure 0108005d=drawable/ic_popup_disk_full 0108005e=drawable/ic_popup_reminder 0108005f=drawable/ic_popup_sync 01080060=drawable/ic_search_category_default 01080061=drawable/ic_secure 01080062=drawable/list_selector_background 01080063=drawable/menu_frame 01080064=drawable/menu_full_frame 01080065=drawable/menuitem_background 01080066=drawable/picture_frame 01080067=drawable/presence_away 01080068=drawable/presence_busy 01080069=drawable/presence_invisible 0108006a=drawable/presence_offline 0108006b=drawable/presence_online 0108006c=drawable/progress_horizontal 0108006d=drawable/progress_indeterminate_horizontal 0108006e=drawable/radiobutton_off_background 0108006f=drawable/radiobutton_on_background 01080070=drawable/spinner_background 01080071=drawable/spinner_dropdown_background 01080072=drawable/star_big_on 01080073=drawable/star_big_off 01080074=drawable/star_on 01080075=drawable/star_off 01080076=drawable/stat_notify_call_mute 01080077=drawable/stat_notify_chat 01080078=drawable/stat_notify_error 01080079=drawable/stat_notify_more 0108007a=drawable/stat_notify_sdcard 0108007b=drawable/stat_notify_sdcard_usb 0108007c=drawable/stat_notify_sync 0108007d=drawable/stat_notify_sync_noanim 0108007e=drawable/stat_notify_voicemail 0108007f=drawable/stat_notify_missed_call 01080080=drawable/stat_sys_data_bluetooth 01080081=drawable/stat_sys_download 01080082=drawable/stat_sys_download_done 01080083=drawable/stat_sys_headset 01080084=drawable/stat_sys_phone_call 01080085=drawable/stat_sys_phone_call_forward 01080086=drawable/stat_sys_phone_call_on_hold 01080087=drawable/stat_sys_speakerphone 01080088=drawable/stat_sys_upload 01080089=drawable/stat_sys_upload_done 0108008a=drawable/stat_sys_warning 0108008b=drawable/status_bar_item_app_background 0108008c=drawable/status_bar_item_background 0108008d=drawable/sym_action_call 0108008e=drawable/sym_action_chat 0108008f=drawable/sym_action_email 01080090=drawable/sym_call_incoming 01080091=drawable/sym_call_missed 01080092=drawable/sym_call_outgoing 01080093=drawable/sym_def_app_icon 01080094=drawable/sym_contact_card 01080095=drawable/title_bar 01080096=drawable/toast_frame 01080097=drawable/zoom_plate 01080098=drawable/screen_background_dark 01080099=drawable/screen_background_light 0108009a=drawable/bottom_bar 0108009b=drawable/ic_dialog_info 0108009c=drawable/ic_menu_sort_alphabetically 0108009d=drawable/ic_menu_sort_by_size 0108009e=drawable/$$chooser_direct_share_icon_placeholder__0__0 0108009f=drawable/$$chooser_direct_share_icon_placeholder__1__0 010800a0=drawable/$$loader_horizontal_watch__1__0 010800a1=drawable/$$loader_horizontal_watch__2__0 010800a2=drawable/$$loader_horizontal_watch__3__0 010800a3=drawable/$$loader_horizontal_watch__4__0 010800a4=drawable/ic_btn_speak_now 010800a5=drawable/dark_header 010800a6=drawable/title_bar_tall 010800a7=drawable/stat_sys_vp_phone_call 010800a8=drawable/stat_sys_vp_phone_call_on_hold 010800a9=drawable/screen_background_dark_transparent 010800aa=drawable/screen_background_light_transparent 010800ab=drawable/stat_notify_sdcard_prepare 010800ac=drawable/presence_video_away 010800ad=drawable/presence_video_busy 010800ae=drawable/presence_video_online 010800af=drawable/presence_audio_away 010800b0=drawable/presence_audio_busy 010800b1=drawable/presence_audio_online 010800b2=drawable/dialog_holo_dark_frame 010800b3=drawable/dialog_holo_light_frame 010800b4=drawable/ic_info 010800b5=drawable/ic_safety_protection 010800b6=drawable/$$loader_horizontal_watch__5__0 010800b7=drawable/$$loading_spinner__1__0 010800b8=drawable/$$notification_progress_indeterminate_horizontal_material__0__0 010800b9=drawable/$$notification_progress_indeterminate_horizontal_material__0__1 010800ba=drawable/$$notification_progress_indeterminate_horizontal_material__0__2 010800bb=drawable/$$notification_progress_indeterminate_horizontal_material__1__0 010800bc=drawable/$$notification_progress_indeterminate_horizontal_material__1__1 010800bd=drawable/$$notification_progress_indeterminate_horizontal_material__1__2 010800be=drawable/$$notification_progress_indeterminate_horizontal_material__2__0 010800bf=drawable/$$notification_progress_indeterminate_horizontal_material__2__1 010800c0=drawable/$$notification_progress_indeterminate_horizontal_material__2__2 010800c1=drawable/$$notification_progress_indeterminate_horizontal_material__3__0 010800c2=drawable/$$notification_progress_indeterminate_horizontal_material__3__1 010800c3=drawable/$$notification_progress_indeterminate_horizontal_material__3__2 010800c4=drawable/$chooser_direct_share_icon_placeholder__0 010800c5=drawable/$chooser_direct_share_icon_placeholder__1 010800c6=drawable/$input_method_item_background_selected__0 010800c7=drawable/$loader_horizontal_watch__0 010800c8=drawable/$loader_horizontal_watch__1 010800c9=drawable/$loader_horizontal_watch__2 010800ca=drawable/$loader_horizontal_watch__3 010800cb=drawable/$loader_horizontal_watch__4 010800cc=drawable/$loader_horizontal_watch__5 010800cd=drawable/$loader_horizontal_watch__6 010800ce=drawable/$loader_horizontal_watch__7 010800cf=drawable/$loading_spinner__0 010800d0=drawable/$loading_spinner__1 010800d1=drawable/$loading_spinner__2 010800d2=drawable/$notification_progress_indeterminate_horizontal_material__0 010800d3=drawable/$notification_progress_indeterminate_horizontal_material__1 010800d4=drawable/$notification_progress_indeterminate_horizontal_material__2 010800d5=drawable/$notification_progress_indeterminate_horizontal_material__3 010800d6=drawable/$progress_indeterminate_horizontal_material__0 010800d7=drawable/$progress_indeterminate_horizontal_material__1 010800d8=drawable/$progress_indeterminate_horizontal_material__10 010800d9=drawable/$progress_indeterminate_horizontal_material__11 010800da=drawable/$progress_indeterminate_horizontal_material__12 010800db=drawable/$progress_indeterminate_horizontal_material__13 010800dc=drawable/$progress_indeterminate_horizontal_material__14 010800dd=drawable/$progress_indeterminate_horizontal_material__15 010800de=drawable/$progress_indeterminate_horizontal_material__16 010800df=drawable/$progress_indeterminate_horizontal_material__17 010800e0=drawable/$progress_indeterminate_horizontal_material__18 010800e1=drawable/$progress_indeterminate_horizontal_material__19 010800e2=drawable/$progress_indeterminate_horizontal_material__2 010800e3=drawable/$progress_indeterminate_horizontal_material__20 010800e4=drawable/$progress_indeterminate_horizontal_material__21 010800e5=drawable/$progress_indeterminate_horizontal_material__22 010800e6=drawable/$progress_indeterminate_horizontal_material__23 010800e7=drawable/$progress_indeterminate_horizontal_material__24 010800e8=drawable/$progress_indeterminate_horizontal_material__25 010800e9=drawable/$progress_indeterminate_horizontal_material__26 010800ea=drawable/$progress_indeterminate_horizontal_material__27 010800eb=drawable/$progress_indeterminate_horizontal_material__28 010800ec=drawable/$progress_indeterminate_horizontal_material__29 010800ed=drawable/$progress_indeterminate_horizontal_material__3 010800ee=drawable/$progress_indeterminate_horizontal_material__30 010800ef=drawable/$progress_indeterminate_horizontal_material__31 010800f0=drawable/$progress_indeterminate_horizontal_material__32 010800f1=drawable/$progress_indeterminate_horizontal_material__33 010800f2=drawable/$progress_indeterminate_horizontal_material__34 010800f3=drawable/$progress_indeterminate_horizontal_material__35 010800f4=drawable/$progress_indeterminate_horizontal_material__36 010800f5=drawable/$progress_indeterminate_horizontal_material__37 010800f6=drawable/$progress_indeterminate_horizontal_material__38 010800f7=drawable/$progress_indeterminate_horizontal_material__39 010800f8=drawable/$progress_indeterminate_horizontal_material__4 010800f9=drawable/$progress_indeterminate_horizontal_material__40 010800fa=drawable/$progress_indeterminate_horizontal_material__41 010800fb=drawable/$progress_indeterminate_horizontal_material__42 010800fc=drawable/$progress_indeterminate_horizontal_material__43 010800fd=drawable/$progress_indeterminate_horizontal_material__44 010800fe=drawable/$progress_indeterminate_horizontal_material__45 010800ff=drawable/$progress_indeterminate_horizontal_material__46 01080100=drawable/$progress_indeterminate_horizontal_material__47 01080101=drawable/$progress_indeterminate_horizontal_material__48 01080102=drawable/$progress_indeterminate_horizontal_material__49 01080103=drawable/$progress_indeterminate_horizontal_material__5 01080104=drawable/$progress_indeterminate_horizontal_material__50 01080105=drawable/$progress_indeterminate_horizontal_material__51 01080106=drawable/$progress_indeterminate_horizontal_material__52 01080107=drawable/$progress_indeterminate_horizontal_material__53 01080108=drawable/$progress_indeterminate_horizontal_material__54 01080109=drawable/$progress_indeterminate_horizontal_material__55 0108010a=drawable/$progress_indeterminate_horizontal_material__56 0108010b=drawable/$progress_indeterminate_horizontal_material__57 0108010c=drawable/$progress_indeterminate_horizontal_material__58 0108010d=drawable/$progress_indeterminate_horizontal_material__59 0108010e=drawable/$progress_indeterminate_horizontal_material__6 0108010f=drawable/$progress_indeterminate_horizontal_material__7 01080110=drawable/$progress_indeterminate_horizontal_material__8 01080111=drawable/$progress_indeterminate_horizontal_material__9 01080112=drawable/ab_bottom_solid_dark_holo 01080113=drawable/ab_bottom_solid_inverse_holo 01080114=drawable/ab_bottom_solid_light_holo 01080115=drawable/ab_bottom_transparent_dark_holo 01080116=drawable/ab_bottom_transparent_light_holo 01080117=drawable/ab_share_pack_holo_dark 01080118=drawable/ab_share_pack_holo_light 01080119=drawable/ab_share_pack_material 0108011a=drawable/ab_share_pack_mtrl_alpha 0108011b=drawable/ab_solid_dark_holo 0108011c=drawable/ab_solid_light_holo 0108011d=drawable/ab_solid_shadow_holo 0108011e=drawable/ab_solid_shadow_material 0108011f=drawable/ab_solid_shadow_mtrl_alpha 01080120=drawable/ab_stacked_solid_dark_holo 01080121=drawable/ab_stacked_solid_inverse_holo 01080122=drawable/ab_stacked_solid_light_holo 01080123=drawable/ab_stacked_transparent_dark_holo 01080124=drawable/ab_stacked_transparent_light_holo 01080125=drawable/ab_transparent_dark_holo 01080126=drawable/ab_transparent_light_holo 01080127=drawable/accessibility_autoclick_button_group_rounded_background 01080128=drawable/accessibility_autoclick_button_rounded_background 01080129=drawable/accessibility_autoclick_double_click 0108012a=drawable/accessibility_autoclick_drag 0108012b=drawable/accessibility_autoclick_left_click 0108012c=drawable/accessibility_autoclick_pause 0108012d=drawable/accessibility_autoclick_position 0108012e=drawable/accessibility_autoclick_resume 0108012f=drawable/accessibility_autoclick_right_click 01080130=drawable/accessibility_autoclick_scroll 01080131=drawable/accessibility_autoclick_scroll_down 01080132=drawable/accessibility_autoclick_scroll_exit 01080133=drawable/accessibility_autoclick_scroll_left 01080134=drawable/accessibility_autoclick_scroll_right 01080135=drawable/accessibility_autoclick_scroll_up 01080136=drawable/accessibility_autoclick_type_panel_rounded_background 01080137=drawable/accessibility_magnification_thumbnail_background_bg 01080138=drawable/accessibility_magnification_thumbnail_background_fg 01080139=drawable/accessibility_magnification_thumbnail_bg 0108013a=drawable/action_bar_background 0108013b=drawable/action_bar_divider 0108013c=drawable/action_bar_item_background_material 0108013d=drawable/activated_background 0108013e=drawable/activated_background_holo_dark 0108013f=drawable/activated_background_holo_light 01080140=drawable/activated_background_light 01080141=drawable/activated_background_material 01080142=drawable/activity_embedding_divider_handle 01080143=drawable/activity_embedding_divider_handle_default 01080144=drawable/activity_embedding_divider_handle_pressed 01080145=drawable/activity_title_bar 01080146=drawable/alert_window_layer 01080147=drawable/android_logotype 01080148=drawable/app_icon_background 01080149=drawable/archived_app_cloud_overlay 0108014a=drawable/autofill_bottomsheet_background 0108014b=drawable/autofill_dataset_picker_background 0108014c=drawable/autofill_half_sheet_divider 0108014d=drawable/autofilled_highlight 0108014e=drawable/background_holo_dark 0108014f=drawable/background_holo_light 01080150=drawable/background_leanback_setup 01080151=drawable/battery_charge_background 01080152=drawable/bilingual_language_item_selection_indicator 01080153=drawable/blank_tile 01080154=drawable/bottomsheet_background 01080155=drawable/box 01080156=drawable/btn_background_material_filled_tonal_watch 01080157=drawable/btn_background_material_filled_watch 01080158=drawable/btn_background_material_outlined_watch 01080159=drawable/btn_background_material_text_watch 0108015a=drawable/btn_borderless_material 0108015b=drawable/btn_borderless_rect 0108015c=drawable/btn_browser_zoom_fit_page 0108015d=drawable/btn_browser_zoom_page_overview 0108015e=drawable/btn_cab_done_default_holo_dark 0108015f=drawable/btn_cab_done_default_holo_light 01080160=drawable/btn_cab_done_focused_holo_dark 01080161=drawable/btn_cab_done_focused_holo_light 01080162=drawable/btn_cab_done_holo_dark 01080163=drawable/btn_cab_done_holo_light 01080164=drawable/btn_cab_done_pressed_holo_dark 01080165=drawable/btn_cab_done_pressed_holo_light 01080166=drawable/btn_check 01080167=drawable/btn_check_buttonless_off 01080168=drawable/btn_check_buttonless_on 01080169=drawable/btn_check_holo_dark 0108016a=drawable/btn_check_holo_light 0108016b=drawable/btn_check_label_background 0108016c=drawable/btn_check_material_anim 0108016d=drawable/btn_check_off 0108016e=drawable/btn_check_off_disable 0108016f=drawable/btn_check_off_disable_focused 01080170=drawable/btn_check_off_disable_focused_holo_dark 01080171=drawable/btn_check_off_disable_focused_holo_light 01080172=drawable/btn_check_off_disable_holo_dark 01080173=drawable/btn_check_off_disable_holo_light 01080174=drawable/btn_check_off_disabled_focused_holo_dark 01080175=drawable/btn_check_off_disabled_focused_holo_light 01080176=drawable/btn_check_off_disabled_holo_dark 01080177=drawable/btn_check_off_disabled_holo_light 01080178=drawable/btn_check_off_focused_holo_dark 01080179=drawable/btn_check_off_focused_holo_light 0108017a=drawable/btn_check_off_holo 0108017b=drawable/btn_check_off_holo_dark 0108017c=drawable/btn_check_off_holo_light 0108017d=drawable/btn_check_off_normal_holo_dark 0108017e=drawable/btn_check_off_normal_holo_light 0108017f=drawable/btn_check_off_pressed 01080180=drawable/btn_check_off_pressed_holo_dark 01080181=drawable/btn_check_off_pressed_holo_light 01080182=drawable/btn_check_off_selected 01080183=drawable/btn_check_on 01080184=drawable/btn_check_on_disable 01080185=drawable/btn_check_on_disable_focused 01080186=drawable/btn_check_on_disable_focused_holo_light 01080187=drawable/btn_check_on_disable_holo_dark 01080188=drawable/btn_check_on_disable_holo_light 01080189=drawable/btn_check_on_disabled_focused_holo_dark 0108018a=drawable/btn_check_on_disabled_focused_holo_light 0108018b=drawable/btn_check_on_disabled_holo_dark 0108018c=drawable/btn_check_on_disabled_holo_light 0108018d=drawable/btn_check_on_focused_holo_dark 0108018e=drawable/btn_check_on_focused_holo_light 0108018f=drawable/btn_check_on_holo 01080190=drawable/btn_check_on_holo_dark 01080191=drawable/btn_check_on_holo_light 01080192=drawable/btn_check_on_pressed 01080193=drawable/btn_check_on_pressed_holo_dark 01080194=drawable/btn_check_on_pressed_holo_light 01080195=drawable/btn_check_on_selected 01080196=drawable/btn_checkbox_checked_mtrl 01080197=drawable/btn_checkbox_checked_to_unchecked_mtrl_animation 01080198=drawable/btn_checkbox_unchecked_mtrl 01080199=drawable/btn_checkbox_unchecked_to_checked_mtrl_animation 0108019a=drawable/btn_circle 0108019b=drawable/btn_circle_disable 0108019c=drawable/btn_circle_disable_focused 0108019d=drawable/btn_circle_normal 0108019e=drawable/btn_circle_pressed 0108019f=drawable/btn_circle_selected 010801a0=drawable/btn_clock_material 010801a1=drawable/btn_close 010801a2=drawable/btn_close_normal 010801a3=drawable/btn_close_pressed 010801a4=drawable/btn_close_selected 010801a5=drawable/btn_colored_material 010801a6=drawable/btn_default_disabled_focused_holo_dark 010801a7=drawable/btn_default_disabled_focused_holo_light 010801a8=drawable/btn_default_disabled_holo 010801a9=drawable/btn_default_disabled_holo_dark 010801aa=drawable/btn_default_disabled_holo_light 010801ab=drawable/btn_default_focused_holo 010801ac=drawable/btn_default_focused_holo_dark 010801ad=drawable/btn_default_focused_holo_light 010801ae=drawable/btn_default_holo_dark 010801af=drawable/btn_default_holo_light 010801b0=drawable/btn_default_material 010801b1=drawable/btn_default_mtrl_shape 010801b2=drawable/btn_default_normal 010801b3=drawable/btn_default_normal_disable 010801b4=drawable/btn_default_normal_disable_focused 010801b5=drawable/btn_default_normal_holo 010801b6=drawable/btn_default_normal_holo_dark 010801b7=drawable/btn_default_normal_holo_light 010801b8=drawable/btn_default_pressed 010801b9=drawable/btn_default_pressed_holo 010801ba=drawable/btn_default_pressed_holo_dark 010801bb=drawable/btn_default_pressed_holo_light 010801bc=drawable/btn_default_selected 010801bd=drawable/btn_default_small_normal 010801be=drawable/btn_default_small_normal_disable 010801bf=drawable/btn_default_small_normal_disable_focused 010801c0=drawable/btn_default_small_pressed 010801c1=drawable/btn_default_small_selected 010801c2=drawable/btn_default_transparent 010801c3=drawable/btn_default_transparent_normal 010801c4=drawable/btn_dialog_disable 010801c5=drawable/btn_dialog_normal 010801c6=drawable/btn_dialog_pressed 010801c7=drawable/btn_dialog_selected 010801c8=drawable/btn_dropdown_disabled 010801c9=drawable/btn_dropdown_disabled_focused 010801ca=drawable/btn_dropdown_normal 010801cb=drawable/btn_dropdown_pressed 010801cc=drawable/btn_dropdown_selected 010801cd=drawable/btn_erase_default 010801ce=drawable/btn_erase_pressed 010801cf=drawable/btn_erase_selected 010801d0=drawable/btn_global_search 010801d1=drawable/btn_global_search_normal 010801d2=drawable/btn_group_disabled_holo_dark 010801d3=drawable/btn_group_disabled_holo_light 010801d4=drawable/btn_group_focused_holo_dark 010801d5=drawable/btn_group_focused_holo_light 010801d6=drawable/btn_group_holo_dark 010801d7=drawable/btn_group_holo_light 010801d8=drawable/btn_group_normal_holo_dark 010801d9=drawable/btn_group_normal_holo_light 010801da=drawable/btn_group_pressed_holo_dark 010801db=drawable/btn_group_pressed_holo_light 010801dc=drawable/btn_keyboard_key 010801dd=drawable/btn_keyboard_key_dark_normal_holo 010801de=drawable/btn_keyboard_key_dark_normal_off_holo 010801df=drawable/btn_keyboard_key_dark_normal_on_holo 010801e0=drawable/btn_keyboard_key_dark_pressed_holo 010801e1=drawable/btn_keyboard_key_dark_pressed_off_holo 010801e2=drawable/btn_keyboard_key_dark_pressed_on_holo 010801e3=drawable/btn_keyboard_key_fulltrans 010801e4=drawable/btn_keyboard_key_fulltrans_normal 010801e5=drawable/btn_keyboard_key_fulltrans_normal_off 010801e6=drawable/btn_keyboard_key_fulltrans_normal_on 010801e7=drawable/btn_keyboard_key_fulltrans_pressed 010801e8=drawable/btn_keyboard_key_fulltrans_pressed_off 010801e9=drawable/btn_keyboard_key_fulltrans_pressed_on 010801ea=drawable/btn_keyboard_key_ics 010801eb=drawable/btn_keyboard_key_light_normal_holo 010801ec=drawable/btn_keyboard_key_light_pressed_holo 010801ed=drawable/btn_keyboard_key_material 010801ee=drawable/btn_keyboard_key_normal 010801ef=drawable/btn_keyboard_key_normal_off 010801f0=drawable/btn_keyboard_key_normal_on 010801f1=drawable/btn_keyboard_key_pressed 010801f2=drawable/btn_keyboard_key_pressed_off 010801f3=drawable/btn_keyboard_key_pressed_on 010801f4=drawable/btn_keyboard_key_trans 010801f5=drawable/btn_keyboard_key_trans_normal 010801f6=drawable/btn_keyboard_key_trans_normal_off 010801f7=drawable/btn_keyboard_key_trans_normal_on 010801f8=drawable/btn_keyboard_key_trans_pressed 010801f9=drawable/btn_keyboard_key_trans_pressed_off 010801fa=drawable/btn_keyboard_key_trans_pressed_on 010801fb=drawable/btn_keyboard_key_trans_selected 010801fc=drawable/btn_leanback 010801fd=drawable/btn_lock_normal 010801fe=drawable/btn_media_player 010801ff=drawable/btn_media_player_disabled 01080200=drawable/btn_media_player_disabled_selected 01080201=drawable/btn_media_player_pressed 01080202=drawable/btn_media_player_selected 01080203=drawable/btn_minus_default 01080204=drawable/btn_minus_disable 01080205=drawable/btn_minus_disable_focused 01080206=drawable/btn_minus_pressed 01080207=drawable/btn_minus_selected 01080208=drawable/btn_notification_emphasized 01080209=drawable/btn_outlined 0108020a=drawable/btn_plus_default 0108020b=drawable/btn_plus_disable 0108020c=drawable/btn_plus_disable_focused 0108020d=drawable/btn_plus_pressed 0108020e=drawable/btn_plus_selected 0108020f=drawable/btn_radio_holo_dark 01080210=drawable/btn_radio_holo_light 01080211=drawable/btn_radio_label_background 01080212=drawable/btn_radio_material_anim 01080213=drawable/btn_radio_off 01080214=drawable/btn_radio_off_disabled_focused_holo_dark 01080215=drawable/btn_radio_off_disabled_focused_holo_light 01080216=drawable/btn_radio_off_disabled_holo_dark 01080217=drawable/btn_radio_off_disabled_holo_light 01080218=drawable/btn_radio_off_focused_holo_dark 01080219=drawable/btn_radio_off_focused_holo_light 0108021a=drawable/btn_radio_off_holo 0108021b=drawable/btn_radio_off_holo_dark 0108021c=drawable/btn_radio_off_holo_light 0108021d=drawable/btn_radio_off_mtrl 0108021e=drawable/btn_radio_off_pressed 0108021f=drawable/btn_radio_off_pressed_holo_dark 01080220=drawable/btn_radio_off_pressed_holo_light 01080221=drawable/btn_radio_off_selected 01080222=drawable/btn_radio_off_to_on_mtrl_animation 01080223=drawable/btn_radio_on 01080224=drawable/btn_radio_on_disabled_focused_holo_dark 01080225=drawable/btn_radio_on_disabled_focused_holo_light 01080226=drawable/btn_radio_on_disabled_holo_dark 01080227=drawable/btn_radio_on_disabled_holo_light 01080228=drawable/btn_radio_on_focused_holo_dark 01080229=drawable/btn_radio_on_focused_holo_light 0108022a=drawable/btn_radio_on_holo 0108022b=drawable/btn_radio_on_holo_dark 0108022c=drawable/btn_radio_on_holo_light 0108022d=drawable/btn_radio_on_mtrl 0108022e=drawable/btn_radio_on_mtrl_alpha 0108022f=drawable/btn_radio_on_pressed 01080230=drawable/btn_radio_on_pressed_holo_dark 01080231=drawable/btn_radio_on_pressed_holo_light 01080232=drawable/btn_radio_on_pressed_mtrl_alpha 01080233=drawable/btn_radio_on_selected 01080234=drawable/btn_radio_on_to_off_mtrl_animation 01080235=drawable/btn_rating_star_off_disabled_focused_holo_dark 01080236=drawable/btn_rating_star_off_disabled_focused_holo_light 01080237=drawable/btn_rating_star_off_disabled_holo_dark 01080238=drawable/btn_rating_star_off_disabled_holo_light 01080239=drawable/btn_rating_star_off_focused_holo_dark 0108023a=drawable/btn_rating_star_off_focused_holo_light 0108023b=drawable/btn_rating_star_off_mtrl_alpha 0108023c=drawable/btn_rating_star_off_normal 0108023d=drawable/btn_rating_star_off_normal_holo_dark 0108023e=drawable/btn_rating_star_off_normal_holo_light 0108023f=drawable/btn_rating_star_off_pressed 01080240=drawable/btn_rating_star_off_pressed_holo_dark 01080241=drawable/btn_rating_star_off_pressed_holo_light 01080242=drawable/btn_rating_star_off_selected 01080243=drawable/btn_rating_star_on_disabled_focused_holo_dark 01080244=drawable/btn_rating_star_on_disabled_focused_holo_light 01080245=drawable/btn_rating_star_on_disabled_holo_dark 01080246=drawable/btn_rating_star_on_disabled_holo_light 01080247=drawable/btn_rating_star_on_focused_holo_dark 01080248=drawable/btn_rating_star_on_focused_holo_light 01080249=drawable/btn_rating_star_on_mtrl_alpha 0108024a=drawable/btn_rating_star_on_normal 0108024b=drawable/btn_rating_star_on_normal_holo_dark 0108024c=drawable/btn_rating_star_on_normal_holo_light 0108024d=drawable/btn_rating_star_on_pressed 0108024e=drawable/btn_rating_star_on_pressed_holo_dark 0108024f=drawable/btn_rating_star_on_pressed_holo_light 01080250=drawable/btn_rating_star_on_selected 01080251=drawable/btn_search_dialog 01080252=drawable/btn_search_dialog_default 01080253=drawable/btn_search_dialog_pressed 01080254=drawable/btn_search_dialog_selected 01080255=drawable/btn_search_dialog_voice 01080256=drawable/btn_search_dialog_voice_default 01080257=drawable/btn_search_dialog_voice_pressed 01080258=drawable/btn_search_dialog_voice_selected 01080259=drawable/btn_square_overlay 0108025a=drawable/btn_square_overlay_disabled 0108025b=drawable/btn_square_overlay_disabled_focused 0108025c=drawable/btn_square_overlay_normal 0108025d=drawable/btn_square_overlay_pressed 0108025e=drawable/btn_square_overlay_selected 0108025f=drawable/btn_star_big_off_disable 01080260=drawable/btn_star_big_off_disable_focused 01080261=drawable/btn_star_big_off_pressed 01080262=drawable/btn_star_big_off_selected 01080263=drawable/btn_star_big_on_disable 01080264=drawable/btn_star_big_on_disable_focused 01080265=drawable/btn_star_big_on_pressed 01080266=drawable/btn_star_big_on_selected 01080267=drawable/btn_star_holo_dark 01080268=drawable/btn_star_holo_light 01080269=drawable/btn_star_label_background 0108026a=drawable/btn_star_material 0108026b=drawable/btn_star_mtrl_alpha 0108026c=drawable/btn_star_off_disabled_focused_holo_dark 0108026d=drawable/btn_star_off_disabled_focused_holo_light 0108026e=drawable/btn_star_off_disabled_holo_dark 0108026f=drawable/btn_star_off_disabled_holo_light 01080270=drawable/btn_star_off_focused_holo_dark 01080271=drawable/btn_star_off_focused_holo_light 01080272=drawable/btn_star_off_normal_holo_dark 01080273=drawable/btn_star_off_normal_holo_light 01080274=drawable/btn_star_off_pressed_holo_dark 01080275=drawable/btn_star_off_pressed_holo_light 01080276=drawable/btn_star_on_disabled_focused_holo_dark 01080277=drawable/btn_star_on_disabled_focused_holo_light 01080278=drawable/btn_star_on_disabled_holo_dark 01080279=drawable/btn_star_on_disabled_holo_light 0108027a=drawable/btn_star_on_focused_holo_dark 0108027b=drawable/btn_star_on_focused_holo_light 0108027c=drawable/btn_star_on_normal_holo_dark 0108027d=drawable/btn_star_on_normal_holo_light 0108027e=drawable/btn_star_on_pressed_holo_dark 0108027f=drawable/btn_star_on_pressed_holo_light 01080280=drawable/btn_switch_to_off_mtrl_00001 01080281=drawable/btn_switch_to_off_mtrl_00002 01080282=drawable/btn_switch_to_off_mtrl_00003 01080283=drawable/btn_switch_to_off_mtrl_00004 01080284=drawable/btn_switch_to_off_mtrl_00005 01080285=drawable/btn_switch_to_off_mtrl_00006 01080286=drawable/btn_switch_to_off_mtrl_00007 01080287=drawable/btn_switch_to_off_mtrl_00008 01080288=drawable/btn_switch_to_off_mtrl_00009 01080289=drawable/btn_switch_to_off_mtrl_00010 0108028a=drawable/btn_switch_to_off_mtrl_00011 0108028b=drawable/btn_switch_to_off_mtrl_00012 0108028c=drawable/btn_switch_to_on_mtrl_00001 0108028d=drawable/btn_switch_to_on_mtrl_00002 0108028e=drawable/btn_switch_to_on_mtrl_00003 0108028f=drawable/btn_switch_to_on_mtrl_00004 01080290=drawable/btn_switch_to_on_mtrl_00005 01080291=drawable/btn_switch_to_on_mtrl_00006 01080292=drawable/btn_switch_to_on_mtrl_00007 01080293=drawable/btn_switch_to_on_mtrl_00008 01080294=drawable/btn_switch_to_on_mtrl_00009 01080295=drawable/btn_switch_to_on_mtrl_00010 01080296=drawable/btn_switch_to_on_mtrl_00011 01080297=drawable/btn_switch_to_on_mtrl_00012 01080298=drawable/btn_toggle 01080299=drawable/btn_toggle_bg 0108029a=drawable/btn_toggle_holo_dark 0108029b=drawable/btn_toggle_holo_light 0108029c=drawable/btn_toggle_material 0108029d=drawable/btn_toggle_off 0108029e=drawable/btn_toggle_off_disabled_focused_holo_dark 0108029f=drawable/btn_toggle_off_disabled_focused_holo_light 010802a0=drawable/btn_toggle_off_disabled_holo_dark 010802a1=drawable/btn_toggle_off_disabled_holo_light 010802a2=drawable/btn_toggle_off_focused_holo_dark 010802a3=drawable/btn_toggle_off_focused_holo_light 010802a4=drawable/btn_toggle_off_normal_holo_dark 010802a5=drawable/btn_toggle_off_normal_holo_light 010802a6=drawable/btn_toggle_off_pressed_holo_dark 010802a7=drawable/btn_toggle_off_pressed_holo_light 010802a8=drawable/btn_toggle_on 010802a9=drawable/btn_toggle_on_disabled_focused_holo_dark 010802aa=drawable/btn_toggle_on_disabled_focused_holo_light 010802ab=drawable/btn_toggle_on_disabled_holo_dark 010802ac=drawable/btn_toggle_on_disabled_holo_light 010802ad=drawable/btn_toggle_on_focused_holo_dark 010802ae=drawable/btn_toggle_on_focused_holo_light 010802af=drawable/btn_toggle_on_normal_holo_dark 010802b0=drawable/btn_toggle_on_normal_holo_light 010802b1=drawable/btn_toggle_on_pressed_holo_dark 010802b2=drawable/btn_toggle_on_pressed_holo_light 010802b3=drawable/btn_tonal 010802b4=drawable/btn_zoom_down 010802b5=drawable/btn_zoom_down_disabled 010802b6=drawable/btn_zoom_down_disabled_focused 010802b7=drawable/btn_zoom_down_normal 010802b8=drawable/btn_zoom_down_pressed 010802b9=drawable/btn_zoom_down_selected 010802ba=drawable/btn_zoom_page 010802bb=drawable/btn_zoom_page_normal 010802bc=drawable/btn_zoom_page_press 010802bd=drawable/btn_zoom_up 010802be=drawable/btn_zoom_up_disabled 010802bf=drawable/btn_zoom_up_disabled_focused 010802c0=drawable/btn_zoom_up_normal 010802c1=drawable/btn_zoom_up_pressed 010802c2=drawable/btn_zoom_up_selected 010802c3=drawable/button_inset 010802c4=drawable/cab_background_bottom_holo_dark 010802c5=drawable/cab_background_bottom_holo_light 010802c6=drawable/cab_background_bottom_material 010802c7=drawable/cab_background_bottom_mtrl_alpha 010802c8=drawable/cab_background_top_holo_dark 010802c9=drawable/cab_background_top_holo_light 010802ca=drawable/cab_background_top_material 010802cb=drawable/cab_background_top_mtrl_alpha 010802cc=drawable/call_contact 010802cd=drawable/car_button_background 010802ce=drawable/car_dialog_button_background 010802cf=drawable/car_seekbar_thumb 010802d0=drawable/car_seekbar_thumb_dark 010802d1=drawable/car_seekbar_thumb_light 010802d2=drawable/car_seekbar_track 010802d3=drawable/car_seekbar_track_dark 010802d4=drawable/car_seekbar_track_light 010802d5=drawable/car_switch_thumb 010802d6=drawable/car_switch_track 010802d7=drawable/chooser_action_button_bg 010802d8=drawable/chooser_content_preview_rounded 010802d9=drawable/chooser_dialog_background 010802da=drawable/chooser_direct_share_icon_placeholder 010802db=drawable/chooser_direct_share_label_placeholder 010802dc=drawable/chooser_file_generic 010802dd=drawable/chooser_group_background 010802de=drawable/chooser_pinned_background 010802df=drawable/chooser_row_layer_list 010802e0=drawable/cling_arrow_up 010802e1=drawable/cling_bg 010802e2=drawable/cling_button 010802e3=drawable/cling_button_normal 010802e4=drawable/cling_button_pressed 010802e5=drawable/clock_dial 010802e6=drawable/clock_hand_hour 010802e7=drawable/clock_hand_minute 010802e8=drawable/close_button_bg 010802e9=drawable/code_lock_bottom 010802ea=drawable/code_lock_left 010802eb=drawable/code_lock_top 010802ec=drawable/combobox_disabled 010802ed=drawable/combobox_nohighlight 010802ee=drawable/compass_arrow 010802ef=drawable/compass_base 010802f0=drawable/config_scrollbarThumbVertical 010802f1=drawable/config_scrollbarTrackVertical 010802f2=drawable/contact_header_bg 010802f3=drawable/control_background_32dp_material 010802f4=drawable/control_background_40dp_material 010802f5=drawable/conversation_badge_background 010802f6=drawable/conversation_badge_ring 010802f7=drawable/create_contact 010802f8=drawable/dark_header_dither 010802f9=drawable/day_picker_week_view_dayline_holo 010802fa=drawable/default_lock_wallpaper 010802fb=drawable/default_wallpaper 010802fc=drawable/dialog_alert_button_background_negative_watch 010802fd=drawable/dialog_alert_button_background_positive_watch 010802fe=drawable/dialog_alert_button_negative_watch 010802ff=drawable/dialog_alert_button_positive_watch 01080300=drawable/dialog_background_material 01080301=drawable/dialog_bottom_holo_dark 01080302=drawable/dialog_bottom_holo_light 01080303=drawable/dialog_divider_horizontal_holo_dark 01080304=drawable/dialog_divider_horizontal_holo_light 01080305=drawable/dialog_divider_horizontal_light 01080306=drawable/dialog_full_holo_dark 01080307=drawable/dialog_full_holo_light 01080308=drawable/dialog_ic_close_focused_holo_dark 01080309=drawable/dialog_ic_close_focused_holo_light 0108030a=drawable/dialog_ic_close_normal_holo_dark 0108030b=drawable/dialog_ic_close_normal_holo_light 0108030c=drawable/dialog_ic_close_pressed_holo_dark 0108030d=drawable/dialog_ic_close_pressed_holo_light 0108030e=drawable/dialog_middle_holo 0108030f=drawable/dialog_middle_holo_dark 01080310=drawable/dialog_middle_holo_light 01080311=drawable/dialog_top_holo_dark 01080312=drawable/dialog_top_holo_light 01080313=drawable/divider_horizontal_bright_opaque 01080314=drawable/divider_horizontal_dark_opaque 01080315=drawable/divider_horizontal_holo_dark 01080316=drawable/divider_horizontal_holo_light 01080317=drawable/divider_strong_holo 01080318=drawable/divider_vertical_bright 01080319=drawable/divider_vertical_bright_opaque 0108031a=drawable/divider_vertical_dark 0108031b=drawable/divider_vertical_dark_opaque 0108031c=drawable/divider_vertical_holo_dark 0108031d=drawable/divider_vertical_holo_light 0108031e=drawable/dropdown_disabled_focused_holo_dark 0108031f=drawable/dropdown_disabled_focused_holo_light 01080320=drawable/dropdown_disabled_holo_dark 01080321=drawable/dropdown_disabled_holo_light 01080322=drawable/dropdown_focused_holo_dark 01080323=drawable/dropdown_focused_holo_light 01080324=drawable/dropdown_ic_arrow_disabled_focused_holo_dark 01080325=drawable/dropdown_ic_arrow_disabled_focused_holo_light 01080326=drawable/dropdown_ic_arrow_disabled_holo_dark 01080327=drawable/dropdown_ic_arrow_disabled_holo_light 01080328=drawable/dropdown_ic_arrow_focused_holo_dark 01080329=drawable/dropdown_ic_arrow_focused_holo_light 0108032a=drawable/dropdown_ic_arrow_normal_holo_dark 0108032b=drawable/dropdown_ic_arrow_normal_holo_light 0108032c=drawable/dropdown_ic_arrow_pressed_holo_dark 0108032d=drawable/dropdown_ic_arrow_pressed_holo_light 0108032e=drawable/dropdown_normal_holo_dark 0108032f=drawable/dropdown_normal_holo_light 01080330=drawable/dropdown_pressed_holo_dark 01080331=drawable/dropdown_pressed_holo_light 01080332=drawable/edit_query 01080333=drawable/edit_query_background 01080334=drawable/edit_query_background_normal 01080335=drawable/edit_query_background_pressed 01080336=drawable/edit_query_background_selected 01080337=drawable/edit_text_holo_dark 01080338=drawable/edit_text_holo_light 01080339=drawable/edit_text_material 0108033a=drawable/editbox_background_focus_yellow 0108033b=drawable/editbox_dropdown_background 0108033c=drawable/editbox_dropdown_background_dark 0108033d=drawable/emergency_icon 0108033e=drawable/emo_im_angel 0108033f=drawable/emo_im_cool 01080340=drawable/emo_im_crying 01080341=drawable/emo_im_embarrassed 01080342=drawable/emo_im_foot_in_mouth 01080343=drawable/emo_im_happy 01080344=drawable/emo_im_kissing 01080345=drawable/emo_im_laughing 01080346=drawable/emo_im_lips_are_sealed 01080347=drawable/emo_im_money_mouth 01080348=drawable/emo_im_sad 01080349=drawable/emo_im_surprised 0108034a=drawable/emo_im_tongue_sticking_out 0108034b=drawable/emo_im_undecided 0108034c=drawable/emo_im_winking 0108034d=drawable/emo_im_wtf 0108034e=drawable/emo_im_yelling 0108034f=drawable/emulator_circular_window_overlay 01080350=drawable/expand_button_pill_bg 01080351=drawable/expander_close_holo_dark 01080352=drawable/expander_close_holo_light 01080353=drawable/expander_close_mtrl_alpha 01080354=drawable/expander_group 01080355=drawable/expander_group_holo_dark 01080356=drawable/expander_group_holo_light 01080357=drawable/expander_group_material 01080358=drawable/expander_ic_maximized 01080359=drawable/expander_ic_minimized 0108035a=drawable/expander_open_holo_dark 0108035b=drawable/expander_open_holo_light 0108035c=drawable/expander_open_mtrl_alpha 0108035d=drawable/fastscroll_label_left_holo_dark 0108035e=drawable/fastscroll_label_left_holo_light 0108035f=drawable/fastscroll_label_left_material 01080360=drawable/fastscroll_label_right_holo_dark 01080361=drawable/fastscroll_label_right_holo_light 01080362=drawable/fastscroll_label_right_material 01080363=drawable/fastscroll_thumb_default_holo 01080364=drawable/fastscroll_thumb_holo 01080365=drawable/fastscroll_thumb_material 01080366=drawable/fastscroll_thumb_pressed_holo 01080367=drawable/fastscroll_track_default_holo_dark 01080368=drawable/fastscroll_track_default_holo_light 01080369=drawable/fastscroll_track_holo_dark 0108036a=drawable/fastscroll_track_holo_light 0108036b=drawable/fastscroll_track_material 0108036c=drawable/fastscroll_track_pressed_holo_dark 0108036d=drawable/fastscroll_track_pressed_holo_light 0108036e=drawable/floating_popup_background 0108036f=drawable/focus_event_pressed_key_background 01080370=drawable/focus_event_rotary_input_background 01080371=drawable/focused_application_background_static 01080372=drawable/frame_gallery_thumb 01080373=drawable/frame_gallery_thumb_pressed 01080374=drawable/frame_gallery_thumb_selected 01080375=drawable/ft_avd_toarrow 01080376=drawable/ft_avd_toarrow_animation 01080377=drawable/ft_avd_tooverflow 01080378=drawable/ft_avd_tooverflow_animation 01080379=drawable/gallery_item_background 0108037a=drawable/gallery_selected_default 0108037b=drawable/gallery_selected_focused 0108037c=drawable/gallery_selected_pressed 0108037d=drawable/gallery_unselected_default 0108037e=drawable/gallery_unselected_pressed 0108037f=drawable/global_action_item_divider 01080380=drawable/global_actions_item_grey_background 01080381=drawable/global_actions_item_grey_background_shape 01080382=drawable/global_actions_item_red_background 01080383=drawable/global_actions_item_red_background_shape 01080384=drawable/grid_selector_background 01080385=drawable/grid_selector_background_focus 01080386=drawable/grid_selector_background_pressed 01080387=drawable/highlight_disabled 01080388=drawable/highlight_pressed 01080389=drawable/highlight_selected 0108038a=drawable/ic_ab_back_holo_dark 0108038b=drawable/ic_ab_back_holo_dark_am 0108038c=drawable/ic_ab_back_holo_light 0108038d=drawable/ic_ab_back_holo_light_am 0108038e=drawable/ic_ab_back_material 0108038f=drawable/ic_ab_back_material_dark 01080390=drawable/ic_ab_back_material_light 01080391=drawable/ic_ab_back_material_settings 01080392=drawable/ic_accessibility_24dp 01080393=drawable/ic_accessibility_autoclick 01080394=drawable/ic_accessibility_autoclick_foreground 01080395=drawable/ic_accessibility_color_correction 01080396=drawable/ic_accessibility_color_correction_foreground 01080397=drawable/ic_accessibility_color_inversion 01080398=drawable/ic_accessibility_color_inversion_foreground 01080399=drawable/ic_accessibility_generic 0108039a=drawable/ic_accessibility_hearing_aid 0108039b=drawable/ic_accessibility_hearing_aid_blue_dot 0108039c=drawable/ic_accessibility_hearing_aid_disconnected 0108039d=drawable/ic_accessibility_hearing_aid_disconnected_foreground 0108039e=drawable/ic_accessibility_hearing_aid_foreground 0108039f=drawable/ic_accessibility_hearing_aid_green_dot 010803a0=drawable/ic_accessibility_magnification 010803a1=drawable/ic_accessibility_magnification_foreground 010803a2=drawable/ic_accessibility_one_handed 010803a3=drawable/ic_accessibility_one_handed_foreground 010803a4=drawable/ic_accessibility_reduce_bright_colors 010803a5=drawable/ic_accessibility_reduce_bright_colors_foreground 010803a6=drawable/ic_account_circle 010803a7=drawable/ic_action_assist_focused 010803a8=drawable/ic_action_open 010803a9=drawable/ic_add_supervised_user 010803aa=drawable/ic_aggregated 010803ab=drawable/ic_alert_window_layer 010803ac=drawable/ic_android_satellite_24px 010803ad=drawable/ic_arrow_drop_right_black_24dp 010803ae=drawable/ic_arrow_forward 010803af=drawable/ic_audio_alarm 010803b0=drawable/ic_audio_alarm_mute 010803b1=drawable/ic_audio_media 010803b2=drawable/ic_audio_media_mute 010803b3=drawable/ic_audio_notification 010803b4=drawable/ic_audio_notification_am_alpha 010803b5=drawable/ic_audio_notification_mute 010803b6=drawable/ic_audio_notification_mute_am_alpha 010803b7=drawable/ic_audio_ring_notif 010803b8=drawable/ic_audio_ring_notif_mute 010803b9=drawable/ic_audio_ring_notif_vibrate 010803ba=drawable/ic_audio_vol 010803bb=drawable/ic_audio_vol_mute 010803bc=drawable/ic_battery 010803bd=drawable/ic_battery_80_24dp 010803be=drawable/ic_bluetooth_transient_animation 010803bf=drawable/ic_bluetooth_transient_animation_drawable 010803c0=drawable/ic_bt_headphones_a2dp 010803c1=drawable/ic_bt_headset_hfp 010803c2=drawable/ic_bt_hearing_aid 010803c3=drawable/ic_bt_laptop 010803c4=drawable/ic_bt_misc_hid 010803c5=drawable/ic_bt_network_pan 010803c6=drawable/ic_bt_pointing_hid 010803c7=drawable/ic_btn_round_more 010803c8=drawable/ic_btn_round_more_disabled 010803c9=drawable/ic_btn_round_more_normal 010803ca=drawable/ic_btn_search_go 010803cb=drawable/ic_btn_square_browser_zoom_fit_page 010803cc=drawable/ic_btn_square_browser_zoom_fit_page_disabled 010803cd=drawable/ic_btn_square_browser_zoom_fit_page_normal 010803ce=drawable/ic_btn_square_browser_zoom_page_overview 010803cf=drawable/ic_btn_square_browser_zoom_page_overview_disabled 010803d0=drawable/ic_btn_square_browser_zoom_page_overview_normal 010803d1=drawable/ic_bullet_key_permission 010803d2=drawable/ic_cab_done_holo 010803d3=drawable/ic_cab_done_holo_dark 010803d4=drawable/ic_cab_done_holo_light 010803d5=drawable/ic_cab_done_mtrl_alpha 010803d6=drawable/ic_call_answer 010803d7=drawable/ic_call_answer_video 010803d8=drawable/ic_call_decline 010803d9=drawable/ic_camera 010803da=drawable/ic_camera_allowed 010803db=drawable/ic_camera_blocked 010803dc=drawable/ic_check_24dp 010803dd=drawable/ic_check_circle_24px 010803de=drawable/ic_check_watch 010803df=drawable/ic_checkmark_holo_light 010803e0=drawable/ic_chevron_end 010803e1=drawable/ic_chevron_start 010803e2=drawable/ic_chooser_group_arrow 010803e3=drawable/ic_chooser_pin 010803e4=drawable/ic_chooser_pin_dialog 010803e5=drawable/ic_clear 010803e6=drawable/ic_clear_disabled 010803e7=drawable/ic_clear_holo_dark 010803e8=drawable/ic_clear_holo_light 010803e9=drawable/ic_clear_material 010803ea=drawable/ic_clear_mtrl_alpha 010803eb=drawable/ic_clear_normal 010803ec=drawable/ic_clear_search_api_disabled_holo_dark 010803ed=drawable/ic_clear_search_api_disabled_holo_light 010803ee=drawable/ic_clear_search_api_holo_dark 010803ef=drawable/ic_clear_search_api_holo_light 010803f0=drawable/ic_clone_badge 010803f1=drawable/ic_clone_icon_badge 010803f2=drawable/ic_close 010803f3=drawable/ic_close_watch 010803f4=drawable/ic_coins_l 010803f5=drawable/ic_coins_s 010803f6=drawable/ic_collapse_bundle 010803f7=drawable/ic_collapse_notification 010803f8=drawable/ic_commit 010803f9=drawable/ic_commit_search_api_holo_dark 010803fa=drawable/ic_commit_search_api_holo_light 010803fb=drawable/ic_commit_search_api_material 010803fc=drawable/ic_commit_search_api_mtrl_alpha 010803fd=drawable/ic_contact_picture 010803fe=drawable/ic_contact_picture_180_holo_dark 010803ff=drawable/ic_contact_picture_180_holo_light 01080400=drawable/ic_contact_picture_2 01080401=drawable/ic_contact_picture_3 01080402=drawable/ic_contact_picture_holo_dark 01080403=drawable/ic_contact_picture_holo_light 01080404=drawable/ic_corp_badge 01080405=drawable/ic_corp_badge_case 01080406=drawable/ic_corp_badge_color 01080407=drawable/ic_corp_badge_no_background 01080408=drawable/ic_corp_badge_off 01080409=drawable/ic_corp_icon 0108040a=drawable/ic_corp_icon_badge_case 0108040b=drawable/ic_corp_icon_badge_color 0108040c=drawable/ic_corp_icon_badge_shadow 0108040d=drawable/ic_corp_statusbar_icon 0108040e=drawable/ic_corp_user_badge 0108040f=drawable/ic_dialog_alert_holo_dark 01080410=drawable/ic_dialog_alert_holo_light 01080411=drawable/ic_dialog_alert_material 01080412=drawable/ic_dialog_close_normal_holo 01080413=drawable/ic_dialog_close_pressed_holo 01080414=drawable/ic_dialog_focused_holo 01080415=drawable/ic_dialog_time 01080416=drawable/ic_dialog_usb 01080417=drawable/ic_dnd_block_notifications 01080418=drawable/ic_doc_apk 01080419=drawable/ic_doc_audio 0108041a=drawable/ic_doc_certificate 0108041b=drawable/ic_doc_codes 0108041c=drawable/ic_doc_compressed 0108041d=drawable/ic_doc_contact 0108041e=drawable/ic_doc_document 0108041f=drawable/ic_doc_event 01080420=drawable/ic_doc_excel 01080421=drawable/ic_doc_folder 01080422=drawable/ic_doc_font 01080423=drawable/ic_doc_generic 01080424=drawable/ic_doc_image 01080425=drawable/ic_doc_pdf 01080426=drawable/ic_doc_powerpoint 01080427=drawable/ic_doc_presentation 01080428=drawable/ic_doc_spreadsheet 01080429=drawable/ic_doc_text 0108042a=drawable/ic_doc_video 0108042b=drawable/ic_doc_word 0108042c=drawable/ic_drag_handle 0108042d=drawable/ic_dual_screen 0108042e=drawable/ic_eject_24dp 0108042f=drawable/ic_emergency 01080430=drawable/ic_expand_bundle 01080431=drawable/ic_expand_more 01080432=drawable/ic_expand_more_48dp 01080433=drawable/ic_expand_notification 01080434=drawable/ic_faster_emergency 01080435=drawable/ic_feedback 01080436=drawable/ic_feedback_alerted 01080437=drawable/ic_feedback_downrank 01080438=drawable/ic_feedback_indicator 01080439=drawable/ic_feedback_silenced 0108043a=drawable/ic_feedback_uprank 0108043b=drawable/ic_file_copy 0108043c=drawable/ic_find_next_holo_dark 0108043d=drawable/ic_find_next_holo_light 0108043e=drawable/ic_find_next_material 0108043f=drawable/ic_find_next_mtrl_alpha 01080440=drawable/ic_find_previous_holo_dark 01080441=drawable/ic_find_previous_holo_light 01080442=drawable/ic_find_previous_material 01080443=drawable/ic_find_previous_mtrl_alpha 01080444=drawable/ic_fingerprint 01080445=drawable/ic_folder_24dp 01080446=drawable/ic_go 01080447=drawable/ic_go_search_api_holo_dark 01080448=drawable/ic_go_search_api_holo_light 01080449=drawable/ic_go_search_api_material 0108044a=drawable/ic_hotspot_transient_animation 0108044b=drawable/ic_hotspot_transient_animation_drawable 0108044c=drawable/ic_ime_nav_back 0108044d=drawable/ic_ime_switcher 0108044e=drawable/ic_ime_switcher_new 0108044f=drawable/ic_info_outline 01080450=drawable/ic_info_outline_24 01080451=drawable/ic_input_extract_action_done 01080452=drawable/ic_input_extract_action_go 01080453=drawable/ic_input_extract_action_next 01080454=drawable/ic_input_extract_action_previous 01080455=drawable/ic_input_extract_action_return 01080456=drawable/ic_input_extract_action_search 01080457=drawable/ic_input_extract_action_send 01080458=drawable/ic_instant_icon_badge_bolt 01080459=drawable/ic_jog_dial_answer 0108045a=drawable/ic_jog_dial_answer_and_end 0108045b=drawable/ic_jog_dial_answer_and_hold 0108045c=drawable/ic_jog_dial_decline 0108045d=drawable/ic_jog_dial_sound_off 0108045e=drawable/ic_jog_dial_sound_on 0108045f=drawable/ic_jog_dial_unlock 01080460=drawable/ic_jog_dial_vibrate_on 01080461=drawable/ic_launcher_android 01080462=drawable/ic_lock 01080463=drawable/ic_lock_airplane_mode 01080464=drawable/ic_lock_airplane_mode_alpha 01080465=drawable/ic_lock_airplane_mode_off 01080466=drawable/ic_lock_airplane_mode_off_am_alpha 01080467=drawable/ic_lock_bugreport 01080468=drawable/ic_lock_idle_alarm_alpha 01080469=drawable/ic_lock_lock_alpha 0108046a=drawable/ic_lock_lockdown 0108046b=drawable/ic_lock_open 0108046c=drawable/ic_lock_open_wht_24dp 0108046d=drawable/ic_lock_outline_wht_24dp 0108046e=drawable/ic_lock_power_off_alpha 0108046f=drawable/ic_lock_ringer_off_alpha 01080470=drawable/ic_lock_ringer_on_alpha 01080471=drawable/ic_lock_silent_mode_vibrate 01080472=drawable/ic_lockscreen_alarm 01080473=drawable/ic_lockscreen_answer_active 01080474=drawable/ic_lockscreen_answer_focused 01080475=drawable/ic_lockscreen_answer_normal 01080476=drawable/ic_lockscreen_camera_activated 01080477=drawable/ic_lockscreen_camera_normal 01080478=drawable/ic_lockscreen_chevron_down 01080479=drawable/ic_lockscreen_chevron_left 0108047a=drawable/ic_lockscreen_chevron_right 0108047b=drawable/ic_lockscreen_chevron_up 0108047c=drawable/ic_lockscreen_decline_activated 0108047d=drawable/ic_lockscreen_decline_focused 0108047e=drawable/ic_lockscreen_decline_normal 0108047f=drawable/ic_lockscreen_emergencycall_normal 01080480=drawable/ic_lockscreen_emergencycall_pressed 01080481=drawable/ic_lockscreen_forgotpassword_normal 01080482=drawable/ic_lockscreen_forgotpassword_pressed 01080483=drawable/ic_lockscreen_google_activated 01080484=drawable/ic_lockscreen_google_focused 01080485=drawable/ic_lockscreen_google_normal 01080486=drawable/ic_lockscreen_handle_normal 01080487=drawable/ic_lockscreen_handle_pressed 01080488=drawable/ic_lockscreen_ime 01080489=drawable/ic_lockscreen_outerring 0108048a=drawable/ic_lockscreen_player_background 0108048b=drawable/ic_lockscreen_puk 0108048c=drawable/ic_lockscreen_silent_activated 0108048d=drawable/ic_lockscreen_silent_focused 0108048e=drawable/ic_lockscreen_silent_normal 0108048f=drawable/ic_lockscreen_sim 01080490=drawable/ic_lockscreen_soundon_activated 01080491=drawable/ic_lockscreen_soundon_focused 01080492=drawable/ic_lockscreen_soundon_normal 01080493=drawable/ic_lockscreen_text_activated 01080494=drawable/ic_lockscreen_text_focusde 01080495=drawable/ic_lockscreen_text_normal 01080496=drawable/ic_lockscreen_unlock_activated 01080497=drawable/ic_lockscreen_unlock_normal 01080498=drawable/ic_lockscreens_now_button 01080499=drawable/ic_logout 0108049a=drawable/ic_maps_indicator_current_position 0108049b=drawable/ic_maps_indicator_current_position_anim 0108049c=drawable/ic_maps_indicator_current_position_anim1 0108049d=drawable/ic_maps_indicator_current_position_anim2 0108049e=drawable/ic_maps_indicator_current_position_anim3 0108049f=drawable/ic_media_embed_play 010804a0=drawable/ic_media_fullscreen 010804a1=drawable/ic_media_route_connected_dark_00_mtrl 010804a2=drawable/ic_media_route_connected_dark_01_mtrl 010804a3=drawable/ic_media_route_connected_dark_02_mtrl 010804a4=drawable/ic_media_route_connected_dark_03_mtrl 010804a5=drawable/ic_media_route_connected_dark_04_mtrl 010804a6=drawable/ic_media_route_connected_dark_05_mtrl 010804a7=drawable/ic_media_route_connected_dark_06_mtrl 010804a8=drawable/ic_media_route_connected_dark_07_mtrl 010804a9=drawable/ic_media_route_connected_dark_08_mtrl 010804aa=drawable/ic_media_route_connected_dark_09_mtrl 010804ab=drawable/ic_media_route_connected_dark_10_mtrl 010804ac=drawable/ic_media_route_connected_dark_11_mtrl 010804ad=drawable/ic_media_route_connected_dark_12_mtrl 010804ae=drawable/ic_media_route_connected_dark_13_mtrl 010804af=drawable/ic_media_route_connected_dark_14_mtrl 010804b0=drawable/ic_media_route_connected_dark_15_mtrl 010804b1=drawable/ic_media_route_connected_dark_16_mtrl 010804b2=drawable/ic_media_route_connected_dark_17_mtrl 010804b3=drawable/ic_media_route_connected_dark_18_mtrl 010804b4=drawable/ic_media_route_connected_dark_19_mtrl 010804b5=drawable/ic_media_route_connected_dark_20_mtrl 010804b6=drawable/ic_media_route_connected_dark_21_mtrl 010804b7=drawable/ic_media_route_connected_dark_22_mtrl 010804b8=drawable/ic_media_route_connected_dark_23_mtrl 010804b9=drawable/ic_media_route_connected_dark_24_mtrl 010804ba=drawable/ic_media_route_connected_dark_25_mtrl 010804bb=drawable/ic_media_route_connected_dark_26_mtrl 010804bc=drawable/ic_media_route_connected_dark_27_mtrl 010804bd=drawable/ic_media_route_connected_dark_28_mtrl 010804be=drawable/ic_media_route_connected_dark_29_mtrl 010804bf=drawable/ic_media_route_connected_dark_30_mtrl 010804c0=drawable/ic_media_route_connected_dark_material 010804c1=drawable/ic_media_route_connected_light_00_mtrl 010804c2=drawable/ic_media_route_connected_light_01_mtrl 010804c3=drawable/ic_media_route_connected_light_02_mtrl 010804c4=drawable/ic_media_route_connected_light_03_mtrl 010804c5=drawable/ic_media_route_connected_light_04_mtrl 010804c6=drawable/ic_media_route_connected_light_05_mtrl 010804c7=drawable/ic_media_route_connected_light_06_mtrl 010804c8=drawable/ic_media_route_connected_light_07_mtrl 010804c9=drawable/ic_media_route_connected_light_08_mtrl 010804ca=drawable/ic_media_route_connected_light_09_mtrl 010804cb=drawable/ic_media_route_connected_light_10_mtrl 010804cc=drawable/ic_media_route_connected_light_11_mtrl 010804cd=drawable/ic_media_route_connected_light_12_mtrl 010804ce=drawable/ic_media_route_connected_light_13_mtrl 010804cf=drawable/ic_media_route_connected_light_14_mtrl 010804d0=drawable/ic_media_route_connected_light_15_mtrl 010804d1=drawable/ic_media_route_connected_light_16_mtrl 010804d2=drawable/ic_media_route_connected_light_17_mtrl 010804d3=drawable/ic_media_route_connected_light_18_mtrl 010804d4=drawable/ic_media_route_connected_light_19_mtrl 010804d5=drawable/ic_media_route_connected_light_20_mtrl 010804d6=drawable/ic_media_route_connected_light_21_mtrl 010804d7=drawable/ic_media_route_connected_light_22_mtrl 010804d8=drawable/ic_media_route_connected_light_23_mtrl 010804d9=drawable/ic_media_route_connected_light_24_mtrl 010804da=drawable/ic_media_route_connected_light_25_mtrl 010804db=drawable/ic_media_route_connected_light_26_mtrl 010804dc=drawable/ic_media_route_connected_light_27_mtrl 010804dd=drawable/ic_media_route_connected_light_28_mtrl 010804de=drawable/ic_media_route_connected_light_29_mtrl 010804df=drawable/ic_media_route_connected_light_30_mtrl 010804e0=drawable/ic_media_route_connected_light_material 010804e1=drawable/ic_media_route_connecting_dark_00_mtrl 010804e2=drawable/ic_media_route_connecting_dark_01_mtrl 010804e3=drawable/ic_media_route_connecting_dark_02_mtrl 010804e4=drawable/ic_media_route_connecting_dark_03_mtrl 010804e5=drawable/ic_media_route_connecting_dark_04_mtrl 010804e6=drawable/ic_media_route_connecting_dark_05_mtrl 010804e7=drawable/ic_media_route_connecting_dark_06_mtrl 010804e8=drawable/ic_media_route_connecting_dark_07_mtrl 010804e9=drawable/ic_media_route_connecting_dark_08_mtrl 010804ea=drawable/ic_media_route_connecting_dark_09_mtrl 010804eb=drawable/ic_media_route_connecting_dark_10_mtrl 010804ec=drawable/ic_media_route_connecting_dark_11_mtrl 010804ed=drawable/ic_media_route_connecting_dark_12_mtrl 010804ee=drawable/ic_media_route_connecting_dark_13_mtrl 010804ef=drawable/ic_media_route_connecting_dark_14_mtrl 010804f0=drawable/ic_media_route_connecting_dark_15_mtrl 010804f1=drawable/ic_media_route_connecting_dark_16_mtrl 010804f2=drawable/ic_media_route_connecting_dark_17_mtrl 010804f3=drawable/ic_media_route_connecting_dark_18_mtrl 010804f4=drawable/ic_media_route_connecting_dark_19_mtrl 010804f5=drawable/ic_media_route_connecting_dark_20_mtrl 010804f6=drawable/ic_media_route_connecting_dark_21_mtrl 010804f7=drawable/ic_media_route_connecting_dark_22_mtrl 010804f8=drawable/ic_media_route_connecting_dark_23_mtrl 010804f9=drawable/ic_media_route_connecting_dark_24_mtrl 010804fa=drawable/ic_media_route_connecting_dark_25_mtrl 010804fb=drawable/ic_media_route_connecting_dark_26_mtrl 010804fc=drawable/ic_media_route_connecting_dark_27_mtrl 010804fd=drawable/ic_media_route_connecting_dark_28_mtrl 010804fe=drawable/ic_media_route_connecting_dark_29_mtrl 010804ff=drawable/ic_media_route_connecting_dark_30_mtrl 01080500=drawable/ic_media_route_connecting_dark_material 01080501=drawable/ic_media_route_connecting_holo_dark 01080502=drawable/ic_media_route_connecting_holo_light 01080503=drawable/ic_media_route_connecting_light_00_mtrl 01080504=drawable/ic_media_route_connecting_light_01_mtrl 01080505=drawable/ic_media_route_connecting_light_02_mtrl 01080506=drawable/ic_media_route_connecting_light_03_mtrl 01080507=drawable/ic_media_route_connecting_light_04_mtrl 01080508=drawable/ic_media_route_connecting_light_05_mtrl 01080509=drawable/ic_media_route_connecting_light_06_mtrl 0108050a=drawable/ic_media_route_connecting_light_07_mtrl 0108050b=drawable/ic_media_route_connecting_light_08_mtrl 0108050c=drawable/ic_media_route_connecting_light_09_mtrl 0108050d=drawable/ic_media_route_connecting_light_10_mtrl 0108050e=drawable/ic_media_route_connecting_light_11_mtrl 0108050f=drawable/ic_media_route_connecting_light_12_mtrl 01080510=drawable/ic_media_route_connecting_light_13_mtrl 01080511=drawable/ic_media_route_connecting_light_14_mtrl 01080512=drawable/ic_media_route_connecting_light_15_mtrl 01080513=drawable/ic_media_route_connecting_light_16_mtrl 01080514=drawable/ic_media_route_connecting_light_17_mtrl 01080515=drawable/ic_media_route_connecting_light_18_mtrl 01080516=drawable/ic_media_route_connecting_light_19_mtrl 01080517=drawable/ic_media_route_connecting_light_20_mtrl 01080518=drawable/ic_media_route_connecting_light_21_mtrl 01080519=drawable/ic_media_route_connecting_light_22_mtrl 0108051a=drawable/ic_media_route_connecting_light_23_mtrl 0108051b=drawable/ic_media_route_connecting_light_24_mtrl 0108051c=drawable/ic_media_route_connecting_light_25_mtrl 0108051d=drawable/ic_media_route_connecting_light_26_mtrl 0108051e=drawable/ic_media_route_connecting_light_27_mtrl 0108051f=drawable/ic_media_route_connecting_light_28_mtrl 01080520=drawable/ic_media_route_connecting_light_29_mtrl 01080521=drawable/ic_media_route_connecting_light_30_mtrl 01080522=drawable/ic_media_route_connecting_light_material 01080523=drawable/ic_media_route_dark_material 01080524=drawable/ic_media_route_disabled_holo_dark 01080525=drawable/ic_media_route_disabled_holo_light 01080526=drawable/ic_media_route_disabled_mtrl_alpha 01080527=drawable/ic_media_route_holo_dark 01080528=drawable/ic_media_route_holo_light 01080529=drawable/ic_media_route_light_material 0108052a=drawable/ic_media_route_off_dark_mtrl 0108052b=drawable/ic_media_route_off_holo_dark 0108052c=drawable/ic_media_route_off_holo_light 0108052d=drawable/ic_media_route_off_light_mtrl 0108052e=drawable/ic_media_route_on_0_holo_dark 0108052f=drawable/ic_media_route_on_0_holo_light 01080530=drawable/ic_media_route_on_1_holo_dark 01080531=drawable/ic_media_route_on_1_holo_light 01080532=drawable/ic_media_route_on_2_holo_dark 01080533=drawable/ic_media_route_on_2_holo_light 01080534=drawable/ic_media_route_on_holo_dark 01080535=drawable/ic_media_route_on_holo_light 01080536=drawable/ic_media_seamless 01080537=drawable/ic_media_stop 01080538=drawable/ic_media_video_poster 01080539=drawable/ic_menu 0108053a=drawable/ic_menu_account_list 0108053b=drawable/ic_menu_allfriends 0108053c=drawable/ic_menu_archive 0108053d=drawable/ic_menu_attachment 0108053e=drawable/ic_menu_back 0108053f=drawable/ic_menu_block 01080540=drawable/ic_menu_blocked_user 01080541=drawable/ic_menu_btn_add 01080542=drawable/ic_menu_cc 01080543=drawable/ic_menu_cc_am 01080544=drawable/ic_menu_chat_dashboard 01080545=drawable/ic_menu_clear_playlist 01080546=drawable/ic_menu_compose 01080547=drawable/ic_menu_copy 01080548=drawable/ic_menu_copy_holo_dark 01080549=drawable/ic_menu_copy_holo_light 0108054a=drawable/ic_menu_copy_material 0108054b=drawable/ic_menu_cut 0108054c=drawable/ic_menu_cut_holo_dark 0108054d=drawable/ic_menu_cut_holo_light 0108054e=drawable/ic_menu_cut_material 0108054f=drawable/ic_menu_emoticons 01080550=drawable/ic_menu_end_conversation 01080551=drawable/ic_menu_find 01080552=drawable/ic_menu_find_holo_dark 01080553=drawable/ic_menu_find_holo_light 01080554=drawable/ic_menu_find_material 01080555=drawable/ic_menu_find_mtrl_alpha 01080556=drawable/ic_menu_forward 01080557=drawable/ic_menu_friendslist 01080558=drawable/ic_menu_goto 01080559=drawable/ic_menu_help_holo_light 0108055a=drawable/ic_menu_home 0108055b=drawable/ic_menu_invite 0108055c=drawable/ic_menu_login 0108055d=drawable/ic_menu_mark 0108055e=drawable/ic_menu_moreoverflow 0108055f=drawable/ic_menu_moreoverflow_focused_holo_dark 01080560=drawable/ic_menu_moreoverflow_focused_holo_light 01080561=drawable/ic_menu_moreoverflow_holo_dark 01080562=drawable/ic_menu_moreoverflow_holo_light 01080563=drawable/ic_menu_moreoverflow_material 01080564=drawable/ic_menu_moreoverflow_material_dark 01080565=drawable/ic_menu_moreoverflow_material_light 01080566=drawable/ic_menu_moreoverflow_normal_holo_dark 01080567=drawable/ic_menu_moreoverflow_normal_holo_light 01080568=drawable/ic_menu_notifications 01080569=drawable/ic_menu_paste 0108056a=drawable/ic_menu_paste_holo_dark 0108056b=drawable/ic_menu_paste_holo_light 0108056c=drawable/ic_menu_paste_material 0108056d=drawable/ic_menu_play_clip 0108056e=drawable/ic_menu_redo_material 0108056f=drawable/ic_menu_refresh 01080570=drawable/ic_menu_search_holo_dark 01080571=drawable/ic_menu_search_holo_light 01080572=drawable/ic_menu_search_material 01080573=drawable/ic_menu_search_mtrl_alpha 01080574=drawable/ic_menu_selectall_holo_dark 01080575=drawable/ic_menu_selectall_holo_light 01080576=drawable/ic_menu_selectall_material 01080577=drawable/ic_menu_settings_holo_light 01080578=drawable/ic_menu_share_holo_dark 01080579=drawable/ic_menu_share_holo_light 0108057a=drawable/ic_menu_share_material 0108057b=drawable/ic_menu_star 0108057c=drawable/ic_menu_start_conversation 0108057d=drawable/ic_menu_stop 0108057e=drawable/ic_menu_undo_material 0108057f=drawable/ic_mic 01080580=drawable/ic_mic_allowed 01080581=drawable/ic_mic_blocked 01080582=drawable/ic_minus 01080583=drawable/ic_mode_edit 01080584=drawable/ic_more_items 01080585=drawable/ic_notification_2025_collapse 01080586=drawable/ic_notification_2025_expand 01080587=drawable/ic_notification_alert 01080588=drawable/ic_notification_block 01080589=drawable/ic_notification_cast_0 0108058a=drawable/ic_notification_cast_1 0108058b=drawable/ic_notification_cast_2 0108058c=drawable/ic_notification_media_route 0108058d=drawable/ic_notification_summarization 0108058e=drawable/ic_notification_summary_auto 0108058f=drawable/ic_notifications_alerted 01080590=drawable/ic_number11 01080591=drawable/ic_pan_tool 01080592=drawable/ic_perm_device_info 01080593=drawable/ic_perm_group_app_info 01080594=drawable/ic_perm_group_audio_settings 01080595=drawable/ic_perm_group_bluetooth 01080596=drawable/ic_perm_group_bookmarks 01080597=drawable/ic_perm_group_calendar 01080598=drawable/ic_perm_group_camera 01080599=drawable/ic_perm_group_device_alarms 0108059a=drawable/ic_perm_group_display 0108059b=drawable/ic_perm_group_effects_battery 0108059c=drawable/ic_perm_group_location 0108059d=drawable/ic_perm_group_messages 0108059e=drawable/ic_perm_group_microphone 0108059f=drawable/ic_perm_group_network 010805a0=drawable/ic_perm_group_personal_info 010805a1=drawable/ic_perm_group_phone_calls 010805a2=drawable/ic_perm_group_screenlock 010805a3=drawable/ic_perm_group_shortrange_network 010805a4=drawable/ic_perm_group_social_info 010805a5=drawable/ic_perm_group_status_bar 010805a6=drawable/ic_perm_group_sync_settings 010805a7=drawable/ic_perm_group_system_clock 010805a8=drawable/ic_perm_group_system_tools 010805a9=drawable/ic_perm_group_voicemail 010805aa=drawable/ic_perm_group_wallpapewr 010805ab=drawable/ic_permission 010805ac=drawable/ic_phone 010805ad=drawable/ic_phone_disabled 010805ae=drawable/ic_plus 010805af=drawable/ic_popup_sync_1 010805b0=drawable/ic_popup_sync_2 010805b1=drawable/ic_popup_sync_3 010805b2=drawable/ic_popup_sync_4 010805b3=drawable/ic_popup_sync_5 010805b4=drawable/ic_popup_sync_6 010805b5=drawable/ic_print 010805b6=drawable/ic_print_error 010805b7=drawable/ic_private_profile_badge 010805b8=drawable/ic_private_profile_icon_badge 010805b9=drawable/ic_qs_airplane 010805ba=drawable/ic_qs_auto_rotate 010805bb=drawable/ic_qs_battery_saver 010805bc=drawable/ic_qs_bluetooth 010805bd=drawable/ic_qs_dnd 010805be=drawable/ic_qs_flashlight 010805bf=drawable/ic_qs_night_display_on 010805c0=drawable/ic_qs_one_handed_mode 010805c1=drawable/ic_qs_ui_mode_night 010805c2=drawable/ic_refresh 010805c3=drawable/ic_restart 010805c4=drawable/ic_satellite_alt_24px 010805c5=drawable/ic_schedule 010805c6=drawable/ic_screenshot 010805c7=drawable/ic_screenshot_edit 010805c8=drawable/ic_sd_card_48dp 010805c9=drawable/ic_search 010805ca=drawable/ic_search_api_holo_dark 010805cb=drawable/ic_search_api_holo_light 010805cc=drawable/ic_search_api_material 010805cd=drawable/ic_settings 010805ce=drawable/ic_settings_24dp 010805cf=drawable/ic_settings_bluetooth 010805d0=drawable/ic_settings_language 010805d1=drawable/ic_settings_print 010805d2=drawable/ic_signal_cellular 010805d3=drawable/ic_signal_cellular_0_4_bar 010805d4=drawable/ic_signal_cellular_0_5_bar 010805d5=drawable/ic_signal_cellular_1_4_bar 010805d6=drawable/ic_signal_cellular_1_5_bar 010805d7=drawable/ic_signal_cellular_2_4_bar 010805d8=drawable/ic_signal_cellular_2_5_bar 010805d9=drawable/ic_signal_cellular_3_4_bar 010805da=drawable/ic_signal_cellular_3_5_bar 010805db=drawable/ic_signal_cellular_4_4_bar 010805dc=drawable/ic_signal_cellular_4_5_bar 010805dd=drawable/ic_signal_cellular_5_5_bar 010805de=drawable/ic_signal_cellular_alt_24px 010805df=drawable/ic_signal_location 010805e0=drawable/ic_signal_wifi_transient_animation 010805e1=drawable/ic_signal_wifi_transient_animation_drawable 010805e2=drawable/ic_sim_card_multi_24px_clr 010805e3=drawable/ic_sim_card_multi_48px_clr 010805e4=drawable/ic_slice_send 010805e5=drawable/ic_spinner_caret 010805e6=drawable/ic_standby 010805e7=drawable/ic_star_black_16dp 010805e8=drawable/ic_star_black_36dp 010805e9=drawable/ic_star_black_48dp 010805ea=drawable/ic_star_half_black_16dp 010805eb=drawable/ic_star_half_black_36dp 010805ec=drawable/ic_star_half_black_48dp 010805ed=drawable/ic_storage_48dp 010805ee=drawable/ic_suggestions_add 010805ef=drawable/ic_suggestions_delete 010805f0=drawable/ic_swap_horiz 010805f1=drawable/ic_swipe_down 010805f2=drawable/ic_sysbar_quicksettings 010805f3=drawable/ic_test_badge_experiment 010805f4=drawable/ic_test_badge_no_background 010805f5=drawable/ic_test_icon_badge_experiment 010805f6=drawable/ic_text_dot 010805f7=drawable/ic_thermostat 010805f8=drawable/ic_thermostat_notification 010805f9=drawable/ic_thread_network 010805fa=drawable/ic_usb_48dp 010805fb=drawable/ic_user_secure 010805fc=drawable/ic_vibrate 010805fd=drawable/ic_vibrate_small 010805fe=drawable/ic_visibility 010805ff=drawable/ic_voice_search 01080600=drawable/ic_voice_search_api_holo_dark 01080601=drawable/ic_voice_search_api_holo_light 01080602=drawable/ic_voice_search_api_material 01080603=drawable/ic_volume 01080604=drawable/ic_volume_bluetooth_ad2p 01080605=drawable/ic_volume_bluetooth_in_call 01080606=drawable/ic_volume_off 01080607=drawable/ic_volume_off_small 01080608=drawable/ic_volume_small 01080609=drawable/ic_wifi_signal_0 0108060a=drawable/ic_wifi_signal_1 0108060b=drawable/ic_wifi_signal_2 0108060c=drawable/ic_wifi_signal_3 0108060d=drawable/ic_wifi_signal_4 0108060e=drawable/ic_zen_24dp 0108060f=drawable/ic_zen_mode_icon_animal_paw 01080610=drawable/ic_zen_mode_icon_apartment_building 01080611=drawable/ic_zen_mode_icon_ball_sports 01080612=drawable/ic_zen_mode_icon_beach 01080613=drawable/ic_zen_mode_icon_book 01080614=drawable/ic_zen_mode_icon_camping 01080615=drawable/ic_zen_mode_icon_child 01080616=drawable/ic_zen_mode_icon_classical_building 01080617=drawable/ic_zen_mode_icon_croissant 01080618=drawable/ic_zen_mode_icon_fork_and_knife 01080619=drawable/ic_zen_mode_icon_gaming 0108061a=drawable/ic_zen_mode_icon_golf 0108061b=drawable/ic_zen_mode_icon_group_of_people 0108061c=drawable/ic_zen_mode_icon_gym 0108061d=drawable/ic_zen_mode_icon_headphones 0108061e=drawable/ic_zen_mode_icon_heart 0108061f=drawable/ic_zen_mode_icon_hiking 01080620=drawable/ic_zen_mode_icon_house 01080621=drawable/ic_zen_mode_icon_lightbulb 01080622=drawable/ic_zen_mode_icon_lotus_flower 01080623=drawable/ic_zen_mode_icon_martial_arts 01080624=drawable/ic_zen_mode_icon_palette 01080625=drawable/ic_zen_mode_icon_piano 01080626=drawable/ic_zen_mode_icon_running 01080627=drawable/ic_zen_mode_icon_shopping_cart 01080628=drawable/ic_zen_mode_icon_snowflake 01080629=drawable/ic_zen_mode_icon_speech_bubble 0108062a=drawable/ic_zen_mode_icon_star_badge 0108062b=drawable/ic_zen_mode_icon_swimming 0108062c=drawable/ic_zen_mode_icon_train 0108062d=drawable/ic_zen_mode_icon_tv 0108062e=drawable/ic_zen_mode_icon_work 0108062f=drawable/ic_zen_mode_icon_workshop 01080630=drawable/ic_zen_mode_type_bedtime 01080631=drawable/ic_zen_mode_type_driving 01080632=drawable/ic_zen_mode_type_immersive 01080633=drawable/ic_zen_mode_type_managed 01080634=drawable/ic_zen_mode_type_other 01080635=drawable/ic_zen_mode_type_schedule_calendar 01080636=drawable/ic_zen_mode_type_schedule_time 01080637=drawable/ic_zen_mode_type_special_dnd 01080638=drawable/ic_zen_mode_type_theater 01080639=drawable/ic_zen_priority_modes 0108063a=drawable/icon_highlight_rectangle 0108063b=drawable/icon_highlight_square 0108063c=drawable/iconfactory_adaptive_icon_drawable_wrapper 0108063d=drawable/ime_qwerty 0108063e=drawable/immersive_cling_bg 0108063f=drawable/immersive_cling_btn_bg 01080640=drawable/indicator_check_mark_dark 01080641=drawable/indicator_check_mark_light 01080642=drawable/indicator_input_error 01080643=drawable/input_extract_action_bg_material_dark 01080644=drawable/input_extract_action_bg_normal_material_dark 01080645=drawable/input_extract_action_bg_pressed_material_dark 01080646=drawable/input_method_fullscreen_background 01080647=drawable/input_method_fullscreen_background_holo 01080648=drawable/input_method_item_background 01080649=drawable/input_method_item_background_selected 0108064a=drawable/input_method_item_background_selector 0108064b=drawable/input_method_switch_button 0108064c=drawable/input_method_switch_item_background 0108064d=drawable/inset_resolver_profile_tab_bg 0108064e=drawable/item_background 0108064f=drawable/item_background_activated_holo_dark 01080650=drawable/item_background_borderless_material 01080651=drawable/item_background_borderless_material_dark 01080652=drawable/item_background_borderless_material_light 01080653=drawable/item_background_holo_dark 01080654=drawable/item_background_holo_light 01080655=drawable/item_background_material 01080656=drawable/item_background_material_dark 01080657=drawable/item_background_material_light 01080658=drawable/jog_dial_arrow_long_left_green 01080659=drawable/jog_dial_arrow_long_left_yellow 0108065a=drawable/jog_dial_arrow_long_middle_yellow 0108065b=drawable/jog_dial_arrow_long_right_red 0108065c=drawable/jog_dial_arrow_long_right_yellow 0108065d=drawable/jog_dial_arrow_short_left 0108065e=drawable/jog_dial_arrow_short_left_and_right 0108065f=drawable/jog_dial_arrow_short_right 01080660=drawable/jog_dial_bg 01080661=drawable/jog_dial_dimple 01080662=drawable/jog_dial_dimple_dim 01080663=drawable/jog_tab_bar_left_answer 01080664=drawable/jog_tab_bar_left_end_confirm_gray 01080665=drawable/jog_tab_bar_left_end_confirm_green 01080666=drawable/jog_tab_bar_left_end_confirm_red 01080667=drawable/jog_tab_bar_left_end_confirm_yellow 01080668=drawable/jog_tab_bar_left_end_normal 01080669=drawable/jog_tab_bar_left_end_pressed 0108066a=drawable/jog_tab_bar_left_generic 0108066b=drawable/jog_tab_bar_left_unlock 0108066c=drawable/jog_tab_bar_right_decline 0108066d=drawable/jog_tab_bar_right_end_confirm_gray 0108066e=drawable/jog_tab_bar_right_end_confirm_green 0108066f=drawable/jog_tab_bar_right_end_confirm_red 01080670=drawable/jog_tab_bar_right_end_confirm_yellow 01080671=drawable/jog_tab_bar_right_end_normal 01080672=drawable/jog_tab_bar_right_end_pressed 01080673=drawable/jog_tab_bar_right_generic 01080674=drawable/jog_tab_bar_right_sound_off 01080675=drawable/jog_tab_bar_right_sound_on 01080676=drawable/jog_tab_left_answer 01080677=drawable/jog_tab_left_confirm_gray 01080678=drawable/jog_tab_left_confirm_green 01080679=drawable/jog_tab_left_confirm_red 0108067a=drawable/jog_tab_left_confirm_yellow 0108067b=drawable/jog_tab_left_generic 0108067c=drawable/jog_tab_left_normal 0108067d=drawable/jog_tab_left_pressed 0108067e=drawable/jog_tab_left_unlock 0108067f=drawable/jog_tab_right_confirm_gray 01080680=drawable/jog_tab_right_confirm_green 01080681=drawable/jog_tab_right_confirm_red 01080682=drawable/jog_tab_right_confirm_yellow 01080683=drawable/jog_tab_right_decline 01080684=drawable/jog_tab_right_generic 01080685=drawable/jog_tab_right_normal 01080686=drawable/jog_tab_right_pressed 01080687=drawable/jog_tab_right_sound_off 01080688=drawable/jog_tab_right_sound_on 01080689=drawable/jog_tab_target_gray 0108068a=drawable/jog_tab_target_green 0108068b=drawable/jog_tab_target_red 0108068c=drawable/jog_tab_target_yellow 0108068d=drawable/keyboard_accessory_bg_landscape 0108068e=drawable/keyboard_background 0108068f=drawable/keyboard_key_feedback 01080690=drawable/keyboard_key_feedback_background 01080691=drawable/keyboard_key_feedback_more_background 01080692=drawable/keyboard_popup_panel_background 01080693=drawable/keyboard_popup_panel_trans_background 01080694=drawable/language_picker_item_bg_selected 01080695=drawable/language_picker_item_text_color2_selector 01080696=drawable/language_picker_item_text_color_selector 01080697=drawable/light_header 01080698=drawable/light_header_dither 01080699=drawable/list_activated_holo 0108069a=drawable/list_choice_background_material 0108069b=drawable/list_divider_holo_dark 0108069c=drawable/list_divider_holo_light 0108069d=drawable/list_divider_horizontal_holo_dark 0108069e=drawable/list_divider_material 0108069f=drawable/list_focused_holo 010806a0=drawable/list_highlight 010806a1=drawable/list_highlight_active 010806a2=drawable/list_highlight_inactive 010806a3=drawable/list_longpressed_holo 010806a4=drawable/list_longpressed_holo_dark 010806a5=drawable/list_longpressed_holo_light 010806a6=drawable/list_pressed_holo_dark 010806a7=drawable/list_pressed_holo_light 010806a8=drawable/list_section_divider_holo_dark 010806a9=drawable/list_section_divider_holo_light 010806aa=drawable/list_section_divider_material 010806ab=drawable/list_section_divider_mtrl_alpha 010806ac=drawable/list_section_header_holo_dark 010806ad=drawable/list_section_header_holo_light 010806ae=drawable/list_selected_background 010806af=drawable/list_selected_background_light 010806b0=drawable/list_selected_holo_dark 010806b1=drawable/list_selected_holo_light 010806b2=drawable/list_selector_activated_holo_dark 010806b3=drawable/list_selector_activated_holo_light 010806b4=drawable/list_selector_background_default 010806b5=drawable/list_selector_background_default_light 010806b6=drawable/list_selector_background_disabled 010806b7=drawable/list_selector_background_disabled_light 010806b8=drawable/list_selector_background_focus 010806b9=drawable/list_selector_background_focused 010806ba=drawable/list_selector_background_focused_light 010806bb=drawable/list_selector_background_focused_selected 010806bc=drawable/list_selector_background_light 010806bd=drawable/list_selector_background_longpress 010806be=drawable/list_selector_background_longpress_light 010806bf=drawable/list_selector_background_pressed 010806c0=drawable/list_selector_background_pressed_light 010806c1=drawable/list_selector_background_selected 010806c2=drawable/list_selector_background_selected_light 010806c3=drawable/list_selector_background_transition 010806c4=drawable/list_selector_background_transition_holo_dark 010806c5=drawable/list_selector_background_transition_holo_light 010806c6=drawable/list_selector_background_transition_light 010806c7=drawable/list_selector_disabled_holo_dark 010806c8=drawable/list_selector_disabled_holo_light 010806c9=drawable/list_selector_focused_holo_dark 010806ca=drawable/list_selector_focused_holo_light 010806cb=drawable/list_selector_holo_dark 010806cc=drawable/list_selector_holo_light 010806cd=drawable/list_selector_multiselect_holo_dark 010806ce=drawable/list_selector_multiselect_holo_light 010806cf=drawable/list_selector_pressed_holo_dark 010806d0=drawable/list_selector_pressed_holo_light 010806d1=drawable/load_average_background 010806d2=drawable/loader_horizontal_watch 010806d3=drawable/loading_spinner 010806d4=drawable/loading_tile 010806d5=drawable/loading_tile_android 010806d6=drawable/lockscreen_notselected 010806d7=drawable/lockscreen_protection_pattern 010806d8=drawable/lockscreen_selected 010806d9=drawable/magnified_region_frame 010806da=drawable/maps_google_logo 010806db=drawable/media_button_background 010806dc=drawable/media_seamless_background 010806dd=drawable/menu_background 010806de=drawable/menu_background_fill_parent_width 010806df=drawable/menu_dropdown_panel_holo_dark 010806e0=drawable/menu_dropdown_panel_holo_light 010806e1=drawable/menu_hardkey_panel_holo_dark 010806e2=drawable/menu_hardkey_panel_holo_light 010806e3=drawable/menu_panel_holo_dark 010806e4=drawable/menu_panel_holo_light 010806e5=drawable/menu_popup_panel_holo_dark 010806e6=drawable/menu_popup_panel_holo_light 010806e7=drawable/menu_selector 010806e8=drawable/menu_separator 010806e9=drawable/menu_submenu_background 010806ea=drawable/menuitem_background_focus 010806eb=drawable/menuitem_background_pressed 010806ec=drawable/menuitem_background_solid 010806ed=drawable/menuitem_background_solid_focused 010806ee=drawable/menuitem_background_solid_pressed 010806ef=drawable/menuitem_checkbox 010806f0=drawable/menuitem_checkbox_on 010806f1=drawable/messaging_user 010806f2=drawable/minitab_lt 010806f3=drawable/minitab_lt_focus 010806f4=drawable/minitab_lt_press 010806f5=drawable/minitab_lt_selected 010806f6=drawable/minitab_lt_unselected 010806f7=drawable/minitab_lt_unselected_press 010806f8=drawable/no_tile_128 010806f9=drawable/no_tile_256 010806fa=drawable/notification_2025_expand_button_pill_bg 010806fb=drawable/notification_big_picture_outline 010806fc=drawable/notification_close_button_icon 010806fd=drawable/notification_icon_circle 010806fe=drawable/notification_large_icon_outline 010806ff=drawable/notification_material_action_background 01080700=drawable/notification_material_media_action_background 01080701=drawable/notification_progress 01080702=drawable/notification_progress_icon_background 01080703=drawable/notification_progress_indeterminate_horizontal_material 01080704=drawable/notification_template_divider 01080705=drawable/notification_template_divider_media 01080706=drawable/notification_template_icon_bg 01080707=drawable/notification_template_icon_low_bg 01080708=drawable/number_picker_divider_material 01080709=drawable/numberpicker_down_btn 0108070a=drawable/numberpicker_down_disabled 0108070b=drawable/numberpicker_down_disabled_focused 0108070c=drawable/numberpicker_down_disabled_focused_holo_dark 0108070d=drawable/numberpicker_down_disabled_focused_holo_light 0108070e=drawable/numberpicker_down_disabled_holo_dark 0108070f=drawable/numberpicker_down_disabled_holo_light 01080710=drawable/numberpicker_down_focused_holo_dark 01080711=drawable/numberpicker_down_focused_holo_light 01080712=drawable/numberpicker_down_longpressed_holo_dark 01080713=drawable/numberpicker_down_longpressed_holo_light 01080714=drawable/numberpicker_down_normal 01080715=drawable/numberpicker_down_normal_holo_dark 01080716=drawable/numberpicker_down_normal_holo_light 01080717=drawable/numberpicker_down_pressed 01080718=drawable/numberpicker_down_pressed_holo_dark 01080719=drawable/numberpicker_down_pressed_holo_light 0108071a=drawable/numberpicker_down_selected 0108071b=drawable/numberpicker_input 0108071c=drawable/numberpicker_input_disabled 0108071d=drawable/numberpicker_input_normal 0108071e=drawable/numberpicker_input_pressed 0108071f=drawable/numberpicker_input_selected 01080720=drawable/numberpicker_selection_divider 01080721=drawable/numberpicker_up_btn 01080722=drawable/numberpicker_up_disabled 01080723=drawable/numberpicker_up_disabled_focused 01080724=drawable/numberpicker_up_disabled_focused_holo_dark 01080725=drawable/numberpicker_up_disabled_focused_holo_light 01080726=drawable/numberpicker_up_disabled_holo_dark 01080727=drawable/numberpicker_up_disabled_holo_light 01080728=drawable/numberpicker_up_focused_holo_dark 01080729=drawable/numberpicker_up_focused_holo_light 0108072a=drawable/numberpicker_up_longpressed_holo_dark 0108072b=drawable/numberpicker_up_longpressed_holo_light 0108072c=drawable/numberpicker_up_normal 0108072d=drawable/numberpicker_up_normal_holo_dark 0108072e=drawable/numberpicker_up_normal_holo_light 0108072f=drawable/numberpicker_up_pressed 01080730=drawable/numberpicker_up_pressed_holo_dark 01080731=drawable/numberpicker_up_pressed_holo_light 01080732=drawable/numberpicker_up_selected 01080733=drawable/panel_background 01080734=drawable/panel_bg_holo_dark 01080735=drawable/panel_bg_holo_light 01080736=drawable/panel_picture_frame_background 01080737=drawable/panel_picture_frame_bg_focus_blue 01080738=drawable/panel_picture_frame_bg_normal 01080739=drawable/panel_picture_frame_bg_pressed_blue 0108073a=drawable/password_field_default 0108073b=drawable/password_keyboard_background_holo 0108073c=drawable/perm_group_accessibility_features 0108073d=drawable/perm_group_activity_recognition 0108073e=drawable/perm_group_affects_battery 0108073f=drawable/perm_group_app_info 01080740=drawable/perm_group_audio_settings 01080741=drawable/perm_group_aural 01080742=drawable/perm_group_bluetooth 01080743=drawable/perm_group_bookmarks 01080744=drawable/perm_group_calendar 01080745=drawable/perm_group_call_log 01080746=drawable/perm_group_camera 01080747=drawable/perm_group_contacts 01080748=drawable/perm_group_device_alarms 01080749=drawable/perm_group_display 0108074a=drawable/perm_group_location 0108074b=drawable/perm_group_microphone 0108074c=drawable/perm_group_nearby_devices 0108074d=drawable/perm_group_network 0108074e=drawable/perm_group_personal_info 0108074f=drawable/perm_group_phone_calls 01080750=drawable/perm_group_read_media_aural 01080751=drawable/perm_group_read_media_visual 01080752=drawable/perm_group_screenlock 01080753=drawable/perm_group_sensors 01080754=drawable/perm_group_shortrange_network 01080755=drawable/perm_group_sms 01080756=drawable/perm_group_status_bar 01080757=drawable/perm_group_storage 01080758=drawable/perm_group_sync_settings 01080759=drawable/perm_group_system_clock 0108075a=drawable/perm_group_system_tools 0108075b=drawable/perm_group_visual 0108075c=drawable/perm_group_voicemail 0108075d=drawable/perm_group_wallpaper 0108075e=drawable/picture_emergency 0108075f=drawable/platlogo 01080760=drawable/platlogo_m 01080761=drawable/pointer_alias 01080762=drawable/pointer_alias_icon 01080763=drawable/pointer_alias_large 01080764=drawable/pointer_alias_large_icon 01080765=drawable/pointer_alias_vector 01080766=drawable/pointer_alias_vector_icon 01080767=drawable/pointer_all_scroll 01080768=drawable/pointer_all_scroll_icon 01080769=drawable/pointer_all_scroll_large 0108076a=drawable/pointer_all_scroll_large_icon 0108076b=drawable/pointer_all_scroll_vector 0108076c=drawable/pointer_all_scroll_vector_icon 0108076d=drawable/pointer_arrow 0108076e=drawable/pointer_arrow_icon 0108076f=drawable/pointer_arrow_large 01080770=drawable/pointer_arrow_large_icon 01080771=drawable/pointer_arrow_vector 01080772=drawable/pointer_arrow_vector_icon 01080773=drawable/pointer_cell 01080774=drawable/pointer_cell_icon 01080775=drawable/pointer_cell_large 01080776=drawable/pointer_cell_large_icon 01080777=drawable/pointer_cell_vector 01080778=drawable/pointer_cell_vector_icon 01080779=drawable/pointer_context_menu 0108077a=drawable/pointer_context_menu_icon 0108077b=drawable/pointer_context_menu_large 0108077c=drawable/pointer_context_menu_large_icon 0108077d=drawable/pointer_context_menu_vector 0108077e=drawable/pointer_context_menu_vector_icon 0108077f=drawable/pointer_copy 01080780=drawable/pointer_copy_icon 01080781=drawable/pointer_copy_large 01080782=drawable/pointer_copy_large_icon 01080783=drawable/pointer_copy_vector 01080784=drawable/pointer_copy_vector_icon 01080785=drawable/pointer_crosshair 01080786=drawable/pointer_crosshair_icon 01080787=drawable/pointer_crosshair_large 01080788=drawable/pointer_crosshair_large_icon 01080789=drawable/pointer_crosshair_vector 0108078a=drawable/pointer_crosshair_vector_icon 0108078b=drawable/pointer_grab 0108078c=drawable/pointer_grab_icon 0108078d=drawable/pointer_grab_large 0108078e=drawable/pointer_grab_large_icon 0108078f=drawable/pointer_grab_vector 01080790=drawable/pointer_grab_vector_icon 01080791=drawable/pointer_grabbing 01080792=drawable/pointer_grabbing_icon 01080793=drawable/pointer_grabbing_large 01080794=drawable/pointer_grabbing_large_icon 01080795=drawable/pointer_grabbing_vector 01080796=drawable/pointer_grabbing_vector_icon 01080797=drawable/pointer_hand 01080798=drawable/pointer_hand_icon 01080799=drawable/pointer_hand_large 0108079a=drawable/pointer_hand_large_icon 0108079b=drawable/pointer_hand_vector 0108079c=drawable/pointer_hand_vector_icon 0108079d=drawable/pointer_handwriting 0108079e=drawable/pointer_handwriting_icon 0108079f=drawable/pointer_handwriting_vector 010807a0=drawable/pointer_handwriting_vector_icon 010807a1=drawable/pointer_help 010807a2=drawable/pointer_help_icon 010807a3=drawable/pointer_help_large 010807a4=drawable/pointer_help_large_icon 010807a5=drawable/pointer_help_vector 010807a6=drawable/pointer_help_vector_icon 010807a7=drawable/pointer_horizontal_double_arrow 010807a8=drawable/pointer_horizontal_double_arrow_icon 010807a9=drawable/pointer_horizontal_double_arrow_large 010807aa=drawable/pointer_horizontal_double_arrow_large_icon 010807ab=drawable/pointer_horizontal_double_arrow_vector 010807ac=drawable/pointer_horizontal_double_arrow_vector_icon 010807ad=drawable/pointer_nodrop 010807ae=drawable/pointer_nodrop_icon 010807af=drawable/pointer_nodrop_large 010807b0=drawable/pointer_nodrop_large_icon 010807b1=drawable/pointer_nodrop_vector 010807b2=drawable/pointer_nodrop_vector_icon 010807b3=drawable/pointer_spot_anchor 010807b4=drawable/pointer_spot_anchor_icon 010807b5=drawable/pointer_spot_anchor_vector 010807b6=drawable/pointer_spot_anchor_vector_icon 010807b7=drawable/pointer_spot_hover 010807b8=drawable/pointer_spot_hover_icon 010807b9=drawable/pointer_spot_hover_vector 010807ba=drawable/pointer_spot_hover_vector_icon 010807bb=drawable/pointer_spot_touch 010807bc=drawable/pointer_spot_touch_icon 010807bd=drawable/pointer_spot_touch_vector 010807be=drawable/pointer_spot_touch_vector_icon 010807bf=drawable/pointer_text 010807c0=drawable/pointer_text_icon 010807c1=drawable/pointer_text_large 010807c2=drawable/pointer_text_large_icon 010807c3=drawable/pointer_text_vector 010807c4=drawable/pointer_text_vector_icon 010807c5=drawable/pointer_top_left_diagonal_double_arrow 010807c6=drawable/pointer_top_left_diagonal_double_arrow_icon 010807c7=drawable/pointer_top_left_diagonal_double_arrow_large 010807c8=drawable/pointer_top_left_diagonal_double_arrow_large_icon 010807c9=drawable/pointer_top_left_diagonal_double_arrow_vector 010807ca=drawable/pointer_top_left_diagonal_double_arrow_vector_icon 010807cb=drawable/pointer_top_right_diagonal_double_arrow 010807cc=drawable/pointer_top_right_diagonal_double_arrow_icon 010807cd=drawable/pointer_top_right_diagonal_double_arrow_large 010807ce=drawable/pointer_top_right_diagonal_double_arrow_large_icon 010807cf=drawable/pointer_top_right_diagonal_double_arrow_vector 010807d0=drawable/pointer_top_right_diagonal_double_arrow_vector_icon 010807d1=drawable/pointer_vertical_double_arrow 010807d2=drawable/pointer_vertical_double_arrow_icon 010807d3=drawable/pointer_vertical_double_arrow_large 010807d4=drawable/pointer_vertical_double_arrow_large_icon 010807d5=drawable/pointer_vertical_double_arrow_vector 010807d6=drawable/pointer_vertical_double_arrow_vector_icon 010807d7=drawable/pointer_vertical_text 010807d8=drawable/pointer_vertical_text_icon 010807d9=drawable/pointer_vertical_text_large 010807da=drawable/pointer_vertical_text_large_icon 010807db=drawable/pointer_vertical_text_vector 010807dc=drawable/pointer_vertical_text_vector_icon 010807dd=drawable/pointer_wait 010807de=drawable/pointer_wait_0 010807df=drawable/pointer_wait_1 010807e0=drawable/pointer_wait_10 010807e1=drawable/pointer_wait_11 010807e2=drawable/pointer_wait_12 010807e3=drawable/pointer_wait_13 010807e4=drawable/pointer_wait_14 010807e5=drawable/pointer_wait_15 010807e6=drawable/pointer_wait_16 010807e7=drawable/pointer_wait_17 010807e8=drawable/pointer_wait_18 010807e9=drawable/pointer_wait_19 010807ea=drawable/pointer_wait_2 010807eb=drawable/pointer_wait_20 010807ec=drawable/pointer_wait_21 010807ed=drawable/pointer_wait_22 010807ee=drawable/pointer_wait_23 010807ef=drawable/pointer_wait_24 010807f0=drawable/pointer_wait_25 010807f1=drawable/pointer_wait_26 010807f2=drawable/pointer_wait_27 010807f3=drawable/pointer_wait_28 010807f4=drawable/pointer_wait_29 010807f5=drawable/pointer_wait_3 010807f6=drawable/pointer_wait_30 010807f7=drawable/pointer_wait_31 010807f8=drawable/pointer_wait_32 010807f9=drawable/pointer_wait_33 010807fa=drawable/pointer_wait_34 010807fb=drawable/pointer_wait_35 010807fc=drawable/pointer_wait_36 010807fd=drawable/pointer_wait_37 010807fe=drawable/pointer_wait_38 010807ff=drawable/pointer_wait_39 01080800=drawable/pointer_wait_4 01080801=drawable/pointer_wait_40 01080802=drawable/pointer_wait_41 01080803=drawable/pointer_wait_42 01080804=drawable/pointer_wait_43 01080805=drawable/pointer_wait_44 01080806=drawable/pointer_wait_45 01080807=drawable/pointer_wait_46 01080808=drawable/pointer_wait_47 01080809=drawable/pointer_wait_48 0108080a=drawable/pointer_wait_49 0108080b=drawable/pointer_wait_5 0108080c=drawable/pointer_wait_50 0108080d=drawable/pointer_wait_51 0108080e=drawable/pointer_wait_52 0108080f=drawable/pointer_wait_53 01080810=drawable/pointer_wait_54 01080811=drawable/pointer_wait_55 01080812=drawable/pointer_wait_56 01080813=drawable/pointer_wait_57 01080814=drawable/pointer_wait_58 01080815=drawable/pointer_wait_59 01080816=drawable/pointer_wait_6 01080817=drawable/pointer_wait_60 01080818=drawable/pointer_wait_61 01080819=drawable/pointer_wait_62 0108081a=drawable/pointer_wait_63 0108081b=drawable/pointer_wait_64 0108081c=drawable/pointer_wait_65 0108081d=drawable/pointer_wait_66 0108081e=drawable/pointer_wait_67 0108081f=drawable/pointer_wait_68 01080820=drawable/pointer_wait_69 01080821=drawable/pointer_wait_7 01080822=drawable/pointer_wait_70 01080823=drawable/pointer_wait_71 01080824=drawable/pointer_wait_72 01080825=drawable/pointer_wait_73 01080826=drawable/pointer_wait_74 01080827=drawable/pointer_wait_75 01080828=drawable/pointer_wait_76 01080829=drawable/pointer_wait_77 0108082a=drawable/pointer_wait_78 0108082b=drawable/pointer_wait_79 0108082c=drawable/pointer_wait_8 0108082d=drawable/pointer_wait_80 0108082e=drawable/pointer_wait_9 0108082f=drawable/pointer_wait_icon 01080830=drawable/pointer_wait_vector 01080831=drawable/pointer_wait_vector_0 01080832=drawable/pointer_wait_vector_1 01080833=drawable/pointer_wait_vector_10 01080834=drawable/pointer_wait_vector_11 01080835=drawable/pointer_wait_vector_12 01080836=drawable/pointer_wait_vector_13 01080837=drawable/pointer_wait_vector_14 01080838=drawable/pointer_wait_vector_15 01080839=drawable/pointer_wait_vector_16 0108083a=drawable/pointer_wait_vector_17 0108083b=drawable/pointer_wait_vector_18 0108083c=drawable/pointer_wait_vector_19 0108083d=drawable/pointer_wait_vector_2 0108083e=drawable/pointer_wait_vector_20 0108083f=drawable/pointer_wait_vector_21 01080840=drawable/pointer_wait_vector_22 01080841=drawable/pointer_wait_vector_23 01080842=drawable/pointer_wait_vector_24 01080843=drawable/pointer_wait_vector_25 01080844=drawable/pointer_wait_vector_26 01080845=drawable/pointer_wait_vector_27 01080846=drawable/pointer_wait_vector_28 01080847=drawable/pointer_wait_vector_29 01080848=drawable/pointer_wait_vector_3 01080849=drawable/pointer_wait_vector_30 0108084a=drawable/pointer_wait_vector_31 0108084b=drawable/pointer_wait_vector_32 0108084c=drawable/pointer_wait_vector_33 0108084d=drawable/pointer_wait_vector_34 0108084e=drawable/pointer_wait_vector_35 0108084f=drawable/pointer_wait_vector_36 01080850=drawable/pointer_wait_vector_37 01080851=drawable/pointer_wait_vector_38 01080852=drawable/pointer_wait_vector_39 01080853=drawable/pointer_wait_vector_4 01080854=drawable/pointer_wait_vector_40 01080855=drawable/pointer_wait_vector_41 01080856=drawable/pointer_wait_vector_42 01080857=drawable/pointer_wait_vector_43 01080858=drawable/pointer_wait_vector_44 01080859=drawable/pointer_wait_vector_45 0108085a=drawable/pointer_wait_vector_46 0108085b=drawable/pointer_wait_vector_47 0108085c=drawable/pointer_wait_vector_48 0108085d=drawable/pointer_wait_vector_49 0108085e=drawable/pointer_wait_vector_5 0108085f=drawable/pointer_wait_vector_50 01080860=drawable/pointer_wait_vector_51 01080861=drawable/pointer_wait_vector_52 01080862=drawable/pointer_wait_vector_53 01080863=drawable/pointer_wait_vector_54 01080864=drawable/pointer_wait_vector_55 01080865=drawable/pointer_wait_vector_56 01080866=drawable/pointer_wait_vector_57 01080867=drawable/pointer_wait_vector_58 01080868=drawable/pointer_wait_vector_59 01080869=drawable/pointer_wait_vector_6 0108086a=drawable/pointer_wait_vector_60 0108086b=drawable/pointer_wait_vector_61 0108086c=drawable/pointer_wait_vector_62 0108086d=drawable/pointer_wait_vector_63 0108086e=drawable/pointer_wait_vector_64 0108086f=drawable/pointer_wait_vector_65 01080870=drawable/pointer_wait_vector_66 01080871=drawable/pointer_wait_vector_67 01080872=drawable/pointer_wait_vector_68 01080873=drawable/pointer_wait_vector_69 01080874=drawable/pointer_wait_vector_7 01080875=drawable/pointer_wait_vector_70 01080876=drawable/pointer_wait_vector_71 01080877=drawable/pointer_wait_vector_72 01080878=drawable/pointer_wait_vector_73 01080879=drawable/pointer_wait_vector_74 0108087a=drawable/pointer_wait_vector_75 0108087b=drawable/pointer_wait_vector_76 0108087c=drawable/pointer_wait_vector_77 0108087d=drawable/pointer_wait_vector_78 0108087e=drawable/pointer_wait_vector_79 0108087f=drawable/pointer_wait_vector_8 01080880=drawable/pointer_wait_vector_80 01080881=drawable/pointer_wait_vector_81 01080882=drawable/pointer_wait_vector_82 01080883=drawable/pointer_wait_vector_83 01080884=drawable/pointer_wait_vector_84 01080885=drawable/pointer_wait_vector_85 01080886=drawable/pointer_wait_vector_86 01080887=drawable/pointer_wait_vector_87 01080888=drawable/pointer_wait_vector_9 01080889=drawable/pointer_wait_vector_icon 0108088a=drawable/pointer_zoom_in 0108088b=drawable/pointer_zoom_in_icon 0108088c=drawable/pointer_zoom_in_large 0108088d=drawable/pointer_zoom_in_large_icon 0108088e=drawable/pointer_zoom_in_vector 0108088f=drawable/pointer_zoom_in_vector_icon 01080890=drawable/pointer_zoom_out 01080891=drawable/pointer_zoom_out_icon 01080892=drawable/pointer_zoom_out_large 01080893=drawable/pointer_zoom_out_large_icon 01080894=drawable/pointer_zoom_out_vector 01080895=drawable/pointer_zoom_out_vector_icon 01080896=drawable/popup_background_material 01080897=drawable/popup_background_mtrl_mult 01080898=drawable/popup_bottom_bright 01080899=drawable/popup_bottom_dark 0108089a=drawable/popup_bottom_medium 0108089b=drawable/popup_center_bright 0108089c=drawable/popup_center_dark 0108089d=drawable/popup_center_medium 0108089e=drawable/popup_full_bright 0108089f=drawable/popup_full_dark 010808a0=drawable/popup_inline_error 010808a1=drawable/popup_inline_error_above 010808a2=drawable/popup_inline_error_above_am 010808a3=drawable/popup_inline_error_above_holo_dark 010808a4=drawable/popup_inline_error_above_holo_dark_am 010808a5=drawable/popup_inline_error_above_holo_light 010808a6=drawable/popup_inline_error_above_holo_light_am 010808a7=drawable/popup_inline_error_am 010808a8=drawable/popup_inline_error_holo_dark 010808a9=drawable/popup_inline_error_holo_dark_am 010808aa=drawable/popup_inline_error_holo_light 010808ab=drawable/popup_inline_error_holo_light_am 010808ac=drawable/popup_top_bright 010808ad=drawable/popup_top_dark 010808ae=drawable/pressed_application_background_static 010808af=drawable/progress_bg_holo_dark 010808b0=drawable/progress_bg_holo_light 010808b1=drawable/progress_horizontal_holo_dark 010808b2=drawable/progress_horizontal_holo_light 010808b3=drawable/progress_horizontal_material 010808b4=drawable/progress_indeterminate_anim_large_material 010808b5=drawable/progress_indeterminate_anim_medium_material 010808b6=drawable/progress_indeterminate_horizontal_holo 010808b7=drawable/progress_indeterminate_horizontal_material 010808b8=drawable/progress_large 010808b9=drawable/progress_large_holo 010808ba=drawable/progress_large_material 010808bb=drawable/progress_large_white 010808bc=drawable/progress_medium 010808bd=drawable/progress_medium_holo 010808be=drawable/progress_medium_material 010808bf=drawable/progress_medium_white 010808c0=drawable/progress_primary_holo_dark 010808c1=drawable/progress_primary_holo_light 010808c2=drawable/progress_ring_watch 010808c3=drawable/progress_secondary_holo_dark 010808c4=drawable/progress_secondary_holo_light 010808c5=drawable/progress_small 010808c6=drawable/progress_small_holo 010808c7=drawable/progress_small_material 010808c8=drawable/progress_small_titlebar 010808c9=drawable/progress_small_white 010808ca=drawable/progress_static_material 010808cb=drawable/progressbar_indeterminate1 010808cc=drawable/progressbar_indeterminate2 010808cd=drawable/progressbar_indeterminate3 010808ce=drawable/progressbar_indeterminate_holo1 010808cf=drawable/progressbar_indeterminate_holo2 010808d0=drawable/progressbar_indeterminate_holo3 010808d1=drawable/progressbar_indeterminate_holo4 010808d2=drawable/progressbar_indeterminate_holo5 010808d3=drawable/progressbar_indeterminate_holo6 010808d4=drawable/progressbar_indeterminate_holo7 010808d5=drawable/progressbar_indeterminate_holo8 010808d6=drawable/quickactions_arrowdown_left_holo_dark 010808d7=drawable/quickactions_arrowdown_left_holo_light 010808d8=drawable/quickactions_arrowdown_right_holo_dark 010808d9=drawable/quickactions_arrowdown_right_holo_light 010808da=drawable/quickactions_arrowup_left_holo_dark 010808db=drawable/quickactions_arrowup_left_holo_light 010808dc=drawable/quickactions_arrowup_left_right_holo_dark 010808dd=drawable/quickactions_arrowup_right_holo_light 010808de=drawable/quickcontact_badge_overlay_dark 010808df=drawable/quickcontact_badge_overlay_focused_dark 010808e0=drawable/quickcontact_badge_overlay_focused_dark_am 010808e1=drawable/quickcontact_badge_overlay_focused_light 010808e2=drawable/quickcontact_badge_overlay_focused_light_am 010808e3=drawable/quickcontact_badge_overlay_light 010808e4=drawable/quickcontact_badge_overlay_normal_dark 010808e5=drawable/quickcontact_badge_overlay_normal_dark_am 010808e6=drawable/quickcontact_badge_overlay_normal_light 010808e7=drawable/quickcontact_badge_overlay_normal_light_am 010808e8=drawable/quickcontact_badge_overlay_pressed_dark 010808e9=drawable/quickcontact_badge_overlay_pressed_dark_am 010808ea=drawable/quickcontact_badge_overlay_pressed_light 010808eb=drawable/quickcontact_badge_overlay_pressed_light_am 010808ec=drawable/rate_star_big_half 010808ed=drawable/rate_star_big_half_holo_dark 010808ee=drawable/rate_star_big_half_holo_light 010808ef=drawable/rate_star_big_off 010808f0=drawable/rate_star_big_off_holo_dark 010808f1=drawable/rate_star_big_off_holo_light 010808f2=drawable/rate_star_big_on 010808f3=drawable/rate_star_big_on_holo_dark 010808f4=drawable/rate_star_big_on_holo_light 010808f5=drawable/rate_star_med_half 010808f6=drawable/rate_star_med_half_holo_dark 010808f7=drawable/rate_star_med_half_holo_light 010808f8=drawable/rate_star_med_off 010808f9=drawable/rate_star_med_off_holo_dark 010808fa=drawable/rate_star_med_off_holo_light 010808fb=drawable/rate_star_med_on 010808fc=drawable/rate_star_med_on_holo_dark 010808fd=drawable/rate_star_med_on_holo_light 010808fe=drawable/rate_star_small_half 010808ff=drawable/rate_star_small_half_holo_dark 01080900=drawable/rate_star_small_half_holo_light 01080901=drawable/rate_star_small_off 01080902=drawable/rate_star_small_off_holo_dark 01080903=drawable/rate_star_small_off_holo_light 01080904=drawable/rate_star_small_on 01080905=drawable/rate_star_small_on_holo_dark 01080906=drawable/rate_star_small_on_holo_light 01080907=drawable/ratingbar 01080908=drawable/ratingbar_full 01080909=drawable/ratingbar_full_empty 0108090a=drawable/ratingbar_full_empty_holo_dark 0108090b=drawable/ratingbar_full_empty_holo_light 0108090c=drawable/ratingbar_full_empty_material 0108090d=drawable/ratingbar_full_filled 0108090e=drawable/ratingbar_full_filled_holo_dark 0108090f=drawable/ratingbar_full_filled_holo_light 01080910=drawable/ratingbar_full_filled_material 01080911=drawable/ratingbar_full_half_material 01080912=drawable/ratingbar_full_holo_dark 01080913=drawable/ratingbar_full_holo_light 01080914=drawable/ratingbar_holo_dark 01080915=drawable/ratingbar_holo_light 01080916=drawable/ratingbar_indicator_material 01080917=drawable/ratingbar_material 01080918=drawable/ratingbar_small 01080919=drawable/ratingbar_small_holo_dark 0108091a=drawable/ratingbar_small_holo_light 0108091b=drawable/ratingbar_small_material 0108091c=drawable/recent_dialog_background 0108091d=drawable/red_shield 0108091e=drawable/resolver_button_bg 0108091f=drawable/resolver_icon_placeholder 01080920=drawable/resolver_outlined_button_bg 01080921=drawable/resolver_profile_tab_bg 01080922=drawable/resolver_turn_on_work_button_ripple_background 01080923=drawable/reticle 01080924=drawable/safe_mode_background 01080925=drawable/screen_background_holo_dark 01080926=drawable/screen_background_holo_light 01080927=drawable/screen_background_selector_dark 01080928=drawable/screen_background_selector_light 01080929=drawable/scroll_indicator_material 0108092a=drawable/scrollbar_handle_accelerated_anim2 0108092b=drawable/scrollbar_handle_holo_dark 0108092c=drawable/scrollbar_handle_holo_light 0108092d=drawable/scrollbar_handle_horizontal 0108092e=drawable/scrollbar_handle_material 0108092f=drawable/scrollbar_handle_vertical 01080930=drawable/scrollbar_vertical_thumb 01080931=drawable/scrollbar_vertical_track 01080932=drawable/scrubber_control_disabled_holo 01080933=drawable/scrubber_control_focused_holo 01080934=drawable/scrubber_control_normal_holo 01080935=drawable/scrubber_control_on_mtrl_alpha 01080936=drawable/scrubber_control_on_pressed_mtrl_alpha 01080937=drawable/scrubber_control_pressed_holo 01080938=drawable/scrubber_control_selector_holo 01080939=drawable/scrubber_primary_holo 0108093a=drawable/scrubber_primary_mtrl_alpha 0108093b=drawable/scrubber_progress_horizontal_holo_dark 0108093c=drawable/scrubber_progress_horizontal_holo_light 0108093d=drawable/scrubber_secondary_holo 0108093e=drawable/scrubber_track_holo_dark 0108093f=drawable/scrubber_track_holo_light 01080940=drawable/scrubber_track_mtrl_alpha 01080941=drawable/search_bar_default_color 01080942=drawable/search_dropdown_background 01080943=drawable/search_dropdown_dark 01080944=drawable/search_dropdown_light 01080945=drawable/search_plate 01080946=drawable/search_plate_global 01080947=drawable/search_spinner 01080948=drawable/seek_thumb 01080949=drawable/seek_thumb_normal 0108094a=drawable/seek_thumb_pressed 0108094b=drawable/seek_thumb_selected 0108094c=drawable/seekbar_thumb_material_anim 0108094d=drawable/seekbar_thumb_pressed_to_unpressed 0108094e=drawable/seekbar_thumb_pressed_to_unpressed_animation 0108094f=drawable/seekbar_thumb_unpressed_to_pressed 01080950=drawable/seekbar_thumb_unpressed_to_pressed_animation 01080951=drawable/seekbar_tick_mark_material 01080952=drawable/seekbar_track_material 01080953=drawable/selected_day_background 01080954=drawable/settings_header 01080955=drawable/settings_header_raw 01080956=drawable/silent_mode_indicator 01080957=drawable/sim_dark_blue 01080958=drawable/sim_dark_green 01080959=drawable/sim_dark_orange 0108095a=drawable/sim_dark_purple 0108095b=drawable/sim_light_blue 0108095c=drawable/sim_light_green 0108095d=drawable/sim_light_orange 0108095e=drawable/sim_light_purple 0108095f=drawable/slice_remote_input_bg 01080960=drawable/slice_ripple_drawable 01080961=drawable/spinner_16_inner_holo 01080962=drawable/spinner_16_outer_holo 01080963=drawable/spinner_48_inner_holo 01080964=drawable/spinner_48_outer_holo 01080965=drawable/spinner_76_inner_holo 01080966=drawable/spinner_76_outer_holo 01080967=drawable/spinner_ab_activated_holo_dark 01080968=drawable/spinner_ab_activated_holo_light 01080969=drawable/spinner_ab_default_holo_dark 0108096a=drawable/spinner_ab_default_holo_dark_am 0108096b=drawable/spinner_ab_default_holo_light 0108096c=drawable/spinner_ab_default_holo_light_am 0108096d=drawable/spinner_ab_disabled_holo_dark 0108096e=drawable/spinner_ab_disabled_holo_dark_am 0108096f=drawable/spinner_ab_disabled_holo_light 01080970=drawable/spinner_ab_disabled_holo_light_am 01080971=drawable/spinner_ab_focused_holo_dark 01080972=drawable/spinner_ab_focused_holo_dark_am 01080973=drawable/spinner_ab_focused_holo_light 01080974=drawable/spinner_ab_focused_holo_light_am 01080975=drawable/spinner_ab_holo_dark 01080976=drawable/spinner_ab_holo_light 01080977=drawable/spinner_ab_pressed_holo_dark 01080978=drawable/spinner_ab_pressed_holo_dark_am 01080979=drawable/spinner_ab_pressed_holo_light 0108097a=drawable/spinner_ab_pressed_holo_light_am 0108097b=drawable/spinner_activated_holo_dark 0108097c=drawable/spinner_activated_holo_light 0108097d=drawable/spinner_background_holo_dark 0108097e=drawable/spinner_background_holo_light 0108097f=drawable/spinner_background_material 01080980=drawable/spinner_black_16 01080981=drawable/spinner_black_20 01080982=drawable/spinner_black_48 01080983=drawable/spinner_black_76 01080984=drawable/spinner_default_holo_dark 01080985=drawable/spinner_default_holo_dark_am 01080986=drawable/spinner_default_holo_light 01080987=drawable/spinner_default_holo_light_am 01080988=drawable/spinner_disabled_holo 01080989=drawable/spinner_disabled_holo_dark 0108098a=drawable/spinner_disabled_holo_dark_am 0108098b=drawable/spinner_disabled_holo_light 0108098c=drawable/spinner_disabled_holo_light_am 0108098d=drawable/spinner_dropdown_background_down 0108098e=drawable/spinner_dropdown_background_up 0108098f=drawable/spinner_focused_holo_dark 01080990=drawable/spinner_focused_holo_dark_am 01080991=drawable/spinner_focused_holo_light 01080992=drawable/spinner_focused_holo_light_am 01080993=drawable/spinner_normal 01080994=drawable/spinner_normal_holo 01080995=drawable/spinner_press 01080996=drawable/spinner_pressed_holo_dark 01080997=drawable/spinner_pressed_holo_dark_am 01080998=drawable/spinner_pressed_holo_light 01080999=drawable/spinner_pressed_holo_light_am 0108099a=drawable/spinner_select 0108099b=drawable/spinner_textfield_background_material 0108099c=drawable/spinner_white_16 0108099d=drawable/spinner_white_48 0108099e=drawable/spinner_white_76 0108099f=drawable/stat_ecb_mode 010809a0=drawable/stat_notify_car_mode 010809a1=drawable/stat_notify_disabled_data 010809a2=drawable/stat_notify_disk_full 010809a3=drawable/stat_notify_email_generic 010809a4=drawable/stat_notify_gmail 010809a5=drawable/stat_notify_mmcc_indication_icn 010809a6=drawable/stat_notify_rssi_in_range 010809a7=drawable/stat_notify_sim_toolkit 010809a8=drawable/stat_notify_sync_anim0 010809a9=drawable/stat_notify_sync_error 010809aa=drawable/stat_notify_wifi_in_range 010809ab=drawable/stat_sys_adb 010809ac=drawable/stat_sys_battery 010809ad=drawable/stat_sys_battery_0 010809ae=drawable/stat_sys_battery_10 010809af=drawable/stat_sys_battery_100 010809b0=drawable/stat_sys_battery_15 010809b1=drawable/stat_sys_battery_20 010809b2=drawable/stat_sys_battery_28 010809b3=drawable/stat_sys_battery_40 010809b4=drawable/stat_sys_battery_43 010809b5=drawable/stat_sys_battery_57 010809b6=drawable/stat_sys_battery_60 010809b7=drawable/stat_sys_battery_71 010809b8=drawable/stat_sys_battery_80 010809b9=drawable/stat_sys_battery_85 010809ba=drawable/stat_sys_battery_charge 010809bb=drawable/stat_sys_battery_charge_anim0 010809bc=drawable/stat_sys_battery_charge_anim1 010809bd=drawable/stat_sys_battery_charge_anim100 010809be=drawable/stat_sys_battery_charge_anim15 010809bf=drawable/stat_sys_battery_charge_anim2 010809c0=drawable/stat_sys_battery_charge_anim28 010809c1=drawable/stat_sys_battery_charge_anim3 010809c2=drawable/stat_sys_battery_charge_anim4 010809c3=drawable/stat_sys_battery_charge_anim43 010809c4=drawable/stat_sys_battery_charge_anim5 010809c5=drawable/stat_sys_battery_charge_anim57 010809c6=drawable/stat_sys_battery_charge_anim71 010809c7=drawable/stat_sys_battery_charge_anim85 010809c8=drawable/stat_sys_battery_unknown 010809c9=drawable/stat_sys_certificate_info 010809ca=drawable/stat_sys_data_usb 010809cb=drawable/stat_sys_data_wimax_signal_3_fully 010809cc=drawable/stat_sys_data_wimax_signal_disconnected 010809cd=drawable/stat_sys_download_anim0 010809ce=drawable/stat_sys_download_anim1 010809cf=drawable/stat_sys_download_anim2 010809d0=drawable/stat_sys_download_anim3 010809d1=drawable/stat_sys_download_anim4 010809d2=drawable/stat_sys_download_anim5 010809d3=drawable/stat_sys_download_done_static 010809d4=drawable/stat_sys_gps_on 010809d5=drawable/stat_sys_managed_profile_status 010809d6=drawable/stat_sys_private_profile_status 010809d7=drawable/stat_sys_r_signal_0_cdma 010809d8=drawable/stat_sys_r_signal_1_cdma 010809d9=drawable/stat_sys_r_signal_2_cdma 010809da=drawable/stat_sys_r_signal_3_cdma 010809db=drawable/stat_sys_r_signal_4_cdma 010809dc=drawable/stat_sys_ra_signal_0_cdma 010809dd=drawable/stat_sys_ra_signal_1_cdma 010809de=drawable/stat_sys_ra_signal_2_cdma 010809df=drawable/stat_sys_ra_signal_3_cdma 010809e0=drawable/stat_sys_ra_signal_4_cdma 010809e1=drawable/stat_sys_signal_0_cdma 010809e2=drawable/stat_sys_signal_1_cdma 010809e3=drawable/stat_sys_signal_2_cdma 010809e4=drawable/stat_sys_signal_3_cdma 010809e5=drawable/stat_sys_signal_4_cdma 010809e6=drawable/stat_sys_signal_evdo_0 010809e7=drawable/stat_sys_signal_evdo_1 010809e8=drawable/stat_sys_signal_evdo_2 010809e9=drawable/stat_sys_signal_evdo_3 010809ea=drawable/stat_sys_signal_evdo_4 010809eb=drawable/stat_sys_tether_wifi 010809ec=drawable/stat_sys_throttled 010809ed=drawable/stat_sys_upload_anim0 010809ee=drawable/stat_sys_upload_anim1 010809ef=drawable/stat_sys_upload_anim2 010809f0=drawable/stat_sys_upload_anim3 010809f1=drawable/stat_sys_upload_anim4 010809f2=drawable/stat_sys_upload_anim5 010809f3=drawable/stat_sys_vitals 010809f4=drawable/status_bar_background 010809f5=drawable/status_bar_closed_default_background 010809f6=drawable/status_bar_header_background 010809f7=drawable/status_bar_item_app_background_normal 010809f8=drawable/status_bar_item_background_focus 010809f9=drawable/status_bar_item_background_normal 010809fa=drawable/status_bar_item_background_pressed 010809fb=drawable/status_bar_opened_default_background 010809fc=drawable/statusbar_background 010809fd=drawable/submenu_arrow 010809fe=drawable/submenu_arrow_nofocus 010809ff=drawable/switch_bg_disabled_holo_dark 01080a00=drawable/switch_bg_disabled_holo_light 01080a01=drawable/switch_bg_focused_holo_dark 01080a02=drawable/switch_bg_focused_holo_light 01080a03=drawable/switch_bg_holo_dark 01080a04=drawable/switch_bg_holo_light 01080a05=drawable/switch_inner_holo_dark 01080a06=drawable/switch_inner_holo_light 01080a07=drawable/switch_thumb_activated_holo_dark 01080a08=drawable/switch_thumb_activated_holo_light 01080a09=drawable/switch_thumb_disabled_holo_dark 01080a0a=drawable/switch_thumb_disabled_holo_light 01080a0b=drawable/switch_thumb_holo_dark 01080a0c=drawable/switch_thumb_holo_light 01080a0d=drawable/switch_thumb_holo_light_v2 01080a0e=drawable/switch_thumb_material_anim 01080a0f=drawable/switch_thumb_pressed_holo_dark 01080a10=drawable/switch_thumb_pressed_holo_light 01080a11=drawable/switch_thumb_watch_default_dark_anim 01080a12=drawable/switch_track_holo_dark 01080a13=drawable/switch_track_holo_light 01080a14=drawable/switch_track_material 01080a15=drawable/sym_action_add 01080a16=drawable/sym_app_on_sd_unavailable_icon 01080a17=drawable/sym_def_app_icon_background 01080a18=drawable/sym_keyboard_delete 01080a19=drawable/sym_keyboard_delete_dim 01080a1a=drawable/sym_keyboard_delete_holo 01080a1b=drawable/sym_keyboard_enter 01080a1c=drawable/sym_keyboard_feedback_delete 01080a1d=drawable/sym_keyboard_feedback_ok 01080a1e=drawable/sym_keyboard_feedback_return 01080a1f=drawable/sym_keyboard_feedback_shift 01080a20=drawable/sym_keyboard_feedback_shift_locked 01080a21=drawable/sym_keyboard_feedback_space 01080a22=drawable/sym_keyboard_num0_no_plus 01080a23=drawable/sym_keyboard_num1 01080a24=drawable/sym_keyboard_num2 01080a25=drawable/sym_keyboard_num3 01080a26=drawable/sym_keyboard_num4 01080a27=drawable/sym_keyboard_num5 01080a28=drawable/sym_keyboard_num6 01080a29=drawable/sym_keyboard_num7 01080a2a=drawable/sym_keyboard_num8 01080a2b=drawable/sym_keyboard_num9 01080a2c=drawable/sym_keyboard_ok 01080a2d=drawable/sym_keyboard_ok_dim 01080a2e=drawable/sym_keyboard_return 01080a2f=drawable/sym_keyboard_return_holo 01080a30=drawable/sym_keyboard_shift 01080a31=drawable/sym_keyboard_shift_locked 01080a32=drawable/sym_keyboard_space 01080a33=drawable/tab_bottom_holo 01080a34=drawable/tab_bottom_left 01080a35=drawable/tab_bottom_left_v4 01080a36=drawable/tab_bottom_right 01080a37=drawable/tab_bottom_right_v4 01080a38=drawable/tab_focus 01080a39=drawable/tab_focus_bar_left 01080a3a=drawable/tab_focus_bar_right 01080a3b=drawable/tab_indicator 01080a3c=drawable/tab_indicator_ab_holo 01080a3d=drawable/tab_indicator_holo 01080a3e=drawable/tab_indicator_material 01080a3f=drawable/tab_indicator_mtrl_alpha 01080a40=drawable/tab_indicator_resolver 01080a41=drawable/tab_indicator_v4 01080a42=drawable/tab_press 01080a43=drawable/tab_press_bar_left 01080a44=drawable/tab_press_bar_right 01080a45=drawable/tab_pressed_holo 01080a46=drawable/tab_selected 01080a47=drawable/tab_selected_bar_left 01080a48=drawable/tab_selected_bar_left_v4 01080a49=drawable/tab_selected_bar_right 01080a4a=drawable/tab_selected_bar_right_v4 01080a4b=drawable/tab_selected_focused_holo 01080a4c=drawable/tab_selected_holo 01080a4d=drawable/tab_selected_pressed_holo 01080a4e=drawable/tab_selected_v4 01080a4f=drawable/tab_unselected 01080a50=drawable/tab_unselected_focused_holo 01080a51=drawable/tab_unselected_holo 01080a52=drawable/tab_unselected_pressed_holo 01080a53=drawable/tab_unselected_v4 01080a54=drawable/text_cursor_holo_dark 01080a55=drawable/text_cursor_holo_light 01080a56=drawable/text_cursor_material 01080a57=drawable/text_edit_paste_window 01080a58=drawable/text_edit_side_paste_window 01080a59=drawable/text_edit_suggestions_window 01080a5a=drawable/text_select_handle_left_material 01080a5b=drawable/text_select_handle_left_mtrl_alpha 01080a5c=drawable/text_select_handle_middle_material 01080a5d=drawable/text_select_handle_middle_mtrl_alpha 01080a5e=drawable/text_select_handle_right_material 01080a5f=drawable/text_select_handle_right_mtrl_alpha 01080a60=drawable/textfield_activated_holo_dark 01080a61=drawable/textfield_activated_holo_light 01080a62=drawable/textfield_activated_mtrl_alpha 01080a63=drawable/textfield_bg_activated_holo_dark 01080a64=drawable/textfield_bg_default_holo_dark 01080a65=drawable/textfield_bg_disabled_focused_holo_dark 01080a66=drawable/textfield_bg_disabled_holo_dark 01080a67=drawable/textfield_bg_focused_holo_dark 01080a68=drawable/textfield_default 01080a69=drawable/textfield_default_holo_dark 01080a6a=drawable/textfield_default_holo_light 01080a6b=drawable/textfield_default_mtrl_alpha 01080a6c=drawable/textfield_disabled 01080a6d=drawable/textfield_disabled_focused_holo_dark 01080a6e=drawable/textfield_disabled_focused_holo_light 01080a6f=drawable/textfield_disabled_holo_dark 01080a70=drawable/textfield_disabled_holo_light 01080a71=drawable/textfield_disabled_selected 01080a72=drawable/textfield_focused_holo_dark 01080a73=drawable/textfield_focused_holo_light 01080a74=drawable/textfield_longpress_holo 01080a75=drawable/textfield_multiline_activated_holo_dark 01080a76=drawable/textfield_multiline_activated_holo_light 01080a77=drawable/textfield_multiline_default_holo_dark 01080a78=drawable/textfield_multiline_default_holo_light 01080a79=drawable/textfield_multiline_disabled_focused_holo_dark 01080a7a=drawable/textfield_multiline_disabled_focused_holo_light 01080a7b=drawable/textfield_multiline_disabled_holo_dark 01080a7c=drawable/textfield_multiline_disabled_holo_light 01080a7d=drawable/textfield_multiline_focused_holo_dark 01080a7e=drawable/textfield_multiline_focused_holo_light 01080a7f=drawable/textfield_pressed_holo 01080a80=drawable/textfield_search 01080a81=drawable/textfield_search_activated_mtrl_alpha 01080a82=drawable/textfield_search_default 01080a83=drawable/textfield_search_default_holo_dark 01080a84=drawable/textfield_search_default_holo_light 01080a85=drawable/textfield_search_default_mtrl_alpha 01080a86=drawable/textfield_search_empty 01080a87=drawable/textfield_search_empty_default 01080a88=drawable/textfield_search_empty_pressed 01080a89=drawable/textfield_search_empty_selected 01080a8a=drawable/textfield_search_material 01080a8b=drawable/textfield_search_pressed 01080a8c=drawable/textfield_search_right_default_holo_dark 01080a8d=drawable/textfield_search_right_default_holo_light 01080a8e=drawable/textfield_search_right_selected_holo_dark 01080a8f=drawable/textfield_search_right_selected_holo_light 01080a90=drawable/textfield_search_selected 01080a91=drawable/textfield_search_selected_holo_dark 01080a92=drawable/textfield_search_selected_holo_light 01080a93=drawable/textfield_searchview_holo_dark 01080a94=drawable/textfield_searchview_holo_light 01080a95=drawable/textfield_searchview_right_holo_dark 01080a96=drawable/textfield_searchview_right_holo_light 01080a97=drawable/textfield_selected 01080a98=drawable/time_picker_editable_background 01080a99=drawable/title_bar_medium 01080a9a=drawable/title_bar_portrait 01080a9b=drawable/title_bar_shadow 01080a9c=drawable/tooltip_frame 01080a9d=drawable/transportcontrol_bg 01080a9e=drawable/unknown_image 01080a9f=drawable/unlock_default 01080aa0=drawable/unlock_halo 01080aa1=drawable/unlock_ring 01080aa2=drawable/unlock_wave 01080aa3=drawable/usb_cable_unknown_issue 01080aa4=drawable/vector_drawable_progress_bar_large 01080aa5=drawable/vector_drawable_progress_bar_medium 01080aa6=drawable/vector_drawable_progress_bar_small 01080aa7=drawable/vector_drawable_progress_indeterminate_horizontal 01080aa8=drawable/vector_notification_progress_indeterminate_horizontal 01080aa9=drawable/view_accessibility_focused 01080aaa=drawable/vpn_connected 01080aab=drawable/vpn_disconnected 01080aac=drawable/watch_switch_thumb_mtrl_14w 01080aad=drawable/watch_switch_thumb_mtrl_15w 01080aae=drawable/watch_switch_thumb_mtrl_16w 01080aaf=drawable/watch_switch_thumb_mtrl_17w 01080ab0=drawable/watch_switch_thumb_mtrl_18w 01080ab1=drawable/watch_switch_track_mtrl 01080ab2=drawable/work_mode_emergency_button_background 01080ab3=drawable/work_widget_mask_view_background 01090000=layout/activity_list_item 01090001=layout/expandable_list_content 01090002=layout/preference_category 01090003=layout/simple_list_item_1 01090004=layout/simple_list_item_2 01090005=layout/simple_list_item_checked 01090006=layout/simple_expandable_list_item_1 01090007=layout/simple_expandable_list_item_2 01090008=layout/simple_spinner_item 01090009=layout/simple_spinner_dropdown_item 0109000a=layout/simple_dropdown_item_1line 0109000b=layout/simple_gallery_item 0109000c=layout/test_list_item 0109000d=layout/two_line_list_item 0109000e=layout/browser_link_context_header 0109000f=layout/simple_list_item_single_choice 01090010=layout/simple_list_item_multiple_choice 01090011=layout/select_dialog_item 01090012=layout/select_dialog_singlechoice 01090013=layout/select_dialog_multichoice 01090014=layout/list_content 01090015=layout/simple_selectable_list_item 01090016=layout/simple_list_item_activated_1 01090017=layout/simple_list_item_activated_2 01090018=layout/accessibility_autoclick_scroll_panel 01090019=layout/accessibility_autoclick_type_panel 0109001a=layout/accessibility_button_chooser 0109001b=layout/accessibility_button_chooser_item 0109001c=layout/accessibility_enable_service_warning 0109001d=layout/accessibility_service_warning 0109001e=layout/accessibility_shortcut_chooser_item 0109001f=layout/action_bar_home 01090020=layout/action_bar_home_material 01090021=layout/action_bar_title_item 01090022=layout/action_bar_up_container 01090023=layout/action_menu_item_layout 01090024=layout/action_menu_layout 01090025=layout/action_mode_bar 01090026=layout/action_mode_close_item 01090027=layout/action_mode_close_item_material 01090028=layout/activity_chooser_view 01090029=layout/activity_chooser_view_list_item 0109002a=layout/activity_list 0109002b=layout/activity_list_item_2 0109002c=layout/adaptive_notification_wrapper 0109002d=layout/alert_dialog 0109002e=layout/alert_dialog_button_bar_leanback 0109002f=layout/alert_dialog_button_bar_material 01090030=layout/alert_dialog_holo 01090031=layout/alert_dialog_icon_button_watch 01090032=layout/alert_dialog_leanback 01090033=layout/alert_dialog_leanback_button_panel_side 01090034=layout/alert_dialog_material 01090035=layout/alert_dialog_progress 01090036=layout/alert_dialog_progress_holo 01090037=layout/alert_dialog_progress_material 01090038=layout/alert_dialog_title_material 01090039=layout/alert_dialog_title_watch 0109003a=layout/alert_dialog_watch 0109003b=layout/always_use_checkbox 0109003c=layout/am_compat_mode_dialog 0109003d=layout/app_anr_dialog 0109003e=layout/app_error_dialog 0109003f=layout/app_language_picker_locale_item 01090040=layout/app_language_picker_system_current 01090041=layout/app_language_picker_system_default 01090042=layout/app_not_authorized 01090043=layout/app_permission_item 01090044=layout/app_permission_item_money 01090045=layout/app_permission_item_old 01090046=layout/app_perms_summary 01090047=layout/auto_complete_list 01090048=layout/autofill_dataset_picker 01090049=layout/autofill_dataset_picker_fullscreen 0109004a=layout/autofill_dataset_picker_header_footer 0109004b=layout/autofill_fill_dialog 0109004c=layout/autofill_save 0109004d=layout/breadcrumbs_in_fragment 0109004e=layout/breadcrumbs_in_fragment_material 0109004f=layout/calendar_view 01090050=layout/car_alert_dialog 01090051=layout/car_alert_dialog_button_bar 01090052=layout/car_alert_dialog_title 01090053=layout/car_preference 01090054=layout/car_preference_category 01090055=layout/cascading_menu_item_layout 01090056=layout/cascading_menu_item_layout_material 01090057=layout/character_picker 01090058=layout/character_picker_button 01090059=layout/choose_account 0109005a=layout/choose_account_row 0109005b=layout/choose_account_type 0109005c=layout/choose_type_and_account 0109005d=layout/chooser_action_button 0109005e=layout/chooser_action_row 0109005f=layout/chooser_az_label_row 01090060=layout/chooser_dialog 01090061=layout/chooser_dialog_item 01090062=layout/chooser_grid 01090063=layout/chooser_grid_preview_file 01090064=layout/chooser_grid_preview_image 01090065=layout/chooser_grid_preview_text 01090066=layout/chooser_list_per_profile 01090067=layout/chooser_profile_row 01090068=layout/chooser_row 01090069=layout/chooser_row_direct_share 0109006a=layout/common_tab_settings 0109006b=layout/conversation_face_pile_layout 0109006c=layout/date_picker_dialog 0109006d=layout/date_picker_header_material 0109006e=layout/date_picker_legacy 0109006f=layout/date_picker_legacy_holo 01090070=layout/date_picker_material 01090071=layout/date_picker_month_item_material 01090072=layout/date_picker_view_animator_material 01090073=layout/day_picker_content_material 01090074=layout/default_navigation 01090075=layout/dialog_custom_title 01090076=layout/dialog_custom_title_holo 01090077=layout/dialog_custom_title_material 01090078=layout/dialog_title 01090079=layout/dialog_title_holo 0109007a=layout/dialog_title_icons 0109007b=layout/dialog_title_icons_holo 0109007c=layout/dialog_title_icons_material 0109007d=layout/dialog_title_material 0109007e=layout/expanded_menu_layout 0109007f=layout/floating_popup_close_overflow_button 01090080=layout/floating_popup_container 01090081=layout/floating_popup_menu_button 01090082=layout/floating_popup_open_overflow_button 01090083=layout/floating_popup_overflow_button 01090084=layout/fragment_bread_crumb_item 01090085=layout/fragment_bread_crumb_item_material 01090086=layout/fragment_bread_crumbs 01090087=layout/global_actions 01090088=layout/global_actions_item 01090089=layout/global_actions_silent_mode 0109008a=layout/grant_credentials_permission 0109008b=layout/harmful_app_warning_dialog 0109008c=layout/heavy_weight_switcher 0109008d=layout/icon_menu_item_layout 0109008e=layout/icon_menu_layout 0109008f=layout/immersive_mode_cling 01090090=layout/input_method 01090091=layout/input_method_extract_view 01090092=layout/input_method_nav_back 01090093=layout/input_method_nav_home_handle 01090094=layout/input_method_nav_ime_switcher 01090095=layout/input_method_navigation_bar 01090096=layout/input_method_navigation_layout 01090097=layout/input_method_switch_dialog_new 01090098=layout/input_method_switch_dialog_title 01090099=layout/input_method_switch_item 0109009a=layout/input_method_switch_item_divider 0109009b=layout/input_method_switch_item_header 0109009c=layout/input_method_switch_item_new 0109009d=layout/input_method_switcher_list_layout 0109009e=layout/js_prompt 0109009f=layout/keyboard_key_preview 010900a0=layout/keyboard_popup_keyboard 010900a1=layout/keyguard 010900a2=layout/language_picker_bilingual_item 010900a3=layout/language_picker_bilingual_section_header 010900a4=layout/language_picker_item 010900a5=layout/language_picker_section_header 010900a6=layout/launch_warning 010900a7=layout/list_content_simple 010900a8=layout/list_gestures_overlay 010900a9=layout/list_menu_item_checkbox 010900aa=layout/list_menu_item_fixed_size_icon 010900ab=layout/list_menu_item_icon 010900ac=layout/list_menu_item_layout 010900ad=layout/list_menu_item_radio 010900ae=layout/locale_picker_item 010900af=layout/media_controller 010900b0=layout/media_route_chooser_dialog 010900b1=layout/media_route_controller_dialog 010900b2=layout/media_route_list_item 010900b3=layout/menu_item 010900b4=layout/miniresolver 010900b5=layout/notification_2025_action_list 010900b6=layout/notification_2025_conversation_face_pile_layout 010900b7=layout/notification_2025_conversation_header 010900b8=layout/notification_2025_conversation_icon_container 010900b9=layout/notification_2025_expand_button 010900ba=layout/notification_2025_messaging_group 010900bb=layout/notification_2025_reply_history_container 010900bc=layout/notification_2025_right_icon 010900bd=layout/notification_2025_template_collapsed_base 010900be=layout/notification_2025_template_collapsed_call 010900bf=layout/notification_2025_template_collapsed_conversation 010900c0=layout/notification_2025_template_collapsed_media 010900c1=layout/notification_2025_template_collapsed_messaging 010900c2=layout/notification_2025_template_compact_heads_up_base 010900c3=layout/notification_2025_template_compact_heads_up_messaging 010900c4=layout/notification_2025_template_expanded_base 010900c5=layout/notification_2025_template_expanded_big_picture 010900c6=layout/notification_2025_template_expanded_big_text 010900c7=layout/notification_2025_template_expanded_call 010900c8=layout/notification_2025_template_expanded_conversation 010900c9=layout/notification_2025_template_expanded_inbox 010900ca=layout/notification_2025_template_expanded_media 010900cb=layout/notification_2025_template_expanded_messaging 010900cc=layout/notification_2025_template_expanded_progress 010900cd=layout/notification_2025_template_header 010900ce=layout/notification_2025_template_heads_up_base 010900cf=layout/notification_2025_text 010900d0=layout/notification_2025_title 010900d1=layout/notification_2025_top_line_views 010900d2=layout/notification_close_button 010900d3=layout/notification_expand_button 010900d4=layout/notification_intruder_content 010900d5=layout/notification_material_action 010900d6=layout/notification_material_action_emphasized 010900d7=layout/notification_material_action_emphasized_tombstone 010900d8=layout/notification_material_action_list 010900d9=layout/notification_material_action_tombstone 010900da=layout/notification_material_media_action 010900db=layout/notification_material_reply_text 010900dc=layout/notification_template_conversation_header 010900dd=layout/notification_template_conversation_icon_container 010900de=layout/notification_template_header 010900df=layout/notification_template_material_base 010900e0=layout/notification_template_material_big_base 010900e1=layout/notification_template_material_big_call 010900e2=layout/notification_template_material_big_media 010900e3=layout/notification_template_material_big_messaging 010900e4=layout/notification_template_material_big_picture 010900e5=layout/notification_template_material_big_text 010900e6=layout/notification_template_material_call 010900e7=layout/notification_template_material_compact_heads_up_base 010900e8=layout/notification_template_material_conversation 010900e9=layout/notification_template_material_heads_up_base 010900ea=layout/notification_template_material_inbox 010900eb=layout/notification_template_material_media 010900ec=layout/notification_template_material_messaging 010900ed=layout/notification_template_material_messaging_compact_heads_up 010900ee=layout/notification_template_material_progress 010900ef=layout/notification_template_messaging_group 010900f0=layout/notification_template_messaging_image_message 010900f1=layout/notification_template_messaging_text_message 010900f2=layout/notification_template_notification_progress_bar 010900f3=layout/notification_template_part_chronometer 010900f4=layout/notification_template_part_line1 010900f5=layout/notification_template_progress 010900f6=layout/notification_template_progressbar 010900f7=layout/notification_template_right_icon 010900f8=layout/notification_template_smart_reply_container 010900f9=layout/notification_template_text 010900fa=layout/notification_template_text_multiline 010900fb=layout/notification_top_line_views 010900fc=layout/number_picker 010900fd=layout/number_picker_material 010900fe=layout/number_picker_with_selector_wheel 010900ff=layout/overlay_display_window 01090100=layout/permissions_account_and_authtokentype 01090101=layout/permissions_package_list_item 01090102=layout/platlogo_layout 01090103=layout/popup_menu_header_item_layout 01090104=layout/popup_menu_item_layout 01090105=layout/popup_menu_item_layout_material 01090106=layout/power_dialog 01090107=layout/preference 01090108=layout/preference_category_holo 01090109=layout/preference_category_material 0109010a=layout/preference_child 0109010b=layout/preference_child_holo 0109010c=layout/preference_child_material 0109010d=layout/preference_dialog_edittext 0109010e=layout/preference_dialog_edittext_material 0109010f=layout/preference_dialog_seekbar 01090110=layout/preference_dialog_seekbar_material 01090111=layout/preference_header_item 01090112=layout/preference_header_item_material 01090113=layout/preference_holo 01090114=layout/preference_information 01090115=layout/preference_information_holo 01090116=layout/preference_information_material 01090117=layout/preference_list_content 01090118=layout/preference_list_content_material 01090119=layout/preference_list_content_single 0109011a=layout/preference_list_fragment 0109011b=layout/preference_list_fragment_material 0109011c=layout/preference_material 0109011d=layout/preference_widget_checkbox 0109011e=layout/preference_widget_seekbar 0109011f=layout/preference_widget_seekbar_material 01090120=layout/preference_widget_switch 01090121=layout/preferences 01090122=layout/progress_dialog 01090123=layout/progress_dialog_holo 01090124=layout/progress_dialog_material 01090125=layout/recent_apps_dialog 01090126=layout/recent_apps_icon 01090127=layout/remote_views_adapter_default_loading_view 01090128=layout/resolve_grid_item 01090129=layout/resolve_list_item 0109012a=layout/resolver_different_item_header 0109012b=layout/resolver_empty_states 0109012c=layout/resolver_list 0109012d=layout/resolver_list_per_profile 0109012e=layout/resolver_list_with_default 0109012f=layout/resolver_profile_tab_button 01090130=layout/restrictions_pin_challenge 01090131=layout/restrictions_pin_setup 01090132=layout/safe_mode 01090133=layout/screen 01090134=layout/screen_action_bar 01090135=layout/screen_custom_title 01090136=layout/screen_progress 01090137=layout/screen_simple 01090138=layout/screen_simple_overlay_action_mode 01090139=layout/screen_title 0109013a=layout/screen_title_icons 0109013b=layout/screen_toolbar 0109013c=layout/search_bar 0109013d=layout/search_dropdown_item_icons_2line 0109013e=layout/search_view 0109013f=layout/select_dialog 01090140=layout/select_dialog_holo 01090141=layout/select_dialog_item_holo 01090142=layout/select_dialog_item_material 01090143=layout/select_dialog_material 01090144=layout/select_dialog_multichoice_holo 01090145=layout/select_dialog_multichoice_material 01090146=layout/select_dialog_singlechoice_holo 01090147=layout/select_dialog_singlechoice_material 01090148=layout/shutdown_dialog 01090149=layout/side_fps_toast 0109014a=layout/simple_account_item 0109014b=layout/simple_dropdown_hint 0109014c=layout/simple_dropdown_item_2line 0109014d=layout/simple_list_item_2_single_choice 0109014e=layout/slice_grid 0109014f=layout/slice_message 01090150=layout/slice_message_local 01090151=layout/slice_remote_input 01090152=layout/slice_secondary_text 01090153=layout/slice_small_template 01090154=layout/slice_title 01090155=layout/sms_short_code_confirmation_dialog 01090156=layout/splash_screen_view 01090157=layout/ssl_certificate 01090158=layout/status_bar_latest_event_content 01090159=layout/subscription_item_layout 0109015a=layout/system_user_home 0109015b=layout/tab_content 0109015c=layout/tab_indicator 0109015d=layout/tab_indicator_holo 0109015e=layout/tab_indicator_material 0109015f=layout/tab_indicator_resolver 01090160=layout/text_drag_thumbnail 01090161=layout/text_edit_action_popup_text 01090162=layout/text_edit_no_paste_window 01090163=layout/text_edit_paste_window 01090164=layout/text_edit_side_no_paste_window 01090165=layout/text_edit_side_paste_window 01090166=layout/text_edit_suggestion_container 01090167=layout/text_edit_suggestion_container_material 01090168=layout/text_edit_suggestion_item 01090169=layout/text_edit_suggestion_item_material 0109016a=layout/text_edit_suggestions_window 0109016b=layout/textview_hint 0109016c=layout/thumbnail_background_view 0109016d=layout/time_picker_dialog 0109016e=layout/time_picker_header_material 0109016f=layout/time_picker_legacy 01090170=layout/time_picker_legacy_material 01090171=layout/time_picker_material 01090172=layout/time_picker_text_input_material 01090173=layout/tooltip 01090174=layout/transient_notification 01090175=layout/transient_notification_with_icon 01090176=layout/twelve_key_entry 01090177=layout/typing_filter 01090178=layout/unsupported_compile_sdk_dialog_content 01090179=layout/unsupported_display_size_dialog_content 0109017a=layout/user_switching_dialog 0109017b=layout/voice_interaction_session 0109017c=layout/watch_base_error_dialog 0109017d=layout/watch_base_error_dialog_title 0109017e=layout/web_runtime 0109017f=layout/web_text_view_dropdown 01090180=layout/webview_find 01090181=layout/webview_select_singlechoice 01090182=layout/work_widget_mask_view 01090183=layout/year_label_text_view 01090184=layout/zoom_browser_accessory_buttons 01090185=layout/zoom_container 01090186=layout/zoom_controls 01090187=layout/zoom_magnify 010a0000=anim/fade_in 010a0001=anim/fade_out 010a0002=anim/slide_in_left 010a0003=anim/slide_out_right 010a0004=anim/accelerate_decelerate_interpolator 010a0005=anim/accelerate_interpolator 010a0006=anim/decelerate_interpolator 010a0007=anim/anticipate_interpolator 010a0008=anim/overshoot_interpolator 010a0009=anim/anticipate_overshoot_interpolator 010a000a=anim/bounce_interpolator 010a000b=anim/linear_interpolator 010a000c=anim/cycle_interpolator 010a000d=anim/activity_close_enter 010a000e=anim/activity_close_exit 010a000f=anim/activity_open_enter 010a0010=anim/activity_open_exit 010a0011=anim/activity_translucent_close_exit 010a0012=anim/activity_translucent_open_enter 010a0013=anim/app_starting_exit 010a0014=anim/btn_checkbox_to_checked_box_inner_merged_animation 010a0015=anim/btn_checkbox_to_checked_box_outer_merged_animation 010a0016=anim/btn_checkbox_to_checked_icon_null_animation 010a0017=anim/btn_checkbox_to_unchecked_box_inner_merged_animation 010a0018=anim/btn_checkbox_to_unchecked_check_path_merged_animation 010a0019=anim/btn_checkbox_to_unchecked_icon_null_animation 010a001a=anim/btn_radio_to_off_mtrl_dot_group_animation 010a001b=anim/btn_radio_to_off_mtrl_ring_outer_animation 010a001c=anim/btn_radio_to_off_mtrl_ring_outer_path_animation 010a001d=anim/btn_radio_to_on_mtrl_dot_group_animation 010a001e=anim/btn_radio_to_on_mtrl_ring_outer_animation 010a001f=anim/btn_radio_to_on_mtrl_ring_outer_path_animation 010a0020=anim/button_state_list_anim_material 010a0021=anim/cross_profile_apps_thumbnail_enter 010a0022=anim/date_picker_fade_in_material 010a0023=anim/date_picker_fade_out_material 010a0024=anim/dialog_enter 010a0025=anim/dialog_exit 010a0026=anim/dream_activity_close_exit 010a0027=anim/dream_activity_open_enter 010a0028=anim/dream_activity_open_exit 010a0029=anim/fast_fade_in 010a002a=anim/fast_fade_out 010a002b=anim/flat_button_state_list_anim_material 010a002c=anim/ft_avd_toarrow_rectangle_1_animation 010a002d=anim/ft_avd_toarrow_rectangle_1_pivot_0_animation 010a002e=anim/ft_avd_toarrow_rectangle_1_pivot_animation 010a002f=anim/ft_avd_toarrow_rectangle_2_animation 010a0030=anim/ft_avd_toarrow_rectangle_2_pivot_0_animation 010a0031=anim/ft_avd_toarrow_rectangle_2_pivot_animation 010a0032=anim/ft_avd_toarrow_rectangle_3_animation 010a0033=anim/ft_avd_toarrow_rectangle_3_pivot_0_animation 010a0034=anim/ft_avd_toarrow_rectangle_3_pivot_animation 010a0035=anim/ft_avd_toarrow_rectangle_4_animation 010a0036=anim/ft_avd_toarrow_rectangle_5_animation 010a0037=anim/ft_avd_toarrow_rectangle_6_animation 010a0038=anim/ft_avd_toarrow_rectangle_path_1_animation 010a0039=anim/ft_avd_toarrow_rectangle_path_2_animation 010a003a=anim/ft_avd_toarrow_rectangle_path_3_animation 010a003b=anim/ft_avd_toarrow_rectangle_path_4_animation 010a003c=anim/ft_avd_toarrow_rectangle_path_5_animation 010a003d=anim/ft_avd_toarrow_rectangle_path_6_animation 010a003e=anim/ft_avd_tooverflow_rectangle_1_animation 010a003f=anim/ft_avd_tooverflow_rectangle_1_pivot_animation 010a0040=anim/ft_avd_tooverflow_rectangle_2_animation 010a0041=anim/ft_avd_tooverflow_rectangle_2_pivot_animation 010a0042=anim/ft_avd_tooverflow_rectangle_3_animation 010a0043=anim/ft_avd_tooverflow_rectangle_3_pivot_animation 010a0044=anim/ft_avd_tooverflow_rectangle_path_1_animation 010a0045=anim/ft_avd_tooverflow_rectangle_path_2_animation 010a0046=anim/ft_avd_tooverflow_rectangle_path_3_animation 010a0047=anim/grow_fade_in 010a0048=anim/grow_fade_in_center 010a0049=anim/grow_fade_in_from_bottom 010a004a=anim/ic_bluetooth_transient_animation_0 010a004b=anim/ic_bluetooth_transient_animation_1 010a004c=anim/ic_bluetooth_transient_animation_2 010a004d=anim/ic_hotspot_transient_animation_0 010a004e=anim/ic_hotspot_transient_animation_1 010a004f=anim/ic_hotspot_transient_animation_2 010a0050=anim/ic_hotspot_transient_animation_3 010a0051=anim/ic_signal_wifi_transient_animation_0 010a0052=anim/ic_signal_wifi_transient_animation_1 010a0053=anim/ic_signal_wifi_transient_animation_2 010a0054=anim/ic_signal_wifi_transient_animation_3 010a0055=anim/ic_signal_wifi_transient_animation_4 010a0056=anim/ic_signal_wifi_transient_animation_5 010a0057=anim/ic_signal_wifi_transient_animation_6 010a0058=anim/ic_signal_wifi_transient_animation_7 010a0059=anim/ic_signal_wifi_transient_animation_8 010a005a=anim/input_method_enter 010a005b=anim/input_method_exit 010a005c=anim/input_method_extract_enter 010a005d=anim/input_method_extract_exit 010a005e=anim/input_method_fancy_enter 010a005f=anim/input_method_fancy_exit 010a0060=anim/launch_task_behind_source 010a0061=anim/launch_task_behind_target 010a0062=anim/lock_screen_behind_enter 010a0063=anim/lock_screen_behind_enter_fade_in 010a0064=anim/lock_screen_behind_enter_subtle 010a0065=anim/lock_screen_behind_enter_wallpaper 010a0066=anim/lock_screen_enter 010a0067=anim/lock_screen_exit 010a0068=anim/lock_screen_wallpaper_exit 010a0069=anim/options_panel_enter 010a006a=anim/options_panel_exit 010a006b=anim/overlay_task_fragment_change 010a006c=anim/overlay_task_fragment_close_to_bottom 010a006d=anim/overlay_task_fragment_close_to_left 010a006e=anim/overlay_task_fragment_close_to_right 010a006f=anim/overlay_task_fragment_close_to_top 010a0070=anim/overlay_task_fragment_open_from_bottom 010a0071=anim/overlay_task_fragment_open_from_left 010a0072=anim/overlay_task_fragment_open_from_right 010a0073=anim/overlay_task_fragment_open_from_top 010a0074=anim/popup_enter_material 010a0075=anim/popup_exit_material 010a0076=anim/progress_indeterminate_horizontal_rect1 010a0077=anim/progress_indeterminate_horizontal_rect2 010a0078=anim/progress_indeterminate_material 010a0079=anim/progress_indeterminate_rotation_material 010a007a=anim/push_down_in 010a007b=anim/push_down_in_no_alpha 010a007c=anim/push_down_out 010a007d=anim/push_down_out_no_alpha 010a007e=anim/push_up_in 010a007f=anim/push_up_out 010a0080=anim/recent_enter 010a0081=anim/recent_exit 010a0082=anim/recents_fade_in 010a0083=anim/recents_fade_out 010a0084=anim/resolver_close_anim 010a0085=anim/resolver_launch_anim 010a0086=anim/rotation_animation_enter 010a0087=anim/rotation_animation_jump_exit 010a0088=anim/rotation_animation_xfade_exit 010a0089=anim/rounded_window_enter 010a008a=anim/rounded_window_exit 010a008b=anim/screen_rotate_0_enter 010a008c=anim/screen_rotate_0_exit 010a008d=anim/screen_rotate_180_enter 010a008e=anim/screen_rotate_180_exit 010a008f=anim/screen_rotate_180_frame 010a0090=anim/screen_rotate_alpha 010a0091=anim/screen_rotate_finish_enter 010a0092=anim/screen_rotate_finish_exit 010a0093=anim/screen_rotate_finish_frame 010a0094=anim/screen_rotate_minus_90_enter 010a0095=anim/screen_rotate_minus_90_exit 010a0096=anim/screen_rotate_plus_90_enter 010a0097=anim/screen_rotate_plus_90_exit 010a0098=anim/screen_rotate_start_enter 010a0099=anim/screen_rotate_start_exit 010a009a=anim/screen_rotate_start_frame 010a009b=anim/screen_user_enter 010a009c=anim/screen_user_exit 010a009d=anim/search_bar_enter 010a009e=anim/search_bar_exit 010a009f=anim/seekbar_thumb_pressed_to_unpressed_thumb_animation 010a00a0=anim/seekbar_thumb_unpressed_to_pressed_thumb_0_animation 010a00a1=anim/shrink_fade_out 010a00a2=anim/shrink_fade_out_center 010a00a3=anim/shrink_fade_out_from_bottom 010a00a4=anim/slide_in_child_bottom 010a00a5=anim/slide_in_enter_micro 010a00a6=anim/slide_in_exit_micro 010a00a7=anim/slide_in_right 010a00a8=anim/slide_in_up 010a00a9=anim/slide_out_down 010a00aa=anim/slide_out_left 010a00ab=anim/slide_out_micro 010a00ac=anim/slow_fade_in 010a00ad=anim/submenu_enter 010a00ae=anim/submenu_exit 010a00af=anim/swipe_window_enter 010a00b0=anim/swipe_window_exit 010a00b1=anim/task_close_enter 010a00b2=anim/task_close_exit 010a00b3=anim/task_fragment_clear_top_close_enter 010a00b4=anim/task_fragment_clear_top_close_exit 010a00b5=anim/task_fragment_clear_top_open_enter 010a00b6=anim/task_fragment_clear_top_open_exit 010a00b7=anim/task_fragment_close_enter 010a00b8=anim/task_fragment_close_exit 010a00b9=anim/task_fragment_open_enter 010a00ba=anim/task_fragment_open_exit 010a00bb=anim/task_open_enter 010a00bc=anim/task_open_enter_cross_profile_apps 010a00bd=anim/task_open_exit 010a00be=anim/toast_enter 010a00bf=anim/toast_exit 010a00c0=anim/tooltip_enter 010a00c1=anim/tooltip_exit 010a00c2=anim/translucent_enter 010a00c3=anim/translucent_exit 010a00c4=anim/voice_activity_close_enter 010a00c5=anim/voice_activity_close_exit 010a00c6=anim/voice_activity_open_enter 010a00c7=anim/voice_activity_open_exit 010a00c8=anim/voice_layer_enter 010a00c9=anim/voice_layer_exit 010a00ca=anim/wallpaper_close_enter 010a00cb=anim/wallpaper_close_exit 010a00cc=anim/wallpaper_enter 010a00cd=anim/wallpaper_exit 010a00ce=anim/wallpaper_intra_close_enter 010a00cf=anim/wallpaper_intra_close_exit 010a00d0=anim/wallpaper_intra_open_enter 010a00d1=anim/wallpaper_intra_open_exit 010a00d2=anim/wallpaper_open_enter 010a00d3=anim/wallpaper_open_exit 010a00d4=anim/window_move_from_decor 010a00d5=anim/grow_fade_in_center 010a00d6=anim/grow_fade_in_from_bottom 010a00d7=anim/ic_bluetooth_transient_animation_0 010a00d8=anim/ic_bluetooth_transient_animation_1 010a00d9=anim/ic_bluetooth_transient_animation_2 010a00da=anim/ic_hotspot_transient_animation_0 010a00db=anim/ic_hotspot_transient_animation_1 010a00dc=anim/ic_hotspot_transient_animation_2 010a00dd=anim/ic_hotspot_transient_animation_3 010a00de=anim/ic_signal_wifi_transient_animation_0 010a00df=anim/ic_signal_wifi_transient_animation_1 010a00e0=anim/ic_signal_wifi_transient_animation_2 010a00e1=anim/ic_signal_wifi_transient_animation_3 010a00e2=anim/ic_signal_wifi_transient_animation_4 010a00e3=anim/ic_signal_wifi_transient_animation_5 010a00e4=anim/ic_signal_wifi_transient_animation_6 010a00e5=anim/ic_signal_wifi_transient_animation_7 010a00e6=anim/ic_signal_wifi_transient_animation_8 010a00e7=anim/input_method_enter 010a00e8=anim/input_method_exit 010a00e9=anim/input_method_extract_enter 010a00ea=anim/input_method_extract_exit 010a00eb=anim/input_method_fancy_enter 010a00ec=anim/input_method_fancy_exit 010a00ed=anim/launch_task_behind_source 010a00ee=anim/launch_task_behind_target 010a00ef=anim/lock_in 010a00f0=anim/lock_lock 010a00f1=anim/lock_scanning 010a00f2=anim/lock_screen_behind_enter 010a00f3=anim/lock_screen_behind_enter_fade_in 010a00f4=anim/lock_screen_behind_enter_wallpaper 010a00f5=anim/lock_screen_enter 010a00f6=anim/lock_screen_exit 010a00f7=anim/lock_screen_wallpaper_exit 010a00f8=anim/lock_to_error 010a00f9=anim/lock_unlock 010a00fa=anim/options_panel_enter 010a00fb=anim/options_panel_exit 010a00fc=anim/popup_enter_material 010a00fd=anim/popup_exit_material 010a00fe=anim/progress_indeterminate_horizontal_rect1 010a00ff=anim/progress_indeterminate_horizontal_rect2 010a0100=anim/progress_indeterminate_material 010a0101=anim/progress_indeterminate_rotation_material 010a0102=anim/push_down_in 010a0103=anim/push_down_in_no_alpha 010a0104=anim/push_down_out 010a0105=anim/push_down_out_no_alpha 010a0106=anim/push_up_in 010a0107=anim/push_up_out 010a0108=anim/recent_enter 010a0109=anim/recent_exit 010a010a=anim/recents_fade_in 010a010b=anim/recents_fade_out 010a010c=anim/resolver_close_anim 010a010d=anim/resolver_launch_anim 010a010e=anim/rotation_animation_enter 010a010f=anim/rotation_animation_jump_exit 010a0110=anim/rotation_animation_xfade_exit 010a0111=anim/screen_rotate_0_enter 010a0112=anim/screen_rotate_0_exit 010a0113=anim/screen_rotate_0_frame 010a0114=anim/screen_rotate_180_enter 010a0115=anim/screen_rotate_180_exit 010a0116=anim/screen_rotate_180_frame 010a0117=anim/screen_rotate_finish_enter 010a0118=anim/screen_rotate_finish_exit 010a0119=anim/screen_rotate_finish_frame 010a011a=anim/screen_rotate_minus_90_enter 010a011b=anim/screen_rotate_minus_90_exit 010a011c=anim/screen_rotate_minus_90_frame 010a011d=anim/screen_rotate_plus_90_enter 010a011e=anim/screen_rotate_plus_90_exit 010a011f=anim/screen_rotate_plus_90_frame 010a0120=anim/screen_rotate_start_enter 010a0121=anim/screen_rotate_start_exit 010a0122=anim/screen_rotate_start_frame 010a0123=anim/screen_user_enter 010a0124=anim/screen_user_exit 010a0125=anim/search_bar_enter 010a0126=anim/search_bar_exit 010a0127=anim/seekbar_thumb_pressed_to_unpressed_thumb_animation 010a0128=anim/seekbar_thumb_unpressed_to_pressed_thumb_0_animation 010a0129=anim/shrink_fade_out 010a012a=anim/shrink_fade_out_center 010a012b=anim/shrink_fade_out_from_bottom 010a012c=anim/slide_in_child_bottom 010a012d=anim/slide_in_enter_micro 010a012e=anim/slide_in_exit_micro 010a012f=anim/slide_in_right 010a0130=anim/slide_in_up 010a0131=anim/slide_out_down 010a0132=anim/slide_out_left 010a0133=anim/slide_out_micro 010a0134=anim/slow_fade_in 010a0135=anim/submenu_enter 010a0136=anim/submenu_exit 010a0137=anim/swipe_window_enter 010a0138=anim/swipe_window_exit 010a0139=anim/task_close_enter 010a013a=anim/task_close_exit 010a013b=anim/task_open_enter 010a013c=anim/task_open_enter_cross_profile_apps 010a013d=anim/task_open_exit 010a013e=anim/toast_enter 010a013f=anim/toast_exit 010a0140=anim/tooltip_enter 010a0141=anim/tooltip_exit 010a0142=anim/translucent_enter 010a0143=anim/translucent_exit 010a0144=anim/voice_activity_close_enter 010a0145=anim/voice_activity_close_exit 010a0146=anim/voice_activity_open_enter 010a0147=anim/voice_activity_open_exit 010a0148=anim/voice_layer_enter 010a0149=anim/voice_layer_exit 010a014a=anim/wallpaper_close_enter 010a014b=anim/wallpaper_close_exit 010a014c=anim/wallpaper_enter 010a014d=anim/wallpaper_exit 010a014e=anim/wallpaper_intra_close_enter 010a014f=anim/wallpaper_intra_close_exit 010a0150=anim/wallpaper_intra_open_enter 010a0151=anim/wallpaper_intra_open_exit 010a0152=anim/wallpaper_open_enter 010a0153=anim/wallpaper_open_exit 010a0154=anim/window_move_from_decor 010b0000=animator/fade_in 010b0001=animator/fade_out 010b0002=animator/fragment_close_enter 010b0003=animator/fragment_close_exit 010b0004=animator/fragment_fade_enter 010b0005=animator/fragment_fade_exit 010b0006=animator/fragment_open_enter 010b0007=animator/fragment_open_exit 010b0008=animator/leanback_setup_fragment_close_enter 010b0009=animator/leanback_setup_fragment_close_exit 010b000a=animator/leanback_setup_fragment_open_enter 010b000b=animator/leanback_setup_fragment_open_exit 010b000c=xml/time_zones_by_country 010c0000=interpolator/accelerate_quad 010c0001=interpolator/decelerate_quad 010c0002=interpolator/accelerate_cubic 010c0003=interpolator/decelerate_cubic 010c0004=interpolator/accelerate_quint 010c0005=interpolator/decelerate_quint 010c0006=interpolator/accelerate_decelerate 010c0007=interpolator/anticipate 010c0008=interpolator/overshoot 010c0009=interpolator/anticipate_overshoot 010c000a=interpolator/bounce 010c000b=interpolator/linear 010c000c=interpolator/cycle 010c000d=interpolator/fast_out_slow_in 010c000e=interpolator/linear_out_slow_in 010c000f=interpolator/fast_out_linear_in 010c0010=interpolator/accelerate_quart 010c0011=interpolator/activity_close_dim 010c0012=interpolator/aggressive_ease 010c0013=interpolator/btn_checkbox_checked_mtrl_animation_interpolator_0 010c0014=interpolator/btn_checkbox_checked_mtrl_animation_interpolator_1 010c0015=interpolator/btn_checkbox_unchecked_mtrl_animation_interpolator_0 010c0016=interpolator/btn_checkbox_unchecked_mtrl_animation_interpolator_1 010c0017=interpolator/btn_radio_to_off_mtrl_animation_interpolator_0 010c0018=interpolator/btn_radio_to_on_mtrl_animation_interpolator_0 010c0019=interpolator/decelerate_quart 010c001a=interpolator/fast_out_extra_slow_in 010c001b=interpolator/emphasized 010c001c=interpolator/emphasized_accelerate 010c001d=interpolator/emphasized_decelerate 010c001e=interpolator/ft_avd_toarrow_animation_interpolator_0 010c001f=interpolator/ft_avd_toarrow_animation_interpolator_1 010c0020=interpolator/ft_avd_toarrow_animation_interpolator_2 010c0021=interpolator/ft_avd_toarrow_animation_interpolator_3 010c0022=interpolator/ft_avd_toarrow_animation_interpolator_4 010c0023=interpolator/ft_avd_toarrow_animation_interpolator_5 010c0024=interpolator/ft_avd_toarrow_animation_interpolator_6 010c0025=interpolator/launch_task_behind_source_scale_1 010c0026=interpolator/launch_task_behind_source_scale_2 010c0027=interpolator/launch_task_behind_target_ydelta 010c0028=interpolator/launch_task_micro_alpha 010c0029=interpolator/launch_task_micro_ydelta 010c002a=interpolator/progress_indeterminate_horizontal_rect1_scalex 010c002b=interpolator/progress_indeterminate_horizontal_rect1_translatex 010c002c=interpolator/progress_indeterminate_horizontal_rect2_scalex 010c002d=interpolator/progress_indeterminate_horizontal_rect2_translatex 010c002e=interpolator/progress_indeterminate_rotation_interpolator 010c002f=interpolator/rounded_window_interpolator 010c0030=interpolator/screen_rotation 010c0031=interpolator/screen_rotation_alpha_in 010c0032=interpolator/screen_rotation_alpha_out 010c0033=interpolator/standard 010c0034=interpolator/standard_accelerate 010c0035=interpolator/standard_decelerate 010c0036=interpolator/transient_interpolator 010c0037=interpolator/trim_end_interpolator 010c0038=interpolator/trim_offset_interpolator 010c0039=interpolator/trim_start_interpolator 010d0000=mipmap/sym_def_app_icon 010d0001=mipmap/sym_def_app_icon_foreground 010d0002=mipmap/sym_def_app_icon_maskable 010d0003=mipmap/sym_def_app_icon_maskable 010d0004=bool/config_bypass_keyguard_if_slider_open 010d0005=bool/config_automatic_brightness_available 010d0006=bool/config_annoy_dianne 010d0007=bool/config_unplugTurnsOnScreen 010d0008=bool/config_animateScreenLights 010d0009=bool/config_deskDockEnablesAccelerometer 010d000a=bool/config_carDockEnablesAccelerometer 010d000b=bool/config_batterySdCardAccessibility 010d000c=bool/config_use_strict_phone_number_comparation 010d000d=bool/config_disableMenuKeyInLockScreen 010d000e=bool/config_swipeDisambiguation 010d000f=bool/config_filterTouchEvents 010d0010=bool/config_filterJumpyTouchEvents 010d0011=bool/config_bluetooth_sco_off_call 010d0012=bool/config_sip_wifi_only 010d0013=bool/skip_restoring_network_selection 010d0014=bool/lockscreen_isPortrait 010e0000=integer/config_shortAnimTime 010e0001=integer/config_mediumAnimTime 010e0002=integer/config_longAnimTime 010e0003=integer/status_bar_notification_info_maxnum 010e0004=integer/auto_data_switch_availability_stability_time_threshold_millis 010e0005=integer/auto_data_switch_availability_switchback_stability_time_threshold_millis 010e0006=integer/auto_data_switch_performance_stability_time_threshold_millis 010e0007=integer/auto_data_switch_score_tolerance 010e0008=integer/auto_data_switch_validation_max_retry 010e0009=integer/autofill_max_visible_datasets 010e000a=integer/bugreport_state_hide 010e000b=integer/bugreport_state_show 010e000c=integer/bugreport_state_unknown 010e000d=integer/button_pressed_animation_delay 010e000e=integer/button_pressed_animation_duration 010e000f=integer/config_MaxConcurrentDownloadsAllowed 010e0010=integer/config_accessibilityColorMode 010e0011=integer/config_accumulatedBatteryUsageStatsSpanSize 010e0012=integer/config_activeTaskDurationHours 010e0013=integer/config_activityDefaultDur 010e0014=integer/config_activityShortDur 010e0015=integer/config_aggregatedPowerStatsSpanDuration 010e0016=integer/config_alertDialogController 010e0017=integer/config_allowedUnprivilegedKeepalivePerUid 010e0018=integer/config_am_tieredCachedAdjUiTierSize 010e0019=integer/config_app_exit_info_history_list_size 010e001a=integer/config_attentionMaximumExtension 010e001b=integer/config_attentiveTimeout 010e001c=integer/config_attentiveWarningDuration 010e001d=integer/config_audio_alarm_min_vol 010e001e=integer/config_audio_notif_vol_default 010e001f=integer/config_audio_notif_vol_steps 010e0020=integer/config_audio_ring_vol_default 010e0021=integer/config_audio_ring_vol_steps 010e0022=integer/config_autoBrightnessBrighteningLightDebounce 010e0023=integer/config_autoBrightnessDarkeningLightDebounce 010e0024=integer/config_autoBrightnessInitialLightSensorRate 010e0025=integer/config_autoBrightnessLightSensorRate 010e0026=integer/config_autoBrightnessShortTermModelTimeout 010e0027=integer/config_autoGroupAtCount 010e0028=integer/config_autoPowerModeAnyMotionSensor 010e0029=integer/config_autoPowerModeThresholdAngle 010e002a=integer/config_backgroundUserScheduledStopTimeSecs 010e002b=integer/config_batteryHistoryStorageSize 010e002c=integer/config_batterySaver_full_locationMode 010e002d=integer/config_batterySaver_full_soundTriggerMode 010e002e=integer/config_bg_current_drain_exempted_types 010e002f=integer/config_bg_current_drain_location_min_duration 010e0030=integer/config_bg_current_drain_media_playback_min_duration 010e0031=integer/config_bg_current_drain_power_components 010e0032=integer/config_bg_current_drain_types_to_bg_restricted 010e0033=integer/config_bg_current_drain_types_to_restricted_bucket 010e0034=integer/config_bg_current_drain_window 010e0035=integer/config_bluetooth_idle_cur_ma 010e0036=integer/config_bluetooth_operating_voltage_mv 010e0037=integer/config_bluetooth_rx_cur_ma 010e0038=integer/config_bluetooth_tx_cur_ma 010e0039=integer/config_brightness_ramp_rate_fast 010e003a=integer/config_brightness_ramp_rate_slow 010e003b=integer/config_burnInProtectionMaxHorizontalOffset 010e003c=integer/config_burnInProtectionMaxRadius 010e003d=integer/config_burnInProtectionMaxVerticalOffset 010e003e=integer/config_burnInProtectionMinHorizontalOffset 010e003f=integer/config_burnInProtectionMinVerticalOffset 010e0040=integer/config_cameraLaunchGestureSensorType 010e0041=integer/config_cameraLiftTriggerSensorType 010e0042=integer/config_cameraPrivacyLightAlsAveragingIntervalMillis 010e0043=integer/config_carDockKeepsScreenOn 010e0044=integer/config_carDockRotation 010e0045=integer/config_cdma_3waycall_flash_delay 010e0046=integer/config_chooser_max_targets_per_row 010e0047=integer/config_criticalBatteryWarningLevel 010e0048=integer/config_cursorWindowSize 010e0049=integer/config_customizedMaxCachedProcesses 010e004a=integer/config_datagram_wait_for_connected_state_for_last_message_timeout_millis 010e004b=integer/config_datagram_wait_for_connected_state_timeout_millis 010e004c=integer/config_datause_notification_type 010e004d=integer/config_datause_polling_period_sec 010e004e=integer/config_datause_threshold_bytes 010e004f=integer/config_datause_throttle_kbitsps 010e0050=integer/config_debugSystemServerPssThresholdBytes 010e0051=integer/config_defaultActionModeHideDurationMillis 010e0052=integer/config_defaultAlarmVibrationIntensity 010e0053=integer/config_defaultAnalogClockSecondsHandFps 010e0054=integer/config_defaultBinderHeavyHitterAutoSamplerBatchSize 010e0055=integer/config_defaultBinderHeavyHitterWatcherBatchSize 010e0056=integer/config_defaultDisplayDefaultColorMode 010e0057=integer/config_defaultHapticFeedbackIntensity 010e0058=integer/config_defaultKeyboardVibrationIntensity 010e0059=integer/config_defaultMaxDurationBetweenUndimsMillis 010e005a=integer/config_defaultMediaVibrationIntensity 010e005b=integer/config_defaultMinEmergencyGestureTapDurationMillis 010e005c=integer/config_defaultNightDisplayAutoMode 010e005d=integer/config_defaultNightDisplayCustomEndTime 010e005e=integer/config_defaultNightDisplayCustomStartTime 010e005f=integer/config_defaultNightMode 010e0060=integer/config_defaultNotificationLedOff 010e0061=integer/config_defaultNotificationLedOn 010e0062=integer/config_defaultNotificationVibrationIntensity 010e0063=integer/config_defaultPeakRefreshRate 010e0064=integer/config_defaultPictureInPictureGravity 010e0065=integer/config_defaultPreventScreenTimeoutForMillis 010e0066=integer/config_defaultRefreshRate 010e0067=integer/config_defaultRefreshRateInHbmHdr 010e0068=integer/config_defaultRefreshRateInHbmSunlight 010e0069=integer/config_defaultRefreshRateInZone 010e006a=integer/config_defaultRingVibrationIntensity 010e006b=integer/config_defaultUiModeType 010e006c=integer/config_defaultUndimsRequired 010e006d=integer/config_defaultVibrationAmplitude 010e006e=integer/config_default_cellular_usage_setting 010e006f=integer/config_delay_for_ims_dereg_millis 010e0070=integer/config_demo_pointing_aligned_duration_millis 010e0071=integer/config_demo_pointing_not_aligned_duration_millis 010e0072=integer/config_deskDockKeepsScreenOn 010e0073=integer/config_deskDockRotation 010e0074=integer/config_deviceStateConcurrentRearDisplay 010e0075=integer/config_deviceStateRearDisplay 010e0076=integer/config_displayWhiteBalanceBrightnessFilterHorizon 010e0077=integer/config_displayWhiteBalanceBrightnessSensorRate 010e0078=integer/config_displayWhiteBalanceColorTemperatureDefault 010e0079=integer/config_displayWhiteBalanceColorTemperatureFilterHorizon 010e007a=integer/config_displayWhiteBalanceColorTemperatureMax 010e007b=integer/config_displayWhiteBalanceColorTemperatureMin 010e007c=integer/config_displayWhiteBalanceColorTemperatureSensorRate 010e007d=integer/config_displayWhiteBalanceDecreaseDebounce 010e007e=integer/config_displayWhiteBalanceDisplayNominalWhiteCct 010e007f=integer/config_displayWhiteBalanceIncreaseDebounce 010e0080=integer/config_displayWhiteBalanceTransitionTime 010e0081=integer/config_displayWhiteBalanceTransitionTimeDecrease 010e0082=integer/config_displayWhiteBalanceTransitionTimeIncrease 010e0083=integer/config_dockedStackDividerSnapMode 010e0084=integer/config_doublePressOnPowerBehavior 010e0085=integer/config_doublePressOnStemPrimaryBehavior 010e0086=integer/config_doubleTapMinTimeMillis 010e0087=integer/config_doubleTapOnHomeBehavior 010e0088=integer/config_doubleTapPowerGestureMode 010e0089=integer/config_doubleTapPowerGestureMultiTargetDefaultAction 010e008a=integer/config_doubleTapTimeoutMillis 010e008b=integer/config_doublelineClockDefault 010e008c=integer/config_downloadDataDirLowSpaceThreshold 010e008d=integer/config_downloadDataDirSize 010e008e=integer/config_dozeWakeLockScreenDebounce 010e008f=integer/config_drawLockTimeoutMillis 010e0090=integer/config_dreamCloseAnimationDuration 010e0091=integer/config_dreamOpenAnimationDuration 010e0092=integer/config_dreamsBatteryLevelDrainCutoff 010e0093=integer/config_dreamsBatteryLevelMinimumWhenNotPowered 010e0094=integer/config_dreamsBatteryLevelMinimumWhenPowered 010e0095=integer/config_dropboxLowPriorityBroadcastRateLimitPeriod 010e0096=integer/config_dynamicPowerSavingsDefaultDisableThreshold 010e0097=integer/config_emergency_call_wait_for_connection_timeout_millis 010e0098=integer/config_esim_bootstrap_data_limit_bytes 010e0099=integer/config_externalDisplayPeakHeight 010e009a=integer/config_externalDisplayPeakRefreshRate 010e009b=integer/config_externalDisplayPeakWidth 010e009c=integer/config_extraFreeKbytesAbsolute 010e009d=integer/config_extraFreeKbytesAdjust 010e009e=integer/config_faceMaxTemplatesPerUser 010e009f=integer/config_fingerprintMaxTemplatesPerUser 010e00a0=integer/config_fixedRefreshRateInHighZone 010e00a1=integer/config_flipToScreenOffMaxLatencyMicros 010e00a2=integer/config_globalActionsKeyTimeout 010e00a3=integer/config_hotwordDetectedResultMaxBundleSize 010e00a4=integer/config_hoverTapTimeoutMillis 010e00a5=integer/config_hsumBootStrategy 010e00a6=integer/config_immersive_mode_confirmation_panic 010e00a7=integer/config_jobSchedulerBackgroundJobsDelay 010e00a8=integer/config_jobSchedulerIdleWindowSlop 010e00a9=integer/config_jobSchedulerInactivityIdleThreshold 010e00aa=integer/config_jobSchedulerInactivityIdleThresholdOnStablePower 010e00ab=integer/config_jobSchedulerUserGracePeriod 010e00ac=integer/config_jumpTapTimeoutMillis 010e00ad=integer/config_keepPreloadsMinDays 010e00ae=integer/config_keyChordPowerVolumeUp 010e00af=integer/config_keyboardBacklightTimeoutMs 010e00b0=integer/config_keyguardDrawnTimeout 010e00b1=integer/config_letterboxActivityCornersRadius 010e00b2=integer/config_letterboxBackgroundType 010e00b3=integer/config_letterboxDefaultPositionForBookModeReachability 010e00b4=integer/config_letterboxDefaultPositionForHorizontalReachability 010e00b5=integer/config_letterboxDefaultPositionForTabletopModeReachability 010e00b6=integer/config_letterboxDefaultPositionForVerticalReachability 010e00b7=integer/config_lidKeyboardAccessibility 010e00b8=integer/config_lidNavigationAccessibility 010e00b9=integer/config_lidOpenRotation 010e00ba=integer/config_lightSensorWarmupTime 010e00bb=integer/config_lockSoundVolumeDb 010e00bc=integer/config_longPressOnBackBehavior 010e00bd=integer/config_longPressOnHomeBehavior 010e00be=integer/config_longPressOnPowerBehavior 010e00bf=integer/config_longPressOnPowerDurationMs 010e00c0=integer/config_longPressOnStemPrimaryBehavior 010e00c1=integer/config_lowBatteryAutoTriggerDefaultLevel 010e00c2=integer/config_lowBatteryCloseWarningBump 010e00c3=integer/config_lowBatteryWarningLevel 010e00c4=integer/config_lowMemoryKillerMinFreeKbytesAbsolute 010e00c5=integer/config_lowMemoryKillerMinFreeKbytesAdjust 010e00c6=integer/config_lowPowerStandbyNonInteractiveTimeout 010e00c7=integer/config_maxDesktopWindowingActiveTasks 010e00c8=integer/config_maxNumVisibleRecentTasks 010e00c9=integer/config_maxNumVisibleRecentTasks_lowRam 010e00ca=integer/config_maxResolverActivityColumns 010e00cb=integer/config_maxScanTasksForHomeVisibility 010e00cc=integer/config_maxShortcutTargetsPerApp 010e00cd=integer/config_maxUiWidth 010e00ce=integer/config_maxWearableSensingServiceConcurrentConnections 010e00cf=integer/config_maximumCallLogEntriesPerSim 010e00d0=integer/config_maximumScreenDimDuration 010e00d1=integer/config_mdc_initial_max_retry 010e00d2=integer/config_mediaOutputSwitchDialogVersion 010e00d3=integer/config_mediaRouter_builtInSpeakerSuitability 010e00d4=integer/config_metrics_pull_cooldown_millis 010e00d5=integer/config_minMillisBetweenInputUserActivityEvents 010e00d6=integer/config_minNumVisibleRecentTasks 010e00d7=integer/config_minNumVisibleRecentTasks_lowRam 010e00d8=integer/config_minimumScreenOffTimeout 010e00d9=integer/config_mobile_hotspot_provision_check_period 010e00da=integer/config_mobile_mtu 010e00db=integer/config_motionPredictionOffsetNanos 010e00dc=integer/config_mt_sms_polling_throttle_millis 010e00dd=integer/config_multiuserMaxRunningUsers 010e00de=integer/config_multiuserMaximumUsers 010e00df=integer/config_navBarInteractionMode 010e00e0=integer/config_navBarOpacityMode 010e00e1=integer/config_networkAvoidBadWifi 010e00e2=integer/config_networkDefaultDailyMultipathQuotaBytes 010e00e3=integer/config_networkMeteredMultipathPreference 010e00e4=integer/config_networkNotifySwitchType 010e00e5=integer/config_networkPolicyDefaultWarning 010e00e6=integer/config_networkWakeupPacketMark 010e00e7=integer/config_networkWakeupPacketMask 010e00e8=integer/config_nightDisplayColorTemperatureDefault 010e00e9=integer/config_nightDisplayColorTemperatureMax 010e00ea=integer/config_nightDisplayColorTemperatureMin 010e00eb=integer/config_notificationLongTextMaxLineCount 010e00ec=integer/config_notificationServiceArchiveSize 010e00ed=integer/config_notificationStripRemoteViewSizeBytes 010e00ee=integer/config_notificationWarnRemoteViewSizeBytes 010e00ef=integer/config_notificationsBatteryFullARGB 010e00f0=integer/config_notificationsBatteryLedOff 010e00f1=integer/config_notificationsBatteryLedOn 010e00f2=integer/config_notificationsBatteryLowARGB 010e00f3=integer/config_notificationsBatteryLowBehavior 010e00f4=integer/config_notificationsBatteryMediumARGB 010e00f5=integer/config_notificationsBatteryNearlyFullLevel 010e00f6=integer/config_ntpPollingInterval 010e00f7=integer/config_ntpPollingIntervalShorter 010e00f8=integer/config_ntpRetry 010e00f9=integer/config_ntpTimeout 010e00fa=integer/config_num_physical_slots 010e00fb=integer/config_oem_enabled_satellite_location_fresh_duration 010e00fc=integer/config_overrideHasPermanentMenuKey 010e00fd=integer/config_pauseRotationWhenUnfolding_displaySwitchTimeout 010e00fe=integer/config_pauseRotationWhenUnfolding_hingeEventTimeout 010e00ff=integer/config_pauseRotationWhenUnfolding_maxHingeAngle 010e0100=integer/config_pdp_reject_retry_delay_ms 010e0101=integer/config_phonenumber_compare_min_match 010e0102=integer/config_pictureInPictureMaxNumberOfActions 010e0103=integer/config_pinnerAssistantPinBytes 010e0104=integer/config_pinnerCameraPinBytes 010e0105=integer/config_pinnerHomePinBytes 010e0106=integer/config_pinnerMaxPinnedMemoryPercentage 010e0107=integer/config_pinnerWebviewPinBytes 010e0108=integer/config_powerStatsAggregationPeriod 010e0109=integer/config_pressedStateDurationMillis 010e010a=integer/config_previousVibrationsDumpAggregationTimeMillisLimit 010e010b=integer/config_previousVibrationsDumpSizeLimit 010e010c=integer/config_progressTimeoutFallbackHome 010e010d=integer/config_radioScanningTimeout 010e010e=integer/config_recentVibrationsDumpSizeLimit 010e010f=integer/config_reduceBrightColorsStrengthDefault 010e0110=integer/config_reduceBrightColorsStrengthMax 010e0111=integer/config_reduceBrightColorsStrengthMin 010e0112=integer/config_reevaluate_bootstrap_sim_data_usage_millis 010e0113=integer/config_requestVibrationParamsTimeout 010e0114=integer/config_reservedPrivilegedKeepaliveSlots 010e0115=integer/config_respectsActivityMinWidthHeightMultiWindow 010e0116=integer/config_safe_media_volume_index 010e0117=integer/config_safe_media_volume_usb_mB 010e0118=integer/config_satellite_delay_minutes_before_retry_validating_possible_change_in_allowed_region 010e0119=integer/config_satellite_demo_mode_nb_iot_inactivity_timeout_millis 010e011a=integer/config_satellite_emergency_mode_duration 010e011b=integer/config_satellite_location_query_throttle_interval_minutes 010e011c=integer/config_satellite_max_retry_count_for_validating_possible_change_in_allowed_region 010e011d=integer/config_satellite_modem_image_switching_duration_millis 010e011e=integer/config_satellite_nb_iot_inactivity_timeout_millis 010e011f=integer/config_satellite_stay_at_listening_from_receiving_millis 010e0120=integer/config_satellite_stay_at_listening_from_sending_millis 010e0121=integer/config_satellite_wait_for_cellular_modem_off_timeout_millis 010e0122=integer/config_screenBrightnessCapForWearBedtimeMode 010e0123=integer/config_screenBrightnessDark 010e0124=integer/config_screenBrightnessDim 010e0125=integer/config_screenBrightnessDoze 010e0126=integer/config_screenBrightnessSettingDefault 010e0127=integer/config_screenBrightnessSettingMaximum 010e0128=integer/config_screenBrightnessSettingMinimum 010e0129=integer/config_screenTimeoutOverride 010e012a=integer/config_screen_magnification_multi_tap_adjustment 010e012b=integer/config_screen_rotation_color_transition 010e012c=integer/config_screen_rotation_fade_in 010e012d=integer/config_screen_rotation_fade_in_delay 010e012e=integer/config_screen_rotation_fade_out 010e012f=integer/config_screen_rotation_total_180 010e0130=integer/config_screen_rotation_total_90 010e0131=integer/config_screenshotChordKeyTimeout 010e0132=integer/config_searchKeyBehavior 010e0133=integer/config_selected_udfps_touch_detection 010e0134=integer/config_settingsKeyBehavior 010e0135=integer/config_shortPressOnPowerBehavior 010e0136=integer/config_shortPressOnSleepBehavior 010e0137=integer/config_shortPressOnStemPrimaryBehavior 010e0138=integer/config_showOperatorNameDefault 010e0139=integer/config_shutdownBatteryTemperature 010e013a=integer/config_sideFpsToastTimeout 010e013b=integer/config_sidefpsBpPowerPressWindow 010e013c=integer/config_sidefpsKeyguardPowerPressWindow 010e013d=integer/config_sidefpsPostAuthDowntime 010e013e=integer/config_sidefpsSkipWaitForPowerAcquireMessage 010e013f=integer/config_sidefpsSkipWaitForPowerVendorAcquireMessage 010e0140=integer/config_smartSelectionInitializedTimeoutMillis 010e0141=integer/config_smartSelectionInitializingTimeoutMillis 010e0142=integer/config_soundEffectVolumeDb 010e0143=integer/config_stableDeviceDisplayHeight 010e0144=integer/config_stableDeviceDisplayWidth 010e0145=integer/config_storageManagerDaystoRetainDefault 010e0146=integer/config_supportsNonResizableMultiWindow 010e0147=integer/config_tapTimeoutMillis 010e0148=integer/config_timeDetectorAutoUpdateDiffMillis 010e0149=integer/config_timeout_to_receive_delivered_ack_millis 010e014a=integer/config_toastDefaultGravity 010e014b=integer/config_tooltipAnimTime 010e014c=integer/config_triplePressOnPowerBehavior 010e014d=integer/config_triplePressOnStemPrimaryBehavior 010e014e=integer/config_undockedHdmiRotation 010e014f=integer/config_unfoldTransitionHalfFoldedTimeout 010e0150=integer/config_userTypePackageWhitelistMode 010e0151=integer/config_valid_wappush_index 010e0152=integer/config_veryLongPressOnPowerBehavior 010e0153=integer/config_veryLongPressTimeout 010e0154=integer/config_vibrationPipelineMaxDuration 010e0155=integer/config_vibrationWaveformRampDownDuration 010e0156=integer/config_vibrationWaveformRampStepDuration 010e0157=integer/config_vibratorControlServiceDumpAggregationTimeMillisLimit 010e0158=integer/config_vibratorControlServiceDumpSizeLimit 010e0159=integer/config_virtualDisplayLimit 010e015a=integer/config_virtualDisplayLimitPerPackage 010e015b=integer/config_virtualKeyQuietTimeMillis 010e015c=integer/config_volte_replacement_rat 010e015d=integer/config_wait_for_datagram_sending_response_for_last_message_timeout_millis 010e015e=integer/config_wait_for_datagram_sending_response_timeout_millis 010e015f=integer/config_wait_for_satellite_enabling_response_timeout_millis 010e0160=integer/config_wakeUpToLastStateTimeoutMillis 010e0161=integer/config_wallpaperFrameRateCompatibility 010e0162=integer/config_whenToStartHubModeDefault 010e0163=integer/config_windowOutsetBottom 010e0164=integer/config_zen_repeat_callers_threshold 010e0165=integer/config_zoomControlsTimeoutMillis 010e0166=integer/date_picker_header_max_lines_material 010e0167=integer/date_picker_mode 010e0168=integer/date_picker_mode_material 010e0169=integer/db_connection_pool_size 010e016a=integer/db_default_idle_connection_timeout 010e016b=integer/db_journal_size_limit 010e016c=integer/db_wal_autocheckpoint 010e016d=integer/db_wal_truncate_size 010e016e=integer/default_data_warning_level_mb 010e016f=integer/default_reserved_data_coding_scheme 010e0170=integer/device_idle_flex_time_short_ms 010e0171=integer/device_idle_idle_after_inactive_to_ms 010e0172=integer/device_idle_idle_factor 010e0173=integer/device_idle_idle_pending_factor 010e0174=integer/device_idle_idle_pending_to_ms 010e0175=integer/device_idle_idle_to_ms 010e0176=integer/device_idle_inactive_to_ms 010e0177=integer/device_idle_light_after_inactive_to_ms 010e0178=integer/device_idle_light_idle_factor 010e0179=integer/device_idle_light_idle_flex_linear_increase_factor_ms 010e017a=integer/device_idle_light_idle_linear_increase_factor_ms 010e017b=integer/device_idle_light_idle_maintenance_max_budget_ms 010e017c=integer/device_idle_light_idle_maintenance_min_budget_ms 010e017d=integer/device_idle_light_idle_to_init_flex_ms 010e017e=integer/device_idle_light_idle_to_max_flex_ms 010e017f=integer/device_idle_light_idle_to_ms 010e0180=integer/device_idle_light_max_idle_to_ms 010e0181=integer/device_idle_locating_to_ms 010e0182=integer/device_idle_location_accuracy 010e0183=integer/device_idle_max_idle_pending_to_ms 010e0184=integer/device_idle_max_idle_to_ms 010e0185=integer/device_idle_max_temp_app_allowlist_duration_ms 010e0186=integer/device_idle_min_deep_maintenance_time_ms 010e0187=integer/device_idle_min_light_maintenance_time_ms 010e0188=integer/device_idle_min_time_to_alarm_ms 010e0189=integer/device_idle_mms_temp_app_allowlist_duration_ms 010e018a=integer/device_idle_motion_inactive_to_flex_ms 010e018b=integer/device_idle_motion_inactive_to_ms 010e018c=integer/device_idle_notification_allowlist_duration_ms 010e018d=integer/device_idle_quick_doze_delay_to_ms 010e018e=integer/device_idle_sensing_to_ms 010e018f=integer/device_idle_sms_temp_app_allowlist_duration_ms 010e0190=integer/disabled_alpha_animation_duration 010e0191=integer/dock_enter_exit_duration 010e0192=integer/kg_carousel_angle 010e0193=integer/kg_glowpad_rotation_offset 010e0194=integer/kg_security_flipper_weight 010e0195=integer/kg_selector_gravity 010e0196=integer/kg_widget_region_weight 010e0197=integer/leanback_setup_alpha_activity_in_bkg_delay 010e0198=integer/leanback_setup_alpha_activity_in_bkg_duration 010e0199=integer/leanback_setup_alpha_activity_out_bkg_delay 010e019a=integer/leanback_setup_alpha_activity_out_bkg_duration 010e019b=integer/leanback_setup_alpha_backward_in_content_delay 010e019c=integer/leanback_setup_alpha_backward_in_content_duration 010e019d=integer/leanback_setup_alpha_backward_out_content_delay 010e019e=integer/leanback_setup_alpha_backward_out_content_duration 010e019f=integer/leanback_setup_alpha_forward_in_content_delay 010e01a0=integer/leanback_setup_alpha_forward_in_content_duration 010e01a1=integer/leanback_setup_alpha_forward_out_content_delay 010e01a2=integer/leanback_setup_alpha_forward_out_content_duration 010e01a3=integer/leanback_setup_base_animation_duration 010e01a4=integer/leanback_setup_translation_backward_out_content_delay 010e01a5=integer/leanback_setup_translation_backward_out_content_duration 010e01a6=integer/leanback_setup_translation_content_cliff_v4 010e01a7=integer/leanback_setup_translation_content_resting_point_v4 010e01a8=integer/leanback_setup_translation_forward_in_content_delay 010e01a9=integer/leanback_setup_translation_forward_in_content_duration 010e01aa=integer/lock_pattern_fade_pattern_delay 010e01ab=integer/lock_pattern_fade_pattern_duration 010e01ac=integer/lock_pattern_line_fade_out_delay 010e01ad=integer/lock_pattern_line_fade_out_duration 010e01ae=integer/preference_fragment_scrollbarStyle 010e01af=integer/preference_screen_header_scrollbarStyle 010e01b0=integer/preferences_left_pane_weight 010e01b1=integer/preferences_right_pane_weight 010e01b2=integer/thumbnail_width_tv 010e01b3=integer/time_picker_mode 010e01b4=integer/time_picker_mode_material 010e01b5=integer/timepicker_title_visibility 010f0000=transition/no_transition 010f0001=transition/move 010f0002=transition/fade 010f0003=transition/explode 010f0004=transition/slide_bottom 010f0005=transition/slide_top 010f0006=transition/slide_right 010f0007=transition/slide_left 010f0008=transition/popup_window_enter 010f0009=transition/popup_window_exit 010f000a=xml/password_kbd_qwerty_shifted 010f000b=xml/password_kbd_symbols 010f000c=xml/password_kbd_symbols_shift 010f000d=xml/power_profile 010f000e=xml/preferred_time_zones 010f000f=xml/sms_short_codes 010f0010=xml/storage_list 010f0011=xml/time_zones_by_country 010f0012=plurals/wifi_available_detailed 01100000=raw/loaderror 01100001=raw/nodomain 01100002=raw/color_fade_frag 01100003=raw/color_fade_vert 01100004=raw/default_ringtone_vibration_effect 01100005=raw/fallback_categories 01100006=raw/fallbackring 01100007=raw/remote_views_color_resources 01110000=bool/config_sendPackageName 01110001=bool/config_showDefaultAssistant 01110002=bool/config_showDefaultEmergency 01110003=bool/config_showDefaultHome 01110004=bool/config_perDisplayFocusEnabled 01110005=bool/config_assistantOnTopOfDream 01110006=bool/config_remoteInsetsControllerControlsSystemBars 01110007=bool/config_preventImeStartupUnlessTextEditor 01110008=bool/config_enableQrCodeScannerOnLockScreen 01110009=bool/config_safetyProtectionEnabled 0111000a=bool/config_enableDefaultNotes 0111000b=bool/config_enableDefaultNotesForWorkProfile 0111000c=bool/ImsConnectedDefaultValue 0111000d=bool/action_bar_embed_tabs 0111000e=bool/action_bar_expanded_action_views_exclusive 0111000f=bool/allow_clear_initial_attach_data_profile 01110010=bool/allow_test_udfps 01110011=bool/auto_data_switch_ping_test_before_switch 01110012=bool/autofill_dialog_horizontal_space_included 01110013=bool/config_LTE_eri_for_network_name 01110014=bool/config_actionMenuItemAllCaps 01110015=bool/config_adaptive_sleep_available 01110016=bool/config_allow3rdPartyAppOnInternal 01110017=bool/config_allowAlarmsOnStoppedUsers 01110018=bool/config_allowAllRotations 01110019=bool/config_allowAnimationsInLowPowerMode 0111001a=bool/config_allowAutoBrightnessWhileDozing 0111001b=bool/config_allowChangeUserSwitcherEnabled 0111001c=bool/config_allowDisablingAssistDisclosure 0111001d=bool/config_allowDockBeforeProvision 0111001e=bool/config_allowEscrowTokenForTrustAgent 0111001f=bool/config_allowFloatingWindowsFillScreen 01110020=bool/config_allowNormalBrightnessForDozePolicy 01110021=bool/config_allowPriorityVibrationsInLowPowerMode 01110022=bool/config_allowRotationResolver 01110023=bool/config_allowSeamlessRotationDespiteNavBarMoving 01110024=bool/config_allowStartActivityForLongPressOnPowerInSetup 01110025=bool/config_allowTheaterModeWakeFromCameraLens 01110026=bool/config_allowTheaterModeWakeFromDock 01110027=bool/config_allowTheaterModeWakeFromGesture 01110028=bool/config_allowTheaterModeWakeFromKey 01110029=bool/config_allowTheaterModeWakeFromLidSwitch 0111002a=bool/config_allowTheaterModeWakeFromMotion 0111002b=bool/config_allowTheaterModeWakeFromMotionWhenNotDreaming 0111002c=bool/config_allowTheaterModeWakeFromPowerKey 0111002d=bool/config_allowTheaterModeWakeFromUnplug 0111002e=bool/config_allowTheaterModeWakeFromWindowLayout 0111002f=bool/config_allow_pin_storage_for_unattended_reboot 01110030=bool/config_allow_ussd_over_ims 01110031=bool/config_alwaysScaleWallpaper 01110032=bool/config_alwaysUseCdmaRssi 01110033=bool/config_am_disablePssProfiling 01110034=bool/config_animateScreenLights 01110035=bool/config_annoy_dianne 01110036=bool/config_appCompatUserAppAspectRatioFullscreenIsEnabled 01110037=bool/config_appCompatUserAppAspectRatioSettingsIsEnabled 01110038=bool/config_assistLongPressHomeEnabledDefault 01110039=bool/config_assistTouchGestureEnabledDefault 0111003a=bool/config_attachNavBarToAppDuringTransition 0111003b=bool/config_audio_ringer_mode_affects_alarm_stream 0111003c=bool/config_autoBrightnessResetAmbientLuxAfterWarmUp 0111003d=bool/config_autoPowerModePreferWristTilt 0111003e=bool/config_autoPowerModePrefetchLocation 0111003f=bool/config_autoPowerModeUseMotionSensor 01110040=bool/config_autoResetAirplaneMode 01110041=bool/config_auto_attach_data_on_creation 01110042=bool/config_automatic_brightness_available 01110043=bool/config_avoidGfxAccel 01110044=bool/config_awareSettingAvailable 01110045=bool/config_batterySaverStickyBehaviourDisabled 01110046=bool/config_batterySaverSupported 01110047=bool/config_batterySaverTurnedOffNotificationEnabled 01110048=bool/config_batterySaver_full_deferFullBackup 01110049=bool/config_batterySaver_full_deferKeyValueBackup 0111004a=bool/config_batterySaver_full_disableAnimation 0111004b=bool/config_batterySaver_full_disableAod 0111004c=bool/config_batterySaver_full_disableLaunchBoost 0111004d=bool/config_batterySaver_full_disableOptionalSensors 0111004e=bool/config_batterySaver_full_disableVibration 0111004f=bool/config_batterySaver_full_enableAdjustBrightness 01110050=bool/config_batterySaver_full_enableDataSaver 01110051=bool/config_batterySaver_full_enableFirewall 01110052=bool/config_batterySaver_full_enableNightMode 01110053=bool/config_batterySaver_full_enableQuickDoze 01110054=bool/config_batterySaver_full_forceAllAppsStandby 01110055=bool/config_batterySaver_full_forceBackgroundCheck 01110056=bool/config_batterySdCardAccessibility 01110057=bool/config_batteryStatsResetOnUnplugAfterSignificantCharge 01110058=bool/config_batteryStatsResetOnUnplugHighBatteryLevel 01110059=bool/config_battery_percentage_setting_available 0111005a=bool/config_batterymeterDualTone 0111005b=bool/config_bg_current_drain_auto_restrict_abusive_apps 0111005c=bool/config_bg_current_drain_event_duration_based_threshold_enabled 0111005d=bool/config_bg_current_drain_high_threshold_by_bg_location 0111005e=bool/config_bg_current_drain_monitor_enabled 0111005f=bool/config_bg_prompt_abusive_apps_to_bg_restricted 01110060=bool/config_bg_prompt_fgs_with_noti_to_bg_restricted 01110061=bool/config_biometricFrrNotificationEnabled 01110062=bool/config_bluetooth_address_validation 01110063=bool/config_bluetooth_sco_off_call 01110064=bool/config_brightWhenDozing 01110065=bool/config_bugReportHandlerEnabled 01110066=bool/config_built_in_sip_phone 01110067=bool/config_buttonTextAllCaps 01110068=bool/config_callNotificationActionColorsRequireColorized 01110069=bool/config_cameraDoubleTapPowerGestureEnabled 0111006a=bool/config_camera_autorotate 0111006b=bool/config_camera_sound_forced 0111006c=bool/config_canInternalDisplayHostDesktops 0111006d=bool/config_canRemoveFirstAccount 0111006e=bool/config_canSwitchToHeadlessSystemUser 0111006f=bool/config_carDockEnablesAccelerometer 01110070=bool/config_carrier_volte_available 01110071=bool/config_carrier_volte_tty_supported 01110072=bool/config_carrier_vt_available 01110073=bool/config_carrier_wfc_ims_available 01110074=bool/config_cbrs_supported 01110075=bool/config_cecHdmiCecControlDisabled_allowed 01110076=bool/config_cecHdmiCecControlDisabled_default 01110077=bool/config_cecHdmiCecControlEnabled_allowed 01110078=bool/config_cecHdmiCecControlEnabled_default 01110079=bool/config_cecHdmiCecEnabled_userConfigurable 0111007a=bool/config_cecHdmiCecVersion14b_allowed 0111007b=bool/config_cecHdmiCecVersion14b_default 0111007c=bool/config_cecHdmiCecVersion20_allowed 0111007d=bool/config_cecHdmiCecVersion20_default 0111007e=bool/config_cecHdmiCecVersion_userConfigurable 0111007f=bool/config_cecPowerControlModeBroadcast_allowed 01110080=bool/config_cecPowerControlModeBroadcast_default 01110081=bool/config_cecPowerControlModeNone_allowed 01110082=bool/config_cecPowerControlModeNone_default 01110083=bool/config_cecPowerControlModeTvAndAudioSystem_allowed 01110084=bool/config_cecPowerControlModeTvAndAudioSystem_default 01110085=bool/config_cecPowerControlModeTv_allowed 01110086=bool/config_cecPowerControlModeTv_default 01110087=bool/config_cecPowerControlMode_userConfigurable 01110088=bool/config_cecPowerStateChangeOnActiveSourceLostNone_allowed 01110089=bool/config_cecPowerStateChangeOnActiveSourceLostNone_default 0111008a=bool/config_cecPowerStateChangeOnActiveSourceLostStandbyNow_allowed 0111008b=bool/config_cecPowerStateChangeOnActiveSourceLostStandbyNow_default 0111008c=bool/config_cecPowerStateChangeOnActiveSourceLost_userConfigurable 0111008d=bool/config_cecQuerySadAacDisabled_allowed 0111008e=bool/config_cecQuerySadAacDisabled_default 0111008f=bool/config_cecQuerySadAacEnabled_allowed 01110090=bool/config_cecQuerySadAacEnabled_default 01110091=bool/config_cecQuerySadAac_userConfigurable 01110092=bool/config_cecQuerySadAtracDisabled_allowed 01110093=bool/config_cecQuerySadAtracDisabled_default 01110094=bool/config_cecQuerySadAtracEnabled_allowed 01110095=bool/config_cecQuerySadAtracEnabled_default 01110096=bool/config_cecQuerySadAtrac_userConfigurable 01110097=bool/config_cecQuerySadDdDisabled_allowed 01110098=bool/config_cecQuerySadDdDisabled_default 01110099=bool/config_cecQuerySadDdEnabled_allowed 0111009a=bool/config_cecQuerySadDdEnabled_default 0111009b=bool/config_cecQuerySadDd_userConfigurable 0111009c=bool/config_cecQuerySadDdpDisabled_allowed 0111009d=bool/config_cecQuerySadDdpDisabled_default 0111009e=bool/config_cecQuerySadDdpEnabled_allowed 0111009f=bool/config_cecQuerySadDdpEnabled_default 011100a0=bool/config_cecQuerySadDdp_userConfigurable 011100a1=bool/config_cecQuerySadDstDisabled_allowed 011100a2=bool/config_cecQuerySadDstDisabled_default 011100a3=bool/config_cecQuerySadDstEnabled_allowed 011100a4=bool/config_cecQuerySadDstEnabled_default 011100a5=bool/config_cecQuerySadDst_userConfigurable 011100a6=bool/config_cecQuerySadDtsDisabled_allowed 011100a7=bool/config_cecQuerySadDtsDisabled_default 011100a8=bool/config_cecQuerySadDtsEnabled_allowed 011100a9=bool/config_cecQuerySadDtsEnabled_default 011100aa=bool/config_cecQuerySadDts_userConfigurable 011100ab=bool/config_cecQuerySadDtshdDisabled_allowed 011100ac=bool/config_cecQuerySadDtshdDisabled_default 011100ad=bool/config_cecQuerySadDtshdEnabled_allowed 011100ae=bool/config_cecQuerySadDtshdEnabled_default 011100af=bool/config_cecQuerySadDtshd_userConfigurable 011100b0=bool/config_cecQuerySadLpcmDisabled_allowed 011100b1=bool/config_cecQuerySadLpcmDisabled_default 011100b2=bool/config_cecQuerySadLpcmEnabled_allowed 011100b3=bool/config_cecQuerySadLpcmEnabled_default 011100b4=bool/config_cecQuerySadLpcm_userConfigurable 011100b5=bool/config_cecQuerySadMaxDisabled_allowed 011100b6=bool/config_cecQuerySadMaxDisabled_default 011100b7=bool/config_cecQuerySadMaxEnabled_allowed 011100b8=bool/config_cecQuerySadMaxEnabled_default 011100b9=bool/config_cecQuerySadMax_userConfigurable 011100ba=bool/config_cecQuerySadMp3Disabled_allowed 011100bb=bool/config_cecQuerySadMp3Disabled_default 011100bc=bool/config_cecQuerySadMp3Enabled_allowed 011100bd=bool/config_cecQuerySadMp3Enabled_default 011100be=bool/config_cecQuerySadMp3_userConfigurable 011100bf=bool/config_cecQuerySadMpeg1Disabled_allowed 011100c0=bool/config_cecQuerySadMpeg1Disabled_default 011100c1=bool/config_cecQuerySadMpeg1Enabled_allowed 011100c2=bool/config_cecQuerySadMpeg1Enabled_default 011100c3=bool/config_cecQuerySadMpeg1_userConfigurable 011100c4=bool/config_cecQuerySadMpeg2Disabled_allowed 011100c5=bool/config_cecQuerySadMpeg2Disabled_default 011100c6=bool/config_cecQuerySadMpeg2Enabled_allowed 011100c7=bool/config_cecQuerySadMpeg2Enabled_default 011100c8=bool/config_cecQuerySadMpeg2_userConfigurable 011100c9=bool/config_cecQuerySadOnebitaudioDisabled_allowed 011100ca=bool/config_cecQuerySadOnebitaudioDisabled_default 011100cb=bool/config_cecQuerySadOnebitaudioEnabled_allowed 011100cc=bool/config_cecQuerySadOnebitaudioEnabled_default 011100cd=bool/config_cecQuerySadOnebitaudio_userConfigurable 011100ce=bool/config_cecQuerySadTruehdDisabled_allowed 011100cf=bool/config_cecQuerySadTruehdDisabled_default 011100d0=bool/config_cecQuerySadTruehdEnabled_allowed 011100d1=bool/config_cecQuerySadTruehdEnabled_default 011100d2=bool/config_cecQuerySadTruehd_userConfigurable 011100d3=bool/config_cecQuerySadWmaproDisabled_allowed 011100d4=bool/config_cecQuerySadWmaproDisabled_default 011100d5=bool/config_cecQuerySadWmaproEnabled_allowed 011100d6=bool/config_cecQuerySadWmaproEnabled_default 011100d7=bool/config_cecQuerySadWmapro_userConfigurable 011100d8=bool/config_cecRcProfileSourceContentsMenuHandled_allowed 011100d9=bool/config_cecRcProfileSourceContentsMenuHandled_default 011100da=bool/config_cecRcProfileSourceContentsMenuNotHandled_allowed 011100db=bool/config_cecRcProfileSourceContentsMenuNotHandled_default 011100dc=bool/config_cecRcProfileSourceContentsMenu_userConfigurable 011100dd=bool/config_cecRcProfileSourceMediaContextSensitiveMenuHandled_allowed 011100de=bool/config_cecRcProfileSourceMediaContextSensitiveMenuHandled_default 011100df=bool/config_cecRcProfileSourceMediaContextSensitiveMenuNotHandled_allowed 011100e0=bool/config_cecRcProfileSourceMediaContextSensitiveMenuNotHandled_default 011100e1=bool/config_cecRcProfileSourceMediaContextSensitiveMenu_userConfigurable 011100e2=bool/config_cecRcProfileSourceRootMenuHandled_allowed 011100e3=bool/config_cecRcProfileSourceRootMenuHandled_default 011100e4=bool/config_cecRcProfileSourceRootMenuNotHandled_allowed 011100e5=bool/config_cecRcProfileSourceRootMenuNotHandled_default 011100e6=bool/config_cecRcProfileSourceRootMenu_userConfigurable 011100e7=bool/config_cecRcProfileSourceSetupMenuHandled_allowed 011100e8=bool/config_cecRcProfileSourceSetupMenuHandled_default 011100e9=bool/config_cecRcProfileSourceSetupMenuNotHandled_allowed 011100ea=bool/config_cecRcProfileSourceSetupMenuNotHandled_default 011100eb=bool/config_cecRcProfileSourceSetupMenu_userConfigurable 011100ec=bool/config_cecRcProfileSourceTopMenuHandled_allowed 011100ed=bool/config_cecRcProfileSourceTopMenuHandled_default 011100ee=bool/config_cecRcProfileSourceTopMenuNotHandled_allowed 011100ef=bool/config_cecRcProfileSourceTopMenuNotHandled_default 011100f0=bool/config_cecRcProfileSourceTopMenu_userConfigurable 011100f1=bool/config_cecRcProfileTvFour_allowed 011100f2=bool/config_cecRcProfileTvFour_default 011100f3=bool/config_cecRcProfileTvNone_allowed 011100f4=bool/config_cecRcProfileTvNone_default 011100f5=bool/config_cecRcProfileTvOne_allowed 011100f6=bool/config_cecRcProfileTvOne_default 011100f7=bool/config_cecRcProfileTvThree_allowed 011100f8=bool/config_cecRcProfileTvThree_default 011100f9=bool/config_cecRcProfileTvTwo_allowed 011100fa=bool/config_cecRcProfileTvTwo_default 011100fb=bool/config_cecRcProfileTv_userConfigurable 011100fc=bool/config_cecRoutingControlDisabled_allowed 011100fd=bool/config_cecRoutingControlDisabled_default 011100fe=bool/config_cecRoutingControlEnabled_allowed 011100ff=bool/config_cecRoutingControlEnabled_default 01110100=bool/config_cecRoutingControl_userConfigurable 01110101=bool/config_cecSetMenuLanguageDisabled_allowed 01110102=bool/config_cecSetMenuLanguageDisabled_default 01110103=bool/config_cecSetMenuLanguageEnabled_allowed 01110104=bool/config_cecSetMenuLanguageEnabled_default 01110105=bool/config_cecSetMenuLanguage_userConfigurable 01110106=bool/config_cecSoundbarModeDisabled_allowed 01110107=bool/config_cecSoundbarModeDisabled_default 01110108=bool/config_cecSoundbarModeEnabled_allowed 01110109=bool/config_cecSoundbarModeEnabled_default 0111010a=bool/config_cecSoundbarMode_userConfigurable 0111010b=bool/config_cecSystemAudioControlDisabled_allowed 0111010c=bool/config_cecSystemAudioControlDisabled_default 0111010d=bool/config_cecSystemAudioControlEnabled_allowed 0111010e=bool/config_cecSystemAudioControlEnabled_default 0111010f=bool/config_cecSystemAudioControl_userConfigurable 01110110=bool/config_cecSystemAudioModeMutingDisabled_allowed 01110111=bool/config_cecSystemAudioModeMutingDisabled_default 01110112=bool/config_cecSystemAudioModeMutingEnabled_allowed 01110113=bool/config_cecSystemAudioModeMutingEnabled_default 01110114=bool/config_cecSystemAudioModeMuting_userConfigurable 01110115=bool/config_cecTvSendStandbyOnSleepDisabled_allowed 01110116=bool/config_cecTvSendStandbyOnSleepDisabled_default 01110117=bool/config_cecTvSendStandbyOnSleepEnabled_allowed 01110118=bool/config_cecTvSendStandbyOnSleepEnabled_default 01110119=bool/config_cecTvSendStandbyOnSleep_userConfigurable 0111011a=bool/config_cecTvWakeOnOneTouchPlayDisabled_allowed 0111011b=bool/config_cecTvWakeOnOneTouchPlayDisabled_default 0111011c=bool/config_cecTvWakeOnOneTouchPlayEnabled_allowed 0111011d=bool/config_cecTvWakeOnOneTouchPlayEnabled_default 0111011e=bool/config_cecTvWakeOnOneTouchPlay_userConfigurable 0111011f=bool/config_cecVolumeControlModeDisabled_allowed 01110120=bool/config_cecVolumeControlModeDisabled_default 01110121=bool/config_cecVolumeControlModeEnabled_allowed 01110122=bool/config_cecVolumeControlModeEnabled_default 01110123=bool/config_cecVolumeControlMode_userConfigurable 01110124=bool/config_cellBroadcastAppLinks 01110125=bool/config_checkWallpaperAtBoot 01110126=bool/config_closeDialogWhenTouchOutside 01110127=bool/config_customBugreport 01110128=bool/config_customUserSwitchUi 01110129=bool/config_debugEnableAutomaticSystemServerHeapDumps 0111012a=bool/config_decoupleStatusBarAndDisplayCutoutFromScreenSize 0111012b=bool/config_defaultAdasGnssLocationEnabled 0111012c=bool/config_defaultBatteryPercentageSetting 0111012d=bool/config_defaultBinderHeavyHitterAutoSamplerEnabled 0111012e=bool/config_defaultBinderHeavyHitterWatcherEnabled 0111012f=bool/config_defaultEmergencyGestureEnabled 01110130=bool/config_defaultEmergencyGestureSoundEnabled 01110131=bool/config_defaultInTouchMode 01110132=bool/config_defaultPreventScreenTimeoutEnabled 01110133=bool/config_defaultRingtonePickerEnabled 01110134=bool/config_defaultWindowFeatureContextMenu 01110135=bool/config_defaultWindowFeatureOptionsPanel 01110136=bool/config_deskDockEnablesAccelerometer 01110137=bool/config_deviceSupportsHighPerfTransitions 01110138=bool/config_deviceSupportsWifiUsd 01110139=bool/config_device_respects_hold_carrier_config 0111013a=bool/config_device_volte_available 0111013b=bool/config_device_vt_available 0111013c=bool/config_device_wfc_ims_available 0111013d=bool/config_disableLockscreenByDefault 0111013e=bool/config_disableMenuKeyInLockScreen 0111013f=bool/config_disableShutdownVibrationInZen 01110140=bool/config_disableTaskSnapshots 01110141=bool/config_disableTransitionAnimation 01110142=bool/config_disableUsbPermissionDialogs 01110143=bool/config_disableWeaverOnUnsecuredUsers 01110144=bool/config_disable_all_cb_messages 01110145=bool/config_dismissDreamOnActivityStart 01110146=bool/config_displayBlanksAfterDoze 01110147=bool/config_displayBrightnessBucketsInDoze 01110148=bool/config_displayColorFadeDisabled 01110149=bool/config_displayWhiteBalanceAvailable 0111014a=bool/config_displayWhiteBalanceEnabledDefault 0111014b=bool/config_displayWhiteBalanceLightModeAllowed 0111014c=bool/config_dockBigOverlayWindows 0111014d=bool/config_dockedStackDividerFreeSnapMode 0111014e=bool/config_dontPreferApn 0111014f=bool/config_dozeAfterScreenOffByDefault 01110150=bool/config_dozeAlwaysOnDisplayAvailable 01110151=bool/config_dozeAlwaysOnEnabled 01110152=bool/config_dozePickupGestureEnabled 01110153=bool/config_dozePulsePickup 01110154=bool/config_dozeSupportsAodWallpaper 01110155=bool/config_dozeWakeLockScreenSensorAvailable 01110156=bool/config_dragToMaximizeInDesktopMode 01110157=bool/config_dreamsActivatedOnDockByDefault 01110158=bool/config_dreamsActivatedOnPosturedByDefault 01110159=bool/config_dreamsActivatedOnSleepByDefault 0111015a=bool/config_dreamsDisabledByAmbientModeSuppressionConfig 0111015b=bool/config_dreamsEnabledByDefault 0111015c=bool/config_dreamsEnabledOnBattery 0111015d=bool/config_dreamsOnlyEnabledForDockUser 0111015e=bool/config_dreamsSupported 0111015f=bool/config_dropboxmanager_persistent_logging_enabled 01110160=bool/config_duplicate_port_omadm_wappush 01110161=bool/config_eap_sim_based_auth_supported 01110162=bool/config_earcEnabled_userConfigurable 01110163=bool/config_earcFeatureDisabled_allowed 01110164=bool/config_earcFeatureDisabled_default 01110165=bool/config_earcFeatureEnabled_allowed 01110166=bool/config_earcFeatureEnabled_default 01110167=bool/config_emergencyGestureEnabled 01110168=bool/config_enableActivityRecognitionHardwareOverlay 01110169=bool/config_enableAppCloningBuildingBlocks 0111016a=bool/config_enableAppWidgetService 0111016b=bool/config_enableAutoPowerModes 0111016c=bool/config_enableBackSound 0111016d=bool/config_enableBurnInProtection 0111016e=bool/config_enableCarDockHomeLaunch 0111016f=bool/config_enableContextSyncInCall 01110170=bool/config_enableCredentialFactoryResetProtection 01110171=bool/config_enableCrossTaskScaleUpAnimation 01110172=bool/config_enableDefaultHdrConversionPassthrough 01110173=bool/config_enableFusedLocationOverlay 01110174=bool/config_enableGaiaEducationInPrivateSpace 01110175=bool/config_enableGeocoderOverlay 01110176=bool/config_enableGeofenceOverlay 01110177=bool/config_enableGeolocationTimeZoneDetection 01110178=bool/config_enableGnssAssistanceOverlay 01110179=bool/config_enableGnssLocationOverlay 0111017a=bool/config_enableGnssTimeUpdateService 0111017b=bool/config_enableHapticTextHandle 0111017c=bool/config_enableIdleScreenBrightnessMode 0111017d=bool/config_enableLockBeforeUnlockScreen 0111017e=bool/config_enableLockScreenRotation 0111017f=bool/config_enableMotionPrediction 01110180=bool/config_enableMultiUserUI 01110181=bool/config_enableMultipleAdmins 01110182=bool/config_enableNetworkLocationOverlay 01110183=bool/config_enableNewAutoSelectNetworkUI 01110184=bool/config_enableNightMode 01110185=bool/config_enableNotificationAccessibilityEvents 01110186=bool/config_enablePopulationDensityProviderOverlay 01110187=bool/config_enablePrimaryLocationTimeZoneProvider 01110188=bool/config_enableProximityService 01110189=bool/config_enableSafetyCenter 0111018a=bool/config_enableScreenshotChord 0111018b=bool/config_enableSearchTileHideIllustrationInPrivateSpace 0111018c=bool/config_enableSecondaryLocationTimeZoneProvider 0111018d=bool/config_enableServerNotificationEffectsForAutomotive 0111018e=bool/config_enableStylusPointerIcon 0111018f=bool/config_enableTelephonyTimeZoneDetection 01110190=bool/config_enableTimeZoneManualChangeTrackingSupported 01110191=bool/config_enableTimeZoneNotificationsSupported 01110192=bool/config_enableTimeZoneNotificationsTrackingSupported 01110193=bool/config_enableTimeoutToDockUserWhenDocked 01110194=bool/config_enableUdcSysfsUsbStateUpdate 01110195=bool/config_enableUserSwitcherUponUserCreation 01110196=bool/config_enableViewGroupScalingFading 01110197=bool/config_enableVirtualDeviceManager 01110198=bool/config_enableWallpaperService 01110199=bool/config_enableWcgMode 0111019a=bool/config_enableWifiDisplay 0111019b=bool/config_enable_a11y_fullscreen_magnification_overscroll_handler 0111019c=bool/config_enable_a11y_magnification_single_panning 0111019d=bool/config_enable_cellular_on_boot_default 0111019e=bool/config_enable_emergency_call_while_sim_locked 0111019f=bool/config_enable_iwlan_handover_policy 011101a0=bool/config_enable_puk_unlock_screen 011101a1=bool/config_enabled_mt_sms_polling 011101a2=bool/config_enhancedConfirmationModeEnabled 011101a3=bool/config_enhanced_iwlan_handover_check 011101a4=bool/config_enterDesktopByDefaultOnFreeformDisplay 011101a5=bool/config_evenDimmerEnabled 011101a6=bool/config_expandLockScreenUserSwitcher 011101a7=bool/config_faceAuthDismissesKeyguard 011101a8=bool/config_faceAuthSupportsSelfIllumination 011101a9=bool/config_fillMainBuiltInDisplayCutout 011101aa=bool/config_fillSecondaryBuiltInDisplayCutout 011101ab=bool/config_fingerprintSupportsGestures 011101ac=bool/config_flexibleSplitRatios 011101ad=bool/config_flipToScreenOffEnabled 011101ae=bool/config_focusScrollContainersInTouchMode 011101af=bool/config_fold_lock_behavior 011101b0=bool/config_forceOrientationListenerEnabledWhileDreaming 011101b1=bool/config_forceSystemPackagesQueryable 011101b2=bool/config_forceWindowDrawsStatusBarBackground 011101b3=bool/config_force_phone_globals_creation 011101b4=bool/config_fusedLocationOverlayUnstableFallback 011101b5=bool/config_glanceableHubEnabled 011101b6=bool/config_gnssLocationOverlayUnstableFallback 011101b7=bool/config_goToSleepOnButtonPressTheaterMode 011101b8=bool/config_guestUserAllowEphemeralStateChange 011101b9=bool/config_guestUserAutoCreated 011101ba=bool/config_guestUserEphemeral 011101bb=bool/config_handleVolumeAliasesUsingVolumeGroups 011101bc=bool/config_handleVolumeKeysInWindowManager 011101bd=bool/config_hasPermanentDpad 011101be=bool/config_hasRecents 011101bf=bool/config_hibernationDeletesOatArtifactsEnabled 011101c0=bool/config_hideDisplayCutoutWithDisplayArea 011101c1=bool/config_hideNavBarForKeyboard 011101c2=bool/config_honor_data_retry_timer_for_emergency_network 011101c3=bool/config_hotspotNetworksEnabledForService 011101c4=bool/config_hotswapCapable 011101c5=bool/config_ignoreUdfpsVote 011101c6=bool/config_ignoreVibrationsOnWirelessCharger 011101c7=bool/config_imeDrawsImeNavBar 011101c8=bool/config_intrusiveNotificationLed 011101c9=bool/config_isCompatFakeFocusEnabled 011101ca=bool/config_isDesktopModeDevOptionSupported 011101cb=bool/config_isDesktopModeSupported 011101cc=bool/config_isDisplayHingeAlwaysSeparating 011101cd=bool/config_isMainUserPermanentAdmin 011101ce=bool/config_isPreApprovalRequestAvailable 011101cf=bool/config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled 011101d0=bool/config_isWindowManagerCameraCompatTreatmentEnabled 011101d1=bool/config_is_powerbutton_fps 011101d2=bool/config_jobSchedulerRestrictBackgroundUser 011101d3=bool/config_keepDreamingWhenUnplugging 011101d4=bool/config_keepRestrictedProfilesInBackground 011101d5=bool/config_keyboardVibrationSettingsSupported 011101d6=bool/config_keyguardUserSwitcher 011101d7=bool/config_knownNetworksEnabledForService 011101d8=bool/config_launchCameraOnCameraLensCoverToggle 011101d9=bool/config_leftRightSplitInPortrait 011101da=bool/config_letterboxIsAutomaticReachabilityInBookModeEnabled 011101db=bool/config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled 011101dc=bool/config_letterboxIsDisplayRotationImmersiveAppCompatPolicyEnabled 011101dd=bool/config_letterboxIsEducationEnabled 011101de=bool/config_letterboxIsEnabledForTranslucentActivities 011101df=bool/config_letterboxIsHorizontalReachabilityEnabled 011101e0=bool/config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled 011101e1=bool/config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled 011101e2=bool/config_letterboxIsVerticalReachabilityEnabled 011101e3=bool/config_lidControlsScreenLock 011101e4=bool/config_lidControlsSleep 011101e5=bool/config_localDisplaysMirrorContent 011101e6=bool/config_lockDayNightMode 011101e7=bool/config_lockUiMode 011101e8=bool/config_longPressOnPowerForAssistantSettingAvailable 011101e9=bool/config_lowPowerStandbyEnabledByDefault 011101ea=bool/config_lowPowerStandbySupported 011101eb=bool/config_magnification_always_on_enabled 011101ec=bool/config_magnification_area 011101ed=bool/config_magnification_keep_zoom_level_when_context_changed 011101ee=bool/config_mainBuiltInDisplayIsRound 011101ef=bool/config_maskMainBuiltInDisplayCutout 011101f0=bool/config_maskSecondaryBuiltInDisplayCutout 011101f1=bool/config_matchSecondaryInternalDisplaysOrientationToReverseDefaultDisplay 011101f2=bool/config_mms_content_disposition_support 011101f3=bool/config_mobile_data_capable 011101f4=bool/config_multiuserDelayUserDataLocking 011101f5=bool/config_multiuserVisibleBackgroundUsers 011101f6=bool/config_multiuserVisibleBackgroundUsersOnDefaultDisplay 011101f7=bool/config_navBarAlwaysShowOnSideEdgeGesture 011101f8=bool/config_navBarCanMove 011101f9=bool/config_navBarDefaultTransparent 011101fa=bool/config_navBarNeedsScrim 011101fb=bool/config_navBarTapThrough 011101fc=bool/config_networkSamplingWakesDevice 011101fd=bool/config_nightDisplayAvailable 011101fe=bool/config_noHomeScreen 011101ff=bool/config_notificationBadging 01110200=bool/config_notificationCloseButtonSupported 01110201=bool/config_notificationHeaderClickableForExpand 01110202=bool/config_notificationReviewPermissions 01110203=bool/config_oem_enabled_satellite_access_allow 01110204=bool/config_offsetWallpaperToCenterOfLargestDisplay 01110205=bool/config_omnipresentCommunalUser 01110206=bool/config_orderUnlockAndWake 01110207=bool/config_overrideRemoteViewsActivityTransition 01110208=bool/config_pauseWallpaperRenderWhenStateChangeEnabled 01110209=bool/config_pdp_reject_enable_retry 0111020a=bool/config_performantAuthDefault 0111020b=bool/config_permissionsIndividuallyControlled 0111020c=bool/config_persistBrightnessNitsForDefaultDisplay 0111020d=bool/config_pinnerAssistantApp 0111020e=bool/config_pinnerCameraApp 0111020f=bool/config_powerDecoupleAutoSuspendModeFromDisplay 01110210=bool/config_powerDecoupleInteractiveModeFromDisplay 01110211=bool/config_predictShowStartingSurface 01110212=bool/config_preferKeepClearForFocus 01110213=bool/config_preferenceFragmentClipToPadding 01110214=bool/config_profcollectReportUploaderEnabled 01110215=bool/config_pulseOnNotificationsAvailable 01110216=bool/config_quickSettingsShowMediaPlayer 01110217=bool/config_quickSettingsSupported 01110218=bool/config_rawContactsAccountRestrictionEnabled 01110219=bool/config_reduceBrightColorsAvailable 0111021a=bool/config_refreshRateSynchronizationEnabled 0111021b=bool/config_remoteInsetsControllerSystemBarsCanBeShownByUserAction 0111021c=bool/config_repairModeSupported 0111021d=bool/config_requireCallCapableAccountForHandle 0111021e=bool/config_resetScreenTimeoutOnUnexpectedDreamExit 0111021f=bool/config_restartRadioAfterProvisioning 01110220=bool/config_restart_radio_on_pdp_fail_regular_deactivation 01110221=bool/config_reverseDefaultRotation 01110222=bool/config_ringtoneVibrationSettingsSupported 01110223=bool/config_safe_media_disable_on_volume_up 01110224=bool/config_safe_media_volume_enabled 01110225=bool/config_safe_sound_dosage_enabled 01110226=bool/config_satellite_allow_check_message_in_not_connected 01110227=bool/config_satellite_allow_tn_scanning_during_satellite_session 01110228=bool/config_satellite_modem_support_concurrent_tn_scanning 01110229=bool/config_satellite_should_notify_availability 0111022a=bool/config_screen_off_udfps_default_on 0111022b=bool/config_screen_off_udfps_enabled 0111022c=bool/config_searchAllEntrypointsEnabledDefault 0111022d=bool/config_secondaryBuiltInDisplayIsRound 0111022e=bool/config_sendAudioBecomingNoisy 0111022f=bool/config_send_satellite_datagram_to_modem_in_demo_mode 01110230=bool/config_sensorPrivacyRequiresAuthentication 01110231=bool/config_setColorTransformAccelerated 01110232=bool/config_setColorTransformAcceleratedPerLayer 01110233=bool/config_settingsHelpLinksEnabled 01110234=bool/config_sf_limitedAlpha 01110235=bool/config_sf_slowBlur 01110236=bool/config_shortPressEarlyOnPower 01110237=bool/config_shortPressEarlyOnStemPrimary 01110238=bool/config_showAreaUpdateInfoSettings 01110239=bool/config_showBuiltinWirelessChargingAnim 0111023a=bool/config_showGesturalNavigationHints 0111023b=bool/config_showMenuShortcutsWhenKeyboardPresent 0111023c=bool/config_showNavigationBar 0111023d=bool/config_showNotificationForBackgroundUserAlarms 0111023e=bool/config_showPercentageTextDuringRebootToUpdate 0111023f=bool/config_showSysuiShutdown 01110240=bool/config_showUserSwitcherByDefault 01110241=bool/config_shutdownIfNoPower 01110242=bool/config_silenceRingerOnSleepKey 01110243=bool/config_silenceSensorAvailable 01110244=bool/config_single_volume 01110245=bool/config_sip_wifi_only 01110246=bool/config_skipActivityRelaunchWhenDocking 01110247=bool/config_skipScreenOffTransition 01110248=bool/config_skipScreenOnBrightnessRamp 01110249=bool/config_skipSensorAvailable 0111024a=bool/config_smart_battery_available 0111024b=bool/config_smma_notification_supported_over_ims 0111024c=bool/config_smppsim_response_via_ims 0111024d=bool/config_sms_ask_every_time_support 0111024e=bool/config_sms_capable 0111024f=bool/config_sms_decode_gsm_8bit_data 01110250=bool/config_sms_force_7bit_encoding 01110251=bool/config_sms_utf8_support 01110252=bool/config_spatial_audio_head_tracking_enabled_default 01110253=bool/config_speed_up_audio_on_mt_calls 01110254=bool/config_startDreamImmediatelyOnDock 01110255=bool/config_stkNoAlphaUsrCnf 01110256=bool/config_stk_sms_send_support 01110257=bool/config_stopSystemPackagesByDefault 01110258=bool/config_strongAuthRequiredOnBoot 01110259=bool/config_subscription_database_async_update 0111025a=bool/config_supportAudioSourceUnprocessed 0111025b=bool/config_supportAutoRotation 0111025c=bool/config_supportDoubleTapSleep 0111025d=bool/config_supportDoubleTapWake 0111025e=bool/config_supportLongPressPowerWhenNonInteractive 0111025f=bool/config_supportMicNearUltrasound 01110260=bool/config_supportPreRebootSecurityLogs 01110261=bool/config_supportProfilesOnNonMainUser 01110262=bool/config_supportShortPressPowerWhenDefaultDisplayOn 01110263=bool/config_supportSpeakerNearUltrasound 01110264=bool/config_supportSystemNavigationKeys 01110265=bool/config_supportTelephonyTimeZoneFallback 01110266=bool/config_support_disable_satellite_while_enable_in_progress 01110267=bool/config_supportsBubble 01110268=bool/config_supportsCamToggle 01110269=bool/config_supportsConcurrentInternalDisplays 0111026a=bool/config_supportsHardwareCamToggle 0111026b=bool/config_supportsHardwareMicToggle 0111026c=bool/config_supportsInsecureLockScreen 0111026d=bool/config_supportsMicToggle 0111026e=bool/config_supportsMultiDisplay 0111026f=bool/config_supportsMultiWindow 01110270=bool/config_supportsRoundedCornersOnWindows 01110271=bool/config_supportsSeamlessRefreshRateSwitching 01110272=bool/config_supportsSplitScreenMultiWindow 01110273=bool/config_supportsSystemDecorsOnSecondaryDisplays 01110274=bool/config_suspendWhenScreenOffDueToProximity 01110275=bool/config_sustainedPerformanceModeSupported 01110276=bool/config_swipeDisambiguation 01110277=bool/config_swipe_up_gesture_setting_available 01110278=bool/config_switch_phone_on_voice_reg_state_change 01110279=bool/config_syncstorageengine_masterSyncAutomatically 0111027a=bool/config_systemCaptionsServiceCallsEnabled 0111027b=bool/config_telephony5gNonStandalone 0111027c=bool/config_telephony5gStandalone 0111027d=bool/config_textShareSupported 0111027e=bool/config_tintNotificationActionButtons 0111027f=bool/config_trackerAppNeedsPermissions 01110280=bool/config_tvExternalInputLoggingDisplayNameFilterEnabled 01110281=bool/config_ui_enableFadingMarquee 01110282=bool/config_unfoldTransitionEnabled 01110283=bool/config_unfoldTransitionHapticsEnabled 01110284=bool/config_unfoldTransitionHingeAngle 01110285=bool/config_unplugTurnsOnScreen 01110286=bool/config_usbChargingMessage 01110287=bool/config_use16BitTaskSnapshotPixelFormat 01110288=bool/config_useAssistantVolume 01110289=bool/config_useAttentionLight 0111028a=bool/config_useAutoSuspend 0111028b=bool/config_useCurrentRotationOnRotationLockChange 0111028c=bool/config_useDefaultFocusHighlight 0111028d=bool/config_useDevInputEventForAudioJack 0111028e=bool/config_useFixedVolume 0111028f=bool/config_useGnssHardwareProvider 01110290=bool/config_useLegacySplit 01110291=bool/config_useRoundIcon 01110292=bool/config_useSmsAppService 01110293=bool/config_useSystemProvidedLauncherForSecondary 01110294=bool/config_useVideoPauseWorkaround 01110295=bool/config_useVolumeKeySounds 01110296=bool/config_use_sim_language_file 01110297=bool/config_use_strict_phone_number_comparation 01110298=bool/config_use_strict_phone_number_comparation_for_kazakhstan 01110299=bool/config_use_strict_phone_number_comparation_for_russia 0111029a=bool/config_use_voip_mode_for_ims 0111029b=bool/config_user_notification_of_restrictied_mobile_access 0111029c=bool/config_vehicleInternalNetworkAlwaysRequested 0111029d=bool/config_viewBasedRotaryEncoderHapticsEnabled 0111029e=bool/config_viewRotaryEncoderHapticScrollFedbackEnabled 0111029f=bool/config_viewTouchScreenHapticScrollFeedbackEnabled 011102a0=bool/config_voice_capable 011102a1=bool/config_voice_data_sms_auto_fallback 011102a2=bool/config_volumeHushGestureEnabled 011102a3=bool/config_volume_down_to_enter_silent 011102a4=bool/config_volume_up_to_exit_silent 011102a5=bool/config_wait_for_device_alignment_in_demo_datagram 011102a6=bool/config_wakeOnAssistKeyPress 011102a7=bool/config_wakeOnBackKeyPress 011102a8=bool/config_wakeOnDpadKeyPress 011102a9=bool/config_wallpaperTopApp 011102aa=bool/config_watchlistUseFileHashesCache 011102ab=bool/config_wifiDisplaySupportsProtectedBuffers 011102ac=bool/config_wimaxEnabled 011102ad=bool/config_windowActionBarSupported 011102ae=bool/config_windowEnableCircularEmulatorDisplayOverlay 011102af=bool/config_windowIsRound 011102b0=bool/config_windowManagerHalfFoldAutoRotateOverride 011102b1=bool/config_windowManagerPauseRotationWhenUnfolding 011102b2=bool/config_windowNoTitleDefault 011102b3=bool/config_windowOverscanByDefault 011102b4=bool/config_windowShowCircularMask 011102b5=bool/config_wirelessConsentRequired 011102b6=bool/config_wlan_data_service_conn_persistence_on_restart 011102b7=bool/config_zramWriteback 011102b8=bool/device_idle_light_idle_increase_linearly 011102b9=bool/device_idle_use_mode_manager 011102ba=bool/device_idle_use_window_alarms 011102bb=bool/device_idle_wait_for_unlock 011102bc=bool/editable_voicemailnumber 011102bd=bool/euicc_optimize_apdu_sender 011102be=bool/ignore_emergency_number_routing_from_db 011102bf=bool/ignore_modem_config_emergency_numbers 011102c0=bool/imsServiceAllowTurnOff 011102c1=bool/kg_center_small_widgets_vertically 011102c2=bool/kg_enable_camera_default_widget 011102c3=bool/kg_share_status_area 011102c4=bool/kg_sim_puk_account_full_screen 011102c5=bool/kg_top_align_page_shrink_on_bouncer_visible 011102c6=bool/lockscreen_isPortrait 011102c7=bool/preferences_prefer_dual_pane 011102c8=bool/reset_geo_fencing_check_after_boot_or_apm 011102c9=bool/resolver_landscape_phone 011102ca=bool/show_ongoing_ime_switcher 011102cb=bool/skipHoldBeforeMerge 011102cc=bool/skip_restoring_network_selection 011102cd=bool/split_action_bar_is_narrow 011102ce=bool/use_lock_pattern_drawable 01120000=^attr-private/__removed0 01120001=^attr-private/__removed1 01120002=^attr-private/__removed2 01120003=^attr-private/__removed3 01120004=^attr-private/__removed4 01120005=^attr-private/__removed5 01120006=^attr-private/__removed6 01120007=^attr-private/accessibilityFocusedDrawable 01120008=^attr-private/actionModePopupWindowStyle 01120009=^attr-private/actionModeRedoDrawable 0112000a=^attr-private/actionModeUndoDrawable 0112000b=^attr-private/activityChooserViewStyle 0112000c=^attr-private/activityOpenRemoteViewsEnterAnimation 0112000d=^attr-private/adjustable 0112000e=^attr-private/alertDialogButtonGroupStyle 0112000f=^attr-private/alertDialogCenterButtons 01120010=^attr-private/allowAutoRevokePermissionsExemption 01120011=^attr-private/allowMassStorage 01120012=^attr-private/allowStacking 01120013=^attr-private/alwaysFocusable 01120014=^attr-private/aspect 01120015=^attr-private/autofillDatasetPickerMaxHeight 01120016=^attr-private/autofillDatasetPickerMaxWidth 01120017=^attr-private/autofillSaveCustomSubtitleMaxHeight 01120018=^attr-private/backgroundLeft 01120019=^attr-private/backgroundRequest 0112001a=^attr-private/backgroundRequestDetail 0112001b=^attr-private/backgroundRight 0112001c=^attr-private/borderBottom 0112001d=^attr-private/borderLeft 0112001e=^attr-private/borderRight 0112001f=^attr-private/borderTop 01120020=^attr-private/buttonPanelSideLayout 01120021=^attr-private/calendarViewMode 01120022=^attr-private/checkMarkGravity 01120023=^attr-private/clickColor 01120024=^attr-private/closeItemLayout 01120025=^attr-private/colorAccentPrimary 01120026=^attr-private/colorAccentPrimaryVariant 01120027=^attr-private/colorAccentSecondary 01120028=^attr-private/colorAccentSecondaryVariant 01120029=^attr-private/colorAccentTertiary 0112002a=^attr-private/colorAccentTertiaryVariant 0112002b=^attr-private/colorListDivider 0112002c=^attr-private/colorPopupBackground 0112002d=^attr-private/colorProgressBackgroundNormal 0112002e=^attr-private/colorSurface 0112002f=^attr-private/colorSurfaceHeader 01120030=^attr-private/colorSurfaceHighlight 01120031=^attr-private/colorSurfaceVariant 01120032=^attr-private/colorSwitchThumbNormal 01120033=^attr-private/controllerType 01120034=^attr-private/cornerRadius 01120035=^attr-private/dayHighlightColor 01120036=^attr-private/daySelectorColor 01120037=^attr-private/defaultQueryHint 01120038=^attr-private/dialogCustomTitleDecorLayout 01120039=^attr-private/dialogMode 0112003a=^attr-private/dialogTitleDecorLayout 0112003b=^attr-private/dialogTitleIconsDecorLayout 0112003c=^attr-private/disableChildrenWhenDisabled 0112003d=^attr-private/dotActivatedColor 0112003e=^attr-private/dotColor 0112003f=^attr-private/dotSize 01120040=^attr-private/drawableAlpha 01120041=^attr-private/dreamActivityCloseExitAnimation 01120042=^attr-private/dreamActivityOpenEnterAnimation 01120043=^attr-private/dreamActivityOpenExitAnimation 01120044=^attr-private/dropdownListPreferredItemHeight 01120045=^attr-private/emergencyInstaller 01120046=^attr-private/emulated 01120047=^attr-private/enableControlView 01120048=^attr-private/enableSubtitle 01120049=^attr-private/enlargeVertexEntryArea 0112004a=^attr-private/errorColor 0112004b=^attr-private/errorMessageAboveBackground 0112004c=^attr-private/errorMessageBackground 0112004d=^attr-private/expandActivityOverflowButtonDrawable 0112004e=^attr-private/externalRouteEnabledDrawable 0112004f=^attr-private/fadedHeight 01120050=^attr-private/findOnPageNextDrawable 01120051=^attr-private/findOnPagePreviousDrawable 01120052=^attr-private/floatingToolbarCloseDrawable 01120053=^attr-private/floatingToolbarDividerColor 01120054=^attr-private/floatingToolbarItemBackgroundBorderlessDrawable 01120055=^attr-private/floatingToolbarItemBackgroundDrawable 01120056=^attr-private/floatingToolbarOpenDrawable 01120057=^attr-private/foregroundInsidePadding 01120058=^attr-private/fragmentBreadCrumbsStyle 01120059=^attr-private/frameDuration 0112005a=^attr-private/framesCount 0112005b=^attr-private/fromBottom 0112005c=^attr-private/fromLeft 0112005d=^attr-private/fromRight 0112005e=^attr-private/fromTop 0112005f=^attr-private/gestureOverlayViewStyle 01120060=^attr-private/glowDot 01120061=^attr-private/glyphDrawable 01120062=^attr-private/glyphMap 01120063=^attr-private/hasRoundedCorners 01120064=^attr-private/headerLayout 01120065=^attr-private/headerRemoveIconIfEmpty 01120066=^attr-private/headerTextColor 01120067=^attr-private/hideWheelUntilFocused 01120068=^attr-private/horizontalProgressLayout 01120069=^attr-private/iconfactoryBadgeSize 0112006a=^attr-private/iconfactoryIconSize 0112006b=^attr-private/ignoreOffsetTopLimit 0112006c=^attr-private/initialActivityCount 0112006d=^attr-private/internalLayout 0112006e=^attr-private/internalMaxHeight 0112006f=^attr-private/internalMaxWidth 01120070=^attr-private/internalMinHeight 01120071=^attr-private/internalMinWidth 01120072=^attr-private/interpolatorX 01120073=^attr-private/interpolatorY 01120074=^attr-private/interpolatorZ 01120075=^attr-private/itemColor 01120076=^attr-private/itemLayout 01120077=^attr-private/keepDotActivated 01120078=^attr-private/keyboardViewStyle 01120079=^attr-private/layoutManager 0112007a=^attr-private/layout_alwaysShow 0112007b=^attr-private/layout_childType 0112007c=^attr-private/layout_hasNestedScrollIndicator 0112007d=^attr-private/layout_ignoreOffset 0112007e=^attr-private/layout_maxHeight 0112007f=^attr-private/layout_removeBorders 01120080=^attr-private/leftToRight 01120081=^attr-private/legacyLayout 01120082=^attr-private/lightRadius 01120083=^attr-private/lightY 01120084=^attr-private/lightZ 01120085=^attr-private/listItemLayout 01120086=^attr-private/listLayout 01120087=^attr-private/lockPatternStyle 01120088=^attr-private/magnifierColorOverlay 01120089=^attr-private/magnifierElevation 0112008a=^attr-private/magnifierHeight 0112008b=^attr-private/magnifierHorizontalOffset 0112008c=^attr-private/magnifierStyle 0112008d=^attr-private/magnifierVerticalOffset 0112008e=^attr-private/magnifierWidth 0112008f=^attr-private/magnifierZoom 01120090=^attr-private/majorWeightMax 01120091=^attr-private/majorWeightMin 01120092=^attr-private/maxCollapsedHeight 01120093=^attr-private/maxCollapsedHeightSmall 01120094=^attr-private/maxFileSize 01120095=^attr-private/maxItems 01120096=^attr-private/minorWeightMax 01120097=^attr-private/minorWeightMin 01120098=^attr-private/modifier 01120099=^attr-private/modifierState 0112009a=^attr-private/monthTextAppearance 0112009b=^attr-private/mountPoint 0112009c=^attr-private/mtpReserve 0112009d=^attr-private/multiChoiceItemLayout 0112009e=^attr-private/navigationButtonStyle 0112009f=^attr-private/needsDefaultBackgrounds 011200a0=^attr-private/notificationHeaderAppNameVisibility 011200a1=^attr-private/notificationHeaderIconSize 011200a2=^attr-private/notificationHeaderStyle 011200a3=^attr-private/notificationHeaderTextAppearance 011200a4=^attr-private/numDots 011200a5=^attr-private/opacityListDivider 011200a6=^attr-private/outKeycode 011200a7=^attr-private/paddingBottomNoButtons 011200a8=^attr-private/paddingTopNoTitle 011200a9=^attr-private/pageSpacing 011200aa=^attr-private/panelMenuIsCompact 011200ab=^attr-private/panelMenuListTheme 011200ac=^attr-private/panelMenuListWidth 011200ad=^attr-private/pathColor 011200ae=^attr-private/pointerIconAlias 011200af=^attr-private/pointerIconAllScroll 011200b0=^attr-private/pointerIconArrow 011200b1=^attr-private/pointerIconCell 011200b2=^attr-private/pointerIconContextMenu 011200b3=^attr-private/pointerIconCopy 011200b4=^attr-private/pointerIconCrosshair 011200b5=^attr-private/pointerIconGrab 011200b6=^attr-private/pointerIconGrabbing 011200b7=^attr-private/pointerIconHand 011200b8=^attr-private/pointerIconHandwriting 011200b9=^attr-private/pointerIconHelp 011200ba=^attr-private/pointerIconHorizontalDoubleArrow 011200bb=^attr-private/pointerIconNodrop 011200bc=^attr-private/pointerIconSpotAnchor 011200bd=^attr-private/pointerIconSpotHover 011200be=^attr-private/pointerIconSpotTouch 011200bf=^attr-private/pointerIconText 011200c0=^attr-private/pointerIconTopLeftDiagonalDoubleArrow 011200c1=^attr-private/pointerIconTopRightDiagonalDoubleArrow 011200c2=^attr-private/pointerIconVectorFill 011200c3=^attr-private/pointerIconVectorFillInverse 011200c4=^attr-private/pointerIconVectorStroke 011200c5=^attr-private/pointerIconVectorStrokeInverse 011200c6=^attr-private/pointerIconVerticalDoubleArrow 011200c7=^attr-private/pointerIconVerticalText 011200c8=^attr-private/pointerIconWait 011200c9=^attr-private/pointerIconZoomIn 011200ca=^attr-private/pointerIconZoomOut 011200cb=^attr-private/popupPromptView 011200cc=^attr-private/position 011200cd=^attr-private/preferenceActivityStyle 011200ce=^attr-private/preferenceFragmentListStyle 011200cf=^attr-private/preferenceFragmentPaddingSide 011200d0=^attr-private/preferenceFrameLayoutStyle 011200d1=^attr-private/preferenceHeaderPanelStyle 011200d2=^attr-private/preferenceListStyle 011200d3=^attr-private/preferencePanelStyle 011200d4=^attr-private/preserveIconSpacing 011200d5=^attr-private/primary 011200d6=^attr-private/productId 011200d7=^attr-private/progressBarCornerRadius 011200d8=^attr-private/progressLayout 011200d9=^attr-private/quickContactBadgeOverlay 011200da=^attr-private/quickContactWindowSize 011200db=^attr-private/regularColor 011200dc=^attr-private/relativeTimeDisambiguationText 011200dd=^attr-private/relativeTimeUnitDisplayLength 011200de=^attr-private/removable 011200df=^attr-private/removeBeforeMRelease 011200e0=^attr-private/request 011200e1=^attr-private/requestDetail 011200e2=^attr-private/resOutColor 011200e3=^attr-private/reverseLayout 011200e4=^attr-private/screenLayout 011200e5=^attr-private/scrollCaptureHint 011200e6=^attr-private/scrollIndicatorPaddingLeft 011200e7=^attr-private/scrollIndicatorPaddingRight 011200e8=^attr-private/searchDialogTheme 011200e9=^attr-private/searchResultListItemHeight 011200ea=^attr-private/searchWidgetCorpusItemBackground 011200eb=^attr-private/seekBarDialogPreferenceStyle 011200ec=^attr-private/seekBarPreferenceStyle 011200ed=^attr-private/segMinWidth 011200ee=^attr-private/segPointGap 011200ef=^attr-private/segSegGap 011200f0=^attr-private/selectionDivider 011200f1=^attr-private/selectionDividersDistance 011200f2=^attr-private/selectionScrollOffset 011200f3=^attr-private/showAtTop 011200f4=^attr-private/showRelative 011200f5=^attr-private/showSeekBarValue 011200f6=^attr-private/showTitle 011200f7=^attr-private/showWallpaper 011200f8=^attr-private/singleChoiceItemLayout 011200f9=^attr-private/spanCount 011200fa=^attr-private/stackFromEnd 011200fb=^attr-private/state_accessibility_focused 011200fc=^attr-private/state_ux_restricted 011200fd=^attr-private/storageDescription 011200fe=^attr-private/strokeCap 011200ff=^attr-private/successColor 01120100=^attr-private/tabLayout 01120101=^attr-private/textAppearanceAutoCorrectionSuggestion 01120102=^attr-private/textAppearanceEasyCorrectSuggestion 01120103=^attr-private/textAppearanceGrammarErrorSuggestion 01120104=^attr-private/textAppearanceMisspelledSuggestion 01120105=^attr-private/textColorOnAccent 01120106=^attr-private/textColorPrimaryActivated 01120107=^attr-private/textColorSearchUrl 01120108=^attr-private/textColorSecondaryActivated 01120109=^attr-private/textEditSuggestionContainerLayout 0112010a=^attr-private/textEditSuggestionHighlightStyle 0112010b=^attr-private/textUnderlineColor 0112010c=^attr-private/textUnderlineThickness 0112010d=^attr-private/thumbDrawable 0112010e=^attr-private/thumbMinHeight 0112010f=^attr-private/thumbMinWidth 01120110=^attr-private/toBottom 01120111=^attr-private/toLeft 01120112=^attr-private/toRight 01120113=^attr-private/toTop 01120114=^attr-private/toastFrameBackground 01120115=^attr-private/tooltipBackgroundColor 01120116=^attr-private/tooltipCornerRadius 01120117=^attr-private/tooltipFontSize 01120118=^attr-private/tooltipForegroundColor 01120119=^attr-private/tooltipFrameBackground 0112011a=^attr-private/tooltipHorizontalPadding 0112011b=^attr-private/tooltipVerticalPadding 0112011c=^attr-private/trackDrawable 0112011d=^attr-private/tracker 0112011e=^attr-private/trackerHeight 0112011f=^attr-private/unlockProfile 01120120=^attr-private/updatableSystem 01120121=^attr-private/useDisabledAlpha 01120122=^attr-private/vendorId 01120123=^attr-private/viewType 01120124=^attr-private/virtualButtonPressedDrawable 01120125=^attr-private/windowActionBarFullscreenDecorLayout 01120126=^attr-private/windowFixedHeightMajor 01120127=^attr-private/windowFixedHeightMinor 01120128=^attr-private/windowFixedWidthMajor 01120129=^attr-private/windowFixedWidthMinor 0112012a=^attr-private/windowOutsetBottom 0112012b=^attr-private/yearListItemActivatedTextAppearance 0112012c=^attr-private/showWallpaper 0112012d=^attr-private/singleChoiceItemLayout 0112012e=^attr-private/spanCount 0112012f=^attr-private/stackFromEnd 01120130=^attr-private/state_accessibility_focused 01120131=^attr-private/state_ux_restricted 01120132=^attr-private/storageDescription 01120133=^attr-private/successColor 01120134=^attr-private/tabLayout 01120135=^attr-private/textAppearanceAutoCorrectionSuggestion 01120136=^attr-private/textAppearanceEasyCorrectSuggestion 01120137=^attr-private/textAppearanceGrammarErrorSuggestion 01120138=^attr-private/textAppearanceMisspelledSuggestion 01120139=^attr-private/textColorOnAccent 0112013a=^attr-private/textColorPrimaryActivated 0112013b=^attr-private/textColorSearchUrl 0112013c=^attr-private/textColorSecondaryActivated 0112013d=^attr-private/textEditSuggestionContainerLayout 0112013e=^attr-private/textEditSuggestionHighlightStyle 0112013f=^attr-private/textUnderlineColor 01120140=^attr-private/textUnderlineThickness 01120141=^attr-private/thumbDrawable 01120142=^attr-private/thumbMinHeight 01120143=^attr-private/thumbMinWidth 01120144=^attr-private/toBottom 01120145=^attr-private/toLeft 01120146=^attr-private/toRight 01120147=^attr-private/toTop 01120148=^attr-private/toastFrameBackground 01120149=^attr-private/tooltipBackgroundColor 0112014a=^attr-private/tooltipForegroundColor 0112014b=^attr-private/tooltipFrameBackground 0112014c=^attr-private/trackDrawable 0112014d=^attr-private/unlockProfile 0112014e=^attr-private/updatableSystem 0112014f=^attr-private/useDisabledAlpha 01120150=^attr-private/vendorId 01120151=^attr-private/viewType 01120152=^attr-private/virtualButtonPressedDrawable 01120153=^attr-private/windowActionBarFullscreenDecorLayout 01120154=^attr-private/windowFixedHeightMajor 01120155=^attr-private/windowFixedHeightMinor 01120156=^attr-private/windowFixedWidthMajor 01120157=^attr-private/windowFixedWidthMinor 01120158=^attr-private/windowOutsetBottom 01120159=^attr-private/yearListItemActivatedTextAppearance 01130000=fraction/config_autoBrightnessAdjustmentMaxGamma 01130001=fraction/config_biometricNotificationFrrThreshold 01130002=fraction/config_dimBehindFadeDuration 01130003=fraction/config_maximumScreenDimRatio 01130004=fraction/config_prescaleAbsoluteVolume_index1 01130005=fraction/config_prescaleAbsoluteVolume_index2 01130006=fraction/config_prescaleAbsoluteVolume_index3 01130007=fraction/config_screenAutoBrightnessDozeScaleFactor 01130008=fraction/docked_stack_divider_fixed_ratio 01130009=fraction/global_actions_horizontal_padding_percentage 0113000a=fraction/global_actions_vertical_padding_percentage 0113000b=fraction/input_extract_action_margin_bottom 0113000c=fraction/input_extract_layout_height 0113000d=fraction/input_extract_layout_padding_left 0113000e=fraction/input_extract_layout_padding_left_no_action 0113000f=fraction/input_extract_layout_padding_right 01130010=fraction/input_extract_text_margin_bottom 01130011=fraction/thumbnail_fullscreen_scale 01130012=plurals/duration_seconds 01130013=plurals/duration_minutes 01130014=plurals/duration_hours 01130015=plurals/wifi_available 01130016=plurals/wifi_available_detailed 01130017=plurals/matches_found 01130018=plurals/restr_pin_countdown 01140000=menu/language_selection_list 01140001=menu/webview_copy 01140002=menu/webview_find 01140003=plurals/last_num_days 01140004=plurals/duration_seconds 01140005=plurals/duration_minutes 01140006=plurals/duration_hours 01140007=plurals/duration_minutes_shortest 01140008=plurals/duration_hours_shortest 01140009=plurals/duration_days_shortest 0114000a=plurals/duration_years_shortest 0114000b=plurals/duration_minutes_shortest_future 0114000c=plurals/duration_hours_shortest_future 0114000d=plurals/duration_days_shortest_future 0114000e=plurals/duration_years_shortest_future 0114000f=plurals/duration_minutes_relative 01140010=plurals/duration_hours_relative 01140011=plurals/duration_days_relative 01140012=plurals/duration_years_relative 01140013=plurals/duration_minutes_relative_future 01140014=plurals/duration_hours_relative_future 01140015=plurals/duration_days_relative_future 01140016=plurals/duration_years_relative_future 01140017=plurals/wifi_available 01140018=plurals/wifi_available_detailed 01140019=plurals/matches_found 0114001a=plurals/restr_pin_countdown 0114001b=plurals/zen_mode_duration_minutes_summary 0114001c=plurals/zen_mode_duration_minutes_summary_short 0114001d=plurals/zen_mode_duration_hours_summary 0114001e=plurals/zen_mode_duration_hours_summary_short 0114001f=plurals/zen_mode_duration_minutes 01140020=plurals/zen_mode_duration_minutes_short 01140021=plurals/zen_mode_duration_hours 01140022=plurals/zen_mode_duration_hours_short 01140023=plurals/selected_count 01150000=plurals/pinpuk_attempts 01150001=plurals/bugreport_countdown 01150002=plurals/duration_days_relative 01150003=plurals/duration_days_relative_future 01150004=plurals/duration_days_shortest 01150005=plurals/duration_days_shortest_future 01150006=plurals/duration_hours_relative 01150007=plurals/duration_hours_relative_future 01150008=plurals/duration_hours_shortest 01150009=plurals/duration_hours_shortest_future 0115000a=plurals/duration_minutes_relative 0115000b=plurals/duration_minutes_relative_future 0115000c=plurals/duration_minutes_shortest 0115000d=plurals/duration_minutes_shortest_future 0115000e=plurals/duration_years_relative 0115000f=plurals/duration_years_relative_future 01150010=plurals/duration_years_shortest 01150011=plurals/duration_years_shortest_future 01150012=plurals/file_count 01150013=plurals/kg_too_many_failed_attempts_countdown 01150014=plurals/last_num_days 01150015=plurals/matches_found 01150016=plurals/pinpuk_attempts 01150017=plurals/restr_pin_countdown 01150018=plurals/selected_count 01150019=plurals/ssl_ca_cert_warning 0115001a=plurals/zen_mode_duration_hours 0115001b=plurals/zen_mode_duration_hours_short 0115001c=plurals/zen_mode_duration_hours_summary 0115001d=plurals/zen_mode_duration_hours_summary_short 0115001e=plurals/zen_mode_duration_minutes 0115001f=plurals/zen_mode_duration_minutes_short 01150020=plurals/zen_mode_duration_minutes_summary 01150021=plurals/zen_mode_duration_minutes_summary_short 01150022=plurals/zen_mode_duration_minutes_summary 01150023=plurals/zen_mode_duration_minutes_summary_short 01160000=^attr-private/isLightTheme 01160001=^attr-private/textColorPrimaryActivated 01160002=^attr-private/textColorSecondaryActivated 01160003=^attr-private/textColorSearchUrl 01160004=^attr-private/searchWidgetCorpusItemBackground 01160005=^attr-private/textAppearanceEasyCorrectSuggestion 01160006=^attr-private/textAppearanceMisspelledSuggestion 01160007=^attr-private/textAppearanceAutoCorrectionSuggestion 01160008=^attr-private/textUnderlineColor 01160009=^attr-private/textUnderlineThickness 0116000a=^attr-private/errorMessageBackground 0116000b=^attr-private/errorMessageAboveBackground 0116000c=^attr-private/searchResultListItemHeight 0116000d=^attr-private/dropdownListPreferredItemHeight 0116000e=^attr-private/windowActionBarFullscreenDecorLayout 0116000f=^attr-private/floatingToolbarCloseDrawable 01160010=^attr-private/floatingToolbarForegroundColor 01160011=^attr-private/floatingToolbarItemBackgroundBorderlessDrawable 01160012=^attr-private/floatingToolbarItemBackgroundDrawable 01160013=^attr-private/floatingToolbarOpenDrawable 01160014=^attr-private/floatingToolbarPopupBackgroundDrawable 01160015=^attr-private/alertDialogButtonGroupStyle 01160016=^attr-private/alertDialogCenterButtons 01160017=^attr-private/panelMenuIsCompact 01160018=^attr-private/panelMenuListWidth 01160019=^attr-private/panelMenuListTheme 0116001a=^attr-private/gestureOverlayViewStyle 0116001b=^attr-private/quickContactBadgeOverlay 0116001c=^attr-private/fragmentBreadCrumbsStyle 0116001d=^attr-private/activityChooserViewStyle 0116001e=^attr-private/actionModePopupWindowStyle 0116001f=^attr-private/preferenceActivityStyle 01160020=^attr-private/seekBarDialogPreferenceStyle 01160021=^attr-private/preferencePanelStyle 01160022=^attr-private/preferenceHeaderPanelStyle 01160023=^attr-private/preferenceListStyle 01160024=^attr-private/preferenceFragmentListStyle 01160025=^attr-private/preferenceFragmentPaddingSide 01160026=^attr-private/seekBarPreferenceStyle 01160027=^attr-private/textEditSuggestionContainerLayout 01160028=^attr-private/textEditSuggestionHighlightStyle 01160029=^attr-private/dialogTitleIconsDecorLayout 0116002a=^attr-private/dialogCustomTitleDecorLayout 0116002b=^attr-private/dialogTitleDecorLayout 0116002c=^attr-private/toastFrameBackground 0116002d=^attr-private/searchDialogTheme 0116002e=^attr-private/preferenceFrameLayoutStyle 0116002f=^attr-private/accessibilityFocusedDrawable 01160030=^attr-private/findOnPageNextDrawable 01160031=^attr-private/findOnPagePreviousDrawable 01160032=^attr-private/colorSwitchThumbNormal 01160033=^attr-private/lightY 01160034=^attr-private/lightZ 01160035=^attr-private/lightRadius 01160036=^attr-private/windowFixedWidthMajor 01160037=^attr-private/windowFixedHeightMinor 01160038=^attr-private/windowFixedWidthMinor 01160039=^attr-private/windowFixedHeightMajor 0116003a=^attr-private/windowOutsetBottom 0116003b=^attr-private/buttonPanelSideLayout 0116003c=^attr-private/listLayout 0116003d=^attr-private/multiChoiceItemLayout 0116003e=^attr-private/singleChoiceItemLayout 0116003f=^attr-private/listItemLayout 01160040=^attr-private/progressLayout 01160041=^attr-private/horizontalProgressLayout 01160042=^attr-private/showTitle 01160043=^attr-private/needsDefaultBackgrounds 01160044=^attr-private/controllerType 01160045=^attr-private/allowStacking 01160046=^attr-private/activityOpenRemoteViewsEnterAnimation 01160047=^attr-private/foregroundInsidePadding 01160048=^attr-private/paddingBottomNoButtons 01160049=^attr-private/paddingTopNoTitle 0116004a=^attr-private/checkMarkGravity 0116004b=^attr-private/thumbDrawable 0116004c=^attr-private/thumbMinWidth 0116004d=^attr-private/thumbMinHeight 0116004e=^attr-private/trackDrawable 0116004f=^attr-private/backgroundRight 01160050=^attr-private/backgroundLeft 01160051=^attr-private/position 01160052=^attr-private/drawableAlpha 01160053=^attr-private/borderTop 01160054=^attr-private/borderBottom 01160055=^attr-private/borderLeft 01160056=^attr-private/borderRight 01160057=^attr-private/layout_removeBorders 01160058=^attr-private/preserveIconSpacing 01160059=^attr-private/maxItems 0116005a=^attr-private/useDisabledAlpha 0116005b=^attr-private/resOutColor 0116005c=^attr-private/clickColor 0116005d=^attr-private/tabLayout 0116005e=^attr-private/popupPromptView 0116005f=^attr-private/disableChildrenWhenDisabled 01160060=^attr-private/internalLayout 01160061=^attr-private/headerTextColor 01160062=^attr-private/yearListItemActivatedTextAppearance 01160063=^attr-private/dialogMode 01160064=^attr-private/quickContactWindowSize 01160065=^attr-private/majorWeightMin 01160066=^attr-private/minorWeightMin 01160067=^attr-private/majorWeightMax 01160068=^attr-private/minorWeightMax 01160069=^attr-private/monthTextAppearance 0116006a=^attr-private/daySelectorColor 0116006b=^attr-private/dayHighlightColor 0116006c=^attr-private/calendarViewMode 0116006d=^attr-private/selectionDivider 0116006e=^attr-private/selectionDividerHeight 0116006f=^attr-private/selectionDividersDistance 01160070=^attr-private/internalMinHeight 01160071=^attr-private/internalMaxHeight 01160072=^attr-private/internalMinWidth 01160073=^attr-private/internalMaxWidth 01160074=^attr-private/virtualButtonPressedDrawable 01160075=^attr-private/hideWheelUntilFocused 01160076=^attr-private/legacyLayout 01160077=^attr-private/frameDuration 01160078=^attr-private/framesCount 01160079=^attr-private/opticalInsetLeft 0116007a=^attr-private/opticalInsetTop 0116007b=^attr-private/opticalInsetRight 0116007c=^attr-private/opticalInsetBottom 0116007d=^attr-private/interpolatorX 0116007e=^attr-private/interpolatorY 0116007f=^attr-private/interpolatorZ 01160080=^attr-private/removeBeforeMRelease 01160081=^attr-private/state_accessibility_focused 01160082=^attr-private/initialActivityCount 01160083=^attr-private/expandActivityOverflowButtonDrawable 01160084=^attr-private/keyboardViewStyle 01160085=^attr-private/aspect 01160086=^attr-private/pathColor 01160087=^attr-private/regularColor 01160088=^attr-private/errorColor 01160089=^attr-private/successColor 0116008a=^attr-private/closeItemLayout 0116008b=^attr-private/defaultQueryHint 0116008c=^attr-private/pointerIconArrow 0116008d=^attr-private/pointerIconSpotHover 0116008e=^attr-private/pointerIconSpotTouch 0116008f=^attr-private/pointerIconSpotAnchor 01160090=^attr-private/pointerIconContextMenu 01160091=^attr-private/pointerIconHand 01160092=^attr-private/pointerIconHelp 01160093=^attr-private/pointerIconWait 01160094=^attr-private/pointerIconCell 01160095=^attr-private/pointerIconCrosshair 01160096=^attr-private/pointerIconText 01160097=^attr-private/pointerIconVerticalText 01160098=^attr-private/pointerIconAlias 01160099=^attr-private/pointerIconCopy 0116009a=^attr-private/pointerIconNodrop 0116009b=^attr-private/pointerIconAllScroll 0116009c=^attr-private/pointerIconHorizontalDoubleArrow 0116009d=^attr-private/pointerIconVerticalDoubleArrow 0116009e=^attr-private/pointerIconTopRightDiagonalDoubleArrow 0116009f=^attr-private/pointerIconTopLeftDiagonalDoubleArrow 011600a0=^attr-private/pointerIconZoomIn 011600a1=^attr-private/pointerIconZoomOut 011600a2=^attr-private/pointerIconGrab 011600a3=^attr-private/pointerIconGrabbing 011600a4=^attr-private/mountPoint 011600a5=^attr-private/storageDescription 011600a6=^attr-private/primary 011600a7=^attr-private/removable 011600a8=^attr-private/emulated 011600a9=^attr-private/mtpReserve 011600aa=^attr-private/allowMassStorage 011600ab=^attr-private/maxFileSize 011600ac=^attr-private/screenLayout 011600ad=^attr-private/headerLayout 011600ae=^attr-private/headerRemoveIconIfEmpty 011600af=^attr-private/locale 011600b0=^attr-private/vendorId 011600b1=^attr-private/productId 011600b2=^attr-private/externalRouteEnabledDrawable 011600b3=^attr-private/pageSpacing 011600b4=^attr-private/scrollIndicatorPaddingLeft 011600b5=^attr-private/scrollIndicatorPaddingRight 011600b6=^attr-private/dotSize 011600b7=^attr-private/numDots 011600b8=^attr-private/glowDot 011600b9=^attr-private/leftToRight 011600ba=^attr-private/layout_childType 011600bb=^attr-private/itemLayout 011600bc=^attr-private/itemColor 011600bd=^attr-private/navigationButtonStyle 011600be=^attr-private/maxCollapsedHeight 011600bf=^attr-private/maxCollapsedHeightSmall 011600c0=^attr-private/showRelative 011600c1=^attr-private/layout_alwaysShow 011600c2=^attr-private/layout_ignoreOffset 011600c3=^attr-private/layout_hasNestedScrollIndicator 011600c4=^attr-private/cantSaveState 011600c5=^attr-private/systemUserOnly 011600c6=^attr-private/alwaysFocusable 011600c7=^attr-private/minimalWidth 011600c8=^attr-private/minimalHeight 01170000=xml/apns 01170001=xml/audio_assets 01170002=xml/autofill_compat_accessibility_service 01170003=xml/autotext 01170004=xml/bookmarks 01170005=xml/color_extraction 01170006=xml/config_user_types 01170007=xml/config_webview_packages 01170008=xml/default_zen_mode_config 01170009=xml/global_keys 0117000a=xml/haptic_feedback_customization 0117000b=xml/haptic_feedback_customization_source_rotary_encoder 0117000c=xml/haptic_feedback_customization_source_touchscreen 0117000d=xml/irq_device_map 0117000e=xml/kg_password_kbd_numeric 0117000f=xml/password_kbd_extension 01170010=xml/password_kbd_numeric 01170011=xml/password_kbd_popup_template 01170012=xml/password_kbd_qwerty 01170013=xml/password_kbd_qwerty_shifted 01170014=xml/password_kbd_symbols 01170015=xml/password_kbd_symbols_shift 01170016=xml/power_profile 01170017=xml/power_profile_test 01170018=xml/sms_7bit_translation_table 01170019=xml/sms_short_codes 0117001a=xml/storage_list 01a60000=integer/config_motionStandardFastSpatialStiffness 01a60001=integer/config_motionStandardFastEffectStiffness 01a60002=integer/config_motionStandardDefaultSpatialStiffness 01a60003=integer/config_motionStandardDefaultEffectStiffness 01a60004=integer/config_motionStandardSlowSpatialStiffness 01a60005=integer/config_motionStandardSlowEffectStiffness 01a60006=integer/config_motionExpressiveFastSpatialStiffness 01a60007=integer/config_motionExpressiveFastEffectStiffness 01a60008=integer/config_motionExpressiveDefaultSpatialStiffness 01a60009=integer/config_motionExpressiveDefaultEffectStiffness 01a6000a=integer/config_motionExpressiveSlowSpatialStiffness 01a6000b=integer/config_motionExpressiveSlowEffectStiffness 01ae0000=color/system_inverse_on_surface_light 01ae0001=color/system_inverse_primary_light 01ae0002=color/system_inverse_surface_light 01ae0003=color/system_scrim_light 01ae0004=color/system_shadow_light 01ae0005=color/system_surface_tint_light 01ae0006=color/system_inverse_on_surface_dark 01ae0007=color/system_inverse_primary_dark 01ae0008=color/system_inverse_surface_dark 01ae0009=color/system_scrim_dark 01ae000a=color/system_shadow_dark 01ae000b=color/system_surface_tint_dark 01af0000=dimen/config_motionStandardFastSpatialDamping 01af0001=dimen/config_motionStandardFastEffectDamping 01af0002=dimen/config_motionStandardDefaultSpatialDamping 01af0003=dimen/config_motionStandardDefaultEffectDamping 01af0004=dimen/config_motionStandardSlowSpatialDamping 01af0005=dimen/config_motionStandardSlowEffectDamping 01af0006=dimen/config_motionExpressiveFastSpatialDamping 01af0007=dimen/config_motionExpressiveFastEffectDamping 01af0008=dimen/config_motionExpressiveDefaultSpatialDamping 01af0009=dimen/config_motionExpressiveDefaultEffectDamping 01af000a=dimen/config_motionExpressiveSlowSpatialDamping 01af000b=dimen/config_motionExpressiveSlowEffectDamping 01af000c=dimen/config_shapeCornerRadiusXsmall 01af000d=dimen/config_shapeCornerRadiusSmall 01af000e=dimen/config_shapeCornerRadiusMedium 01af000f=dimen/config_shapeCornerRadiusLarge 01af0010=dimen/config_shapeCornerRadiusXlarge 01b00000=string/config_defaultOnDeviceIntelligenceService 01b00001=string/config_defaultOnDeviceSandboxedInferenceService 01b00002=string/config_defaultOnDeviceIntelligenceDeviceConfigNamespace 01b20001=id/accessibilityActionSetExtendedSelection 01b30000=attr/optional 01b30001=attr/alternateLauncherIcons 01b30002=attr/alternateLauncherLabels 01b40000=string/config_systemDependencyInstaller 01b40002=string/config_systemVendorIntelligence 01b70000=attr/optional 01b70001=attr/adServiceTypes 01b70002=attr/languageSettingsActivity 01b70003=attr/dreamCategory 01b70004=attr/backgroundPermission 01b70005=attr/supplementalDescription 01b70006=attr/intentMatchingFlags 01b70007=attr/layoutLabel 01b7000a=attr/pageSizeCompat 01b7000b=attr/wantsRoleHolderPriority 01b80000=color/system_surface_disabled 01b80001=color/system_on_surface_disabled 01b80002=color/system_outline_disabled 01b80003=color/system_error_0 01b80004=color/system_error_10 01b80005=color/system_error_50 01b80006=color/system_error_100 01b80007=color/system_error_200 01b80008=color/system_error_300 01b80009=color/system_error_400 01b8000a=color/system_error_500 01b8000b=color/system_error_600 01b8000c=color/system_error_700 01b8000d=color/system_error_800 01b8000e=color/system_error_900 01b8000f=color/system_error_1000 01ba0000=string/config_defaultRetailDemo 01ba0001=string/config_defaultWallet 01bd0000=attr/defaultLocale 01bd0001=attr/isVirtualDeviceOnly 01bd0004=attr/featureFlag 01bd0005=attr/systemUserOnly 01bd0006=attr/allow 01bd0007=attr/query 01bd0008=attr/queryPrefix 01bd0009=attr/queryPattern 01bd000a=attr/queryAdvancedPattern 01bd000b=attr/querySuffix 01bd000c=attr/fragmentPrefix 01bd000d=attr/fragmentPattern 01bd000e=attr/fragmentAdvancedPattern 01bd000f=attr/fragmentSuffix 01bd0010=attr/useBoundsForWidth 01bd0011=attr/autoTransact 01bd0012=attr/windowOptOutEdgeToEdgeEnforcement 01bd0013=attr/requireContentUriPermissionFromCaller 01bd0015=attr/useLocalePreferredLineHeightForMinimum 01bd0016=attr/contentSensitivity 01bd0017=attr/supportsConnectionlessStylusHandwriting 01bd0018=attr/shouldDefaultToObserveMode 01bd0019=attr/allowCrossUidActivitySwitchFromBelow 01bd001a=attr/shiftDrawingOffsetForStartOverhang 01bd001b=attr/windowIsFrameRatePowerSavingsBalanced 01be0000=bool/config_safetyProtectionEnabled 01be0001=bool/config_enableDefaultNotes 01be0002=bool/config_enableDefaultNotesForWorkProfile 01c90000=color/system_primary_container_light 01c90001=color/system_on_primary_container_light 01c90002=color/system_primary_light 01c90003=color/system_on_primary_light 01c90004=color/system_secondary_container_light 01c90005=color/system_on_secondary_container_light 01c90006=color/system_secondary_light 01c90007=color/system_on_secondary_light 01c90008=color/system_tertiary_container_light 01c90009=color/system_on_tertiary_container_light 01c9000a=color/system_tertiary_light 01c9000b=color/system_on_tertiary_light 01c9000c=color/system_background_light 01c9000d=color/system_on_background_light 01c9000e=color/system_surface_light 01c9000f=color/system_on_surface_light 01c90010=color/system_surface_container_low_light 01c90011=color/system_surface_container_lowest_light 01c90012=color/system_surface_container_light 01c90013=color/system_surface_container_high_light 01c90014=color/system_surface_container_highest_light 01c90015=color/system_surface_bright_light 01c90016=color/system_surface_dim_light 01c90017=color/system_surface_variant_light 01c90018=color/system_on_surface_variant_light 01c90019=color/system_outline_light 01c9001a=color/system_error_light 01c9001b=color/system_on_error_light 01c9001c=color/system_error_container_light 01c9001d=color/system_on_error_container_light 01c9002a=color/system_control_activated_light 01c9002b=color/system_control_normal_light 01c9002c=color/system_control_highlight_light 01c9002d=color/system_text_primary_inverse_light 01c9002e=color/system_text_secondary_and_tertiary_inverse_light 01c9002f=color/system_text_primary_inverse_disable_only_light 01c90030=color/system_text_secondary_and_tertiary_inverse_disabled_light 01c90031=color/system_text_hint_inverse_light 01c90032=color/system_palette_key_color_primary_light 01c90033=color/system_palette_key_color_secondary_light 01c90034=color/system_palette_key_color_tertiary_light 01c90035=color/system_palette_key_color_neutral_light 01c90036=color/system_palette_key_color_neutral_variant_light 01c90037=color/system_primary_container_dark 01c90038=color/system_on_primary_container_dark 01c90039=color/system_primary_dark 01c9003a=color/system_on_primary_dark 01c9003b=color/system_secondary_container_dark 01c9003c=color/system_on_secondary_container_dark 01c9003d=color/system_secondary_dark 01c9003e=color/system_on_secondary_dark 01c9003f=color/system_tertiary_container_dark 01c90040=color/system_on_tertiary_container_dark 01c90041=color/system_tertiary_dark 01c90042=color/system_on_tertiary_dark 01c90043=color/system_background_dark 01c90044=color/system_on_background_dark 01c90045=color/system_surface_dark 01c90046=color/system_on_surface_dark 01c90047=color/system_surface_container_low_dark 01c90048=color/system_surface_container_lowest_dark 01c90049=color/system_surface_container_dark 01c9004a=color/system_surface_container_high_dark 01c9004b=color/system_surface_container_highest_dark 01c9004c=color/system_surface_bright_dark 01c9004d=color/system_surface_dim_dark 01c9004e=color/system_surface_variant_dark 01c9004f=color/system_on_surface_variant_dark 01c90050=color/system_outline_dark 01c90051=color/system_error_dark 01c90052=color/system_on_error_dark 01c90053=color/system_error_container_dark 01c90054=color/system_on_error_container_dark 01c90061=color/system_control_activated_dark 01c90062=color/system_control_normal_dark 01c90063=color/system_control_highlight_dark 01c90064=color/system_text_primary_inverse_dark 01c90065=color/system_text_secondary_and_tertiary_inverse_dark 01c90066=color/system_text_primary_inverse_disable_only_dark 01c90067=color/system_text_secondary_and_tertiary_inverse_disabled_dark 01c90068=color/system_text_hint_inverse_dark 01c90069=color/system_palette_key_color_primary_dark 01c9006a=color/system_palette_key_color_secondary_dark 01c9006b=color/system_palette_key_color_tertiary_dark 01c9006c=color/system_palette_key_color_neutral_dark 01c9006d=color/system_palette_key_color_neutral_variant_dark 01c9006e=color/system_primary_fixed 01c9006f=color/system_primary_fixed_dim 01c90070=color/system_on_primary_fixed 01c90071=color/system_on_primary_fixed_variant 01c90072=color/system_secondary_fixed 01c90073=color/system_secondary_fixed_dim 01c90074=color/system_on_secondary_fixed 01c90075=color/system_on_secondary_fixed_variant 01c90076=color/system_tertiary_fixed 01c90077=color/system_tertiary_fixed_dim 01c90078=color/system_on_tertiary_fixed 01c90079=color/system_on_tertiary_fixed_variant 01c9007a=color/system_outline_variant_light 01c9007b=color/system_outline_variant_dark 01ca0000=dimen/config_viewConfigurationHandwritingGestureLineMargin 01cb0000=string/config_systemWearHealthService 01cb0001=string/config_defaultNotes 01cb0002=string/config_systemFinancedDeviceController 01cb0003=string/config_systemCallStreaming 01cd0000=id/bold 01cd0001=id/italic 01cd0002=id/underline 01cd0003=id/accessibilityActionScrollInDirection 01ce0000=attr/handwritingBoundsOffsetLeft 01ce0001=attr/handwritingBoundsOffsetTop 01ce0002=attr/handwritingBoundsOffsetRight 01ce0003=attr/handwritingBoundsOffsetBottom 01ce0004=attr/accessibilityDataSensitive 01ce0005=attr/enableTextStylingShortcuts 01ce0006=attr/requiredDisplayCategory 01ce0008=attr/visualQueryDetectionService 01ce0009=attr/physicalKeyboardHintLanguageTag 01ce000a=attr/physicalKeyboardHintLayoutType 01ce000b=attr/allowSharedIsolatedProcess 01ce000c=attr/keyboardLocale 01ce000d=attr/keyboardLayoutType 01ce000e=attr/allowUpdateOwnership 01ce000f=attr/isCredential 01ce0010=attr/searchResultHighlightColor 01ce0011=attr/focusedSearchResultHighlightColor 01ce0012=attr/stylusHandwritingSettingsActivity 01ce0013=attr/windowNoMoveAnimation 01ce0014=attr/settingsSubtitle 01ce0015=attr/capability 01cf0000=bool/config_preventImeStartupUnlessTextEditor 01cf0001=bool/config_enableQrCodeScannerOnLockScreen 01d80000=drawable/ic_safety_protection 01d90000=array/config_optionalIpSecAlgorithms 01dc0000=string/config_systemSupervision 01dc0001=string/config_devicePolicyManagement 01dc0002=string/config_systemAppProtectionService 01dc0003=string/config_systemAutomotiveCalendarSyncManager 01dc0004=string/config_defaultAutomotiveNavigation 01dc0005=string/safety_protection_display_text 01dc0006=string/config_systemSettingsIntelligence 01dc0007=string/config_systemBluetoothStack 01dd0000=style/TextAppearance.DeviceDefault.Headline 01de0004=id/accessibilityActionShowTextSuggestions 01de0005=id/inputExtractAction 01de0006=id/inputExtractAccessories 01df0000=attr/sharedUserMaxSdkVersion 01df0001=attr/requiredSplitTypes 01df0002=attr/splitTypes 01df0003=attr/canDisplayOnRemoteDevices 01df0004=attr/supportedTypes 01df0005=attr/resetEnabledSettingsOnAppDataCleared 01df0006=attr/supportsStylusHandwriting 01df0007=attr/showClockAndComplications 01df0008=attr/gameSessionService 01df0009=attr/supportsBatteryGameMode 01df000a=attr/supportsPerformanceGameMode 01df000b=attr/allowGameAngleDriver 01df000c=attr/allowGameDownscaling 01df000d=attr/allowGameFpsOverride 01df000e=attr/localeConfig 01df000f=attr/showBackdrop 01df0012=attr/preferKeepClear 01df0013=attr/autoHandwritingEnabled 01df0014=attr/fromExtendLeft 01df0015=attr/fromExtendTop 01df0016=attr/fromExtendRight 01df0017=attr/fromExtendBottom 01df0018=attr/toExtendLeft 01df0019=attr/toExtendTop 01df001a=attr/toExtendRight 01df001b=attr/toExtendBottom 01df001c=attr/tileService 01df001d=attr/windowSplashScreenBehavior 01df001e=attr/allowUntrustedActivityEmbedding 01df001f=attr/knownActivityEmbeddingCerts 01df0020=attr/intro 01df0021=attr/enableOnBackInvokedCallback 01df0022=attr/supportsInlineSuggestionsWithTouchExploration 01df0023=attr/lineBreakStyle 01df0024=attr/lineBreakWordStyle 01df0025=attr/maxDrawableWidth 01df0026=attr/maxDrawableHeight 01df0027=attr/backdropColor 01fe0000=id/accessibilityActionDragStart 01fe0001=id/accessibilityActionDragDrop 01fe0002=id/accessibilityActionDragCancel 01ff0000=attr/shouldUseDefaultUnfoldTransition ================================================ FILE: jadx-core/src/main/resources/export/android/app.build.gradle.tmpl ================================================ plugins { id 'com.android.application' } android { namespace "{{applicationId}}" compileSdkVersion {{compileSdkVersion}} defaultConfig { applicationId '{{applicationId}}' minSdkVersion {{minSdkVersion}} //noinspection ExpiringTargetSdkVersion,ExpiredTargetSdkVersion targetSdkVersion {{targetSdkVersion}} versionCode {{versionCode}} versionName "{{versionName}}" {{additionalOptions}} testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { abortOnError false } buildFeatures { buildConfig = false } } dependencies { // TODO: dependencies } ================================================ FILE: jadx-core/src/main/resources/export/android/build.gradle.tmpl ================================================ buildscript { repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.10.1' } } allprojects { repositories { google() mavenCentral() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: jadx-core/src/main/resources/export/android/lib.build.gradle.tmpl ================================================ plugins { id 'com.android.library' } android { namespace '{{packageId}}' compileSdk {{compileSdkVersion}} defaultConfig { minSdk {{minSdkVersion}} {{additionalOptions}} } buildTypes { release { minifyEnabled false } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { abortOnError false } buildFeatures { buildConfig = false } } dependencies { // TODO: dependencies } ================================================ FILE: jadx-core/src/main/resources/export/android/settings.gradle.tmpl ================================================ rootProject.name = '{{projectName}}' include '{{mainModuleName}}' ================================================ FILE: jadx-core/src/main/resources/export/java/build.gradle.kts.tmpl ================================================ plugins { java } repositories { google() mavenCentral() } dependencies { // some dependencies } ================================================ FILE: jadx-core/src/main/resources/export/java/settings.gradle.kts.tmpl ================================================ rootProject.name = "{{projectName}}" include("app") ================================================ FILE: jadx-core/src/main/resources/jadx/core/deobf/conditions/tlds.txt ================================================ # All tld domains # Source: https://data.iana.org/TLD/tlds-alpha-by-domain.txt # Version 2024102400, Last Updated Thu Oct 24 07:07:01 2024 UTC # ASCII domains aaa aarp abb abbott abbvie abc able abogado abudhabi ac academy accenture accountant accountants aco actor ad ads adult ae aeg aero aetna af afl africa ag agakhan agency ai aig airbus airforce airtel akdn al alibaba alipay allfinanz allstate ally alsace alstom am amazon americanexpress americanfamily amex amfam amica amsterdam analytics android anquan anz ao aol apartments app apple aq aquarelle ar arab aramco archi army arpa art arte as asda asia associates at athleta attorney au auction audi audible audio auspost author auto autos aw aws ax axa az azure ba baby baidu banamex band bank bar barcelona barclaycard barclays barefoot bargains baseball basketball bauhaus bayern bb bbc bbt bbva bcg bcn bd be beats beauty beer bentley berlin best bestbuy bet bf bg bh bharti bi bible bid bike bing bingo bio biz bj black blackfriday blockbuster blog bloomberg blue bm bms bmw bn bnpparibas bo boats boehringer bofa bom bond boo book booking bosch bostik boston bot boutique box br bradesco bridgestone broadway broker brother brussels bs bt build builders business buy buzz bv bw by bz bzh ca cab cafe cal call calvinklein cam camera camp canon capetown capital capitalone car caravan cards care career careers cars casa case cash casino cat catering catholic cba cbn cbre cc cd center ceo cern cf cfa cfd cg ch chanel channel charity chase chat cheap chintai christmas chrome church ci cipriani circle cisco citadel citi citic city ck cl claims cleaning click clinic clinique clothing cloud club clubmed cm cn co coach codes coffee college cologne com commbank community company compare computer comsec condos construction consulting contact contractors cooking cool coop corsica country coupon coupons courses cpa cr credit creditcard creditunion cricket crown crs cruise cruises cu cuisinella cv cw cx cy cymru cyou cz dad dance data date dating datsun day dclk dds de deal dealer deals degree delivery dell deloitte delta democrat dental dentist desi design dev dhl diamonds diet digital direct directory discount discover dish diy dj dk dm dnp do docs doctor dog domains dot download drive dtv dubai dunlop dupont durban dvag dvr dz earth eat ec eco edeka edu education ee eg email emerck energy engineer engineering enterprises epson equipment er ericsson erni es esq estate et eu eurovision eus events exchange expert exposed express extraspace fage fail fairwinds faith family fan fans farm farmers fashion fast fedex feedback ferrari ferrero fi fidelity fido film final finance financial fire firestone firmdale fish fishing fit fitness fj fk flickr flights flir florist flowers fly fm fo foo food football ford forex forsale forum foundation fox fr free fresenius frl frogans frontier ftr fujitsu fun fund furniture futbol fyi ga gal gallery gallo gallup game games gap garden gay gb gbiz gd gdn ge gea gent genting george gf gg ggee gh gi gift gifts gives giving gl glass gle global globo gm gmail gmbh gmo gmx gn godaddy gold goldpoint golf goo goodyear goog google gop got gov gp gq gr grainger graphics gratis green gripe grocery group gs gt gu gucci guge guide guitars guru gw gy hair hamburg hangout haus hbo hdfc hdfcbank health healthcare help helsinki here hermes hiphop hisamitsu hitachi hiv hk hkt hm hn hockey holdings holiday homedepot homegoods homes homesense honda horse hospital host hosting hot hotels hotmail house how hr hsbc ht hu hughes hyatt hyundai ibm icbc ice icu id ie ieee ifm ikano il im imamat imdb immo immobilien in inc industries infiniti info ing ink institute insurance insure int international intuit investments io ipiranga iq ir irish is ismaili ist istanbul it itau itv jaguar java jcb je jeep jetzt jewelry jio jll jm jmp jnj jo jobs joburg jot joy jp jpmorgan jprs juegos juniper kaufen kddi ke kerryhotels kerrylogistics kerryproperties kfh kg kh ki kia kids kim kindle kitchen kiwi km kn koeln komatsu kosher kp kpmg kpn kr krd kred kuokgroup kw ky kyoto kz la lacaixa lamborghini lamer lancaster land landrover lanxess lasalle lat latino latrobe law lawyer lb lc lds lease leclerc lefrak legal lego lexus lgbt li lidl life lifeinsurance lifestyle lighting like lilly limited limo lincoln link lipsy live living lk llc llp loan loans locker locus lol london lotte lotto love lpl lplfinancial lr ls lt ltd ltda lu lundbeck luxe luxury lv ly ma madrid maif maison makeup man management mango map market marketing markets marriott marshalls mattel mba mc mckinsey md me med media meet melbourne meme memorial men menu merckmsd mg mh miami microsoft mil mini mint mit mitsubishi mk ml mlb mls mm mma mn mo mobi mobile moda moe moi mom monash money monster mormon mortgage moscow moto motorcycles mov movie mp mq mr ms msd mt mtn mtr mu museum music mv mw mx my mz na nab nagoya name navy nba nc ne nec net netbank netflix network neustar new news next nextdirect nexus nf nfl ng ngo nhk ni nico nike nikon ninja nissan nissay nl no nokia norton now nowruz nowtv np nr nra nrw ntt nu nyc nz obi observer office okinawa olayan olayangroup ollo om omega one ong onl online ooo open oracle orange org organic origins osaka otsuka ott ovh pa page panasonic paris pars partners parts party pay pccw pe pet pf pfizer pg ph pharmacy phd philips phone photo photography photos physio pics pictet pictures pid pin ping pink pioneer pizza pk pl place play playstation plumbing plus pm pn pnc pohl poker politie porn post pr pramerica praxi press prime pro prod productions prof progressive promo properties property protection pru prudential ps pt pub pw pwc py qa qpon quebec quest racing radio re read realestate realtor realty recipes red redstone redumbrella rehab reise reisen reit reliance ren rent rentals repair report republican rest restaurant review reviews rexroth rich richardli ricoh ril rio rip ro rocks rodeo rogers room rs rsvp ru rugby ruhr run rw rwe ryukyu sa saarland safe safety sakura sale salon samsclub samsung sandvik sandvikcoromant sanofi sap sarl sas save saxo sb sbi sbs sc scb schaeffler schmidt scholarships school schule schwarz science scot sd se search seat secure security seek select sener services seven sew sex sexy sfr sg sh shangrila sharp shell shia shiksha shoes shop shopping shouji show si silk sina singles site sj sk ski skin sky skype sl sling sm smart smile sn sncf so soccer social softbank software sohu solar solutions song sony soy spa space sport spot sr srl ss st stada staples star statebank statefarm stc stcgroup stockholm storage store stream studio study style su sucks supplies supply support surf surgery suzuki sv swatch swiss sx sy sydney systems sz tab taipei talk taobao target tatamotors tatar tattoo tax taxi tc tci td tdk team tech technology tel temasek tennis teva tf tg th thd theater theatre tiaa tickets tienda tips tires tirol tj tjmaxx tjx tk tkmaxx tl tm tmall tn to today tokyo tools top toray toshiba total tours town toyota toys tr trade trading training travel travelers travelersinsurance trust trv tt tube tui tunes tushu tv tvs tw tz ua ubank ubs ug uk unicom university uno uol ups us uy uz va vacations vana vanguard vc ve vegas ventures verisign versicherung vet vg vi viajes video vig viking villas vin vip virgin visa vision viva vivo vlaanderen vn vodka volvo vote voting voto voyage vu wales walmart walter wang wanggou watch watches weather weatherchannel webcam weber website wed wedding weibo weir wf whoswho wien wiki williamhill win windows wine winners wme wolterskluwer woodside work works world wow ws wtc wtf xbox xerox xihuan xin xxx xyz yachts yahoo yamaxun yandex ye yodobashi yoga yokohama you youtube yt yun za zappos zara zero zip zm zone zuerich zw # Non-ASCII domains कॉम セール 佛山 ಭಾರತ 慈善 集团 在线 한국 ଭାରତ 点看 คอม ভাৰত ভারত 八卦 ישראל موقع বাংলা 公益 公司 香格里拉 网站 移动 我爱你 москва қаз католик онлайн сайт 联通 срб бг бел קום 时尚 微博 淡马锡 ファッション орг नेट ストア アマゾン 삼성 சிங்கப்பூர் 商标 商店 商城 дети мкд ею ポイント 新闻 家電 كوم 中文网 中信 中国 中國 娱乐 谷歌 భారత్ ලංකා 電訊盈科 购物 クラウド ભારત 通販 भारतम् भारत भारोत 网店 संगठन 餐厅 网络 ком укр 香港 亚马逊 食品 飞利浦 台湾 台灣 手机 мон الجزائر عمان ارامكو ایران العليان امارات بازار موريتانيا پاکستان الاردن بارت بھارت المغرب ابوظبي البحرين السعودية ڀارت كاثوليك سودان همراه عراق مليسيا 澳門 닷컴 政府 شبكة بيتك عرب გე 机构 组织机构 健康 ไทย سورية 招聘 рус рф تونس 大拿 ລາວ みんな グーグル ευ ελ 世界 書籍 ഭാരതം ਭਾਰਤ 网址 닷넷 コム 天主教 游戏 vermögensberater vermögensberatung 企业 信息 嘉里大酒店 嘉里 مصر قطر 广东 இலங்கை இந்தியா հայ 新加坡 فلسطين 政务 ================================================ FILE: jadx-core/src/test/java/jadx/NotYetImplemented.java ================================================ package jadx; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Indicates a test which is known to fail. * *

* This would cause a failure to be considered as success and a success as failure, * with the benefit of updating the related issue when it has been resolved even unintentionally. *

* *

* To have an effect, the test class must be annotated with: * * * @ExtendWith(NotYetImplementedExtension.class) * *

*/ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) public @interface NotYetImplemented { String value() default ""; } ================================================ FILE: jadx-core/src/test/java/jadx/NotYetImplementedExtension.java ================================================ package jadx; import java.lang.reflect.Method; import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; public class NotYetImplementedExtension implements AfterTestExecutionCallback, TestExecutionExceptionHandler { private final Set knownFailedMethods = new HashSet<>(); @Override public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable { if (!isNotYetImplemented(context)) { throw throwable; } knownFailedMethods.add(context.getTestMethod().get()); } @Override public void afterTestExecution(ExtensionContext context) throws Exception { if (!knownFailedMethods.contains(context.getTestMethod().get()) && isNotYetImplemented(context) && context.getExecutionException().isEmpty()) { throw new AssertionError("Test " + context.getTestClass().get().getName() + '.' + context.getTestMethod().get().getName() + " is marked as @NotYetImplemented, but passes!"); } } private static boolean isNotYetImplemented(ExtensionContext context) { return context.getTestMethod().get().getAnnotation(NotYetImplemented.class) != null || context.getTestClass().get().getAnnotation(NotYetImplemented.class) != null; } } ================================================ FILE: jadx-core/src/test/java/jadx/api/JadxArgsValidatorOutDirsTest.java ================================================ package jadx.api; import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static jadx.core.utils.files.FileUtils.toFile; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class JadxArgsValidatorOutDirsTest { private static final Logger LOG = LoggerFactory.getLogger(JadxArgsValidatorOutDirsTest.class); public JadxArgs args; @TempDir Path testDir; @Test public void checkAllSet() { setOutDirs("r", "s", "r"); checkOutDirs("r", "s", "r"); } @Test public void checkRootOnly() { setOutDirs("out", null, null); checkOutDirs("out", "out/" + JadxArgs.DEFAULT_SRC_DIR, "out/" + JadxArgs.DEFAULT_RES_DIR); } @Test public void checkSrcOnly() { setOutDirs(null, "src", null); checkOutDirs("src", "src", "src/" + JadxArgs.DEFAULT_RES_DIR); } @Test public void checkResOnly() { setOutDirs(null, null, "res"); checkOutDirs("res", "res/" + JadxArgs.DEFAULT_SRC_DIR, "res"); } @Test public void checkNone() { setOutDirs(null, null, null); String inputFileBase = args.getInputFiles().get(0).getName().replace(".apk", ""); checkOutDirs(inputFileBase, inputFileBase + '/' + JadxArgs.DEFAULT_SRC_DIR, inputFileBase + '/' + JadxArgs.DEFAULT_RES_DIR); } private void setOutDirs(String outDir, String srcDir, String resDir) { args = makeArgs(); args.setOutDir(toFile(outDir)); args.setOutDirSrc(toFile(srcDir)); args.setOutDirRes(toFile(resDir)); LOG.debug("Set dirs: out={}, src={}, res={}", outDir, srcDir, resDir); } private void checkOutDirs(String outDir, String srcDir, String resDir) { JadxArgsValidator.validate(new JadxDecompiler(args)); LOG.debug("Got dirs: out={}, src={}, res={}", args.getOutDir(), args.getOutDirSrc(), args.getOutDirRes()); assertThat(args.getOutDir()).isEqualTo(toFile(outDir)); assertThat(args.getOutDirSrc()).isEqualTo(toFile(srcDir)); assertThat(args.getOutDirRes()).isEqualTo(toFile(resDir)); } private JadxArgs makeArgs() { try { JadxArgs args = new JadxArgs(); args.getInputFiles().add(Files.createTempFile(testDir, "test-", ".apk").toFile()); return args; } catch (Exception e) { throw new RuntimeException(e); } } } ================================================ FILE: jadx-core/src/test/java/jadx/api/JadxDecompilerTest.java ================================================ package jadx.api; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import jadx.core.xmlgen.ResContainer; import jadx.plugins.input.dex.DexInputPlugin; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class JadxDecompilerTest { @TempDir File testDir; @Test public void testExampleUsage() { File sampleApk = getFileFromSampleDir("app-with-fake-dex.apk"); // test simple apk loading JadxArgs args = new JadxArgs(); args.getInputFiles().add(sampleApk); args.setOutDir(testDir); try (JadxDecompiler jadx = new JadxDecompiler(args)) { jadx.load(); jadx.save(); jadx.printErrorsReport(); // test class print for (JavaClass cls : jadx.getClasses()) { System.out.println(cls.getCode()); } assertThat(jadx.getClasses()).hasSize(3); assertThat(jadx.getErrorsCount()).isEqualTo(0); } } @Test public void testDirectDexInput() throws IOException { try (JadxDecompiler jadx = new JadxDecompiler(); InputStream in = new FileInputStream(getFileFromSampleDir("hello.dex"))) { jadx.addCustomCodeLoader(new DexInputPlugin().loadDexFromInputStream(in, "input")); jadx.load(); for (JavaClass cls : jadx.getClasses()) { System.out.println(cls.getCode()); } assertThat(jadx.getClasses()).hasSize(1); assertThat(jadx.getErrorsCount()).isEqualTo(0); } } @Test public void testResourcesLoad() { File sampleApk = getFileFromSampleDir("app-with-fake-dex.apk"); JadxArgs args = new JadxArgs(); args.getInputFiles().add(sampleApk); args.setOutDir(testDir); args.setSkipSources(true); try (JadxDecompiler jadx = new JadxDecompiler(args)) { jadx.load(); List resources = jadx.getResources(); assertThat(resources).hasSize(8); ResourceFile arsc = resources.stream() .filter(r -> r.getType() == ResourceType.ARSC) .findFirst().orElseThrow(); ResContainer resContainer = arsc.loadContent(); ResContainer xmlRes = resContainer.getSubFiles().stream() .filter(r -> r.getName().equals("res/values/colors.xml")) .findFirst().orElseThrow(); assertThat(xmlRes.getText()) .code() .containsOne("#008577"); } } private static final String TEST_SAMPLES_DIR = "test-samples/"; public static File getFileFromSampleDir(String fileName) { URL resource = JadxDecompilerTest.class.getClassLoader().getResource(TEST_SAMPLES_DIR + fileName); assertThat(resource).isNotNull(); String pathStr = resource.getFile(); return new File(pathStr); } // TODO add more tests } ================================================ FILE: jadx-core/src/test/java/jadx/api/JadxInternalAccess.java ================================================ package jadx.api; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; public class JadxInternalAccess { public static RootNode getRoot(JadxDecompiler d) { return d.getRoot(); } public static JavaClass convertClassNode(JadxDecompiler d, ClassNode clsNode) { return d.convertClassNode(clsNode); } public static JavaMethod convertMethodNode(JadxDecompiler d, MethodNode mthNode) { return d.convertMethodNode(mthNode); } public static JavaField convertFieldNode(JadxDecompiler d, FieldNode fldNode) { return d.convertFieldNode(fldNode); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/deobf/NameMapperTest.java ================================================ package jadx.core.deobf; import org.junit.jupiter.api.Test; import static jadx.core.deobf.NameMapper.isValidIdentifier; import static jadx.core.deobf.NameMapper.removeInvalidChars; import static jadx.core.deobf.NameMapper.removeInvalidCharsMiddle; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class NameMapperTest { @Test public void validIdentifiers() { assertThat(isValidIdentifier("ACls")).isTrue(); } @Test public void notValidIdentifiers() { assertThat(isValidIdentifier("1cls")).isFalse(); assertThat(isValidIdentifier("-cls")).isFalse(); assertThat(isValidIdentifier("A-cls")).isFalse(); } @Test public void testRemoveInvalidCharsMiddle() { assertThat(removeInvalidCharsMiddle("1cls")).isEqualTo("1cls"); assertThat(removeInvalidCharsMiddle("-cls")).isEqualTo("cls"); assertThat(removeInvalidCharsMiddle("A-cls")).isEqualTo("Acls"); } @Test public void testRemoveInvalidChars() { assertThat(removeInvalidChars("1cls", "C")).isEqualTo("C1cls"); assertThat(removeInvalidChars("-cls", "C")).isEqualTo("cls"); assertThat(removeInvalidChars("A-cls", "C")).isEqualTo("Acls"); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/dex/info/AccessInfoTest.java ================================================ package jadx.core.dex.info; import org.junit.jupiter.api.Test; import jadx.api.plugins.input.data.AccessFlags; import jadx.core.dex.info.AccessInfo.AFType; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class AccessInfoTest { @Test public void changeVisibility() { AccessInfo accessInfo = new AccessInfo(AccessFlags.PROTECTED | AccessFlags.STATIC, AFType.METHOD); AccessInfo result = accessInfo.changeVisibility(AccessFlags.PUBLIC); assertThat(result.isPublic()).isTrue(); assertThat(result.isPrivate()).isFalse(); assertThat(result.isProtected()).isFalse(); assertThat(result.isStatic()).isTrue(); } @Test public void changeVisibilityNoOp() { AccessInfo accessInfo = new AccessInfo(AccessFlags.PUBLIC, AFType.METHOD); AccessInfo result = accessInfo.changeVisibility(AccessFlags.PUBLIC); assertThat(result).isSameAs(accessInfo); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/dex/instructions/args/ArgTypeTest.java ================================================ package jadx.core.dex.instructions.args; import org.junit.jupiter.api.Test; import static jadx.core.dex.instructions.args.ArgType.WildcardBound.SUPER; import static jadx.core.dex.instructions.args.ArgType.generic; import static jadx.core.dex.instructions.args.ArgType.genericType; import static jadx.core.dex.instructions.args.ArgType.wildcard; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; class ArgTypeTest { @Test public void testEqualsOfGenericTypes() { ArgType first = ArgType.generic("java.lang.List", ArgType.STRING); ArgType second = ArgType.generic("Ljava/lang/List;", ArgType.STRING); assertThat(first).isEqualTo(second); } @Test void testContainsGenericType() { ArgType wildcard = wildcard(genericType("T"), SUPER); assertThat(wildcard.containsTypeVariable()).isTrue(); ArgType type = generic("java.lang.List", wildcard); assertThat(type.containsTypeVariable()).isTrue(); } @Test void testInnerGeneric() { ArgType[] genericTypes = new ArgType[] { ArgType.genericType("K"), ArgType.genericType("V") }; ArgType base = ArgType.generic("java.util.Map", genericTypes); ArgType genericInner = ArgType.outerGeneric(base, ArgType.generic("Entry", genericTypes)); assertThat(genericInner.toString()).isEqualTo("java.util.Map$Entry"); assertThat(genericInner.containsTypeVariable()).isTrue(); ArgType genericInner2 = ArgType.outerGeneric(base, ArgType.object("Entry")); assertThat(genericInner2.toString()).isEqualTo("java.util.Map$Entry"); assertThat(genericInner2.containsTypeVariable()).isTrue(); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/dex/nodes/utils/SelectFromDuplicatesTest.java ================================================ package jadx.core.dex.nodes.utils; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; import jadx.tests.api.utils.TestUtils; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; class SelectFromDuplicatesTest { private RootNode root; @BeforeEach public void init() { JadxArgs args = new JadxArgs(); args.addInputFile(TestUtils.getFileForSample("test-samples/hello.dex")); JadxDecompiler decompiler = new JadxDecompiler(args); decompiler.load(); root = decompiler.getRoot(); } @Test void testSelectBySource() { selectBySources(0, false, "classes.dex", "classes2.dex"); selectBySources(2, false, "classes10.dex", "classes20.dex", "classes2.dex"); } @RepeatedTest(10) void testSelectBySourceShuffled() { selectFirstByShuffleSources("classes.dex", "classes2.dex", "classes4.dex"); selectFirstByShuffleSources("classes2.dex", "classes10.dex", "classes20.dex"); selectFirstByShuffleSources("classes10.dex", "classes1.dex", "classes01.dex", "classes000.dex", "classes02.dex"); } private void selectFirstByShuffleSources(String... sources) { selectBySources(0, true, sources); } private void selectBySources(int selectedPos, boolean shuffle, String... sources) { List clsList = Arrays.stream(sources) .map(this::buildClassNodeBySource) .collect(Collectors.toList()); ClassNode expected = clsList.get(selectedPos); if (shuffle) { Collections.shuffle(clsList, new Random(System.currentTimeMillis() + System.nanoTime())); } ClassNode selectedCls = SelectFromDuplicates.process(clsList); assertThat(selectedCls) .describedAs("Expect %s, but got %s from list: %s", expected, selectedCls, clsList) .isSameAs(expected); } private ClassNode buildClassNodeBySource(String clsSource) { ClassInfo clsInfo = ClassInfo.fromName(root, "ClassFromSource:" + clsSource); ClassNode cls = ClassNode.addSyntheticClass(root, clsInfo, 0); cls.setInputFileName(clsSource); return cls; } } ================================================ FILE: jadx-core/src/test/java/jadx/core/dex/nodes/utils/TypeUtilsTest.java ================================================ package jadx.core.dex.nodes.utils; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import jadx.api.JadxArgs; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.RootNode; import static jadx.core.dex.instructions.args.ArgType.EXCEPTION; import static jadx.core.dex.instructions.args.ArgType.STRING; import static jadx.core.dex.instructions.args.ArgType.array; import static jadx.core.dex.instructions.args.ArgType.generic; import static jadx.core.dex.instructions.args.ArgType.genericType; import static jadx.core.dex.instructions.args.ArgType.object; import static jadx.core.dex.instructions.args.ArgType.outerGeneric; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; class TypeUtilsTest { private TypeUtils typeUtils; @BeforeEach public void init() { typeUtils = new TypeUtils(new RootNode(new JadxArgs())); } @Test void replaceTypeVariablesUsingMap() { ArgType typeVar = genericType("T"); ArgType listCls = object("java.util.List"); Map typeMap = Collections.singletonMap(typeVar, STRING); replaceTypeVar(typeVar, typeMap, STRING); replaceTypeVar(generic(listCls, typeVar), typeMap, generic(listCls, STRING)); replaceTypeVar(array(typeVar), typeMap, array(STRING)); } @Test void replaceTypeVariablesUsingMap2() { ArgType kVar = genericType("K"); ArgType vVar = genericType("V"); ArgType mapCls = object("java.util.Map"); ArgType entryCls = object("Entry"); ArgType typedMap = generic(mapCls, kVar, vVar); ArgType typedEntry = generic(entryCls, kVar, vVar); Map typeMap = new HashMap<>(); typeMap.put(kVar, STRING); typeMap.put(vVar, EXCEPTION); ArgType replacedMap = typeUtils.replaceTypeVariablesUsingMap(typedMap, typeMap); ArgType replacedEntry = typeUtils.replaceTypeVariablesUsingMap(typedEntry, typeMap); replaceTypeVar(outerGeneric(typedMap, entryCls), typeMap, outerGeneric(replacedMap, entryCls)); replaceTypeVar(outerGeneric(typedMap, typedEntry), typeMap, outerGeneric(replacedMap, replacedEntry)); } private void replaceTypeVar(ArgType typeVar, Map typeMap, ArgType expected) { ArgType resultType = typeUtils.replaceTypeVariablesUsingMap(typeVar, typeMap); assertThat(resultType) .as("Replace %s using map %s", typeVar, typeMap) .isEqualTo(expected); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/dex/trycatch/TryCatchBlockAttrTest.java ================================================ package jadx.core.dex.trycatch; import org.junit.jupiter.api.Nested; public class TryCatchBlockAttrTest { @Nested public class TryCatchBlockAttrIntegration { } } ================================================ FILE: jadx-core/src/test/java/jadx/core/dex/visitors/typeinference/PrimitiveConversionsTests.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import jadx.api.JadxArgs; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.RootNode; import static jadx.core.dex.instructions.args.ArgType.BOOLEAN; import static jadx.core.dex.instructions.args.ArgType.BYTE; import static jadx.core.dex.instructions.args.ArgType.CHAR; import static jadx.core.dex.instructions.args.ArgType.DOUBLE; import static jadx.core.dex.instructions.args.ArgType.FLOAT; import static jadx.core.dex.instructions.args.ArgType.INT; import static jadx.core.dex.instructions.args.ArgType.LONG; import static jadx.core.dex.instructions.args.ArgType.SHORT; import static jadx.core.dex.instructions.args.ArgType.VOID; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.CONFLICT; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.EQUAL; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.NARROW; import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.WIDER; import static org.assertj.core.api.Assertions.assertThat; public class PrimitiveConversionsTests { private static TypeCompare comparator; @BeforeAll static void before() { JadxArgs args = new JadxArgs(); RootNode root = new RootNode(args); comparator = new TypeCompare(root); } @DisplayName("Check conversion of numeric types") @ParameterizedTest(name = "{0} -> {1} (should be {2})") @MethodSource("provideArgsForNumericConversionsTest") void testNumericConversions(ArgType firstType, ArgType secondType, TypeCompareEnum expectedResult) { assertThat(comparator.compareTypes(firstType, secondType)).isEqualTo(expectedResult); } @DisplayName("Ensure that `boolean` is not convertible to other primitive types") @ParameterizedTest(name = "{0} <-> boolean") @MethodSource("providePrimitiveTypesWithVoid") void testBooleanConversions(ArgType type) { final var expectedResult = type.equals(BOOLEAN) ? EQUAL : CONFLICT; assertThat(comparator.compareTypes(type, BOOLEAN)).isEqualTo(expectedResult); assertThat(comparator.compareTypes(BOOLEAN, type)).isEqualTo(expectedResult); } @DisplayName("Ensure that `void` is not convertible to other primitive types") @ParameterizedTest(name = "{0} <-> void") @MethodSource("providePrimitiveTypesWithVoid") void testVoidConversions(ArgType type) { final var expectedResult = type.equals(VOID) ? EQUAL : CONFLICT; assertThat(comparator.compareTypes(type, VOID)).isEqualTo(expectedResult); assertThat(comparator.compareTypes(VOID, type)).isEqualTo(expectedResult); } private static Stream provideArgsForNumericConversionsTest() { return Stream.of( Arguments.of(BYTE, BYTE, EQUAL), Arguments.of(BYTE, SHORT, NARROW), Arguments.of(BYTE, CHAR, WIDER), Arguments.of(BYTE, INT, NARROW), Arguments.of(BYTE, LONG, NARROW), Arguments.of(BYTE, FLOAT, NARROW), Arguments.of(BYTE, DOUBLE, NARROW), Arguments.of(SHORT, BYTE, WIDER), Arguments.of(SHORT, SHORT, EQUAL), Arguments.of(SHORT, CHAR, WIDER), Arguments.of(SHORT, INT, NARROW), Arguments.of(SHORT, LONG, NARROW), Arguments.of(SHORT, FLOAT, NARROW), Arguments.of(SHORT, DOUBLE, NARROW), Arguments.of(CHAR, BYTE, WIDER), Arguments.of(CHAR, SHORT, WIDER), Arguments.of(CHAR, CHAR, EQUAL), Arguments.of(CHAR, INT, NARROW), Arguments.of(CHAR, LONG, NARROW), Arguments.of(CHAR, FLOAT, NARROW), Arguments.of(CHAR, DOUBLE, NARROW), Arguments.of(INT, BYTE, WIDER), Arguments.of(INT, SHORT, WIDER), Arguments.of(INT, CHAR, WIDER), Arguments.of(INT, INT, EQUAL), Arguments.of(INT, LONG, NARROW), Arguments.of(INT, FLOAT, NARROW), Arguments.of(INT, DOUBLE, NARROW), Arguments.of(LONG, BYTE, WIDER), Arguments.of(LONG, SHORT, WIDER), Arguments.of(LONG, CHAR, WIDER), Arguments.of(LONG, INT, WIDER), Arguments.of(LONG, LONG, EQUAL), Arguments.of(LONG, FLOAT, NARROW), Arguments.of(LONG, DOUBLE, NARROW), Arguments.of(FLOAT, BYTE, WIDER), Arguments.of(FLOAT, SHORT, WIDER), Arguments.of(FLOAT, CHAR, WIDER), Arguments.of(FLOAT, INT, WIDER), Arguments.of(FLOAT, LONG, WIDER), Arguments.of(FLOAT, FLOAT, EQUAL), Arguments.of(FLOAT, DOUBLE, NARROW), Arguments.of(DOUBLE, BYTE, WIDER), Arguments.of(DOUBLE, SHORT, WIDER), Arguments.of(DOUBLE, CHAR, WIDER), Arguments.of(DOUBLE, INT, WIDER), Arguments.of(DOUBLE, LONG, WIDER), Arguments.of(DOUBLE, FLOAT, WIDER), Arguments.of(DOUBLE, DOUBLE, EQUAL)); } private static Stream providePrimitiveTypesWithVoid() { return Stream.of(BYTE, SHORT, CHAR, INT, LONG, FLOAT, DOUBLE, BOOLEAN, VOID); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/dex/visitors/typeinference/TypeCompareTest.java ================================================ package jadx.core.dex.visitors.typeinference; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.NotYetImplementedExtension; import jadx.api.JadxArgs; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.ArgType.WildcardBound; import jadx.core.dex.nodes.RootNode; import static jadx.core.dex.instructions.args.ArgType.BYTE; import static jadx.core.dex.instructions.args.ArgType.CHAR; import static jadx.core.dex.instructions.args.ArgType.CLASS; import static jadx.core.dex.instructions.args.ArgType.EXCEPTION; import static jadx.core.dex.instructions.args.ArgType.INT; import static jadx.core.dex.instructions.args.ArgType.NARROW; import static jadx.core.dex.instructions.args.ArgType.NARROW_INTEGRAL; import static jadx.core.dex.instructions.args.ArgType.OBJECT; import static jadx.core.dex.instructions.args.ArgType.STRING; import static jadx.core.dex.instructions.args.ArgType.THROWABLE; import static jadx.core.dex.instructions.args.ArgType.UNKNOWN; import static jadx.core.dex.instructions.args.ArgType.UNKNOWN_ARRAY; import static jadx.core.dex.instructions.args.ArgType.UNKNOWN_OBJECT; import static jadx.core.dex.instructions.args.ArgType.array; import static jadx.core.dex.instructions.args.ArgType.generic; import static jadx.core.dex.instructions.args.ArgType.genericType; import static jadx.core.dex.instructions.args.ArgType.object; import static jadx.core.dex.instructions.args.ArgType.wildcard; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @ExtendWith(NotYetImplementedExtension.class) public class TypeCompareTest { private static final Logger LOG = LoggerFactory.getLogger(TypeCompareTest.class); private TypeCompare compare; @BeforeEach public void init() { JadxArgs args = new JadxArgs(); RootNode root = new RootNode(args); root.loadClasses(Collections.emptyList()); root.initClassPath(); compare = new TypeCompare(root); } @Test public void compareTypes() { check(INT, UNKNOWN_OBJECT, TypeCompareEnum.CONFLICT); check(INT, OBJECT, TypeCompareEnum.CONFLICT); firstIsNarrow(INT, UNKNOWN); firstIsNarrow(CHAR, NARROW_INTEGRAL); firstIsNarrow(array(UNKNOWN), UNKNOWN); firstIsNarrow(array(UNKNOWN), NARROW); firstIsNarrow(array(CHAR), UNKNOWN_OBJECT); } @Test public void compareArrays() { firstIsNarrow(array(CHAR), OBJECT); firstIsNarrow(array(CHAR), array(UNKNOWN)); firstIsNarrow(array(OBJECT), OBJECT); firstIsNarrow(array(OBJECT), array(UNKNOWN_OBJECT)); firstIsNarrow(array(STRING), array(UNKNOWN_OBJECT)); firstIsNarrow(array(STRING), array(OBJECT)); firstIsNarrow(UNKNOWN_ARRAY, OBJECT); firstIsNarrow(array(BYTE), OBJECT); firstIsNarrow(array(array(BYTE)), array(OBJECT)); check(array(OBJECT), array(INT), TypeCompareEnum.CONFLICT); ArgType integerType = object("java.lang.Integer"); check(array(OBJECT), array(integerType), TypeCompareEnum.WIDER); check(array(INT), array(integerType), TypeCompareEnum.CONFLICT); check(array(INT), array(INT), TypeCompareEnum.EQUAL); ArgType wildClass = generic(CLASS, wildcard()); check(array(wildClass), array(CLASS), TypeCompareEnum.NARROW_BY_GENERIC); check(array(CLASS), array(wildClass), TypeCompareEnum.WIDER_BY_GENERIC); } @Test public void compareGenerics() { ArgType mapCls = object("java.util.Map"); ArgType setCls = object("java.util.Set"); ArgType keyType = genericType("K"); ArgType valueType = genericType("V"); ArgType mapGeneric = ArgType.generic(mapCls.getObject(), keyType, valueType); check(mapCls, mapGeneric, TypeCompareEnum.WIDER_BY_GENERIC); check(mapCls, setCls, TypeCompareEnum.CONFLICT); ArgType setGeneric = ArgType.generic(setCls.getObject(), valueType); ArgType setWildcard = ArgType.generic(setCls.getObject(), ArgType.wildcard()); check(setWildcard, setGeneric, TypeCompareEnum.CONFLICT); check(setWildcard, setCls, TypeCompareEnum.NARROW_BY_GENERIC); // TODO implement compare for wildcard with bounds } @Test public void compareWildCards() { ArgType clsWildcard = generic(CLASS.getObject(), wildcard()); check(clsWildcard, CLASS, TypeCompareEnum.NARROW_BY_GENERIC); ArgType clsExtendedWildcard = generic(CLASS.getObject(), wildcard(STRING, WildcardBound.EXTENDS)); check(clsWildcard, clsExtendedWildcard, TypeCompareEnum.WIDER); ArgType listWildcard = generic(CLASS.getObject(), wildcard(object("java.util.List"), WildcardBound.EXTENDS)); ArgType collWildcard = generic(CLASS.getObject(), wildcard(object("java.util.Collection"), WildcardBound.EXTENDS)); check(listWildcard, collWildcard, TypeCompareEnum.NARROW); ArgType collSuperWildcard = generic(CLASS.getObject(), wildcard(object("java.util.Collection"), WildcardBound.SUPER)); check(collSuperWildcard, listWildcard, TypeCompareEnum.CONFLICT); } @Test public void compareGenericWildCards() { // 'java.util.List' and 'java.util.List' ArgType listCls = object("java.util.List"); ArgType genericType = genericType("T"); ArgType genericList = generic(listCls, genericType); ArgType genericExtendedList = generic(listCls, wildcard(genericType, WildcardBound.EXTENDS)); check(genericList, genericExtendedList, TypeCompareEnum.CONFLICT_BY_GENERIC); } @Test public void compareGenericTypes() { ArgType vType = genericType("V"); check(vType, OBJECT, TypeCompareEnum.NARROW); check(vType, STRING, TypeCompareEnum.CONFLICT); ArgType rType = genericType("R"); check(vType, rType, TypeCompareEnum.CONFLICT); check(vType, vType, TypeCompareEnum.EQUAL); ArgType tType = genericType("T"); ArgType tStringType = genericType("T", STRING); check(tStringType, STRING, TypeCompareEnum.NARROW); check(tStringType, OBJECT, TypeCompareEnum.NARROW); check(tStringType, tType, TypeCompareEnum.NARROW); ArgType tObjType = genericType("T", OBJECT); check(tObjType, OBJECT, TypeCompareEnum.NARROW); check(tObjType, tType, TypeCompareEnum.EQUAL); check(tStringType, tObjType, TypeCompareEnum.NARROW); } @Test public void compareGenericTypes2() { ArgType npeType = object("java.lang.NullPointerException"); // check clsp graph check(npeType, THROWABLE, TypeCompareEnum.NARROW); check(npeType, EXCEPTION, TypeCompareEnum.NARROW); check(EXCEPTION, THROWABLE, TypeCompareEnum.NARROW); ArgType typeVar = genericType("T", EXCEPTION); // T extends Exception // target checks check(THROWABLE, typeVar, TypeCompareEnum.WIDER); check(EXCEPTION, typeVar, TypeCompareEnum.WIDER); check(npeType, typeVar, TypeCompareEnum.NARROW); } @Test public void compareOuterGenerics() { ArgType hashMapType = object("java.util.HashMap"); ArgType innerEntrySetType = object("EntrySet"); ArgType firstInstance = ArgType.outerGeneric(generic(hashMapType, STRING, STRING), innerEntrySetType); ArgType secondInstance = ArgType.outerGeneric(generic(hashMapType, OBJECT, OBJECT), innerEntrySetType); check(firstInstance, secondInstance, TypeCompareEnum.NARROW); } private void firstIsNarrow(ArgType first, ArgType second) { check(first, second, TypeCompareEnum.NARROW); } private void check(ArgType first, ArgType second, TypeCompareEnum expectedResult) { LOG.debug("Compare: '{}' and '{}', expect: '{}'", first, second, expectedResult); assertThat(compare.compareTypes(first, second)) .as("Compare '%s' and '%s'", first, second) .isEqualTo(expectedResult); assertThat(compare.compareTypes(second, first)) .as("Compare '%s' and '%s'", second, first) .isEqualTo(expectedResult.invert()); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/plugins/versions/VerifyRequiredVersionTest.java ================================================ package jadx.core.plugins.versions; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class VerifyRequiredVersionTest { @Test public void test() { isCompatible("1.5.0, r2000", "1.5.1", true); isCompatible("1.5.1, r3000", "1.5.1", true); isCompatible("1.5.1, r3000", "1.6.0", true); isCompatible("1.5.1, r3000", "1.5.0", false); isCompatible("1.5.1, r3000", "r3001.417bb7a", true); isCompatible("1.5.1, r3000", "r4000", true); isCompatible("1.5.1, r3000", "r3000", true); isCompatible("1.5.1, r3000", "r2000", false); } private static void isCompatible(String requiredVersion, String jadxVersion, boolean result) { assertThat(new VerifyRequiredVersion(jadxVersion).isCompatible(requiredVersion)) .as("Expect plugin with required version %s is%s compatible with jadx %s", requiredVersion, result ? "" : " not", jadxVersion) .isEqualTo(result); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/plugins/versions/VersionComparatorTest.java ================================================ package jadx.core.plugins.versions; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class VersionComparatorTest { @Test public void testCompare() { checkCompare("", "", 0); checkCompare("1", "1", 0); checkCompare("1", "2", -1); checkCompare("1.1", "1.1", 0); checkCompare("0.5", "0.5", 0); checkCompare("0.5", "0.5.0", 0); checkCompare("0.5", "0.5.00", 0); checkCompare("0.5", "0.5.0.0", 0); checkCompare("0.5", "0.5.0.1", -1); checkCompare("0.5.0", "0.5.0", 0); checkCompare("0.5.0", "0.5.1", -1); checkCompare("0.5", "0.5.1", -1); checkCompare("0.4.8", "0.5", -1); checkCompare("0.4.8", "0.5.0", -1); checkCompare("0.4.8", "0.6", -1); checkCompare("1.3.3.1", "1.3.3", 1); checkCompare("1.3.3-1", "1.3.3", 1); checkCompare("1.3.3.1-1", "1.3.3", 1); } @Test public void testCompareUnstable() { checkCompare("r2190.ce527ed", "jadx-r2299.742d30d", -1); } private static void checkCompare(String a, String b, int result) { assertThat(VersionComparator.checkAndCompare(a, b)) .as("Compare %s and %s expect %d", a, b, result) .isEqualTo(result); assertThat(VersionComparator.checkAndCompare(b, a)) .as("Compare %s and %s expect %d", b, a, -result) .isEqualTo(-result); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/utils/PassMergeTest.java ================================================ package jadx.core.utils; import java.util.List; import org.junit.jupiter.api.Test; import jadx.api.impl.passes.DecompilePassWrapper; import jadx.api.plugins.pass.JadxPass; import jadx.api.plugins.pass.JadxPassInfo; import jadx.api.plugins.pass.impl.OrderedJadxPassInfo; import jadx.api.plugins.pass.impl.SimpleJadxPassInfo; import jadx.api.plugins.pass.types.JadxDecompilePass; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.catchThrowable; class PassMergeTest { @Test public void testSimple() { List base = asList("a", "b", "c"); check(base, mockPass("x"), asList("a", "b", "c", "x")); check(base, mockPass(mockInfo("x").after(JadxPassInfo.START)), asList("x", "a", "b", "c")); check(base, mockPass(mockInfo("x").before(JadxPassInfo.END)), asList("a", "b", "c", "x")); } @Test public void testSingle() { List base = asList("a", "b", "c"); check(base, mockPass(mockInfo("x").after("a")), asList("a", "x", "b", "c")); check(base, mockPass(mockInfo("x").before("c")), asList("a", "b", "x", "c")); check(base, mockPass(mockInfo("x").before("a")), asList("x", "a", "b", "c")); check(base, mockPass(mockInfo("x").after("c")), asList("a", "b", "c", "x")); } @Test public void testMulti() { List base = asList("a", "b", "c"); JadxPass x = mockPass(mockInfo("x").after("a")); JadxPass y = mockPass(mockInfo("y").after("a")); JadxPass z = mockPass(mockInfo("z").before("b")); check(base, asList(x, y, z), asList("a", "y", "x", "z", "b", "c")); } @Test public void testMultiWithDeps() { List base = asList("a", "b", "c"); JadxPass x = mockPass(mockInfo("x").after("a")); JadxPass y = mockPass(mockInfo("y").after("x")); JadxPass z = mockPass(mockInfo("z").before("b").after("y")); check(base, asList(x, y, z), asList("a", "x", "y", "z", "b", "c")); } @Test public void testMultiWithDeps2() { List base = asList("a", "b", "c"); JadxPass x = mockPass(mockInfo("x").before("y")); JadxPass y = mockPass(mockInfo("y").before("b")); JadxPass z = mockPass(mockInfo("z").after("y")); check(base, asList(x, y, z), asList("a", "x", "y", "z", "b", "c")); } @Test public void testMultiWithDeps3() { List base = asList("a", "b", "c"); JadxPass x = mockPass(mockInfo("x")); JadxPass y = mockPass(mockInfo("y").after("x").before("b")); check(base, asList(x, y), asList("a", "x", "y", "b", "c")); } @Test public void testLoop() { List base = asList("a", "b", "c"); JadxPass x = mockPass(mockInfo("x").before("y")); JadxPass y = mockPass(mockInfo("y").before("x")); Throwable thrown = catchThrowable(() -> check(base, asList(x, y), emptyList())); assertThat(thrown).isInstanceOf(JadxRuntimeException.class); } private void check(List visitorNames, JadxPass pass, List result) { check(visitorNames, singletonList(pass), result); } private void check(List visitorNames, List passes, List result) { List visitors = ListUtils.map(visitorNames, PassMergeTest::mockVisitor); new PassMerge(visitors).merge(passes, p -> new DecompilePassWrapper((JadxDecompilePass) p)); List resultVisitors = ListUtils.map(visitors, IDexTreeVisitor::getName); assertThat(resultVisitors).isEqualTo(result); } private static IDexTreeVisitor mockVisitor(String name) { return new AbstractVisitor() { @Override public String getName() { return name; } }; } private JadxPass mockPass(String name) { return mockPass(new SimpleJadxPassInfo(name)); } private OrderedJadxPassInfo mockInfo(String name) { return new OrderedJadxPassInfo(name, name); } private JadxPass mockPass(JadxPassInfo info) { return new JadxDecompilePass() { @Override public void init(RootNode root) { } @Override public boolean visit(ClassNode cls) { return false; } @Override public void visit(MethodNode mth) { } @Override public JadxPassInfo getInfo() { return info; } @Override public String toString() { return info.getName(); } }; } } ================================================ FILE: jadx-core/src/test/java/jadx/core/utils/TestBetterName.java ================================================ package jadx.core.utils; import org.junit.jupiter.api.Test; import static jadx.core.utils.BetterName.calcRating; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBetterName { @Deprecated @Test public void test() { expectFirst("color_main", "t0"); expectFirst("done", "oOo0oO0o"); } @Deprecated private void expectFirst(String first, String second) { String best = BetterName.compareAndGet(first, second); assertThat(best) .as(() -> String.format("'%s'=%d, '%s'=%d", first, calcRating(first), second, calcRating(second))) .isEqualTo(first); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/utils/TestGetBetterClassName.java ================================================ package jadx.core.utils; import org.assertj.core.api.AbstractStringAssert; import org.junit.jupiter.api.Test; import static jadx.core.utils.BetterName.getBetterClassName; import static org.assertj.core.api.Assertions.assertThat; public class TestGetBetterClassName { @Test public void testGoodNamesVsGeneratedAliases() { assertThatBetterClassName("AppCompatButton", "C2404e2").isEqualTo("AppCompatButton"); assertThatBetterClassName("ContextThemeWrapper", "C2106b1").isEqualTo("ContextThemeWrapper"); assertThatBetterClassName("ListPopupWindow", "C2344a3").isEqualTo("ListPopupWindow"); } @Test public void testShortGoodNamesVsGeneratedAliases() { assertThatBetterClassName("Room", "C2937kh").isEqualTo("Room"); assertThatBetterClassName("Fade", "C1428qi").isEqualTo("Fade"); assertThatBetterClassName("Scene", "C4063yi").isEqualTo("Scene"); } @Test public void testGoodNamesVsGeneratedAliasesWithPrefix() { assertThatBetterClassName("AppCompatActivity", "ActivityC2646h0").isEqualTo("AppCompatActivity"); assertThatBetterClassName("PagerAdapter", "AbstractC3038lk").isEqualTo("PagerAdapter"); assertThatBetterClassName("Lazy", "InterfaceC6434a").isEqualTo("Lazy"); assertThatBetterClassName("MembersInjector", "InterfaceC6435b").isEqualTo("MembersInjector"); assertThatBetterClassName("Subscriber", "InterfaceC6439c").isEqualTo("Subscriber"); } @Test public void testGoodNamesWithDigitsVsGeneratedAliases() { assertThatBetterClassName("ISO8061Formatter", "C1121uq4").isEqualTo("ISO8061Formatter"); assertThatBetterClassName("Jdk9Platform", "C1189rn6").isEqualTo("Jdk9Platform"); assertThatBetterClassName("WrappedDrawableApi14", "C2847i9").isEqualTo("WrappedDrawableApi14"); assertThatBetterClassName("WrappedDrawableApi21", "C2888j9").isEqualTo("WrappedDrawableApi21"); } @Test public void testShortNamesVsLongNames() { assertThatBetterClassName("az", "Observer").isEqualTo("Observer"); assertThatBetterClassName("bb", "RenderEvent").isEqualTo("RenderEvent"); assertThatBetterClassName("aaaa", "FontUtils").isEqualTo("FontUtils"); } /** * Tests {@link BetterName#getBetterClassName(String, String)} on equally good names. * In this case, according to the documentation, the method should return the first argument. * * @see BetterName#getBetterClassName(String, String) */ @Test public void testEquallyGoodNames() { assertThatBetterClassName("AAAA", "BBBB").isEqualTo("AAAA"); assertThatBetterClassName("BBBB", "AAAA").isEqualTo("BBBB"); assertThatBetterClassName("XYXYXY", "YZYZYZ").isEqualTo("XYXYXY"); assertThatBetterClassName("YZYZYZ", "XYXYXY").isEqualTo("YZYZYZ"); } private AbstractStringAssert assertThatBetterClassName(String firstName, String secondName) { return assertThat(getBetterClassName(firstName, secondName)); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/utils/TestGetBetterResourceName.java ================================================ package jadx.core.utils; import org.assertj.core.api.AbstractStringAssert; import org.junit.jupiter.api.Test; import static jadx.core.utils.BetterName.getBetterResourceName; import static org.assertj.core.api.Assertions.assertThat; public class TestGetBetterResourceName { @Test public void testGoodNamesVsSyntheticNames() { assertThatBetterResourceName("color_main", "t0").isEqualTo("color_main"); assertThatBetterResourceName("done", "oOo0oO0o").isEqualTo("done"); } /** * Tests {@link BetterName#getBetterResourceName(String, String)} on equally good names. * In this case, according to the documentation, the method should return the first argument. * * @see BetterName#getBetterResourceName(String, String) */ @Test public void testEquallyGoodNames() { assertThatBetterResourceName("AAAA", "BBBB").isEqualTo("AAAA"); assertThatBetterResourceName("BBBB", "AAAA").isEqualTo("BBBB"); assertThatBetterResourceName("Theme.AppCompat.Light", "Theme_AppCompat_Light") .isEqualTo("Theme.AppCompat.Light"); assertThatBetterResourceName("Theme_AppCompat_Light", "Theme.AppCompat.Light") .isEqualTo("Theme_AppCompat_Light"); } private AbstractStringAssert assertThatBetterResourceName(String firstName, String secondName) { return assertThat(getBetterResourceName(firstName, secondName)); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/utils/TypeUtilsTest.java ================================================ package jadx.core.utils; import java.util.List; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.RootNode; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; class TypeUtilsTest { private static final Logger LOG = LoggerFactory.getLogger(TypeUtilsTest.class); private static RootNode root; @BeforeAll public static void init() { root = new RootNode(new JadxArgs()); root.initClassPath(); } @Test public void testReplaceGenericsWithWildcards() { // check classpath graph List classGenerics = root.getTypeUtils().getClassGenerics(ArgType.object("java.util.ArrayList")); assertThat(classGenerics).hasSize(1); ArgType genericInfo = classGenerics.get(0); assertThat(genericInfo.getObject()).isEqualTo("E"); assertThat(genericInfo.getExtendTypes()).hasSize(0); // prepare input ArgType instanceType = ArgType.generic("java.util.ArrayList", ArgType.OBJECT); LOG.debug("instanceType: {}", instanceType); ArgType generic = ArgType.generic("java.util.List", ArgType.wildcard(ArgType.genericType("E"), ArgType.WildcardBound.SUPER)); LOG.debug("generic: {}", generic); // replace ArgType result = root.getTypeUtils().replaceClassGenerics(instanceType, generic); LOG.debug("result: {}", result); ArgType expected = ArgType.generic("java.util.List", ArgType.wildcard(ArgType.OBJECT, ArgType.WildcardBound.SUPER)); assertThat(result).isEqualTo(expected); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/utils/log/LogUtilsTest.java ================================================ package jadx.core.utils.log; import org.junit.jupiter.api.Test; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; class LogUtilsTest { @Test void escape() { String src = "a.b,c:d;e disallowed\"a'b#c*d\te\rf\ng"; String out = "a.b,c:d;e disallowed.a.b.c.d.e.f.g"; assertThat(LogUtils.escape(src)).isEqualTo(out); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/xmlgen/ResNameUtilsTest.java ================================================ package jadx.core.xmlgen; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static org.assertj.core.api.Assertions.assertThat; class ResNameUtilsTest { @DisplayName("Check sanitizeAsResourceName(name, postfix, allowNonPrintable)") @ParameterizedTest(name = "({0}, {1}, {2}) -> {3}") @MethodSource("provideArgsForSanitizeAsResourceNameTest") void testSanitizeAsResourceName(String name, String postfix, boolean allowNonPrintable, String expectedResult) { assertThat(ResNameUtils.sanitizeAsResourceName(name, postfix, allowNonPrintable)).isEqualTo(expectedResult); } @DisplayName("Check convertToRFieldName(resourceName)") @ParameterizedTest(name = "{0} -> {1}") @MethodSource("provideArgsForConvertToRFieldNameTest") void testConvertToRFieldName(String resourceName, String expectedResult) { assertThat(ResNameUtils.convertToRFieldName(resourceName)).isEqualTo(expectedResult); } private static Stream provideArgsForSanitizeAsResourceNameTest() { return Stream.of( Arguments.of("name", "_postfix", false, "name"), Arguments.of("/name", "_postfix", true, "_name_postfix"), Arguments.of("na/me", "_postfix", true, "na_me_postfix"), Arguments.of("name/", "_postfix", true, "name__postfix"), Arguments.of("$name", "_postfix", true, "_name_postfix"), Arguments.of("na$me", "_postfix", true, "na_me_postfix"), Arguments.of("name$", "_postfix", true, "name__postfix"), Arguments.of(".name", "_postfix", true, "_.name_postfix"), Arguments.of("na.me", "_postfix", true, "na.me"), Arguments.of("name.", "_postfix", true, "name."), Arguments.of("0name", "_postfix", true, "_0name_postfix"), Arguments.of("na0me", "_postfix", true, "na0me"), Arguments.of("name0", "_postfix", true, "name0"), Arguments.of("-name", "_postfix", true, "_name_postfix"), Arguments.of("na-me", "_postfix", true, "na_me_postfix"), Arguments.of("name-", "_postfix", true, "name__postfix"), Arguments.of("Ĉname", "_postfix", false, "_name_postfix"), Arguments.of("naĈme", "_postfix", false, "na_me_postfix"), Arguments.of("nameĈ", "_postfix", false, "name__postfix"), Arguments.of("Ĉname", "_postfix", true, "Ĉname"), Arguments.of("naĈme", "_postfix", true, "naĈme"), Arguments.of("nameĈ", "_postfix", true, "nameĈ"), // Uncomment this when XID_Start and XID_Continue characters are correctly determined. // Arguments.of("Жname", "_postfix", true, "Жname"), // Arguments.of("naЖme", "_postfix", true, "naЖme"), // Arguments.of("nameЖ", "_postfix", true, "nameЖ"), // // Arguments.of("€name", "_postfix", true, "_name_postfix"), // Arguments.of("na€me", "_postfix", true, "na_me_postfix"), // Arguments.of("name€", "_postfix", true, "name__postfix"), Arguments.of("", "_postfix", true, "_postfix"), Arguments.of("if", "_postfix", true, "if_postfix"), Arguments.of("default", "_postfix", true, "default_postfix"), Arguments.of("true", "_postfix", true, "true_postfix"), Arguments.of("_", "_postfix", true, "__postfix")); } private static Stream provideArgsForConvertToRFieldNameTest() { return Stream.of( Arguments.of("ThemeDesign", "ThemeDesign"), Arguments.of("Theme.Design", "Theme_Design"), Arguments.of("Ĉ_ThemeDesign_Ĉ", "Ĉ_ThemeDesign_Ĉ"), Arguments.of("Ĉ_Theme.Design_Ĉ", "Ĉ_Theme_Design_Ĉ"), // The function must return a plausible result even though the resource name is invalid. Arguments.of("/_ThemeDesign_/", "/_ThemeDesign_/"), Arguments.of("/_Theme.Design_/", "/_Theme_Design_/")); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/xmlgen/ResXmlGenTest.java ================================================ package jadx.core.xmlgen; import java.util.ArrayList; import java.util.List; import org.assertj.core.util.Lists; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import jadx.api.JadxArgs; import jadx.api.security.IJadxSecurity; import jadx.api.security.JadxSecurityFlag; import jadx.api.security.impl.JadxSecurity; import jadx.core.xmlgen.entry.RawNamedValue; import jadx.core.xmlgen.entry.RawValue; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ValuesParser; import static org.assertj.core.api.Assertions.assertThat; class ResXmlGenTest { private final JadxArgs args = new JadxArgs(); private final IJadxSecurity security = new JadxSecurity(JadxSecurityFlag.all()); private final ManifestAttributes manifestAttributes = new ManifestAttributes(security); @BeforeEach void init() { args.setCodeNewLineStr("\n"); } @Test void testSimpleAttr() { ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "attr", "size", ""); re.setNamedValues(Lists.list(new RawNamedValue(16777216, new RawValue(16, 64)))); resStorage.add(re); ValuesParser vp = new ValuesParser(null, resStorage.getResourcesNames()); ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); assertThat(files.get(0).getName()).isEqualTo("res/values/attrs.xml"); String input = files.get(0).getText().toString(); assertThat(input).isEqualTo("\n" + "\n" + " \n" + " \n" + ""); } @Test void testAttrEnum() { ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "attr", "size", ""); re.setNamedValues( Lists.list( new RawNamedValue(0x01000000, new RawValue(16, 65536)), new RawNamedValue(0x01040000, new RawValue(16, 1)))); resStorage.add(re); ValuesParser vp = new ValuesParser(null, resStorage.getResourcesNames()); ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); assertThat(files.get(0).getName()).isEqualTo("res/values/attrs.xml"); String input = files.get(0).getText().toString(); assertThat(input).isEqualTo("\n" + "\n" + " \n" + " \n" + " \n" + ""); } @Test void testAttrFlag() { ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "attr", "size", ""); re.setNamedValues( Lists.list( new RawNamedValue(0x01000000, new RawValue(16, 131072)), new RawNamedValue(0x01040000, new RawValue(16, 1)))); resStorage.add(re); ValuesParser vp = new ValuesParser(null, resStorage.getResourcesNames()); ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); assertThat(files.get(0).getName()).isEqualTo("res/values/attrs.xml"); String input = files.get(0).getText().toString(); assertThat(input).isEqualTo("\n" + "\n" + " \n" + " \n" + " \n" + ""); } @Test void testAttrMin() { ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "attr", "size", ""); re.setNamedValues( Lists.list(new RawNamedValue(16777216, new RawValue(16, 4)), new RawNamedValue(16777217, new RawValue(16, 1)))); resStorage.add(re); ValuesParser vp = new ValuesParser(null, resStorage.getResourcesNames()); ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); assertThat(files.get(0).getName()).isEqualTo("res/values/attrs.xml"); String input = files.get(0).getText().toString(); assertThat(input).isEqualTo("\n" + "\n" + " \n" + " \n" + ""); } @Test void testStyle() { ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "style", "JadxGui", ""); re.setNamedValues(Lists.list(new RawNamedValue(16842836, new RawValue(1, 17170445)))); resStorage.add(re); re = new ResourceEntry(2130903104, "jadx.gui.app", "style", "JadxGui.Dialog", ""); re.setParentRef(2130903103); re.setNamedValues(new ArrayList<>()); resStorage.add(re); ValuesParser vp = new ValuesParser(null, resStorage.getResourcesNames()); ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); assertThat(files.get(0).getName()).isEqualTo("res/values/styles.xml"); String input = files.get(0).getText().toString(); assertThat(input).isEqualTo("\n" + "\n" + " \n" + " \n" + ""); } @Test void testString() { ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "string", "app_name", ""); re.setSimpleValue(new RawValue(3, 0)); re.setNamedValues(Lists.list()); resStorage.add(re); BinaryXMLStrings strings = new BinaryXMLStrings(); strings.put(0, "Jadx Decompiler App"); ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); assertThat(files.get(0).getName()).isEqualTo("res/values/strings.xml"); String input = files.get(0).getText().toString(); assertThat(input).isEqualTo("\n" + "\n" + " Jadx Decompiler App\n" + ""); } @Test void testStringFormattedFalse() { ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "string", "app_name", ""); re.setSimpleValue(new RawValue(3, 0)); re.setNamedValues(Lists.list()); resStorage.add(re); BinaryXMLStrings strings = new BinaryXMLStrings(); strings.put(0, "%s at %s"); ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); assertThat(files.get(0).getName()).isEqualTo("res/values/strings.xml"); String input = files.get(0).getText().toString(); assertThat(input).isEqualTo("\n" + "\n" + " %s at %s\n" + ""); } @Test void testArrayEscape() { ResourceStorage resStorage = new ResourceStorage(security); ResourceEntry re = new ResourceEntry(2130903103, "jadx.gui.app", "array", "single_quote_escape_sample", ""); re.setNamedValues( Lists.list(new RawNamedValue(16777216, new RawValue(3, 0)))); resStorage.add(re); BinaryXMLStrings strings = new BinaryXMLStrings(); strings.put(0, "Let's go"); ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); ResXmlGen resXmlGen = new ResXmlGen(resStorage, vp, manifestAttributes); List files = resXmlGen.makeResourcesXml(args); assertThat(files).hasSize(1); assertThat(files.get(0).getName()).isEqualTo("res/values/arrays.xml"); String input = files.get(0).getText().toString(); assertThat(input).isEqualTo("\n" + "\n" + " \n" + " Let\\'s go\n" + " \n" + ""); } } ================================================ FILE: jadx-core/src/test/java/jadx/core/xmlgen/entry/ValuesParserTest.java ================================================ package jadx.core.xmlgen.entry; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.core.utils.android.AndroidResourcesMap; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; class ValuesParserTest { @Test void testResMapLoad() { Map androidResMap = AndroidResourcesMap.getMap(); assertThat(androidResMap).isNotNull().isNotEmpty(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java ================================================ package jadx.tests.api; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.List; import org.junit.jupiter.api.io.TempDir; import jadx.api.ICodeInfo; import jadx.api.JadxArgs; import jadx.api.ResourceFile; import jadx.api.ResourceFileContainer; import jadx.api.ResourceFileContent; import jadx.api.ResourceType; import jadx.api.impl.SimpleCodeInfo; import jadx.core.dex.nodes.RootNode; import jadx.core.export.ExportGradle; import jadx.core.export.ExportGradleType; import jadx.core.export.OutDirs; import jadx.core.xmlgen.ResContainer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; public abstract class ExportGradleTest { private static final String MANIFEST_TESTS_DIR = "manifest"; private final RootNode root = new RootNode(new JadxArgs()); @TempDir private File exportDir; protected ICodeInfo loadResource(String filename) { return new SimpleCodeInfo(loadResourceContent(MANIFEST_TESTS_DIR, filename)); } private static String loadFileContent(File filePath) { try { return Files.readString(filePath.toPath()); } catch (IOException e) { fail("Loading file failed", e); return ""; } } private String loadResourceContent(String dir, String filename) { String resPath = dir + '/' + filename; try (InputStream in = getClass().getClassLoader().getResourceAsStream(resPath)) { if (in == null) { fail("Resource not found: " + resPath); return ""; } return new String(in.readAllBytes(), StandardCharsets.UTF_8); } catch (Exception e) { fail("Loading file failed: " + resPath, e); return ""; } } protected RootNode getRootNode() { return root; } protected void exportGradle(String manifestFilename, String stringsFileName) { ResourceFile androidManifest = new ResourceFileContent("AndroidManifest.xml", ResourceType.MANIFEST, loadResource(manifestFilename)); ResContainer strings = ResContainer.textResource(stringsFileName, loadResource(stringsFileName)); ResContainer arsc = ResContainer.resourceTable("resources.arsc", List.of(strings), new SimpleCodeInfo("empty")); ResourceFile arscFile = new ResourceFileContainer("resources.arsc", ResourceType.ARSC, arsc); List resources = List.of(androidManifest, arscFile); root.getArgs().setExportGradleType(ExportGradleType.ANDROID_APP); ExportGradle export = new ExportGradle(root, exportDir, resources); OutDirs outDirs = export.init(); assertThat(outDirs.getSrcOutDir()).exists(); assertThat(outDirs.getResOutDir()).exists(); export.generateGradleFiles(); } protected String getAppGradleBuild() { return loadFileContent(new File(exportDir, "app/build.gradle")); } protected String getSettingsGradle() { return loadFileContent(new File(exportDir, "settings.gradle")); } protected File getGradleProperiesFile() { return new File(exportDir, "gradle.properties"); } protected String getGradleProperties() { return loadFileContent(getGradleProperiesFile()); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java ================================================ package jadx.tests.api; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.CommentsLevel; import jadx.api.DecompilationMode; import jadx.api.ICodeInfo; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.JadxInternalAccess; import jadx.api.JavaClass; import jadx.api.JavaMethod; import jadx.api.JavaVariable; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.api.ResourcesLoader; import jadx.api.args.GeneratedRenamesMappingFileMode; import jadx.api.data.IJavaNodeRef; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxCodeRename; import jadx.api.data.impl.JadxNodeRef; import jadx.api.impl.SimpleCodeInfo; import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.VarNode; import jadx.api.plugins.CustomResourcesLoader; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.core.xmlgen.BinaryXMLStrings; import jadx.core.xmlgen.IResTableParser; import jadx.core.xmlgen.ResContainer; import jadx.core.xmlgen.ResourceStorage; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.tests.api.compiler.CompilerOptions; import jadx.tests.api.compiler.JavaUtils; import jadx.tests.api.compiler.TestCompiler; import jadx.tests.api.utils.TestFilesGetter; import jadx.tests.api.utils.TestUtils; import static org.apache.commons.lang3.StringUtils.leftPad; import static org.apache.commons.lang3.StringUtils.rightPad; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; public abstract class IntegrationTest extends TestUtils { private static final Logger LOG = LoggerFactory.getLogger(IntegrationTest.class); private static final String TEST_DIRECTORY = "src/test/java"; private static final String TEST_DIRECTORY2 = "jadx-core/" + TEST_DIRECTORY; private static final String DEFAULT_INPUT_PLUGIN = "dx"; /** * Set 'TEST_INPUT_PLUGIN' env variable to use 'java' or 'dx' input in tests */ static final boolean USE_JAVA_INPUT = Utils.getOrElse(System.getenv("TEST_INPUT_PLUGIN"), DEFAULT_INPUT_PLUGIN).equals("java"); /** * Run auto check method if defined: * *
	 * public void check() {
	 * }
	 * 
*/ private static final String CHECK_METHOD_NAME = "check"; protected JadxArgs args; protected boolean compile; private CompilerOptions compilerOptions; private boolean saveTestJar = false; protected Map resMap = Collections.emptyMap(); private boolean allowWarnInCode; private boolean printLineNumbers; private boolean printOffsets; private boolean printDisassemble; private @Nullable Boolean useJavaInput; private boolean removeParentClassOnInput; private @Nullable TestCompiler sourceCompiler; private @Nullable TestCompiler decompiledCompiler; /** * Run check method on decompiled code even if source check method not found. * Useful for smali test if check method added to smali code */ private boolean forceDecompiledCheck = false; protected JadxDecompiler jadxDecompiler; @TempDir Path testDir; @BeforeEach public void init() { this.compile = true; this.compilerOptions = new CompilerOptions(); this.resMap = Collections.emptyMap(); this.removeParentClassOnInput = true; this.useJavaInput = null; args = new JadxArgs(); args.setOutDir(testDir.toFile()); args.setShowInconsistentCode(true); args.setThreadsCount(1); args.setSkipResources(true); args.setCommentsLevel(CommentsLevel.DEBUG); args.setDeobfuscationOn(false); args.setGeneratedRenamesMappingFileMode(GeneratedRenamesMappingFileMode.IGNORE); args.setRunDebugChecks(true); args.setFilesGetter(new TestFilesGetter(testDir)); // use the same values on all systems args.setFsCaseSensitive(false); args.setCodeNewLineStr("\n"); args.setCodeIndentStr(JadxArgs.DEFAULT_INDENT_STR); } @AfterEach public void after() throws IOException { close(jadxDecompiler); close(sourceCompiler); close(decompiledCompiler); } private void close(Closeable closeable) throws IOException { if (closeable != null) { closeable.close(); } } public void setOutDirSuffix(String suffix) { args.setOutDir(new File(testDir.toFile(), suffix)); } public String getTestName() { return this.getClass().getSimpleName(); } public String getTestPkg() { return this.getClass().getPackage().getName().replace("jadx.tests.integration.", ""); } public ClassNode getClassNode(Class clazz) { try { List files = compileClass(clazz); assertThat(files).as("File list is empty").isNotEmpty(); return getClassNodeFromFiles(files, clazz.getName()); } catch (Exception e) { LOG.error("Failed to get class node", e); fail(e.getMessage()); return null; } } public List getClassNodes(Class... classes) { try { assertThat(classes).as("Class list is empty").isNotEmpty(); List srcFiles = Stream.of(classes).map(this::getSourceFileForClass).collect(Collectors.toList()); List clsFiles = compileSourceFiles(srcFiles); assertThat(clsFiles).as("Class files list is empty").isNotEmpty(); return decompileFiles(clsFiles); } catch (Exception e) { LOG.error("Failed to get class node", e); fail(e.getMessage()); return null; } } public ClassNode getClassNodeFromFiles(List files, String clsName) { jadxDecompiler = loadFiles(files); RootNode root = JadxInternalAccess.getRoot(jadxDecompiler); ClassNode cls = root.resolveClass(clsName); assertThat(cls).as("Class not found: " + clsName).isNotNull(); if (removeParentClassOnInput) { assertThat(clsName).isEqualTo(cls.getClassInfo().getFullName()); } else { LOG.info("Convert back to top level: {}", cls); cls.getTopParentClass().decompile(); // keep correct process order cls.notInner(); } decompileAndCheck(cls); return cls; } public List decompileFiles(List files) { jadxDecompiler = loadFiles(files); List sortedClsNodes = jadxDecompiler.getDecompileScheduler() .buildBatches(jadxDecompiler.getClasses()) .stream() .flatMap(Collection::stream) .map(JavaClass::getClassNode) .collect(Collectors.toList()); decompileAndCheck(sortedClsNodes); return sortedClsNodes; } @NotNull public ClassNode searchTestCls(List list, String shortClsName) { return searchCls(list, getTestPkg() + '.' + shortClsName); } @NotNull public ClassNode searchCls(List list, String clsName) { for (ClassNode cls : list) { if (cls.getClassInfo().getFullName().equals(clsName)) { return cls; } } for (ClassNode cls : list) { if (cls.getClassInfo().getShortName().equals(clsName)) { return cls; } } fail("Class not found by name " + clsName + " in list: " + list); return null; } protected JadxDecompiler loadFiles(List inputFiles) { args.setInputFiles(inputFiles); boolean useDx = !isJavaInput(); LOG.info(useDx ? "Using dex input" : "Using java input"); args.setUseDxInput(useDx); JadxDecompiler d = new JadxDecompiler(args); try { insertResources(d); d.load(); return d; } catch (Exception e) { LOG.error("Load failed", e); d.close(); fail(e.getMessage()); return null; } } protected void decompileAndCheck(ClassNode cls) { decompileAndCheck(Collections.singletonList(cls)); } protected void decompileAndCheck(List clsList) { clsList.forEach(cls -> cls.add(AFlag.DONT_UNLOAD_CLASS)); // keep error and warning attributes clsList.forEach(ClassNode::decompile); for (ClassNode cls : clsList) { System.out.println("-----------------------------------------------------------"); ICodeInfo code = cls.getCode(); if (printLineNumbers) { printCodeWithLineNumbers(code); } else if (printOffsets) { printCodeWithOffsets(code); } else { System.out.println(code); } } System.out.println("-----------------------------------------------------------"); if (printDisassemble) { clsList.forEach(this::printDisasm); } runChecks(clsList); } public void runChecks(ClassNode cls) { runChecks(Collections.singletonList(cls)); } protected void runChecks(List clsList) { clsList.forEach(cls -> checkCode(cls, allowWarnInCode)); compileClassNode(clsList); clsList.forEach(this::runAutoCheck); } private void printDisasm(ClassNode cls) { System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); System.out.println(cls.getDisassembledCode()); System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); } private void printCodeWithLineNumbers(ICodeInfo code) { String codeStr = code.getCodeStr(); Map lineMapping = code.getCodeMetadata().getLineMapping(); String[] lines = codeStr.split("\\R"); for (int i = 0; i < lines.length; i++) { String line = lines[i]; int curLine = i + 1; String lineNumStr = "/* " + leftPad(String.valueOf(curLine), 3) + " */"; Integer sourceLine = lineMapping.get(curLine); if (sourceLine != null) { lineNumStr += " /* " + sourceLine + " */"; } System.out.println(rightPad(lineNumStr, 20) + line); } } private void printCodeWithOffsets(ICodeInfo code) { String codeStr = code.getCodeStr(); ICodeMetadata metadata = code.getCodeMetadata(); int lineStartPos = 0; String newLineStr = args.getCodeNewLineStr(); int newLineLen = newLineStr.length(); for (String line : codeStr.split(newLineStr)) { Object ann = metadata.getAt(lineStartPos); String offsetStr = ""; if (ann instanceof InsnCodeOffset) { int offset = ((InsnCodeOffset) ann).getOffset(); offsetStr = "/* " + leftPad(String.valueOf(offset), 5) + " */"; } System.out.println(rightPad(offsetStr, 12) + line); lineStartPos += line.length() + newLineLen; } } /** * Insert mock resource table data ('.arsc' file) */ private void insertResources(JadxDecompiler decompiler) throws IOException { if (resMap.isEmpty()) { return; } String resTableName = "test-res-table"; Path resTablePath = testDir.resolve(resTableName); File resTableFile = resTablePath.toFile().getAbsoluteFile(); FileUtils.makeDirsForFile(resTableFile); Files.writeString(resTablePath, resTableName); JadxArgs jadxArgs = decompiler.getArgs(); jadxArgs.getInputFiles().add(resTableFile); // load mock file as 'arsc' decompiler.addCustomResourcesLoader(new CustomResourcesLoader() { @Override public boolean load(ResourcesLoader loader, List list, File file) { if (file == resTableFile) { list.add(ResourceFile.createResourceFile(decompiler, resTableFile, ResourceType.ARSC)); return true; } return false; } @Override public void close() { } }); // convert resources map to resource storage object ResourceStorage resStorage = new ResourceStorage(jadxArgs.getSecurity()); for (Map.Entry entry : resMap.entrySet()) { Integer id = entry.getKey(); String name = entry.getValue(); String[] parts = name.split("\\."); resStorage.add(new ResourceEntry(id, "", parts[0], parts[1], "")); } // mock res table parser to just return resource storage IResTableParser resTableParser = new IResTableParser() { @Override public void decode(InputStream inputStream) { } @Override public ResourceStorage getResStorage() { return resStorage; } @Override public ResContainer decodeFiles() { return ResContainer.textResource(resTableName, new SimpleCodeInfo(resTableName)); } @Override public BinaryXMLStrings getStrings() { return new BinaryXMLStrings(); } }; // directly return generated resource storage instead parsing for mock res file decompiler.getResourcesLoader().addResTableParserProvider(resFile -> { if (resFile.getOriginalName().equals(resTableFile.getAbsolutePath())) { return resTableParser; } return null; }); } private void runAutoCheck(ClassNode cls) { String clsName = cls.getClassInfo().getRawName().replace('/', '.'); try { // run 'check' method from original class boolean sourceCheckFound = runSourceAutoCheck(clsName); // run 'check' method from decompiled class if (compile && (sourceCheckFound || forceDecompiledCheck)) { runDecompiledAutoCheck(cls); } } catch (Exception e) { LOG.error("Auto check failed", e); fail("Auto check exception: " + e.getMessage()); } } private boolean runSourceAutoCheck(String clsName) { if (sourceCompiler == null) { System.out.println("Source check: no code"); return false; } Class origCls; try { origCls = sourceCompiler.getClass(clsName); } catch (ClassNotFoundException e) { rethrow("Missing class: " + clsName, e); return false; } Method checkMth; try { checkMth = sourceCompiler.getMethod(origCls, CHECK_METHOD_NAME, new Class[] {}); } catch (NoSuchMethodException e) { // ignore return false; } if (!checkMth.getReturnType().equals(void.class) || !Modifier.isPublic(checkMth.getModifiers()) || Modifier.isStatic(checkMth.getModifiers())) { fail("Wrong 'check' method"); return false; } try { limitExecTime(() -> checkMth.invoke(origCls.getConstructor().newInstance())); System.out.println("Source check: PASSED"); return true; } catch (Throwable e) { throw new JadxRuntimeException("Source check failed", e); } } public void runDecompiledAutoCheck(ClassNode cls) { try { limitExecTime(() -> invoke(decompiledCompiler, cls.getFullName(), CHECK_METHOD_NAME)); System.out.println("Decompiled check: PASSED"); } catch (Throwable e) { rethrow("Decompiled check failed", e); } } private T limitExecTime(Callable call) { ExecutorService executor = Executors.newSingleThreadExecutor(); Future future = executor.submit(call); try { return future.get(5, TimeUnit.SECONDS); } catch (TimeoutException ex) { future.cancel(true); rethrow("Execution timeout", ex); } catch (Throwable ex) { rethrow(ex.getMessage(), ex); } finally { executor.shutdownNow(); } return null; } public static void rethrow(String msg, Throwable e) { if (e instanceof InvocationTargetException) { rethrow(msg, e.getCause()); } else if (e instanceof ExecutionException) { rethrow(msg, e.getCause()); } else if (e instanceof AssertionError) { System.err.println(msg); throw (AssertionError) e; } else { throw new RuntimeException(msg, e); } } protected MethodNode getMethod(ClassNode cls, String methodName) { for (MethodNode mth : cls.getMethods()) { if (mth.getName().equals(methodName)) { return mth; } } fail("Method not found " + methodName + " in class " + cls); return null; } protected FieldNode getField(ClassNode cls, String fieldName) { for (FieldNode fld : cls.getFields()) { if (fld.getName().equals(fieldName)) { return fld; } } fail("Field not found " + fieldName + " in class " + cls); return null; } void compileClassNode(List clsList) { if (!compile) { return; } try { // TODO: eclipse uses files or compilation units providers added in Java 9 compilerOptions.setUseEclipseCompiler(false); decompiledCompiler = new TestCompiler(compilerOptions); decompiledCompiler.compileNodes(clsList); System.out.println("Compilation: PASSED"); } catch (Exception e) { fail(e); } } public Object invoke(TestCompiler compiler, String clsFullName, String method) throws Exception { assertThat(compiler).as("compiler not ready").isNotNull(); return compiler.invoke(clsFullName, method, new Class[] {}, new Object[] {}); } private List compileClass(Class cls) throws IOException { File sourceFile = getSourceFileForClass(cls); List clsFiles = compileSourceFiles(Collections.singletonList(sourceFile)); if (removeParentClassOnInput) { // remove classes which are parents for test class String clsFullName = cls.getName(); String clsName = clsFullName.substring(clsFullName.lastIndexOf('.') + 1); clsFiles.removeIf(next -> !next.getName().contains(clsName)); } return clsFiles; } private File getSourceFileForClass(Class cls) { String clsFullName = cls.getName(); int innerEnd = clsFullName.indexOf('$'); String rootClsName = innerEnd == -1 ? clsFullName : clsFullName.substring(0, innerEnd); String javaFileName = rootClsName.replace('.', '/') + ".java"; File file = new File(TEST_DIRECTORY, javaFileName); if (file.exists()) { return file; } File file2 = new File(TEST_DIRECTORY2, javaFileName); if (file2.exists()) { return file2; } throw new JadxRuntimeException("Test source not found for class: " + clsFullName); } private List compileSourceFiles(List compileFileList) throws IOException { Path outTmp = Files.createTempDirectory(testDir, "jadx-tmp-classes"); sourceCompiler = new TestCompiler(compilerOptions); List files = sourceCompiler.compileFiles(compileFileList, outTmp); if (saveTestJar) { saveToJar(files, outTmp); } return files; } private void saveToJar(List files, Path baseDir) throws IOException { Path jarFile = Files.createTempFile("tests-" + getTestName() + '-', ".jar"); try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(jarFile))) { for (File file : files) { Path fullPath = file.toPath(); Path relativePath = baseDir.relativize(fullPath); JarEntry entry = new JarEntry(relativePath.toString()); jar.putNextEntry(entry); jar.write(Files.readAllBytes(fullPath)); jar.closeEntry(); } } LOG.info("Test jar saved to: {}", jarFile.toAbsolutePath()); } public JadxArgs getArgs() { return args; } public CompilerOptions getCompilerOptions() { return compilerOptions; } public void setArgs(JadxArgs args) { this.args = args; } public void setResMap(Map resMap) { this.resMap = resMap; } protected void noDebugInfo() { this.compilerOptions.setIncludeDebugInfo(false); } public void useEclipseCompiler() { Assumptions.assumeTrue(JavaUtils.checkJavaVersion(11), "eclipse compiler library using Java 11"); this.compilerOptions.setUseEclipseCompiler(true); } public void useTargetJavaVersion(int version) { Assumptions.assumeTrue(JavaUtils.checkJavaVersion(version), "skip test for higher java version"); this.compilerOptions.setJavaVersion(version); } protected void setFallback() { disableCompilation(); this.args.setDecompilationMode(DecompilationMode.FALLBACK); } protected void disableCompilation() { this.compile = false; } protected void forceDecompiledCheck() { this.forceDecompiledCheck = true; } protected void enableDeobfuscation() { args.setDeobfuscationOn(true); args.setGeneratedRenamesMappingFileMode(GeneratedRenamesMappingFileMode.IGNORE); args.setDeobfuscationMinLength(2); args.setDeobfuscationMaxLength(64); } protected void allowWarnInCode() { allowWarnInCode = true; } protected void printLineNumbers() { printLineNumbers = true; } protected void printOffsets() { printOffsets = true; } public void useJavaInput() { this.useJavaInput = true; } public void useDexInput() { Assumptions.assumeFalse(USE_JAVA_INPUT, "skip dex input tests"); this.useJavaInput = false; } public void useDexInput(String mode) { useDexInput(); this.getArgs().getPluginOptions().put("java-convert.mode", mode); } protected boolean isJavaInput() { return Utils.getOrElse(useJavaInput, USE_JAVA_INPUT); } public void keepParentClassOnInput() { this.removeParentClassOnInput = false; } // Use only for debug purpose protected void printDisassemble() { this.printDisassemble = true; } // Use only for debug purpose protected void saveTestJar() { this.saveTestJar = true; } protected void addClsRename(String fullClsName, String newName) { JadxNodeRef clsRef = JadxNodeRef.forCls(fullClsName); getCodeData().getRenames().add(new JadxCodeRename(clsRef, newName)); } protected void addMthRename(String fullClsName, String mthSignature, String newName) { JadxNodeRef mthRef = new JadxNodeRef(IJavaNodeRef.RefType.METHOD, fullClsName, mthSignature); getCodeData().getRenames().add(new JadxCodeRename(mthRef, newName)); } protected void addFldRename(String fullClsName, String fldSignature, String newName) { JadxNodeRef fldRef = new JadxNodeRef(IJavaNodeRef.RefType.FIELD, fullClsName, fldSignature); getCodeData().getRenames().add(new JadxCodeRename(fldRef, newName)); } protected JadxCodeData getCodeData() { JadxCodeData codeData = (JadxCodeData) getArgs().getCodeData(); if (codeData == null) { codeData = new JadxCodeData(); codeData.setRenames(new ArrayList<>()); codeData.setComments(new ArrayList<>()); getArgs().setCodeData(codeData); } return codeData; } protected JavaClass toJavaClass(ClassNode cls) { JavaClass javaClass = JadxInternalAccess.convertClassNode(jadxDecompiler, cls); assertThat(javaClass).isNotNull(); return javaClass; } protected JavaMethod toJavaMethod(MethodNode mth) { JavaMethod javaMethod = JadxInternalAccess.convertMethodNode(jadxDecompiler, mth); assertThat(javaMethod).isNotNull(); return javaMethod; } protected JavaVariable toJavaVariable(VarNode varNode) { JavaVariable javaVariable = (JavaVariable) jadxDecompiler.getJavaNodeByCodeAnnotation(null, varNode); assertThat(javaVariable).isNotNull(); return javaVariable; } public File getResourceFile(String filePath) { URL resource = getClass().getClassLoader().getResource(filePath); assertThat(resource).as("Resource not found: %s", filePath).isNotNull(); String resPath = resource.getFile(); return new File(resPath); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/RaungTest.java ================================================ package jadx.tests.api; import java.io.File; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import jadx.api.JadxInternalAccess; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public abstract class RaungTest extends IntegrationTest { private static final String RAUNG_TESTS_PROJECT = "jadx-core"; private static final String RAUNG_TESTS_DIR = "src/test/raung"; private static final String RAUNG_TESTS_EXT = ".raung"; @BeforeEach public void init() { super.init(); this.useJavaInput(); } /** * Preferred method for one file raung test */ protected ClassNode getClassNodeFromRaung() { String pkg = getTestPkg(); String clsName = getTestName(); return getClassNodeFromRaung(pkg + File.separatorChar + clsName, pkg + '.' + clsName); } protected ClassNode getClassNodeFromRaung(String file, String clsName) { File raungFile = getRaungFile(file); return getClassNodeFromFiles(Collections.singletonList(raungFile), clsName); } protected List loadFromRaungFiles() { jadxDecompiler = loadFiles(collectRaungFiles(getTestPkg(), getTestName())); RootNode root = JadxInternalAccess.getRoot(jadxDecompiler); List classes = root.getClasses(false); decompileAndCheck(classes); return classes; } private List collectRaungFiles(String pkg, String testDir) { String raungFilesDir = pkg + File.separatorChar + testDir + File.separatorChar; File raungDir = getRaungDir(raungFilesDir); String[] raungFileNames = raungDir.list((dir, name) -> name.endsWith(".raung")); assertThat(raungFileNames).as("Raung files not found in " + raungDir).isNotNull(); return Stream.of(raungFileNames) .map(file -> new File(raungDir, file)) .collect(Collectors.toList()); } private static File getRaungFile(String baseName) { File raungFile = new File(RAUNG_TESTS_DIR, baseName + RAUNG_TESTS_EXT); if (raungFile.exists()) { return raungFile; } File pathFromRoot = new File(RAUNG_TESTS_PROJECT, raungFile.getPath()); if (pathFromRoot.exists()) { return pathFromRoot; } throw new AssertionError("Raung file not found: " + raungFile.getPath()); } private static File getRaungDir(String baseName) { File raungDir = new File(RAUNG_TESTS_DIR, baseName); if (raungDir.exists()) { return raungDir; } File pathFromRoot = new File(RAUNG_TESTS_PROJECT, raungDir.getPath()); if (pathFromRoot.exists()) { return pathFromRoot; } throw new AssertionError("Raung dir not found: " + raungDir.getPath()); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/SmaliTest.java ================================================ package jadx.tests.api; import java.io.File; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import jadx.api.JadxInternalAccess; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; import static org.assertj.core.api.Assertions.assertThat; public abstract class SmaliTest extends IntegrationTest { private static final String SMALI_TESTS_DIR = "src/test/smali"; private static final String SMALI_TESTS_EXT = ".smali"; private String currentProject = "jadx-core"; public void setCurrentProject(String currentProject) { this.currentProject = currentProject; } @BeforeEach public void init() { Assumptions.assumeFalse(USE_JAVA_INPUT, "skip smali test for java input tests"); super.init(); this.useDexInput(); } protected ClassNode getClassNodeFromSmali(String file, String clsName) { File smaliFile = getSmaliFile(file); return getClassNodeFromFiles(Collections.singletonList(smaliFile), clsName); } /** * Preferred method for one file smali test */ protected ClassNode getClassNodeFromSmali() { return getClassNodeFromSmaliWithPkg(getTestPkg(), getTestName()); } protected ClassNode getClassNodeFromSmaliWithClsName(String fullClsName) { return getClassNodeFromSmali(getTestPkg() + File.separatorChar + getTestName(), fullClsName); } protected ClassNode getClassNodeFromSmaliWithPath(String path, String clsName) { return getClassNodeFromSmali(path + File.separatorChar + clsName, clsName); } protected ClassNode getClassNodeFromSmaliWithPkg(String pkg, String clsName) { return getClassNodeFromSmali(pkg + File.separatorChar + clsName, pkg + '.' + clsName); } protected ClassNode getClassNodeFromSmaliFiles(String pkg, String testName, String clsName) { return getClassNodeFromFiles(collectSmaliFiles(pkg, testName), pkg + '.' + clsName); } protected ClassNode getClassNodeFromSmaliFiles(String clsName) { return searchCls(loadFromSmaliFiles(), getTestPkg() + '.' + clsName); } protected ClassNode getClassNodeFromSmaliFiles() { return searchCls(loadFromSmaliFiles(), getTestPkg() + '.' + getTestName()); } protected List loadFromSmaliFiles() { jadxDecompiler = loadFiles(collectSmaliFiles(getTestPkg(), getTestName())); RootNode root = JadxInternalAccess.getRoot(jadxDecompiler); List classes = root.getClasses(false); decompileAndCheck(classes); return classes; } private List collectSmaliFiles(String pkg, @Nullable String testDir) { String smaliFilesDir; if (testDir == null) { smaliFilesDir = pkg + File.separatorChar; } else { smaliFilesDir = pkg + File.separatorChar + testDir + File.separatorChar; } File smaliDir = getSmaliDir(smaliFilesDir); String[] smaliFileNames = smaliDir.list((dir, name) -> name.endsWith(".smali")); assertThat(smaliFileNames).as("Smali files not found in " + smaliDir).isNotNull(); return Stream.of(smaliFileNames) .map(file -> new File(smaliDir, file)) .collect(Collectors.toList()); } private File getSmaliFile(String baseName) { File smaliFile = new File(SMALI_TESTS_DIR, baseName + SMALI_TESTS_EXT); if (smaliFile.exists()) { return smaliFile; } File pathFromRoot = new File(currentProject, smaliFile.getPath()); if (pathFromRoot.exists()) { return pathFromRoot; } throw new AssertionError("Smali file not found: " + smaliFile.getPath()); } private File getSmaliDir(String baseName) { File smaliDir = new File(SMALI_TESTS_DIR, baseName); if (smaliDir.exists()) { return smaliDir; } File pathFromRoot = new File(currentProject, smaliDir.getPath()); if (pathFromRoot.exists()) { return pathFromRoot; } throw new AssertionError("Smali dir not found: " + smaliDir.getPath()); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/compiler/ClassFileManager.java ================================================ package jadx.tests.api.compiler; import java.io.Closeable; import java.io.File; import java.util.ArrayList; import java.util.List; import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import static javax.tools.JavaFileObject.Kind; public class ClassFileManager extends ForwardingJavaFileManager implements Closeable { private final DynamicClassLoader classLoader; public ClassFileManager(StandardJavaFileManager standardManager) { super(standardManager); classLoader = new DynamicClassLoader(); } public List getJavaFileObjectsFromFiles(List sourceFiles) { List list = new ArrayList<>(); for (JavaFileObject javaFileObject : fileManager.getJavaFileObjectsFromFiles(sourceFiles)) { list.add(javaFileObject); } return list; } @Override public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) { JavaClassObject clsObject = new JavaClassObject(className, kind); classLoader.add(className, clsObject); return clsObject; } @Override public ClassLoader getClassLoader(Location location) { return classLoader; } public DynamicClassLoader getClassLoader() { return classLoader; } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/compiler/CompilerOptions.java ================================================ package jadx.tests.api.compiler; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class CompilerOptions { private boolean includeDebugInfo = true; private boolean useEclipseCompiler = false; private int javaVersion = 8; List arguments = Collections.emptyList(); public boolean isIncludeDebugInfo() { return includeDebugInfo; } public void setIncludeDebugInfo(boolean includeDebugInfo) { this.includeDebugInfo = includeDebugInfo; } public boolean isUseEclipseCompiler() { return useEclipseCompiler; } public void setUseEclipseCompiler(boolean useEclipseCompiler) { this.useEclipseCompiler = useEclipseCompiler; } public int getJavaVersion() { return javaVersion; } public void setJavaVersion(int javaVersion) { this.javaVersion = javaVersion; } public List getArguments() { return Collections.unmodifiableList(arguments); } public void addArgument(String argName) { if (arguments.isEmpty()) { arguments = new ArrayList<>(); } arguments.add(argName); } public void addArgument(String argName, String argValue) { if (arguments.isEmpty()) { arguments = new ArrayList<>(); } arguments.add(argName); arguments.add(argValue); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/compiler/DynamicClassLoader.java ================================================ package jadx.tests.api.compiler; import java.security.SecureClassLoader; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.Nullable; public class DynamicClassLoader extends SecureClassLoader { private final Map clsMap = new ConcurrentHashMap<>(); private final Map> clsCache = new ConcurrentHashMap<>(); public void add(String className, JavaClassObject clsObject) { this.clsMap.put(className, clsObject); } @Override public Class findClass(String name) throws ClassNotFoundException { Class cls = replaceClass(name); if (cls != null) { return cls; } return super.findClass(name); } public Class loadClass(String name) throws ClassNotFoundException { Class cls = replaceClass(name); if (cls != null) { return cls; } return super.loadClass(name); } @Nullable public Class replaceClass(String name) { Class cacheCls = clsCache.get(name); if (cacheCls != null) { return cacheCls; } JavaClassObject clsObject = clsMap.get(name); if (clsObject == null) { return null; } byte[] clsBytes = clsObject.getBytes(); Class cls = super.defineClass(name, clsBytes, 0, clsBytes.length); clsCache.put(name, cls); return cls; } public Collection getClassObjects() { return clsMap.values(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/compiler/EclipseCompilerUtils.java ================================================ package jadx.tests.api.compiler; import javax.tools.JavaCompiler; public class EclipseCompilerUtils { public static JavaCompiler newInstance() { if (!JavaUtils.checkJavaVersion(11)) { throw new IllegalArgumentException("Eclipse compiler build with Java 11"); } try { Class ecjCls = Class.forName("org.eclipse.jdt.internal.compiler.tool.EclipseCompiler"); return (JavaCompiler) ecjCls.getConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException("Failed to init Eclipse compiler", e); } } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/compiler/JavaClassObject.java ================================================ package jadx.tests.api.compiler; import java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.net.URI; import javax.tools.SimpleJavaFileObject; public class JavaClassObject extends SimpleJavaFileObject { private final String name; private final ByteArrayOutputStream bos = new ByteArrayOutputStream(); public JavaClassObject(String name, Kind kind) { super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind); this.name = name; } @Override public String getName() { return name; } public byte[] getBytes() { return bos.toByteArray(); } @Override public OutputStream openOutputStream() { return bos; } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/compiler/JavaUtils.java ================================================ package jadx.tests.api.compiler; import org.apache.commons.lang3.SystemUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class JavaUtils { private static final Logger LOG = LoggerFactory.getLogger(JavaUtils.class); public static final int JAVA_VERSION_INT = getJavaVersionInt(); public static boolean checkJavaVersion(int requiredVersion) { return JAVA_VERSION_INT >= requiredVersion; } private static int getJavaVersionInt() { String javaSpecVerStr = SystemUtils.JAVA_SPECIFICATION_VERSION; if (javaSpecVerStr == null) { LOG.warn("Unknown current java specification version, use 8 as fallback"); return 8; // fallback version } if (javaSpecVerStr.startsWith("1.")) { return Integer.parseInt(javaSpecVerStr.substring(2)); } return Integer.parseInt(javaSpecVerStr); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/compiler/StringJavaFileObject.java ================================================ package jadx.tests.api.compiler; import java.net.URI; import javax.tools.SimpleJavaFileObject; public class StringJavaFileObject extends SimpleJavaFileObject { private final String content; public StringJavaFileObject(String className, String content) { super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); this.content = content; } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) { return content; } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/compiler/TestCompiler.java ================================================ package jadx.tests.api.compiler; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.tools.DiagnosticListener; import javax.tools.JavaCompiler; import javax.tools.JavaCompiler.CompilationTask; import javax.tools.JavaFileObject; import javax.tools.ToolProvider; import org.jetbrains.annotations.NotNull; import jadx.api.impl.SimpleCodeWriter; import jadx.core.dex.nodes.ClassNode; import jadx.core.utils.files.FileUtils; import jadx.tests.api.IntegrationTest; import static org.assertj.core.api.Assertions.assertThat; public class TestCompiler implements Closeable { private final CompilerOptions options; private final JavaCompiler compiler; private final ClassFileManager fileManager; public TestCompiler(CompilerOptions options) { this.options = options; int javaVersion = options.getJavaVersion(); if (!JavaUtils.checkJavaVersion(javaVersion)) { throw new IllegalArgumentException("Current java version not meet requirement: " + "current: " + JavaUtils.JAVA_VERSION_INT + ", required: " + javaVersion); } if (options.isUseEclipseCompiler()) { compiler = EclipseCompilerUtils.newInstance(); } else { compiler = ToolProvider.getSystemJavaCompiler(); if (compiler == null) { throw new IllegalStateException("Can not find compiler, please use JDK instead"); } } fileManager = new ClassFileManager(compiler.getStandardFileManager(null, null, null)); } public List compileFiles(List sourceFiles, Path outTmp) throws IOException { compile(fileManager.getJavaFileObjectsFromFiles(sourceFiles)); List files = new ArrayList<>(); for (JavaClassObject classObject : fileManager.getClassLoader().getClassObjects()) { Path path = outTmp.resolve(classObject.getName().replace('.', '/') + ".class"); FileUtils.makeDirsForFile(path); Files.write(path, classObject.getBytes()); files.add(path.toFile()); } return files; } public void compileNodes(List clsNodeList) { List jfObjects = new ArrayList<>(clsNodeList.size()); for (ClassNode clsNode : clsNodeList) { jfObjects.add(new StringJavaFileObject(clsNode.getFullName(), clsNode.getCode().getCodeStr())); } compile(jfObjects); } private void compile(List jfObjects) { List arguments = new ArrayList<>(); arguments.add(options.isIncludeDebugInfo() ? "-g" : "-g:none"); int javaVersion = options.getJavaVersion(); String javaVerStr = javaVersion <= 8 ? "1." + javaVersion : Integer.toString(javaVersion); arguments.add("-source"); arguments.add(javaVerStr); arguments.add("-target"); arguments.add(javaVerStr); arguments.addAll(options.getArguments()); SimpleCodeWriter output = new SimpleCodeWriter(); DiagnosticListener diagnostic = diagObj -> { String msg = "Compiler diagnostic: " + diagObj; output.startLine(msg); System.out.println(msg); }; Writer out = new PrintWriter(System.out); CompilationTask compilerTask = compiler.getTask(out, fileManager, diagnostic, arguments, null, jfObjects); if (Boolean.FALSE.equals(compilerTask.call())) { throw new RuntimeException("Compilation failed: " + output); } } private ClassLoader getClassLoader() { return fileManager.getClassLoader(); } public Class getClass(String clsFullName) throws ClassNotFoundException { return getClassLoader().loadClass(clsFullName); } @NotNull public Method getMethod(Class cls, String methodName, Class[] types) throws NoSuchMethodException { return cls.getMethod(methodName, types); } public Object invoke(String clsFullName, String methodName, Class[] types, Object[] args) { try { for (Class type : types) { checkType(type); } Class cls = getClass(clsFullName); Method mth = getMethod(cls, methodName, types); Object inst = cls.getConstructor().newInstance(); assertThat(mth).as("Failed to get method " + methodName + '(' + Arrays.toString(types) + ')').isNotNull(); return mth.invoke(inst, args); } catch (Throwable e) { IntegrationTest.rethrow("Invoke error for method: " + methodName, e); return null; } } private Class checkType(Class type) throws ClassNotFoundException { if (type.isPrimitive()) { return type; } if (type.isArray()) { return checkType(type.getComponentType()); } Class cls = getClassLoader().loadClass(type.getName()); if (type != cls) { throw new IllegalArgumentException("Internal test class cannot be used in method invoke"); } return cls; } @Override public void close() throws IOException { fileManager.close(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/extensions/profiles/JadxTestProfilesExtension.java ================================================ package jadx.tests.api.extensions.profiles; import java.lang.reflect.Method; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; import org.junit.platform.commons.util.AnnotationUtils; import org.junit.platform.commons.util.Preconditions; import jadx.tests.api.IntegrationTest; import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated; public class JadxTestProfilesExtension implements TestTemplateInvocationContextProvider { @Override public boolean supportsTestTemplate(ExtensionContext context) { return isAnnotated(context.getTestMethod(), TestWithProfiles.class); } @Override public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { Preconditions.condition(IntegrationTest.class.isAssignableFrom(context.getRequiredTestClass()), "@TestWithProfiles should be used only in IntegrationTest subclasses"); Method testMethod = context.getRequiredTestMethod(); boolean testAnnAdded = AnnotationUtils.findAnnotation(testMethod, Test.class).isPresent(); Preconditions.condition(!testAnnAdded, "@Test annotation should be removed"); TestWithProfiles profilesAnn = AnnotationUtils.findAnnotation(testMethod, TestWithProfiles.class).get(); EnumSet profilesSet = EnumSet.noneOf(TestProfile.class); Collections.addAll(profilesSet, profilesAnn.value()); if (profilesSet.contains(TestProfile.ALL)) { Collections.addAll(profilesSet, TestProfile.values()); } profilesSet.remove(TestProfile.ALL); return profilesSet.stream() .sorted() .map(RunWithProfile::new); } private static class RunWithProfile implements TestTemplateInvocationContext { private final TestProfile testProfile; public RunWithProfile(TestProfile testProfile) { this.testProfile = testProfile; } @Override public String getDisplayName(int invocationIndex) { return testProfile.getDescription(); } @Override public List getAdditionalExtensions() { return Collections.singletonList(beforeTest()); } private BeforeTestExecutionCallback beforeTest() { return execContext -> testProfile.accept((IntegrationTest) execContext.getRequiredTestInstance()); } } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/extensions/profiles/TestProfile.java ================================================ package jadx.tests.api.extensions.profiles; import java.util.function.Consumer; import jadx.tests.api.IntegrationTest; public enum TestProfile implements Consumer { DX_J8("dx-j8", test -> { test.useTargetJavaVersion(8); test.useDexInput("dx"); }), D8_J8("d8-j8", test -> { test.useTargetJavaVersion(8); test.useDexInput("d8"); }), D8_J11("d8-j11", test -> { test.useTargetJavaVersion(11); test.useDexInput("d8"); }), D8_J11_DESUGAR("d8-j11-desugar", test -> { test.useTargetJavaVersion(11); test.useDexInput("d8"); test.keepParentClassOnInput(); test.getArgs().getPluginOptions().put("java-convert.d8-desugar", "yes"); }), JAVA8("java-8", test -> { test.useTargetJavaVersion(8); test.useJavaInput(); }), JAVA11("java-11", test -> { test.useTargetJavaVersion(11); test.useJavaInput(); }), JAVA17("java-17", test -> { test.useTargetJavaVersion(17); test.useJavaInput(); }), ECJ_DX_J8("ecj-dx-j8", test -> { test.useEclipseCompiler(); test.useTargetJavaVersion(8); test.useDexInput(); }), ECJ_J8("ecj-j8", test -> { test.useEclipseCompiler(); test.useTargetJavaVersion(8); test.useJavaInput(); }), ALL("all", null); private final String description; private final Consumer setup; TestProfile(String description, Consumer setup) { this.description = description; this.setup = setup; } @Override public void accept(IntegrationTest test) { this.setup.accept(test); test.setOutDirSuffix(description); } public String getDescription() { return description; } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/extensions/profiles/TestWithProfiles.java ================================================ package jadx.tests.api.extensions.profiles; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @TestTemplate @ExtendWith(JadxTestProfilesExtension.class) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface TestWithProfiles { TestProfile[] value(); } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/utils/TestFilesGetter.java ================================================ package jadx.tests.api.utils; import java.nio.file.Path; import jadx.core.plugins.files.IJadxFilesGetter; import jadx.core.utils.files.FileUtils; public class TestFilesGetter implements IJadxFilesGetter { private final Path testDir; public TestFilesGetter(Path testDir) { this.testDir = testDir; } @Override public Path getConfigDir() { return makeSubDir("config"); } @Override public Path getCacheDir() { return makeSubDir("cache"); } @Override public Path getTempDir() { return testDir; } private Path makeSubDir(String config) { Path dir = testDir.resolve(config); FileUtils.makeDirs(dir); return dir; } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/utils/TestUtils.java ================================================ package jadx.tests.api.utils; import java.io.File; import org.junit.jupiter.api.extension.ExtendWith; import jadx.NotYetImplementedExtension; import jadx.api.CommentsLevel; import jadx.api.JadxArgs; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.attributes.nodes.JadxCommentsAttr; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.fail; @ExtendWith(NotYetImplementedExtension.class) public class TestUtils { public static String indent() { return JadxArgs.DEFAULT_INDENT_STR; } public static String indent(int indent) { if (indent == 1) { return JadxArgs.DEFAULT_INDENT_STR; } return Utils.strRepeat(JadxArgs.DEFAULT_INDENT_STR, indent); } public static int count(String string, String substring) { if (substring == null || substring.isEmpty()) { throw new IllegalArgumentException("Substring can't be null or empty"); } int count = 0; int idx = 0; while ((idx = string.indexOf(substring, idx)) != -1) { idx++; count++; } return count; } protected static void checkCode(ClassNode cls, boolean allowWarnInCode) { assertThat(hasErrors(cls, allowWarnInCode)).as("Inconsistent cls: " + cls).isFalse(); for (MethodNode mthNode : cls.getMethods()) { if (hasErrors(mthNode, allowWarnInCode)) { fail("Method with problems: " + mthNode + "\n " + Utils.listToString(mthNode.getAttributesStringsList(), "\n ")); } } if (!cls.contains(AFlag.DONT_GENERATE)) { assertThat(cls) .code() .doesNotContain("inconsistent") .doesNotContain("JADX ERROR"); } } protected static boolean hasErrors(IAttributeNode node, boolean allowWarnInCode) { if (node.contains(AFlag.INCONSISTENT_CODE) || node.contains(AType.JADX_ERROR)) { return true; } if (!allowWarnInCode) { JadxCommentsAttr commentsAttr = node.get(AType.JADX_COMMENTS); if (commentsAttr != null) { return commentsAttr.getComments().get(CommentsLevel.WARN) != null; } } return false; } public static File getFileForSample(String resPath) { try { return new File(ClassLoader.getSystemResource(resPath).toURI().getRawPath()); } catch (Exception e) { throw new JadxRuntimeException("Resource load failed: " + resPath, e); } } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxAssertions.java ================================================ package jadx.tests.api.utils.assertj; import org.assertj.core.api.Assertions; import jadx.api.ICodeInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; public class JadxAssertions extends Assertions { public static JadxClassNodeAssertions assertThat(ClassNode cls) { Assertions.assertThat(cls).isNotNull(); return new JadxClassNodeAssertions(cls); } public static JadxMethodNodeAssertions assertThat(MethodNode mth) { Assertions.assertThat(mth).isNotNull(); return new JadxMethodNodeAssertions(mth); } public static JadxCodeInfoAssertions assertThat(ICodeInfo codeInfo) { Assertions.assertThat(codeInfo).isNotNull(); return new JadxCodeInfoAssertions(codeInfo); } public static JadxCodeAssertions assertThat(String code) { return new JadxCodeAssertions(code); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxClassNodeAssertions.java ================================================ package jadx.tests.api.utils.assertj; import java.util.Map; import org.assertj.core.api.AbstractObjectAssert; import jadx.api.ICodeInfo; import jadx.api.metadata.ICodeAnnotation; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; public class JadxClassNodeAssertions extends AbstractObjectAssert { public JadxClassNodeAssertions(ClassNode cls) { super(cls, JadxClassNodeAssertions.class); } public JadxCodeInfoAssertions decompile() { isNotNull(); ICodeInfo codeInfo = actual.getCode(); assertThat(codeInfo).isNotNull(); return new JadxCodeInfoAssertions(codeInfo); } public JadxCodeAssertions code() { isNotNull(); ICodeInfo code = actual.getCode(); assertThat(code).isNotNull(); String codeStr = code.getCodeStr(); assertThat(codeStr).isNotBlank(); return new JadxCodeAssertions(codeStr); } public JadxCodeAssertions disasmCode() { isNotNull(); String disasmCode = actual.getDisassembledCode(); assertThat(disasmCode).isNotNull().isNotBlank(); return new JadxCodeAssertions(disasmCode); } public JadxCodeAssertions reloadCode(IntegrationTest testInstance) { isNotNull(); ICodeInfo code = actual.reloadCode(); assertThat(code).isNotNull(); String codeStr = code.getCodeStr(); assertThat(codeStr).isNotBlank(); JadxCodeAssertions codeAssertions = new JadxCodeAssertions(codeStr); codeAssertions.print(); testInstance.runChecks(actual); return codeAssertions; } /** * Force running auto check on decompiled code. * Useful for smali tests. */ public JadxClassNodeAssertions runDecompiledAutoCheck(IntegrationTest testInstance) { isNotNull(); testInstance.runDecompiledAutoCheck(actual); return this; } public JadxClassNodeAssertions checkCodeAnnotationFor(String refStr, ICodeAnnotation node) { checkCodeAnnotationFor(refStr, 0, node); return this; } public JadxClassNodeAssertions checkCodeAnnotationFor(String refStr, int refOffset, ICodeAnnotation node) { ICodeInfo code = actual.getCode(); int codePos = code.getCodeStr().indexOf(refStr); assertThat(codePos).describedAs("String '%s' not found", refStr).isNotEqualTo(-1); int refPos = codePos + refOffset; for (Map.Entry entry : code.getCodeMetadata().getAsMap().entrySet()) { if (entry.getKey() == refPos) { assertThat(entry.getValue()).isEqualTo(node); return this; } } fail("Annotation for reference string: '%s' at position %d not found", refStr, refPos); return this; } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeAssertions.java ================================================ package jadx.tests.api.utils.assertj; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.assertj.core.api.AbstractStringAssert; import org.assertj.core.internal.Failures; import jadx.tests.api.utils.TestUtils; import static org.assertj.core.error.ShouldNotContainSubsequence.shouldNotContainSubsequence; public class JadxCodeAssertions extends AbstractStringAssert { private Failures failures = Failures.instance(); public JadxCodeAssertions(String code) { super(code, JadxCodeAssertions.class); } public JadxCodeAssertions containsOne(String substring) { return countString(1, substring); } public JadxCodeAssertions countString(int count, String substring) { isNotNull(); int actualCount = TestUtils.count(actual, substring); if (actualCount != count) { failWithMessage("Expected a substring <%s> count <%d> but was <%d>", substring, count, actualCount); } return this; } public JadxCodeAssertions notContainsLine(int indent, String line) { return countLine(0, indent, line); } public JadxCodeAssertions containsLine(int indent, String line) { return countLine(1, indent, line); } private JadxCodeAssertions countLine(int count, int indent, String line) { String indentStr = TestUtils.indent(indent); return countString(count, indentStr + line); } public JadxCodeAssertions containsLines(String... lines) { return containsLines(0, lines); } public JadxCodeAssertions containsLines(int commonIndent, String... lines) { if (lines.length == 1) { return containsLine(commonIndent, lines[0]); } String indent = TestUtils.indent(commonIndent); StringBuilder sb = new StringBuilder(); for (String line : lines) { sb.append('\n'); if (line.isEmpty()) { // don't add common indent to empty lines continue; } String searchLine = indent + line; sb.append(searchLine); // check every line for easier debugging contains(searchLine); } return containsOnlyOnce(sb.substring(1)); } public JadxCodeAssertions doesNotContainSubsequence(CharSequence... values) { final var regex = Arrays.stream(values) .map(value -> Pattern.quote(value.toString())) .collect(Collectors.joining(".*")); final var pattern = Pattern.compile(regex, Pattern.DOTALL); final var matcher = pattern.matcher(actual); if (matcher.find()) { throw failures.failure(info, shouldNotContainSubsequence(actual, values, matcher.start())); } return this; } public JadxCodeAssertions removeBlockComments() { String code = actual.replaceAll("/\\*.*\\*/", ""); JadxCodeAssertions newCode = new JadxCodeAssertions(code); newCode.print(); return newCode; } public JadxCodeAssertions removeLineComments() { String code = actual.replaceAll("//.*(?!$)", ""); JadxCodeAssertions newCode = new JadxCodeAssertions(code); newCode.print(); return newCode; } public JadxCodeAssertions print() { System.out.println("-----------------------------------------------------------"); System.out.println(actual); System.out.println("-----------------------------------------------------------"); return this; } public JadxCodeAssertions containsOneOf(String... substringArr) { int matches = 0; for (String substring : substringArr) { matches += TestUtils.count(actual, substring); } if (matches != 1) { failWithMessage("Expected only one match from <%s> but was <%d>", Arrays.toString(substringArr), matches); } return this; } @SuppressWarnings("UnusedReturnValue") @SafeVarargs public final JadxCodeAssertions oneOf(Function... checks) { int passed = 0; List failed = new ArrayList<>(); for (Function check : checks) { try { check.apply(this); passed++; } catch (Throwable e) { failed.add(e); } } if (passed != 1) { failWithMessage("Expected only one match but passed: <%d>, failed: <%d>, details:\n<%s>", passed, failed.size(), failed.stream().map(Throwable::getMessage).collect(Collectors.joining("\nFailed check:\n "))); } return this; } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxCodeInfoAssertions.java ================================================ package jadx.tests.api.utils.assertj; import java.util.stream.Collectors; import org.assertj.core.api.AbstractObjectAssert; import jadx.api.ICodeInfo; import jadx.api.metadata.annotations.InsnCodeOffset; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class JadxCodeInfoAssertions extends AbstractObjectAssert { public JadxCodeInfoAssertions(ICodeInfo cls) { super(cls, JadxCodeInfoAssertions.class); } public JadxCodeAssertions code() { isNotNull(); String codeStr = actual.getCodeStr(); assertThat(codeStr).isNotBlank(); return new JadxCodeAssertions(codeStr); } public JadxCodeInfoAssertions checkCodeOffsets() { long dupOffsetCount = actual.getCodeMetadata().getAsMap().values().stream() .filter(InsnCodeOffset.class::isInstance) .collect(Collectors.groupingBy(o -> ((InsnCodeOffset) o).getOffset(), Collectors.toList())) .values().stream() .filter(list -> list.size() > 1) .count(); assertThat(dupOffsetCount) .describedAs("Found duplicated code offsets") .isEqualTo(0); return this; } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxMethodNodeAssertions.java ================================================ package jadx.tests.api.utils.assertj; import org.assertj.core.api.AbstractObjectAssert; import jadx.core.dex.nodes.MethodNode; import static org.assertj.core.api.Assertions.assertThat; public class JadxMethodNodeAssertions extends AbstractObjectAssert { public JadxMethodNodeAssertions(MethodNode mth) { super(mth, JadxMethodNodeAssertions.class); } public JadxCodeAssertions code() { isNotNull(); String codeStr = actual.getCodeStr(); assertThat(codeStr).isNotBlank(); return new JadxCodeAssertions(codeStr); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/export/IllegalCharsForGradleWrapper.java ================================================ package jadx.tests.export; import org.junit.jupiter.api.Test; import jadx.tests.api.ExportGradleTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; class IllegalCharsForGradleWrapper extends ExportGradleTest { @Test void test() { exportGradle("IllegalCharsForGradleWrapper.xml", "strings.xml"); assertThat(getSettingsGradle()).contains("'JadxTestApp'"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/export/OptionalTargetSdkVersion.java ================================================ package jadx.tests.export; import org.junit.jupiter.api.Test; import jadx.tests.api.ExportGradleTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class OptionalTargetSdkVersion extends ExportGradleTest { @Test void test() { exportGradle("OptionalTargetSdkVersion.xml", "strings.xml"); assertThat(getAppGradleBuild()).contains("targetSdkVersion 14").doesNotContain(" vectorDrawables.useSupportLibrary = true"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/export/TestApacheHttpClient.java ================================================ package jadx.tests.export; import org.junit.jupiter.api.Test; import jadx.core.export.GradleInfoStorage; import jadx.tests.api.ExportGradleTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestApacheHttpClient extends ExportGradleTest { @Test void test() { GradleInfoStorage gradleInfo = getRootNode().getGradleInfoStorage(); gradleInfo.setUseApacheHttpLegacy(true); exportGradle("OptionalTargetSdkVersion.xml", "strings.xml"); assertThat(getAppGradleBuild()).contains(" useLibrary 'org.apache.http.legacy'"); gradleInfo.setUseApacheHttpLegacy(false); exportGradle("OptionalTargetSdkVersion.xml", "strings.xml"); assertThat(getAppGradleBuild()).doesNotContain(" useLibrary 'org.apache.http.legacy'"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/export/TestNonFinalResIds.java ================================================ package jadx.tests.export; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import jadx.core.export.GradleInfoStorage; import jadx.tests.api.ExportGradleTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNonFinalResIds extends ExportGradleTest { @Test void test() { GradleInfoStorage gradleInfo = getRootNode().getGradleInfoStorage(); gradleInfo.setNonFinalResIds(false); exportGradle("OptionalTargetSdkVersion.xml", "strings.xml"); Assertions.assertFalse(getGradleProperiesFile().exists()); gradleInfo.setNonFinalResIds(true); exportGradle("OptionalTargetSdkVersion.xml", "strings.xml"); assertThat(getGradleProperties()).containsOne("android.nonFinalResIds=false"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/export/VectorDrawablesUseSupportLibrary.java ================================================ package jadx.tests.export; import org.junit.jupiter.api.Test; import jadx.core.export.GradleInfoStorage; import jadx.tests.api.ExportGradleTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class VectorDrawablesUseSupportLibrary extends ExportGradleTest { @Test void test() { GradleInfoStorage gradleInfo = getRootNode().getGradleInfoStorage(); gradleInfo.setVectorFillType(true); exportGradle("OptionalTargetSdkVersion.xml", "strings.xml"); assertThat(getAppGradleBuild()).contains(" vectorDrawables.useSupportLibrary = true"); gradleInfo.setVectorFillType(false); gradleInfo.setVectorPathData(true); exportGradle("OptionalTargetSdkVersion.xml", "strings.xml"); assertThat(getAppGradleBuild()).contains(" vectorDrawables.useSupportLibrary = true"); exportGradle("MinSdkVersion25.xml", "strings.xml"); assertThat(getAppGradleBuild()).doesNotContain(" vectorDrawables.useSupportLibrary = true"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java ================================================ package jadx.tests.external; import java.io.File; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.CommentsLevel; import jadx.api.ICodeInfo; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.JadxInternalAccess; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.tests.api.utils.TestUtils; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public abstract class BaseExternalTest extends TestUtils { private static final Logger LOG = LoggerFactory.getLogger(BaseExternalTest.class); protected JadxDecompiler decompiler; protected abstract String getSamplesDir(); protected JadxArgs prepare(String inputFile) { return prepare(new File(getSamplesDir(), inputFile)); } protected JadxArgs prepare(File input) { JadxArgs args = new JadxArgs(); args.getInputFiles().add(input); args.setOutDir(new File("../jadx-external-tests-tmp")); args.setSkipFilesSave(true); args.setSkipResources(true); args.setShowInconsistentCode(true); args.setCommentsLevel(CommentsLevel.DEBUG); return args; } protected JadxDecompiler decompile(JadxArgs jadxArgs) { return decompile(jadxArgs, null, null); } protected JadxDecompiler decompile(JadxArgs jadxArgs, String clsPatternStr) { return decompile(jadxArgs, clsPatternStr, null); } protected JadxDecompiler decompile(JadxArgs jadxArgs, @Nullable String clsPatternStr, @Nullable String mthPatternStr) { decompiler = new JadxDecompiler(jadxArgs); decompiler.load(); if (clsPatternStr == null) { decompiler.save(); } else { processByPatterns(decompiler, clsPatternStr, mthPatternStr); } printErrorReport(decompiler); return decompiler; } private void processByPatterns(JadxDecompiler jadx, String clsPattern, @Nullable String mthPattern) { RootNode root = JadxInternalAccess.getRoot(jadx); int processed = 0; for (ClassNode classNode : root.getClasses(true)) { String clsFullName = classNode.getClassInfo().getFullName(); if (clsFullName.equals(clsPattern)) { if (processCls(mthPattern, classNode)) { processed++; } } } assertThat(processed).as("No classes processed").isGreaterThan(0); } private boolean processCls(@Nullable String mthPattern, ClassNode classNode) { classNode.load(); boolean decompile = false; if (mthPattern == null) { decompile = true; } else { for (MethodNode mth : classNode.getMethods()) { if (isMthMatch(mth, mthPattern)) { decompile = true; break; } } } if (!decompile) { return false; } try { classNode.decompile(); } catch (Exception e) { throw new JadxRuntimeException("Class process failed", e); } LOG.info("----------------------------------------------------------------"); LOG.info("Print class: {} from: {}", classNode.getFullName(), classNode.getInputFileName()); if (mthPattern != null) { printMethods(classNode, mthPattern); } else { LOG.info("Code: \n{}", classNode.getCode()); } checkCode(classNode, false); return true; } private boolean isMthMatch(MethodNode mth, String mthPattern) { String shortId = mth.getMethodInfo().getShortId(); return isMatch(shortId, mthPattern); } private boolean isMatch(String str, String pattern) { if (str.equals(pattern)) { return true; } return str.startsWith(pattern); } private void printMethods(ClassNode classNode, @NotNull String mthPattern) { ICodeInfo codeInfo = classNode.getCode(); String code = codeInfo.getCodeStr(); if (code == null) { return; } String dashLine = "======================================================================================"; for (MethodNode mth : classNode.getMethods()) { if (isMthMatch(mth, mthPattern)) { LOG.info("Print method: {}\n{}\n{}\n{}", mth.getMethodInfo().getRawFullId(), dashLine, mth.getCodeStr(), dashLine); } } } private void printErrorReport(JadxDecompiler jadx) { jadx.printErrorsReport(); assertThat(jadx.getErrorsCount()).isEqualTo(0); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/functional/AttributeStorageTest.java ================================================ package jadx.tests.functional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttributeStorage; import static jadx.core.dex.attributes.AFlag.SYNTHETIC; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class AttributeStorageTest { private AttributeStorage storage; @BeforeEach public void setup() { storage = new AttributeStorage(); } @Test public void testAdd() { storage.add(SYNTHETIC); assertThat(storage.contains(SYNTHETIC)).isTrue(); } @Test public void testRemove() { storage.add(SYNTHETIC); storage.remove(SYNTHETIC); assertThat(storage.contains(SYNTHETIC)).isFalse(); } public static final AType TEST = new AType<>(); public static class TestAttr implements IJadxAttribute { @Override public AType getAttrType() { return TEST; } } @Test public void testAddAttribute() { TestAttr attr = new TestAttr(); storage.add(attr); assertThat(storage.contains(TEST)).isTrue(); assertThat(storage.get(TEST)).isEqualTo(attr); } @Test public void testRemoveAttribute() { TestAttr attr = new TestAttr(); storage.add(attr); storage.remove(attr); assertThat(storage.contains(TEST)).isFalse(); assertThat(storage.get(TEST)).isNull(); } @Test public void testRemoveOtherAttribute() { TestAttr attr = new TestAttr(); storage.add(attr); storage.remove(new TestAttr()); assertThat(storage.contains(TEST)).isTrue(); assertThat(storage.get(TEST)).isEqualTo(attr); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/functional/JadxClasspathTest.java ================================================ package jadx.tests.functional; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import jadx.api.JadxArgs; import jadx.core.clsp.ClspGraph; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.RootNode; import static jadx.core.dex.instructions.args.ArgType.OBJECT; import static jadx.core.dex.instructions.args.ArgType.STRING; import static jadx.core.dex.instructions.args.ArgType.isCastNeeded; import static jadx.core.dex.instructions.args.ArgType.object; import static org.assertj.core.api.Assertions.assertThat; public class JadxClasspathTest { private static final String JAVA_LANG_EXCEPTION = "java.lang.Exception"; private static final String JAVA_LANG_THROWABLE = "java.lang.Throwable"; private RootNode root; private ClspGraph clsp; @BeforeEach public void initClsp() { this.root = new RootNode(new JadxArgs()); this.root.loadClasses(Collections.emptyList()); this.root.initClassPath(); this.clsp = root.getClsp(); } @Test public void test() { ArgType objExc = object(JAVA_LANG_EXCEPTION); ArgType objThr = object(JAVA_LANG_THROWABLE); assertThat(clsp.isImplements(JAVA_LANG_EXCEPTION, JAVA_LANG_THROWABLE)).isTrue(); assertThat(clsp.isImplements(JAVA_LANG_THROWABLE, JAVA_LANG_EXCEPTION)).isFalse(); assertThat(isCastNeeded(root, objExc, objThr)).isFalse(); assertThat(isCastNeeded(root, objThr, objExc)).isTrue(); assertThat(isCastNeeded(root, OBJECT, STRING)).isTrue(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/functional/JadxVisitorsOrderTest.java ================================================ package jadx.tests.functional; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.core.Jadx; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.JadxVisitor; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class JadxVisitorsOrderTest { private static final Logger LOG = LoggerFactory.getLogger(JadxVisitorsOrderTest.class); @Test public void testOrder() { checkPassList(Jadx.getPassesList(new JadxArgs())); checkPassList(Jadx.getPreDecompilePassesList()); checkPassList(Jadx.getFallbackPassesList()); } private void checkPassList(List passes) { List errors = check(passes); for (String str : errors) { LOG.error(str); } assertThat(errors).isEmpty(); } private static List check(List passes) { List> classList = new ArrayList<>(passes.size()); for (IDexTreeVisitor pass : passes) { classList.add(pass.getClass()); } List errors = new ArrayList<>(); Set names = new HashSet<>(); Set> passClsSet = new HashSet<>(); for (int i = 0; i < passes.size(); i++) { IDexTreeVisitor pass = passes.get(i); Class passClass = pass.getClass(); JadxVisitor info = passClass.getAnnotation(JadxVisitor.class); if (info == null) { LOG.warn("No JadxVisitor annotation for visitor: {}", passClass.getName()); continue; } boolean firstOccurrence = passClsSet.add(passClass); String passName = passClass.getSimpleName(); if (firstOccurrence && !names.add(passName)) { errors.add("Visitor name conflict: " + passName + ", class: " + passClass.getName()); } for (Class cls : info.runBefore()) { int beforeIndex = classList.indexOf(cls); if (beforeIndex != -1 && beforeIndex < i) { errors.add("Pass " + passName + " must be before " + cls.getSimpleName()); } } for (Class cls : info.runAfter()) { if (classList.indexOf(cls) > i) { errors.add("Pass " + passName + " must be after " + cls.getSimpleName()); } } } return errors; } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/functional/NameMapperTest.java ================================================ package jadx.tests.functional; import org.junit.jupiter.api.Test; import jadx.core.deobf.NameMapper; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class NameMapperTest { @Test public void testValidFullIdentifiers() { String[] validNames = { "C", "Cc", "b.C", "b.Cc", "aAa.b.Cc", "a.b.Cc", "a.b.C_c", "a.b.C$c", "a.b.C9" }; for (String validName : validNames) { assertThat(NameMapper.isValidFullIdentifier(validName)).isTrue(); } } @Test public void testInvalidFullIdentifiers() { String[] invalidNames = { "", "5", "7A", ".C", "b.9C", "b..C", }; for (String invalidName : invalidNames) { assertThat(NameMapper.isValidFullIdentifier(invalidName)).isFalse(); } } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/functional/SignatureParserTest.java ================================================ package jadx.tests.functional; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.ArgType.WildcardBound; import jadx.core.dex.nodes.parser.SignatureParser; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.dex.instructions.args.ArgType.INT; import static jadx.core.dex.instructions.args.ArgType.OBJECT; import static jadx.core.dex.instructions.args.ArgType.array; import static jadx.core.dex.instructions.args.ArgType.generic; import static jadx.core.dex.instructions.args.ArgType.genericType; import static jadx.core.dex.instructions.args.ArgType.object; import static jadx.core.dex.instructions.args.ArgType.outerGeneric; import static jadx.core.dex.instructions.args.ArgType.wildcard; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class SignatureParserTest { @Test public void testSimpleTypes() { checkType("", null); checkType("I", INT); checkType("[I", array(INT)); checkType("Ljava/lang/Object;", OBJECT); checkType("[Ljava/lang/Object;", array(OBJECT)); checkType("[[I", array(array(INT))); } private static void checkType(String str, ArgType type) { assertThat(new SignatureParser(str).consumeType()).isEqualTo(type); } @Test public void testGenerics() { checkType("TD;", genericType("D")); checkType("La;", generic("La;", genericType("V"), object("b"))); checkType("La;>;", generic("La;", generic("Lb;", object("Lc;")))); checkType("La/b/C;>;", generic("La/b/C;", generic("Ld/E;", object("Lf/G;")))); checkType("La.c;", outerGeneric(generic("La;", genericType("D")), ArgType.object("c"))); checkType("La.c/d;", outerGeneric(generic("La;", genericType("D")), ArgType.object("c.d"))); checkType("La.c;", outerGeneric(generic("La;", object("Lb;")), ArgType.generic("c", genericType("V")))); } @Test public void testInnerGeneric() { String signature = "La.LinkedHashIterator;>;"; String objectStr = new SignatureParser(signature).consumeType().getObject(); assertThat(objectStr).isEqualTo("a$LinkedHashIterator"); } @Test public void testNestedInnerGeneric() { String signature = "La.I.X;"; ArgType result = new SignatureParser(signature).consumeType(); assertThat(result.getObject()).isEqualTo("a$I$X"); // nested 'outerGeneric' objects ArgType obj = generic("La;", genericType("V")); assertThat(result).isEqualTo(outerGeneric(outerGeneric(obj, object("I")), object("X"))); } @Test public void testNestedInnerGeneric2() { // full name in inner class String signature = "Lsome/long/pkg/ba.some/long/pkg/bb;"; ArgType result = new SignatureParser(signature).consumeType(); System.out.println(result); assertThat(result.getObject()).isEqualTo("some.long.pkg.ba$some.long.pkg.bb"); ArgType baseObj = generic("Lsome/long/pkg/ba;", object("Lsome/pkg/s;")); ArgType innerObj = generic("Lsome/long/pkg/bb;", object("Lsome/pkg/p;"), object("Lsome/pkg/n;")); ArgType obj = outerGeneric(baseObj, innerObj); assertThat(result).isEqualTo(obj); } @Test public void testWildcards() { checkWildcards("*", wildcard()); checkWildcards("+Lb;", wildcard(object("b"), WildcardBound.EXTENDS)); checkWildcards("-Lb;", wildcard(object("b"), WildcardBound.SUPER)); checkWildcards("+TV;", wildcard(genericType("V"), WildcardBound.EXTENDS)); checkWildcards("-TV;", wildcard(genericType("V"), WildcardBound.SUPER)); checkWildcards("**", wildcard(), wildcard()); checkWildcards("*Lb;", wildcard(), object("b")); checkWildcards("*TV;", wildcard(), genericType("V")); checkWildcards("TV;*", genericType("V"), wildcard()); checkWildcards("Lb;*", object("b"), wildcard()); checkWildcards("***", wildcard(), wildcard(), wildcard()); checkWildcards("*Lb;*", wildcard(), object("b"), wildcard()); } private static void checkWildcards(String w, ArgType... types) { ArgType parsedType = new SignatureParser("La<" + w + ">;").consumeType(); ArgType expectedType = generic("La;", types); assertThat(parsedType).isEqualTo(expectedType); } @Test public void testGenericMap() { checkGenerics(""); checkGenerics("", "T", emptyList()); checkGenerics("", "K", emptyList(), "LongType", emptyList()); checkGenerics("", "ResultT", singletonList(object("java.lang.Exception"))); } @SuppressWarnings("unchecked") private static void checkGenerics(String g, Object... objs) { List genericsList = new SignatureParser(g).consumeGenericTypeParameters(); List expectedList = new ArrayList<>(); for (int i = 0; i < objs.length; i += 2) { String typeVar = (String) objs[i]; List list = (List) objs[i + 1]; expectedList.add(ArgType.genericType(typeVar, list)); } assertThat(genericsList).isEqualTo(expectedList); } @Test public void testMethodArgs() { List argTypes = new SignatureParser("(Ljava/util/List<*>;)V").consumeMethodArgs(1); assertThat(argTypes).hasSize(1); assertThat(argTypes.get(0)).isEqualTo(generic("Ljava/util/List;", wildcard())); } @Test public void testMethodArgs2() { List argTypes = new SignatureParser("(La/b/C.d/E;)V").consumeMethodArgs(1); assertThat(argTypes).hasSize(1); ArgType argType = argTypes.get(0); assertThat(argType.getObject().indexOf('/')).isEqualTo(-1); assertThat(argType).isEqualTo(outerGeneric(generic("La/b/C;", genericType("T")), object("d.E"))); } @Test public void testBadGenericMap() { assertThatExceptionOfType(JadxRuntimeException.class) .isThrownBy(() -> new SignatureParser(" new SignatureParser("(TCONTENT)Lpkg/Cls;").consumeMethodArgs(1)); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/functional/StringUtilsTest.java ================================================ package jadx.tests.functional; import org.junit.jupiter.api.Test; import jadx.api.JadxArgs; import jadx.core.utils.StringUtils; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; class StringUtilsTest { private StringUtils stringUtils; @Test @SuppressWarnings("AvoidEscapedUnicodeCharacters") public void testStringUnescape() { JadxArgs args = new JadxArgs(); args.setEscapeUnicode(true); stringUtils = new StringUtils(args); checkStringUnescape("", ""); checkStringUnescape("'", "'"); checkStringUnescape("a", "a"); checkStringUnescape("\n", "\\n"); checkStringUnescape("\t", "\\t"); checkStringUnescape("\r", "\\r"); checkStringUnescape("\b", "\\b"); checkStringUnescape("\f", "\\f"); checkStringUnescape("\\", "\\\\"); checkStringUnescape("\"", "\\\""); checkStringUnescape("\u1234", "\\u1234"); } private void checkStringUnescape(String input, String result) { assertThat(stringUtils.unescapeString(input)).isEqualTo('"' + result + '"'); } @Test public void testCharUnescape() { stringUtils = new StringUtils(new JadxArgs()); checkCharUnescape('a', "a"); checkCharUnescape(' ', " "); checkCharUnescape('\n', "\\n"); checkCharUnescape('\'', "\\'"); assertThat(stringUtils.unescapeChar('\0')).isEqualTo("0"); } private void checkCharUnescape(char input, String result) { assertThat(stringUtils.unescapeChar(input)).isEqualTo('\'' + result + '\''); } @Test public void testResStrValueEscape() { checkResStrValueEscape("line\nnew line", "line\\nnew line"); checkResStrValueEscape("can't", "can\\'t"); checkResStrValueEscape("quote\"end", "quote\\\"end"); } private void checkResStrValueEscape(String input, String result) { assertThat(StringUtils.escapeResStrValue(input)).isEqualTo(result); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java ================================================ package jadx.tests.functional; import org.junit.jupiter.api.Test; import jadx.core.export.TemplateFile; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TemplateFileTest { @Test public void testBuildGradle() throws Exception { TemplateFile tmpl = TemplateFile.fromResources("/export/android/app.build.gradle.tmpl"); tmpl.add("applicationId", "SOME_ID"); tmpl.add("minSdkVersion", 1); tmpl.add("targetSdkVersion", 2); tmpl.add("versionCode", 3); tmpl.add("versionName", "1.2.3"); tmpl.add("additionalOptions", "useLibrary 'org.apache.http.legacy'"); tmpl.add("compileSdkVersion", 4); String res = tmpl.build(); System.out.println(res); assertThat(res).contains("applicationId 'SOME_ID'"); assertThat(res).contains("targetSdkVersion 2"); assertThat(res).contains("versionCode 3"); assertThat(res).contains("versionName \"1.2.3\""); assertThat(res).contains("compileSdkVersion 4"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/functional/TestIfCondition.java ================================================ package jadx.tests.functional; import org.junit.jupiter.api.Test; import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.IfOp; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.regions.conditions.Compare; import jadx.core.dex.regions.conditions.IfCondition; import static jadx.core.dex.regions.conditions.IfCondition.Mode; import static jadx.core.dex.regions.conditions.IfCondition.Mode.AND; import static jadx.core.dex.regions.conditions.IfCondition.Mode.COMPARE; import static jadx.core.dex.regions.conditions.IfCondition.Mode.NOT; import static jadx.core.dex.regions.conditions.IfCondition.Mode.OR; import static jadx.core.dex.regions.conditions.IfCondition.merge; import static jadx.core.dex.regions.conditions.IfCondition.not; import static jadx.core.dex.regions.conditions.IfCondition.simplify; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIfCondition { private static IfCondition makeCondition(IfOp op, InsnArg a, InsnArg b) { return IfCondition.fromIfNode(new IfNode(op, -1, a, b)); } private static IfCondition makeSimpleCondition() { return makeCondition(IfOp.EQ, mockArg(), LiteralArg.litTrue()); } private static IfCondition makeNegCondition() { return makeCondition(IfOp.NE, mockArg(), LiteralArg.litTrue()); } private static InsnArg mockArg() { return InsnArg.reg(0, ArgType.INT); } @Test public void testNormalize() { // 'a != false' => 'a == true' InsnArg a = mockArg(); IfCondition c = makeCondition(IfOp.NE, a, LiteralArg.litFalse()); IfCondition simp = simplify(c); assertThat(simp.getMode()).isEqualTo(COMPARE); Compare compare = simp.getCompare(); assertThat(compare.getA()).isEqualTo(a); assertThat(compare.getB()).isEqualTo(LiteralArg.litTrue()); } @Test public void testMerge() { IfCondition a = makeSimpleCondition(); IfCondition b = makeSimpleCondition(); IfCondition c = merge(Mode.OR, a, b); assertThat(c.getMode()).isEqualTo(OR); assertThat(c.first()).isEqualTo(a); assertThat(c.second()).isEqualTo(b); } @Test public void testSimplifyNot() { // !(!a) => a IfCondition a = not(not(makeSimpleCondition())); assertThat(simplify(a)).isEqualTo(a); } @Test public void testSimplifyNot2() { // !(!a) => a IfCondition a = not(makeNegCondition()); assertThat(simplify(a)).isEqualTo(a); } @Test public void testSimplify() { // '!(!a || !b)' => 'a && b' IfCondition a = makeSimpleCondition(); IfCondition b = makeSimpleCondition(); IfCondition c = not(merge(Mode.OR, not(a), not(b))); IfCondition simp = simplify(c); assertThat(simp.getMode()).isEqualTo(AND); assertThat(simp.first()).isEqualTo(a); assertThat(simp.second()).isEqualTo(b); } @Test public void testSimplify2() { // '(!a || !b) && !c' => '!((a && b) || c)' IfCondition a = makeSimpleCondition(); IfCondition b = makeSimpleCondition(); IfCondition c = makeSimpleCondition(); IfCondition cond = merge(Mode.AND, merge(Mode.OR, not(a), not(b)), not(c)); IfCondition simp = simplify(cond); assertThat(simp.getMode()).isEqualTo(NOT); IfCondition f = simp.first(); assertThat(f.getMode()).isEqualTo(OR); assertThat(f.first().getMode()).isEqualTo(AND); assertThat(f.first().first()).isEqualTo(a); assertThat(f.first().second()).isEqualTo(b); assertThat(f.second()).isEqualTo(c); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/android/TestRFieldAccess.java ================================================ package jadx.tests.integration.android; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("TypeName") public class TestRFieldAccess extends IntegrationTest { public static class R { public static final class id { public static final int BUTTON_01 = 2131230730; } } public static class TestR { public int test() { return R.id.BUTTON_01; } } @Test public void test() { assertThat(getClassNode(TestRFieldAccess.class)) .code() .countString(2, "return R.id.BUTTON_01;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/android/TestRFieldRestore.java ================================================ package jadx.tests.integration.android; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.tests.api.IntegrationTest; import static jadx.api.plugins.input.data.attributes.JadxAttrType.CONSTANT_VALUE; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestRFieldRestore extends IntegrationTest { public static class TestCls { public int test() { return 2131230730; } } @Test public void test() { // unknown R class disableCompilation(); Map map = new HashMap<>(); int buttonConstValue = 2131230730; map.put(buttonConstValue, "id.Button"); setResMap(map); ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code() .containsOne("return R.id.Button;") .doesNotContain("import R;"); // check 'R' class ClassNode rCls = cls.root().searchClassByFullAlias("R"); assertThat(rCls).isNotNull(); // check inner 'id' class List innerClasses = rCls.getInnerClasses(); assertThat(innerClasses).hasSize(1); ClassNode idCls = innerClasses.get(0); assertThat(idCls.getShortName()).isEqualTo("id"); // check 'Button' field FieldNode buttonField = idCls.searchFieldByName("Button"); assertThat(buttonField).isNotNull(); EncodedValue constVal = buttonField.get(CONSTANT_VALUE); Integer buttonValue = (Integer) constVal.getValue(); assertThat(buttonValue).isEqualTo(buttonConstValue); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/android/TestRFieldRestore2.java ================================================ package jadx.tests.integration.android; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestRFieldRestore2 extends IntegrationTest { public static class TestCls { public static class R { } public int test() { return 2131230730; } } @Test public void test() { Map map = new HashMap<>(); map.put(2131230730, "id.Button"); setResMap(map); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("R.id.Button;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/android/TestRFieldRestore3.java ================================================ package jadx.tests.integration.android; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestRFieldRestore3 extends IntegrationTest { public static class TestCls { @T(2131230730) public static class A { @F(2131230730) private int f; @M(bind = 2137373737) private void mth() { } @T(2137373737) private class D { } } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @interface T { int value(); } @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD }) @interface F { int value(); } @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) @interface M { int bind(); } public static class R { } } @Test public void test() { Map map = new HashMap<>(); map.put(2131230730, "id.Button"); map.put(2137373737, "id.MyId"); setResMap(map); assertThat(getClassNode(TestCls.class)) .code() .containsOnlyOnce("@T(R.id.Button)") .containsOnlyOnce("@T(R.id.MyId)") .containsOnlyOnce("@F(R.id.Button)") .containsOnlyOnce("@M(bind = R.id.MyId)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/android/TestResConstReplace.java ================================================ package jadx.tests.integration.android; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestResConstReplace extends IntegrationTest { public static class TestCls { public int test() { return 0x0101013f; // android.R.attr.minWidth } } @Test public void test() { disableCompilation(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("import android.R;") .containsOne("return R.attr.minWidth;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/android/TestResConstReplace2.java ================================================ package jadx.tests.integration.android; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestResConstReplace2 extends IntegrationTest { public static class TestCls { public int test(int i) { switch (i) { case 0x0101013f: // android.R.attr.minWidth return 1; case 0x01010140: // android.R.attr.minHeight return 2; default: return 0; } } } @Test public void test() { disableCompilation(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("import android.R;") .containsOne("case R.attr.minWidth:"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/android/TestResConstReplace3.java ================================================ package jadx.tests.integration.android; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestResConstReplace3 extends IntegrationTest { @Retention(RetentionPolicy.RUNTIME) public @interface UsesAndroidResource { int value() default 0; } @UsesAndroidResource(17039370 /* android.R.string.ok */) public static class TestCls { public void test(@UsesAndroidResource(17039370 /* android.R.string.ok */) int i) { } } @Test public void test() { disableCompilation(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("import android.R;") .countString(2, "@TestResConstReplace3.UsesAndroidResource(R.string.ok)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotations.java ================================================ package jadx.tests.integration.annotations; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnnotations extends IntegrationTest { public static class TestCls { private @interface A { int a(); } @A(a = -1) public void methodA1() { } @A(a = -253) public void methodA2() { } @A(a = -11253) public void methodA3() { } private @interface V { boolean value(); } @V(false) public void methodV() { } private @interface D { float value() default 1.1f; } @D public void methodD() { } } @Test public void test() { assertThat(getClassNode(TestCls.class)).code() .doesNotContain("@A(a = 255)") .containsOne("@A(a = -1)") .containsOne("@A(a = -253)") .containsOne("@A(a = -11253)") .containsOne("@V(false)") .doesNotContain("@D()") .containsOne("@D") .containsOne("int a();") .containsOne("float value() default 1.1f;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotations2.java ================================================ package jadx.tests.integration.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestAnnotations2 extends IntegrationTest { public static class TestCls { @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface A { int i(); float f(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("@Target({ElementType.TYPE})") .contains("@Retention(RetentionPolicy.RUNTIME)") .contains("public @interface A {") .contains("float f();") .contains("int i();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotationsMix.java ================================================ package jadx.tests.integration.annotations; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.Arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static java.lang.Thread.State.TERMINATED; import static org.assertj.core.api.Assertions.assertThat; public class TestAnnotationsMix extends IntegrationTest { public static class TestCls { public boolean test() throws Exception { Class cls = TestCls.class; new Thread(); Method err = cls.getMethod("error"); assertThat(err.getExceptionTypes().length > 0).isTrue(); assertThat(err.getExceptionTypes()[0]).isSameAs(Exception.class); Method d = cls.getMethod("depr", String[].class); assertThat(d.getAnnotations().length > 0).isTrue(); assertThat(d.getAnnotations()[0].annotationType()).isSameAs(Deprecated.class); Method ma = cls.getMethod("test", String[].class); assertThat(ma.getAnnotations().length > 0).isTrue(); MyAnnotation a = (MyAnnotation) ma.getAnnotations()[0]; assertThat(a.num()).isEqualTo(7); assertThat(a.state()).isSameAs(TERMINATED); return true; } @Deprecated public int a; public void error() throws Exception { throw new Exception("error"); } @Deprecated public static Object depr(String[] a) { return Arrays.asList(a); } @MyAnnotation( name = "b", num = 7, cls = Exception.class, doubles = { 0.0, 1.1 }, value = 9.87f, simple = @SimpleAnnotation(false) ) public static Object test(String[] a) { return Arrays.asList(a); } @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyAnnotation { String name() default "a"; String str() default "str"; int num(); float value(); double[] doubles(); Class cls(); SimpleAnnotation simple(); Thread.State state() default Thread.State.TERMINATED; } public @interface SimpleAnnotation { boolean value(); } public void check() throws Exception { test(); } } @Test public void test() { // useDexInput(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("int i = false;"); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } @Test public void testDeclaration() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("Thread thread = new Thread();") .contains("new Thread();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotationsRename.java ================================================ package jadx.tests.integration.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnnotationsRename extends IntegrationTest { public static class TestCls { @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface A { int x(); } @A(x = 5) void test() { } public void check() throws NoSuchMethodException { Method test = TestCls.class.getDeclaredMethod("test"); A annotation = test.getAnnotation(A.class); assertThat(annotation.x()).isEqualTo(5); } } @Test public void test() { enableDeobfuscation(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("public @interface ") .doesNotContain("(x = 5)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotationsRenameDef.java ================================================ package jadx.tests.integration.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestAnnotationsRenameDef extends IntegrationTest { public static class TestCls { @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface A { int value(); } @A(5) void test() { } } @Test public void test() { enableDeobfuscation(); // force rename 'value' method args.setDeobfuscationMinLength(20); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("public @interface ") .doesNotContain("int value();") .doesNotContain("(5)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotationsUsage.java ================================================ package jadx.tests.integration.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnnotationsUsage extends IntegrationTest { public static class TestCls { @Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) public @interface A { Class c(); } @A(c = TestCls.class) public static class B { } public static class C { @A(c = B.class) public String field; } @A(c = B.class) void test() { } void test2(@A(c = B.class) Integer value) { } } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); ClassNode annCls = searchCls(cls.getInnerClasses(), "A"); ClassNode bCls = searchCls(cls.getInnerClasses(), "B"); ClassNode cCls = searchCls(cls.getInnerClasses(), "C"); MethodNode testMth = getMethod(cls, "test"); MethodNode testMth2 = getMethod(cls, "test2"); assertThat(annCls.getUseIn()).contains(cls, bCls, cCls); assertThat(annCls.getUseInMth()).contains(testMth, testMth2); assertThat(bCls.getUseIn()).contains(cCls); assertThat(bCls.getUseInMth()).contains(testMth, testMth2); assertThat(cls) .code() .countString(3, "@A(c = B.class)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/annotations/TestParamAnnotations.java ================================================ package jadx.tests.integration.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestParamAnnotations extends IntegrationTest { public static class TestCls { @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public static @interface A { int i() default 7; } void test1(@A int i) { } void test2(int i, @A int j) { } void test3(@A(i = 5) int i) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("void test1(@A int i) {") .contains("void test2(int i, @A int j) {") .contains("void test3(@A(i = 5) int i) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestArith.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArith extends IntegrationTest { public static class TestCls { public static final int F = 7; public int test(int a) { a += 2; use(a); return a; } public int test2(int a) { a++; use(a); return a; } private static void use(int i) { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code(); } @Test @NotYetImplemented public void test2() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("a += 2;") .contains("a++;"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code(); } @Test @NotYetImplemented public void testNoDebug2() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("i += 2;") .contains("i++;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestArith2.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestArith2 extends IntegrationTest { public static class TestCls { public int test1(int a) { return (a + 2) * 3; } public int test2(int a, int b, int c) { return a + b + c; } public boolean test3(boolean a, boolean b, boolean c) { return a | b | c; } public boolean test4(boolean a, boolean b, boolean c) { return a & b & c; } public int substract(int a, int b, int c) { return a - (b - c); } public int divide(int a, int b, int c) { return a / (b / c); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("return (a + 2) * 3;") .doesNotContain("a + 2 * 3") .contains("return a + b + c;") .doesNotContain("return (a + b) + c;") .contains("return a | b | c;") .doesNotContain("return (a | b) | c;") .contains("return a & b & c;") .doesNotContain("return (a & b) & c;") .contains("return a - (b - c);") .doesNotContain("return a - b - c;") .contains("return a / (b / c);") .doesNotContain("return a / b / c;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestArith3.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestArith3 extends IntegrationTest { public static class TestCls { public int vp; public void test(byte[] buffer) { int n = ((buffer[3] & 255) + 4) + ((buffer[2] & 15) << 8); while (n + 4 < buffer.length) { int p = (buffer[n + 2] & 255) + ((buffer[n + 1] & 31) << 8); int len = (buffer[n + 4] & 255) + ((buffer[n + 3] & 15) << 8); int c = buffer[n] & 255; switch (c) { case 27: this.vp = p; break; } n += len + 5; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("while (n + 4 < buffer.length) {") .containsOne(indent() + "n += len + 5;") .doesNotContain("; n += len + 5) {") .doesNotContain("default:"); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("while ("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestArith4.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArith4 extends IntegrationTest { public static class TestCls { public static byte test(byte b) { int k = b & 7; return (byte) (((b & 255) >>> (8 - k)) | (b << k)); } public static int test2(String str) { int k = 'a' | str.charAt(0); return (1 - k) & (1 + k); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("int k = b & 7;") .containsOne("& 255") .containsOneOf("return (1 - k) & (1 + k);", "return (1 - k) & (k + 1);"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("& 255"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestArithConst.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArithConst extends SmaliTest { @Test public void test() { noDebugInfo(); assertThat(getClassNodeFromSmaliWithPath("arith", "TestArithConst")) .code() .containsOne("return i + CONST_INT;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestArithNot.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArithNot extends SmaliTest { // @formatter:off /* Smali Code equivalent: public static class TestCls { public int test1(int a) { return ~a; } public long test2(long b) { return ~b; } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmaliWithPath("arith", "TestArithNot")) .code() .contains("return ~a;") .contains("return ~b;") .doesNotContain("^"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestFieldIncrement.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestFieldIncrement extends IntegrationTest { public static class TestCls { public int instanceField = 1; public static int staticField = 1; public static String result = ""; public void method() { instanceField++; } public void method2() { staticField--; } public void method3(String s) { result += s + '_'; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("instanceField++;") .contains("staticField--;") .contains("result += s + '_';"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestFieldIncrement2.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestFieldIncrement2 extends IntegrationTest { public static class TestCls { private static class A { int f = 5; } public A a; public void test1(int n) { this.a.f = this.a.f + n; } public void test2(int n) { this.a.f *= n; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("this.a.f += n;") .contains("this.a.f *= n;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestFieldIncrement3.java ================================================ package jadx.tests.integration.arith; import java.util.Random; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestFieldIncrement3 extends IntegrationTest { public static class TestCls { static int tileX; static int tileY; static Vector2 targetPos = new Vector2(); static Vector2 directVect = new Vector2(); static Vector2 newPos = new Vector2(); public static void test() { Random rd = new Random(); int direction = rd.nextInt(7); switch (direction) { case 0: targetPos.x = ((tileX + 1) * 55) + 55; targetPos.y = ((tileY + 1) * 35) + 35; break; case 2: targetPos.x = ((tileX + 1) * 55) + 55; targetPos.y = ((tileY - 1) * 35) + 35; break; case 4: targetPos.x = ((tileX - 1) * 55) + 55; targetPos.y = ((tileY - 1) * 35) + 35; break; case 6: targetPos.x = ((tileX - 1) * 55) + 55; targetPos.y = ((tileY + 1) * 35) + 35; break; default: break; } directVect.x = targetPos.x - newPos.x; directVect.y = targetPos.y - newPos.y; float hPos = (float) Math.sqrt((directVect.x * directVect.x) + (directVect.y * directVect.y)); directVect.x /= hPos; directVect.y /= hPos; } static class Vector2 { public float x; public float y; public Vector2() { this.x = 0.0f; this.y = 0.0f; } public boolean equals(Vector2 other) { return (this.x == other.x && this.y == other.y); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("directVect.x = targetPos.x - newPos.x;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestNumbersFormat.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.api.args.IntegerFormat; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNumbersFormat extends IntegrationTest { @SuppressWarnings({ "FieldCanBeLocal", "UnusedAssignment", "unused" }) public static class TestCls { private Object obj; public void test() { obj = new byte[] { 0, -1, -0xA, (byte) 0xff, Byte.MIN_VALUE, Byte.MAX_VALUE }; obj = new short[] { 0, -1, -0xA, (short) 0xffff, Short.MIN_VALUE, Short.MAX_VALUE }; obj = new int[] { 0, -1, -0xA, 0xffff_ffff, Integer.MIN_VALUE, Integer.MAX_VALUE }; obj = new long[] { 0, -1, -0xA, 0xffff_ffff_ffff_ffffL, Long.MIN_VALUE, Long.MAX_VALUE }; } } @Test public void test() { getArgs().setIntegerFormat(IntegerFormat.AUTO); assertThat(getClassNode(TestCls.class)) .code() .containsOne("new byte[]{0, -1, -10, -1, -128, 127}") .containsOne("new short[]{0, -1, -10, -1, Short.MIN_VALUE, Short.MAX_VALUE}") .containsOne("new int[]{0, -1, -10, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}") .containsOne("new long[]{0, -1, -10, -1, Long.MIN_VALUE, Long.MAX_VALUE}"); } @Test public void testDecimalFormat() { getArgs().setIntegerFormat(IntegerFormat.DECIMAL); assertThat(getClassNode(TestCls.class)) .code() .containsOne("new byte[]{0, -1, -10, -1, -128, 127}") .containsOne("new short[]{0, -1, -10, -1, -32768, 32767}") .containsOne("new int[]{0, -1, -10, -1, -2147483648, 2147483647}") .containsOne("new long[]{0, -1, -10, -1, -9223372036854775808L, 9223372036854775807L}"); } @Test public void testHexFormat() { getArgs().setIntegerFormat(IntegerFormat.HEXADECIMAL); assertThat(getClassNode(TestCls.class)) .code() .containsOne("new byte[]{0x0, (byte) 0xff, (byte) 0xf6, (byte) 0xff, (byte) 0x80, 0x7f}") .containsOne("new short[]{0x0, (short) 0xffff, (short) 0xfff6, (short) 0xffff, (short) 0x8000, 0x7fff}") .containsOne("new int[]{0x0, (int) 0xffffffff, (int) 0xfffffff6, (int) 0xffffffff, (int) 0x80000000, 0x7fffffff}") .containsOne( "new long[]{0x0, 0xffffffffffffffffL, 0xfffffffffffffff6L, 0xffffffffffffffffL, 0x8000000000000000L, 0x7fffffffffffffffL}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestPrimitivesNegate.java ================================================ package jadx.tests.integration.arith; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestPrimitivesNegate extends IntegrationTest { @SuppressWarnings("UnnecessaryUnaryMinus") public static class TestCls { public double test() { double[] arr = new double[5]; arr[0] = -20; arr[0] += -79; return arr[0]; } public void check() { assertThat(test()).isEqualTo(-99); } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("dArr[0] = -20.0d;") .containsOne("dArr[0] = dArr[0] - 79.0d;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestSpecialValues.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSpecialValues extends IntegrationTest { public static class TestCls { public void test() { shorts(Short.MIN_VALUE, Short.MAX_VALUE); ints(Integer.MIN_VALUE, Integer.MAX_VALUE); longs(Long.MIN_VALUE, Long.MAX_VALUE); floats(Float.NaN, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.MIN_VALUE, Float.MAX_VALUE, Float.MIN_NORMAL); doubles(Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.MIN_VALUE, Double.MAX_VALUE, Double.MIN_NORMAL); } private void shorts(short... v) { } private void ints(int... v) { } private void longs(long... v) { } private void floats(float... v) { } private void doubles(double... v) { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne( "Float.NaN, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.MIN_VALUE, Float.MAX_VALUE, Float.MIN_NORMAL") .containsOne("Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, " + "Double.MIN_VALUE, Double.MAX_VALUE, Double.MIN_NORMAL") .containsOne("Short.MIN_VALUE, Short.MAX_VALUE") .containsOne("Integer.MIN_VALUE, Integer.MAX_VALUE") .containsOne("Long.MIN_VALUE, Long.MAX_VALUE"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestSpecialValues2.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSpecialValues2 extends IntegrationTest { public static class TestCls { private static int compareUnsigned(final int x, final int y) { return Integer.compare(x + Integer.MIN_VALUE, y + Integer.MIN_VALUE); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .countString(2, "Integer.MIN_VALUE"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arith/TestXor.java ================================================ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestXor extends SmaliTest { @SuppressWarnings("PointlessBooleanExpression") public static class TestCls { public boolean test1() { return test() ^ true; } public boolean test2(boolean v) { return v ^ true; } public boolean test() { return true; } public void check() { assertThat(test1()).isFalse(); assertThat(test2(true)).isFalse(); assertThat(test2(false)).isTrue(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return !test();") .containsOne("return !v;"); } @Test public void smali() { // @formatter:off /* public boolean test1() { return test() ^ true; } public boolean test2() { return test() ^ false; } public boolean test() { return true; } */ // @formatter:on assertThat(getClassNodeFromSmali()) .code() .containsOne("return !test();") .containsOne("return test();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayFill.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestArrayFill extends IntegrationTest { public static class TestCls { public String[] method() { return new String[] { "1", "2", "3" }; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("return new String[]{\"1\", \"2\", \"3\"};"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayFill2.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestArrayFill2 extends IntegrationTest { public static class TestCls { public int[] test(int a) { return new int[] { 1, a + 1, 2 }; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("return new int[]{1, a + 1, 2};"); } public static class TestCls2 { public int[] test2(int a) { return new int[] { 1, a++, a * 2 }; } } @Test @NotYetImplemented public void test2() { JadxAssertions.assertThat(getClassNode(TestCls2.class)) .code() .contains("return new int[]{1, a++, a * 2};"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayFill3.java ================================================ package jadx.tests.integration.arrays; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrayFill3 extends IntegrationTest { public static class TestCls { public byte[] test() { return new byte[] { 0, 1, 2 }; } } @TestWithProfiles({ TestProfile.ECJ_J8, TestProfile.ECJ_DX_J8 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return new byte[]{0, 1, 2}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayFill4.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrayFill4 extends IntegrationTest { public static class TestCls { // replaced constant break filled array creation private static final int ARRAY_SIZE = 4; public long[] test() { return new long[] { 0, 1, Long.MAX_VALUE, Long.MIN_VALUE + 1 }; } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("new long[ARRAY_SIZE];") .containsOne("return new long[]{0, 1, "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayFillConstReplace.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrayFillConstReplace extends IntegrationTest { public static class TestCls { public static final int CONST_INT = 0xffff; public int[] test() { return new int[] { 127, 129, CONST_INT }; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne(" int CONST_INT = 65535;") .containsOne("return new int[]{127, 129, CONST_INT};"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayFillNegative.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrayFillNegative extends IntegrationTest { public static class TestCls { public int[] test() { int[] arr = new int[3]; arr[0] = 1; arr[1] = arr[0] + 1; arr[2] = arr[1] + 1; return arr; } public void check() { assertThat(test()).isEqualTo(new int[] { 1, 2, 3 }); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("int[] arr = {1, ") .containsOne("int[] arr = new int[3];") .containsOne("arr[1] = arr[0] + 1;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayFillWithMove.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrayFillWithMove extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmaliFiles("TestCls")) .code() .doesNotContain("// fill-array-data instruction") .doesNotContain("arr[0] = 0;") .contains("return new long[]{0, 1}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayInit.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestArrayInit extends IntegrationTest { public static class TestCls { byte[] bytes; @SuppressWarnings("unused") public void test() { byte[] arr = new byte[] { 10, 20, 30 }; } public void test2() { bytes = new byte[] { 10, 20, 30 }; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("= {10, 20, 30};") .contains("this.bytes = new byte[]{10, 20, 30};"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayInitField.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrayInitField extends IntegrationTest { public static class TestCls { static byte[] a = new byte[] { 10, 20, 30 }; byte[] b = new byte[] { 40, 50, 60 }; } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("static byte[] a = {10, 20, 30};") .containsOne("byte[] b = {40, 50, 60};"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayInitField2.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrayInitField2 extends SmaliTest { @Test public void test() { forceDecompiledCheck(); assertThat(getClassNodeFromSmali()) .code() .containsOne("static long[] myArr = {1282979400, 0, 0};"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestArrays extends IntegrationTest { public static class TestCls { public int test1(int i) { int[] a = new int[] { 1, 2, 3, 5 }; return a[i]; } public int test2(int i) { int[][] a = new int[i][i + 1]; return a.length; } } @Test public void test() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("return new int[]{1, 2, 3, 5}[i];"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays2.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrays2 extends IntegrationTest { public static class TestCls { private static Object test4(int type) { if (type == 1) { return new int[] { 1, 2 }; } else if (type == 2) { return new float[] { 1, 2 }; } else if (type == 3) { return new short[] { 1, 2 }; } else if (type == 4) { return new byte[] { 1, 2 }; } else { return null; } } public void check() { assertThat(test4(4)).isInstanceOf(byte[].class); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("new int[]{1, 2}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays3.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrays3 extends IntegrationTest { public static class TestCls { private Object test(byte[] bArr) { return new Object[] { bArr }; } public void check() { byte[] inputArr = { 1, 2 }; Object result = test(inputArr); assertThat(result).isInstanceOf(Object[].class); assertThat(((Object[]) result)[0]).isEqualTo(inputArr); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return new Object[]{bArr};"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("return new Object[]{bArr};"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays4.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestArrays4 extends IntegrationTest { public static class TestCls { char[] payload; public TestCls(byte[] bytes) { char[] a = toChars(bytes); this.payload = new char[a.length]; System.arraycopy(a, 0, this.payload, 0, bytes.length); } private static char[] toChars(byte[] bArr) { return new char[bArr.length]; } } @Test public void testArrayTypeInference() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("char[] chars = toChars(bArr);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestFillArrayData.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFillArrayData extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmaliFiles("TestCls")) .code() .contains("jArr[0] = 1;") .contains("jArr[1] = 2;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/arrays/TestMultiDimArrayFill.java ================================================ package jadx.tests.integration.arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestMultiDimArrayFill extends IntegrationTest { public static class TestCls { public static Obj test(int a, int b) { return new Obj( new int[][] { { 1 }, { 2 }, { 3 }, { 4, 5 }, new int[0] }, new int[] { a, a, a, a, b }); } private static class Obj { public Obj(int[][] ints, int[] ints2) { } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("return new Obj(" + "new int[][]{new int[]{1}, new int[]{2}, new int[]{3}, new int[]{4, 5}, new int[0]}, " + "new int[]{a, a, a, a, b});"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/code/TestArrayAccessReorder.java ================================================ package jadx.tests.integration.code; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrayAccessReorder extends IntegrationTest { public static class TestCls { public int[] test(int[] arr) { int len = arr.length; int[] result = new int[len]; int i = 0; int k = len; while (k != 0) { int v = arr[i]; k--; int t = -v; i++; result[k] = t * 5; } return result; } public void check() { assertThat(test(new int[] { 1, 2, 3 })).isEqualTo(new int[] { -15, -10, -5 }); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("i++"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/code/TestCodeCommentStyle.java ================================================ package jadx.tests.integration.code; import org.junit.jupiter.api.Test; import jadx.api.data.CommentStyle; import jadx.api.data.IJavaNodeRef; import jadx.api.data.impl.JadxCodeComment; import jadx.api.data.impl.JadxNodeRef; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCodeCommentStyle extends IntegrationTest { @SuppressWarnings("unused") public static class TestCls { public int aSingleLine; public int aMultiLine; public int block; public int blockMulti; public int blockCondensed; public int blockCondensedMulti; public int javaDoc; public int javaDocMulti; public int javaDocCondensed; public int javaDocCondensedMulti; } @Test public void test() { addFldComment("aSingleLine", "Test line comment", CommentStyle.LINE); addFldComment("aMultiLine", "Test multi\nline comment", CommentStyle.LINE); addFldComment("block", "Test block comment", CommentStyle.BLOCK); addFldComment("blockMulti", "Test multi\nline block comment", CommentStyle.BLOCK); addFldComment("blockCondensed", "Test condensed block comment", CommentStyle.BLOCK_CONDENSED); addFldComment("blockCondensedMulti", "Test condensed multi\nline block comment", CommentStyle.BLOCK_CONDENSED); addFldComment("javaDoc", "Test javaDoc comment", CommentStyle.JAVADOC); addFldComment("javaDocMulti", "Test multi\nline javaDoc comment", CommentStyle.JAVADOC); addFldComment("javaDocCondensed", "Test condensed javaDoc comment", CommentStyle.JAVADOC_CONDENSED); addFldComment("javaDocCondensedMulti", "Test condensed multi\nline javaDoc comment", CommentStyle.JAVADOC_CONDENSED); assertThat(getClassNode(TestCls.class)) .code() .containsOne("// Test line comment") .containsOne("/* Test condensed block comment */"); } private void addFldComment(String fldName, String comment, CommentStyle style) { String clsName = "jadx.tests.integration.code.TestCodeCommentStyle$TestCls"; JadxNodeRef fldRef = new JadxNodeRef(IJavaNodeRef.RefType.FIELD, clsName, fldName + ":I"); getCodeData().getComments().add(new JadxCodeComment(fldRef, comment, style)); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestBitwiseAnd.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; @SuppressWarnings({ "PointlessBooleanExpression", "unused" }) public class TestBitwiseAnd extends IntegrationTest { public static class TestCls { private boolean a; private boolean b; public void test() { if ((a & b) != false) { test(); } } } @Test public void test() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (this.a && this.b) {"); } public static class TestCls2 { private boolean a; private boolean b; public void test() { if ((a & b) != true) { test(); } } } @Test public void test2() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls2.class)) .code() .containsOne("if (!this.a || !this.b) {"); } public static class TestCls3 { private boolean a; private boolean b; public void test() { if ((a & b) == false) { test(); } } } @Test public void test3() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls3.class)) .code() .containsOne("if (!this.a || !this.b) {"); } public static class TestCls4 { private boolean a; private boolean b; public void test() { if ((a & b) == true) { test(); } } } @Test public void test4() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls4.class)) .code() .containsOne("if (this.a && this.b) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestBitwiseOr.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestBitwiseOr extends IntegrationTest { public static class TestCls { private boolean a; private boolean b; public void test() { if ((a | b) != false) { test(); } } } @Test public void test() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (this.a || this.b) {"); } public static class TestCls2 { private boolean a; private boolean b; public void test() { if ((a | b) != true) { test(); } } } @Test public void test2() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls2.class)) .code() .containsOne("if (!this.a && !this.b) {"); } public static class TestCls3 { private boolean a; private boolean b; public void test() { if ((a | b) == false) { test(); } } } @Test public void test3() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls3.class)) .code() .containsOne("if (!this.a && !this.b) {"); } public static class TestCls4 { private boolean a; private boolean b; public void test() { if ((a | b) == true) { test(); } } } @Test public void test4() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls4.class)) .code() .containsOne("if (this.a || this.b) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestBooleanToByte.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBooleanToByte extends SmaliTest { // @formatter:off /* private boolean showConsent; public void write(byte b) { } public void writeToParcel(TestBooleanToByte testBooleanToByte) { testBooleanToByte.write(this.showConsent ? (byte) 1 : 0); } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("write(this.showConsent ? (byte) 1 : (byte) 0);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestBooleanToChar.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBooleanToChar extends SmaliTest { // @formatter:off /* private boolean showConsent; public void write(char b) { } public void writeToParcel(TestBooleanToChar testBooleanToChar) { testBooleanToChar.write(this.showConsent ? (char) 1 : 0); } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("write(this.showConsent ? (char) 1 : (char) 0);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestBooleanToDouble.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBooleanToDouble extends SmaliTest { // @formatter:off /* private boolean showConsent; public void write(double d) { } public void writeToParcel(TestBooleanToDouble testBooleanToDouble) { testBooleanToDouble.write(this.showConsent ? 1 : 0); } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("write(this.showConsent ? 1.0d : 0.0d);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestBooleanToFloat.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBooleanToFloat extends SmaliTest { // @formatter:off /* private boolean showConsent; public void write(float f) { } public void writeToParcel(TestBooleanToFloat testBooleanToFloat) { testBooleanToFloat.write(this.showConsent ? 1 : 0); } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("write(this.showConsent ? 1.0f : 0.0f);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestBooleanToInt.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestBooleanToInt extends SmaliTest { // @formatter:off /* private boolean showConsent; public void write(int b) { } public void writeToParcel(TestBooleanToInt testBooleanToInt) { testBooleanToInt.write(this.showConsent ? 1 : 0); } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("write(this.showConsent ? 1 : 0);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestBooleanToInt2.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestBooleanToInt2 extends SmaliTest { // @formatter:off /* public static class TestCls { public void test() { boolean v = getValue(); use1(Integer.valueOf(v)); use2(v); } private boolean getValue() { return false; } private void use1(Integer v) { } private void use2(int v) { } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("use1(Integer.valueOf(value ? 1 : 0));") .containsOne("use2(value ? 1 : 0);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestBooleanToLong.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBooleanToLong extends SmaliTest { // @formatter:off /* private boolean showConsent; public void write(long j) { } public void writeToParcel(TestBooleanToLong testBooleanToLong) { testBooleanToLong.write(this.showConsent ? 1 : 0); } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("write(this.showConsent ? 1L : 0L);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestBooleanToShort.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBooleanToShort extends SmaliTest { // @formatter:off /* private boolean showConsent; public void write(short b) { } public void writeToParcel(TestBooleanToShort testBooleanToShort) { testBooleanToShort.write(this.showConsent ? (short) 1 : 0); } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("write(this.showConsent ? (short) 1 : (short) 0);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestCast.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestCast extends IntegrationTest { public static class TestCls { byte myByte; short myShort; public void test1(boolean a) { write(a ? (byte) 0 : 1); } public void test2(boolean a) { write(a ? 0 : myByte); } public void test3(boolean a) { write(a ? 0 : (byte) 127); } public void test4(boolean a) { write(a ? (short) 0 : 1); } public void test5(boolean a) { write(a ? myShort : 0); } public void test6(boolean a) { write(a ? Short.MIN_VALUE : 0); } public void write(byte b) { } public void write(short b) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("write(a ? (byte) 0 : (byte) 1);") .contains("write(a ? (byte) 0 : this.myByte);") .contains("write(a ? (byte) 0 : (byte) 127);") .contains("write(a ? (short) 0 : (short) 1);") .contains("write(a ? this.myShort : (short) 0);") .contains("write(a ? Short.MIN_VALUE : (short) 0);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestCmpOp.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestCmpOp extends IntegrationTest { public static class TestCls { public boolean testGT(float a) { return a > 3.0f; } public boolean testLT(float b) { return b < 2.0f; } public boolean testEQ(float c) { return c == 1.0f; } public boolean testNE(float d) { return d != 0.0f; } public boolean testGE(float e) { return e >= -1.0f; } public boolean testLE(float f) { return f <= -2.0f; } public boolean testGT2(float g) { return 4.0f > g; } public boolean testLT2(long h) { return 5 < h; } public boolean testGE2(double i) { return 6.5d < i; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("return a > 3.0f;") .contains("return b < 2.0f;") .contains("return c == 1.0f;") .contains("return d != 0.0f;") .contains("return e >= -1.0f;") .contains("return f <= -2.0f;") .contains("return 4.0f > g;") .contains("return 5 < h;").contains("return 6.5d < i;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestCmpOp2.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestCmpOp2 extends IntegrationTest { public static class TestCls { public boolean testGT(float a, float b) { return a > b; } public boolean testLT(float c, double d) { return c < d; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("return a > b;") .contains("return ((double) c) < d;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestComplexIf.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestComplexIf extends SmaliTest { // @formatter:off /* public final class TestComplexIf { private String a; private int b; private float c; public final boolean test() { if (this.a.equals("GT-P6200") || this.a.equals("GT-P6210") || ... ) { return true; } if (this.a.equals("SM-T810") || this.a.equals("SM-T813") || ...) { return false; } return this.c > 160.0f ? true : this.c <= 0.0f && ((this.b & 15) == 4 ? 1 : null) != null; } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmaliWithPkg("conditions", "TestComplexIf")) .code() .containsOne("if (this.a.equals(\"GT-P6200\") || this.a.equals(\"GT-P6210\") || this.a.equals(\"A100\") " + "|| this.a.equals(\"A101\") || this.a.equals(\"LIFETAB_S786X\") || this.a.equals(\"VS890 4G\")) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestComplexIf2.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestComplexIf2 extends SmaliTest { // @formatter:off /* public void test() { if (this.isSaved) { throw new RuntimeException("Error"); } if (LoaderUtils.isContextLoaderAvailable()) { this.savedContextLoader = LoaderUtils.getContextClassLoader(); ClassLoader loader = this; if (this.project != null && "simple".equals(this.project)) { loader = getClass().getClassLoader(); } LoaderUtils.setContextClassLoader(loader); this.isSaved = true; } } */ // @formatter:on @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmaliWithPkg("conditions", "TestComplexIf2")).code() .containsOne("if (this.project != null && \"simple\".equals(this.project)) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestComplexIf3.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestComplexIf3 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .countString(1, "iArr = null;") .countString(2, "z = false;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestComplexIf4.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestComplexIf4 extends SmaliTest { @Test void test() { disableCompilation(); allowWarnInCode(); // this is just to allow a harmless duplicated region warning assertThat(getClassNodeFromSmali()).code().contains("if (0 >= 0) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditionInLoop.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConditionInLoop extends IntegrationTest { public static class TestCls { private static int test(int a, int b) { int c = a + b; for (int i = a; i < b; i++) { if (i == 7) { c += 2; } else { c *= 2; } } c--; return c; } public void check() { assertThat(test(5, 9)).isEqualTo(115); assertThat(test(8, 23)).isEqualTo(1015807); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("for (int i = a; i < b; i++) {") .containsOne("c += 2;") .containsOne("c *= 2;"); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("while"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConditions extends IntegrationTest { public static class TestCls { public boolean test(boolean a, boolean b, boolean c) { return (a && b) || c; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("(!a || !b) && !c") .contains("return (a && b) || c;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions10.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConditions10 extends IntegrationTest { public static class TestCls { public void test(boolean a, int b) { if (a || b > 2) { b++; } if (!a || (b >= 0 && b <= 11)) { System.out.println("1"); } else { System.out.println("2"); } System.out.println("3"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("return") .containsOne("if (a || b > 2) {") .containsOne("b++;") .containsOne("if (!a || (b >= 0 && b <= 11)) {") .containsOne("System.out.println(\"1\");") .containsOne("} else {") .containsOne("System.out.println(\"2\");") .containsOne("System.out.println(\"3\");"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions11.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConditions11 extends IntegrationTest { public static class TestCls { public void test(boolean a, int b) { if (a || b > 2) { f(); } } private void f() { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (a || b > 2) {") .containsOne("f();") .doesNotContain("return") .doesNotContain("else"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions12.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConditions12 extends IntegrationTest { public static class TestCls { static boolean autoStop = true; static boolean qualityReading = false; static int lastValidRaw = -1; public static void main(String[] args) throws Exception { int a = 5; int b = 30; dataProcess(a, b); } public static void dataProcess(int raw, int quality) { if (quality >= 10 && raw != 0) { System.out.println("OK" + raw); qualityReading = false; } else if (raw == 0 || quality < 6 || !qualityReading) { System.out.println("Not OK" + raw); } else { System.out.println("Quit OK" + raw); } if (quality < 30) { int timeLeft = 30 - quality; if (quality >= 10) { System.out.println("Processing" + timeLeft); } } else { System.out.println("Finish Processing"); if (raw > 0) { lastValidRaw = raw; } } if (quality >= 30 && autoStop) { System.out.println("Finished"); } if (!autoStop && lastValidRaw > -1 && quality < 10) { System.out.println("Finished"); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (quality >= 10 && raw != 0) {") .containsOne("} else if (raw == 0 || quality < 6 || !qualityReading) {") .containsOne("if (quality < 30) {") .containsOne("if (quality >= 10) {") .containsOne("if (raw > 0) {") .containsOne("if (quality >= 30 && autoStop) {") .containsOne("if (!autoStop && lastValidRaw > -1 && quality < 10) {") .doesNotContain("return"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions13.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConditions13 extends IntegrationTest { public static class TestCls { static boolean qualityReading; public static void dataProcess(int raw, int quality) { if (quality >= 10 && raw != 0) { System.out.println("OK" + raw); qualityReading = false; } else if (raw == 0 || quality < 6 || !qualityReading) { System.out.println("Not OK" + raw); } else { System.out.println("Quit OK" + raw); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (quality >= 10 && raw != 0) {") .containsOne("System.out.println(\"OK\" + raw);") .containsOne("qualityReading = false;") .containsOne("} else if (raw == 0 || quality < 6 || !qualityReading) {") .doesNotContain("return"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions14.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConditions14 extends IntegrationTest { @SuppressWarnings({ "EqualsReplaceableByObjectsCall", "ConstantConditions" }) public static class TestCls { public static boolean test(Object a, Object b) { boolean r = a == null ? b != null : !a.equals(b); if (r) { return false; } System.out.println("r=" + r); return true; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("boolean r = a == null ? b != null : !a.equals(b);") .containsOne("if (r) {") .containsOne("System.out.println(\"r=\" + r);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions15.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConditions15 extends IntegrationTest { public static class TestCls { public static boolean test(final String name) { if (isEmpty(name)) { return false; } if ("1".equals(name) || "2".equals(name) || "3".equals(name) || "4".equals(name) || "5".equals(name) || "6".equals(name) || "7".equals(name) || "8".equals(name) || "9".equals(name) || "10".equals(name) || "11".equals(name) || "12".equals(name) || "13".equals(name) || "14".equals(name) || "15".equals(name) || "16".equals(name) || "17".equals(name) || "18".equals(name) || "19".equals(name) || "20".equals(name) || "22".equals(name) || "22".equals(name) || "23".equals(name) || "24".equals(name) || "25".equals(name) || "26".equals(name) || "27".equals(name) || "28".equals(name) || "29".equals(name) || "30".equals(name)) { return false; } else { return true; } } private static boolean isEmpty(String name) { return name.isEmpty(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("\"1\".equals(name)") .containsOne("\"30\".equals(name)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions16.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestConditions16 extends IntegrationTest { public static class TestCls { private static boolean test(int a, int b) { return a < 0 || b % 2 != 0 && a > 28 || b < 0; } public void check() { assertThat(test(-1, 1)).isTrue(); assertThat(test(1, -1)).isTrue(); assertThat(test(29, 3)).isTrue(); assertThat(test(2, 2)).isFalse(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("return a < 0 || (b % 2 != 0 && a > 28) || b < 0;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions17.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConditions17 extends IntegrationTest { public static class TestCls { public static final int SOMETHING = 2; public static void test(int a) { if ((a & SOMETHING) != 0) { print(1); } print(2); } public static void print(Object o) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne(" & "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions18.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestConditions18 extends SmaliTest { // @formatter:off /* public static class TestConditions18 { private Map map; public boolean test(Object obj) { return this == obj || ((obj instanceof TestConditions18) && st(this.map, ((TestConditions18) obj).map)); } private static boolean st(Object obj, Object obj2) { return false; } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsLines(2, "if (this != obj) {", indent() + "return (obj instanceof TestConditions18) && st(this.map, ((TestConditions18) obj).map);", "}", "return true;"); } @Test @NotYetImplemented public void testNYI() { assertThat(getClassNodeFromSmali()) .code() .containsOne("return this == obj || ((obj instanceof TestConditions18) && st(this.map, ((TestConditions18) obj).map));"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions2.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; public class TestConditions2 extends IntegrationTest { public static class TestCls { int c; String d; String f; public void testComplexIf(String a, int b) { if (d == null || (c == 0 && b != -1 && d.length() == 0)) { c = a.codePointAt(c); } else { if (a.hashCode() != 0xCDE) { c = f.compareTo(a); } } } } @Test public void test() { getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions21.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConditions21 extends SmaliTest { // @formatter:off /* public boolean check(Object obj) { if (this == obj) { return true; } if (obj instanceof List) { List list = (List) obj; if (!list.isEmpty() && list.contains(this)) { return true; } } return false; } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()).code() .containsOne("!list.isEmpty() && list.contains(this)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions3.java ================================================ package jadx.tests.integration.conditions; import java.util.List; import java.util.regex.Pattern; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConditions3 extends IntegrationTest { public static class TestCls { private static final Pattern PATTERN = Pattern.compile("[a-f0-9]{20}"); public static Object test(final A a) { List list = a.getList(); if (list == null) { return null; } if (list.size() != 1) { return null; } String s = list.get(0); if (isEmpty(s)) { return null; } if (isDigitsOnly(s)) { return new A().set(s); } if (PATTERN.matcher(s).matches()) { return new A().set(s); } return null; } private static boolean isDigitsOnly(String s) { return false; } private static boolean isEmpty(String s) { return false; } private static class A { public Object set(String s) { return null; } public List getList() { return null; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .contains("return null;") .doesNotContain("else") .doesNotContain("AnonymousClass_1"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions4.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConditions4 extends IntegrationTest { public static class TestCls { public int test(int num) { boolean inRange = (num >= 59 && num <= 66); return inRange ? num + 1 : num; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("num >= 59 && num <= 66") .contains("? num + 1 : num;").doesNotContain("else"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions5.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConditions5 extends IntegrationTest { public static class TestCls { public static void test(Object a1, Object a2) { if (a1 == null) { if (a2 != null) { throw new AssertionError(a1 + " != " + a2); } } else if (!a1.equals(a2)) { throw new AssertionError(a1 + " != " + a2); } } public static void test2(Object a1, Object a2) { if (a1 != null) { if (!a1.equals(a2)) { throw new AssertionError(a1 + " != " + a2); } } else { if (a2 != null) { throw new AssertionError(a1 + " != " + a2); } } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("if (a1 == null) {") .contains("if (a2 != null) {") .contains("throw new AssertionError(a1 + \" != \" + a2);") .doesNotContain("if (a1.equals(a2)) {") .contains("} else if (!a1.equals(a2)) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions6.java ================================================ package jadx.tests.integration.conditions; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConditions6 extends IntegrationTest { public static class TestCls { public boolean test(List l1, List l2) { if (l2.size() > 0) { l1.clear(); } return l1.size() == 0; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("return l1.size() == 0;") .doesNotContain("else"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions7.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConditions7 extends IntegrationTest { public static class TestCls { public void test(int[] a, int i) { if (i >= 0 && i < a.length) { a[i]++; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("if (i >= 0 && i < a.length) {") .doesNotContain("||"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions8.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConditions8 extends IntegrationTest { public static class TestCls { private TestCls pager; private TestCls listView; public void test(TestCls view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (!isUsable()) { return; } if (!pager.hasMore()) { return; } if (getLoaderManager().hasRunningLoaders()) { return; } if (listView != null && listView.getLastVisiblePosition() >= pager.size()) { showMore(); } } private void showMore() { } private int size() { return 0; } private int getLastVisiblePosition() { return 0; } private boolean hasRunningLoaders() { return false; } private TestCls getLoaderManager() { return null; } private boolean hasMore() { return false; } private boolean isUsable() { return false; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("showMore();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions9.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConditions9 extends IntegrationTest { public static class TestCls { public void test(boolean a, int b) throws Exception { if (!a || (b >= 0 && b <= 11)) { System.out.println('1'); } else { System.out.println('2'); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (!a || (b >= 0 && b <= 11)) {") .containsOne("System.out.println('1');") .containsOne("} else {") .containsOne("System.out.println('2');") .doesNotContain("return;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestElseIf.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("IfCanBeSwitch") public class TestElseIf extends IntegrationTest { public static class TestCls { public int testIfElse(String str) { int r; if (str.equals("a")) { r = 1; } else if (str.equals("b")) { r = 2; } else if (str.equals("3")) { r = 3; } else if (str.equals("$")) { r = 4; } else { r = -1; System.out.println(); } r = r * 10; return Math.abs(r); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("} else if (str.equals(\"b\")) {") .containsOne("} else {") .containsOne("int r;") .containsOne("r = 1;") .containsOne("r = -1;") .doesNotContain(" ? ") .doesNotContain(" : "); // no ternary operator } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestElseIfCodeStyle.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestElseIfCodeStyle extends IntegrationTest { @SuppressWarnings("unused") public static class TestCls { public void test(String str) { if ("a".equals(str)) { call(1); } else if ("b".equals(str)) { call(2); } else if ("c".equals(str)) { call(3); } } private void call(int i) { } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("!\"c\".equals(str)") .doesNotContain("{\n" + indent(2) + "} else {"); // no empty `then` block } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestIfAndSwitch.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestIfAndSwitch extends SmaliTest { /* @formatter:off private final static int C = 0; private static int i; private static final Random rd = new Random(); private static final int ACTION_MOVE = 2; public static boolean ifAndSwitch() { boolean update = false; if (rd.nextInt() == ACTION_MOVE) { switch (i) { case C: update = true; break; } } if (update) { return true; } return false; } @formatter:on */ @Test public void test() { allowWarnInCode(); JadxAssertions.assertThat(getClassNodeFromSmali()) .code() .countString(1, "if (rd.nextInt() == ACTION_MOVE) {") .countString(1, "switch (") .countString(1, "else {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestIfCodeStyle.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue #1455 */ public class TestIfCodeStyle extends SmaliTest { @SuppressWarnings({ "ConstantConditions", "FieldCanBeLocal", "unused" }) public static class TestCls { private String moduleName; private String modulePath; private String preinstalledModulePath; private long versionCode; private String versionName; private boolean isFactory; private boolean isActive; public void test(Parcel parcel) { int startPos = parcel.dataPosition(); int size = parcel.readInt(); if (size < 0) { if (startPos > Integer.MAX_VALUE - size) { throw new RuntimeException("Overflow in the size of parcelable"); } parcel.setDataPosition(startPos + size); return; } try { if (parcel.dataPosition() - startPos >= size) { if (startPos > Integer.MAX_VALUE - size) { throw new RuntimeException("Overflow in the size of parcelable"); } parcel.setDataPosition(startPos + size); return; } this.moduleName = parcel.readString(); if (parcel.dataPosition() - startPos >= size) { if (startPos > Integer.MAX_VALUE - size) { throw new RuntimeException("Overflow in the size of parcelable"); } parcel.setDataPosition(startPos + size); return; } this.modulePath = parcel.readString(); if (parcel.dataPosition() - startPos >= size) { if (startPos > Integer.MAX_VALUE - size) { throw new RuntimeException("Overflow in the size of parcelable"); } parcel.setDataPosition(startPos + size); return; } this.preinstalledModulePath = parcel.readString(); if (parcel.dataPosition() - startPos >= size) { if (startPos > Integer.MAX_VALUE - size) { throw new RuntimeException("Overflow in the size of parcelable"); } parcel.setDataPosition(startPos + size); return; } this.versionCode = parcel.readLong(); if (parcel.dataPosition() - startPos >= size) { if (startPos > Integer.MAX_VALUE - size) { throw new RuntimeException("Overflow in the size of parcelable"); } parcel.setDataPosition(startPos + size); return; } this.versionName = parcel.readString(); if (parcel.dataPosition() - startPos >= size) { if (startPos > Integer.MAX_VALUE - size) { throw new RuntimeException("Overflow in the size of parcelable"); } parcel.setDataPosition(startPos + size); return; } this.isFactory = parcel.readInt() != 0; if (parcel.dataPosition() - startPos >= size) { if (startPos > Integer.MAX_VALUE - size) { throw new RuntimeException("Overflow in the size of parcelable"); } parcel.setDataPosition(startPos + size); return; } this.isActive = parcel.readInt() != 0; if (startPos > Integer.MAX_VALUE - size) { throw new RuntimeException("Overflow in the size of parcelable"); } parcel.setDataPosition(startPos + size); } catch (Throwable e) { if (startPos <= Integer.MAX_VALUE - size) { parcel.setDataPosition(startPos + size); throw e; } throw new RuntimeException("Overflow in the size of parcelable"); } } private static class Parcel { public void setDataPosition(int i) { } public int dataPosition() { return 0; } public int readInt() { return 0; } public String readString() { return null; } public long readLong() { return 0; } } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() // allow one last 'else' .oneOf(c -> c.doesNotContain("else").countString(8, "return;"), c -> c.countString(1, "else").countString(7, "return;")) .containsLines(2, "if (i < 0) {", indent() + "if (iDataPosition > Integer.MAX_VALUE - i) {", indent(2) + "throw new RuntimeException(\"Overflow in the size of parcelable\");", indent() + "}", indent() + "parcel.setDataPosition(iDataPosition + i);", indent() + "return;", "}"); } @Test public void testSmali() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() // allow one last 'else' .oneOf(c -> c.doesNotContain("else").countString(8, "return;"), c -> c.countString(1, "else").countString(7, "return;")) .containsLines(2, "if (_aidl_parcelable_size < 0) {", indent() + "if (_aidl_start_pos > Integer.MAX_VALUE - _aidl_parcelable_size) {", indent(2) + "throw new RuntimeException(\"Overflow in the size of parcelable\");", indent() + "}", indent() + "_aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);", indent() + "return;", "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestIfCodeStyle2.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue #2052 */ public class TestIfCodeStyle2 extends SmaliTest { @Test public void testSmali() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .countString(1, "} else if (") .countString(1, "} else {") .countString(19, "return "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestIfElseAndConditionIntermediateInstruction.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import jadx.tests.api.utils.assertj.JadxAssertions; // Here there are two IF blocks for each part of the IF predicate. // In some cases with optimised dex, two IF blocks cannot merged into the same IF region. // This happens where there are intermediate instructions between the two blocks which cannot be // inlined. // Both IF blocks share the same ELSE block which is then added to both resultant regions. // The resultant code does not reproduce the single if-else statement but it is better than failing // to decompile. public class TestIfElseAndConditionIntermediateInstruction extends SmaliTest { /* @formatter:off private boolean bool; private float num; private static final float CONST = 342; public void function() { if (bool && num < 1) { num += CONST; } else { nothing2(); } nothing1(); } private void nothing1() { } private void nothing2() { } @formatter:on */ @Test public void test() { allowWarnInCode(); JadxAssertions.assertThat(getClassNodeFromSmali()) .code() .countString(2, "else") .countString(2, "nothing2();") .countString(1, "nothing1();") .countString(1, "if (this.bool)") .countString(1, "if (f < 1.0f)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestInnerAssign.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInnerAssign extends IntegrationTest { public static class TestCls { private String result; @SuppressWarnings("checkstyle:InnerAssignment") public void test(String str) { int len; if (str.isEmpty() || (len = str.length()) > 5) { result += "bad"; } else { result += "good, len: " + len; } result += ", str: " + str; System.out.println("done"); } private String runTest(String str) { result = ""; test(str); return result; } public void check() { assertThat(runTest("")).isEqualTo("bad, str: "); assertThat(runTest("1234")).isEqualTo("good, len: 4, str: 1234"); assertThat(runTest("1234567")).isEqualTo("bad, str: 1234567"); } } @Test public void test() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("str.length()") .containsOne("System.out.println(\"done\");"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestInnerAssign2.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInnerAssign2 extends IntegrationTest { public static class TestCls { private String field; private String swapField; @SuppressWarnings("checkstyle:InnerAssignment") public boolean test(String str) { String sub; return call(str) || ((sub = this.field) != null && sub.isEmpty()); } private boolean call(String str) { this.field = swapField; return str.isEmpty(); } public boolean testWrap(String str, String fieldValue) { this.field = null; this.swapField = fieldValue; return test(str); } public void check() { assertThat(testWrap("", null)).isTrue(); assertThat(testWrap("a", "")).isTrue(); assertThat(testWrap("b", null)).isFalse(); assertThat(testWrap("c", "d")).isFalse(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("sub = this.field") .containsOne("return call(str) || ((sub = this.field) != null && sub.isEmpty());"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestInnerAssign3.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue #820 */ public class TestInnerAssign3 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOne("(testClass2TestMethod = (testClass1 = null).testMethod()) == null") .containsOne("testClass1.testField != null"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestNestedIf.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNestedIf extends IntegrationTest { public static class TestCls { private boolean a0 = false; private int a1 = 1; private int a2 = 2; private int a3 = 1; private int a4 = 2; public boolean test1() { if (a0) { if (a1 == 0 || a2 == 0) { return false; } } else if (a3 == 0 || a4 == 0) { return false; } test1(); return true; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (this.a0) {") .containsOne("if (this.a1 == 0 || this.a2 == 0) {") .containsOne("} else if (this.a3 == 0 || this.a4 == 0) {") .countString(2, "return false;") .containsOne("test1();") .containsOne("return true;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestNestedIf2.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestNestedIf2 extends IntegrationTest { public static class TestCls { static int executedCount = 0; static boolean finished = false; static int repeatCount = 2; static boolean test(float delta, Object object) { if (executedCount != repeatCount && isRun(delta, object)) { if (finished) { return true; } if (repeatCount == -1) { ++executedCount; action(); return false; } ++executedCount; if (executedCount >= repeatCount) { return true; } action(); } return false; } public static void action() { } public static boolean isRun(float delta, Object object) { return delta == 0; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (executedCount != repeatCount && isRun(delta, object)) {") .containsOne("if (finished) {") .doesNotContain("else"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestOutBlock.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue #2384 */ public class TestOutBlock extends SmaliTest { @Test public void test() { allowWarnInCode(); disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOne("setContentView"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestSimpleConditions.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestSimpleConditions extends IntegrationTest { public static class TestCls { public boolean test1(boolean[] a) { return (a[0] && a[1] && a[2]) || (a[3] && a[4]); } public boolean test2(boolean[] a) { return a[0] || a[1] || a[2] || a[3]; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("return (a[0] && a[1] && a[2]) || (a[3] && a[4]);") .contains("return a[0] || a[1] || a[2] || a[3];"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTernary extends IntegrationTest { public static class TestCls { public boolean test1(int a) { return a != 2; } public void test2(int a) { checkTrue(a == 3); } public int test3(int a) { return a > 0 ? a : (a + 2) * 3; } private static void checkTrue(boolean v) { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("else") .contains("return a != 2;") .contains("checkTrue(a == 3)") .contains("return a > 0 ? a : (a + 2) * 3;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary2.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTernary2 extends IntegrationTest { public static class TestCls { public void test() { checkFalse(f(1, 0) == 0); } private int f(int a, int b) { return a + b; } private void checkFalse(boolean b) { if (b) { throw new AssertionError("Must be false"); } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("f(1, 0)"); } @Test @NotYetImplemented public void test2() { assertThat(getClassNode(TestCls.class)) .code() .contains("assertTrue(f(1, 0) == 0);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary3.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.Named; import jadx.core.dex.instructions.args.RegisterArg; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTernary3 extends IntegrationTest { public static class TestCls { public boolean isNameEquals(InsnArg arg) { String n = getName(arg); if (n == null || !(arg instanceof Named)) { return false; } return n.equals(((Named) arg).getName()); } private String getName(InsnArg arg) { if (arg instanceof RegisterArg) { return "r"; } if (arg instanceof Named) { return "n"; } return arg.toString(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (n == null || !(arg instanceof Named)) {") .containsOne("return n.equals(((Named) arg).getName());") .doesNotContain("if ((arg instanceof RegisterArg)) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary4.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestTernary4 extends SmaliTest { // @formatter:off /* private Set test(HashMap hashMap) { boolean z; HashSet hashSet = new HashSet(); synchronized (this.defaultValuesByPath) { for (String next : this.defaultValuesByPath.keySet()) { Object obj = hashMap.get(next); if (obj != null) { z = !getValueObject(next).equals(obj); } else { z = this.valuesByPath.get(next) != null;; } if (z) { hashSet.add(next); } } } return hashSet; } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .removeBlockComments() .doesNotContain("5") .doesNotContain("try"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernaryInIf.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTernaryInIf extends IntegrationTest { public static class TestCls { public boolean test1(boolean a, boolean b, boolean c) { return a ? b : c; } public int test2(boolean a, boolean b, boolean c) { return (!a ? c : b) ? 1 : 2; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("if") .doesNotContain("else") .containsOne("return a ? b : c;") .containsOneOf( "return (a ? b : c) ? 1 : 2;", "return (a ? !b : !c) ? 2 : 1;" // TODO: simplify this ); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernaryInIf2.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTernaryInIf2 extends SmaliTest { public static class TestCls { private String a = "a"; private String b = "b"; public boolean equals(TestCls other) { if (this.a == null ? other.a == null : this.a.equals(other.a)) { if (this.b == null ? other.b == null : this.b.equals(other.b)) { return true; } } return false; } public void check() { TestCls other = new TestCls(); other.a = "a"; other.b = "b"; assertThat(this.equals(other)).isTrue(); other.b = "not-b"; assertThat(this.equals(other)).isFalse(); other.b = null; assertThat(this.equals(other)).isFalse(); this.b = null; assertThat(this.equals(other)).isTrue(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsLines(2, "if (this.a != null ? this.a.equals(other.a) : other.a == null) {"); // .containsLines(3, "if (this.b != null ? this.b.equals(other.b) : other.b == null) {") // .containsLines(4, "return true;") // .containsLines(2, "return false;") } @Test @NotYetImplemented public void testNYI() { assertThat(getClassNode(TestCls.class)) .code() .containsLines(2, "return (this.a != null ? this.a.equals(other.a) : other.a == null) " + "&& (this.b == null ? other.b == null : this.b.equals(other.b));"); } @Test public void test2() { getClassNodeFromSmaliWithPath("conditions", "TestTernaryInIf2"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernaryInIf3.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; public class TestTernaryInIf3 extends SmaliTest { @Test public void test() { disableCompilation(); getClassNodeFromSmali(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernaryOneBranchInConstructor.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTernaryOneBranchInConstructor extends IntegrationTest { public static class TestCls { public TestCls(String str, int i) { this(str == null ? 0 : i); } public TestCls(int i) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("this(str == null ? 0 : i);") .doesNotContain("//") .doesNotContain("call moved to the top of the method"); } public static class TestCls2 { public TestCls2(String str, int i) { this(i == 1 ? str : "", i == 0 ? "" : str); } public TestCls2(String a, String b) { } } @Test public void test2() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls2.class)) .code() .containsOne("this(i == 1 ? str : \"\", i == 0 ? \"\" : str);") .doesNotContain("//"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernaryOneBranchInConstructor2.java ================================================ package jadx.tests.integration.conditions; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTernaryOneBranchInConstructor2 extends SmaliTest { // @formatter:off /* public class A { public A(String str, String str2, String str3, boolean z) {} public A(String str, String str2, String str3, boolean z, int i, int i2) { this(str, (i & 2) != 0 ? "" : str2, (i & 4) != 0 ? "" : str3, (i & 8) != 0 ? false : z); } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("this(str, (i & 2) != 0 ? \"\" : str2, (i & 4) != 0 ? \"\" : str3, (i & 8) != 0 ? false : z);") .doesNotContain("//"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers.java ================================================ package jadx.tests.integration.debuginfo; import org.junit.jupiter.api.Test; import jadx.api.utils.CodeUtils; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLineNumbers extends IntegrationTest { public static class TestCls { int field; public void func() { } public static class Inner { int innerField; public void innerFunc() { } public void innerFunc2() { new Runnable() { @Override public void run() { } }.run(); } public void innerFunc3() { } } } @Test public void test() { printLineNumbers(); ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); FieldNode field = cls.searchFieldByName("field"); MethodNode func = cls.searchMethodByShortId("func()V"); ClassNode inner = cls.getInnerClasses().get(0); MethodNode innerFunc = inner.searchMethodByShortId("innerFunc()V"); MethodNode innerFunc2 = inner.searchMethodByShortId("innerFunc2()V"); MethodNode innerFunc3 = inner.searchMethodByShortId("innerFunc3()V"); FieldNode innerField = inner.searchFieldByName("innerField"); // check source lines (available only for instructions and methods) int testClassLine = 16; assertThat(testClassLine + 3).isEqualTo(func.getSourceLine()); assertThat(testClassLine + 9).isEqualTo(innerFunc.getSourceLine()); assertThat(testClassLine + 12).isEqualTo(innerFunc2.getSourceLine()); assertThat(testClassLine + 20).isEqualTo(innerFunc3.getSourceLine()); // check decompiled lines checkLine(code, field, "int field;"); checkLine(code, func, "public void func() {"); checkLine(code, inner, "public static class Inner {"); checkLine(code, innerField, "int innerField;"); checkLine(code, innerFunc, "public void innerFunc() {"); checkLine(code, innerFunc2, "public void innerFunc2() {"); checkLine(code, innerFunc3, "public void innerFunc3() {"); } private static void checkLine(String code, LineAttrNode node, String str) { String line = CodeUtils.getLineForPos(code, node.getDefPosition()); assertThat(line).contains(str); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers2.java ================================================ package jadx.tests.integration.debuginfo; import java.lang.ref.WeakReference; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static org.assertj.core.api.Assertions.assertThat; public class TestLineNumbers2 extends IntegrationTest { public static class TestCls { private WeakReference f; // keep constructor at line 18 public TestCls(TestCls s) { } public TestCls test(TestCls s) { TestCls store = f != null ? f.get() : null; if (store == null) { store = new TestCls(s); f = new WeakReference<>(store); } return store; } public Object test2() { return new Object(); } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { printLineNumbers(); ClassNode cls = getClassNode(TestCls.class); String linesMapStr = cls.getCode().getCodeMetadata().getLineMapping().toString(); if (isJavaInput()) { assertThat(linesMapStr).isEqualTo("{6=16, 9=17, 12=21, 13=22, 14=23, 15=24, 16=25, 18=27, 21=30, 22=31}"); } else { assertThat(linesMapStr).isEqualTo("{6=16, 9=17, 12=21, 13=22, 14=23, 15=24, 16=25, 17=27, 19=27, 22=30, 23=31}"); } } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers3.java ================================================ package jadx.tests.integration.debuginfo; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLineNumbers3 extends IntegrationTest { public static class TestCls extends Exception { public TestCls(final Object message) { super((message == null) ? "" : message.toString()); /* * comment to increase line number in return instruction * - * - * - * - * - * - * - * - * - * - */ } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code().containsOne("super(message == null ? \"\" : message.toString());"); String linesMapStr = cls.getCode().getCodeMetadata().getLineMapping().toString(); assertThat(linesMapStr).isEqualTo("{4=13, 5=14, 6=15}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestReturnSourceLine.java ================================================ package jadx.tests.integration.debuginfo; import org.junit.jupiter.api.Test; import jadx.api.ICodeInfo; import jadx.api.utils.CodeUtils; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestReturnSourceLine extends IntegrationTest { public static class TestCls { public int test1(boolean v) { if (v) { f(); return 1; } f(); return 0; } public int test2(int v) { if (v == 0) { f(); return v - 1; } f(); return v + 1; } public int test3(int v) { if (v == 0) { f(); return v; } f(); return v + 1; } private void f() { } } @Test public void test() { printLineNumbers(); ClassNode cls = getClassNode(TestCls.class); ICodeInfo codeInfo = cls.getCode(); String[] lines = codeInfo.getCodeStr().split("\\R"); MethodNode test1 = cls.searchMethodByShortId("test1(Z)I"); checkLine(lines, codeInfo, test1, 3, "return 1;"); MethodNode test2 = cls.searchMethodByShortId("test2(I)I"); checkLine(lines, codeInfo, test2, 3, "return v - 1;"); checkLine(lines, codeInfo, test2, 6, "return v + 1;"); MethodNode test3 = cls.searchMethodByShortId("test3(I)I"); if (isJavaInput()) { // dx lost line number for this return checkLine(lines, codeInfo, test3, 3, "return v;"); } checkLine(lines, codeInfo, test3, 6, "return v + 1;"); } private static void checkLine(String[] lines, ICodeInfo cw, LineAttrNode node, int offset, String str) { int nodeDefLine = CodeUtils.getLineNumForPos(cw.getCodeStr(), node.getDefPosition(), "\n"); int decompiledLine = nodeDefLine + offset; assertThat(lines[decompiledLine - 1]).containsOne(str); Integer sourceLine = cw.getCodeMetadata().getLineMapping().get(decompiledLine); assertThat(sourceLine).isNotNull(); assertThat((int) sourceLine).isEqualTo(node.getSourceLine() + offset); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestVariablesNames.java ================================================ package jadx.tests.integration.debuginfo; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestVariablesNames extends SmaliTest { // @formatter:off /* public static class TestCls { public void test(String s, int k) { f1(s); int i = k + 3; String s2 = "i" + i; f2(i, s2); double d = i * 5; String s3 = "d" + d; f3(d, s3); } private void f1(String s) { } private void f2(int i, String i2) { } private void f3(double d, String d2) { } } */ // @formatter:on /** * Parameter register reused in variables assign with different types and names * No variables names in debug info */ @Test public void test() { assertThat(getClassNodeFromSmaliWithPath("debuginfo", "TestVariablesNames")) .code() // TODO: don't use current variables naming in tests .containsOne("f1(str);") .containsOne("f2(i2, \"i\" + i2);") .containsOne("f3(d, \"d\" + d);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/deobf/TestDontRenameClspOverriddenMethod.java ================================================ package jadx.tests.integration.deobf; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDontRenameClspOverriddenMethod extends IntegrationTest { public static class TestCls { public static class A implements Runnable { @Override public void run() { } } public static class B extends A { @Override public void run() { } } } @Test public void test() { noDebugInfo(); enableDeobfuscation(); args.setDeobfuscationMinLength(100); // rename everything assertThat(getClassNode(TestCls.class)) .code() .countString(2, "public void run() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/deobf/TestFieldFromInnerClass.java ================================================ package jadx.tests.integration.deobf; import java.util.List; import java.util.Queue; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldFromInnerClass extends IntegrationTest { public static class TestCls { TestCls.I f; public class I { Queue a; Queue.I> b; public class X { List.I.X> c; } } } @Test public void test() { noDebugInfo(); enableDeobfuscation(); ClassNode cls = getClassNode(TestCls.class); assertThat(cls) .code() .doesNotContain("class I {") .doesNotContain(".I ") .doesNotContain(".I.X>"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/deobf/TestInheritedMethodRename.java ================================================ package jadx.tests.integration.deobf; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInheritedMethodRename extends IntegrationTest { public static class TestCls { public static class A extends B { } public static class B { public void call() { System.out.println("call"); } } public void test(A a) { // reference to A.call() not renamed, // should be resolved to B.call() and use alias a.call(); } } @Test public void test() { noDebugInfo(); enableDeobfuscation(); getArgs().setDeobfuscationMinLength(99); assertThat(getClassNode(TestCls.class)) .code() .containsOne("public void m1call() {") .doesNotContain(".call();") .containsOne(".m1call();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/deobf/TestMthRename.java ================================================ package jadx.tests.integration.deobf; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestMthRename extends IntegrationTest { public static class TestCls { public abstract static class TestAbstractCls { public abstract void a(); } public void test(TestAbstractCls a) { a.a(); } } @Test public void test() { noDebugInfo(); enableDeobfuscation(); assertThat(getClassNode(TestCls.class)).code() .doesNotContain("public abstract void a();") .doesNotContain(".a();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/deobf/TestRenameOverriddenMethod.java ================================================ package jadx.tests.integration.deobf; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestRenameOverriddenMethod extends IntegrationTest { public static class TestCls { public interface I { void m(); } public static class A implements I { @Override public void m() { } } public static class B extends A { @Override public void m() { } } } @Test public void test() { noDebugInfo(); enableDeobfuscation(); args.setDeobfuscationMinLength(100); // rename everything assertThat(getClassNode(TestCls.class)) .code() .countString(2, "@Override") .countString(3, "renamed from: m") .containsOne("void mo0m();") .countString(2, "public void mo0m() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/deobf/TestRenameOverriddenMethod2.java ================================================ package jadx.tests.integration.deobf; import org.junit.jupiter.api.Test; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestRenameOverriddenMethod2 extends IntegrationTest { public static class TestCls { public interface I { int call(); } public static class A implements I { @Override public int call() { return 1; } } public static class B implements I { @Override public int call() { return 2; } } } @Test public void test() { enableDeobfuscation(); args.setDeobfuscationMinLength(100); // rename everything ClassNode cls = getClassNode(TestCls.class); assertThat(cls) .code() .countString(2, "@Override") .countString(3, "int mo0call()"); assertThat(searchCls(cls.getInnerClasses(), "I")).isNotNull() .extracting(c -> c.searchMethodByShortName("call")).isNotNull() .extracting(m -> m.get(AType.METHOD_OVERRIDE)).isNotNull() .satisfies(ovrdAttr -> { assertThat(ovrdAttr.getRelatedMthNodes()).hasSize(3); assertThat(ovrdAttr.getOverrideList()).isEmpty(); }); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/deobf/TestRenameOverriddenMethod3.java ================================================ package jadx.tests.integration.deobf; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestRenameOverriddenMethod3 extends IntegrationTest { public static class TestCls { public abstract static class A { public abstract int call(); } public static class B extends A { @Override public final int call() { return 1; } } } @Test public void test() { addMthRename(TestCls.class.getName() + "$A", "call()I", "callRenamed"); assertThat(getClassNode(TestCls.class)) .code() .countString(1, "@Override") .countString(2, "int callRenamed()"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/deobf/a/TestNegativeRenameCondition.java ================================================ package jadx.tests.integration.deobf.a; import java.util.Collections; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNegativeRenameCondition extends IntegrationTest { public static class TestCls { @SuppressWarnings("checkstyle:TypeName") public interface a { @SuppressWarnings("checkstyle:MethodName") void a(); } public void test(a a) { a.a(); } } @Test public void test() { noDebugInfo(); enableDeobfuscation(); // disable rename by length args.setDeobfuscationMinLength(0); args.setDeobfuscationMaxLength(999); // disable all renaming options args.setRenameFlags(Collections.emptySet()); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("renamed from") .containsOne("package jadx.tests.integration.deobf.a;") .containsOne("public interface a {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumKotlinEntries.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Test for Kotlin 1.9+ enum $ENTRIES pattern. */ public class TestEnumKotlinEntries extends SmaliTest { @Test public void test() { disableCompilation(); // kotlin.enums.EnumEntries not on test classpath assertThat(getClassNodeFromSmali()) .code() .containsLines(1, "ALPHA,", "BETA,", "GAMMA;") .containsOne("EnumEntries $ENTRIES = EnumEntriesKt.enumEntries(values());") .doesNotContain("$VALUES") .doesNotContain("Failed to restore enum"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumObfuscated.java ================================================ package jadx.tests.integration.enums; import java.util.Collections; import org.junit.jupiter.api.Test; import jadx.api.CommentsLevel; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnumObfuscated extends SmaliTest { // @formatter:off /* public enum TestEnumObfuscated { private static final synthetic TestEnumObfuscated[] $VLS = {ONE, TWO}; public static final TestEnumObfuscated ONE = new TestEnumObfuscated("ONE", 0, 1); public static final TestEnumObfuscated TWO = new TestEnumObfuscated("TWO", 1, 2); private final int num; private TestEnumObfuscated(String str, int i, int i2) { super(str, i); this.num = i2; } public static TestEnumObfuscated vo(String str) { return (TestEnumObfuscated) Enum.valueOf(TestEnumObfuscated.class, str); } public static TestEnumObfuscated[] vs() { return (TestEnumObfuscated[]) $VLS.clone(); } public synthetic int getNum() { return this.num; } // custom values method // should be kept and renamed to avoid collision to enum 'values()' method public static int values() { return new TestEnumObfuscated[0]; } // usage of renamed 'values()' method, should be renamed back to 'values' public static int valuesCount() { return vs().length; } // usage of renamed '$VALUES' field, should be replaced with 'values()' method call public static int valuesFieldUse() { return $VLS.length; } } */ // @formatter:on @Test public void test() { getArgs().setCommentsLevel(CommentsLevel.WARN); getArgs().setRenameFlags(Collections.emptySet()); assertThat(getClassNodeFromSmali()) .code() .doesNotContain("$VLS") .doesNotContain("vo(") .doesNotContain("vs(") .containsOne("int getNum() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumUsesOtherEnum.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnumUsesOtherEnum extends SmaliTest { public static class TestCls { public enum VType { INT(1), OTHER_INT(INT); private final int type; VType(int type) { this.type = type; } VType(VType refType) { this(refType.type); } } } @TestWithProfiles(TestProfile.D8_J11) public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("OTHER_INT(INT);") .doesNotContain("\n \n"); // no indentation for empty string } @Test public void testSmali() { assertThat(getClassNodeFromSmali()) .code() .containsOne("public enum TestEnumUsesOtherEnum {") .doesNotContain("static {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumWithConstInlining.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestEnumWithConstInlining extends SmaliTest { enum TestCls { E0, E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14, E15, E16, E17, E18, E19, E20, E21, E22, E23, E24, E25, E26, E27, E28, E29, E30, E31, E32, E33, E34, E35, E36, E37, E38, E39, E40, E41, E42, E43, E44, E45, E46, E47, E48, E49, E50, E51, E52, E53, E54, E55, E56, E57, E58, E59, E60, E61, E62, E63, E64, E65, E66, E67, E68, E69, E70, E71, E72, E73, E74, E75, E76, E77, E78, E79, E80, E81, E82, E83, E84, E85, E86, E87, E88, E89, E90, E91, E92, E93, E94, E95, E96, E97, E98, E99, E100; /** * Match the length of the $VALUES array. */ public static final int CONST = 101; } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("E42,"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumWithFields.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestEnumWithFields extends SmaliTest { public static class TestCls { public enum SearchTimeout { DISABLED(0), TWO_SECONDS(2), FIVE_SECONDS(5); public static final SearchTimeout DEFAULT = DISABLED; public static final SearchTimeout MAX = FIVE_SECONDS; public final int sec; SearchTimeout(int val) { this.sec = val; } } public void check() { assertThat(SearchTimeout.DISABLED.sec).isEqualTo(0); assertThat(SearchTimeout.DEFAULT.sec).isEqualTo(0); assertThat(SearchTimeout.TWO_SECONDS.sec).isEqualTo(2); assertThat(SearchTimeout.MAX.sec).isEqualTo(5); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code(); } @Test public void test2() { assertThat(getClassNodeFromSmali()) .code(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnums extends IntegrationTest { public static class TestCls { public enum EmptyEnum { } @SuppressWarnings("NoWhitespaceBefore") public enum EmptyEnum2 { ; public static void mth() { } } public enum Direction { NORTH, SOUTH, EAST, WEST } public enum Singleton { INSTANCE; public String test() { return ""; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsLines(1, "public enum EmptyEnum {", "}") .containsLines(1, "public enum EmptyEnum2 {", indent(1) + ';', "", indent(1) + "public static void mth() {", indent(1) + '}', "}") .containsLines(1, "public enum Direction {", indent(1) + "NORTH,", indent(1) + "SOUTH,", indent(1) + "EAST,", indent(1) + "WEST", "}") .containsLines(1, "public enum Singleton {", indent(1) + "INSTANCE;", "", indent(1) + "public String test() {", indent(2) + "return \"\";", indent(1) + '}', "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums10.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Some enum field was removed, but still exist in values array */ public class TestEnums10 extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .doesNotContain("Failed to restore enum class") .containsOne("enum TestEnums10 {") .countString(4, "Fake field"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums11.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.RaungTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnums11 extends RaungTest { @Test public void test() { assertThat(getClassNodeFromRaung()) .code() .containsLines("public enum TestEnums11 {", indent(1) + "UNKNOWN;") .containsOne("public final int a = -99;") .doesNotContain("TestEnums11() {"); } @Test public void testDisableEnumRestore() { // constructor method incorrectly removed getArgs().getDisabledPasses().add("EnumVisitor"); disableCompilation(); assertThat(getClassNodeFromRaung()) .code() .containsOne("public TestEnums11() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums2.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.api.CommentsLevel; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnums2 extends IntegrationTest { public static class TestCls { public enum Operation { PLUS { @Override public int apply(int x, int y) { return x + y; } }, MINUS { @Override public int apply(int x, int y) { return x - y; } }; public abstract int apply(int x, int y); } } @Test public void test() { getArgs().setCommentsLevel(CommentsLevel.WARN); assertThat(getClassNode(TestCls.class)) .code() .containsLines(1, "public enum Operation {", indent(1) + "PLUS {", indent(2) + "@Override", indent(2) + "public int apply(int x, int y) {", indent(3) + "return x + y;", indent(2) + '}', indent(1) + "},", indent(1) + "MINUS {", indent(2) + "@Override", indent(2) + "public int apply(int x, int y) {", indent(3) + "return x - y;", indent(2) + '}', indent(1) + "};", "", indent(1) + "public abstract int apply(int i, int i2);", "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums2a.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static jadx.tests.integration.enums.TestEnums2a.TestCls.DoubleOperations.DIVIDE; import static jadx.tests.integration.enums.TestEnums2a.TestCls.DoubleOperations.TIMES; public class TestEnums2a extends IntegrationTest { public static class TestCls { public interface IOps { double apply(double x, double y); } public enum DoubleOperations implements IOps { TIMES("*") { @Override public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { @Override public double apply(double x, double y) { return x / y; } }; private final String op; DoubleOperations(String op) { this.op = op; } public String getOp() { return op; } } public void check() { assertThat(TIMES.getOp()).isEqualTo("*"); assertThat(DIVIDE.getOp()).isEqualTo("/"); assertThat(TIMES.apply(2, 3)).isEqualTo(6); assertThat(DIVIDE.apply(10, 5)).isEqualTo(2); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("TIMES(\"*\") {") .containsOne("DIVIDE(\"/\")"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums3.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestEnums3 extends IntegrationTest { public static class TestCls { private static int three = 3; public enum Numbers { ONE(1), TWO(2), THREE(three), FOUR(three + 1); private final int num; Numbers(int n) { this.num = n; } public int getNum() { return num; } } public void check() { assertThat(Numbers.ONE.getNum()).isEqualTo(1); assertThat(Numbers.THREE.getNum()).isEqualTo(3); assertThat(Numbers.FOUR.getNum()).isEqualTo(4); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("ONE(1)") .containsOne("Numbers(int n) {"); // assertThat(code, containsOne("THREE(three)")); // assertThat(code, containsOne("assertTrue(Numbers.ONE.getNum() == 1);")); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums4.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnums4 extends IntegrationTest { public static class TestCls { public enum ResType { CODE(".dex", ".class"), MANIFEST("AndroidManifest.xml"), XML(".xml"), ARSC(".arsc"), FONT(".ttf"), IMG(".png", ".gif", ".jpg"), LIB(".so"), UNKNOWN; private final String[] exts; ResType(String... extensions) { this.exts = extensions; } public String[] getExts() { return exts; } } public void check() { assertThat(ResType.CODE.getExts()).containsExactly(new String[] { ".dex", ".class" }); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("CODE(\".dex\", \".class\"),") .containsOne("ResType(String... extensions) {"); // assertThat(code, not(containsString("private ResType"))); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums5.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnums5 extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmaliWithClsName("kotlin.collections.State")) .code() .containsLines( "enum State {", indent() + "Ready,", indent() + "NotReady,", indent() + "Done,", indent() + "Failed", "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums6.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestEnums6 extends IntegrationTest { public static class TestCls { public enum Numbers { ZERO, ONE(1); private final int n; Numbers() { this(0); } Numbers(int n) { this.n = n; } public int getN() { return n; } } public void check() { assertThat(TestCls.Numbers.ZERO.getN()).isEqualTo(0); assertThat(TestCls.Numbers.ONE.getN()).isEqualTo(1); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("ZERO,") .containsOne("Numbers() {") .containsOne("ONE(1);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums7.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnums7 extends IntegrationTest { public static class TestCls { public enum Numbers { ZERO, ONE; private final int n; Numbers() { this.n = this.name().equals("ZERO") ? 0 : 1; } public int getN() { return n; } } public void check() { assertThat(Numbers.ZERO.getN()).isEqualTo(0); assertThat(Numbers.ONE.getN()).isEqualTo(1); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("ZERO,") .containsOne("ONE;") .containsOne("Numbers() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums8.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnums8 extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("enum TestEnums8"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums9.java ================================================ package jadx.tests.integration.enums; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnums9 extends IntegrationTest { public static class TestCls { public enum Types { INT, FLOAT, LONG, DOUBLE, OBJECT, ARRAY; private static Set primitives = EnumSet.of(INT, FLOAT, LONG, DOUBLE); public static List references = new ArrayList<>(); static { references.add(OBJECT); references.add(ARRAY); } public static Set getPrimitives() { return primitives; } } public void check() { assertThat(Types.getPrimitives()).contains(Types.INT); assertThat(Types.references).hasSize(2); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("EnumSet.of((Enum) INT,"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsInterface.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.api.CommentsLevel; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnumsInterface extends IntegrationTest { public static class TestCls { public enum Operation implements IOperation { PLUS { @Override public int apply(int x, int y) { return x + y; } }, MINUS { @Override public int apply(int x, int y) { return x - y; } } } public interface IOperation { int apply(int x, int y); } } @Test public void test() { getArgs().setCommentsLevel(CommentsLevel.WARN); assertThat(getClassNode(TestCls.class)) .code() .containsLines(1, "public enum Operation implements IOperation {", indent(1) + "PLUS {", indent(2) + "@Override", indent(2) + "public int apply(int x, int y) {", indent(3) + "return x + y;", indent(2) + '}', indent(1) + "},", indent(1) + "MINUS {", indent(2) + "@Override", indent(2) + "public int apply(int x, int y) {", indent(3) + "return x - y;", indent(2) + '}', indent(1) + '}', "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsWithAssert.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnumsWithAssert extends IntegrationTest { public static class TestCls { public enum Numbers { ONE(1), TWO(2), THREE(3); private final int num; Numbers(int n) { this.num = n; } public int getNum() { assert num > 0; return num; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)).code() .containsOne("ONE(1)") .doesNotContain("Failed to restore enum class"); } @NotYetImplemented("handle java assert") @Test public void testNYI() { assertThat(getClassNode(TestCls.class)).code() .containsOne("assert num > 0;") .doesNotContain("$assertionsDisabled") .doesNotContain("throw new AssertionError()"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsWithConsts.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnumsWithConsts extends IntegrationTest { public static class TestCls { public static final int C1 = 1; public static final int C2 = 2; public static final int C4 = 4; public static final String S = "NORTH"; public enum Direction { NORTH, SOUTH, EAST, WEST } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsLines(1, "public enum Direction {", indent(1) + "NORTH,", indent(1) + "SOUTH,", indent(1) + "EAST,", indent(1) + "WEST", "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsWithCustomInit.java ================================================ package jadx.tests.integration.enums; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnumsWithCustomInit extends IntegrationTest { public enum TestCls { ONE("I"), TWO("II"), THREE("III"); public static final Map MAP = new HashMap<>(); static { for (TestCls value : values()) { MAP.put(value.toString(), value); } } private final String str; TestCls(String str) { this.str = str; } public String toString() { return str; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("ONE(\"I\"),") .doesNotContain("new TestEnumsWithCustomInit$TestCls("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsWithStaticFields.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnumsWithStaticFields extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOnlyOnce("INSTANCE;") .containsOnlyOnce("private static c sB") .doesNotContain(" sA") .doesNotContain(" sC") .doesNotContain("private TestEnumsWithStaticFields(String str) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsWithTernary.java ================================================ package jadx.tests.integration.enums; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEnumsWithTernary extends IntegrationTest { public enum TestCls { FIRST(useNumber() ? "1" : "A"), SECOND(useNumber() ? "2" : "B"), ANY(useNumber() ? "1" : "2"); private final String str; TestCls(String str) { this.str = str; } public String getStr() { return str; } public static boolean useNumber() { return false; } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J8 }) public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("ANY(useNumber() ? \"1\" : \"2\");") .doesNotContain("static {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestInnerEnums.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestInnerEnums extends IntegrationTest { public static class TestCls { public enum Numbers { ONE((byte) 1, NumString.ONE), TWO((byte) 2, NumString.TWO); private final byte num; private final NumString str; public enum NumString { ONE("one"), TWO("two"); private final String name; NumString(String name) { this.name = name; } public String getName() { return name; } } Numbers(byte n, NumString str) { this.num = n; this.str = str; } public int getNum() { return num; } public NumString getNumStr() { return str; } public String getName() { return str.getName(); } } public void check() { assertThat(Numbers.ONE.getNum()).isEqualTo(1); assertThat(Numbers.ONE.getNumStr()).isEqualTo(Numbers.NumString.ONE); assertThat(Numbers.ONE.getName()).isEqualTo("one"); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("ONE((byte) 1, NumString.ONE)") .containsOne("ONE(\"one\")"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchOverEnum extends SmaliTest { public enum Count { ONE, TWO, THREE } public int testEnum(Count c) { switch (c) { case ONE: return 1; case TWO: return 2; } return 0; } public void check() { assertThat(testEnum(Count.ONE)).isEqualTo(1); assertThat(testEnum(Count.TWO)).isEqualTo(2); assertThat(testEnum(Count.THREE)).isEqualTo(0); } @Test public void test() { // remapping array placed in top class, place test also in top class assertThat(getClassNode(TestSwitchOverEnum.class)) .code() .countString(1, "synthetic") .countString(2, "switch (c) {") .countString(3, "case ONE:"); } /** * Java 21 compiler can omit a remapping array and use switch over ordinal directly */ @Test public void testSmaliDirect() { assertThat(getClassNodeFromSmaliFiles()) .code() .containsOne("switch (v) {") .containsOne("case ONE:"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum2.java ================================================ package jadx.tests.integration.enums; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestSwitchOverEnum2 extends IntegrationTest { public enum Count { ONE, TWO, THREE } public enum Animal { CAT, DOG } public int testEnum(Count c, Animal a) { int result = 0; switch (c) { case ONE: result = 1; break; case TWO: result = 2; break; } switch (a) { case CAT: result += 10; break; case DOG: result += 20; break; } return result; } public void check() { assertThat(testEnum(Count.ONE, Animal.DOG)).isEqualTo(21); } @Test public void test() { assertThat(getClassNode(TestSwitchOverEnum2.class)) .code() .countString(1, "synthetic") .countString(2, "switch (c) {") .countString(2, "case ONE:") .countString(2, "case DOG:"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/fallback/TestFallbackManyNops.java ================================================ package jadx.tests.integration.fallback; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFallbackManyNops extends SmaliTest { @Test public void test() { setFallback(); disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .contains("public static void test() {") .containsOne("return") .doesNotContain("Method dump skipped"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/fallback/TestFallbackMode.java ================================================ package jadx.tests.integration.fallback; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestFallbackMode extends IntegrationTest { public static class TestCls { public int test(int a) { while (a < 10) { a++; } return a; } } @Test public void test() { useDexInput(); setFallback(); disableCompilation(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("public int test(int r2) {") .containsOne("r1 = this;") .containsOne("L0:") .containsOne("L7:") .containsOne("int r2 = r2 + 1") .doesNotContain("throw new UnsupportedOperationException"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestClassSignature.java ================================================ package jadx.tests.integration.generics; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestClassSignature extends SmaliTest { // @formatter:off /* Incorrect class signature, super class is equals to this class: Lgenerics/TestClassSignature; */ // @formatter:on @Test public void test() { allowWarnInCode(); assertThat(getClassNodeFromSmali()) .code() .containsOne("Incorrect class signature") .doesNotContain("StackOverflowError"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestConstructorGenerics.java ================================================ package jadx.tests.integration.generics; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConstructorGenerics extends IntegrationTest { @SuppressWarnings({ "MismatchedQueryAndUpdateOfCollection", "RedundantOperationOnEmptyContainer" }) public static class TestCls { public String test() { Map map = new HashMap<>(); return map.get("test"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("Map map = new HashMap<>();"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("return (String) new HashMap().get(\"test\");"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestGeneric8.java ================================================ package jadx.tests.integration.generics; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestGeneric8 extends IntegrationTest { public static class TestCls { @SuppressWarnings("InnerClassMayBeStatic") public class TestNumber { private final T n; public TestNumber(T n) { this.n = n; } public boolean isEven() { return n.intValue() % 2 == 0; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("public TestNumber(T n"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestGenericFields.java ================================================ package jadx.tests.integration.generics; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestGenericFields extends IntegrationTest { public static class TestCls { public static class Summary { Value price; } public static class Value { T value; } public static class Amount { String cur; int val; } public String test(Summary summary) { Amount amount = summary.price.value; return amount.val + " " + amount.cur; } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("T t = ") .containsOne("Amount amount ="); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics.java ================================================ package jadx.tests.integration.generics; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestGenerics extends IntegrationTest { public static class TestCls { class A { } public static void mthWildcard(List list) { } public static void mthExtends(List list) { } public static void mthSuper(List list) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("mthWildcard(List list)") .contains("mthExtends(List list)") .contains("mthSuper(List list)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics2.java ================================================ package jadx.tests.integration.generics; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestGenerics2 extends IntegrationTest { @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") public static class TestCls { public static class ItemReference extends WeakReference { public Object id; public ItemReference(V item, Object objId, ReferenceQueue queue) { super(item, queue); this.id = objId; } } public static class ItemReferences { private Map> items; public V get(Object id) { WeakReference ref = this.items.get(id); if (ref != null) { return ref.get(); } return null; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("public ItemReference(V item, Object objId, ReferenceQueue queue) {") .containsOne("public V get(Object id) {") .containsOne("WeakReference ref = ") .containsOne("return ref.get();"); } @Test public void testDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("ItemReference itemReference = this.items.get(obj);") .containsOne("return itemReference.get();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics3.java ================================================ package jadx.tests.integration.generics; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestGenerics3 extends IntegrationTest { public static class TestCls { public static void mthExtendsArray(List list) { } public static void mthSuperArray(List list) { } public static void mthSuperInteger(List list) { } public static void mthExtendsString(List list) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("mthExtendsArray(List list)") .contains("mthSuperArray(List list)") .contains("mthSuperInteger(List list)") .contains("mthExtendsString(List list)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics4.java ================================================ package jadx.tests.integration.generics; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestGenerics4 extends IntegrationTest { public static class TestCls { public static Class method(int i) { Class[] a = new Class[0]; return a[a.length - i]; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("Class[] a =") .doesNotContain("Class[] a ="); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics6.java ================================================ package jadx.tests.integration.generics; import java.util.Collection; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestGenerics6 extends IntegrationTest { public static class TestCls { public void test1(Collection as) { for (A a : as) { a.f(); } } public void test2(Collection is) { for (I i : is) { i.f(); } } private interface I { void f(); } private class A implements I { public void f() { } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("for (A a : as) {") .containsOne("for (I i : is) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics7.java ================================================ package jadx.tests.integration.generics; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestGenerics7 extends IntegrationTest { public static class TestCls { public void test() { declare(String.class); } public T declare(Class cls) { return null; } public void declare(Object cls) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("declare(String.class);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics8.java ================================================ package jadx.tests.integration.generics; import java.util.Iterator; import java.util.LinkedHashMap; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestGenerics8 extends IntegrationTest { @SuppressWarnings("IllegalType") public static class TestCls extends LinkedHashMap implements Iterable { @Override public Iterator iterator() { return keySet().iterator(); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("return keySet().iterator();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestGenericsInArgs.java ================================================ package jadx.tests.integration.generics; import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestGenericsInArgs extends IntegrationTest { public static class TestCls { public static void test(List genericList, Set set) { if (genericList == null) { throw new RuntimeException("list is null"); } if (set == null) { throw new RuntimeException("set is null"); } genericList.clear(); use(genericList); set.clear(); } private static void use(List l) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("public static void test(List genericList, Set set) {") .contains("if (genericList == null) {"); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("public static void test(List list, Set set) {") .contains("if (list == null) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestGenericsMthOverride.java ================================================ package jadx.tests.integration.generics; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestGenericsMthOverride extends IntegrationTest { public static class TestCls { public interface I { Y method(X x); } public static class A implements I { @Override public Y method(X x) { return null; } } public static class B implements I { @Override public Y method(Object x) { return null; } } public static class C implements I { @Override public Y method(Exception x) { return null; } } @SuppressWarnings("unchecked") public static class D implements I { @Override public Object method(Object x) { return null; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("public Y method(X x) {") .containsOne("public Y method(Object x) {") .containsOne("public Y method(Exception x) {") .containsOne("public Object method(Object x) {") .countString(4, "@Override"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestImportGenericMap.java ================================================ package jadx.tests.integration.generics; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.integration.generics.TestImportGenericMap.SuperClass.NotToImport; import static jadx.tests.integration.generics.TestImportGenericMap.SuperClass.ToImport; public class TestImportGenericMap extends IntegrationTest { public static class SuperClass { interface ToImport { } interface NotToImport { } static final class Class1 { } public SuperClass(Class1 zzf) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(SuperClass.class)) .code() .contains("import " + ToImport.class.getName().replace("$ToImport", ".ToImport") + ';') .doesNotContain("import " + NotToImport.class.getName().replace("NotToImport", ".NotToImport") + ';'); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestMethodOverride.java ================================================ package jadx.tests.integration.generics; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestMethodOverride extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOne("String createFromParcel(Parcel parcel) {") .containsOne("String[] newArray(int i) {") .countString(2, "@Override"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestMissingGenericsTypes2.java ================================================ package jadx.tests.integration.generics; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestMissingGenericsTypes2 extends SmaliTest { // @formatter:off /* package generics; import java.util.Iterator; public class TestMissingGenericsTypes2 implements Iterable { @Override public Iterator iterator() { return null; } public void test(TestMissingGenericsTypes2 l) { Iterator i = l.iterator(); // <-- This generics type was removed in smali while (i.hasNext()) { String s = i.next(); doSomething(s); } } private void doSomething(String s) { } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .doesNotContain("Iterator i") .containsOne("for (String s : l) {"); } @Test public void testTypes() { // prevent loop from converting to 'for-each' to keep iterator variable type in code getArgs().getDisabledPasses().add("LoopRegionVisitor"); assertThat(getClassNodeFromSmali()) .code() .doesNotContain("Iterator i") .containsOne("Iterator it = "); // variable name reject along with type } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestOuterGeneric.java ================================================ package jadx.tests.integration.generics; import java.util.Set; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestOuterGeneric extends IntegrationTest { public static class TestCls { public static class A { public class B { } public class C { } } public static class D { public class E { } } public void test1() { A a = new A<>(); use(a); A.B b = a.new B(); use(b); use(b); A.C c = a.new C(); use(c); use(c); use(new A>().new C()); } public void test2() { D d = new D(); D.E e = d.new E(); use(e); use(e); } public void test3() { use(A.class); use(A.B.class); use(A.C.class); } private void use(Object obj) { } } @NotYetImplemented("Instance constructor for inner classes") @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("A a = new A<>();") .containsOne("A.B b = a.new B();") .containsOne("A.C c = a.new C();") .containsOne("use(new A>().new C());") .containsOne("D.E e = d.new E();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestSyntheticOverride.java ================================================ package jadx.tests.integration.generics; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSyntheticOverride extends SmaliTest { // @formatter:off /* final class TestSyntheticOverride extends Lambda implements Function1 { // fixing method types to match interface (i.e Unit invoke(String str)) // make duplicate methods signatures public bridge synthetic Object invoke(Object str) { invoke(str); return Unit.INSTANCE; } public final void invoke(String str) { ... } } */ // @formatter:on @Test public void test() { allowWarnInCode(); disableCompilation(); List classNodes = loadFromSmaliFiles(); assertThat(searchCls(classNodes, "TestSyntheticOverride")) .code() .containsOne("invoke(String str)") .containsOne("invoke2(String str)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestTypeVarsFromOuterClass.java ================================================ package jadx.tests.integration.generics; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeVarsFromOuterClass extends IntegrationTest { public static class TestCls { public interface I { Map.Entry entry(); } public static class Outer { public class Inner implements I { @Override public Map.Entry entry() { return null; } } public Inner getInner() { return null; } } private Outer outer; public void test() { Outer.Inner inner = this.outer.getInner(); use(inner, inner); Map.Entry entry = inner.entry(); use(entry.getKey(), entry.getValue()); } public void test2() { // force interface virtual call I base = this.outer.getInner(); use(base, base); Map.Entry entry = base.entry(); use(entry.getKey(), entry.getValue()); } public void use(Object a, Object b) { } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("Outer.Inner inner") .doesNotContain("Object entry = ") .countString(2, "Outer.Inner inner = this.outer.getInner();") .countString(2, "Map.Entry entry = "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestTypeVarsFromSuperClass.java ================================================ package jadx.tests.integration.generics; import java.util.Objects; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeVarsFromSuperClass extends IntegrationTest { @SuppressWarnings("ResultOfMethodCallIgnored") public static class TestCls { public static class C1 { } public static class C2 extends C1 { public B call() { return null; } } public static class C3 extends C2 { } public static class C4 extends C3 { public Object test() { String str = call(); Objects.nonNull(str); return str; } } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("= call();") .doesNotContain("(String)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/generics/TestUsageInGenerics.java ================================================ package jadx.tests.integration.generics; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestUsageInGenerics extends IntegrationTest { public static class TestCls { public static class A { } public static class B { } public static class C { public List list; } public T test() { return null; } } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); ClassNode testCls = searchCls(cls.getInnerClasses(), "A"); ClassNode bCls = searchCls(cls.getInnerClasses(), "B"); ClassNode cCls = searchCls(cls.getInnerClasses(), "C"); MethodNode testMth = getMethod(cls, "test"); assertThat(testCls.getUseIn()).contains(cls, bCls, cCls); assertThat(testCls.getUseInMth()).contains(testMth); assertThat(cls) .code() .containsOne("public T test() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestConstInline.java ================================================ package jadx.tests.integration.inline; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestConstInline extends IntegrationTest { public static class TestCls { public boolean test() { try { return f(0); } catch (Exception e) { return false; } } public boolean f(int i) { return true; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("return f(0);") .containsOne("return false;") .doesNotContain(" = "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestGetterInlineNegative.java ================================================ package jadx.tests.integration.inline; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestGetterInlineNegative extends SmaliTest { // @formatter:off /* public class TestGetterInlineNegative { public static final String field = "some string"; public static synthetic String getter() { return field; } public void test() { getter(); // inline will produce 'field;' and fail to compile with 'not a statement' error } public String test2() { return getter(); } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .doesNotContain(indent() + "field;") .containsOne("return field;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestInline.java ================================================ package jadx.tests.integration.inline; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInline extends IntegrationTest { public static class TestCls { public static void main(String[] args) throws Exception { System.out.println("Test: " + new TestCls().testRun()); } private boolean testRun() { return false; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("System.out.println(\"Test: \" + new TestInline$TestCls().testRun());"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestInline2.java ================================================ package jadx.tests.integration.inline; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInline2 extends IntegrationTest { public static class TestCls { public int test() throws InterruptedException { int[] a = new int[] { 1, 2, 4, 6, 8 }; int b = 0; for (int i = 0; i < a.length; i += 2) { b += a[i]; } for (long i = b; i > 0; i--) { b += i; } return b; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("int[] a = {1, 2, 4, 6, 8};") .containsOne("for (int i = 0; i < a.length; i += 2) {") .containsOne("for (long i2 = b; i2 > 0; i2--) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestInline3.java ================================================ package jadx.tests.integration.inline; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInline3 extends IntegrationTest { public static class TestCls { public TestCls(int b1, int b2) { this(b1, b2, 0, 0, 0); } public TestCls(int a1, int a2, int a3, int a4, int a5) { } public class A extends TestCls { public A(int a) { super(a, a); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("this(b1, b2, 0, 0, 0);") .contains("super(a, a);") .doesNotContain("super(a, a).this$0") .contains("public class A extends TestInline3$TestCls {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestInline6.java ================================================ package jadx.tests.integration.inline; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInline6 extends IntegrationTest { public static class TestCls { public void f() { } public void test(int a, int b) { long start = System.nanoTime(); f(); System.out.println(System.nanoTime() - start); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("System.out.println(System.nanoTime() - start);") .doesNotContain("System.out.println(System.nanoTime() - System.nanoTime());"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestInline7.java ================================================ package jadx.tests.integration.inline; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInline7 extends SmaliTest { // @formatter:off /* public void onViewCreated(View view, @Nullable Bundle bundle) { super.onViewCreated(view, bundle); view.findViewById(R.id.done_button_early_release_failure).setOnClickListener(new SafeClickListener(this)); Bundle arguments = getArguments(); if (arguments != null) { ((TextView) view.findViewById(R.id.summary_content_early_release_failure)).setText( getString(R.string.withdraw_id_capture_failure_content, new Object[]{arguments.getString("withdrawAmount"), arguments.getString ("withdrawHoldTime")}) ); } } */ // @formatter:on @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmaliWithPkg("inline", "TestInline7")) .code() .doesNotContain("Bundle arguments;") .containsOne("Bundle arguments = getArguments();") .containsOne("@Nullable Bundle bundle"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestInstanceLambda.java ================================================ package jadx.tests.integration.inline; import java.util.List; import java.util.Map; import java.util.function.Function; import org.junit.jupiter.api.Test; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.visitors.ProcessAnonymous; import jadx.core.utils.ListUtils; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInstanceLambda extends SmaliTest { @SuppressWarnings({ "unchecked", "rawtypes", "SameParameterValue" }) public static class TestCls { public Map test(List list) { return toMap(list, Lambda$1.INSTANCE); } /** * Smali test missing 'T' definition in 'Lambda' * Note: use '$1' so class looks like generated by compiler and pass check in * {@link ProcessAnonymous#canBeAnonymous(ClassNode)} */ @SuppressWarnings({ "CheckStyle", "checkstyle:TypeName" }) private static class Lambda$1 implements Function { public static final Lambda$1 INSTANCE = new Lambda$1(); @Override public T apply(T t) { return t; } } private static Map toMap(List list, Function valueMap) { return null; } } @Test public void test() { useJavaInput(); noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code(); } @Test public void testSmaliDisableInline() { args.setInlineAnonymousClasses(false); List classNodes = loadFromSmaliFiles(); assertThat(searchTestCls(classNodes, "Lambda$1")) .code() .containsOne("class Lambda$1 implements Function {"); assertThat(searchTestCls(classNodes, "TestCls")) .code() .containsOne("Lambda$1.INSTANCE"); } @Test public void testSmali() { List classNodes = loadFromSmaliFiles(); assertThat(ListUtils.filter(classNodes, c -> !c.contains(AFlag.DONT_GENERATE))) .describedAs("Expect lambda to be inlined") .hasSize(1); assertThat(searchTestCls(classNodes, "TestCls")) .code() .doesNotContain("Lambda$1.INSTANCE") .containsOne("toMap(list, new Function() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestIssue86.java ================================================ package jadx.tests.integration.inline; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; @SuppressWarnings("checkstyle:printstacktrace") public class TestIssue86 extends IntegrationTest { public static class TestCls { private static final String SERVER_ERR = "server-err"; private static final String NOT_FOUND = "not-found"; private static final String LIST_TAG = "list-tag"; private static final String TEMP_TAG = "temp-tag"; private static final String MIN_TAG = "min-tag"; private static final String MAX_TAG = "max-tag"; private static final String MILLIS_TAG = "millis-tag"; private static final String WEATHER_TAG = "weather-tag"; private static final String DESC_TAG = "desc-tag"; public List test(String response) { List reportList = new ArrayList<>(); try { System.out.println(response); if (response != null && (response.startsWith(SERVER_ERR) || response.startsWith(NOT_FOUND))) { return reportList; } JSONObject jsonObj = new JSONObject(response); JSONArray days = jsonObj.getJSONArray(LIST_TAG); for (int i = 0; i < days.length(); i++) { JSONObject c = days.getJSONObject(i); long millis = c.getLong(MILLIS_TAG); JSONObject temp = c.getJSONObject(TEMP_TAG); String max = temp.getString(MAX_TAG); String min = temp.getString(MIN_TAG); JSONArray weather = c.getJSONArray(WEATHER_TAG); String weatherDesc = weather.getJSONObject(0).getString(DESC_TAG); Day d = new Day(); d.setMilis(millis); d.setMinTmp(min); d.setMaxTmp(max); d.setWeatherDesc(weatherDesc); reportList.add(d); } } catch (JSONException e) { e.printStackTrace(); } return reportList; } private static class Day { public void setMilis(long milis) { } public void setMinTmp(String min) { } public void setMaxTmp(String max) { } public void setWeatherDesc(String weatherDesc) { } } private static class JSONObject { public JSONObject(String response) { } public JSONArray getJSONArray(String tag) throws JSONException { return null; } public JSONObject getJSONObject(String tag) throws JSONException { return null; } public String getString(String tag) throws JSONException { return null; } public long getLong(String tag) throws JSONException { return 0; } } private class JSONArray { public JSONObject getJSONObject(int i) throws JSONException { return null; } public int length() { return 0; } } private class JSONException extends Exception { private static final long serialVersionUID = -4358405506584551910L; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("response.startsWith(NOT_FOUND)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestMethodInline.java ================================================ package jadx.tests.integration.inline; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestMethodInline extends SmaliTest { // @formatter:off /* package inline; public class A { public static void useMth() { inline.other.B.bridgeMth(); // after inline 'inline.other.C.test()' is not accessible } } ----------------------------------------------------------- package inline.other; public class B { public static bridge synthetic void bridgeMth() { inline.other.C.test(); } } ---------------------------------------------------------- package inline.other; class C { public static void test() { } } */ // @formatter:on @Test public void test() { List classes = loadFromSmaliFiles(); ClassNode aCls = searchCls(classes, "inline.A"); ClassNode bCls = searchCls(classes, "inline.other.B"); ClassNode cCls = searchCls(classes, "inline.other.C"); assertThat(bCls).code().doesNotContain("bridgeMth()"); assertThat(aCls).code().containsOne("C.test()"); assertThat(cCls).code().containsOne("public class C {"); // TODO: update dependencies? // assertThat(aCls.getDependencies()).contains(cCls); // assertThat(cCls.getUsedIn()).contains(aCls); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestOverlapSyntheticMethods.java ================================================ package jadx.tests.integration.inline; import java.util.Collections; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestOverlapSyntheticMethods extends SmaliTest { // @formatter:off /* public String test(int i) { return a(i) + "|" + a(i); } public int a(int i) { return i; } public String a(int i) { return "i:" + i; } */ // @formatter:on @Test public void testSmali() { assertThat(getClassNodeFromSmali()) .code() .containsOne("int a(int i) {") .containsOne("String m0a(int i) {"); } @Test public void testSmaliNoRename() { getArgs().setRenameFlags(Collections.emptySet()); disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOne("int a(int i) {") .containsOne("String a(int i) {") .containsOne("return a(i) + \"|\" + a(i);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestOverrideBridgeMerge.java ================================================ package jadx.tests.integration.inline; import java.util.function.Function; import org.junit.jupiter.api.Test; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestOverrideBridgeMerge extends SmaliTest { public static class TestCls implements Function { @Override public /* bridge */ /* synthetic */ Integer apply(String str) { return test(str); } public Integer test(String str) { return str.length(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("Integer test(String str) {"); // not inlined } @Test public void testSmali() { ClassNode cls = getClassNodeFromSmali(); ICodeAnnotation mthDef = new NodeDeclareRef(getMethod(cls, "apply")); assertThat(cls) .checkCodeAnnotationFor("apply(String str) {", mthDef) .code() .containsOne("@Override") .containsOne("public Integer apply(String str) {") .doesNotContain("test(String str)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestSyntheticBridgeRename.java ================================================ package jadx.tests.integration.inline; import org.assertj.core.api.Condition; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSyntheticBridgeRename extends IntegrationTest { @SuppressWarnings("InnerClassMayBeStatic") public static class TestCls { private abstract class Inner { public abstract V get(String value); } public class IntInner extends Inner { public Integer get(String value) { return value.length(); } } public void test() { IntInner inner = new IntInner(); call(inner.get("a")); } private static void call(Integer value) { } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(searchCls(cls.getInnerClasses(), "IntInner").getMethods()) .as("check that bridge method was generated by compiler") .haveAtLeastOne(new Condition<>(mth -> mth.getAccessFlags().isBridge(), "bridge")); assertThat(cls) .code() .doesNotContain("mo0get") .containsOne("call(inner.get(\"a\"));"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestSyntheticClassInline.java ================================================ package jadx.tests.integration.inline; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSyntheticClassInline extends SmaliTest { @Test public void test() { List classes = loadFromSmaliFiles(); assertThat(searchCls(classes, "inline.A")) .code() .containsOne("static Supplier test(final long x1, final long x2) {") .containsOne("return new Supplier() {") .containsOne("return A.lambda$test$0(x1, x2);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestSyntheticInline.java ================================================ package jadx.tests.integration.inline; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSyntheticInline extends IntegrationTest { public static class TestCls { private int f; private int func() { return -1; } public class A { public int getF() { return f; } public void setF(int v) { f = v; } public int callFunc() { return func(); } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("synthetic") .doesNotContain("access$") .doesNotContain("x0") .contains("f = v;") .contains("return TestSyntheticInline$TestCls.this.f;") // .contains("return f;"); // .contains("return func();"); // Temporary solution .contains("return TestSyntheticInline$TestCls.this.func();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestSyntheticInline2.java ================================================ package jadx.tests.integration.inline; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSyntheticInline2 extends IntegrationTest { public static class Base { protected void call() { System.out.println("base call"); } } public static class TestCls extends Base { public class A { public void invokeCall() { TestCls.this.call(); } public void invokeSuperCall() { TestCls.super.call(); } } @Override public void call() { System.out.println("TestCls call"); } public void check() { A a = new A(); a.invokeSuperCall(); a.invokeCall(); } } @Test public void test() { disableCompilation(); // strange java compiler bug assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("synthetic") .doesNotContain("access$").contains("TestSyntheticInline2$TestCls.this.call();") .contains("TestSyntheticInline2$TestCls.super.call();"); } @Test public void testTopClass() { JadxAssertions.assertThat(getClassNode(TestSyntheticInline2.class)) .code() .contains(indent(1) + "TestCls.super.call();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestSyntheticInline3.java ================================================ package jadx.tests.integration.inline; import java.util.function.Function; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSyntheticInline3 extends SmaliTest { @SuppressWarnings({ "Convert2Lambda", "TrivialFunctionalExpressionUsage" }) public static class TestCls { private String strField; private String str() { return "a"; } private void test() { new Function() { @Override public Void apply(String s) { System.out.println(s + strField + str()); return null; } }.apply("c"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code(); } @Test public void testSmali() { allowWarnInCode(); disableCompilation(); assertThat(getClassNodeFromSmaliFiles()) .code() .doesNotContain(".access$getDialog$p(") .doesNotContain(".access$getChooserIntent(") .doesNotContain("= r1;") .doesNotContain("this$0"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inline/TestTernaryCast.java ================================================ package jadx.tests.integration.inline; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTernaryCast extends IntegrationTest { public static class TestCls { public String test(boolean b, Object obj, CharSequence cs) { return (String) (b ? obj : cs); } public void check() { assertThat(test(true, "a", "b")).isEqualTo("a"); assertThat(test(false, "a", "b")).isEqualTo("b"); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("return (String) (b ? obj : cs);"); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass.java ================================================ package jadx.tests.integration.inner; import java.io.File; import java.io.FilenameFilter; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass extends IntegrationTest { public static class TestCls { public int test() { String[] files = new File("a").list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.equals("a"); } }); return files.length; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .contains("new File(\"a\").list(new FilenameFilter()") .doesNotContain("synthetic") .doesNotContain("this") .doesNotContain("null") .doesNotContain("AnonymousClass_") .doesNotContain("class AnonymousClass"); } @Test public void testNoInline() { getArgs().setInlineAnonymousClasses(false); assertThat(getClassNode(TestCls.class)) .code() .contains("class AnonymousClass1 implements FilenameFilter {") .containsOne("new AnonymousClass1()"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass10.java ================================================ package jadx.tests.integration.inner; import java.util.Random; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestAnonymousClass10 extends IntegrationTest { public static class TestCls { public A test() { Random random = new Random(); int a2 = random.nextInt(); return new A(this, a2, a2 + 3, 4, 5, random.nextDouble()) { @Override public void m() { System.out.println(1); } }; } public abstract class A { public A(TestCls a1, int a2, int a3, int a4, int a5, double a6) { } public abstract void m(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("return new A(this, a2, a2 + 3, 4, 5, random.nextDouble()) {") .doesNotContain("synthetic"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass11.java ================================================ package jadx.tests.integration.inner; import java.util.Random; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestAnonymousClass11 extends IntegrationTest { public static class TestCls { public void test() { final int a = new Random().nextInt(); final long l = new Random().nextLong(); func(new A(l) { @Override public void m() { System.out.println(a); } }); System.out.println("a" + a); print(a); print2(1, a); print3(1, l); } public abstract class A { public A(long l) { } public abstract void m(); } private void func(A a) { } private void print(int a) { } private void print2(int i, int a) { } private void print3(int i, long l) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("System.out.println(\"a\" + a);") .containsOne("print(a);") .doesNotContain("synthetic"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass12.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestAnonymousClass12 extends IntegrationTest { public static class TestCls { public abstract static class BasicAbstract { public abstract void doSomething(); } public BasicAbstract outer; public BasicAbstract inner; public void test() { outer = new BasicAbstract() { @Override public void doSomething() { inner = new BasicAbstract() { @Override public void doSomething() { inner = null; } }; } }; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("outer = new BasicAbstract() {") .containsOne("inner = new BasicAbstract() {") .containsOne("inner = null;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass13.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; public class TestAnonymousClass13 extends IntegrationTest { public static class TestCls { public void test() { new TestCls() { }; } } @Test public void test() { getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass14.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.api.CommentsLevel; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.ListUtils; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass14 extends SmaliTest { // @formatter:off /* public class OuterCls implements Runnable { class TestCls { private TestCls() { new ArrayList(); } } public void makeAnonymousCls() { use(new Thread(this) { / * class inner.OuterCls.AnonymousClass1 * / public void someMethod() { } }); } public void makeTestCls() { new TestCls(); } public void run() { } public void use(Thread thread) { } } */ // @formatter:on @Test public void test() { getArgs().setCommentsLevel(CommentsLevel.WARN); ClassNode outerCls = getClassNodeFromSmaliFiles("OuterCls"); assertThat(outerCls).code() .doesNotContain("synthetic", "AnonymousClass1") .describedAs("only one constructor").containsOne("private TestCls(") .describedAs("constructor without args").containsOne("private TestCls() {"); MethodNode makeTestClsMth = outerCls.searchMethodByShortName("makeTestCls"); assertThat(makeTestClsMth).isNotNull(); ClassNode testCls = searchCls(outerCls.getInnerClasses(), "TestCls"); MethodNode ctrMth = ListUtils.filterOnlyOne(testCls.getMethods(), m -> m.isConstructor() && !m.getAccessFlags().isSynthetic()); assertThat(ctrMth).isNotNull(); assertThat(ctrMth.getUseIn()).hasSize(1); assertThat(ctrMth.getUseIn().get(0)).isEqualTo(makeTestClsMth); assertThat(outerCls).checkCodeAnnotationFor("new TestCls();", 4, ctrMth); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass15.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass15 extends IntegrationTest { public static class TestCls { public Thread test(Runnable run) { return new Thread(run) { @Override public void run() { System.out.println("run"); super.run(); } }; } public Thread test2(Runnable run) { return new Thread(run) { { setName("run"); } @Override public void run() { } }; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(2, "return new Thread(run) {") .containsOne("setName(\"run\");"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass16.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.api.CommentsLevel; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass16 extends IntegrationTest { public static class TestCls { public Something test() { Something a = new Something() { { put("a", "b"); } }; a.put("c", "d"); return a; } public class Something { public void put(Object o, Object o2) { } } } @Test public void test() { getArgs().setCommentsLevel(CommentsLevel.NONE); noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("r0") .doesNotContain("AnonymousClass1 r0 = ") .containsLines(2, "Something something = new Something() {", indent() + "{", indent(2) + "put(\"a\", \"b\");", indent() + "}", "};"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass17.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass17 extends IntegrationTest { public static class TestCls { @SuppressWarnings({ "checkstyle:InnerAssignment", "Convert2Lambda" }) public void test(boolean a, boolean b) { String v; if (a && (v = get(b)) != null) { use(new Runnable() { @Override public void run() { System.out.println(v); } }); } } public String get(boolean a) { return a ? "str" : null; } public void use(Runnable r) { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (a && (v = get(b)) != null) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass18.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.api.CommentsLevel; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass18 extends IntegrationTest { @SuppressWarnings({ "Convert2Lambda", "Anonymous2MethodRef", "unused" }) public static class TestCls { public interface Job { void executeJob(); } public void start() { runJob(new Job() { @Override public void executeJob() { runJob(new Job() { @Override public void executeJob() { doSomething(); } }); } private void doSomething() { } }); } public static void runJob(Job job) { } } @Test public void test() { getArgs().setCommentsLevel(CommentsLevel.WARN); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("AnonymousClass1.this") .doesNotContain("class AnonymousClass1") // .doesNotContain("TestAnonymousClass18$TestCls.runJob(") // TODO: ??? .containsOne(indent() + "doSomething();"); } @Test public void testNoInline() { getArgs().setInlineAnonymousClasses(false); assertThat(getClassNode(TestCls.class)) .code() .containsOne("class AnonymousClass1 implements Job {") .containsOne("class C00001 implements Job {") .containsOne("AnonymousClass1.this.doSomething();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass19.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass19 extends SmaliTest { @SuppressWarnings({ "Convert2Lambda", "unused" }) public static class TestCls { public void test(boolean a, boolean b) { boolean c = a && b; use(new Runnable() { @Override public void run() { System.out.println(a + " && " + b + " = " + c); } }); } public void use(Runnable r) { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("System.out.println(a + \" && \" + b + \" = \" + c);"); } @Test public void testSmali() { assertThat(getClassNodeFromSmaliFiles("ATestCls")) .code() .containsOne("System.out.println(a + \" && \" + b + \" = \" + c);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass2.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass2 extends IntegrationTest { public static class TestCls { public static class Inner { public int f; public Runnable test() { return new Runnable() { @Override public void run() { f = 1; } }; } public Runnable test2() { return new Runnable() { @Override @SuppressWarnings("unused") public void run() { Object obj = Inner.this; } }; } public Runnable test3() { final int i = f + 2; return new Runnable() { @Override public void run() { f = i; } }; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("synthetic") .doesNotContain("AnonymousClass_") .contains("f = 1;") .contains("f = i;") .doesNotContain("Inner obj = ;") .contains("Inner.this;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass20.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.api.JadxInternalAccess; import jadx.api.JavaClass; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass20 extends IntegrationTest { @SuppressWarnings({ "unused", "checkstyle:TypeName", "Convert2Lambda", "Anonymous2MethodRef" }) public static class Test$Cls { public Runnable test() { return new Runnable() { @Override public void run() { new Test$Cls(); } }; } } @Test public void test() { ClassNode cls = getClassNode(Test$Cls.class); assertThat(cls.get(AType.ANONYMOUS_CLASS)).isNull(); JavaClass javaClass = JadxInternalAccess.convertClassNode(jadxDecompiler, cls); assertThat(javaClass.getTopParentClass()).isEqualTo(javaClass); assertThat(cls) .code() .containsOne("new TestAnonymousClass20$Test$Cls();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass21.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Don't inline const string into anonymous class constructor */ public class TestAnonymousClass21 extends IntegrationTest { @SuppressWarnings("Convert2Lambda") public static class TestCls { public void test() { String str = "str"; new Thread(new Runnable() { @Override public void run() { System.out.println(str); } }).start(); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("String str = \"str\";") .containsOne("System.out.println(str);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass22.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass22 extends IntegrationTest { public static class TestCls { public static class Parent { public static Parent test(Class cls) { final AnotherClass another = new AnotherClass(); return new Parent() { @Override public String func() { return another.toString(); } }; } public String func() { return ""; } } public static class AnotherClass { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return another.toString();") .doesNotContain("AnotherClass.this"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass3.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass3 extends IntegrationTest { public static class TestCls { public static class Inner { private int f; public double d; public void test() { new Thread() { @Override public void run() { int a = f--; p(a); f += 2; f *= 2; a = ++f; p(a); d /= 3; } public void p(int a) { } }.start(); } } } @Test public void test() { disableCompilation(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains(indent(4) + "public void run() {") .contains(indent(3) + "}.start();") .doesNotContain("AnonymousClass_"); } @Test @NotYetImplemented public void test2() { disableCompilation(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("synthetic") .contains("a = f--;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass3a.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.api.CommentsLevel; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass3a extends IntegrationTest { public static class TestCls { public static class Inner { private int f; private int r; public void test() { new Runnable() { @Override public void run() { int a = --Inner.this.f; p(a); } public void p(int a) { Inner.this.r = a; } }.run(); } } public void check() { Inner inner = new Inner(); inner.f = 2; inner.test(); assertThat(inner.f).isEqualTo(1); assertThat(inner.r).isEqualTo(1); } } @Test @NotYetImplemented public void test() { getArgs().setCommentsLevel(CommentsLevel.NONE); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("synthetic") .doesNotContain("access$00") .doesNotContain("AnonymousClass_") .doesNotContain("unused = ") .containsLine(4, "public void run() {") .containsLine(3, "}.run();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass4.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestAnonymousClass4 extends IntegrationTest { public static class TestCls { @SuppressWarnings("unused") public static class Inner { private int f; private double d; public void test() { new Thread() { { f = 1; } @Override public void run() { d = 7.5; } }.start(); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne(indent(3) + "new Thread() {") .containsOne(indent(4) + '{') .containsOne("f = 1;") .countString(2, indent(4) + '}') .containsOne(indent(4) + "public void run() {") .containsOne("d = 7.5") .containsOne(indent(3) + "}.start();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass5.java ================================================ package jadx.tests.integration.inner; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousClass5 extends IntegrationTest { public static class TestCls { private final Map map = new HashMap<>(); private int a; public Iterable test(String name) { final TestCls cls = map.get(name); if (cls == null) { return null; } final int listSize = cls.size(); final Iterator iterator = new Iterator() { int counter = 0; @Override public TestCls next() { cls.a++; counter++; return cls; } @Override public boolean hasNext() { return counter < listSize; } @Override public void remove() { throw new UnsupportedOperationException(); } }; return new Iterable() { @Override public Iterator iterator() { return iterator; } }; } private int size() { return 7; } public void check() { TestCls v = new TestCls(); v.a = 3; map.put("a", v); Iterable it = test("a"); TestCls next = it.iterator().next(); assertThat(next).isSameAs(v); assertThat(next.a).isEqualTo(4); } } @Test @NotYetImplemented public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("map.get(name);") .doesNotContain("access$008") .doesNotContain("synthetic"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass6.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestAnonymousClass6 extends IntegrationTest { public static class TestCls { public Runnable test(final double d) { return new Runnable() { public void run() { System.out.println(d); } }; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("public Runnable test(final double d) {") .containsOne("return new Runnable() {") .containsOne("public void run() {") .containsOne("System.out.println(d);") .doesNotContain("synthetic"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass7.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestAnonymousClass7 extends IntegrationTest { public static class TestCls { public static Runnable test(final double d) { return new Runnable() { public void run() { System.out.println(d); } }; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("public static Runnable test(final double d) {") .containsOne("return new Runnable() {") .containsOne("public void run() {") .containsOne("System.out.println(d);") .doesNotContain("synthetic"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass8.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestAnonymousClass8 extends IntegrationTest { public static class TestCls { public final double d = Math.abs(4); public Runnable test() { return new Runnable() { public void run() { System.out.println(d); } }; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("public Runnable test() {") .containsOne("return new Runnable() {") .containsOne("public void run() {") .containsOne("this.d);") .doesNotContain("synthetic"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass9.java ================================================ package jadx.tests.integration.inner; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestAnonymousClass9 extends IntegrationTest { public static class TestCls { public Callable c = new Callable() { @Override public String call() throws Exception { return "str"; } }; public Runnable test() { return new FutureTask(this.c) { public void run() { System.out.println(6); } }; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("c = new Callable() {") .containsOne("return new FutureTask(this.c) {") .doesNotContain("synthetic"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestIncorrectAnonymousClass.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIncorrectAnonymousClass extends SmaliTest { // @formatter:off /* public static class TestCls { public final class 1 { public void invoke() { new 1(); // cause infinite self inline } } public void test() { new 1(); } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmaliFiles("TestCls")) .code() .containsOne("public final class AnonymousClass1 {") .countString(2, "new AnonymousClass1();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestInner2Samples.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInner2Samples extends IntegrationTest { public static class TestInner2 { private String a; public class A { public A() { a = "a"; } public String a() { return a; } } private static String b; public static class B { public B() { b = "b"; } public String b() { return b; } } private String c; private void setC(String c) { this.c = c; } public class C { public String c() { setC("c"); return c; } } private static String d; private static void setD(String s) { d = s; } public static class D { public String d() { setD("d"); return d; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestInner2.class)) .code() .containsOne("setD(\"d\");") .doesNotContain("synthetic") .doesNotContain("access$"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClass.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInnerClass extends IntegrationTest { public static class TestCls { public class Inner { public class Inner2 extends Thread { } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("Inner {") .contains("Inner2 extends Thread {") .doesNotContain("super();") .doesNotContain("this$") .doesNotContain("/* synthetic */"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClass2.java ================================================ package jadx.tests.integration.inner; import java.util.Timer; import java.util.TimerTask; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInnerClass2 extends IntegrationTest { public static class TestCls { private static class TerminateTask extends TimerTask { @Override public void run() { System.err.println("Test timed out"); } } public void test() { new Timer().schedule(new TerminateTask(), 1000L); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("new Timer().schedule(new TerminateTask(), 1000L);") .doesNotContain("synthetic") .doesNotContain("this") .doesNotContain("null") .doesNotContain("AnonymousClass"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClass3.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInnerClass3 extends IntegrationTest { public static class TestCls { private String c; private void setC(String c) { this.c = c; } public class C { public String c() { setC("c"); return c; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("synthetic") .doesNotContain("access$") .doesNotContain("x0") .contains("setC(\"c\");"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClass4.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInnerClass4 extends IntegrationTest { public static class TestCls { public class C { public String c; private C() { this.c = "c"; } } public String test() { return new C().c; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("return new C().c;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClass5.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestInnerClass5 extends IntegrationTest { public static class TestCls { private String i0; public class A { protected String a; public A() { a = ""; } public String a() { return ""; } } public class I0 { private String i0; private String i1; public class I1 { private String i0; private String i1; private String i2; public I1() { TestCls.this.i0 = "i0"; I0.this.i0 = "i1"; I0.this.i1 = "i2"; i0 = "i0"; i1 = "i1"; i2 = "i2"; } public String i() { String result = TestCls.this.i0 + I0.this.i0 + I0.this.i1 + i0 + i1 + i2; A a = new A() { public String a() { TestCls.this.i0 = "i1"; I0.this.i0 = "i2"; I0.this.i1 = "i3"; I1.this.i0 = "i1"; I1.this.i1 = "i2"; I1.this.i2 = "i3"; a = "a"; return TestCls.this.i0 + I0.this.i0 + I0.this.i1 + I1.this.i0 + I1.this.i1 + I1.this.i2 + a; } }; return result + a.a(); } } public I0() { TestCls.this.i0 = "i-"; i0 = "i0"; i1 = "i1"; } public String i() { String result = TestCls.this.i0 + i0 + i1; return result + new I1().i(); } } public void check() throws Exception { assertThat(new I0().i()).isEqualTo("i-i0i1i0i1i2i0i1i2i1i2i3i1i2i3a"); assertThat(i0).isEqualTo("i1"); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("public class I0 {") .containsOne("public class I1 {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClassFakeSyntheticConstructor.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInnerClassFakeSyntheticConstructor extends SmaliTest { // @formatter:off /* public class TestCls { public synthetic TestCls(String a) { this(a, true); } public TestCls(String a, boolean b) { } public static TestCls build(String str) { return new TestCls(str); } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali("inner/TestInnerClassFakeSyntheticConstructor", "jadx.tests.inner.TestCls")) .code() .containsOne("TestCls(String a) {"); // and must compile } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClassSyntheticConstructor.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; public class TestInnerClassSyntheticConstructor extends IntegrationTest { private class TestCls { private int mth() { return 1; } } public int call() { return new TestCls().mth(); } @Test public void test() { getClassNode(TestInnerClassSyntheticConstructor.class); // must compile, no usage of removed synthetic empty class } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClassSyntheticRename.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue: #336 */ @SuppressWarnings("CommentedOutCode") public class TestInnerClassSyntheticRename extends SmaliTest { // @formatter:off /* private class TestCls extends AsyncTask> { @Override protected List doInBackground(Uri... uris) { Log.i("MyAsync", "doInBackground"); return null; } @Override protected void onPostExecute(List uris) { Log.i("MyAsync", "onPostExecute"); } } */ // @formatter:on @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOne("List doInBackground(Uri... uriArr) {") .containsOne("void onPostExecute(List list) {") .doesNotContain("synthetic"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerConstructorCall.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInnerConstructorCall extends IntegrationTest { public static class TestCls { @SuppressWarnings("InnerClassMayBeStatic") public class A { public class AA { public void test() { } } } public void test() { A a = new A(); A.AA aa = a.new AA(); aa.test(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("A.AA aa = a.new AA();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestNestedAnonymousClass.java ================================================ package jadx.tests.integration.inner; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNestedAnonymousClass extends SmaliTest { @SuppressWarnings("Convert2Lambda") public static class TestCls { public void test() { use(new Callable() { @Override public Runnable call() { return new Runnable() { @Override public void run() { System.out.println("run"); } }; } }); } public void testLambda() { use(() -> () -> System.out.println("lambda")); } public void use(Callable r) { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("use(new Callable() {") .containsOne("return new Runnable() {"); } @Test public void testSmali() { getArgs().setRenameFlags(Collections.emptySet()); List classes = loadFromSmaliFiles(); assertThat(searchCls(classes, "A")) .code() .containsOne("use(new Callable() {") .containsOne("return new Runnable() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestOuterConstructorCall.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.api.CommentsLevel; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestOuterConstructorCall extends IntegrationTest { @SuppressWarnings({ "InnerClassMayBeStatic", "unused" }) public static class TestCls { private TestCls(Inner inner) { System.out.println(inner); } private class Inner { private TestCls test() { return new TestCls(this); } } } @Test public void test() { getArgs().setCommentsLevel(CommentsLevel.WARN); assertThat(getClassNode(TestCls.class)) .code() .containsOne("class Inner {") .containsOne("return new TestOuterConstructorCall$TestCls(this);") .doesNotContain("synthetic", "this$0"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestReplaceConstsInAnnotations.java ================================================ package jadx.tests.integration.inner; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestReplaceConstsInAnnotations extends IntegrationTest { public static class TestCls { @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface A { int i(); float f(); } @A(i = -1, f = C.FLOAT_CONST) public static class C { public static final float FLOAT_CONST = 3.14f; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOnlyOnce("f = C.FLOAT_CONST"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestReplaceConstsInAnnotations2.java ================================================ package jadx.tests.integration.inner; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestReplaceConstsInAnnotations2 extends IntegrationTest { public static class TestCls { @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface A { int[] value(); } @A(C.INT_CONST) public static class C { public static final int INT_CONST = 23412342; } @A({ C.INT_CONST, C2.INT_CONST }) public static class C2 { public static final int INT_CONST = 34563456; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() // .containsOne("@A(C.INT_CONST)") // TODO: remove brackets for single element .containsOne("@A({C.INT_CONST}") .containsOne("@A({C.INT_CONST, C2.INT_CONST})") .containsOne("23412342") .containsOne("34563456"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/inner/TestSyntheticMthRename.java ================================================ package jadx.tests.integration.inner; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue: https://github.com/skylot/jadx/issues/397 */ public class TestSyntheticMthRename extends SmaliTest { // @formatter:off /* public class TestCls { public interface I { R call(P... p); } public static final class A implements I { public synthetic virtual Object call(Object[] objArr) { return renamedCall((Runnable[]) objArr); } private varargs direct String renamedCall(Runnable... p) { return "str"; } } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmaliFiles("inner", "TestSyntheticMthRename", "TestCls")) .code() .containsOne("public String call(Runnable... p) {") .doesNotContain("synthetic"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedAccessor.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCastInOverloadedAccessor extends SmaliTest { static class X { void test() { new Runnable() { @Override public void run() { outerMethod(""); outerMethod("", ""); } }; } private void outerMethod(String s) { } private void outerMethod(String s, String t) { } private void outerMethod(int a) { } private void outerMethod(int a, int b) { } } @Test public void test() { assertThat(getClassNode(X.class)) .code() .containsOne("outerMethod(\"\")") .containsOne("outerMethod(\"\", \"\")"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke.java ================================================ package jadx.tests.integration.invoke; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestCastInOverloadedInvoke extends IntegrationTest { @SuppressWarnings("IllegalType") public static class TestCls { int c = 0; public void test() { call(new ArrayList<>()); call((List) new ArrayList()); } public void test2(Object obj) { if (obj instanceof String) { call((String) obj); } } public void test3() { call((String) null); call((List) null); call((ArrayList) null); } public void call(String str) { c += 1; } public void call(List list) { c += 10; } public void call(ArrayList list) { c += 100; } public void check() { test(); assertThat(c).isEqualTo(10 + 100); c = 0; test2("str"); assertThat(c).isEqualTo(1); c = 0; test3(); assertThat(c).isEqualTo(111); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("call(new ArrayList<>());") .containsOne("call((List) new ArrayList());") .containsOne("call((String) obj);"); } @NotYetImplemented @Test public void testNYI() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("call((List) new ArrayList());"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke2.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCastInOverloadedInvoke2 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOne("new Intent().putExtra(\"param\", (Parcelable) null);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke3.java ================================================ package jadx.tests.integration.invoke; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Test cast for 'unknown' but overloaded method */ public class TestCastInOverloadedInvoke3 extends IntegrationTest { public static class OuterCls { static int c = 0; public static void call(String str) { c = 1; } public static void call(List list) { c = 10; } } public static class TestCls { public void test() { OuterCls.call((String) null); } } @Test public void test() { disableCompilation(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("OuterCls.call((String) null);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke4.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCastInOverloadedInvoke4 extends IntegrationTest { public static class TestCls { public String test(String str) { return str.replace('\n', ' '); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("return str.replace('\\n', ' ');"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestConstructorWithMoves.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConstructorWithMoves extends SmaliTest { // @formatter:off /* public boolean test() { java.lang.Boolean r5 = new java.lang.Boolean r8 = r5 r5 = r8 r6 = r8 java.lang.String r7 = "test" r6.(r7) java.lang.Boolean r5 = (java.lang.Boolean) r5 boolean r5 = r5.booleanValue() r3 = r5 return r3 } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("return new Boolean(\"test\").booleanValue();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestHierarchyOverloadedInvoke.java ================================================ package jadx.tests.integration.invoke; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestHierarchyOverloadedInvoke extends IntegrationTest { @SuppressWarnings("IllegalType") public static class TestCls { static int c = 0; B b = new B(); public interface I { default void call(String str) { c += 1; } } public static class A implements I { public void call(List list) { c += 10; } } public static class B extends A { public void call(ArrayList list) { c += 100; } } public void test() { b.call(new ArrayList<>()); b.call((List) new ArrayList()); } public void test2(Object obj) { if (obj instanceof String) { b.call((String) obj); } } public void test3() { b.call((String) null); b.call((List) null); b.call((ArrayList) null); } public void test4() { ((I) b).call(null); ((A) b).call((String) null); ((A) b).call((List) null); } public void check() { test(); assertThat(c).isEqualTo(10 + 100); c = 0; test2("str"); assertThat(c).isEqualTo(1); c = 0; test3(); assertThat(c).isEqualTo(111); c = 0; test4(); assertThat(c).isEqualTo(12); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("b.call(new ArrayList<>());") .containsOne("b.call((List) new ArrayList());") .containsOne("b.call((String) obj);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestInheritedStaticInvoke.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInheritedStaticInvoke extends IntegrationTest { public static class TestCls { public static class A { public static int a() { return 1; } } public static class B extends A { } public int test() { return B.a(); // not A.a() } } @Test public void test() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("return B.a();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestInvoke1.java ================================================ package jadx.tests.integration.invoke; import java.io.IOException; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInvoke1 extends IntegrationTest { public static class TestCls { private A is; public C test(int start) throws IOException { int id = is.readInt32(); String name = is.readString16Fixed(128); long typeStringsOffset = start + is.readInt32(); long keyStringsOffset = start + is.readInt32(); String[] types = null; if (typeStringsOffset != 0) { types = strs(); } String[] keys = null; if (keyStringsOffset != 0) { keys = strs(); } C pkg = new C(id, name, types, keys); if (id == 0x7F) { is.readInt32(); } return pkg; } private String[] strs() { return new String[0]; } private static final class C { public C(int id, String name, String[] types, String[] keys) { } } private final class A { public int readInt32() { return 0; } public String readString16Fixed(int i) { return null; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("C pkg = new C(id, name, types, keys);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestInvokeInCatch.java ================================================ package jadx.tests.integration.invoke; import java.io.IOException; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInvokeInCatch extends IntegrationTest { public static class TestCls { private static final String TAG = "TAG"; public void test(int[] a, int b) { try { exc(); } catch (IOException e) { if (b == 1) { log(TAG, "Error: {}", e.getMessage()); } } } private static void log(String tag, String str, String... args) { } private void exc() throws IOException { throw new IOException(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("try {") .containsOne("exc();") .doesNotContain("return;") .containsOne("} catch (IOException e) {") .containsOne("if (b == 1) {") .containsOne("log(TAG, \"Error: {}\", e.getMessage());"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestInvokeWithWideVars.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInvokeWithWideVars extends IntegrationTest { @SuppressWarnings("SameParameterValue") public static class TestCls { public long test1() { return call(1, 2L); } public long test2() { return rangeCall(1L, 2, 3.0d, (byte) 4); } private long call(int a, long b) { return 0L; } private long rangeCall(long a, int b, double c, byte d) { return 0L; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return call(1, 2L);") .containsOne("return rangeCall(1L, 2, 3.0d, (byte) 4);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestOverloadedInvoke.java ================================================ package jadx.tests.integration.invoke; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestOverloadedInvoke extends IntegrationTest { public static class TestCls { public static final int N = 10; public void test() { int[][][] arr = new int[N][N][N]; use(arr, -1); use(arr[0], -2); } public void use(Object[][] arr, Object obj) { } public void use(int[][] arr, int i) { } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11, TestProfile.JAVA8 }) public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("use(iArr[0], -2);") .containsOne("use((Object[][]) iArr, (Object) (-1));"); // TODO: don't add unnecessary casts // .containsOne("use(iArr, -1);"); // TODO: replace call `Array.newInstance` with new array creation: `new int[N][N][N]` // .containsOne("new int[10][10][10];"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestOverloadedMethodInvoke.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestOverloadedMethodInvoke extends IntegrationTest { public static class TestCls { int c; public void method(Throwable th, int a) { c++; if (th != null) { c += 100; } c += a; } public void method(Exception e, int a) { c += 1000; if (e != null) { c += 10000; } c += a; } public void test(Throwable th, Exception e) { method(e, 10); method(th, 100); method((Throwable) e, 1000); method((Exception) th, 10000); } public void check() { test(null, new Exception()); assertThat(c).isEqualTo(23212); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("public void test(Throwable th, Exception e) {") .containsOne("method(e, 10);") .containsOne("method(th, 100);") .containsOne("method((Throwable) e, 1000);") .containsOne("method((Exception) th, 10000);") .doesNotContain("(Exception) e"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestOverloadedMethodInvoke2.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestOverloadedMethodInvoke2 extends IntegrationTest { public static class AbstractItem { public void doSomething(Container c, Item i) { c.add(i); } public static class Container { public int add(T t) { return 0; } public void add(AbstractItem... item) { } } public static class Item extends AbstractItem { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestOverloadedMethodInvoke2.AbstractItem.class)) .code().containsOne("c.add(i);") .doesNotContain("(Container)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestPolymorphicInvoke.java ================================================ package jadx.tests.integration.invoke; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.fail; public class TestPolymorphicInvoke extends SmaliTest { public static class TestCls { public String func(int a, int c) { return String.valueOf(a + c); } public String test() { try { MethodType methodType = MethodType.methodType(String.class, Integer.TYPE, Integer.TYPE); MethodHandle methodHandle = MethodHandles.lookup().findVirtual(TestCls.class, "func", methodType); return (String) methodHandle.invoke(this, 1, 2); } catch (Throwable e) { fail("", e); return null; } } public void check() { assertThat(test()).isEqualTo("3"); } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11 }) public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code() .containsOne("return (String) methodHandle.invoke(this, 1, 2);"); assertThat(cls).disasmCode() .containsOne("invoke-polymorphic"); } @TestWithProfiles({ TestProfile.JAVA8, TestProfile.JAVA11 }) public void testJava() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return (String) methodHandle.invoke(this, 1, 2);"); // java uses 'invokevirtual' } @Test public void testSmali() { assertThat(getClassNodeFromSmali()) .code() .containsOne("String ret = (String) methodHandle.invoke(this, 10, 20);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestPolymorphicRangeInvoke.java ================================================ package jadx.tests.integration.invoke; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.fail; public class TestPolymorphicRangeInvoke extends IntegrationTest { public static class TestCls { public String func2(int a, int b, int c, int d, int e, int f) { return String.valueOf(a + b + c + d + e + f); } public String test() { try { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodType methodType = MethodType.methodType(String.class, Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE); MethodHandle methodHandle = lookup.findVirtual(TestCls.class, "func2", methodType); return (String) methodHandle.invoke(this, 10, 20, 30, 40, 50, 60); } catch (Throwable e) { fail("", e); return null; } } public void check() { assertThat(test()).isEqualTo("210"); } } @TestWithProfiles({ TestProfile.DX_J8 }) public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code() .containsOne("return (String) methodHandle.invoke(this, 10, 20, 30, 40, 50, 60);"); assertThat(cls).disasmCode() .containsOne("invoke-polymorphic/range"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestRawCustomInvoke.java ================================================ package jadx.tests.integration.invoke; import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.fail; public class TestRawCustomInvoke extends SmaliTest { public static class TestCls { public static String func(int a, double b) { return String.valueOf(a + b); } private static CallSite staticBootstrap(MethodHandles.Lookup lookup, String name, MethodType type) { try { return new ConstantCallSite(lookup.findStatic(lookup.lookupClass(), name, type)); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } } public String test() { try { return (String) staticBootstrap(MethodHandles.lookup(), "func", MethodType.methodType(String.class, Integer.TYPE, Double.TYPE)) .dynamicInvoker().invoke(1, 2.0d); } catch (Throwable e) { fail("", e); return null; } } public void check() { assertThat(test()).isEqualTo("3.0"); } } @Test public void test() { noDebugInfo(); // this code does not contain `invoke-custom` instruction // only check if equivalent polymorphic call is correct assertThat(getClassNode(TestCls.class)) .code() .containsOne( "return (String) staticBootstrap(MethodHandles.lookup(), \"func\", MethodType.methodType(String.class, Integer.TYPE, Double.TYPE)).dynamicInvoker().invoke(1, 2.0d);"); } @Test public void testSmali() { forceDecompiledCheck(); assertThat(getClassNodeFromSmali()) .code() .containsOne( "return (String) staticBootstrap(MethodHandles.lookup(), \"func\", MethodType.methodType(String.class, Integer.TYPE, Double.TYPE)).dynamicInvoker().invoke(1, 2.0d) /* invoke-custom */;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvoke.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestSuperInvoke extends IntegrationTest { public class A { public int a() { return 1; } } public class B extends A { @Override public int a() { return super.a() + 2; } public int test() { return a(); } } public void check() { assertThat(new B().test()).isEqualTo(3); } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestSuperInvoke.class)) .code() .countString(2, "return super.a() + 2;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvoke2.java ================================================ package jadx.tests.integration.invoke; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSuperInvoke2 extends IntegrationTest { public static class TestCls { @Override public String toString() { return super.toString(); } public void check() { assertThat(new TestCls().toString()).containsOne("@"); } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("return super.toString();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvokeUnknown.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSuperInvokeUnknown extends IntegrationTest { public static class TestCls { public static class BaseClass { public int doSomething() { return 0; } } public static class NestedClass extends BaseClass { @Override public int doSomething() { return super.doSomething(); } } } @Test public void test() { disableCompilation(); noDebugInfo(); assertThat(getClassNode(TestCls.NestedClass.class)) // BaseClass unknown .code() .containsOne("return super.doSomething();"); } @Test public void testTopCls() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("return super.doSomething();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvokeWithGenerics.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestSuperInvokeWithGenerics extends IntegrationTest { public static class TestCls { public static class A { public A(T t) { System.out.println("t" + t); } public A(V v) { System.out.println("v" + v); } } public static class B extends A { public B(String s) { super(s); } public B(Exception e) { super(e); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("super(e);") .containsOne("super(s);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestVarArg.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestVarArg extends IntegrationTest { public static class TestCls { public void test1(int... a) { } public void test2(int i, Object... a) { } public void test3(int[] a) { } public void call() { test1(1, 2); test2(3, "1", 7); test3(new int[] { 5, 8 }); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("void test1(int... a) {") .contains("void test2(int i, Object... a) {") .contains("test1(1, 2);") .contains("test2(3, \"1\", 7);") // negative case .contains("void test3(int[] a) {") .contains("test3(new int[]{5, 8});"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/invoke/TestVarArg2.java ================================================ package jadx.tests.integration.invoke; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestVarArg2 extends IntegrationTest { @SuppressWarnings("ConstantConditions") public static class TestCls { protected static boolean b1; protected static final boolean IS_VALID = b1 && isValid("test"); private static boolean isValid(String... string) { return false; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("isValid(\"test\")"); // TODO: .containsOne("b1 && isValid(\"test\")"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaArgs.java ================================================ package jadx.tests.integration.java8; import java.util.function.BiFunction; import java.util.function.Function; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLambdaArgs extends IntegrationTest { public static class TestCls { public void test1() { call1(a -> -a); } public void test2() { call2((a, b) -> a - b); } private void call1(Function func) { } private void call2(BiFunction func) { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("call1(a ->") .containsOne("call2((a, b) ->"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaConstructor.java ================================================ package jadx.tests.integration.java8; import java.util.function.Supplier; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLambdaConstructor extends IntegrationTest { public static class TestCls { public Supplier test() { return RuntimeException::new; } public void check() throws Exception { assertThat(test().get()).isInstanceOf(RuntimeException.class); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return RuntimeException::new;"); } @Test public void testFallback() { setFallback(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("r0 = java.lang.RuntimeException::new") .containsOne("return r0"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaExtVar.java ================================================ package jadx.tests.integration.java8; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLambdaExtVar extends IntegrationTest { public static class TestCls { public void test(List list, String str) { list.removeIf(s -> s.equals(str)); } public void check() { List list = new ArrayList<>(Arrays.asList("a", "str", "b")); test(list, "str"); assertThat(list).isEqualTo(Arrays.asList("a", "b")); } } @TestWithProfiles(TestProfile.DX_J8) public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls) .code() .doesNotContain("lambda$") .containsOne("return s.equals(str);"); // TODO: simplify to expression } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaExtVar2.java ================================================ package jadx.tests.integration.java8; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLambdaExtVar2 extends IntegrationTest { public static class TestCls { public void test(List list) { String space = " "; list.removeIf(s -> s.equals(space) || s.contains(space)); } public void check() { List list = new ArrayList<>(Arrays.asList("a", " ", "b", "r ")); test(list); assertThat(list).isEqualTo(Arrays.asList("a", "b")); } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11, TestProfile.JAVA11 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("lambda$") .containsOne("String space = \" \";") .containsOne("s.equals(space) || s.contains(space)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaInArray.java ================================================ package jadx.tests.integration.java8; import java.util.Arrays; import java.util.List; import java.util.function.Function; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLambdaInArray extends IntegrationTest { public static class TestCls { public List> test() { return Arrays.asList(this::call1, this::call2); } private Integer call1(String s) { return null; } private Integer call2(String s) { return null; } public void check() throws Exception { assertThat(test()).hasSize(2); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return Arrays.asList(this::call1, this::call2);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaInstance.java ================================================ package jadx.tests.integration.java8; import java.util.function.Function; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLambdaInstance extends IntegrationTest { @SuppressWarnings("Convert2MethodRef") public static class TestCls { public Function test() { return str -> this.call(str); } public Function testMthRef() { return this::call; } public Integer call(String str) { return Integer.parseInt(str); } public Function test2() { return num -> num.toString(); } public Function testMthRef2() { return Object::toString; } public void check() throws Exception { assertThat(test().apply("11")).isEqualTo(11); assertThat(testMthRef().apply("7")).isEqualTo(7); assertThat(test2().apply(15)).isEqualTo("15"); assertThat(testMthRef2().apply(13)).isEqualTo("13"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("lambda$") .doesNotContain("renamed") .containsLines(2, "return str -> {", indent() + "return call(str);", "};") // .containsOne("return Object::toString;") // TODO .containsOne("return this::call;"); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } @Test public void testFallback() { setFallback(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaInstance2.java ================================================ package jadx.tests.integration.java8; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLambdaInstance2 extends IntegrationTest { public static class TestCls { private String field; public Runnable test(String str, int i) { return () -> call(str, i); } public void call(String str, int i) { field = str + '=' + i; } public void check() throws Exception { field = ""; test("num", 7).run(); assertThat(field).isEqualTo("num=7"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("lambda$") .containsOne("call(str, i)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaInstance3.java ================================================ package jadx.tests.integration.java8; import java.util.function.Supplier; import org.junit.jupiter.api.Test; import jadx.tests.api.RaungTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("DataFlowIssue") public class TestLambdaInstance3 extends RaungTest { public interface TestCls extends Supplier { default TestCls test() { return (TestCls & Memoized) Lazy.of(this)::get; } } public static final class Lazy implements Supplier { public static Lazy of(Supplier supplier) { return null; } @Override public T get() { return null; } } interface Memoized { } @Test public void test() { // some java versions failed to compile usage of interface with '$' in name addClsRename("jadx.tests.integration.java8.TestLambdaInstance3$TestCls", "java8.TestCls"); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("this::get") .containsOne("return (TestCls) lazyOf::get;"); // TODO: type inference set type for 'lazyOf' to Memoized and cast incorrectly removed // .containsOne("Memoized)"); } @Test public void testRaung() { disableCompilation(); assertThat(getClassNodeFromRaung()) .code() .doesNotContain("this::get") .containsOne(" lazyOf::get"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaResugar.java ================================================ package jadx.tests.integration.java8; import java.util.function.Function; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLambdaResugar extends IntegrationTest { public static class TestCls { private String field; public void test() { call(s -> { this.field = s; return s.length(); }); } public void call(Function func) { } } @NotYetImplemented("Inline lambda methods") @TestWithProfiles(TestProfile.D8_J11_DESUGAR) public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("lambda$"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaReturn.java ================================================ package jadx.tests.integration.java8; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLambdaReturn extends IntegrationTest { @SuppressWarnings("unused") public static class TestCls { interface Function0 { R apply(); } public static class T2 { public long l; public T2(long l) { this.l = l; } public void w() { } } public Byte test(Byte b1) { Function0 f1 = () -> { new T2(94L).w(); return null; }; f1.apply(); return null; } } @TestWithProfiles(TestProfile.DX_J8) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsLines(2, "Function0 f1 = () -> {", indent() + "new T2(94L).w();", indent() + "return null;", "};"); } @TestWithProfiles(TestProfile.D8_J11_DESUGAR) public void testLambda() { getClassNode(TestCls.class); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/java8/TestLambdaStatic.java ================================================ package jadx.tests.integration.java8; import java.util.concurrent.Callable; import java.util.function.Function; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLambdaStatic extends IntegrationTest { public static class TestCls { public Callable test1() { return () -> "test"; } public Callable test2(String str) { return () -> str; } public Function test3(String a) { return (b) -> Integer.parseInt(a) - Integer.parseInt(b); } public Function test4() { return Integer::parseInt; } @SuppressWarnings("Convert2MethodRef") public Function test4a() { return s -> Integer.parseInt(s); } public Function test5() { String str = Integer.toString(3); return (s) -> Integer.parseInt(str) - Integer.parseInt(s); } public void check() throws Exception { assertThat(test1().call()).isEqualTo("test"); assertThat(test2("a").call()).isEqualTo("a"); assertThat(test3("3").apply("1")).isEqualTo(2); assertThat(test4().apply("5")).isEqualTo(5); assertThat(test4a().apply("7")).isEqualTo(7); assertThat(test5().apply("1")).isEqualTo(2); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("lambda$") .doesNotContain("renamed") .containsLines(2, "return () -> {", indent() + "return \"test\";", "};") .containsLines(2, "return () -> {", indent() + "return str;", "};") .containsOne("return Integer::parseInt;"); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } @Test public void testFallback() { setFallback(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/jbc/TestDup2x1.java ================================================ package jadx.tests.integration.jbc; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDup2x1 extends IntegrationTest { @SuppressWarnings({ "FieldCanBeLocal", "checkstyle:InnerAssignment", "unused" }) public static class TestCls { private long value; public long setValue(long v) { return this.value = v; } } @TestWithProfiles(TestProfile.JAVA11) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("this.value = v;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/jbc/TestStackConvert.java ================================================ package jadx.tests.integration.jbc; import org.junit.jupiter.api.Test; import jadx.tests.api.RaungTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestStackConvert extends RaungTest { @SuppressWarnings({ "UnnecessaryLocalVariable", "CallToPrintStackTrace", "printstacktrace" }) public static class TestCls { public int parseIntDefault(String num, int defaultNum) { try { int defaultNum2 = Integer.parseInt(num); return defaultNum2; } catch (NumberFormatException e) { System.out.println("Before println"); e.printStackTrace(); return defaultNum; } } } @TestWithProfiles(TestProfile.JAVA11) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("Integer.parseInt(num)"); } @Test public void testRaung() { assertThat(getClassNodeFromRaung()) .code() .containsOne("Integer.parseInt(num)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestArrayForEach.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrayForEach extends IntegrationTest { public static class TestCls { public int test(int[] a) { int sum = 0; for (int n : a) { sum += n; } return sum; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsLines(2, "int sum = 0;", "for (int n : a) {", indent() + "sum += n;", "}", "return sum;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestArrayForEach2.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrayForEach2 extends IntegrationTest { public static class TestCls { public void test(String str) { for (String s : str.split("\n")) { String t = s.trim(); if (t.length() > 0) { System.out.println(t); } } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("int ") .containsLines(2, "for (String s : str.split(\"\\n\")) {", indent(1) + "String t = s.trim();", indent(1) + "if (t.length() > 0) {", indent(2) + "System.out.println(t);", indent(1) + '}', "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestArrayForEach3.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.fail; /** * Issue #977 */ public class TestArrayForEach3 extends IntegrationTest { public static class TestCls { public void test(String[] arr) { for (String s : arr) { if (s.length() > 0) { return; } } throw new IllegalArgumentException("All strings are empty"); } public void check() { test(new String[] { "", "a" }); // no exception try { test(new String[] { "", "" }); fail("IllegalArgumentException expected"); } catch (IllegalArgumentException e) { // expected } } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("while") .containsOne("for (String str : strArr) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestArrayForEachNegative.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.api.CommentsLevel; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestArrayForEachNegative extends IntegrationTest { public static class TestCls { public int test(int[] a, int[] b) { int sum = 0; for (int i = 0; i < a.length; i += 2) { sum += a[i]; } for (int i = 1; i < a.length; i++) { sum += a[i]; } for (int i = 0; i < a.length; i--) { sum += a[i]; } for (int i = 0; i <= a.length; i++) { sum += a[i]; } for (int i = 0; i + 1 < a.length; i++) { sum += a[i]; } for (int i = 0; i < a.length; i++) { sum += a[i - 1]; } for (int i = 0; i < b.length; i++) { sum += a[i]; } int j = 0; for (int i = 0; i < a.length; j++) { sum += a[j]; } return sum; } } @Test public void test() { disableCompilation(); // Remove all comments - as the comment created by CodeGenUtils.addInputFileInfo always contains a // colon getArgs().setCommentsLevel(CommentsLevel.NONE); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain(":"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInComplexIf.java ================================================ package jadx.tests.integration.loops; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBreakInComplexIf extends IntegrationTest { public static class TestCls { private int test(Map map, int mapX) { int length = 1; for (int x = mapX + 1; x < 100; x++) { Point tile = map.get(x + ""); if (tile == null || tile.y != 100) { break; } length++; } return length; } class Point { public final int x; public final int y; Point(int x, int y) { this.x = x; this.y = y; } } public void check() { Map first = new HashMap<>(); first.put("3", new Point(100, 100)); first.put("4", new Point(60, 100)); assertThat(test(first, 2)).isEqualTo(3); Map second = new HashMap<>(); second.put("3", new Point(100, 100)); second.put("4", new Point(60, 0)); // check break second.put("5", new Point(60, 100)); assertThat(test(second, 2)).isEqualTo(2); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (tile == null || tile.y != 100) {") .containsOne("break;"); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("break;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInComplexIf2.java ================================================ package jadx.tests.integration.loops; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBreakInComplexIf2 extends IntegrationTest { public static class TestCls { private int test(List list) { int length = 0; for (String str : list) { if (str.isEmpty() || str.length() > 4) { break; } if (str.equals("skip")) { continue; } if (str.equals("a")) { break; } length++; } return length; } public void check() { assertThat(test(Arrays.asList("x", "y", "skip", "z", "a"))).isEqualTo(3); assertThat(test(Arrays.asList("x", "skip", ""))).isEqualTo(1); assertThat(test(Arrays.asList("skip", "y", "12345"))).isEqualTo(1); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(2, "break;"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .countString(2, "break;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestBreakInLoop extends IntegrationTest { public static class TestCls { public int f; public void test(int[] a, int b) { for (int i = 0; i < a.length; i++) { a[i]++; if (i < b) { break; } } this.f++; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("for (int i = 0; i < a.length; i++) {") .containsOne("if (i < b) {") .containsOne("break;") .containsOne("this.f++;") // .containsOne("a[i]++;") .countString(0, "else"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop2.java ================================================ package jadx.tests.integration.loops; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestBreakInLoop2 extends IntegrationTest { @SuppressWarnings({ "BusyWait", "ResultOfMethodCallIgnored" }) public static class TestCls { public void test(List data) throws Exception { for (;;) { try { funcB(data); break; } catch (Exception ex) { if (funcC()) { throw ex; } data.clear(); } Thread.sleep(100L); } } private boolean funcB(List data) { return false; } private boolean funcC() { return true; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("while (true) {") .containsOneOf("break;", "return;") .containsOne("throw ex;") .containsOne("data.clear();") .containsOne("Thread.sleep(100L);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop3.java ================================================ package jadx.tests.integration.loops; import java.io.File; import java.io.FileOutputStream; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import static org.assertj.core.api.Assertions.assertThat; public class TestBreakInLoop3 extends IntegrationTest { public static class TestCls { private StringBuilder builder = new StringBuilder(); public void writeMore(String fid) { boolean tryMkdir = true; File ff = new File(fid); while (true) { prt("1"); try { new FileOutputStream(fid).close(); } catch (Exception ex) { if (tryMkdir) { // On first error, try creating the base dirs. tryMkdir = false; prt("2"); continue; } prt("3"); if (ff.exists()) { prt("4"); } else { prt("5"); } prt("6"); } prt("7"); break; } // end of while true, loop to allow retry of fos.write after mkdir prt("8"); } // end of writeMore private void prt(String s) { builder.append(s); } public void check() { writeMore(""); assertThat(builder).hasToString("12135678"); } } // @Test @NotYetImplemented public void test43() throws Exception { getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop4.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBreakInLoop4 extends IntegrationTest { public static class TestCls { public double test(char c) { double m = 1.0; for (int i = 0; i < 5; i++) { if (c != '.') { if (c == 'a' || c == 'b') { m = 1024.0; } break; } } return m; } public void check() { assertThat(test('.')).isEqualTo(1.0); assertThat(test('a')).isEqualTo(1024.0); assertThat(test('b')).isEqualTo(1024.0); assertThat(test('c')).isEqualTo(1.0); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("while") .containsOne("for"); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("while") .containsOne("for"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop5.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBreakInLoop5 extends IntegrationTest { public static class TestCls { public long test(String spaceStr) { try { double multiplier = 1.0; // Often the compiler will move this line to each of the loop exits creating complexity char c; StringBuffer sb = new StringBuffer(); for (int i = 0; i < spaceStr.length(); i++) { c = spaceStr.charAt(i); if (!Character.isDigit(c) && c != '.') { if (c == 'm' || c == 'M') { multiplier = 1024.0; } else if (c == 'g' || c == 'G') { multiplier = 1024.0 * 1024.0; } break; } sb.append(spaceStr.charAt(i)); } return (long) Math.ceil(Double.valueOf(sb.toString()) * multiplier); } catch (Exception e) { return -1; } } public void check() { assertThat(test("1.2")).isEqualTo(2); assertThat(test("12am")).isEqualTo(12); assertThat(test("13m")).isEqualTo(13 * 1024); assertThat(test("1G4")).isEqualTo(1 * 1024 * 1024); assertThat(test("")).isEqualTo(-1); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code(); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop6.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Test that a continue is not erroneously inserted where a break edge instruction already exists, * leading to the continue being lost and causing a decompile failure. */ public class TestBreakInLoop6 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()).code().containsOne("break"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakWithLabel.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestBreakWithLabel extends IntegrationTest { public static class TestCls { public boolean test(int[][] arr, int b) { boolean found = false; loop0: for (int i = 0; i < arr.length; i++) { for (int j = 0; j < arr[i].length; j++) { if (arr[i][j] == b) { found = true; break loop0; } } } System.out.println("found: " + found); return found; } public void check() { int[][] testArray = { { 1, 2 }, { 3, 4 } }; assertThat(test(testArray, 3)).isTrue(); assertThat(test(testArray, 5)).isFalse(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("loop0:") .containsOne("break loop0;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestComplexWhileLoop.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestComplexWhileLoop extends IntegrationTest { public static class TestCls { public static String test(String[] arr) { int index = 0; int length = arr.length; String str; while ((str = arr[index]) != null) { if (str.length() == 1) { return str.trim(); } if (++index >= length) { index = 0; } } System.out.println("loop end"); return ""; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("for (int at = 0; at < len; at = endAt) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestContinueInLoop.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestContinueInLoop extends IntegrationTest { public static class TestCls { public int f; public void test(int[] a, int b) { for (int i = 0; i < a.length; i++) { int v = a[i]; if (v < b) { a[i]++; } else if (v > b) { a[i]--; } else { continue; } if (i < b) { break; } } this.f++; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("for (int i = 0; i < a.length; i++) {") .containsOne("if (i < b) {") .containsOne("continue;") .containsOne("break;") .containsOne("this.f++;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestDoWhileBreak.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestDoWhileBreak extends IntegrationTest { public static class TestCls { public int test(int k) throws InterruptedException { int i = 3; do { if (k > 9) { i = 0; break; } i++; } while (i < 5); return i; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("while ("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestDoWhileBreak2.java ================================================ package jadx.tests.integration.loops; import java.util.Arrays; import java.util.Iterator; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDoWhileBreak2 extends IntegrationTest { public static class TestCls { Iterator it; @SuppressWarnings("ConstantConditions") public Object test() { String obj; do { obj = this.it.next(); if (obj == null) { return obj; // 'return null' or 'break' also fine } } while (this.it.hasNext()); return obj; } public void check() { this.it = Arrays.asList("a", "b").iterator(); assertThat(test()).isEqualTo("b"); this.it = Arrays.asList("a", "b", null).iterator(); assertThat(test()).isEqualTo(null); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsLine(2, "do {") .containsLine(3, "obj = this.it.next();") .containsLine(3, "if (obj == null) {") .containsLine(2, "} while (this.it.hasNext());"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestDoWhileBreak3.java ================================================ package jadx.tests.integration.loops; import java.util.Iterator; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDoWhileBreak3 extends IntegrationTest { public static class TestCls { Iterator it; public void test() { do { if (!it.hasNext()) { break; } } while (it.next() != null); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("while") .containsLines(2, "while (this.it.hasNext() && this.it.next() != null) {", "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestEndlessLoop.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestEndlessLoop extends IntegrationTest { public static class TestCls { void test1() { while (this == this) { } } void test2() { do { } while (this == this); } void test3() { while (true) { if (this != this) { return; } } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("while (this == this)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestEndlessLoop2.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Empty endless loop, issue #1611 */ public class TestEndlessLoop2 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .countString(2, "while (true) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestIfInLoop2.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIfInLoop2 extends IntegrationTest { public static class TestCls { public static void test(String str) { int len = str.length(); int at = 0; while (at < len) { char c = str.charAt(at); int endAt = at + 1; if (c == 'A') { while (endAt < len) { c = str.charAt(endAt); if (c == 'B') { break; } endAt++; } } at = endAt; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("for (int at = 0; at < len; at = endAt) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestIfInLoop3.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIfInLoop3 extends IntegrationTest { public static class TestCls { static boolean[][] occupied = new boolean[70][70]; static boolean placingStone = true; private static boolean test(int xx, int yy) { int[] extraArray = new int[] { 10, 45, 50, 50, 20, 20 }; if (extraArray != null && placingStone) { for (int i = 0; i < extraArray.length; i += 2) { int tX; int tY; if (yy % 2 == 0) { if (extraArray[i + 1] % 2 == 0) { tX = xx + extraArray[i]; } else { tX = extraArray[i] + xx - 1; } tY = yy + extraArray[i + 1]; } else { tX = xx + extraArray[i]; tY = yy + extraArray[i + 1]; } if (tX < 0 || tY < 0 || tY % 2 != 0 && tX > 28 || tY > 70 || occupied[tX][tY]) { return false; } } } return true; } public void check() { assertThat(test(14, 2)).isTrue(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("for (int i = 0; i < extraArray.length; i += 2) {") .containsOne("if (extraArray != null && placingStone) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestIfInLoop4.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIfInLoop4 extends SmaliTest { /* * Test handling of edge instructions when generated from a loop near an if statement with no else. * They should not be added to an else region, since the if statement has no else. * The actual condition here is less important than if decompilation succeeds at all. */ @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmaliWithPath("loops", "TestIfInLoop4")) .code() .containsOne("return true;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestIndexForLoop.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestIndexForLoop extends IntegrationTest { public static class TestCls { private int test(int[] a, int b) { int sum = 0; for (int i = 0; i < b; i++) { sum += a[i]; } return sum; } public void check() { int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; assertThat(test(array, 0)).isEqualTo(0); assertThat(test(array, 3)).isEqualTo(6); assertThat(test(array, 8)).isEqualTo(36); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsLines(2, "int sum = 0;", "for (int i = 0; i < b; i++) {", indent(1) + "sum += a[i];", "}", "return sum;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestIndexedLoop.java ================================================ package jadx.tests.integration.loops; import java.io.File; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIndexedLoop extends IntegrationTest { public static class TestCls { public File test(File[] files) { File file = null; if (files != null) { int length = files.length; if (length == 0) { file = null; } else { for (int i = 0; i < length; i++) { file = files[i]; if (file.getName().equals("f")) { break; } } } } else { file = null; } if (file != null) { file.deleteOnExit(); } return file; } public void check() { assertThat(test(null)).isNull(); assertThat(test(new File[] {})).isNull(); File file = new File("f"); assertThat(test(new File[] { new File("a"), file })).isEqualTo(file); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("for (File file :") .containsOne("for (int i = 0; i < length; i++) {"); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("for (File file :") .containsOne("for (int i = 0; i < length; i++) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestIterableForEach.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIterableForEach extends IntegrationTest { public static class TestCls { public String test(Iterable a) { StringBuilder sb = new StringBuilder(); for (String s : a) { sb.append(s); } return sb.toString(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsLines(2, "StringBuilder sb = new StringBuilder();", "for (String s : a) {", indent(1) + "sb.append(s);", "}", "return sb.toString();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestIterableForEach2.java ================================================ package jadx.tests.integration.loops; import java.io.IOException; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestIterableForEach2 extends IntegrationTest { public static class TestCls { public static String test(final Service service) throws IOException { for (Authorization auth : service.getAuthorizations()) { if (isValid(auth)) { return auth.getToken(); } } return null; } private static boolean isValid(Authorization auth) { return false; } private static class Service { public List getAuthorizations() { return null; } } private static class Authorization { public String getToken() { return ""; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("for (Authorization auth : service.getAuthorizations()) {") .containsOne("if (isValid(auth)) {") .containsOne("return auth.getToken();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestIterableForEach3.java ================================================ package jadx.tests.integration.loops; import java.util.Set; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestIterableForEach3 extends IntegrationTest { public static class TestCls { private Set a; private Set b; public void test(T str) { Set set = str.length() == 1 ? a : b; for (T s : set) { if (s.length() == str.length()) { if (str.length() == 0) { set.remove(s); } else { set.add(str); } return; } } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("for (T s : set) {") .containsOne("if (str.length() == 0) {"); // TODO move return outside 'if' } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestIterableForEach4.java ================================================ package jadx.tests.integration.loops; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIterableForEach4 extends IntegrationTest { public static class TestCls { public void test(List objects) { for (Object o : objects) { if (o.hashCode() != 42 || o.hashCode() != 1) { break; } } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("while ("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopCondition.java ================================================ package jadx.tests.integration.loops; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestLoopCondition extends IntegrationTest { public static class TestCls { public void test(List list) { for (int i = 0; i != 16 && i < 255; i++) { list.set(i, "ABC"); if (i == 128) { return; } list.set(i, "DEF"); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("list.set(i, \"ABC\")") .containsOne("list.set(i, \"DEF\")"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopCondition2.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestLoopCondition2 extends IntegrationTest { public static class TestCls { public int test(boolean a) { int i = 0; while (a && i < 10) { i++; } return i; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("int i = 0;") .containsOne("while (a && i < 10) {") .containsOne("i++;") .containsOne("return i;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopCondition3.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestLoopCondition3 extends IntegrationTest { public static class TestCls { public static void test(int a, int b, int c) { while (a < 12) { if (b + a < 9 && b < 8) { if (b >= 2 && a > -1 && b < 6) { System.out.println("OK"); c = b + 1; } b = a; } c = b; b++; b = c; a++; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("while (a < 12) {") .containsOne("if (b + a < 9 && b < 8) {") .containsOne("if (b >= 2 && a > -1 && b < 6) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopCondition4.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestLoopCondition4 extends IntegrationTest { public static class TestCls { public static void test() { int n = -1; while (n < 0) { n += 12; } while (n > 11) { n -= 12; } System.out.println(n); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("int n = -1;") .containsOne("while (n < 0) {") .containsOne("n += 12;") .containsOne("while (n > 11) {") .containsOne("n -= 12;") .containsOne("System.out.println(n);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopCondition5.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLoopCondition5 extends SmaliTest { public static class TestCls { public static int lastIndexOf(int[] array, int target, int start, int end) { for (int i = end - 1; i >= start; i--) { if (array[i] == target) { return i; } } return -1; } } @Test public void test0() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("for (") .containsOne("return -1;") .countString(2, "return "); } @Test public void test1() { assertThat(getClassNodeFromSmaliWithPath("loops", "TestLoopCondition5")) .code() .containsOneOf("for (", "while (true) {", "} while (iArr[i3] != i);") .containsOne("return -1;") .countString(2, "return "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopConditionInvoke.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestLoopConditionInvoke extends IntegrationTest { public static class TestCls { private static final char STOP_CHAR = 0; private int pos; public boolean test(char lastChar) { int startPos = pos; char ch; while ((ch = next()) != STOP_CHAR) { if (ch == lastChar) { return true; } } pos = startPos; return false; } private char next() { return 0; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("do {") .containsOne("if (ch == 0) {") .containsOne("this.pos = startPos;") .containsOne("return false;") .containsOne("} while (ch != lastChar);") .containsOne("return true;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopDetection.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLoopDetection extends IntegrationTest { public static class TestCls { public void test(int[] a, int b) { int i = 0; while (i < a.length && i < b) { a[i]++; i++; } while (i < a.length) { a[i]--; i++; } } public void check() { int[] a = { 1, 1, 1, 1, 1 }; test(a, 3); assertThat(a).containsExactly(new int[] { 2, 2, 2, 0, 0 }); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("while (i < a.length && i < b) {") .contains("while (i < a.length) {") .contains("int i = 0;") .doesNotContain("i_2") .contains("i++;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopDetection2.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestLoopDetection2 extends IntegrationTest { public static class TestCls { public int test(int a, int b) { int c = a + b; for (int i = a; i < b; i++) { if (i == 7) { c += 2; } else { c *= 2; } } c--; return c; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("int c = a + b;") .containsOne("for (int i = a; i < b; i++) {") .doesNotContain("c_2"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopDetection3.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestLoopDetection3 extends IntegrationTest { public static class TestCls { public void test(TestCls parent, int pos) { Object item; while (--pos >= 0) { item = parent.get(pos); if (item instanceof String) { func((String) item); return; } } } private Object get(int pos) { return null; } private void func(String item) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("while"); } @Test @NotYetImplemented public void test2() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("while (--pos >= 0) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopDetection4.java ================================================ package jadx.tests.integration.loops; import java.util.Iterator; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestLoopDetection4 extends IntegrationTest { public static class TestCls { private Iterator iterator; private SomeCls filter; public String test() { while (iterator.hasNext()) { String next = iterator.next(); String filtered = filter.filter(next); if (filtered != null) { return filtered; } } return null; } private class SomeCls { public String filter(String str) { return str; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("while (this.iterator.hasNext()) {") .containsOne("if (filtered != null) {") .containsOne("return filtered;") .containsOne("return null;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopDetection5.java ================================================ package jadx.tests.integration.loops; import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLoopDetection5 extends IntegrationTest { public static class TestCls { public String test(String str) { Iterator it = getStrings().iterator(); String otherStr = null; while (it.hasNext()) { otherStr = it.next(); if (otherStr.equalsIgnoreCase(str)) { break; } } return otherStr; } private List getStrings() { return Arrays.asList("str", "otherStr", "STR", "OTHERSTR"); } public void check() { assertThat(test("OTHERSTR")).isEqualTo("otherStr"); } } @Test public void test() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("for (") .containsOne("it.next();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopRestore.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLoopRestore extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("try {") .containsOne("for (byte b : bArrDigest) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopRestore2.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.RaungTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLoopRestore2 extends RaungTest { @Test public void test() { disableCompilation(); // unreachable statement assertThat(getClassNodeFromRaung()) .code() .containsOne("while (1 == 0) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopRestore3.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLoopRestore3 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .countString(3, "while ("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestMultiEntryLoop.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestMultiEntryLoop extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("while (true) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestMultiEntryLoop2.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestMultiEntryLoop2 extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("while (true) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops.java ================================================ package jadx.tests.integration.loops; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestNestedLoops extends IntegrationTest { public static class TestCls { public void test(List l1, List l2) { for (String s1 : l1) { for (String s2 : l2) { if (s1.equals(s2)) { if (s1.length() == 5) { l2.add(s1); } else { l1.remove(s2); } } } } if (l2.size() > 0) { l1.clear(); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("for (String s1 : l1) {") .containsOne("for (String s2 : l2) {") .containsOne("if (s1.equals(s2)) {") .containsOne("l2.add(s1);") .containsOne("l1.remove(s2);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops2.java ================================================ package jadx.tests.integration.loops; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestNestedLoops2 extends IntegrationTest { public static class TestCls { public boolean test(List list) { int j = 0; for (int i = 0; i < list.size(); i++) { String s = list.get(i); while (j < s.length()) { j++; } } return j > 10; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("for (int i = 0; i < list.size(); i++) {") .containsOne("while (j < s.length()) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops3.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestNestedLoops3 extends IntegrationTest { public static class TestCls { int c = 0; public int test(int b) { int i; loop0: while (true) { f1(); i = 0; while (true) { f2(); if (i != 0) { break loop0; } i += 3; if (b >= 16) { break loop0; } try { exc(); break; } catch (Exception e) { // ignore } } } return i; } private void exc() throws Exception { if (c > 200) { throw new Exception(); } } private void f1() { c += 1; } private void f2() { c += 100; } public void check() { test(1); assertThat(c).isEqualTo(302); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("} catch (Exception e) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops4.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNestedLoops4 extends IntegrationTest { public static class TestCls { public int testFor() { int tmp = 1; for (int i = 10; i > -1; i--) { if (i > tmp) { for (int j = 0; j < 54; j += 4) { if (i < j) { for (int k = j; k < j + 4; k++) { if (tmp > k) { return 0; } } break; } } } tmp++; } return tmp; } public void check() { assertThat(testFor()).isEqualTo(12); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("break;") .containsOne("return 0;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops5.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestNestedLoops5 extends IntegrationTest { public static class TestCls { public int testFor() { int tmp = 1; for (int i = 5; i > -1; i--) { if (i > tmp) { for (int j = 1; j < 5; j++) { if (tmp > j * 100) { return 0; } } } tmp++; } return tmp; } public void check() { assertThat(testFor()).isEqualTo(7); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("continue;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestNotIndexedLoop.java ================================================ package jadx.tests.integration.loops; import java.io.File; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNotIndexedLoop extends IntegrationTest { public static class TestCls { public File test(File[] files) { File file; if (files != null) { int length = files.length; if (length == 0) { file = null; } else { int i = 0; while (true) { if (i >= length) { file = new File("h"); break; } file = files[i]; if (file.getName().equals("f")) { break; } i++; } } } else { file = null; } if (file != null) { file.deleteOnExit(); } return file; } public void check() { assertThat(test(null)).isNull(); assertThat(test(new File[] {})).isNull(); File file = new File("f"); assertThat(test(new File[] { new File("a"), file })).isEqualTo(file); assertThat(test(new File[] { new File("a") }).getName()).isEqualTo("h"); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("for (") .containsOne("while (true) {"); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("for (") .containsOne("while (true) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestSequentialLoops.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSequentialLoops extends IntegrationTest { public static class TestCls { public int test(int a, int b) { int c = b; int z; while (true) { z = c + a; if (z >= 7) { break; } c = z; } while ((z = c + a) >= 7) { c = z; } return c; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(2, "while (") .containsOne("break;") .containsOne("return c;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestSequentialLoops2.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSequentialLoops2 extends IntegrationTest { @SuppressWarnings({ "unused", "FieldMayBeFinal" }) public static class TestCls { private static char[] lowercases = new char[] { 'a' }; public static String asciiToLowerCase(String s) { char[] c = null; int i = s.length(); while (i-- > 0) { char c1 = s.charAt(i); if (c1 <= 127) { char c2 = lowercases[c1]; if (c1 != c2) { c = s.toCharArray(); c[i] = c2; break; } } } while (i-- > 0) { if (c[i] <= 127) { c[i] = lowercases[c[i]]; } } return c == null ? s : new String(c); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(2, "while (") .contains("break;") .containsOne("return c") .countString(2, "<= 127"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .countString(2, "while (") .contains("break;") .countString(2, "<= 127"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestSynchronizedInEndlessLoop.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestSynchronizedInEndlessLoop extends IntegrationTest { @SuppressWarnings("BusyWait") public static class TestCls { int f = 5; int test() { while (true) { synchronized (this) { if (f > 7) { return 7; } if (f < 3) { return 3; } } try { f++; Thread.sleep(100L); } catch (Exception e) { throw new RuntimeException(e); } } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("synchronized (this) {") .containsOne("try {") .containsOne("f++;") .containsOne("Thread.sleep(100L);") .containsOne("} catch (Exception e) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestTryCatchInLoop.java ================================================ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestTryCatchInLoop extends IntegrationTest { public static class TestCls { int c = 0; public int test() { while (true) { try { exc(); break; } catch (Exception e) { // } } if (c == 5) { System.out.println(c); } return 0; } private void exc() throws Exception { c++; if (c < 3) { throw new Exception(); } } public void check() { test(); assertThat(c).isEqualTo(3); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("} catch (Exception e) {") .containsOne("break;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/loops/TestTryCatchInLoop2.java ================================================ package jadx.tests.integration.loops; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTryCatchInLoop2 extends IntegrationTest { public static class TestCls { private static class MyItem { int idx; String name; } private final Map mCache = new HashMap<>(); void test(MyItem[] items) { synchronized (this.mCache) { for (int i = 0; i < items.length; ++i) { MyItem existingItem = mCache.get(items[i].idx); if (null == existingItem) { mCache.put(items[i].idx, items[i]); } else { existingItem.name = items[i].name; } } } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("synchronized (this.mCache) {") .containsOne("for (int i = 0; i < items.length; i++) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestCaseSensitiveChecks.java ================================================ package jadx.tests.integration.names; import java.util.List; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import jadx.core.Consts; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCaseSensitiveChecks extends SmaliTest { /* * public class A {} * public class a {} */ @Test public void test() { args.setFsCaseSensitive(false); List classes = loadFromSmaliFiles(); for (ClassNode cls : classes) { assertThat(cls.getPackage()).isEqualTo(Consts.DEFAULT_PACKAGE_NAME); } long namesCount = classes.stream().map(cls -> cls.getAlias().toLowerCase()).distinct().count(); assertThat(namesCount).isEqualTo(2L); } @Test public void testCaseSensitiveFS() { args.setFsCaseSensitive(true); List classes = loadFromSmaliFiles(); for (ClassNode cls : classes) { assertThat(cls.getPackage()).isEqualTo(Consts.DEFAULT_PACKAGE_NAME); } List names = classes.stream().map(ClassNode::getAlias).collect(Collectors.toList()); assertThat(names).containsExactlyInAnyOrder("A", "a"); } @Test public void testWithDeobfuscation() { enableDeobfuscation(); List classes = loadFromSmaliFiles(); for (ClassNode cls : classes) { assertThat(cls.getPackage()).isNotEmpty(); assertThat(cls.getPackage()).isNotEqualTo(Consts.DEFAULT_PACKAGE_NAME); } long namesCount = classes.stream().map(cls -> cls.getAlias().toLowerCase()).distinct().count(); assertThat(namesCount).isEqualTo(2L); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestClassNameWithInvalidChar.java ================================================ package jadx.tests.integration.names; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; public class TestClassNameWithInvalidChar extends SmaliTest { /* * public class do- {} * public class i-f {} */ @Test public void test() { loadFromSmaliFiles(); } @Test public void testWithDeobfuscation() { enableDeobfuscation(); loadFromSmaliFiles(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestClassNamesCollision.java ================================================ package jadx.tests.integration.names; import java.util.List; import org.junit.jupiter.api.Test; import jadx.api.CommentsLevel; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import jadx.tests.integration.names.pkg.a; import jadx.tests.integration.names.pkg.b; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestClassNamesCollision extends IntegrationTest { @Test public void test() { getArgs().setCommentsLevel(CommentsLevel.WARN); List classNodes = getClassNodes(a.class, b.class); assertThat(searchCls(classNodes, "a")) .code() .containsOne("public class a {") .containsOne("public static a a() {"); assertThat(searchCls(classNodes, "b")) .code() .containsOne("class a {") .containsOne("jadx.tests.integration.names.pkg.a a = jadx.tests.integration.names.pkg.a.a();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestClassNamesCollision2.java ================================================ package jadx.tests.integration.names; import org.junit.jupiter.api.Test; import jadx.api.CommentsLevel; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestClassNamesCollision2 extends IntegrationTest { @SuppressWarnings("rawtypes") public static class TestCls { static class List { public static List getList() { return null; } } protected List list = List.getList(); protected void clearList(java.util.List l) { l.clear(); } } @Test public void test() { getArgs().setCommentsLevel(CommentsLevel.WARN); assertThat(getClassNode(TestCls.class)) .code() .containsOne("static class List {") .containsOne("protected void clearList(java.util.List l) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestCollisionWithJavaLangClasses.java ================================================ package jadx.tests.integration.names; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCollisionWithJavaLangClasses extends IntegrationTest { public static class TestCls1 { public static class System { public static void main(String[] args) { java.lang.System.out.println("Hello world"); } } } @Test public void test1() { assertThat(getClassNode(TestCls1.class)) .code() .containsOne("java.lang.System.out.println"); } public static class TestCls2 { public void doSomething() { System.doSomething(); java.lang.System.out.println("Hello World"); } public static class System { public static void doSomething() { } } } @Test public void test2() { assertThat(getClassNode(TestCls2.class)) .code() .containsLine(2, "System.doSomething();") .containsOne("java.lang.System.out.println"); } @Test public void test3() { List classes = getClassNodes( jadx.tests.integration.names.pkg2.System.class, jadx.tests.integration.names.pkg2.TestCls.class); assertThat(searchCls(classes, "TestCls")) .code() .containsLine(2, "System.doSomething();") .containsOne("java.lang.System.out.println"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestConstructorArgNames.java ================================================ package jadx.tests.integration.names; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConstructorArgNames extends IntegrationTest { @SuppressWarnings({ "FieldCanBeLocal", "FieldMayBeFinal", "StaticVariableName", "ParameterName" }) public static class TestCls { private static String STR = "static field"; private final String str; private final String store; public TestCls(String str, String STR) { this.str = str; this.store = STR; } public TestCls() { this.str = "a"; this.store = STR; } public void check() { assertThat(new TestCls("a", "b").store).isEqualTo("b"); assertThat(new TestCls().store).isEqualTo(STR); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("this.str = str;") .containsOne("this.store = STR2;") .containsOne("this.store = STR;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestDefPkgRename.java ================================================ package jadx.tests.integration.names; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDefPkgRename extends SmaliTest { @Test public void test() { List clsList = loadFromSmaliFiles(); // class A moved to 'defpackage' assertThat(searchCls(clsList, "A")) .code() .containsOne("package defpackage;"); assertThat(searchCls(clsList, "pkg.B")) .code() .containsOne("import defpackage.A;") .containsOne("public A a;"); } @Test public void testNoImports() { args.setUseImports(false); List clsList = loadFromSmaliFiles(); // class A moved to 'defpackage', but use full names assertThat(searchCls(clsList, "A")) .code() .containsOne("package defpackage;"); assertThat(searchCls(clsList, "pkg.B")) .code() .doesNotContain("import") .containsOne("public defpackage.A a;"); } @Test public void testDeobf() { enableDeobfuscation(); List clsList = loadFromSmaliFiles(); // package for class A deobfuscated assertThat(searchCls(clsList, "pkg.B")) .code() .containsOne("import p000.C0000A;") .containsOne("public C0000A f0a;"); } @Test public void testRenameDisabled() { disableCompilation(); args.setRenameFlags(Collections.emptySet()); List clsList = loadFromSmaliFiles(); // no renaming, code will not compile assertThat(searchCls(clsList, "A")) .code() .containsOne("// default package"); assertThat(searchCls(clsList, "pkg.B")) .code() .doesNotContain("import") // omit import .containsOne("public A a;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestDuplicateVarNames.java ================================================ package jadx.tests.integration.names; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDuplicateVarNames extends IntegrationTest { public static class TestCls { public static class A { public String mth(A a) { return null; } @Override public String toString() { return "1"; } } public A test(A a) { return new A() { @Override public String mth(A innerA) { return a + "." + innerA; } }; } public void check() { String str = test(new A()).mth(new A() { @Override public String toString() { return "2"; } }); assertThat(str).isEqualTo("1.2"); } } @Test public void test() { noDebugInfo(); ClassNode cls = getClassNode(TestCls.class); assertThat(cls) .code() .doesNotContain("return a + \".\" + a;") .doesNotContain("AnonymousClass1"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestDuplicatedNames.java ================================================ package jadx.tests.integration.names; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDuplicatedNames extends SmaliTest { // @formatter:off /* public static class TestCls { public Object fieldName; public String fieldName; public Object run() { return this.fieldName; } public String run() { return this.fieldName; } } */ // @formatter:on @Test public void test() { commonChecks(); } @Test public void testWithDeobf() { enableDeobfuscation(); commonChecks(); } private void commonChecks() { assertThat(getClassNodeFromSmaliWithPath("names", "TestDuplicatedNames")) .code() .containsOne("Object fieldName;") .containsOne("String f0fieldName") .containsOne("this.fieldName") .containsOne("this.f0fieldName") .containsOne("public Object run() {") .containsOne("public String m0run() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestFieldCollideWithPackage.java ================================================ package jadx.tests.integration.names; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldCollideWithPackage extends SmaliTest { //@formatter:off /* ----------------------------------------------------------- package first; public class A { public A first; public second.A second; public String test() { return second.A.call(); // compiler treat 'second' as field name } } ----------------------------------------------------------- package second; public class A { public static String call() { return null; } } ----------------------------------------------------------- */ //@formatter:on @Test public void test() { List clsList = loadFromSmaliFiles(); ClassNode firstA = searchCls(clsList, "first.A"); String code = firstA.getCode().toString(); assertThat(code) .contains("second.A") .doesNotContain("public second.A second;"); // expect field to be renamed } @Test public void testWithoutImports() { getArgs().setUseImports(false); loadFromSmaliFiles(); } @Test public void testWithDeobfuscation() { enableDeobfuscation(); loadFromSmaliFiles(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestLocalVarCollideWithPackage.java ================================================ package jadx.tests.integration.names; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLocalVarCollideWithPackage extends SmaliTest { //@formatter:off /* ----------------------------------------------------------- package first; import pkg.Second; public class A { public String test() { Second second = new Second(); second.A.call(); // collision return second.str; } } ----------------------------------------------------------- package pkg; public class Second { public String str; } ----------------------------------------------------------- package second; public class A { } ----------------------------------------------------------- */ //@formatter:on @Test public void test() { List clsList = loadFromSmaliFiles(); ClassNode firstA = searchCls(clsList, "first.A"); String code = firstA.getCode().toString(); assertThat(code) .contains("second.A.call();") .doesNotContain("Second second = new Second();"); } @Test public void testNoDebug() { noDebugInfo(); loadFromSmaliFiles(); } @Test public void testWithoutImports() { getArgs().setUseImports(false); loadFromSmaliFiles(); } @Test public void testWithDeobfuscation() { enableDeobfuscation(); loadFromSmaliFiles(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestNameAssign2.java ================================================ package jadx.tests.integration.names; import java.util.ArrayDeque; import java.util.BitSet; import java.util.Deque; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.ssa.LiveVarAnalysis; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNameAssign2 extends IntegrationTest { public static class TestCls { public static void test(MethodNode mth, int regNum, LiveVarAnalysis la) { List blocks = mth.getBasicBlocks(); int blocksCount = blocks.size(); BitSet hasPhi = new BitSet(blocksCount); BitSet processed = new BitSet(blocksCount); Deque workList = new ArrayDeque<>(); BitSet assignBlocks = la.getAssignBlocks(regNum); for (int id = assignBlocks.nextSetBit(0); id >= 0; id = assignBlocks.nextSetBit(id + 1)) { processed.set(id); workList.add(blocks.get(id)); } while (!workList.isEmpty()) { BlockNode block = workList.pop(); BitSet domFrontier = block.getDomFrontier(); for (int id = domFrontier.nextSetBit(0); id >= 0; id = domFrontier.nextSetBit(id + 1)) { if (!hasPhi.get(id) && la.isLive(id, regNum)) { BlockNode df = blocks.get(id); addPhi(df, regNum); hasPhi.set(id); if (!processed.get(id)) { processed.set(id); workList.add(df); } } } } } private static void addPhi(BlockNode df, int regNum) { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("int id;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestReservedClassNames.java ================================================ package jadx.tests.integration.names; import java.io.File; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestReservedClassNames extends SmaliTest { /* * public class do { * } */ @Test public void test() { assertThat(getClassNodeFromSmali("names" + File.separatorChar + "TestReservedClassNames", "do")) .code() .doesNotContain("public class do"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestReservedNames.java ================================================ package jadx.tests.integration.names; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestReservedNames extends SmaliTest { // @formatter:off /* public static class TestCls { public String do; // reserved name public String 0f; // invalid identifier public String try() { return this.do; } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .doesNotContain("public String do;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestReservedPackageNames.java ================================================ package jadx.tests.integration.names; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestReservedPackageNames extends SmaliTest { // @formatter:off /* package do.if; public class A {} */ // @formatter:on @Test public void test() { List clsList = loadFromSmaliFiles(); for (ClassNode cls : clsList) { assertThat(cls).code().doesNotContain("package do.if;"); } } @Test public void testDeobf() { enableDeobfuscation(); List clsList = loadFromSmaliFiles(); for (ClassNode cls : clsList) { assertThat(cls).code().doesNotContain("package do.if;"); } } @Test public void testRenameDisabled() { disableCompilation(); args.setRenameFlags(Collections.emptySet()); for (ClassNode cls : loadFromSmaliFiles()) { if (cls.getAlias().equals("A")) { assertThat(cls).code().contains("package do.if;"); } } } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/TestSameMethodsNames.java ================================================ package jadx.tests.integration.names; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestSameMethodsNames extends IntegrationTest { public static class TestCls { public static void test() { new Bug().Bug(); } public static class Bug { public Bug() { System.out.println("constructor"); } @SuppressWarnings({ "MethodName", "MethodNameSameAsClassName" }) void Bug() { System.out.println("Bug"); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("new Bug().Bug();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/pkg/a.java ================================================ package jadx.tests.integration.names.pkg; @SuppressWarnings({ "TypeName", "MethodName" }) public class a { public static a a() { return null; } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/pkg/b.java ================================================ package jadx.tests.integration.names.pkg; @SuppressWarnings("TypeName") public class b { class a { } private jadx.tests.integration.names.pkg.a a = jadx.tests.integration.names.pkg.a.a(); } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/pkg2/System.java ================================================ package jadx.tests.integration.names.pkg2; public class System { public static void doSomething() { } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/names/pkg2/TestCls.java ================================================ package jadx.tests.integration.names.pkg2; public class TestCls { public void doSomething() { System.doSomething(); java.lang.System.out.println("Hello World"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestAllNops.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAllNops extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsLines(1, "private boolean test() {", "}") .containsLines(1, "private boolean testWithTryCatch() {", "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestArgInline.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestArgInline extends IntegrationTest { public static class TestCls { public void test(int a) { while (a < 10) { int b = a + 1; a = b; } } } @Test public void test() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("i++;") .doesNotContain("i = i + 1;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestBadClassAccessModifiers.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBadClassAccessModifiers extends SmaliTest { // @formatter:off /* // class others.A public class A { public void call() { B.BB.BBB.test(); } } // class others.B public class B { private static class BB { public static class BBB { public static void test() { } } } } */ @Test public void test() { assertThat(getClassNodeFromSmaliFiles("A")) .code(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestBadMethodAccessModifiers.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBadMethodAccessModifiers extends SmaliTest { // @formatter:off /* public static class TestCls { public abstract class A { public abstract void test(); } public class B extends A { protected void test() { } } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmaliFiles("others", "TestBadMethodAccessModifiers", "TestCls")) .code() .doesNotContain("protected void test() {") .containsOne("public void test() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestCastOfNull.java ================================================ package jadx.tests.integration.others; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("unused") public class TestCastOfNull extends IntegrationTest { public static class TestCls { public void test() { m((long[]) null); m((String) null); m((List) null); } public void m(long[] a) { } public void m(String s) { } public void m(List list) { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("m((long[]) null);") .containsOne("m((String) null);") .containsOne("m((List) null);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestClassGen.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestClassGen extends IntegrationTest { public static class TestCls { public interface I { int test(); public int test3(); } public abstract static class A { public abstract int test2(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("public interface I {") .contains(indent(2) + "int test();") .doesNotContain("public int test();") .contains(indent(2) + "int test3();") .contains("public static abstract class A {") .contains(indent(2) + "public abstract int test2();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestClassImplementsSignature.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.RaungTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestClassImplementsSignature extends RaungTest { public static class TestCls { public abstract static class A implements Comparable> { T value; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("public static abstract class A implements Comparable> {"); } @Test public void testRaung() { allowWarnInCode(); assertThat(getClassNodeFromRaung()) .code() .containsOne("public class TestClassImplementsSignature {") .containsOne("Unexpected interfaces in signature"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestClassReGen.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestClassReGen extends IntegrationTest { public static class TestCls { private int intField = 5; public static class A { } public int test() { return 0; } } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls) .code() .containsOnlyOnce("private int intField = 5;") .containsOnlyOnce("public static class A {") .containsOnlyOnce("public int test() {"); cls.getInnerClasses().get(0).getClassInfo().changeShortName("ARenamed"); cls.searchMethodByShortName("test").getMethodInfo().setAlias("testRenamed"); cls.searchFieldByName("intField").getFieldInfo().setAlias("intFieldRenamed"); assertThat(cls) .reloadCode(this) .containsOnlyOnce("private int intFieldRenamed = 5;") .containsOnlyOnce("public static class ARenamed {") .containsOnlyOnce("public int testRenamed() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments.java ================================================ package jadx.tests.integration.others; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.Test; import jadx.api.data.ICodeComment; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef.RefType; import jadx.api.data.impl.JadxCodeComment; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxCodeRef; import jadx.api.data.impl.JadxNodeRef; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCodeComments extends IntegrationTest { @SuppressWarnings("FieldCanBeLocal") public static class TestCls { private int intField = 5; public static class A { } public int test() { System.out.println("Hello"); System.out.println("comment"); return intField; } } @Test public void test() { String baseClsId = TestCls.class.getName(); ICodeComment clsComment = new JadxCodeComment(JadxNodeRef.forCls(baseClsId), "class comment"); ICodeComment innerClsComment = new JadxCodeComment(JadxNodeRef.forCls(baseClsId + "$A"), "inner class comment"); ICodeComment fldComment = new JadxCodeComment(new JadxNodeRef(RefType.FIELD, baseClsId, "intField:I"), "field comment"); JadxNodeRef mthRef = new JadxNodeRef(RefType.METHOD, baseClsId, "test()I"); ICodeComment mthComment = new JadxCodeComment(mthRef, "method comment"); IJavaCodeRef insnRef = JadxCodeRef.forInsn(isJavaInput() ? 13 : 11); ICodeComment insnComment = new JadxCodeComment(mthRef, insnRef, "insn comment"); JadxCodeData codeData = new JadxCodeData(); getArgs().setCodeData(codeData); codeData.setComments(Arrays.asList(clsComment, innerClsComment, fldComment, mthComment, insnComment)); ClassNode cls = getClassNode(TestCls.class); assertThat(cls) .decompile() .checkCodeOffsets() .code() .containsOne("// class comment") .containsOne("// inner class comment") .containsOne("// field comment") .containsOne("// method comment") .containsOne("System.out.println(\"comment\"); // insn comment"); String code = cls.getCode().getCodeStr(); assertThat(cls) .reloadCode(this) .isEqualTo(code); ICodeComment updInsnComment = new JadxCodeComment(mthRef, insnRef, "updated insn comment"); codeData.setComments(Collections.singletonList(updInsnComment)); jadxDecompiler.reloadCodeData(); assertThat(cls) .reloadCode(this) .containsOne("System.out.println(\"comment\"); // updated insn comment") .doesNotContain("class comment") .containsOne(" comment"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments2.java ================================================ package jadx.tests.integration.others; import java.util.Arrays; import org.junit.jupiter.api.Test; import jadx.api.data.ICodeComment; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef.RefType; import jadx.api.data.impl.JadxCodeComment; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxCodeRef; import jadx.api.data.impl.JadxNodeRef; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCodeComments2 extends IntegrationTest { public static class TestCls { public int test(boolean z) { if (z) { System.out.println("z"); return 1; } return 3; } } @Test public void test() { printOffsets(); String baseClsId = TestCls.class.getName(); JadxNodeRef mthRef = new JadxNodeRef(RefType.METHOD, baseClsId, "test(Z)I"); IJavaCodeRef insnRef = JadxCodeRef.forInsn(isJavaInput() ? 13 : 10); ICodeComment insnComment = new JadxCodeComment(mthRef, insnRef, "return comment"); IJavaCodeRef insnRef2 = JadxCodeRef.forInsn(isJavaInput() ? 15 : 11); ICodeComment insnComment2 = new JadxCodeComment(mthRef, insnRef2, "another return comment"); JadxCodeData codeData = new JadxCodeData(); codeData.setComments(Arrays.asList(insnComment, insnComment2)); getArgs().setCodeData(codeData); assertThat(getClassNode(TestCls.class)) .decompile() .checkCodeOffsets() .code() .containsOne("return 1; // " + insnComment.getComment()) .containsOne("return 3; // " + insnComment2.getComment()); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestCodeComments2a.java ================================================ package jadx.tests.integration.others; import java.util.Arrays; import java.util.Random; import org.junit.jupiter.api.Test; import jadx.api.data.ICodeComment; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef.RefType; import jadx.api.data.impl.JadxCodeComment; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxCodeRef; import jadx.api.data.impl.JadxNodeRef; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCodeComments2a extends IntegrationTest { @SuppressWarnings("unused") public static class TestCls { private int f; public int test(boolean z) { if (z) { System.out.println("z"); return new Random().nextInt(); } return f; } } @Test public void test() { printOffsets(); String baseClsId = TestCls.class.getName(); JadxNodeRef mthRef = new JadxNodeRef(RefType.METHOD, baseClsId, "test(Z)I"); IJavaCodeRef insnRef = JadxCodeRef.forInsn(isJavaInput() ? 22 : 18); ICodeComment insnComment = new JadxCodeComment(mthRef, insnRef, "return comment"); IJavaCodeRef insnRef2 = JadxCodeRef.forInsn(isJavaInput() ? 27 : 19); ICodeComment insnComment2 = new JadxCodeComment(mthRef, insnRef2, "another return comment"); JadxCodeData codeData = new JadxCodeData(); codeData.setComments(Arrays.asList(insnComment, insnComment2)); getArgs().setCodeData(codeData); assertThat(getClassNode(TestCls.class)) .decompile() .checkCodeOffsets() .code() .containsOne("// " + insnComment.getComment()) .containsOne("// " + insnComment2.getComment()); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestCodeCommentsMultiline.java ================================================ package jadx.tests.integration.others; import java.util.Collections; import org.junit.jupiter.api.Test; import jadx.api.data.ICodeComment; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef.RefType; import jadx.api.data.impl.JadxCodeComment; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxCodeRef; import jadx.api.data.impl.JadxNodeRef; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCodeCommentsMultiline extends IntegrationTest { public static class TestCls { public int test(boolean z) { if (z) { System.out.println("z"); return 1; } return 3; } } @Test public void test() { printOffsets(); String baseClsId = TestCls.class.getName(); JadxNodeRef mthRef = new JadxNodeRef(RefType.METHOD, baseClsId, "test(Z)I"); IJavaCodeRef insnRef = JadxCodeRef.forInsn(isJavaInput() ? 15 : 11); ICodeComment insnComment = new JadxCodeComment(mthRef, insnRef, "multi\nline\ncomment"); JadxCodeData codeData = new JadxCodeData(); codeData.setComments(Collections.singletonList(insnComment)); getArgs().setCodeData(codeData); assertThat(getClassNode(TestCls.class)) .code() .containsOne("// multi") .containsOne("// line") .containsOne("// comment"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestCodeCommentsOverride.java ================================================ package jadx.tests.integration.others; import java.util.Arrays; import org.junit.jupiter.api.Test; import jadx.api.data.ICodeComment; import jadx.api.data.IJavaNodeRef.RefType; import jadx.api.data.impl.JadxCodeComment; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxNodeRef; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCodeCommentsOverride extends IntegrationTest { public static class TestCls { public interface I { void mth(); } public static class A implements I { @Override public void mth() { System.out.println("mth"); } } } @Test public void test() { String baseClsId = TestCls.class.getName(); JadxNodeRef iMthRef = new JadxNodeRef(RefType.METHOD, baseClsId + "$I", "mth()V"); ICodeComment iMthComment = new JadxCodeComment(iMthRef, "interface mth comment"); JadxNodeRef mthRef = new JadxNodeRef(RefType.METHOD, baseClsId + "$A", "mth()V"); ICodeComment mthComment = new JadxCodeComment(mthRef, "mth comment"); JadxCodeData codeData = new JadxCodeData(); codeData.setComments(Arrays.asList(iMthComment, mthComment)); getArgs().setCodeData(codeData); ClassNode cls = getClassNode(TestCls.class); assertThat(cls) .decompile() .checkCodeOffsets() .code() .containsOne("@Override") .containsOne("// " + iMthComment.getComment()) .containsOne("// " + mthComment.getComment()); assertThat(cls) .reloadCode(this) .containsOne("@Override") .containsOne("// " + iMthComment.getComment()) .containsOne("// " + mthComment.getComment()); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestCodeMetadata.java ================================================ package jadx.tests.integration.others; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; import jadx.api.JadxInternalAccess; import jadx.api.JavaClass; import jadx.api.JavaMethod; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeAnnotation.AnnType; import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.ICodeNodeRef; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCodeMetadata extends IntegrationTest { public static class TestCls { public static class A { public String str; } public String test() { A a = new A(); a.str = call(); return a.str; } public static String call() { return "str"; } } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code().containsOne("return a.str;"); MethodNode testMth = getMethod(cls, "test"); MethodNode callMth = getMethod(cls, "call"); int callDefPos = callMth.getDefPosition(); assertThat(callDefPos).isNotZero(); JavaClass javaClass = JadxInternalAccess.convertClassNode(jadxDecompiler, cls); JavaMethod callJavaMethod = JadxInternalAccess.convertMethodNode(jadxDecompiler, callMth); List callUsePlaces = javaClass.getUsePlacesFor(javaClass.getCodeInfo(), callJavaMethod); assertThat(callUsePlaces).hasSize(1); int callUse = callUsePlaces.get(0); ICodeMetadata metadata = cls.getCode().getCodeMetadata(); System.out.println(metadata); ICodeNodeRef callDef = metadata.getNodeAt(callUse); assertThat(callDef).isSameAs(testMth); AtomicInteger endPos = new AtomicInteger(); ICodeAnnotation testEnd = metadata.searchUp(callDefPos, (pos, ann) -> { if (ann.getAnnType() == AnnType.END) { endPos.set(pos); return ann; } return null; }); assertThat(testEnd).isNotNull(); int testEndPos = endPos.get(); ICodeAnnotation closest = metadata.getClosestUp(testEndPos); assertThat(closest).isInstanceOf(FieldNode.class); // field reference from 'return a.str;' ICodeNodeRef nodeBelow = metadata.getNodeBelow(testEndPos); assertThat(nodeBelow).isSameAs(callMth); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestCodeMetadata2.java ================================================ package jadx.tests.integration.others; import java.util.List; import org.junit.jupiter.api.Test; import jadx.api.JavaClass; import jadx.api.JavaMethod; import jadx.api.metadata.ICodeMetadata; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static jadx.api.JadxInternalAccess.convertClassNode; import static jadx.api.JadxInternalAccess.convertMethodNode; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestCodeMetadata2 extends IntegrationTest { public static class TestCls { @SuppressWarnings("Convert2Lambda") public Runnable test(boolean a) { if (a) { return new Runnable() { @Override public void run() { System.out.println("test"); } }; } System.out.println("another"); return empty(); } public static Runnable empty() { return new Runnable() { @Override public void run() { // empty } }; } } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code().containsOne("return empty();"); MethodNode testMth = getMethod(cls, "test"); MethodNode emptyMth = getMethod(cls, "empty"); JavaClass javaClass = convertClassNode(jadxDecompiler, cls); JavaMethod emptyJavaMethod = convertMethodNode(jadxDecompiler, emptyMth); List emptyUsePlaces = javaClass.getUsePlacesFor(javaClass.getCodeInfo(), emptyJavaMethod); assertThat(emptyUsePlaces).hasSize(1); int callUse = emptyUsePlaces.get(0); ICodeMetadata metadata = cls.getCode().getCodeMetadata(); assertThat(metadata.getNodeAt(callUse)).isSameAs(testMth); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestCodeMetadata3.java ================================================ package jadx.tests.integration.others; import java.util.List; import org.junit.jupiter.api.Test; import jadx.api.ICodeInfo; import jadx.api.JavaClass; import jadx.api.JavaVariable; import jadx.api.metadata.annotations.VarNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Test variables refs in code metadata */ public class TestCodeMetadata3 extends IntegrationTest { public static class TestCls { public String test(String str) { int k = str.length(); k++; return str + ':' + k; } } @Test public void test() { disableCompilation(); ClassNode cls = getClassNode(TestCls.class); ICodeInfo codeInfo = cls.getCode(); System.out.println(codeInfo.getCodeMetadata()); MethodNode testMth = getMethod(cls, "test"); JavaClass javaClass = toJavaClass(cls); List varNodes = testMth.collectArgNodes(); assertThat(varNodes).hasSize(1); VarNode strVar = varNodes.get(0); JavaVariable strJavaVar = toJavaVariable(strVar); assertThat(strJavaVar.getName()).isEqualTo("str"); List strUsePlaces = javaClass.getUsePlacesFor(codeInfo, strJavaVar); assertThat(strUsePlaces).hasSize(2); assertThat(codeInfo).code().countString(3, "str"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestConstReplace.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConstReplace extends IntegrationTest { public static class TestCls { public static final String CONST_VALUE = "string"; public String test() { return CONST_VALUE; } } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); MethodNode testMth = cls.searchMethodByShortName("test"); assertThat(testMth) .code() .print() .containsOne("return CONST_VALUE;"); FieldNode constField = cls.searchFieldByName("CONST_VALUE"); assertThat(constField).isNotNull(); assertThat(constField.getUseIn()).containsExactly(testMth); } @Test public void testWithoutReplace() { getArgs().setReplaceConsts(false); ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code().containsOne("return \"string\";"); FieldNode constField = cls.searchFieldByName("CONST_VALUE"); assertThat(constField).isNotNull(); assertThat(constField.getUseIn()).isEmpty(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestConstStringConcat.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConstStringConcat extends IntegrationTest { @SuppressWarnings("StringBufferReplaceableByString") public static class TestCls { public String test1(int value) { return new StringBuilder().append("Value").append(" equals ").append(value).toString(); } public String test2() { return new StringBuilder().append("App ").append("version: ").append(1).append('.').append(2).toString(); } public String test3(String name, int value) { return "value " + name + " = " + value; } public void check() { assertThat(test1(7)).isEqualTo("Value equals 7"); assertThat(test2()).isEqualTo("App version: 1.2"); assertThat(test3("v", 4)).isEqualTo("value v = 4"); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("return \"Value equals \" + ") .containsOne("return \"App version: 1.2\";") .containsOne("return \"value \" + str + \" = \" + i;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestConstructor.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConstructor extends SmaliTest { // @formatter:off /* private SomeObject test(double r23, double r25, SomeObject r27) { SomeObject r17 = new SomeObject r0 = r17 r1 = r27 r0.(r1) return r17 } */ // @formatter:on @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOne("new SomeObject(arg3);") .doesNotContain("= someObject"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestConstructor2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Constructor called on object instance is from Object not instance type */ public class TestConstructor2 extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmaliFiles()) .code() .containsOne("A a = new A();") .doesNotContain("return"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestConstructorBranched.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestConstructorBranched extends SmaliTest { // @formatter:off /* public Set test(Collection collection) { Set set; if (collection == null) { set = new HashSet<>(); } else { set = new HashSet<>(collection); } set.add("end"); return set; } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("new HashSet()") .containsOne("new HashSet(collection)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestConstructorBranched2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestConstructorBranched2 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .countString(3, "new StringBuilder()"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestConstructorBranched3.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestConstructorBranched3 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .countString(2, "return new f("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestDeadBlockReferencesStart.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDeadBlockReferencesStart extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .countString(0, "throw"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestDeboxing.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDeboxing extends IntegrationTest { public static class TestCls { public Object testInt() { return 1; } public Object testBoolean() { return true; } public Object testByte() { return (byte) 2; } public Short testShort() { return 3; } public Character testChar() { return 'c'; } public Long testLong() { return 4L; } public void testConstInline() { Boolean v = true; use(v); use(v); } private void use(Boolean v) { } public void check() { // don't mind weird comparisons // need to get primitive without using boxing or literal // otherwise will get same result after decompilation assertThat(testInt()).isEqualTo(Integer.sum(0, 1)); assertThat(testBoolean()).isEqualTo(Boolean.TRUE); assertThat(testByte()).isEqualTo(Byte.parseByte("2")); assertThat(testShort()).isEqualTo(Short.parseShort("3")); assertThat(testChar()).isEqualTo("c".charAt(0)); assertThat(testLong()).isEqualTo(Long.valueOf("4")); testConstInline(); } } @Test public void test() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("return 1;") .containsOne("return true;") .containsOne("return (byte) 2;") .containsOne("return (short) 3;") .containsOne("return 'c';") .containsOne("return 4L;") .countString(2, "use(true);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestDeboxing2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDeboxing2 extends IntegrationTest { public static class TestCls { public long test(Long l) { if (l == null) { l = 0L; } return l; } public void check() { assertThat(test(null)).isEqualTo(0L); assertThat(test(0L)).isEqualTo(0L); assertThat(test(7L)).isEqualTo(7L); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("long test(Long l)") .containsOne("if (l == null) {") .containsOne("l = 0L;") .containsOne("test(null)") .containsOne("test(0L)") // checks for 'check' method .countString(2, "isEqualTo(0L)") .containsOne("test(7L)") .containsOne("isEqualTo(7L)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestDeboxing3.java ================================================ package jadx.tests.integration.others; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestDeboxing3 extends IntegrationTest { public static class TestCls { public static class Pair { public F first; public S second; } private Map> cache = new HashMap<>(); public boolean test(String id, Long l) { if (l == null) { l = 900000L; } Pair pair = this.cache.get(id); if (pair == null) { return false; } return pair.first + l > System.currentTimeMillis(); } } @Test public void test() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("l = 900000L;"); } @Test @NotYetImplemented("Full deboxing and generics propagation") public void testFull() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("Pair pair = this.cache.get(id);") .containsOne("return pair.first + l > System.currentTimeMillis();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestDeboxing4.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDeboxing4 extends IntegrationTest { public static class TestCls { public boolean test(Integer i) { return ((Integer) 1).equals(i); } public void check() { assertThat(test(null)).isFalse(); assertThat(test(0)).isFalse(); assertThat(test(1)).isTrue(); } } @Test public void test() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("return 1.equals(num);"); } @Test @NotYetImplemented("Inline boxed types") public void testInline() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("return ((Integer) 1).equals(i);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestDeboxing5.java ================================================ package jadx.tests.integration.others; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDeboxing5 extends IntegrationTest { @SuppressWarnings("WrapperTypeMayBePrimitive") public static class TestCls { private static String type; public static void test(String[] args) { Float f = (float) -47.99; Boolean b = args.length == 0; Object o = ((b) ? false : f); call(o); } public static void call(Object o) { if (o instanceof Boolean) { type = "Boolean"; } if (o instanceof Float) { type = "Float"; } } private static void verify(String[] arr, String str) { type = null; test(arr); assertThat(type).isEqualTo(str); } public void check() { verify(new String[0], "Boolean"); verify(new String[] { "1" }, "Float"); } } @TestWithProfiles(TestProfile.D8_J11) public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("boolean valueOf"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestDefConstructorNotRemoved.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDefConstructorNotRemoved extends IntegrationTest { public static class TestCls { static { // empty } public static class A { public final String s; public A() { s = "a"; } public A(String str) { s = str; } } public static class B extends A { public B() { super(); } public B(String s) { super(s); } } public void check() { new A(); new A("a"); new B(); new B("b"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("super();") .doesNotContain("static {") .containsOne("public B() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestDefConstructorWithAnnotation.java ================================================ package jadx.tests.integration.others; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestDefConstructorWithAnnotation extends IntegrationTest { public static class TestCls { @AnnotationTest public TestCls() { } @Target(ElementType.CONSTRUCTOR) @Retention(RetentionPolicy.RUNTIME) public @interface AnnotationTest { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("@AnnotationTest"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestDuplicateCast.java ================================================ package jadx.tests.integration.others; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static jadx.core.dex.instructions.InsnType.CHECK_CAST; import static jadx.core.dex.instructions.InsnType.RETURN; import static jadx.core.utils.BlockUtils.collectAllInsns; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Test duplicate 'check-cast' instruction produced because of bug in javac: * http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6246854 */ public class TestDuplicateCast extends IntegrationTest { public static class TestCls { public int[] method(Object o) { return (int[]) o; } } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); MethodNode mth = getMethod(cls, "method"); assertThat(cls) .code() .contains("return (int[]) o;"); List insns = collectAllInsns(mth.getBasicBlocks()); assertThat(insns).hasSize(1); InsnNode insnNode = insns.get(0); assertThat(insnNode.getType()).isEqualTo(RETURN); assertThat(insnNode.getArg(0).isInsnWrap()).isTrue(); InsnNode wrapInsn = ((InsnWrapArg) insnNode.getArg(0)).getWrapInsn(); assertThat(wrapInsn.getType()).isEqualTo(CHECK_CAST); assertThat(wrapInsn.getArg(0).isInsnWrap()).isFalse(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestExplicitOverride.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestExplicitOverride extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .countString(1, "@Override"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestFieldAccessReorder.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldAccessReorder extends IntegrationTest { public static class TestCls { private long field = 10; public final boolean test() { long value = longCall(); long diff = value - this.field; this.field = value; return diff > 250; } public static long longCall() { return 261L; } public void check() { assertThat(test()).isTrue(); } } @Test public void test() { noDebugInfo(); getClassNode(TestCls.class); // auto check should pass } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestFieldInit2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestFieldInit2 extends IntegrationTest { public static class TestCls { public interface BasicAbstract { void doSomething(); } public BasicAbstract x = new BasicAbstract() { @Override public void doSomething() { y = 1; } }; public int y = 0; public TestCls() { } public TestCls(int z) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("x = new BasicAbstract() {") .containsOne("y = 0;") .containsLines(1, "public TestFieldInit2$TestCls(int z) {", "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestFieldInit3.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldInit3 extends IntegrationTest { public static class TestCls { public abstract static class A { public int field = 4; } public static final class B extends A { public B() { // IPUT for A.field super.field = 7; } } public static final class C extends A { public int other = 11; public C() { // IPUT for C.field not A.field !!! this.field = 9; } } public static final class D extends A { } public void check() { assertThat(new B().field).isEqualTo(7); assertThat(new C().field).isEqualTo(9); assertThat(new C().other).isEqualTo(11); assertThat(new D().field).isEqualTo(4); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("public int field = 4;") .containsOne("field = 7;") .containsOne("field = 9;") .containsOne("public int other = 11;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestFieldInitInTryCatch.java ================================================ package jadx.tests.integration.others; import java.net.MalformedURLException; import java.net.URL; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldInitInTryCatch extends IntegrationTest { public static class TestCls { public static final URL A; static { try { A = new URL("http://www.example.com/"); } catch (MalformedURLException e) { throw new RuntimeException(e); } } } public static class TestCls2 { public static final URL[] A; static { try { A = new URL[] { new URL("http://www.example.com/") }; } catch (MalformedURLException e) { throw new RuntimeException(e); } } } public static class TestCls3 { public static final String[] A; static { try { A = new String[] { "a" }; // Note: follow code will not be extracted: // a = new String[]{new String("a")}; new URL("http://www.example.com/"); } catch (MalformedURLException e) { throw new RuntimeException(e); } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("public static final URL A;") .containsOne("A = new URL(\"http://www.example.com/\");") .containsLines(2, "try {", indent(1) + "A = new URL(\"http://www.example.com/\");", "} catch (MalformedURLException e) {"); } @Test public void test2() { assertThat(getClassNode(TestCls2.class)) .code() .containsLines(2, "try {", indent(1) + "A = new URL[]{new URL(\"http://www.example.com/\")};", "} catch (MalformedURLException e) {"); } @Test public void test3() { assertThat(getClassNode(TestCls3.class)) .code() // don't move code from try/catch .containsOne("public static final String[] A;") .containsOne("A = new String[]{\"a\"};"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestFieldInitNegative.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Negative case for field initialization move (#1599). * Can't reorder with other instance methods. */ public class TestFieldInitNegative extends IntegrationTest { public static class TestCls { StringBuilder sb; int field; public TestCls() { initBuilder(new StringBuilder("sb")); this.field = initField(); this.sb.append(this.field); } private void initBuilder(StringBuilder sb) { this.sb = sb; } private int initField() { return sb.length(); } public String getStr() { return sb.toString(); } public void check() { assertThat(new TestCls().getStr()).isEqualTo("sb2"); // no NPE } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("int field = initField();") .containsOne("this.field = initField();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestFieldInitOrder.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldInitOrder extends IntegrationTest { public static class TestCls { private final StringBuilder sb = new StringBuilder(); private final String a = sb.append("a").toString(); private final String b = sb.append("b").toString(); private final String c = sb.append("c").toString(); private final String result = sb.toString(); public void check() { assertThat(result).isEqualTo("abc"); assertThat(a).isEqualTo("a"); assertThat(b).isEqualTo("ab"); assertThat(c).isEqualTo("abc"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("TestCls() {") // constructor removed .doesNotContain("String result;") .containsOne("String result = this.sb.toString();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestFieldInitOrder2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldInitOrder2 extends SmaliTest { @SuppressWarnings({ "SpellCheckingInspection", "StaticVariableName" }) public static class TestCls { static String ZPREFIX = "SOME_"; private static final String VALUE = ZPREFIX + "VALUE"; public void check() { assertThat(VALUE).isEqualTo("SOME_VALUE"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("private static final String VALUE = ZPREFIX + \"VALUE\";"); } @Test public void testSmali() { assertThat(getClassNodeFromSmali()) .runDecompiledAutoCheck(this) .code() .containsOne("private static final String VALUE = ZPREFIX + \"VALUE\";"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestFieldInitOrderStatic.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldInitOrderStatic extends IntegrationTest { @SuppressWarnings("ConstantName") public static class TestCls { private static final StringBuilder sb = new StringBuilder(); private static final String a = sb.append("a").toString(); private static final String b = sb.append("b").toString(); private static final String c = sb.append("c").toString(); private static final String result = sb.toString(); public void check() { assertThat(result).isEqualTo("abc"); assertThat(a).isEqualTo("a"); assertThat(b).isEqualTo("ab"); assertThat(c).isEqualTo("abc"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("static {") .doesNotContain("String result;") .containsOne("String result = sb.toString();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestFieldUsageMove.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldUsageMove extends SmaliTest { public static class TestCls { public static void test(Object obj) { if (obj instanceof Boolean) { System.out.println("Boolean: " + obj); } if (obj instanceof Float) { System.out.println("Float: " + obj); } } } @TestWithProfiles(TestProfile.D8_J11) public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("System.out.println(\"Boolean: \" +") .containsOne("System.out.println(\"Float: \" +"); } @Test public void testSmali() { assertThat(getClassNodeFromSmali()) .code() .containsOne("System.out.println(\"Boolean: \" +") .containsOne("System.out.println(\"Float: \" +"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestFixClassAccessModifiers.java ================================================ package jadx.tests.integration.others; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFixClassAccessModifiers extends SmaliTest { // @formatter:off /* // class others.TestCls public Cls.InnerCls field; // class others.Cls public static class Cls { private static class InnerCls { } } */ // @formatter:on @Test public void test() { List classes = loadFromSmaliFiles(); assertThat(searchCls(classes, "others.Cls")) .code(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestFloatValue.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.within; public class TestFloatValue extends IntegrationTest { public static class TestCls { public float[] test() { float[] fa = { 0.55f }; fa[0] /= 2; return fa; } public void check() { assertThat(test()[0]).isCloseTo(0.275f, within(0.0001f)); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("1073741824") .containsOne("0.55f") .containsOne("fa[0] = fa[0] / 2.0f;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestIfInTry.java ================================================ package jadx.tests.integration.others; import java.io.File; import java.io.IOException; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestIfInTry extends IntegrationTest { public static class TestCls { public File dir; public int test() { try { int a = f(); if (a != 0) { return a; } } catch (Exception e) { // skip } try { f(); return 1; } catch (IOException e) { return -1; } } private int f() throws IOException { return 0; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (a != 0) {") .containsOne("} catch (Exception e) {") .countString(2, "try {") .countString(3, "f()") .containsOne("return 1;") .containsOne("} catch (IOException e") .containsOne("return -1;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestIfTryInCatch.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIfTryInCatch extends IntegrationTest { public static class TestCls { public Exception exception; private java.lang.Object data; public java.lang.Object test(final Object obj) { exception = null; try { return f(); } catch (Exception e) { if (a(e) && b(obj)) { try { return f(); } catch (Exception exc) { e = exc; } } System.out.println("Exception" + e); exception = e; return data; } } private static boolean b(Object obj) { return obj == null; } private static boolean a(Exception e) { return e instanceof RuntimeException; } private Object f() { return null; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(2, "try {") .containsOne("if (") .countString(2, "return f();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestIncorrectFieldSignature.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIncorrectFieldSignature extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("public static boolean A;") .containsOne("public static Boolean B;") .countString(2, "/* JADX INFO: Incorrect field signature:"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestIncorrectMethodSignature.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue #858. * Incorrect method signature change argument type and shift register numbers */ public class TestIncorrectMethodSignature extends SmaliTest { @Test public void test() { allowWarnInCode(); assertThat(getClassNodeFromSmali()) .code() .containsOne("public TestIncorrectMethodSignature(String str) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestInlineVarArg.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInlineVarArg extends SmaliTest { @Test public void test() { noDebugInfo(); assertThat(getClassNodeFromSmali()) .code() .containsOne("f(\"a\", \"b\", \"c\");"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestInsnsBeforeSuper.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInsnsBeforeSuper extends SmaliTest { // @formatter:off /* public class A { public A(String s) { } } public class B extends A { public B(String str) { checkNull(str); super(str); } public void checkNull(Object o) { if (o == null) { throw new NullPointerException(); } } } */ // @formatter:on @Test public void test() { allowWarnInCode(); assertThat(getClassNodeFromSmaliFiles("B")) .code() .containsOne("checkNull(str);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestInsnsBeforeSuper2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInsnsBeforeSuper2 extends SmaliTest { // @formatter:off /* public class TestInsnsBeforeSuper2 extends java.lang.Exception { private int mErrorType; public TestInsnsBeforeSuper2(java.lang.String r9, int r10) { r8 = this; r0 = r8 r1 = r9 r2 = r10 r3 = r0 r4 = r1 r5 = r2 r6 = r1 r0.(r6) r7 = 0 r0.mErrorType = r7 r0.mErrorType = r2 return } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("super(message);") .containsOne("this.mErrorType = errorType;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestInsnsBeforeThis.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInsnsBeforeThis extends SmaliTest { // @formatter:off /* public class A { public A(String str) { checkNull(str); this(str.length()); } public A(int i) { } public void checkNull(Object o) { if (o == null) { throw new NullPointerException(); } } } */ // @formatter:on @Test public void test() { allowWarnInCode(); assertThat(getClassNodeFromSmali()) .code() .containsOne("checkNull(str);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestInterfaceDefaultMethod.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInterfaceDefaultMethod extends IntegrationTest { public static class TestCls { @SuppressWarnings("UnnecessaryInterfaceModifier") public interface ITest { void test1(); default void test2() { } static void test3() { } abstract void test4(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("static default") .doesNotContain("abstract") .containsOne("void test1();") .containsOne("default void test2() {") .containsOne("static void test3() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestInvalidExceptions.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; class TestInvalidExceptions extends SmaliTest { @Test void test() { allowWarnInCode(); assertThat(getClassNodeFromSmali()) .code() .containsOne("invalidException() throws FileNotFoundException {") .containsOne("Byte code manipulation detected: skipped illegal throws declaration") .removeBlockComments() .doesNotContain("String"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestInvalidExceptions2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; class TestInvalidExceptions2 extends SmaliTest { @Test void test() { allowWarnInCode(); disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOne("throwPossibleExceptionType() throws UnknownTypeHierarchyException {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestIssue13a.java ================================================ package jadx.tests.integration.others; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIssue13a extends IntegrationTest { public static class TestCls { private static final String TAG = "Parcel"; private static final Map>> M_CREATORS = new HashMap<>(); @SuppressWarnings({ "unchecked", "ConstantConditions", "Java8MapApi", "rawtypes" }) public final T test(ClassLoader loader) { String name = readString(); if (name == null) { return null; } Parcelable.Creator creator; synchronized (M_CREATORS) { Map> map = M_CREATORS.get(loader); if (map == null) { map = new HashMap<>(); M_CREATORS.put(loader, map); } creator = (Parcelable.Creator) map.get(name); if (creator == null) { try { Class c = loader == null ? Class.forName(name) : Class.forName(name, true, loader); Field f = c.getField("CREATOR"); creator = (Parcelable.Creator) f.get(null); } catch (IllegalAccessException e) { Log.e(TAG, '1' + name + ", e: " + e); throw new RuntimeException('2' + name); } catch (ClassNotFoundException e) { Log.e(TAG, '3' + name + ", e: " + e); throw new RuntimeException('4' + name); } catch (ClassCastException e) { throw new RuntimeException('5' + name); } catch (NoSuchFieldException e) { throw new RuntimeException('6' + name); } if (creator == null) { throw new RuntimeException('7' + name); } map.put(name, creator); } } if (creator instanceof Parcelable.ClassLoaderCreator) { return ((Parcelable.ClassLoaderCreator) creator).createFromParcel(this, loader); } return creator.createFromParcel(this); } private String readString() { return ""; } private class Parcelable { public class Creator { public T createFromParcel(TestCls testCls) { return null; } } public class ClassLoaderCreator extends Creator { public T createFromParcel(TestCls testCls, ClassLoader loader) { return null; } } } private static class Log { public static void e(String tag, String s) { } } } @Test public void test() { disableCompilation(); ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); for (int i = 1; i <= 7; i++) { assertThat(code).containsOne("'" + i + '\''); } // TODO: add additional checks assertThat(code).doesNotContain("Throwable"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestIssue13b.java ================================================ package jadx.tests.integration.others; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Properties; import java.util.concurrent.CountDownLatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestIssue13b extends IntegrationTest { public static class TestCls { private static final String PROPERTIES_FILE = ""; private static final String TAG = ""; private final CountDownLatch mInitializedLatch = new CountDownLatch(1); public int mC2KServerPort = 0; private String mSuplServerHost = ""; public int mSuplServerPort = 0; private String mC2KServerHost = ""; public TestCls() { Properties mProperties = new Properties(); try { File file = new File(PROPERTIES_FILE); FileInputStream stream = new FileInputStream(file); mProperties.load(stream); stream.close(); mSuplServerHost = mProperties.getProperty("SUPL_HOST"); String portString = mProperties.getProperty("SUPL_PORT"); if (mSuplServerHost != null && portString != null) { try { mSuplServerPort = Integer.parseInt(portString); } catch (NumberFormatException e) { Log.e(TAG, "unable to parse SUPL_PORT: " + portString); } } mC2KServerHost = mProperties.getProperty("C2K_HOST"); portString = mProperties.getProperty("C2K_PORT"); if (mC2KServerHost != null && portString != null) { try { mC2KServerPort = Integer.parseInt(portString); } catch (NumberFormatException e) { Log.e(TAG, "unable to parse C2K_PORT: " + portString); } } } catch (IOException e) { Log.e(TAG, "Could not open GPS configuration file " + PROPERTIES_FILE); } Thread mThread = new Thread(); mThread.start(); while (true) { try { mInitializedLatch.await(); break; } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } private static class Log { public static void e(String tag, String s) { } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(4, "} catch (") .countString(3, "Log.e(") .containsOne("Thread.currentThread().interrupt();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestJavaDup2x2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.RaungTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestJavaDup2x2 extends RaungTest { @Test public void test() { assertThat(getClassNodeFromRaung()) .code() .containsOne("dArr[0] = 127.5d;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestJavaDupInsn.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestJavaDupInsn extends IntegrationTest { public static class TestCls { private MethodNode mth; private BlockNode block; private SSAVar[] vars; private int[] versions; public SSAVar test(RegisterArg regArg) { int regNum = regArg.getRegNum(); int version = versions[regNum]++; SSAVar ssaVar = mth.makeNewSVar(regNum, version, regArg); vars[regNum] = ssaVar; return ssaVar; } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestJavaJSR.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.RaungTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestJavaJSR extends RaungTest { @Test public void test() { assertThat(getClassNodeFromRaung()) .code() .containsLines(2, "InputStream in = url.openStream();", "try {", indent() + "return call(in);", "} finally {", indent() + "in.close();", "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestJavaSwap.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.RaungTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestJavaSwap extends RaungTest { @SuppressWarnings("StringBufferReplaceableByString") public static class TestCls { private Iterable field; @Override public String toString() { String string = String.valueOf(this.field); return new StringBuilder(8 + String.valueOf(string).length()) .append("concat(").append(string).append(")") .toString(); } } @Test public void testJava() { useJavaInput(); assertThat(getClassNode(TestCls.class)) .code(); } @Test public void test() { useJavaInput(); assertThat(getClassNodeFromRaung()) .code(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestJsonOutput.java ================================================ package jadx.tests.integration.others; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.api.JadxArgs.OutputFormatEnum.JSON; public class TestJsonOutput extends IntegrationTest { public static class TestCls { private final String prefix = "list: "; static { System.out.println("test"); } public void test(boolean b, List list) { if (b) { System.out.println(prefix + list); } } public static class Inner implements Runnable { @Override public void run() { System.out.println("run"); } } } @Test public void test() { disableCompilation(); args.setOutputFormat(JSON); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("\"offset\": \"0x") .containsOne("public static class Inner implements Runnable"); } @Test public void testFallback() { disableCompilation(); setFallback(); args.setOutputFormat(JSON); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("\"offset\": \"0x") .containsOne("public static class Inner implements java.lang.Runnable"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestLoopInTry.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestLoopInTry extends IntegrationTest { public static class TestCls { private static boolean b = true; public int test() { try { if (b) { throw new Exception(); } while (f()) { s(); } } catch (Exception e) { System.out.println("exception"); return 1; } return 0; } private static void s() { } private static boolean f() { return false; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("try {") .containsOne("if (b) {") .containsOne("throw new Exception();") .containsOne("while (f()) {") .containsOne("s();") .containsOne("} catch (Exception e) {") .containsOne("return 1;") .containsOne("return 0;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestMethodParametersAttribute.java ================================================ package jadx.tests.integration.others; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.Arrays; import java.util.stream.Collectors; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestMethodParametersAttribute extends IntegrationTest { public static class TestCls { public String test(String paramStr, final int number) { return paramStr + number; } public String paramNames() throws NoSuchMethodException { Method testMethod = TestCls.class.getMethod("test", String.class, int.class); return Arrays.stream(testMethod.getParameters()) .map(Parameter::getName) .collect(Collectors.joining(", ")); } public void check() throws NoSuchMethodException { assertThat(paramNames()).isEqualTo("paramStr, number"); } } @TestWithProfiles({ TestProfile.JAVA8, TestProfile.D8_J11 }) public void test() { getCompilerOptions().addArgument("-parameters"); noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("public String test(String paramStr, final int number) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestMissingExceptions.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; class TestMissingExceptions extends SmaliTest { @Test void test() { assertThat(getClassNodeFromSmali()) .code() .countString(6, "FileNotFoundException"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestMoveInline.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestMoveInline extends SmaliTest { // @formatter:off /* public final void Y(int i) throws k { int i2 = 0; while ((i & (-128)) != 0) { this.h[i2] = (byte) ((i & 127) | 128); i >>>= 7; i2++; } byte[] bArr = this.h; bArr[i2] = (byte) i; this.a.k(bArr, 0, i2 + 1); } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() // check operations order .containsLines(3, "i >>>= 7;", "i2++;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestMultipleNOPs.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; public class TestMultipleNOPs extends SmaliTest { @Test public void test() { disableCompilation(); // expected no errors loadFromSmaliFiles(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestN21.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestN21 extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .countString(2, "while ("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestNullInline.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNullInline extends IntegrationTest { @SuppressWarnings({ "RedundantCast", "DataFlowIssue", "unused" }) public static class TestCls { public static Long test(Double d1) { T1 t1 = (T1) null; return t1.t2.l; } static class T2 { public long l; } static class T1 { public T2 t2; public T1(T2 t2) { this.t2 = t2; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("Long.valueOf(t1.t2.l);"); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestOverridePackagePrivateMethod.java ================================================ package jadx.tests.integration.others; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestOverridePackagePrivateMethod extends SmaliTest { // @formatter:off /* ----------------------------------------------------------- package test; public class A { void a() { // package-private } } ----------------------------------------------------------- package test; public class B extends A { @Override // test.A public void a() { } } ----------------------------------------------------------- package other; import test.A; public class C extends A { // No @Override here public void a() { } } ----------------------------------------------------------- */ // @formatter:on @Test public void test() { commonChecks(); } @Test public void testDontChangeAccFlags() { getArgs().setRespectBytecodeAccModifiers(true); commonChecks(); } private void commonChecks() { List classes = loadFromSmaliFiles(); assertThat(searchCls(classes, "test.A")) .code() .doesNotContain("/* access modifiers changed") .containsLine(1, "void a() {"); assertThat(searchCls(classes, "test.B")).code().containsOne("@Override"); assertThat(searchCls(classes, "other.C")).code().doesNotContain("@Override"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestOverridePrivateMethod.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestOverridePrivateMethod extends IntegrationTest { public static class TestCls { public static class BaseClass { private int a() { return 1; } } public static class MyClass extends BaseClass { public int a() { return 2; } } public void check() { assertThat(new MyClass().a()).isEqualTo(2); assertThat(new BaseClass().a()).isEqualTo(1); // TODO: assertThat(((BaseClass) new MyClass()).a()).isEqualTo(1); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("@Override"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestOverrideStaticMethod.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestOverrideStaticMethod extends IntegrationTest { public static class TestCls { public static class BaseClass { public static int a() { return 1; } } public static class MyClass extends BaseClass { public static int a() { return 2; } } public void check() { assertThat(BaseClass.a()).isEqualTo(1); assertThat(MyClass.a()).isEqualTo(2); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("@Override"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestOverrideWithSameName.java ================================================ package jadx.tests.integration.others; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestOverrideWithSameName extends SmaliTest { //@formatter:off /* interface A { B a(); C a(); } abstract class B implements A { @Override public C a() { return null; } } public class C extends B { @Override public B a() { return null; } } */ //@formatter:on @Test public void test() { List clsNodes = loadFromSmaliFiles(); assertThat(searchCls(clsNodes, "test.A")) .code() .containsOne("C mo0a();") // assume second method was renamed .doesNotContain("@Override"); ClassNode bCls = searchCls(clsNodes, "test.B"); assertThat(bCls) .code() .containsOne("C mo0a() {") .containsOne("@Override"); assertThat(getMethod(bCls, "a").get(AType.METHOD_OVERRIDE).getOverrideList()) .singleElement() .satisfies(mth -> assertThat(mth.getMethodInfo().getDeclClass().getShortName()).isEqualTo("A")); ClassNode cCls = searchCls(clsNodes, "test.C"); assertThat(cCls) .code() .containsOne("B a() {") .containsOne("@Override"); assertThat(getMethod(cCls, "a").get(AType.METHOD_OVERRIDE).getOverrideList()) .singleElement() .satisfies(mth -> assertThat(mth.getMethodInfo().getDeclClass().getShortName()).isEqualTo("A")); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestOverrideWithTwoBases.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestOverrideWithTwoBases extends IntegrationTest { public static class TestCls { public abstract static class BaseClass { public abstract int a(); } public interface I { int a(); } public static class Cls extends BaseClass implements I { @Override public int a() { return 2; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("@Override"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestOverrideWithTwoBases2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestOverrideWithTwoBases2 extends IntegrationTest { public static class TestCls { public interface I { int a(); } public abstract static class BaseCls implements I { } public static class Cls extends BaseCls implements I { @Override public int a() { return 2; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("@Override"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestPrimitiveCasts.java ================================================ package jadx.tests.integration.others; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestPrimitiveCasts extends IntegrationTest { public static class TestCls { public void test() { useShort((short) 0); useShort((short) getInt()); useByte((byte) 0); useByte((byte) getInt()); useChar((char) 0); useChar((char) getInt()); useShort((short) 0L); useShort((short) getLong()); useByte((byte) 0L); useByte((byte) getLong()); useChar((char) 0L); useChar((char) getLong()); useShort((short) ' '); useShort((short) getChar()); useByte((byte) ' '); useByte((byte) getChar()); useInt((byte) 7); useInt((char) ' '); useInt(getChar()); useInt((int) 2L); useInt((int) getLong()); } private long getLong() { return 1L; } private char getChar() { return ' '; } private int getInt() { return 1; } private void useChar(char c) { } private void useByte(byte b) { } private void useShort(short s) { } private void useInt(int i) { } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("(0)") .doesNotContain(") ((int) getLong())"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestPrimitiveCasts2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; // Source: https://github.com/skylot/jadx/issues/1620 public class TestPrimitiveCasts2 extends IntegrationTest { @SuppressWarnings("DataFlowIssue") public static class TestCls { long instanceCount; { float f = 50.231F; instanceCount &= (long) f; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestRedundantBrackets.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestRedundantBrackets extends IntegrationTest { public static class TestCls { public boolean method(String str) { return str.indexOf('a') != -1; } public int method2(Object obj) { if (obj instanceof String) { return ((String) obj).length(); } return 0; } public int method3(int a, int b) { if (a + b < 10) { return a; } if ((a & b) != 0) { return a * b; } return b; } public void method4(int num) { if (num == 4 || num == 6 || num == 8 || num == 10) { method2(null); } } public void method5(int[] a, int n) { a[1] = n * 2; a[n - 1] = 1; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("(-1)") .doesNotContain("return;") .contains("if (obj instanceof String) {") .contains("return ((String) obj).length();") .contains("a + b < 10") .contains("(a & b) != 0") .contains("if (num == 4 || num == 6 || num == 8 || num == 10)") .contains("a[1] = n * 2;") .contains("a[n - 1] = 1;") .contains("public int method2(Object obj) {") // argument type isn't changed to String // cast not eliminated .contains("((String) obj).length()"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestRedundantReturn.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.fail; public class TestRedundantReturn extends IntegrationTest { public static class TestCls { public void test(int num) { if (num == 4) { fail(""); } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("return;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestReturnWrapping.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestReturnWrapping extends IntegrationTest { public static class TestCls { public static int f1(int arg0) { switch (arg0) { case 1: return 255; } return arg0 + 1; } public static Object f2(Object arg0, int arg1) { Object ret = null; int i = arg1; if (arg0 == null) { return ret + Integer.toHexString(i); } i++; try { ret = new Object().getClass(); } catch (Exception e) { ret = "Qwerty"; } return i > 128 ? arg0.toString() + ret.toString() : i; } public static int f3(int arg0) { while (arg0 > 10) { int abc = 951; if (arg0 == 255) { return arg0 + 2; } arg0 -= abc; } return arg0; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("return 255;") .contains("return arg0 + 1;").contains("return i > 128 ? arg0.toString() + ret.toString() : Integer.valueOf(i);") .contains("return arg0 + 2;") .contains("arg0 -= 951;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestShadowingSuperMember.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestShadowingSuperMember extends IntegrationTest { public static class TestCls { public static class C { public C(String s) { } } public static class A { public int a00; public A(String s) { } } public static class B extends A { public C a00; public B(String str) { super(str); } public int add(int b) { return super.a00 + b; } public int sub(int b) { return ((A) this).a00 - b; } } public void check() { B b = new B(""); ((A) b).a00 = 2; assertThat(b.add(3)).isEqualTo(5); assertThat(b.sub(3)).isEqualTo(-1); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return super.a00 + b;") .containsOne("return super.a00 - b;") .containsOne("((A) b).a00 = 2;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestStaticFieldsInit.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestStaticFieldsInit extends IntegrationTest { public static class TestCls { public static final String S1 = "1"; public static final String S2 = "12".substring(1); public static final String S3 = null; public static final String S4; public static final String S5 = "5"; public static String s6 = "6"; static { if (S5.equals("?")) { S4 = "?"; } else { S4 = "4"; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("public static final String S2 = null;") .contains("public static final String S3 = null;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestStaticMethod.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestStaticMethod extends IntegrationTest { public static class TestCls { static { f(); } private static void f() { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("static {") .contains("private static void f() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestStringBuilderElimination extends IntegrationTest { public static class MyException extends Exception { private static final long serialVersionUID = 4245254480662372757L; public MyException(String str, Exception e) { super("msg:" + str, e); } public void method(int k) { System.out.println("k=" + k); } } @Test public void test() { assertThat(getClassNode(MyException.class)) .code() .contains("MyException(String str, Exception e) {") .contains("super(\"msg:\" + str, e);") .doesNotContain("new StringBuilder") .contains("System.out.println(\"k=\" + k);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.core.dex.visitors.SimplifyVisitor; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; /** * Test the StringBuilder simplification part of {@link SimplifyVisitor} * * @author Jan Peter Stotz */ @SuppressWarnings("StringBufferReplaceableByString") public class TestStringBuilderElimination2 extends IntegrationTest { public static class TestCls1 { public String test() { return new StringBuilder("[init]").append("a1").append('c').append(2).append(0L).append(1.0f).append(2.0d).append(true) .toString(); } } @Test public void test1() { JadxAssertions.assertThat(getClassNode(TestCls1.class)) .code() .contains("return \"[init]a1c201.02.0true\";"); } public static class TestCls2 { public String test() { // A chain with non-final variables String sInit = "[init]"; String s = "a1"; char c = 'c'; int i = 1; long l = 2; float f = 1.0f; double d = 2.0d; boolean b = true; return new StringBuilder(sInit).append(s).append(c).append(i).append(l).append(f).append(d).append(b).toString(); } } @Test public void test2() { JadxAssertions.assertThat(getClassNode(TestCls2.class)) .code() .contains("return \"[init]a1c121.02.0true\";"); } public static class TestClsStringUtilsReverse { /** * Simplified version of org.apache.commons.lang3.StringUtils.reverse() */ public static String reverse(final String str) { return new StringBuilder(str).reverse().toString(); } } @Test public void test3() { JadxAssertions.assertThat(getClassNode(TestClsStringUtilsReverse.class)) .code() .contains("return new StringBuilder(str).reverse().toString();"); } public static class TestClsChainWithDelete { public String test() { // a chain we can't simplify return new StringBuilder("[init]").append("a1").delete(1, 2).toString(); } } @Test public void testChainWithDelete() { JadxAssertions.assertThat(getClassNode(TestClsChainWithDelete.class)) .code() .contains("return new StringBuilder(\"[init]\").append(\"a1\").delete(1, 2).toString();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination3.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestStringBuilderElimination3 extends IntegrationTest { public static class TestCls { public static String test(String a) { StringBuilder sb = new StringBuilder(); sb.append("result = "); sb.append(a); return sb.toString(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("return \"result = \" + a;") .doesNotContain("new StringBuilder()"); } public static class TestClsNegative { private String f = "first"; public String test() { StringBuilder sb = new StringBuilder(); sb.append("before = "); sb.append(this.f); updateF(); sb.append(", after = "); sb.append(this.f); return sb.toString(); } private void updateF() { this.f = "second"; } public void check() { assertThat(test()).isEqualTo("before = first, after = second"); } } @Test public void testNegative() { JadxAssertions.assertThat(getClassNode(TestClsNegative.class)) .code() .contains("return sb.toString();") .contains("new StringBuilder()"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination4Neg.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestStringBuilderElimination4Neg extends IntegrationTest { public static class TestCls { private K k; private V v; public String test() { StringBuilder sb = new StringBuilder(); sb.append(k); sb.append('='); sb.append(v); return sb.toString(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("sb.append('=');"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination5.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestStringBuilderElimination5 extends IntegrationTest { public static class TestCls { @SuppressWarnings("StringConcatenationInLoop") public static String test(long[] a) { String s = ""; final char[] hexChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; for (int i = a.length - 1; i >= 0; i--) { s += hexChars[(int) (a[i] >>> 60) & 0x0f]; s += hexChars[(int) (a[i] >>> 56) & 0x0f]; s += hexChars[(int) (a[i] >>> 52) & 0x0f]; s += hexChars[(int) (a[i] >>> 48) & 0x0f]; s += hexChars[(int) (a[i] >>> 44) & 0x0f]; s += hexChars[(int) (a[i] >>> 40) & 0x0f]; s += hexChars[(int) (a[i] >>> 36) & 0x0f]; s += hexChars[(int) (a[i] >>> 32) & 0x0f]; s += hexChars[(int) (a[i] >>> 28) & 0x0f]; s += hexChars[(int) (a[i] >>> 24) & 0x0f]; s += hexChars[(int) (a[i] >>> 20) & 0x0f]; s += hexChars[(int) (a[i] >>> 16) & 0x0f]; s += hexChars[(int) (a[i] >>> 12) & 0x0f]; s += hexChars[(int) (a[i] >>> 8) & 0x0f]; s += hexChars[(int) (a[i] >>> 4) & 0x0f]; s += hexChars[(int) (a[i]) & 0x0f]; s += " "; } return s; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain(".append("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestStringConcatJava11.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.RaungTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestStringConcatJava11 extends RaungTest { public static class TestCls { public String test(final String s) { return s + "test"; } //@formatter:off /* Dynamic call looks like this: public String test(final String s) { return java.lang.invoke.StringConcatFactory.makeConcatWithConstants( java.lang.invoke.MethodHandles.lookup(), "makeConcatWithConstants", java.lang.invoke.MethodType.fromMethodDescriptorString("(Ljava/lang/String;)Ljava/lang/String;", this.getClass().getClassLoader()), "\u0001test" ).dynamicInvoker().invoke(s); } */ //@formatter:on public String test2(final String s) { return s + "test" + s + 7; } } @Test public void test() { assertThat(getClassNodeFromRaung()) .code() .containsOne("return str + \"test\";") .containsOne("return str + \"test\" + str + 7;"); } @Test public void testJava8() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("return str + \"test\";") .containsOneOf( "return str + \"test\" + str + 7;", "return str + \"test\" + str + \"7\";"); // dynamic concat add const to string recipe } @TestWithProfiles({ TestProfile.D8_J11, TestProfile.JAVA11 }) public void testJava11() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("return str + \"test\";") .containsOne("return str + \"test\" + str + \"7\";"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestStringConcatWithoutResult.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestStringConcatWithoutResult extends IntegrationTest { private static final Logger LOG = LoggerFactory.getLogger(TestStringConcatWithoutResult.class); public static class TestCls { public static final boolean LOG_DEBUG = false; public void test(int i) { String msg = "Input arg value: " + i; if (LOG_DEBUG) { LOG.debug(msg); } } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne(" = \"Input arg value: \" + i;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestStringConstructor.java ================================================ package jadx.tests.integration.others; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestStringConstructor extends IntegrationTest { public static class TestCls { public String tag = new String(new byte[] { 'a', 'b', 'c' }); } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("abc"); } public static class TestCls2 { public String tag = new String(new byte[] { 'a', 'b', 'c' }, StandardCharsets.UTF_8); } @Test public void test2() { JadxAssertions.assertThat(getClassNode(TestCls2.class)) .code() .containsOne("new String(\"abc\".getBytes(), StandardCharsets.UTF_8)"); } public static class TestCls3 { public String tag = new String(new byte[] { 1, 2, 3, 'a', 'b', 'c' }); } @Test public void test3() { JadxAssertions.assertThat(getClassNode(TestCls3.class)) .code() .containsOne("\\u0001\\u0002\\u0003abc"); } public static class TestCls4 { public String tag = new String(new char[] { 1, 2, 3, 'a', 'b', 'c' }); } @Test public void test4() { JadxAssertions.assertThat(getClassNode(TestCls4.class)) .code() .containsOne("\\u0001\\u0002\\u0003abc"); } public static class TestCls5 { public String tag = new String(new char[] { 1, 2, 3, 'a', 'b' }); } @Test public void test5() { JadxAssertions.assertThat(getClassNode(TestCls5.class)) .code() .containsOne("{1, 2, 3, 'a', 'b'}"); } public static class TestClsNegative { public String tag = new String(); } @Test public void testNegative() { JadxAssertions.assertThat(getClassNode(TestClsNegative.class)) .code() .containsOne("tag = new String();"); } public static class TestClsNegative2 { public byte b = 32; public String tag = new String(new byte[] { 31, b }); } @Test public void testNegative2() { JadxAssertions.assertThat(getClassNode(TestClsNegative2.class)) .code() .containsOne("tag = new String(new byte[]{31, this.b});"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestSuperLoop.java ================================================ package jadx.tests.integration.others; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestSuperLoop extends SmaliTest { // @formatter:off /* public class A extends B { public int a; } public class B extends A { public int b; } */ // @formatter:on @Test public void test() { allowWarnInCode(); disableCompilation(); List clsList = loadFromSmaliFiles(); assertThat(searchCls(clsList, "A")) .code() .containsOne("public class A extends B {"); assertThat(searchCls(clsList, "B")) .code() .containsOne("public class B extends A {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestSyntheticConstructor.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSyntheticConstructor extends SmaliTest { // @formatter:off /* public class Test { static { new BuggyConstructor(); } } */ // @formatter:on @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmaliFiles("Test")) .code() .containsLine(2, "new BuggyConstructor();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestThrows.java ================================================ package jadx.tests.integration.others; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestThrows extends IntegrationTest { public static class MissingThrowsTest extends Exception { private void throwCustomException() throws MissingThrowsTest { throw new MissingThrowsTest(); } private void throwException() throws Exception { throw new Exception(); } private void throwRuntimeException1() { throw new RuntimeException(); } private void throwRuntimeException2() { throw new NullPointerException(); } private void throwError() { throw new Error(); } private void throwError2() { throw new OutOfMemoryError(); } @SuppressWarnings("checkstyle:illegalThrows") private void throwThrowable() throws Throwable { throw new Throwable(); } private void exceptionSource() throws FileNotFoundException { throw new FileNotFoundException(""); } public void mergeThrownExceptions() throws IOException { exceptionSource(); } public void rethrowThrowable() { try { } catch (Throwable t) { throw t; } } public void doSomething1(int i) throws FileNotFoundException { if (i == 1) { doSomething2(i); } else { doSomething1(i); } } public void doSomething2(int i) throws FileNotFoundException { if (i == 1) { exceptionSource(); } else { doSomething1(i); } } public int doSomething3(int i) throws IllegalArgumentException { if (i < 0) { throw new IllegalArgumentException(); } return 1; } public void noThrownExceptions1(InputStream i1) { try { i1.close(); } catch (IOException ignore) { } } public void noThrownExceptions2() { try { throw new FileNotFoundException(""); } catch (IOException ignore) { } } public void noThrownExceptions3() { int i = doSomething3(0); System.out.print(i); } } @Test public void test() { assertThat(getClassNode(MissingThrowsTest.class)) .code() .containsOne("throwCustomException() throws TestThrows$MissingThrowsTest {") .containsOne("throwException() throws Exception {") .containsOne("throwRuntimeException1() {") .containsOne("throwRuntimeException2() {") .containsOne("throwError() {") .containsOne("throwError2() {") .containsOne("throwThrowable() throws Throwable {") .containsOne("exceptionSource() throws FileNotFoundException {") .containsOne("mergeThrownExceptions() throws IOException {") .containsOne("rethrowThrowable() {") .containsOne("noThrownExceptions1(InputStream i1) {") .containsOne("noThrownExceptions2() {") .containsOne("noThrownExceptions3() {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestUsageApacheHttpClient.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestUsageApacheHttpClient extends SmaliTest { // @formatter:off /* package others; import org.apache.http.client.HttpClient; public class HttpClientTest { private HttpClient httpClient; } */ // @formatter:on @Test public void test() { disableCompilation(); ClassNode cls = getClassNodeFromSmali(); assertThat(cls.root().getGradleInfoStorage().isUseApacheHttpLegacy()).isTrue(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestWrongCode.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestWrongCode extends IntegrationTest { public static class TestCls { @SuppressWarnings("null") public int test() { int[] a = null; return a.length; } public int test2(int a) { if (a == 0) { } return a; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("return false.length;") .containsOne("int[] a = null;") .containsOne("return a.length;") .containsLines(2, "if (a == 0) {", "}", "return a;"); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/others/TestWrongCode2.java ================================================ package jadx.tests.integration.others; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestWrongCode2 extends IntegrationTest { public static class TestCls { @SuppressWarnings("ConstantConditions") public String test() { A a = null; a.str = ""; return a.str; } @SuppressWarnings("ConstantConditions") public int test2() { int[] a = null; a[1] = 2; return a[0]; } @SuppressWarnings({ "ConstantConditions", "SynchronizationOnLocalVariableOrMethodParameter" }) public boolean test3() { A a = null; synchronized (a) { return true; } } public boolean test4() { return null instanceof A; } // everything is 'A' :) @SuppressWarnings({ "MethodName", "LocalVariableName" }) // ignore checkstyle public A A() { A A = A(); A.A = A; return A; } @SuppressWarnings("MemberName") public static class A { public String str; public A A; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return a.str;"); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/rename/TestAnonymousInline.java ================================================ package jadx.tests.integration.rename; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestAnonymousInline extends IntegrationTest { public static class TestCls { public Runnable test() { return new Runnable() { @Override public void run() { System.out.println("run"); } }; } } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code() .containsOnlyOnce("return new Runnable() {"); assertThat(cls).reloadCode(this) .removeBlockComments() // remove comment about inlined class .containsOnlyOnce("return new Runnable() {") .doesNotContain("AnonymousClass1"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/rename/TestConstReplace.java ================================================ package jadx.tests.integration.rename; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConstReplace extends IntegrationTest { public static class TestCls { public static final String CONST = "SOME_CONST"; public String test() { return CONST; } } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code() .containsOnlyOnce("return CONST;"); assertThat(cls).reloadCode(this) .containsOnlyOnce("return CONST;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/rename/TestFieldRenameFormat.java ================================================ package jadx.tests.integration.rename; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; import com.google.gson.annotations.SerializedName; import jadx.api.data.ICodeRename; import jadx.api.data.IJavaNodeRef.RefType; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxCodeRename; import jadx.api.data.impl.JadxNodeRef; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldRenameFormat extends IntegrationTest { @SuppressWarnings({ "unused", "NonSerializableClassWithSerialVersionUID" }) public static class TestCls { private static final long serialVersionUID = -2619335455376089892L; @SerializedName("id") private int b; @SerializedName("title") private String c; @SerializedName("images") private List d; @SerializedName("authors") private List e; @SerializedName("description") private String f; } @Test public void test() { noDebugInfo(); String baseClsId = TestCls.class.getName(); List renames = Arrays.asList( fieldRename(baseClsId, "b:I", "id"), fieldRename(baseClsId, "c:Ljava/lang/String;", "title"), fieldRename(baseClsId, "e:Ljava/util/List;", "authors")); JadxCodeData codeData = new JadxCodeData(); codeData.setRenames(renames); getArgs().setCodeData(codeData); getArgs().setDeobfuscationOn(false); assertThat(getClassNode(TestCls.class)) .code() .containsOne("private int id;") .containsOne("private List authors;") .containsLines(1, "", "/* JADX INFO: renamed from: c */", "@SerializedName(\"title\")", "private String title;", ""); } private static JadxCodeRename fieldRename(String baseClsId, String shortId, String id) { return new JadxCodeRename(new JadxNodeRef(RefType.FIELD, baseClsId, shortId), id); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/rename/TestFieldWithGenericRename.java ================================================ package jadx.tests.integration.rename; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldWithGenericRename extends IntegrationTest { public static class TestCls { List list; } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code() .containsOnlyOnce("List list;"); cls.searchFieldByName("list").getFieldInfo().setAlias("listFieldRenamed"); assertThat(cls).reloadCode(this) .containsOnlyOnce("List listFieldRenamed;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/rename/TestRenameEnum.java ================================================ package jadx.tests.integration.rename; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestRenameEnum extends IntegrationTest { public static class TestCls { public enum A implements Runnable { ONE { @Override public void run() { System.out.println("ONE"); } }, TWO { @Override public void run() { System.out.println("TWO"); } }; } } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code() .containsOnlyOnce("public enum A ") .containsOnlyOnce("ONE {"); cls.getInnerClasses().get(0).getClassInfo().changeShortName("ARenamed"); assertThat(cls).reloadCode(this) .containsOnlyOnce("public enum ARenamed ") .containsOnlyOnce("ONE {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/rename/TestUserRenames.java ================================================ package jadx.tests.integration.rename; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; import jadx.api.data.CodeRefType; import jadx.api.data.ICodeRename; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef.RefType; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxCodeRef; import jadx.api.data.impl.JadxCodeRename; import jadx.api.data.impl.JadxNodeRef; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestUserRenames extends IntegrationTest { @SuppressWarnings({ "FieldCanBeLocal", "FieldMayBeFinal" }) public static class TestCls { private int intField = 5; public static class A { } public int test(int x) { int y = x + "test".length(); System.out.println(y); int z = y + 1; System.out.println(z); return z; } } @Test public void test() { getArgs().setDeobfuscationOn(false); List renames = new ArrayList<>(); String baseClsId = TestCls.class.getName(); renames.add(new JadxCodeRename(JadxNodeRef.forPkg("jadx.tests"), "renamedPkgTests")); renames.add(new JadxCodeRename(JadxNodeRef.forPkg("jadx.tests.integration.rename"), "renamedPkgRename")); renames.add(new JadxCodeRename(JadxNodeRef.forCls(baseClsId), "RenamedTestCls")); renames.add(new JadxCodeRename(JadxNodeRef.forCls(baseClsId + "$A"), "RenamedInnerCls")); renames.add(new JadxCodeRename(new JadxNodeRef(RefType.FIELD, baseClsId, "intField:I"), "renamedField")); JadxNodeRef mthRef = new JadxNodeRef(RefType.METHOD, baseClsId, "test(I)I"); renames.add(new JadxCodeRename(mthRef, "renamedTestMth")); renames.add(new JadxCodeRename(mthRef, new JadxCodeRef(CodeRefType.MTH_ARG, 0), "renamedX")); JadxCodeRef varDeclareRef = isJavaInput() ? JadxCodeRef.forVar(0, 1) : JadxCodeRef.forVar(0, 0); renames.add(new JadxCodeRename(mthRef, varDeclareRef, "renamedY")); IJavaCodeRef varUseRef = isJavaInput() ? JadxCodeRef.forVar(0, 4) : JadxCodeRef.forVar(1, 0); renames.add(new JadxCodeRename(mthRef, varUseRef, "renamedZ")); JadxCodeData codeData = new JadxCodeData(); codeData.setRenames(renames); getArgs().setCodeData(codeData); ClassNode cls = getClassNode(TestCls.class); assertThat(cls) .decompile() .checkCodeOffsets() .code() .containsOne("package jadx.renamedPkgTests.integration.renamedPkgRename;") .containsOne("public class RenamedTestCls {") .containsOne("private int renamedField") .containsOne("public static class RenamedInnerCls {") .containsOne("public int renamedTestMth(int renamedX) {") .containsOne("int renamedY = renamedX + \"test\".length();") .containsOne("int renamedZ = renamedY + 1;") .containsOne("return renamedZ;"); String code = cls.getCode().getCodeStr(); assertThat(cls) .reloadCode(this) .isEqualTo(code); ICodeRename updVarRename = new JadxCodeRename(mthRef, varUseRef, "anotherZ"); codeData.setRenames(Collections.singletonList(updVarRename)); jadxDecompiler.reloadCodeData(); assertThat(cls) .reloadCode(this) .containsOne("int anotherZ = y + 1;") .doesNotContain("int z") .doesNotContain("int renamedZ"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/rename/TestUsingSourceFileName.java ================================================ package jadx.tests.integration.rename; import org.junit.jupiter.api.Test; import jadx.api.args.UseSourceNameAsClassNameAlias; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestUsingSourceFileName extends SmaliTest { @Test public void testNeverUseSourceName() { args.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.NEVER); assertThat(searchCls(loadFromSmaliFiles(), "b")) .code() .containsOne("class b {"); } @Test public void testIfBetterUseSourceName() { args.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.IF_BETTER); assertThat(searchCls(loadFromSmaliFiles(), "b")) .code() .containsOne("class a {"); } @Test public void testAlwaysUseSourceName() { args.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.ALWAYS); assertThat(searchCls(loadFromSmaliFiles(), "b")) .code() .containsOne("class a {"); } @Test public void testNeverUseSourceNameWithDeobf() { args.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.NEVER); enableDeobfuscation(); args.setDeobfuscationMinLength(100); // rename everything assertThat(searchCls(loadFromSmaliFiles(), "b")) .code() .containsOne("class C0000b {") .containsOne("compiled from: a.java"); } @Test public void testIfBetterUseSourceNameWithDeobf() { args.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.IF_BETTER); enableDeobfuscation(); args.setDeobfuscationMinLength(100); // rename everything assertThat(searchCls(loadFromSmaliFiles(), "b")) .code() .containsOne("class a {") .containsOne("compiled from: a.java"); } @Test public void testAlwaysUseSourceNameWithDeobf() { args.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.ALWAYS); enableDeobfuscation(); args.setDeobfuscationMinLength(100); // rename everything assertThat(searchCls(loadFromSmaliFiles(), "b")) .code() .containsOne("class a {") .containsOne("compiled from: a.java"); } @Test public void testDeprecatedDontUseSourceName() { // noinspection deprecation args.setUseSourceNameAsClassAlias(false); assertThat(searchCls(loadFromSmaliFiles(), "b")) .code() .containsOne("class b {"); } @Test public void testDeprecatedUseSourceName() { // noinspection deprecation args.setUseSourceNameAsClassAlias(true); assertThat(searchCls(loadFromSmaliFiles(), "b")) .code() .containsOne("class a {"); } @Test public void testDeprecatedDontUseSourceNameWithDeobf() { // noinspection deprecation args.setUseSourceNameAsClassAlias(false); enableDeobfuscation(); args.setDeobfuscationMinLength(100); // rename everything assertThat(searchCls(loadFromSmaliFiles(), "b")) .code() .containsOne("class C0000b {") .containsOne("compiled from: a.java"); } @Test public void testDeprecatedUseSourceNameWithDeobf() { // noinspection deprecation args.setUseSourceNameAsClassAlias(true); enableDeobfuscation(); args.setDeobfuscationMinLength(100); // rename everything assertThat(searchCls(loadFromSmaliFiles(), "b")) .code() .containsOne("class a {") .containsOne("compiled from: a.java"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/special/TestPackageInfoSupport.java ================================================ package jadx.tests.integration.special; import java.util.List; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestPackageInfoSupport extends SmaliTest { @Test public void test() { disableCompilation(); List classes = loadFromSmaliFiles(); assertThat(searchCls(classes, "special.pkg1.package-info")) .satisfies(cls -> assertThat(cls.getAlias()).isEqualTo("package-info")) // shouldn't be renamed .code() .containsLines( "@Deprecated", "package special.pkg1;"); assertThat(searchCls(classes, "special.pkg2.package-info")) .code() .containsLines( "@ApiStatus.Internal", "package special.pkg2;", "", "import org.jetbrains.annotations.ApiStatus;"); assertThat(searchCls(classes, "special.pkg3.package-info")) .code().isEqualTo("\npackage special.pkg3;\n"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitch extends IntegrationTest { public static class TestCls { public String test(String str) { int len = str.length(); StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = str.charAt(i); switch (c) { case '.': case '/': sb.append('_'); break; case ']': sb.append('A'); break; case '?': break; default: sb.append(c); break; } } return sb.toString(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .contains("case '/':") .contains(indent(5) + "break;") .contains(indent(4) + "default:") .containsOne("i++") .countString(4, "break;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch2.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitch2 extends IntegrationTest { public static class TestCls { boolean isLongtouchable; boolean isMultiTouchZoom; boolean isCanZoomIn; boolean isCanZoomOut; boolean isScrolling; float multiTouchZoomOldDist; public void test(int action) { switch (action & 255) { case 0: this.isLongtouchable = true; break; case 1: case 6: if (this.isMultiTouchZoom) { this.isMultiTouchZoom = false; } break; case 2: if (this.isMultiTouchZoom) { float dist = multiTouchZoomOldDist; if (Math.abs(dist - this.multiTouchZoomOldDist) > 10.0f) { float scale = dist / this.multiTouchZoomOldDist; if ((scale > 1.0f && this.isCanZoomIn) || (scale < 1.0f && this.isCanZoomOut)) { this.multiTouchZoomOldDist = dist; } } return; } break; case 5: this.multiTouchZoomOldDist = action; if (this.multiTouchZoomOldDist > 10.0f) { this.isMultiTouchZoom = true; this.isLongtouchable = false; return; } break; } if (this.isScrolling && action == 1) { this.isScrolling = false; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(4, "break;") // .countString(2, "return;") // TODO: remove redundant returns .countString(4, "return;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch3.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitch3 extends IntegrationTest { public static class TestCls { private int i; void test(int a) { switch (a) { case 1: i = 1; return; case 2: case 3: i = 2; return; default: i = 4; break; } i = 5; } public void check() { test(1); assertThat(i).isEqualTo(1); test(2); assertThat(i).isEqualTo(2); test(3); assertThat(i).isEqualTo(2); test(4); assertThat(i).isEqualTo(5); test(10); assertThat(i).isEqualTo(5); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(3, "break;") .countString(0, "return;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch4.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitch4 extends IntegrationTest { public static class TestCls { @SuppressWarnings({ "FallThrough", "unused" }) private static int parse(char[] ch, int off, int len) { int num = ch[off + len - 1] - '0'; switch (len) { case 4: num += (ch[off++] - '0') * 1000; case 3: num += (ch[off++] - '0') * 100; case 2: num += (ch[off] - '0') * 10; } return num; } public void check() { assertThat(parse("123".toCharArray(), 0, 3)).isEqualTo(123); assertThat(parse("a=1234".toCharArray(), 2, 4)).isEqualTo(1234); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("switch (") .countString(3, "case ") .doesNotContain("break"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchBreak.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchBreak extends IntegrationTest { public static class TestCls { public String test(int a) { String s = ""; loop: while (a > 0) { switch (a % 4) { case 1: s += "1"; break; case 3: case 4: s += "4"; break; case 5: s += "+"; break loop; } s += "-"; a--; } return s; } public void check() { assertThat(test(9)).isEqualTo("1--4--1--4--1-"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .contains("switch (a % 4) {") .countString(4, "case ") .countString(2, "break;") .doesNotContain("default:") // TODO finish break with label from switch .containsOne("return s + \"+\";"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchBreak2.java ================================================ package jadx.tests.integration.switches; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchBreak2 extends IntegrationTest { @SuppressWarnings("SwitchStatementWithTooFewBranches") public static class TestCls { private int value; public void test(int i, boolean b1, boolean b2) { setValue(-1); switch (i) { case 0: if (b1 && b2) { setValue(1); // no break here; } else { setValue(2); // no break here; } break; default: setValue(0); break; } } private void setValue(int value) { this.value = value; } public void check() { test(0, true, true); assertThat(value).isEqualTo(1); test(0, true, false); assertThat(value).isEqualTo(2); test(1, true, true); assertThat(value).isEqualTo(0); } } @TestWithProfiles({ TestProfile.JAVA11, TestProfile.D8_J11 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(2, "break;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchBreak3.java ================================================ package jadx.tests.integration.switches; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchBreak3 extends IntegrationTest { @SuppressWarnings("SwitchStatementWithTooFewBranches") public static class TestCls { private int value; public void test(int i, boolean b1, boolean b2, boolean b3) { setValue(-1); switch (i) { case 0: if (b1 == b2) { setValue(1); // no break here; } else if (b1 == b3) { setValue(2); // no break here; } break; default: setValue(0); break; } } private void setValue(int value) { this.value = value; } public void check() { test(0, true, true, true); assertThat(value).isEqualTo(1); test(0, true, false, true); assertThat(value).isEqualTo(2); test(1, true, true, true); assertThat(value).isEqualTo(0); } } @TestWithProfiles({ TestProfile.JAVA11, TestProfile.D8_J11 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(2, "break;") .containsOne("} else if ("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchBreak4.java ================================================ package jadx.tests.integration.switches; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchBreak4 extends IntegrationTest { @SuppressWarnings("SwitchStatementWithTooFewBranches") public static class TestCls { private int value; public void test(int i, boolean b1, boolean b2, boolean b3) { setValue(-1); switch (i) { case 0: if (b1 == b2) { setValue(1); } else if (b1 == b3) { setValue(2); } else { setValue(3); } break; default: setValue(0); break; } } private void setValue(int value) { this.value = value; } public void check() { test(0, true, true, true); assertThat(value).isEqualTo(1); test(0, true, false, true); assertThat(value).isEqualTo(2); test(0, true, false, false); assertThat(value).isEqualTo(3); } } @TestWithProfiles({ TestProfile.JAVA11, TestProfile.D8_J11 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(2, "break;") .containsOne("} else if (") .containsOne("} else {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchContinue.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchContinue extends IntegrationTest { @SuppressWarnings({ "StringConcatenationInLoop", "DataFlowIssue" }) public static class TestCls { public String test(int a) { String s = ""; while (a > 0) { switch (a % 4) { case 1: s += "1"; break; case 3: case 4: s += "4"; break; case 5: a -= 2; continue; } s += "-"; a--; } return s; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .contains("switch (a % 4) {") .countString(4, "case ") .countString(2, "break;") .containsOne("a -= 2;") .containsOne("continue;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchFallThrough.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchFallThrough extends IntegrationTest { public static class TestCls { public int r; @SuppressWarnings("fallthrough") public void test(int a) { int i = 10; switch (a) { case 1: i = 1000; // fallthrough case 2: r = i; break; default: r = -1; break; } r *= 2; System.out.println("in: " + a + ", out: " + r); } public int testWrap(int a) { r = 0; test(a); return r; } public void check() { assertThat(testWrap(1)).isEqualTo(2000); assertThat(testWrap(2)).isEqualTo(20); assertThat(testWrap(0)).isEqualTo(-2); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("switch (a) {") .containsOne("r = i;") .containsOne("r = -1;") .countString(2, "break;"); // code correctness checks done in 'check' method } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestSwitchInLoop extends IntegrationTest { public static class TestCls { public int test(int k) { int a = 0; while (true) { switch (k) { case 0: return a; default: a++; k >>= 1; } } } public void check() { assertThat(test(1)).isEqualTo(1); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("switch (k) {") .containsOne("case 0:") .containsOne("return a;") .containsOne("default:") .containsOne("a++;") .containsOne("k >>= 1;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop2.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchInLoop2 extends IntegrationTest { public static class TestCls { public boolean test() { while (true) { switch (call()) { case 0: return false; case 1: return true; } } } private int call() { return 0; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("while (true) {") .containsOne("switch (call()) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop3.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchInLoop3 extends IntegrationTest { @SuppressWarnings("SwitchStatementWithTooFewBranches") public static class TestCls { public int test(int k) { int a = 0; while (true) { int x = 0; // keep this: force to generate the necessary CFG switch (k) { case 0: return a; default: a++; k >>= 1; } } } public void check() { assertThat(test(1)).isEqualTo(1); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsLines(3, "switch (k) {", indent() + "case 0:", indent(2) + "return a;", indent() + "default:", indent(2) + "a++;", indent(2) + "k >>= 1;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop4.java ================================================ package jadx.tests.integration.switches; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchInLoop4 extends IntegrationTest { @SuppressWarnings("SwitchStatementWithTooFewBranches") public static class TestCls { private static boolean test(String s, int start) { boolean foundSeparator = false; for (int i = start; i < s.length(); i++) { char c = s.charAt(i); switch (c) { case '.': foundSeparator = true; break; } if (foundSeparator) { break; } } return foundSeparator; } public void check() { assertThat(test("a.b", 0)).isTrue(); assertThat(test("abc", 1)).isFalse(); } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11, TestProfile.JAVA11 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("switch (c) {") .containsOne("break;"); // allow replacing second 'break' with 'return' } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop5.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchInLoop5 extends IntegrationTest { public static class TestCls { private static int test(int r) { int i; while (true) { switch (r) { case 42: i = 32; break; case 52: i = 42; break; default: System.out.println("Default switch case"); return 1; } r = i; } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("default:") .containsOne("System.out.println("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop6.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchInLoop6 extends IntegrationTest { public static class TestCls { private void test() throws Exception { while (true) { int n = getN(); switch (n) { case 1: n1(); return; case 2: n2(); if (getN() == 3) { return; } break; case 3: n3(); return; case 4: n4(); return; default: throw new Exception(); } } } // Output below: // @formatter:off /* public void function() throws Exception { do { switch (getN()) { case 1: n1(); return; case 2: n2(); break; case 3: n3(); return; case 4: n4(); return; default: throw new Exception(); } } while (getN() != 3); } */ // @formatter:on void n1() { } void n2() { } void n3() { } void n4() { } private int getN() { double i = Math.random(); if (i < 0.25) { return 1; } if (i < 0.5) { return 2; } if (i < 0.75) { return 3; } if (i < 1.0) { return 4; } return -1; } } @Test public void test() { allowWarnInCode(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("switch (n) {") .containsOne("case 1:") .containsOne("case 2:") .containsOne("case 3:") .containsOne("case 4:") .containsOne("do {") .containsOne("while (getN() != 3)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop7.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchInLoop7 extends IntegrationTest { public static class TestCls { private void test() { int i = 0; int n = getN(); while (true) { if (i > n) { break; } i++; if (n == 5) { continue; } switch (n) { case 0: { if (n != 1) { return; } break; } case 1: i++; break; } } return; } // Output below: // @formatter:off /* public void function() { int i = 0; int n = getN(); while (i <= n) { i++; if (n != 5) { switch (n) { case 0: if (n == 1) { break; } else { return; } case 1: i++; break; } } } } */ // @formatter:on private int getN() { double i = Math.random(); if (i < 0.25) { return 1; } if (i < 0.5) { return 2; } if (i < 0.75) { return 3; } if (i < 1.0) { return 4; } return -1; } } @Test public void test() { // Checks that the redundant default continue case is not recovered assertThat(getClassNode(TestCls.class)) .code() .containsOne("switch (n) {") .containsOne("case 0:") .containsOne("case 1:") .containsOne("while (") .doesNotContain("default") .doesNotContain("contine"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop8.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchInLoop8 extends IntegrationTest { public static class TestCls { private void test() { int i = 0; int n = getN(); while (true) { if (i > n) { break; } i++; if (n == 5) { continue; } switch (n) { case 0: { if (n != 1) { return; } break; } case 1: i++; break; default: continue; } if (i < 2) { i++; } } return; } private int getN() { double i = Math.random(); if (i < 0.25) { return 1; } if (i < 0.5) { return 2; } if (i < 0.75) { return 3; } if (i < 1.0) { return 4; } return -1; } } @Test public void test() { // Checks that the default continue case is not removed assertThat(getClassNode(TestCls.class)) .code() .containsOne("switch (n) {") .containsOne("case 0:") .containsOne("case 1:") .containsOne("while (") .containsOne("default"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop9.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchInLoop9 extends IntegrationTest { public static class TestCls { private void test() { int i = 0; int n = getN(); while (true) { if (i > n) { break; } i++; if (n == 5) { continue; } switch (n) { case 0: { if (n != 1) { return; } break; } case 1: i++; break; default: continue; } if (i < 2) { i += 327; } } return; } private int getN() { double i = Math.random(); if (i < 0.25) { return 1; } if (i < 0.5) { return 2; } if (i < 0.75) { return 3; } if (i < 1.0) { return 4; } return -1; } } @Test public void test() { // Checks that the work after the switch is recovered only once assertThat(getClassNode(TestCls.class)) .code() .containsOne("switch (n) {") .containsOne("case 0:") .containsOne("case 1:") .containsOne("while (") .containsOne("default") .containsOne("327"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchLabels.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchLabels extends IntegrationTest { public static class TestCls { public static final int CONST_ABC = 0xABC; public static final int CONST_CDE = 0xCDE; public static class Inner { private static final int CONST_CDE_PRIVATE = 0xCDE; public int f1(int arg0) { switch (arg0) { case CONST_CDE_PRIVATE: return CONST_ABC; } return 0; } } public static int f1(int arg0) { switch (arg0) { case CONST_ABC: return CONST_CDE; } return 0; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .contains("case CONST_ABC") .contains("return CONST_CDE;") .doesNotContain("case CONST_CDE_PRIVATE") .contains(".CONST_ABC;"); } @Test public void testWithDisabledConstReplace() { getArgs().setReplaceConsts(false); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("case CONST_ABC") .contains("case 2748") .doesNotContain("return CONST_CDE;") .contains("return 3294;") .doesNotContain("case CONST_CDE_PRIVATE") .doesNotContain(".CONST_ABC;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchNoDefault.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchNoDefault extends IntegrationTest { public static class TestCls { public void test(int a) { String s = null; switch (a) { case 1: s = "1"; break; case 2: s = "2"; break; case 3: s = "3"; break; case 4: s = "4"; break; } System.out.println(s); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(4, "break;") .containsOne("System.out.println(s);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchOverStrings.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchOverStrings extends IntegrationTest { /** * Strings 'frewhyh', 'phgafkp' and 'ucguedt' have same hash code. */ public static class TestCls { public int test(String str) { switch (str) { case "frewhyh": return 1; case "phgafkp": return 2; case "test": case "test2": return 3; case "other": return 4; default: return 0; } } public void check() { assertThat(test("frewhyh")).isEqualTo(1); assertThat(test("phgafkp")).isEqualTo(2); assertThat(test("test")).isEqualTo(3); assertThat(test("test2")).isEqualTo(3); assertThat(test("other")).isEqualTo(4); assertThat(test("unknown")).isEqualTo(0); assertThat(test("ucguedt")).isEqualTo(0); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("case -603257287:") .doesNotContain("c = ") .doesNotContainSubsequence("default:", "case ") .containsOne("case \"frewhyh\":") .countString(5, "return "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchOverStrings2.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchOverStrings2 extends IntegrationTest { public static class TestCls { public int test(String str) { switch (str) { case "branch1": case "branch2": return 1; case "branch3": case "branch4": default: return 0; } } public void check() { assertThat(test("branch1")).isEqualTo(1); assertThat(test("branch2")).isEqualTo(1); assertThat(test("branch3")).isEqualTo(0); assertThat(test("branch4")).isEqualTo(0); assertThat(test("other")).isEqualTo(0); assertThat(test("other2")).isEqualTo(0); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(4, "case ") .countString(1, "default:") .countString(2, "return "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchOverStrings3.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchOverStrings3 extends IntegrationTest { @SuppressWarnings("SwitchStatementWithTooFewBranches") public static class TestCls { public int test(String v) { switch (v) { case "a": return 1; default: switch (v) { case "b": return 2; case "c": return 3; default: return 4; } } } public void check() { assertThat(test("a")).isEqualTo(1); assertThat(test("b")).isEqualTo(2); assertThat(test("c")).isEqualTo(3); assertThat(test("d")).isEqualTo(4); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(3, "case ") .countString(2, "default:") .countString(4, "return "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestSwitchReturnFromCase extends IntegrationTest { public static class TestCls { public void test(int a) { if (a > 1000) { return; } String s = null; switch (a % 10) { case 1: s = "1"; break; case 2: s = "2"; break; case 3: case 4: s = "4"; break; case 5: break; case 6: return; } if (s == null) { s = "5"; } System.out.println(s); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("switch (a % 10) {") // case 5: removed .countString(5, "case ") .countString(3, "break;") .containsOne("s = \"1\";") .containsOne("s = \"2\";") .containsOne("s = \"4\";") .containsOne("s = \"5\";"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase2.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestSwitchReturnFromCase2 extends IntegrationTest { public static class TestCls { public boolean test(int a) { switch (a % 4) { case 2: case 3: if (a == 2) { return true; } return true; } return false; } public void check() { assertThat(test(2)).isTrue(); assertThat(test(3)).isTrue(); assertThat(test(15)).isTrue(); assertThat(test(1)).isFalse(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("switch (a % 4) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchSimple.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSwitchSimple extends IntegrationTest { public static class TestCls { public void test(int a) { String s = null; switch (a % 4) { case 1: s = "1"; break; case 2: s = "2"; break; case 3: s = "3"; break; case 4: s = "4"; break; default: System.out.println("Not Reach"); break; } System.out.println(s); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(5, "break;") .containsOne("System.out.println(s);") .containsOne("System.out.println(\"Not Reach\");") .doesNotContain("switch ((a % 4)) {") .contains("switch (a % 4) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestSwitchWithFallThroughCase extends IntegrationTest { @SuppressWarnings("fallthrough") public static class TestCls { public String test(int a, boolean b, boolean c) { String str = ""; switch (a % 4) { case 1: str += ">"; if (a == 5 && b) { if (c) { str += "1"; } else { str += "!c"; } break; } // fallthrough case 2: if (b) { str += "2"; } break; case 3: break; default: str += "default"; break; } str += ";"; return str; } public void check() { assertThat(test(5, true, true)).isEqualTo(">1;"); assertThat(test(1, true, true)).isEqualTo(">2;"); assertThat(test(3, true, true)).isEqualTo(";"); assertThat(test(0, true, true)).isEqualTo("default;"); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("switch (a % 4) {") .containsOne("if (a == 5 && b) {") .containsOne("if (b) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase2.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestSwitchWithFallThroughCase2 extends IntegrationTest { @SuppressWarnings("fallthrough") public static class TestCls { public String test(int a, boolean b, boolean c) { String str = ""; if (a > 0) { switch (a % 4) { case 1: str += ">"; if (a == 5 && b) { if (c) { str += "1"; } else { str += "!c"; } break; } case 2: if (b) { str += "2"; } break; case 3: break; default: str += "default"; break; } str += "+"; } if (b && c) { str += "-"; } return str; } public void check() { assertThat(test(5, true, true)).isEqualTo(">1+-"); assertThat(test(1, true, true)).isEqualTo(">2+-"); assertThat(test(3, true, true)).isEqualTo("+-"); assertThat(test(16, true, true)).isEqualTo("default+-"); assertThat(test(-1, true, true)).isEqualTo("-"); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("switch (a % 4) {") .containsOne("if (a == 5 && b) {") .containsOne("if (b) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithThrow.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; public class TestSwitchWithThrow extends IntegrationTest { public static class TestCls { public int test(int i) { if (i != 0) { switch (i % 4) { case 1: throw new IllegalStateException("1"); case 2: throw new IllegalStateException("2"); default: throw new IllegalStateException("Other"); } } System.out.println("0"); return -1; } public void check() { assertThat(test(0)).isEqualTo(-1); assertThat(catchThrowable(() -> test(1))) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("1"); assertThat(catchThrowable(() -> test(3))) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("Other"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .contains("throw new IllegalStateException(\"1\");"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithTryCatch.java ================================================ package jadx.tests.integration.switches; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("checkstyle:printstacktrace") public class TestSwitchWithTryCatch extends IntegrationTest { public static class TestCls { void test(int a) { switch (a) { case 0: try { exc(); return; } catch (Exception e) { e.printStackTrace(); return; } // no break; case 1: try { exc(); return; } catch (Exception e) { e.printStackTrace(); } break; case 2: try { exc(); } catch (Exception e) { e.printStackTrace(); return; } break; case 3: try { exc(); } catch (Exception e) { e.printStackTrace(); } break; } if (a == 10) { System.out.println(a); } } private void exc() throws Exception { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(3, "break;") .countString(4, "return;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/synchronize/TestNestedSynchronize.java ================================================ package jadx.tests.integration.synchronize; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNestedSynchronize extends SmaliTest { // @formatter:off /* public final void test() { Object obj = null; Object obj2 = null; synchronized (obj) { synchronized (obj2) { } } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .countString(2, "synchronized"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized.java ================================================ package jadx.tests.integration.synchronize; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestSynchronized extends IntegrationTest { public static class TestCls { public boolean f = false; public final Object o = new Object(); public int i = 7; public synchronized boolean test1() { return this.f; } public int test2() { synchronized (this.o) { return this.i; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("synchronized (this) {") .containsOne("public synchronized boolean test1() {") .containsOne("return this.f") .containsOne("synchronized (this.o) {") .doesNotContain(indent(3) + ';') .doesNotContain("try {") .doesNotContain("} catch (Throwable th) {") .doesNotContain("throw th;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized2.java ================================================ package jadx.tests.integration.synchronize; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSynchronized2 extends IntegrationTest { @SuppressWarnings("unused") public static class TestCls { private static synchronized boolean test(Object obj) { return obj.toString() != null; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .contains("private static synchronized boolean test(Object obj) {") .doesNotContain("synchronized (") .contains("obj.toString() != null;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized3.java ================================================ package jadx.tests.integration.synchronize; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSynchronized3 extends IntegrationTest { public static class TestCls { private int x; public void f() { } public void test() { while (true) { synchronized (this) { if (x == 0) { throw new IllegalStateException(); } x++; if (x == 10) { break; } } this.x++; f(); } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsLines(3, "}", "this.x++;", "f();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized4.java ================================================ package jadx.tests.integration.synchronize; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSynchronized4 extends SmaliTest { // @formatter:off /* public boolean test(int i) { synchronized (this.obj) { if (isZero(i)) { return call(obj, i); } System.out.println(); return getField() == null; } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("synchronized (this.obj) {") .containsOne("return call(this.obj, i);") .containsOne("return getField() == null;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized5.java ================================================ package jadx.tests.integration.synchronize; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSynchronized5 extends SmaliTest { @Test public void test() { allowWarnInCode(); assertThat(getClassNodeFromSmali()) .code() .contains("1 != 0") .contains("System.gc();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized6.java ================================================ package jadx.tests.integration.synchronize; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSynchronized6 extends SmaliTest { public static class TestCls { private final Object lock = new Object(); private boolean test(Object obj) { synchronized (this.lock) { return isA(obj) || isB(obj); } } private boolean isA(Object obj) { return false; } private boolean isB(Object obj) { return false; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("synchronized (this.lock) {") .containsOne("isA(obj) || isB(obj);"); // TODO: "return isA(obj) || isB(obj);" } @Test public void testSmali() { assertThat(getClassNodeFromSmali()) .code() .containsOne("synchronized (this.lock) {"); // TODO: .containsOne("return isA(obj) || isB(obj);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestEmptyCatch.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Empty catch blocks in enum switch remap building */ public class TestEmptyCatch extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .countString(5, "try {") .countString(5, "} catch (NoSuchFieldError unused"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestEmptyFinally.java ================================================ package jadx.tests.integration.trycatch; import java.io.FileInputStream; import java.io.IOException; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestEmptyFinally extends IntegrationTest { @SuppressWarnings("EmptyFinallyBlock") public static class TestCls { public void test(FileInputStream f1) { try { f1.close(); } catch (IOException e) { // do nothing } finally { // ignore } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("} catch (IOException e) {") .doesNotContain("} finally {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestFinally.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestFinally extends IntegrationTest { public static class TestCls { private static final String DISPLAY_NAME = "name"; String test(Context context, Object uri) { Cursor cursor = null; try { String[] projection = { DISPLAY_NAME }; cursor = context.query(uri, projection); int columnIndex = cursor.getColumnIndexOrThrow(DISPLAY_NAME); cursor.moveToFirst(); return cursor.getString(columnIndex); } finally { if (cursor != null) { cursor.close(); } } } private class Context { public Cursor query(Object o, String[] s) { return null; } } private class Cursor { public void close() { } public void moveToFirst() { } public int getColumnIndexOrThrow(String s) { return 0; } public String getString(int i) { return null; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("} finally {") .containsOne("cursor.getString(columnIndex);") .doesNotContain("String str = true;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestFinally2.java ================================================ package jadx.tests.integration.trycatch; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestFinally2 extends IntegrationTest { public static class TestCls { public Result test(byte[] data) throws IOException { InputStream inputStream = null; try { inputStream = getInputStream(data); decode(inputStream); return new Result(400); } finally { closeQuietly(inputStream); } } public static final class Result { private final int mCode; public Result(int code) { mCode = code; } public int getCode() { return mCode; } } private InputStream getInputStream(byte[] data) throws IOException { return new ByteArrayInputStream(data); } private int decode(InputStream inputStream) throws IOException { return inputStream.available(); } private void closeQuietly(InputStream is) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("decode(inputStream);") .containsOne("return new Result(400);") .doesNotContain("result ="); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestFinally3.java ================================================ package jadx.tests.integration.trycatch; import java.io.ByteArrayInputStream; import java.io.InputStream; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFinally3 extends SmaliTest { @SuppressWarnings({ "RedundantThrows", "unused" }) public static class TestCls { public byte[] bytes; public byte[] test() throws Exception { InputStream inputStream = null; try { if (bytes == null) { if (!validate()) { return null; } inputStream = getInputStream(); bytes = read(inputStream); } return convert(bytes); } finally { close(inputStream); } } private byte[] convert(byte[] bytes) throws Exception { return new byte[0]; } private boolean validate() throws Exception { return false; } private InputStream getInputStream() throws Exception { return new ByteArrayInputStream(new byte[] {}); } private byte[] read(InputStream in) throws Exception { return new byte[] {}; } private static void close(InputStream is) { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("} finally {") .doesNotContain("close(null);") .containsOne("close(inputStream);"); } @NotYetImplemented("Finally extract failed") @Test public void test2NoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("} finally {") .containsOne(indent() + "close("); } @Test public void testSmali() { assertThat(getClassNodeFromSmali()) .code() .doesNotContain("Type inference failed") .containsOne("} finally {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestFinallyExtract.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestFinallyExtract extends IntegrationTest { public static class TestCls { private int result = 0; public String test() { boolean success = false; try { String value = call(); result++; success = true; return value; } finally { if (!success) { result -= 2; } } } private String call() { return "call"; } public void check() { test(); assertThat(result).isEqualTo(1); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("} finally {") .doesNotContain("if (0 == 0) {") .containsOne("boolean success = false;") .containsOne("try {") .containsOne("success = true;") .containsOne("return value;") .containsOne("if (!success) {"); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("this.result++;") .containsOne("} catch (Throwable th) {") .containsOne("this.result -= 2;") .containsOne("throw th;"); // java compiler optimization: 'success' variable completely removed and no code duplication: // @formatter:off /* public String test() { try { String call = call(); this.result++; return call; } catch (Throwable th) { this.result -= 2; throw th; } } */ // @formatter:on } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestIfInTryCatch.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestIfInTryCatch extends IntegrationTest { public static class TestCls { private void test() { /* * 1. if in try * 2. then branch is return * 3. after if, there's more blocks inside try * 4. after try, there's more blocks * this will result in if block and below moved out of try */ try { if (getDouble() > 0.5) { return; } System.out.println("after if"); } catch (Exception e) { System.out.println("exception"); } System.out.println("after try"); } private static double getDouble() throws InterruptedException { Thread.sleep(50L); return Math.random(); } } @Test public void test() { // if ifBlock is moved out of try, there will be uncaught exception JadxAssertions.assertThat(getClassNode(TestCls.class)) .code(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestInlineInCatch.java ================================================ package jadx.tests.integration.trycatch; import java.io.File; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInlineInCatch extends IntegrationTest { public static class TestCls { private File dir; public int test() { File output = null; try { output = File.createTempFile("f", "a", dir); if (!output.exists()) { return 1; } return 0; } catch (Exception e) { if (output != null) { output.delete(); } return 2; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("File output = null;") .containsOne("output = File.createTempFile(\"f\", \"a\", ") .containsOne("return 0;") .containsOne("} catch (Exception e) {") .containsOne("if (output != null) {") .containsOne("output.delete();") .containsOne("return 2;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestLoopInTryCatch.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestLoopInTryCatch extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .oneOf(c -> c.containsLines(2, "int i;", "while (true) {", " try {", " i = getI();", " } catch (RuntimeException unused) {", " return;", " }", " if (i == 1 || i == 2) {", " break;", " }", "}", "if (i != 1) {", " getI();", "}"), c -> c.containsLines(2, "int i;", "while (true) {", " try {", " i = getI();", " if (i == 1 || i == 2) {", " break;", " }", " } catch (RuntimeException unused) {", " return;", " }", "}", "if (i != 1) {", " getI();", "}"), // TODO: weird result but correct, better to not use do-while if not really needed c -> c.containsLines(2, "int i;", "do {", " try {", " i = getI();", " if (i == 1) {", " break;", " }", " } catch (RuntimeException unused) {", " return;", " }", "} while (i != 2);", "if (i != 1) {", " getI();", "}")); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch.java ================================================ package jadx.tests.integration.trycatch; import java.security.ProviderException; import java.time.DateTimeException; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestMultiExceptionCatch extends IntegrationTest { public static class TestCls { public void test() { try { System.out.println("Test"); } catch (ProviderException | DateTimeException e) { throw new RuntimeException(e); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("try {") .containsOne("} catch (ProviderException | DateTimeException e) {") .containsOne("throw new RuntimeException(e);") .doesNotContain("RuntimeException e;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch2.java ================================================ package jadx.tests.integration.trycatch; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; @SuppressWarnings("checkstyle:printstacktrace") public class TestMultiExceptionCatch2 extends IntegrationTest { public static class TestCls { public void test(Constructor constructor) { try { constructor.newInstance(); } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { e.printStackTrace(); } } } @Test public void test() { commonChecks(); } @Test public void testNoDebug() { noDebugInfo(); commonChecks(); } private void commonChecks() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("try {") .containsOne("} catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {") .containsOne("e.printStackTrace();"); // TODO: store vararg attribute for methods from classpath // assertThat(code, containsOne("constructor.newInstance();")); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatchSameJump.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestMultiExceptionCatchSameJump extends SmaliTest { // @formatter:off /* public static class TestCls { public void test() { try { System.out.println("Test"); } catch (ProviderException | DateTimeException e) { throw new RuntimeException(e); } } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmaliWithPkg("trycatch", "TestMultiExceptionCatchSameJump")) .code() .containsOne("try {") .containsOne("} catch (ProviderException | DateTimeException e) {") .containsOne("throw new RuntimeException(e);") .doesNotContain("RuntimeException e;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestNestedTryCatch.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestNestedTryCatch extends IntegrationTest { public static class TestCls { public void test() { try { Thread.sleep(1L); try { Thread.sleep(2L); } catch (InterruptedException ignored) { System.out.println(2); } } catch (Exception ignored) { System.out.println(1); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("try {") .contains("Thread.sleep(1L);") .contains("Thread.sleep(2L);") .contains("} catch (InterruptedException e) {") .contains("} catch (Exception e2) {") .doesNotContain("return"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestNestedTryCatch2.java ================================================ package jadx.tests.integration.trycatch; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNestedTryCatch2 extends IntegrationTest { public static class TestCls { public void test() { try { try { call(); call(); } catch (Exception e) { exc(e); } } catch (Exception e) { exc(e); } } private void call() { } private void exc(Exception e) { } } @TestWithProfiles({ TestProfile.JAVA8, TestProfile.DX_J8 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(2, "} catch (Exception "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestNestedTryCatch3.java ================================================ package jadx.tests.integration.trycatch; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNestedTryCatch3 extends IntegrationTest { public static class TestCls { public I test() { try { try { return new A(); } catch (Throwable e) { return new B(); } } catch (Throwable e) { return new C(); } } private interface I { } private static class A implements I { } private static class B implements I { } private static class C implements I { } } @TestWithProfiles({ TestProfile.JAVA8, TestProfile.DX_J8 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return new A();") .containsOne("return new B();") .containsOne("return new C();") .countString(2, "} catch (Throwable "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestNestedTryCatch4.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNestedTryCatch4 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .doesNotContain("?? "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestNestedTryCatch5.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNestedTryCatch5 extends SmaliTest { @Test @NotYetImplemented("Extracting finally on loop advancement") public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .doesNotContain("?? ") .containsOne("} finally") .containsOne("endTransaction") .countString(1, "throw "); // 1 real throws, 1 implicit throw on finally handler and 1 implicit throw on empty ALL handler } @Test public void testNoFinally() { args.setExtractFinally(false); disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .doesNotContain("?? ") .countString(3, "throw "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryAfterDeclaration.java ================================================ package jadx.tests.integration.trycatch; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTryAfterDeclaration extends IntegrationTest { static class TestClass { public static void consume() throws IOException { InputStream bis = null; try { bis = new FileInputStream("1.txt"); while (bis != null) { System.out.println("c"); } } catch (final IOException ignore) { } } } /** * Issue #62. */ @Test @NotYetImplemented public void test62() { JadxAssertions.assertThat(getClassNode(TestClass.class)) .code() .containsOne("try {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTryCatch extends IntegrationTest { public static class TestCls { public void f() { try { Thread.sleep(50L); } catch (InterruptedException e) { // ignore } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("try {") .contains("Thread.sleep(50L);") .contains("} catch (InterruptedException e) {") .doesNotContain("return"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch10.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatch10 extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .countString(3, "return false;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch11.java ================================================ package jadx.tests.integration.trycatch; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; // Ensure that we can still merge if conditions even in the case where // the BOTTOM_SPLITTER occurs before the final IF block. public class TestTryCatch11 extends IntegrationTest { public static class TestCls { public static class Cursor implements AutoCloseable { @Override public void close() { System.out.println("Closed AutoCloseableResources_First"); } public String getString() { return "jfdkelapgfureiqop[]"; } } public static String test() { try (Cursor cursor = new Cursor()) { String value = cursor.getString(); if (value.startsWith("content://") || !value.startsWith("/") && !value.startsWith("file://")) { return null; } return value; } catch (Exception ignore) { System.out.println("catch"); } return null; } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("value.startsWith(\"content://\") || (!value.startsWith(\"/\") && !value.startsWith(\"file://\"))"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch2.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTryCatch2 extends IntegrationTest { public static class TestCls { private static final Object OBJ = new Object(); public static boolean test() { try { synchronized (OBJ) { OBJ.wait(5L); } return true; } catch (InterruptedException e) { return false; } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("try {") .contains("synchronized (OBJ) {") .contains("OBJ.wait(5L);") .contains("return true;") .contains("} catch (InterruptedException e) {") .contains("return false;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch6.java ================================================ package jadx.tests.integration.trycatch; import java.io.IOException; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatch6 extends IntegrationTest { public static class TestCls { private static boolean test(Object obj) { boolean res = false; while (true) { try { res = exc(obj); return res; } catch (IOException e) { res = true; } catch (Throwable e) { if (obj == null) { obj = new Object(); } } } } private static boolean exc(Object obj) throws IOException { if (obj == null) { throw new IOException(); } return true; } public void check() { assertThat(test(new Object())).isTrue(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("try {"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("try {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch7.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("checkstyle:printstacktrace") public class TestTryCatch7 extends IntegrationTest { public static class TestCls { public Exception test() { Exception e = new Exception(); try { Thread.sleep(50); } catch (Exception ex) { e = ex; } e.printStackTrace(); return e; } } @Test public void test() { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); check(code, "e", "ex"); } @Test public void testNoDebug() { noDebugInfo(); ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); check(code, "e", "e2"); } private void check(String code, String excVarName, String catchExcVarName) { assertThat(code).containsOne("Exception " + excVarName + " = new Exception();") .containsOne("} catch (Exception " + catchExcVarName + ") {") .containsOne(excVarName + " = " + catchExcVarName + ';') .containsOne(excVarName + ".printStackTrace();") .containsOne("return " + excVarName + ';'); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch8.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatch8 extends IntegrationTest { public static class TestCls { static class MyException extends Exception { private static final long serialVersionUID = 7963400419047287279L; MyException() { } MyException(String msg, Throwable cause) { super(msg, cause); } } MyException e = null; public void test() { synchronized (this) { try { throw new MyException(); } catch (MyException myExc) { this.e = myExc; } catch (Exception ex) { this.e = new MyException("MyExc", ex); } } } public void check() { test(); assertThat(e).isInstanceOf(MyException.class); assertThat(e.getMessage()).isNull(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("synchronized (this) {") .containsOne("throw new MyException();") .containsOne("} catch (MyException myExc) {") .containsOne("this.e = myExc;") .containsOne("} catch (Exception ex) {") .containsOne("this.e = new MyException(\"MyExc\", ex);"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("synchronized (this) {") .containsOne("throw new MyException();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch9.java ================================================ package jadx.tests.integration.trycatch; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatch9 extends IntegrationTest { public static class TestCls { public Integer test(final Integer i) { if (i == null) { return null; } Integer res = null; try { if (i == 5) { res = 4; } else { res = 9; } } catch (final Exception ex) { logError(ex); } return res; } private void logError(Exception ex) { } public void check() { assertThat(test(5)).isEqualTo(4); } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("logError(ex);") .containsOne("Integer res") .contains("res = null;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("checkstyle:printstacktrace") public class TestTryCatchFinally extends IntegrationTest { public static class TestCls { public boolean f; @SuppressWarnings("ConstantConditions") private boolean test(Object obj) { this.f = false; try { exc(obj); } catch (Exception e) { e.printStackTrace(); } finally { this.f = true; } return this.f; } private static boolean exc(Object obj) throws Exception { if (obj == null) { throw new Exception("test"); } return (obj instanceof String); } public void check() { assertThat(test("a")).isTrue(); assertThat(test(null)).isTrue(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("this.f = false;") .containsOne("exc(obj);") .containsOne("} catch (Exception e) {") .containsOne("e.printStackTrace();") .containsOne("} finally {") .containsOne("this.f = true;") .containsOne("return this.f;") .doesNotContain("boolean z"); } @Test public void testWithoutFinally() { args.setExtractFinally(false); assertThat(getClassNode(TestCls.class)) .code() .containsOne("exc(obj);") .containsOne(indent(3) + "} catch (Exception e) {") .containsOne(indent(2) + "} catch (Throwable th) {") .containsOne("this.f = false;") .countString(3, "this.f = true;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally10.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchFinally10 extends SmaliTest { // @formatter:off /* public static String test(Context context, int i) { CommonContracts.requireNonNull(context); InputStream inputStream = null; try { inputStream = context.getResources().openRawResource(i); Scanner useDelimiter = new Scanner(inputStream).useDelimiter("\\A"); return useDelimiter.hasNext() ? useDelimiter.next() : ""; } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { l.logException(LogLevel.ERROR, e); } } } } */ // @formatter:on @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .doesNotContain("boolean z = null;") .doesNotContain("} catch (Throwable") .containsOne("} finally {") .containsOne(".close();") .containsOne("} catch (IOException e") .containsOne(".logException("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally11.java ================================================ package jadx.tests.integration.trycatch; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchFinally11 extends IntegrationTest { public static class TestCls { private int count = 0; public void test(List list) { try { call1(); } finally { for (Object item : list) { call2(item); } } } private void call1() { count += 100; } private void call2(Object item) { count++; } public void check() { TestCls t = new TestCls(); t.test(Arrays.asList("1", "2")); assertThat(t.count).isEqualTo(102); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("} finally {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally12.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchFinally12 extends IntegrationTest { public static class TestCls { private StringBuilder sb; public void test1(int excType) { try { try { call(excType); } catch (NullPointerException e) { sb.append("-catch"); } sb.append("-out"); } finally { sb.append("-finally"); } } public void test2(int excType) { try { try { call(excType); } catch (NullPointerException e) { sb.append("-catch"); } } finally { sb.append("-finally"); } } public void test3(int excType) { try { call(excType); } catch (NullPointerException e) { sb.append("-catch"); } finally { sb.append("-finally"); } } public void call(int excType) { sb.append("call"); switch (excType) { case 1: sb.append("-npe"); throw new NullPointerException(); case 2: sb.append("-iae"); throw new IllegalArgumentException(); } } public String runTest(int testNumber, int excType) { sb = new StringBuilder(); try { switch (testNumber) { case 1: test1(excType); break; case 2: test2(excType); break; case 3: test3(excType); break; } } catch (IllegalArgumentException e) { assertThat(excType).isEqualTo(2); } return sb.toString(); } public void check() { assertThat(runTest(1, 0)).isEqualTo("call-out-finally"); assertThat(runTest(1, 1)).isEqualTo("call-npe-catch-out-finally"); assertThat(runTest(1, 2)).isEqualTo("call-iae-finally"); assertThat(runTest(2, 0)).isEqualTo("call-finally"); assertThat(runTest(2, 1)).isEqualTo("call-npe-catch-finally"); assertThat(runTest(2, 2)).isEqualTo("call-iae-finally"); assertThat(runTest(3, 0)).isEqualTo("call-finally"); assertThat(runTest(3, 1)).isEqualTo("call-npe-catch-finally"); assertThat(runTest(3, 2)).isEqualTo("call-iae-finally"); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .countString(3, "} finally {"); } @Test public void testWithoutFinally() { getArgs().setExtractFinally(false); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("} finally {") .countString(2 + 2 + 3, "sb.append(\"-finally\");"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally13.java ================================================ package jadx.tests.integration.trycatch; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchFinally13 extends IntegrationTest { public static class TestCls { public void test(int i) { try { doSomething1(); if (i == -12) { return; } if (i > 10) { doSomething2(); } else if (i == -1) { doSomething3(); } } catch (Exception ex) { logError(); } finally { doSomething4(); } } private void logError() { } private void doSomething1() { } private void doSomething2() { } private void doSomething3() { } private void doSomething4() { } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("} finally {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally14.java ================================================ package jadx.tests.integration.trycatch; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchFinally14 extends IntegrationTest { @SuppressWarnings("unused") public static class TestCls { private TCls t; public void test() { try { if (t != null) { t.doSomething(); } } finally { if (t != null) { t.doFinally(); } } } private static class TCls { public void doSomething() { } public void doFinally() { } } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11, TestProfile.JAVA8 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne(".doSomething();") .containsOne("} finally {") .containsOne(".doFinally();") .countString(2, "!= null) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally15.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Negative test case for finally extract (issue 1592). * Different registers incorrectly merged into one. */ @SuppressWarnings({ "CommentedOutCode", "GrazieInspection" }) public class TestTryCatchFinally15 extends SmaliTest { // @formatter:off /* protected final Parcel test(int i, Parcel parcel) throws RemoteException { Parcel obtain = Parcel.obtain(); try { try { this.zza.transact(i, parcel, obtain, 0); obtain.readException(); return obtain; } catch (RuntimeException e) { obtain.recycle(); throw e; } } finally { parcel.recycle(); } } */ // @formatter:on @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .doesNotContain("parcel = Parcel.obtain();") .containsOne("this.zza.transact(i, parcel, parcelObtain, 0);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally16.java ================================================ package jadx.tests.integration.trycatch; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchFinally16 extends SmaliTest { @SuppressWarnings("unused") public static class TestCls { public void test() { try { TCls.doSomething(); } catch (Exception e) { // do nothing } finally { TCls.doFinally(); } } private static class TCls { public static void doSomething() { } public static void doFinally() { } } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11, TestProfile.JAVA8 }) public void test() { disableCompilation(); ClassNode node = getClassNode(TestCls.class); assertThat(node) .code() .containsOne("TCls.doSomething()") .containsOne("TCls.doFinally()") .containsOne("finally") .containsOne("} catch") .contains("catch (Exception e)"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally17.java ================================================ package jadx.tests.integration.trycatch; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchFinally17 extends SmaliTest { @SuppressWarnings("unused") public static class TestCls { public int test() { try { TCls.doSomething(); } catch (UnsupportedOperationException e) { // do nothing } catch (NullPointerException e) { return 1; } finally { TCls.doFinally(); } return 0; } private static class TCls { public static void doSomething() { } public static void doFinally() { } } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11, TestProfile.JAVA8 }) public void test() { disableCompilation(); ClassNode node = getClassNode(TestCls.class); assertThat(node) .code() .containsOne("TCls.doSomething()") .containsOne("TCls.doFinally()") .containsOne("} finally") .containsOne("catch (NullPointerException ") .containsOne("catch (UnsupportedOperationException ") .doesNotContain("catch (Throwable"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally18.java ================================================ package jadx.tests.integration.trycatch; import jadx.NotYetImplemented; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchFinally18 extends SmaliTest { @SuppressWarnings("unused") public static class TestCls { public int test3() { int val; try { val = TCls.doSomething(); } catch (UnsupportedOperationException e) { return -1; } catch (NullPointerException e) { val = 0; } finally { TCls.dispose(); } val += 4; if (val < 10) { TCls.log("less than 10"); } return val; } private static class TCls { public static int doSomething() { return 14; } public static void dispose() { } public static void log(String msg) { } } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11 }) public void test() { disableCompilation(); ClassNode node = getClassNode(TestCls.class); assertThat(node) .code() .containsOne("TCls.doSomething()") .containsOne("TCls.dispose()") .containsOne("} finally") .containsOne("catch (NullPointerException ") .containsOne("catch (UnsupportedOperationException ") .doesNotContain("catch (Throwable"); } @NotYetImplemented("To be investigated why J8 does not work") @TestWithProfiles({ TestProfile.JAVA8 }) public void testJ8() { disableCompilation(); ClassNode node = getClassNode(TestCls.class); assertThat(node) .code() .containsOne("TCls.doSomething()") .containsOne("TCls.dispose()") .containsOne("} finally") .containsOne("catch (NullPointerException ") .containsOne("catch (UnsupportedOperationException ") .doesNotContain("catch (Throwable"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally19.java ================================================ package jadx.tests.integration.trycatch; import jadx.NotYetImplemented; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * A class which tests finally extraction for cases where the all handler does not rethrow an * exception. */ public class TestTryCatchFinally19 extends SmaliTest { @SuppressWarnings("unused") public static class TestCls { public Integer test() { Integer val; try { return TCls.doSomething(); } catch (Throwable t) { return null; } finally { TCls.dispose(); } } private static class TCls { public static int doSomething() { return 14; } public static void dispose() { } public static void log(String msg) { } } } @TestWithProfiles({ TestProfile.D8_J11 }) @NotYetImplemented("Currently only processing finally blocks if the all handler throws.") public void testDXJ8() { disableCompilation(); ClassNode node = getClassNode(TestCls.class); assertThat(node) .code() .containsOne("TCls.doSomething()") .containsOne("TCls.dispose()") .containsOne("} finally") .containsOne("catch (Throwable ") .containsOne("return null"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally2.java ================================================ package jadx.tests.integration.trycatch; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import org.junit.jupiter.api.Test; import jadx.core.clsp.ClspClass; import jadx.core.dex.instructions.args.ArgType; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTryCatchFinally2 extends IntegrationTest { public static class TestCls { private ClspClass[] classes; public void test(OutputStream output) throws IOException { DataOutputStream out = new DataOutputStream(output); try { out.writeByte(1); out.writeInt(classes.length); for (ClspClass cls : classes) { writeString(out, cls.getName()); } for (ClspClass cls : classes) { ArgType[] parents = cls.getParents(); out.writeByte(parents.length); for (ArgType parent : parents) { out.writeInt(parent.getObject().hashCode()); } } } finally { out.close(); } } private void writeString(DataOutputStream out, String name) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("} finally {") .containsOne("out.close();") .containsOne("for (ArgType parent : parents) {") .containsOne("for (ClspClass cls : this.classes) {") .containsOne("for (ClspClass cls2 : this.classes) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally3.java ================================================ package jadx.tests.integration.trycatch; import java.util.List; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTryCatchFinally3 extends IntegrationTest { public static class TestCls { private static final Logger LOG = LoggerFactory.getLogger(TestCls.class); public static void test(ClassNode cls, List passes) { try { cls.load(); for (IDexTreeVisitor visitor : passes) { DepthTraversal.visit(visitor, cls); } } catch (Exception e) { LOG.error("Class process exception: {}", cls, e); } finally { cls.unload(); } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("for (IDexTreeVisitor visitor : passes) {") .containsOne("} catch (Exception e) {") .containsOne("LOG.error(\"Class process exception: {}\", cls, e);") .containsOne("} finally {") .containsOne("cls.unload();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally4.java ================================================ package jadx.tests.integration.trycatch; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTryCatchFinally4 extends IntegrationTest { public static class TestCls { public void test() throws IOException { File file = File.createTempFile("test", "txt"); OutputStream outputStream = new FileOutputStream(file); try { outputStream.write(1); } finally { try { outputStream.close(); file.delete(); } catch (IOException ignored) { } } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("File file = File.createTempFile(\"test\", \"txt\");") .containsOne("OutputStream outputStream = new FileOutputStream(file);") .containsOne("outputStream.write(1);") .containsOne("} finally {") .containsOne("outputStream.close();") .containsOne("} catch (IOException e) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally5.java ================================================ package jadx.tests.integration.trycatch; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchFinally5 extends IntegrationTest { public static class TestCls { public List test(A a, B b) { C c = p(a); if (c == null) { return null; } D d = b.f(c); try { if (!d.first()) { return null; } List list = new ArrayList<>(); do { list.add(b.load(d)); } while (d.toNext()); return list; } finally { d.close(); } } private C p(A a) { return (C) a; } private interface A { } private interface B { D f(C c); T load(D d); } private interface C { } private interface D { boolean first(); boolean toNext(); void close(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("} finally {") .containsOne("d.close();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally6.java ================================================ package jadx.tests.integration.trycatch; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchFinally6 extends IntegrationTest { public static class TestCls { public static void test() throws IOException { InputStream is = null; try { call(); is = new FileInputStream("1.txt"); } finally { if (is != null) { is.close(); } } } private static void call() { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsLines(2, "InputStream is = null;", "try {", indent(1) + "call();", indent(1) + "is = new FileInputStream(\"1.txt\");", "} finally {", indent(1) + "if (is != null) {", indent(2) + "is.close();", indent(1) + '}', "}"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (0 != 0) {"); // impossible to prove that variables should be merged, so can't restore finally block here } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally7.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestTryCatchFinally7 extends IntegrationTest { public static class TestCls { private int f = 0; private boolean test(Object obj) { boolean res; try { res = exc(obj); } catch (Exception e) { res = false; } finally { f++; } return res; } private boolean exc(Object obj) throws Exception { if ("r".equals(obj)) { throw new AssertionError(); } return true; } public void check() { f = 0; assertThat(test(null)).isTrue(); assertThat(f).isEqualTo(1); f = 0; try { test("r"); } catch (AssertionError e) { // pass } assertThat(f).isEqualTo(1); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("try {") .contains("exc(obj);") .contains("} catch (Exception e) {") .doesNotContain("throw th;"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("throw th;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally8.java ================================================ package jadx.tests.integration.trycatch; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTryCatchFinally8 extends IntegrationTest { public static class TestCls { public Object test(Object obj) { File file = new File("r"); FileOutputStream output = null; try { output = new FileOutputStream(file); if (obj.equals("a")) { return new Object(); } else { return null; } } catch (IOException e) { System.out.println("Exception"); return null; } finally { if (output != null) { try { output.close(); } catch (IOException e) { // Ignored } } file.delete(); } } } @Test @NotYetImplemented("Fix merged catch blocks (shared code between catches)") public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("try {") .contains("} catch (IOException e) {") .contains("} finally {") .contains("file.delete();"); } @Test public void test2() { disableCompilation(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("output = new FileOutputStream(file);") .contains("} catch (IOException e) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally9.java ================================================ package jadx.tests.integration.trycatch; import java.io.IOException; import java.io.InputStream; import java.util.Scanner; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTryCatchFinally9 extends IntegrationTest { public static class TestCls { public String test() throws IOException { InputStream input = null; try { input = this.getClass().getResourceAsStream("resource"); Scanner scanner = new Scanner(input).useDelimiter("\\A"); return scanner.hasNext() ? scanner.next() : ""; } finally { if (input != null) { input.close(); } } } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("JADX INFO: finally extract failed") .doesNotContain(indent() + "throw ") .containsOne("} finally {") .containsOne("if (input != null) {") .containsOne("input.close();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchInIf.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static org.assertj.core.api.Assertions.assertThat; public class TestTryCatchInIf extends IntegrationTest { public static class TestCls { private String test(String name, String value) { if (value != null) { try { int key; if (value.startsWith("0x")) { value = value.substring(2); key = Integer.parseInt(value, 16); } else { key = Integer.parseInt(value); } return name + '=' + key; } catch (NumberFormatException e) { return "Failed to parse number"; } } System.out.println("?"); return null; } public void check() { assertThat(test("n", null)).isNull(); assertThat(test("n", "7")).isEqualTo("n=7"); assertThat(test("n", "0x" + Integer.toHexString(77))).isEqualTo("n=77"); assertThat(test("n", "abc")).isEqualTo("Failed to parse number"); assertThat(test("n", "0xabX")).isEqualTo("Failed to parse number"); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("try {") .containsOne("} catch (NumberFormatException e) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchInIf2.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; // #2384 public class TestTryCatchInIf2 extends IntegrationTest { public static class TestCls { public void test(Class cls) { Object obj = null; if (cls != null) { try { obj = cls.getDeclaredConstructor().newInstance(); } catch (Exception e) { System.out.println("error"); } } System.out.println("obj = " + obj); } } @Test public void test() { // happens only without debug info and java version >= 10 noDebugInfo(); useTargetJavaVersion(10); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code(); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchLastInsn.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchLastInsn extends SmaliTest { // @formatter:off /* public Exception test() { ? r1 = "result"; // String try { r1 = call(); // Exception } catch (Exception e) { System.out.println(r1); // String r1 = e; } return r1; } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("return call();") .containsOne("} catch (Exception e) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchMultiException.java ================================================ package jadx.tests.integration.trycatch; import java.security.ProviderException; import java.time.DateTimeException; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchMultiException extends IntegrationTest { public static class TestCls { public void test() { try { System.out.println("Test"); } catch (ProviderException | DateTimeException e) { throw new RuntimeException(e); } } } @Test public void test() { noDebugInfo(); String catchExcVarName = "e"; assertThat(getClassNode(TestCls.class)) .code() .containsOne("} catch (ProviderException | DateTimeException " + catchExcVarName + ") {") .containsOne("throw new RuntimeException(" + catchExcVarName + ");"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchMultiException2.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestTryCatchMultiException2 extends SmaliTest { // @formatter:off /* public static boolean test() { try { Class cls = Class.forName("c"); return ((Boolean) cls.getMethod("b", new Class[0]).invoke(cls, new Object[0])).booleanValue(); } catch (ClassNotFoundException | NoSuchMethodException | Exception | Throwable unused) { // java compiler don't allow shadow subclasses in multi-catch // in this case leave only Throwable return false; } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("} catch (Throwable unused) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMoveExc.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchNoMoveExc extends SmaliTest { // @formatter:off /* private static void test(AutoCloseable closeable) { if (closeable != null) { try { closeable.close(); } catch (Exception ignored) { } } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmaliWithPkg("trycatch", "TestTryCatchNoMoveExc")) .code() .containsOne("if (autoCloseable != null) {") .containsOne("try {") .containsOne("autoCloseable.close();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMoveExc2.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue: https://github.com/skylot/jadx/issues/395 */ public class TestTryCatchNoMoveExc2 extends SmaliTest { // @formatter:off /* private static void test(AutoCloseable closeable) { if (closeable != null) { try { closeable.close(); } catch (Exception unused) { } System.nanoTime(); } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmaliWithPkg("trycatch", "TestTryCatchNoMoveExc2")) .code() .containsOne("try {") .containsLines(2, "} catch (Exception unused) {", "}", "System.nanoTime();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchStartOnMove.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryCatchStartOnMove extends SmaliTest { // @formatter:off /* private static void test(String s) { try { call(s); } catch (Exception unused) { System.out.println("Failed call for " + s); } } private static void call(String s) {} */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmaliWithPkg("trycatch", "TestTryCatchStartOnMove")) .code() .containsOne("try {") .containsOne("} catch (Exception e) {") .containsOne("System.out.println(\"Failed call for \" + str"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryWithEmptyCatch.java ================================================ package jadx.tests.integration.trycatch; import java.util.Properties; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTryWithEmptyCatch extends IntegrationTest { public static class TestCls extends Exception { private static final long serialVersionUID = -5723049816464070603L; private Properties field; public TestCls(String str) { super(str); Properties properties = null; try { if (str.contains("properties")) { properties = new Properties(); } } catch (Exception unused) { // empty } this.field = properties; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("try {") .containsOne("if ("); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryWithEmptyCatchTriple.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTryWithEmptyCatchTriple extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() // all catches are empty .containsLines(2, "} catch (Error unused) {", "}") .containsLines(2, "} catch (Error unused2) {", "}") .containsLines(2, "} catch (Error unused3) {", "}"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryWithResources.java ================================================ package jadx.tests.integration.trycatch; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.SmaliTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTryWithResources extends SmaliTest { public static class TestCls { public static void writeFully(File file, byte[] data) throws IOException { try (OutputStream out = new FileOutputStream(file)) { out.write(data); } } } @Test @NotYetImplemented public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("try (") .doesNotContain("close()"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestUnreachableCatch.java ================================================ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @SuppressWarnings("CommentedOutCode") public class TestUnreachableCatch extends SmaliTest { // @formatter:off /* private static Map prepareFontData(Context context, FontInfo[] fonts, CancellationSignal cancellationSignal) { final HashMap out = new HashMap<>(); final ContentResolver resolver = context.getContentResolver(); for (FontInfo font : fonts) { if (font.getResultCode() != Columns.RESULT_CODE_OK) { continue; } final Uri uri = font.getUri(); if (out.containsKey(uri)) { continue; } ByteBuffer buffer = null; try (final ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r", cancellationSignal)) { if (pfd != null) { try (final FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) { final FileChannel fileChannel = fis.getChannel(); final long size = fileChannel.size(); buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size); } catch (IOException e) { // ignore } } } catch (IOException e) { // ignore } // TODO: try other approach?, e.g. read all contents instead of mmap. out.put(uri, buffer); } return Collections.unmodifiableMap(out); } */ // @formatter:on @Test public void test() { disableCompilation(); allowWarnInCode(); assertThat(getClassNodeFromSmali()) .code() .contains("IOException") .contains("Collections.unmodifiableMap"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/trycatch/TestUnreachableCatch2.java ================================================ package jadx.tests.integration.trycatch; import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestUnreachableCatch2 extends SmaliTest { @SuppressWarnings({ "unused", "DataFlowIssue" }) public static class UnusedExceptionHandlers1 implements AutoCloseable { public static void test(final Object unused1, final Object[] array, final Object o1, final Object o2, final Object unused2) { for (final Object item : array) { ByteBuffer buffer = null; try (final UnusedExceptionHandlers1 u = doSomething2(o1, "", o2)) { try (final FileInputStream fis = new FileInputStream(u.getFilename())) { final FileChannel fileChannel = fis.getChannel(); buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, 42); } catch (IOException e) { // ignore } } catch (IOException e) { // ignore } } } private String getFilename() { return null; } private static UnusedExceptionHandlers1 doSomething2(final Object o1, final String s, final Object o2) { return null; } @Override public void close() throws IOException { } } @Test public void test() { // TODO: result code not compilable because 'try' block split into 2 block and 'fis' var become // uninitialized disableCompilation(); assertThat(getClassNode(UnusedExceptionHandlers1.class)) .code() .doesNotContain("break;") .countString(2, "} catch (IOException e"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestArrayTypes.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestArrayTypes extends IntegrationTest { public static class TestCls { public void test() { Exception e = new Exception(); System.out.println(e); use(new Object[] { e }); } public void use(Object[] arr) { } public void check() { test(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("use(new Object[]{e});"); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("use(new Object[]{exc});"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestConstInline.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestConstInline extends SmaliTest { // @formatter:off /* private static String test(boolean b) { List list; String str; if (b) { list = Collections.emptyList(); str = "1"; } else { list = null; str = list; // not correct assign in java but bytecode allow it } return use(list, str); } private static String use(List list, String str) { return list + str; } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmaliWithPkg("types", "TestConstInline")) .code() .containsOne("list = null;") .containsOne("str = null;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestConstTypeInference.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestConstTypeInference extends IntegrationTest { @SuppressWarnings({ "overrides", "EqualsHashCode" }) public static class TestCls { private final int a; public TestCls() { this(0); } public TestCls(int a) { this.a = a; } public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null) { if (getClass() == obj.getClass()) { TestCls other = (TestCls) obj; return this.a == other.a; } } return false; } public void check() { TestCls seven = new TestCls(7); assertThat(seven).isEqualTo(seven); assertThat(seven).isNotEqualTo(null); TestCls six = new TestCls(6); assertThat(six).isNotEqualTo(seven); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("obj == this") .containsOneOf("obj == null", "obj != null"); } @Test public void test2() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestFieldAccess.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestFieldAccess extends IntegrationTest { public static class TestCls { private String field; static T testPut(T t) { ((TestCls) t).field = ""; return t; } static T testGet(T t) { System.out.println(((TestCls) t).field); return t; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("t.field"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestFieldCast.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; /** * Issue #962 */ public class TestFieldCast extends IntegrationTest { public static class TestCls { public static class A { public boolean publicField; boolean packagePrivateField; protected boolean protectedField; private boolean privateField; } public static class B extends A { public void test() { ((A) this).publicField = false; ((A) this).protectedField = false; ((A) this).packagePrivateField = false; ((A) this).privateField = false; // cast to 'A' needed only here } } public static class C { public void test(B b) { ((A) b).publicField = false; ((A) b).protectedField = false; ((A) b).packagePrivateField = false; ((A) b).privateField = false; // cast to 'A' needed only here } } private static class D { public void test(T t) { ((A) t).publicField = false; ((A) t).protectedField = false; ((A) t).packagePrivateField = false; ((A) t).privateField = false; // cast to 'A' needed only here } } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("((A) this)") .containsOne("((A) b)") .containsOne("((A) t)") .doesNotContain("unused =") .doesNotContain("access modifiers changed"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestGenerics extends IntegrationTest { public static class TestCls { public T data; public TestCls data(T t) { this.data = t; return this; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("TestCls data(T t) {"); } @Test public void test2() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("TestCls data(T t) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics2.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestGenerics2 extends SmaliTest { // @formatter:off /* public void test() { Map map = this.field; useInt(map.size()); for (Map.Entry entry : map.entrySet()) { useInt(entry.getKey().intValue()); entry.getValue().trim(); } } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("for (Map.Entry entry : map.entrySet()) {") .containsOne("useInt(entry.getKey().intValue());") // no Integer cast .containsOne("entry.getValue().trim();"); // no String cast } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics3.java ================================================ package jadx.tests.integration.types; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestGenerics3 extends IntegrationTest { public static class TestCls { public static void test() { List classes = getClasses(); Collections.sort(classes); int passed = 0; for (String cls : classes) { if (runTest(cls)) { passed++; } } int failed = classes.size() - passed; System.out.println("failed: " + failed); } private static boolean runTest(String clsName) { return false; } private static List getClasses() { return new ArrayList<>(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("List classes") .containsOne("for (String cls : classes) {"); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("List classes"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics4.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestGenerics4 extends IntegrationTest { public static class TestCls { public static class Inner { public void overload(IList list) { } public void overload(T t) { } } public interface IList { void list(T t); } public static class ObjIList implements IList { @Override public void list(Object o) { } } public Inner test() { Inner inner = new Inner<>(); inner.overload(new ObjIList()); return inner; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("public static class ObjIList implements IList {") .containsOne("Inner inner = new Inner<>();") .containsOne("inner.overload((IList) new ObjIList());"); } @NotYetImplemented @Test public void testOmitCast() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("inner.overload(new ObjIList());"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics5.java ================================================ package jadx.tests.integration.types; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestGenerics5 extends IntegrationTest { public static class TestCls { private InheritableThreadLocal> inheritableThreadLocal; public void test(String key, String val) { if (key == null) { throw new IllegalArgumentException("key cannot be null"); } Map map = this.inheritableThreadLocal.get(); if (map == null) { map = new HashMap<>(); this.inheritableThreadLocal.set(map); } map.put(key, val); } public void remove(String key) { Map map = this.inheritableThreadLocal.get(); if (map != null) { map.remove(key); } } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .countString(2, "Map map = this.inheritableThreadLocal.get();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics6.java ================================================ package jadx.tests.integration.types; import java.util.Iterator; import java.util.Map; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestGenerics6 extends IntegrationTest { public static class TestCls implements Iterable> { public V test(K key, V v) { Entry entry = get(key); if (entry != null) { return entry.mValue; } put(key, v); return null; } protected Entry get(K k) { return null; } protected Entry put(K key, V v) { return null; } @Override public Iterator> iterator() { return null; } static class Entry implements Map.Entry { final V mValue; Entry(K key, V value) { this.mValue = value; } @Override public K getKey() { return null; } @Override public V getValue() { return null; } @Override public V setValue(V value) { return null; } } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("Entry entry = get(k);") .containsOne("Entry entry = get(k);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics7.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue https://github.com/skylot/jadx/issues/956 */ public class TestGenerics7 extends IntegrationTest { public static class TestCls { private Object[] elements = new Object[1]; @SuppressWarnings("unchecked") public final T test(int i) { Object[] arr = this.elements; T obj = (T) arr[i]; arr[i] = null; if (obj == null) { throw new NullPointerException(); } return obj; } public void check() { this.elements = new Object[] { 1, "" }; assertThat(test(1)).isEqualTo(""); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("T t = (T) objArr[i];"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics8.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestGenerics8 extends IntegrationTest { public static class TestCls { public abstract static class Class2 extends Parent2 { public void test() { S s = get(); s.i1(); s.i2(); } } static class Parent2 { T t; protected T get() { return t; } } interface I1 { void i1(); } interface I2 { void i2(); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("S s = get();") .containsOne("s.i1();") .containsOne("s.i2();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestGenericsInFullInnerCls.java ================================================ package jadx.tests.integration.types; import java.util.List; import org.junit.jupiter.api.Test; import jadx.api.CommentsLevel; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestGenericsInFullInnerCls extends SmaliTest { @Test public void test() { getArgs().setCommentsLevel(CommentsLevel.WARN); List classNodes = loadFromSmaliFiles(); assertThat(searchCls(classNodes, "types.FieldCls")) .code() .containsOne("private ba.bb a;"); assertThat(searchCls(classNodes, "types.test.ba")) .code() .containsOne("public final class ba {") .containsOne("public final class bb {") .containsOne("private ba b;") .containsOne("private ba.bb.bc c;") .containsOne("public final class bc {") .containsOne("private ba a;"); } @Test public void testWithDeobf() { enableDeobfuscation(); args.setDeobfuscationMinLength(100); // rename everything getArgs().setCommentsLevel(CommentsLevel.WARN); loadFromSmaliFiles(); // compilation should pass } @Test public void testWithFullNames() { getArgs().setUseImports(false); getArgs().setCommentsLevel(CommentsLevel.WARN); loadFromSmaliFiles(); // compilation should pass } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestInterfacesCast.java ================================================ package jadx.tests.integration.types; import java.io.Closeable; import java.io.IOException; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInterfacesCast extends IntegrationTest { public static class TestCls { public Runnable test(Closeable obj) throws IOException { return (Runnable) obj; } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("return (Runnable) closeable;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestLongCast.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public class TestLongCast extends IntegrationTest { public static class TestCls { public long test(char c) { return (long) c << 32; } public int test2(long l) { return (int) l >> 2; } public void check() { assertThat(test((char) 22)).isEqualTo(94489280512L); assertThat(test2((1L << 32) + 8)).isEqualTo(2); } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOneOf( "return (long) c << 32;", "return ((long) c) << 32;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestPrimitiveConversion.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestPrimitiveConversion extends SmaliTest { // @formatter:off /* public void test(long j, boolean z) { putByte(j, z ? (byte) 1 : (byte) 0); } private static void putByte(long j, byte z) { } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .doesNotContain("putByte(j, z);") .containsOne("putByte(j, z ? (byte) 1 : (byte) 0);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestPrimitiveConversion2.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestPrimitiveConversion2 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOne("boolean z2 = !convertedPrice2.code.equals(itemCurrency.code);") .doesNotContain("z2 == 0") .doesNotContain("z2 | 2") .containsOne("(z2 ? 1 : 0) | 2") .containsOne("if (z2 && currency != null) {") .containsOne("i = 1;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestPrimitivesInIf.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestPrimitivesInIf extends IntegrationTest { public static class TestCls { public boolean test(String str) { short sh = Short.parseShort(str); int i = Integer.parseInt(str); System.out.println(sh + " vs " + i); return sh == i; } public void check() { assertThat(test("1")).isTrue(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("short sh = Short.parseShort(str);") .containsOne("int i = Integer.parseInt(str);") .containsOne("return sh == i;"); } @Test public void test2() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("short s = Short.parseShort(str);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeInheritance.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTypeInheritance extends IntegrationTest { public static class TestCls { public interface IRoot { } public interface IBase extends IRoot { } public static class A implements IBase { } public static class B implements IBase { public void b() { } } public static void test(boolean z) { IBase impl; if (z) { impl = new A(); } else { B b = new B(); b.b(); impl = b; // this move is removed in no-debug byte-code } useBase(impl); useRoot(impl); } private static void useRoot(IRoot root) { } private static void useBase(IBase base) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("IBase impl;") .containsOne("impl = new A();") .containsOne("B b = new B();") .containsOne("impl = b;"); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTypeResolver extends IntegrationTest { public static class TestCls { public TestCls(int b1, int b2) { // test 'this' move and constructor invocation on moved register this(b1, b2, 0, 0, 0); } public TestCls(int a1, int a2, int a3, int a4, int a5) { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("this(b1, b2, 0, 0, 0);") .doesNotContain("= this;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver10.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver10 extends SmaliTest { /* * Method argument assigned with different types in separate branches */ @Test public void test() { assertThat(getClassNodeFromSmali()) .code().containsOne("Object test(String str, String str2)") .doesNotContain("Object obj2 = 0;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver11.java ================================================ package jadx.tests.integration.types; import java.util.Arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver11 extends IntegrationTest { public static class TestCls { public Void test(Object... objects) { int val = (Integer) objects[0]; String str = (String) objects[1]; call(str, str, val, val); return null; } private void call(String a, String b, int... val) { } private boolean test2(String s1, String... args) { String str = Arrays.toString(args); return s1.length() + str.length() > 0; } public void check() { test(1, "str"); assertThat(test2("1", "2", "34")).isTrue(); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("(Integer) objects[0]") .containsOne("String str = (String) objects[1];"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("(Integer) objArr[0]") .containsOne("String str = (String) objArr[1];"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver12.java ================================================ package jadx.tests.integration.types; import java.lang.ref.WeakReference; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver12 extends IntegrationTest { public abstract static class TestCls { private WeakReference ref; public void test(String str) { T obj = this.ref.get(); if (obj != null) { call(obj, str); } } public abstract void call(T t, String str); } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("T obj = this.ref.get();"); } @Test public void testNoDebug() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("Object obj") .containsOne("T t = this.ref.get();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver13.java ================================================ package jadx.tests.integration.types; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver13 extends IntegrationTest { @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") public static class TestCls { private static final Set CONST = new HashSet<>(); private Map, List> map = new HashMap<>(); @SuppressWarnings("unchecked") public List test(Set type) { List obj = this.map.get(type == null ? CONST : type); if (obj != null) { return (List) obj; } return null; } } @NotYetImplemented("additional cast for generic types") @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("public List test(Set type) {") .containsOne("return (List) obj;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver14.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver14 extends SmaliTest { // @formatter:off /* public Date test() throws Exception { Date date = null; Long l = null; Cursor query = DBUtil.query(false, (CancellationSignal) null); try { if (query.moveToFirst()) { if (!query.isNull(0)) { l = Long.valueOf(query.getLong(0)); } date = this.this$0.toDate(l); } return date; } finally { query.close(); } } */ // @formatter:on @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .doesNotContain("? r2"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver15.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue 921 (second case) */ public class TestTypeResolver15 extends SmaliTest { public static class TestCls { private void test(boolean z) { useInt(z ? 0 : 8); useInt(!z ? 1 : 0); // replaced with xor in smali test } private void useInt(int i) { } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() // .containsOne("useInt(!z ? 1 : 0);") // TODO: convert to ternary .containsOne("useInt(z ? 0 : 8);"); } @Test public void testSmali() { assertThat(getClassNodeFromSmali()) .code() .containsOne("useInt(z ? 0 : 8);") .containsOne("useInt(!z ? 1 : 0);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver16.java ================================================ package jadx.tests.integration.types; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.function.Function; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static java.util.Collections.emptyList; /** * Issue 1002 * Insertion of additional cast (at use place) needed for successful type inference */ public class TestTypeResolver16 extends SmaliTest { @SuppressWarnings("unchecked") public static class TestCls { public final List test(List list, Set set, Function function) { if (set != null) { List union = list != null ? union(list, set, function) : null; if (union != null) { list = union; } } return list != null ? (List) list : emptyList(); } public static List union( Collection collection, Iterable iterable, Function function) { return null; } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("(List) listUnion"); } @Test public void testSmali() { assertThat(getClassNodeFromSmali()) .code() .containsOne("(List) listUnion"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver17.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue 1197 */ public class TestTypeResolver17 extends SmaliTest { // @formatter:off /* private static String test(Context context, Uri uri, String str, String str2) { Cursor cursor = null; try { cursor = context.getContentResolver().query(uri, new String[]{str}, null, null, null); if (cursor.moveToFirst() && !cursor.isNull(0)) { return cursor.getString(0); } closeQuietly(cursor); return str2; } catch (Exception e) { Log.w("DocumentFile", "Failed query: " + e); return str2; } finally { closeQuietly(cursor); } } */ // @formatter:on @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOne("Cursor cursorQuery = null;") .doesNotContain("(AutoCloseable autoCloseable = "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver18.java ================================================ package jadx.tests.integration.types; import java.io.Closeable; import java.io.IOException; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver18 extends IntegrationTest { public static class TestCls { private final AtomicReference reference = new AtomicReference<>(); public void test() { T t = this.reference.get(); if (t instanceof Closeable) { try { ((Closeable) t).close(); } catch (IOException unused) { // ignore } } this.reference.set(null); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("((Closeable) t).close();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver19.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue 1407 */ public class TestTypeResolver19 extends SmaliTest { public static class TestCls { public static int[] test(byte[] bArr) { int[] iArr = new int[bArr.length]; for (int i = 0; i < bArr.length; i++) { iArr[i] = bArr[i]; } return iArr; } public static int[] test2(byte[] bArr) { int[] iArr = new int[bArr.length]; for (int i = 0; i < bArr.length; i++) { int i2 = bArr[i]; if (i2 < 0) { i2 = (int) ((long) i2 & 0xFFFF_FFFFL); } iArr[i] = i2; } return iArr; } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("iArr[i] = bArr[i];") .containsOne("iArr[i] = i2;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver2.java ================================================ package jadx.tests.integration.types; import java.io.IOException; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTypeResolver2 extends IntegrationTest { public static class TestCls { public static boolean test(Object obj) throws IOException { if (obj != null) { return true; } throw new IOException(); } } @Test public void test() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("if (obj != null) {"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver20.java ================================================ package jadx.tests.integration.types; import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue 1238 */ public class TestTypeResolver20 extends SmaliTest { public static class TestCls { public interface Sequence { Iterator iterator(); } public static > T max(Sequence seq) { Iterator it = seq.iterator(); if (!it.hasNext()) { return null; } T t = it.next(); while (it.hasNext()) { T next = it.next(); if (t.compareTo(next) < 0) { t = next; } } return t; } private static class ArraySeq implements Sequence { private final List list; @SafeVarargs public ArraySeq(T... arr) { this.list = Arrays.asList(arr); } @Override public Iterator iterator() { return list.iterator(); } } public void check() { assertThat(max(new ArraySeq<>(2, 5, 3, 4))).isEqualTo(5); } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("next = next;") .containsOne("T next = it.next();"); } @Test public void testSmali() { assertThat(getClassNodeFromSmaliFiles()) .code() .containsOne("T next = it.next();") .containsOne("T next2 = it.next();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver21.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; /** * Issue 1527 */ @SuppressWarnings("CommentedOutCode") public class TestTypeResolver21 extends SmaliTest { // @formatter:off /* public Number test(Object objectArray) { Object[] arr = (Object[]) objectArray; return (Number) arr[0]; } */ // @formatter:on @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("Object[] arr = (Object[]) objectArray;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver22.java ================================================ package jadx.tests.integration.types; import java.io.IOException; import java.io.InputStream; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver22 extends IntegrationTest { public static class TestCls { public void test(InputStream input, long count) throws IOException { long pos = input.skip(count); while (pos < count) { pos += input.skip(count - pos); } } } @TestWithProfiles({ TestProfile.JAVA8, TestProfile.DX_J8 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("long pos = "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver23.java ================================================ package jadx.tests.integration.types; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver23 extends IntegrationTest { public static class TestCls { public long test(int a) { long v = 1L; if (a == 2) { v = 2L; } else if (a == 3) { v = 3L; } System.out.println(v); return v; } } @TestWithProfiles({ TestProfile.JAVA8, TestProfile.DX_J8 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("long v"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver24.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver24 extends SmaliTest { @SuppressWarnings("DataFlowIssue") public static class TestCls { public void test() { ((T1) null).foo1(); ((T2) null).foo2(); } static class T1 { public void foo1() { } } static class T2 { public void foo2() { } } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .containsOne("((T1) null).foo1();") .containsOne("((T2) null).foo2();"); } @Test public void testSmali() { assertThat(searchCls(loadFromSmaliFiles(), "Test1")) .code() .containsOne("((T1) null).foo1();") .containsOne("((T2) null).foo2();") .doesNotContain("T1 ") .doesNotContain("T2 "); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver25.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver25 extends SmaliTest { @Test public void testSmali() { // TODO: type inference error not yet resolved // Check that no stack overflow in type inference for now allowWarnInCode(); disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .oneOf(c -> c.containsOne("t = obj;"), c -> c.containsOne("t = (T) obj;")); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver26.java ================================================ package jadx.tests.integration.types; import java.util.ArrayList; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver26 extends IntegrationTest { @SuppressWarnings({ "rawtypes", "unchecked", "checkstyle:IllegalType" }) public static class TestCls { final ArrayList target = new ArrayList<>(); final ArrayList source = new ArrayList(); public void test() { ((ArrayList) target).add(source.get(0)); // cast removed in bytecode } } @Test public void test() { noDebugInfo(); assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("type inference failed") .containsOne("this.target.add((String) this.source.get(0));"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver3.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver3 extends IntegrationTest { @SuppressWarnings("UseCompareMethod") public static class TestCls { public int test(String s1, String s2) { int cmp = s2.compareTo(s1); if (cmp != 0) { return cmp; } return s1.length() == s2.length() ? 0 : s1.length() < s2.length() ? -1 : 1; } } @Test public void test() { useJavaInput(); assertThat(getClassNode(TestCls.class)) .code() .containsOneOf( "return s1.length() == s2.length() ? 0 : s1.length() < s2.length() ? -1 : 1;", "return s1.length() < s2.length() ? -1 : 1;"); } @Test public void test2() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver4.java ================================================ package jadx.tests.integration.types; import java.util.Arrays; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver4 extends IntegrationTest { public static class TestCls { private static String test(byte[] strArray, int offset) { int len = strArray.length; int start = offset + f(strArray, offset); int end = start; while (end + 1 < len && (strArray[end] != 0 || strArray[end + 1] != 0)) { end += 2; } byte[] arr = Arrays.copyOfRange(strArray, start, end); return new String(arr); } private static int f(byte[] strArray, int offset) { return 0; } public void check() { String test = test(("1234" + "utfstr\0\0" + "4567").getBytes(), 4); assertThat(test).isEqualTo("utfstr"); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("(strArray[end] != 0 || strArray[end + 1] != 0)"); } @Test public void test2() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver5.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver5 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .doesNotContain("Object string2") .doesNotContain("r1v2"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTypeResolver6 extends IntegrationTest { public static class TestCls { public final Object obj; public TestCls(boolean b) { this.obj = b ? this : makeObj(); } public Object makeObj() { return new Object(); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("this.obj = b ? this : makeObj();"); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6a.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestTypeResolver6a extends IntegrationTest { public static class TestCls implements Runnable { public final Runnable runnable; public TestCls(boolean b) { this.runnable = b ? this : makeRunnable(); } public Runnable makeRunnable() { return new Runnable() { @Override public void run() { // do nothing } }; } @Override public void run() { // do nothing } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("this.runnable = b ? this : makeRunnable();"); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver7.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver7 extends IntegrationTest { public static class TestCls { public void test(boolean a, boolean b) { Object obj = null; if (a) { use(b ? (Exception) getObj() : (Exception) obj); } else { Runnable r = (Runnable) obj; if (b) { r = (Runnable) getObj(); } use(r); } } private Object getObj() { return null; } private void use(Exception e) { } private void use(Runnable r) { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .oneOf(c -> c.containsOne("use(b ? (Exception) getObj() : null);"), c -> c.containsOne("use(b ? (Exception) getObj() : (Exception) null);")); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver8.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver8 extends SmaliTest { // @formatter:off /* public class A {} public class B { public B(A a) { } } public static class TestCls { private A f; public void test() { A x = this.f; if (x != null) { x = new B(x); // different types, type of 'x' can't be resolved } use(x); } private void use(B b) {} } */ // @formatter:on @Test @NotYetImplemented public void test() { assertThat(getClassNodeFromSmaliFiles("types", "TestTypeResolver8", "TestCls")) .code() .containsOne("use(a != null ? new B(a) : null);"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver9.java ================================================ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver9 extends IntegrationTest { public static class TestCls { public int test(byte b) { return 16777216 * b; } public int test2(byte[] array, int offset) { return array[offset] * 128 + (array[offset + 1] & 0xFF); } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("return 16777216 * b;") .doesNotContain("Byte.MIN_VALUE"); } @Test public void testNoDebug() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/usethis/TestDontInlineThis.java ================================================ package jadx.tests.integration.usethis; import java.util.Random; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestDontInlineThis extends IntegrationTest { public static class TestCls { public int field = new Random().nextInt(); public TestCls test() { TestCls res; if (field == 7) { res = this; System.out.println(); } else { res = new TestCls(); } res.method(); return res; } private void method() { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("TestDontInlineThis$TestCls res") .containsOne("res = this;") .containsOne("res = new TestDontInlineThis$TestCls();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/usethis/TestInlineThis.java ================================================ package jadx.tests.integration.usethis; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestInlineThis extends IntegrationTest { public static class TestCls { public int field; public void test() { TestCls something = this; something.method(); something.field = 123; } private void method() { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("something") .doesNotContain("something.method()") .doesNotContain("something.field") .doesNotContain("= this") .containsOne("this.field = 123;") .containsOne("method();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/usethis/TestInlineThis2.java ================================================ package jadx.tests.integration.usethis; import java.util.Objects; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestInlineThis2 extends IntegrationTest { @SuppressWarnings("ConstantValue") public static class TestCls { public int field; public void test() { TestCls thisVar = this; if (Objects.isNull(thisVar)) { System.out.println("null"); } thisVar.method(); thisVar.field = 123; } private void method() { } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("thisVar") .doesNotContain("thisVar.method()") .doesNotContain("thisVar.field") .doesNotContain("= this") .containsOne("if (Objects.isNull(this)) {") .containsOne("this.field = 123;") .containsOne("method();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/usethis/TestRedundantThis.java ================================================ package jadx.tests.integration.usethis; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestRedundantThis extends IntegrationTest { public static class TestCls { public int field1 = 1; public int field2 = 2; public boolean f1() { return false; } public int method() { f1(); return field1; } public void method2(int field2) { this.field2 = field2; } } // @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .doesNotContain("this.f1();") .doesNotContain("return this.field1;") .contains("this.field2 = field2;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestThisBranchDup.java ================================================ package jadx.tests.integration.variables; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestThisBranchDup extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .containsOne("this(") // constructor type correctly detected .countString(6, "(i & "); // ternary used and inlined } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables2.java ================================================ package jadx.tests.integration.variables; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestVariables2 extends IntegrationTest { public static class TestCls { public Object test(Object s) { Object store = s != null ? s : null; if (store == null) { store = new Object(); s = store; } return store; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("Object store = s != null ? s : null;"); } @Test public void testNoDebug() { noDebugInfo(); JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("Object obj2 = obj != null ? obj : null;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables3.java ================================================ package jadx.tests.integration.variables; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestVariables3 extends IntegrationTest { public static class TestCls { String test(Object s) { int i; if (s == null) { i = 2; } else { i = 3; s = null; } return s + " " + i; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("int i;") .contains("i = 2;") .contains("i = 3;") .contains("s = null;") .contains("return s + \" \" + i;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables4.java ================================================ package jadx.tests.integration.variables; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; @SuppressWarnings("checkstyle:printstacktrace") public class TestVariables4 extends IntegrationTest { public static class TestCls { public static boolean runTest(String clsName) { try { boolean pass = false; String msg = null; Throwable exc = null; Class cls = Class.forName(clsName); if (cls.getSuperclass() == AbstractTest.class) { Method mth = cls.getMethod("testRun"); try { AbstractTest test = (AbstractTest) cls.getConstructor().newInstance(); pass = (Boolean) mth.invoke(test); } catch (InvocationTargetException e) { pass = false; exc = e.getCause(); } catch (Throwable e) { pass = false; exc = e; } } else { msg = "not extends AbstractTest"; } System.err.println(">> " + (pass ? "PASS" : "FAIL") + '\t' + clsName + (msg == null ? "" : "\t - " + msg)); if (exc != null) { exc.printStackTrace(); } return pass; } catch (ClassNotFoundException e) { System.err.println("Class '" + clsName + "' not found"); } catch (Exception e) { e.printStackTrace(); } return false; } private static class AbstractTest { } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .contains("} catch (InvocationTargetException e) {") .contains("pass = false;") .contains("exc = e.getCause();") .contains("System.err.println(\"Class '\" + clsName + \"' not found\");") .contains("return pass;"); } @Test public void test2() { noDebugInfo(); getClassNode(TestCls.class); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables5.java ================================================ package jadx.tests.integration.variables; import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestVariables5 extends IntegrationTest { public static class TestCls { public String f = "str//ing"; private boolean enabled; private void testIfInLoop() { int i = 0; for (int i2 = 0; i2 < f.length(); i2++) { char ch = f.charAt(i2); if (ch == '/') { i++; if (i == 2) { setEnabled(true); return; } } } setEnabled(false); } private void setEnabled(boolean b) { this.enabled = b; } public void check() { setEnabled(false); testIfInLoop(); assertThat(enabled).isTrue(); } } @Test public void test() { noDebugInfo(); ClassNode cls = getClassNode(TestCls.class); assertThat(cls) .code() .doesNotContain("int i2++;") .containsOne("int i = 0;") .containsOneOf("i++;", "&& (i = i + 1) == 2"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables6.java ================================================ package jadx.tests.integration.variables; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestVariables6 extends SmaliTest { @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmaliWithPath("variables", "TestVariables6")) .code() .doesNotContain("r4") .doesNotContain("r1v1") .contains("DateStringParser dateStringParser") .contains("FinancialInstrumentMetadataAttribute startYear =" + " this.mFinancialInstrumentMetadataDefinition.getStartYear();"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesDeclAnnotation.java ================================================ package jadx.tests.integration.variables; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import jadx.api.ICodeInfo; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.api.metadata.annotations.VarNode; import jadx.api.utils.CodeUtils; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestVariablesDeclAnnotation extends IntegrationTest { public abstract static class TestCls { public int test(String str, int i) { return i; } public abstract int test2(String str); } @Test public void test() { noDebugInfo(); ClassNode cls = getClassNode(TestCls.class); assertThat(cls).code() .containsOne("public int test(String str, int i) {") .containsOne("public abstract int test2(String str);"); checkArgNamesInMethod(cls, "test", "[str, i]"); checkArgNamesInMethod(cls, "test2", "[str]"); } private static void checkArgNamesInMethod(ClassNode cls, String mthName, String expectedVars) { MethodNode testMth = cls.searchMethodByShortName(mthName); assertThat(testMth).isNotNull(); ICodeInfo codeInfo = cls.getCode(); int mthDefPos = testMth.getDefPosition(); int lineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos); List argNames2 = new ArrayList<>(); codeInfo.getCodeMetadata().searchDown(mthDefPos, (pos, ann) -> { if (pos > lineEndPos) { return Boolean.TRUE; // stop at line end } if (ann instanceof NodeDeclareRef) { ICodeNodeRef declRef = ((NodeDeclareRef) ann).getNode(); if (declRef instanceof VarNode) { VarNode varNode = (VarNode) declRef; if (varNode.getMth().equals(testMth)) { argNames2.add(varNode.getName()); } } } return null; }); assertThat(argNames2).doesNotContainNull(); assertThat(argNames2.toString()).isEqualTo(expectedVars); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesDefinitions.java ================================================ package jadx.tests.integration.variables; import java.util.List; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestVariablesDefinitions extends IntegrationTest { public static class TestCls { private static Logger log; private ClassNode cls; private List passes; public void test() { try { cls.load(); for (IDexTreeVisitor pass : this.passes) { DepthTraversal.visit(pass, cls); } } catch (Exception e) { log.error("Decode exception: {}", cls, e); } } } @Test public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne(indent(3) + "for (IDexTreeVisitor pass : this.passes) {") .doesNotContain("iterator;") .doesNotContain("Iterator"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesDefinitions2.java ================================================ package jadx.tests.integration.variables; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; public class TestVariablesDefinitions2 extends IntegrationTest { public static class TestCls { public static int test(List list) { int i = 0; if (list != null) { for (String str : list) { if (str.isEmpty()) { i++; } } } return i; } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("int i = 0;") .containsOne("i++;") .containsOne("return i;") .doesNotContain("i2;"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesGeneric.java ================================================ package jadx.tests.integration.variables; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestVariablesGeneric extends SmaliTest { // @formatter:off /* public static j a(i iVar, c cVar) { if (iVar == null) { throw new IllegalArgumentException("subscriber can not be null"); } if (cVar.a == null) { throw new IllegalStateException("onSubscribe function can not be null."); } ... } */ // @formatter:on @Test public void test() { disableCompilation(); assertThat(getClassNodeFromSmali()) .code() .doesNotContain("iVar2") .containsOne("public static j a(i iVar, c cVar) throws OnErrorFailedException {") .containsOne("if (iVar == null) {") .countString(2, "} catch (Throwable th"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesIfElseChain.java ================================================ package jadx.tests.integration.variables; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import jadx.tests.api.utils.assertj.JadxAssertions; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestVariablesIfElseChain extends IntegrationTest { public static class TestCls { String used; public String test(int a) { if (a == 0) { use("zero"); } else if (a == 1) { String r = m(a); if (r != null) { use(r); } } else if (a == 2) { String r = m(a); if (r != null) { use(r); } } else { return "miss"; } return null; } public String m(int a) { return "hit" + a; } public void use(String s) { used = s; } public void check() { test(0); assertThat(used).isEqualTo("zero"); test(1); assertThat(used).isEqualTo("hit1"); test(2); assertThat(used).isEqualTo("hit2"); assertThat(test(5)).isEqualTo("miss"); } } @Test public void test() { JadxAssertions.assertThat(getClassNode(TestCls.class)) .code() .containsOne("return \"miss\";"); // and compilable } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesInInlinedAssign.java ================================================ package jadx.tests.integration.variables; import jadx.tests.api.IntegrationTest; import jadx.tests.api.extensions.profiles.TestProfile; import jadx.tests.api.extensions.profiles.TestWithProfiles; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestVariablesInInlinedAssign extends IntegrationTest { public static class TestCls { public final int test(final char[] s) { int i; for (i = 0; i < s.length; i++) { final char c = s[i]; if (c != 'a' && c != 'b') { break; } } return i; } } @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) public void test() { assertThat(getClassNode(TestCls.class)) .code() .containsOne("char c"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesInLoop.java ================================================ package jadx.tests.integration.variables; import org.junit.jupiter.api.Test; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestVariablesInLoop extends SmaliTest { @Test public void test() { assertThat(getClassNodeFromSmali()) .code() .containsOne("int iMth;") .countString(2, "iMth = 0;") .doesNotContain("i2"); } } ================================================ FILE: jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesUsageWithLoops.java ================================================ package jadx.tests.integration.variables; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestVariablesUsageWithLoops extends IntegrationTest { public static class TestEnhancedFor { public void test() { List list; synchronized (this) { list = new ArrayList<>(); } for (Object o : list) { System.out.println(o); } } } @Test public void testEnhancedFor() { assertThat(getClassNode(TestEnhancedFor.class)) .code() .containsLine(2, "synchronized (this) {") .containsLine(3, "list = new ArrayList<>"); } public static class TestForLoop { @SuppressWarnings("rawtypes") public void test() { List list; synchronized (this) { list = new ArrayList<>(); } for (int i = 0; i < list.size(); i++) { System.out.println(i); } } } @Test public void testForLoop() { assertThat(getClassNode(TestEnhancedFor.class)) .code() .containsLine(2, "synchronized (this) {") .containsLine(3, "list = new ArrayList<>"); } } ================================================ FILE: jadx-core/src/test/raung/enums/TestEnums11.raung ================================================ .version 52 # Java 8 .class public final enum enums/TestEnums11 .super java/lang/Enum .signature Ljava/lang/Enum; .source "SourceFile" .field public static final enum A Lenums/TestEnums11; .field public static final synthetic b [Lenums/TestEnums11; .field public final a I .method public static values()[Lenums/TestEnums11; .max stack 1 .max locals 1 getstatic enums/TestEnums11 b [Lenums/TestEnums11; invokevirtual [Lenums/TestEnums11; clone ()Ljava/lang/Object; checkcast [Lenums/TestEnums11; areturn .end method .method public static valueOf(Ljava/lang/String;)Lenums/TestEnums11; .max stack 2 .max locals 1 ldc Lenums/TestEnums11; aload 0 invokestatic java/lang/Enum valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; checkcast enums/TestEnums11 areturn .end method .method public ()V .signature (I)V .max stack 4 .max locals 1 aload 0 dup ldc "UNKNOWN" iconst_0 invokespecial java/lang/Enum (Ljava/lang/String;I)V bipush -99 putfield enums/TestEnums11 a I return .end method .method public static ()V .max stack 4 .max locals 1 new enums/TestEnums11 dup dup astore 0 invokespecial enums/TestEnums11 ()V putstatic enums/TestEnums11 A Lenums/TestEnums11; iconst_1 anewarray enums/TestEnums11 dup iconst_0 aload 0 aastore putstatic enums/TestEnums11 b [Lenums/TestEnums11; return .end method .method public getErrorCode()I .max stack 1 .max locals 1 aload 0 getfield enums/TestEnums11 a I ireturn .end method ================================================ FILE: jadx-core/src/test/raung/java8/TestLambdaInstance3.raung ================================================ .version 52 # Java 8 .class public interface abstract java8/TestLambdaInstance3 .implements java/io/Serializable .implements java/util/function/Supplier .signature Ljava/lang/Object;Ljava/io/Serializable;Ljava/util/function/Supplier; .source "Function0.java" .annotation runtime Ljava/lang/FunctionalInterface; .end annotation .method public memoized()Ljava8/TestLambdaInstance3; .signature ()Ljava8/TestLambdaInstance3; .max stack 2 .max locals 1 .local 0 "this" Ljava8/TestLambdaInstance3; Ljava8/TestLambdaInstance3; .line 197 aload 0 invokeinterface java8/TestLambdaInstance3 isMemoized ()Z ifeq :L1 .line 198 aload 0 areturn :L1 .line 200 .stack same aload 0 invokestatic test/Lazy of (Ljava/util/function/Supplier;)Ltest/Lazy; dup invokevirtual java/lang/Object getClass ()Ljava/lang/Class; pop invokedynamic apply (Ltest/Lazy;)Ljava8/TestLambdaInstance3; .handle invoke-static java/lang/invoke/LambdaMetafactory altMetafactory (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; .arg 0 .methodtype ()Ljava/lang/Object; .arg 1 .handle invoke-virtual test/Lazy get ()Ljava/lang/Object; .arg 2 .methodtype ()Ljava/lang/Object; .arg 3 7 .arg 4 1 .arg 5 Ltest/Memoized; .arg 6 0 .end invokedynamic checkcast test/Memoized checkcast java8/TestLambdaInstance3 areturn .end method ================================================ FILE: jadx-core/src/test/raung/jbc/TestStackConvert.raung ================================================ .version 52 # Java 8 .class public super jbc/TestStackConvert .method public parseIntDefault(Ljava/lang/String;I)I .max stack 3 .max locals 5 :L0 .local 0 "this" Ljbc/TestStackConvert; .local 1 "num" Ljava/lang/String; .local 2 "defaultNum" I .line 13 aload 1 invokestatic java/lang/Integer parseInt (Ljava/lang/String;)I :L1 .catch java/lang/NumberFormatException :L0 .. :L1 goto :L2 ireturn :L3 .line 14 .stack full stack 0 java/lang/NumberFormatException local 0 jbc/TestStackConvert local 1 java/lang/String local 2 int local 3 Top local 4 java/lang/NumberFormatException .end stack astore 3 .local 3 "e" Ljava/lang/NumberFormatException; .line 15 aload 3 invokevirtual java/lang/NumberFormatException printStackTrace ()V .line 17 iload 2 .end local 3 # "e" ireturn :L2 .stack full stack 0 java/lang/NumberFormatException local 0 jbc/TestStackConvert local 1 java/lang/String local 2 int .end stack .end local 0 # "this" .end local 1 # "num" .end local 2 # "defaultNum" astore 4 getstatic java/lang/System out Ljava/io/PrintStream; ldc "Before println" invokevirtual java/io/PrintStream println (Ljava/lang/String;)V aload 4 goto :L3 .end method ================================================ FILE: jadx-core/src/test/raung/loops/TestLoopRestore2.raung ================================================ .version 45.3 # Java 1.1 .class public super loops/TestLoopRestore2 .method public test([I[I)V .max stack 5 .max locals 6 iconst_0 istore 3 sipush 0 ifeq :L0 goto :L1 :L1 sipush 1 ifeq :L1 goto :L0 :L0 iinc 3 1 return .end method ================================================ FILE: jadx-core/src/test/raung/others/TestClassImplementsSignature.raung ================================================ .version 52 # Java 8 .class public super others/TestClassImplementsSignature .signature Ljava/lang/Object;Ljava/lang/Comparable;>; .field value Ljava/lang/Object; .signature TT; .end field .method public ()V .max stack 1 .max locals 1 aload 0 invokespecial java/lang/Object ()V return .end method ================================================ FILE: jadx-core/src/test/raung/others/TestJavaDup2x2.raung ================================================ .version 45.3 .class others/TestJavaDup2x2 .auto frames .method public static test([D)V aload 0 iconst_0 aload 0 iconst_1 aload 0 iconst_2 aload 0 iconst_3 ldc2_w 127.5 dup2_x2 dastore dup2_x2 dastore dup2_x2 dastore dastore return .end method ================================================ FILE: jadx-core/src/test/raung/others/TestJavaJSR.raung ================================================ .version 45.3 .class others/TestJavaJSR .method public test(Ljava/net/URL;)Ljava/lang/String; .throw java/io/IOException .max stack 2 .max locals 6 .local 0 "this" Lothers/TestJavaJSR; .local 1 "url" Ljava/net/URL; .line 88 aload 1 invokevirtual java/net/URL openStream ()Ljava/io/InputStream; astore 2 :L0 .local 2 "in" Ljava/io/InputStream; .line 89 .line 90 aload 0 aload 2 invokevirtual others/TestJavaJSR call (Ljava/io/InputStream;)Ljava/lang/String; astore 3 jsr :L3 aload 3 areturn :L1 .catch all :L0 .. :L1 goto :L1 .line 89 astore 4 jsr :L3 aload 4 athrow :L3 astore 5 .line 92 aload 2 invokevirtual java/io/InputStream close ()V .line 89 ret 5 .end method .method public call(Ljava/io/InputStream;)Ljava/lang/String; .throw java/io/IOException .max stack 1 .max locals 2 ldc "" areturn .end method ================================================ FILE: jadx-core/src/test/raung/others/TestJavaSwap.raung ================================================ .version 52 .class others/TestJavaSwap .auto frames .field private field Ljava/lang/Iterable; .method public toString()Ljava/lang/String; aload 0 getfield others/TestJavaSwap field Ljava/lang/Iterable; invokestatic java/lang/String valueOf (Ljava/lang/Object;)Ljava/lang/String; astore 1 bipush 8 aload 1 invokestatic java/lang/String valueOf (Ljava/lang/Object;)Ljava/lang/String; invokevirtual java/lang/String length ()I iadd new java/lang/StringBuilder dup_x1 swap invokespecial java/lang/StringBuilder (I)V ldc "concat(" invokevirtual java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder; aload 1 invokevirtual java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder; ldc ")" invokevirtual java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder; invokevirtual java/lang/StringBuilder toString ()Ljava/lang/String; areturn .end method ================================================ FILE: jadx-core/src/test/raung/others/TestStringConcatJava11.raung ================================================ .version 55 .class public others/TestStringConcatJava11 .method public test(Ljava/lang/String;)Ljava/lang/String; .max stack 1 .max locals 2 aload 1 invokedynamic makeConcatWithConstants (Ljava/lang/String;)Ljava/lang/String; .handle invoke-static java/lang/invoke/StringConcatFactory makeConcatWithConstants (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; .arg 0 "\u0001test" .end invokedynamic areturn .end method .method public test2(Ljava/lang/String;)Ljava/lang/String; .max stack 2 .max locals 2 aload 1 aload 1 invokedynamic makeConcatWithConstants (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; .handle invoke-static java/lang/invoke/StringConcatFactory makeConcatWithConstants (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; .arg 0 "\u0001test\u0001\u0002" .arg 1 7 # synthetic usage, compiler adds const values into recipe string .end invokedynamic areturn .end method ================================================ FILE: jadx-core/src/test/resources/logback.xml ================================================ %-5level - %msg%n ================================================ FILE: jadx-core/src/test/resources/manifest/IllegalCharsForGradleWrapper.xml ================================================ ================================================ FILE: jadx-core/src/test/resources/manifest/MinSdkVersion25.xml ================================================ ================================================ FILE: jadx-core/src/test/resources/manifest/OptionalTargetSdkVersion.xml ================================================ ================================================ FILE: jadx-core/src/test/resources/manifest/strings.xml ================================================ JadxTestApp ================================================ FILE: jadx-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker ================================================ mock-maker-inline ================================================ FILE: jadx-core/src/test/smali/arith/TestArithConst.smali ================================================ .class public LTestArithConst; .super Ljava/lang/Object; .field public static final CONST_INT:I = 0xff .method private test(I)I .registers 2 add-int/lit16 v0, p1, 0xff return v0 .end method ================================================ FILE: jadx-core/src/test/smali/arith/TestArithNot.smali ================================================ .class public LTestArithNot; .super Ljava/lang/Object; .method private test1(I)I .registers 2 .param p1, "a" not-int v0, p1 return v0 .end method .method private test2(J)J .registers 4 .param p1, "b" not-long v0, p1 return v0 .end method ================================================ FILE: jadx-core/src/test/smali/arith/TestXor.smali ================================================ .class public Larith/TestXor; .super Ljava/lang/Object; .method public test()Z .locals 1 .line 20 const/4 v0, 0x1 return v0 .end method .method public test1()Z .locals 1 .line 12 invoke-virtual {p0}, Larith/TestXor;->test()Z move-result v0 xor-int/lit8 v0, v0, 0x1 return v0 .end method .method public test2()Z .locals 1 .line 16 invoke-virtual {p0}, Larith/TestXor;->test()Z move-result v0 xor-int/lit8 v0, v0, 0x0 return v0 .end method ================================================ FILE: jadx-core/src/test/smali/arrays/TestArrayFillWithMove/TestCls.smali ================================================ .class public Larrays/TestCls; .super Ljava/lang/Object; .method public test()[J .registers 4 const/16 v3, 0x2 move/from16 v0, v3 new-array v0, v0, [J move-object/from16 v1, v0 fill-array-data v1, :array_0 return v1 :array_0 .array-data 8 0x0 0x1 .end array-data .end method ================================================ FILE: jadx-core/src/test/smali/arrays/TestArrayInitField2.smali ================================================ .class public Larrays/TestArrayInitField2; .super Ljava/lang/Object; .field static myArr:[J .method static constructor ()V .locals 4 const v0, 0x3 new-array v0, v0, [J sput-object v0, Larrays/TestArrayInitField2;->myArr:[J const/4 v1, 0x0 const-wide/32 v2, 0x4c78b648 aput-wide v2, v0, v1 return-void nop .end method .method public check()V .registers 5 sget-object v0, Larrays/TestArrayInitField2;->myArr:[J invoke-static {v0}, Lorg/assertj/core/api/Assertions;->assertThat([J)Lorg/assertj/core/api/AbstractLongArrayAssert; move-result-object v0 const/4 v1, 0x3 invoke-virtual {v0, v1}, Lorg/assertj/core/api/AbstractLongArrayAssert;->hasSize(I)Lorg/assertj/core/api/AbstractLongArrayAssert; sget-object v0, Larrays/TestArrayInitField2;->myArr:[J const/4 v1, 0x0 aget-wide v0, v0, v1 invoke-static {v0, v1}, Lorg/assertj/core/api/Assertions;->assertThat(J)Lorg/assertj/core/api/AbstractLongAssert; move-result-object v0 const-wide/32 v2, 0x4c78b648 invoke-virtual {v0, v2, v3}, Lorg/assertj/core/api/AbstractLongAssert;->isEqualTo(J)Lorg/assertj/core/api/AbstractLongAssert; return-void .end method ================================================ FILE: jadx-core/src/test/smali/arrays/TestFillArrayData/TestCls.smali ================================================ .class public Larrays/TestCls; .super Ljava/lang/Object; .method public test([J)V .registers 2 fill-array-data p1, :array_0 return-void :array_0 .array-data 8 0x1 0x2 .end array-data .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestBooleanToByte.smali ================================================ .class public Lconditions/TestBooleanToByte; .super Ljava/lang/Object; .field private showConsent:Z .method public writeToParcel(Lconditions/TestBooleanToByte;)V .locals 0 iget-boolean p1, p0, Lconditions/TestBooleanToByte;->showConsent:Z int-to-byte p1, p1 invoke-virtual {p0, p1}, Lconditions/TestBooleanToByte;->write(B)V return-void .end method .method public write(B)V .locals 0 return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestBooleanToChar.smali ================================================ .class public Lconditions/TestBooleanToChar; .super Ljava/lang/Object; .field private showConsent:Z .method public writeToParcel(Lconditions/TestBooleanToChar;)V .locals 0 iget-boolean p1, p0, Lconditions/TestBooleanToChar;->showConsent:Z int-to-char p1, p1 invoke-virtual {p0, p1}, Lconditions/TestBooleanToChar;->write(C)V return-void .end method .method public write(C)V .locals 0 return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestBooleanToDouble.smali ================================================ .class public Lconditions/TestBooleanToDouble; .super Ljava/lang/Object; .field private showConsent:Z .method public writeToParcel(Lconditions/TestBooleanToDouble;)V .locals 0 iget-boolean p1, p0, Lconditions/TestBooleanToDouble;->showConsent:Z int-to-double p1, p1 invoke-virtual {p0, p1}, Lconditions/TestBooleanToDouble;->write(D)V return-void .end method .method public write(D)V .locals 0 return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestBooleanToFloat.smali ================================================ .class public Lconditions/TestBooleanToFloat; .super Ljava/lang/Object; .field private showConsent:Z .method public writeToParcel(Lconditions/TestBooleanToFloat;)V .locals 0 iget-boolean p1, p0, Lconditions/TestBooleanToFloat;->showConsent:Z int-to-float p1, p1 invoke-virtual {p0, p1}, Lconditions/TestBooleanToFloat;->write(F)V return-void .end method .method public write(F)V .locals 0 return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestBooleanToInt.smali ================================================ .class public Lconditions/TestBooleanToInt; .super Ljava/lang/Object; .field private showConsent:Z .method public writeToParcel(Lconditions/TestBooleanToInt;)V .locals 0 iget-boolean p1, p0, Lconditions/TestBooleanToInt;->showConsent:Z invoke-virtual {p0, p1}, Lconditions/TestBooleanToInt;->write(I)V return-void .end method .method public write(I)V .locals 0 return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestBooleanToInt2.smali ================================================ .class public Lconditions/TestBooleanToInt2; .super Ljava/lang/Object; .method public test()V .registers 3 invoke-direct {p0}, Lconditions/TestBooleanToInt2;->getValue()Z move-result v0 invoke-static {v0}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer; move-result-object v1 invoke-direct {p0, v1}, Lconditions/TestBooleanToInt2;->use1(Ljava/lang/Integer;)V invoke-direct {p0, v0}, Lconditions/TestBooleanToInt2;->use2(I)V return-void .end method .method private getValue()Z .registers 2 const/4 v0, 0x0 return v0 .end method .method private use1(Ljava/lang/Integer;)V .registers 2 return-void .end method .method private use2(I)V .registers 2 return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestBooleanToLong.smali ================================================ .class public Lconditions/TestBooleanToLong; .super Ljava/lang/Object; .field private showConsent:Z .method public writeToParcel(Lconditions/TestBooleanToLong;)V .locals 0 iget-boolean p1, p0, Lconditions/TestBooleanToLong;->showConsent:Z int-to-long p1, p1 invoke-virtual {p0, p1}, Lconditions/TestBooleanToLong;->write(J)V return-void .end method .method public write(J)V .locals 0 return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestBooleanToShort.smali ================================================ .class public Lconditions/TestBooleanToShort; .super Ljava/lang/Object; .field private showConsent:Z .method public writeToParcel(Lconditions/TestBooleanToShort;)V .locals 0 iget-boolean p1, p0, Lconditions/TestBooleanToShort;->showConsent:Z int-to-short p1, p1 invoke-virtual {p0, p1}, Lconditions/TestBooleanToShort;->write(S)V return-void .end method .method public write(S)V .locals 0 return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestComplexIf.smali ================================================ .class public final Lconditions/TestComplexIf; .super Ljava/lang/Object; # instance fields .field private a:Ljava/lang/String; .field private b:I .field private c:F # direct methods .method public constructor ()V .locals 1 return-void .end method .method public final test()Z .locals 5 iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v1, "GT-P6200" invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 const/4 v1, 0x1 if-nez v0, :cond_b iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v2, "GT-P6210" invoke-virtual {v0, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_b iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v2, "A100" invoke-virtual {v0, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_b iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v2, "A101" invoke-virtual {v0, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_b iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v2, "LIFETAB_S786X" invoke-virtual {v0, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_0 goto/16 :goto_2 :cond_0 iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v2, "VS890 4G" invoke-virtual {v0, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_1 return v1 :cond_1 iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v2, "SM-T810" invoke-virtual {v0, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 const/4 v2, 0x0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T813" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T815" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T815N0" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T815Y" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T820" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T825" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-P585" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-P585N0" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T561" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T567V" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T320" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T321" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T325" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T700" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T705" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T705M" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T705Y" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SC-03G" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "GT-N5100" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "GT-N5105" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "GT-N5110" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "GT-N5120" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SHW-M500W" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T310" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T311" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T315" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T330" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T330NU" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T331" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T335" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T337V" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T710" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T715" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T715N0" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SM-T719" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "GT-P6800" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "SC-01E" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_2 goto/16 :goto_1 :cond_2 iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "LG-V500" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "LG-V930" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_3 goto/16 :goto_1 :cond_3 iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "P01T_1" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "P01MA" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "Nexus 9" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "ASUS_P00I" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_4 goto :goto_1 :cond_4 iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "Lenovo YT3-X90X" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_a iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "Lenovo YT-X703F" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_5 goto :goto_1 :cond_5 iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "PMT3408_4G" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_6 return v2 :cond_6 iget-object v0, p0, Lconditions/TestComplexIf;->a:Ljava/lang/String; const-string v3, "MediaPad T2 10.0 Pro" invoke-virtual {v0, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_7 return v2 :cond_7 iget-object v0, p0, Lconditions/TestComplexIf;->b:I and-int/lit8 v0, v0, 0xf const/4 v3, 0x4 if-ne v0, v3, :cond_8 const/4 v0, 0x1 goto :goto_0 :cond_8 const/4 v0, 0x0 :goto_0 iget v3, p0, Lconditions/TestComplexIf;->c:F const/high16 v4, 0x43200000 # 160.0f cmpl-float v3, v3, v4 if-lez v3, :cond_9 return v1 :cond_9 iget v3, p0, Lconditions/TestComplexIf;->c:F const/4 v4, 0x0 cmpg-float v3, v3, v4 if-gtz v3, :cond_a if-eqz v0, :cond_a return v1 :cond_a :goto_1 return v2 :cond_b :goto_2 return v1 .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestComplexIf2.smali ================================================ .class public final Lconditions/TestComplexIf2; .super Ljava/lang/ClassLoader; # instance fields .field private isSaved:Z .field private project:Ljava/lang/String; .method public test()V .locals 4 .line 415 iget-boolean v0, p0, Lconditions/TestComplexIf2;->isSaved:Z if-eqz v0, :cond_0 .line 416 new-instance v0, Ljava/lang/RuntimeException; const-string v1, "Error" invoke-direct {v0, v1}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V throw v0 .line 418 :cond_0 invoke-static {}, Lorg/apache/tools/ant/util/LoaderUtils;->isContextLoaderAvailable()Z move-result v0 if-eqz v0, :cond_2 .line 419 invoke-static {}, Lorg/apache/tools/ant/util/LoaderUtils;->getContextClassLoader()Ljava/lang/ClassLoader; move-result-object v0 iput-object v0, p0, Lconditions/TestComplexIf2;->savedContextLoader:Ljava/lang/ClassLoader; .line 420 move-object v0, p0 .line 421 .local v0, "loader":Ljava/lang/ClassLoader; iget-object v1, p0, Lconditions/TestComplexIf2;->project:Ljava/lang/String; if-eqz v1, :cond_1 const-string v1, "simple" iget-object v2, p0, Lconditions/TestComplexIf2;->project:Ljava/lang/String; invoke-virtual {v1, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v1 if-eqz v1, :cond_1 .line 423 invoke-virtual {p0}, Ljava/lang/Object;->getClass()Ljava/lang/Class; move-result-object v1 invoke-virtual {v1}, Ljava/lang/Class;->getClassLoader()Ljava/lang/ClassLoader; move-result-object v0 .line 425 :cond_1 invoke-static {v0}, Lorg/apache/tools/ant/util/LoaderUtils;->setContextClassLoader(Ljava/lang/ClassLoader;)V .line 426 const/4 v1, 0x1 iput-boolean v1, p0, Lconditions/TestComplexIf2;->isSaved:Z .line 428 .end local v0 # "loader":Ljava/lang/ClassLoader; :cond_2 return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestComplexIf3.smali ================================================ .class public final Lconditions/TestComplexIf3; .super Ljava/lang/Object; .method public test(Landroid/os/Message;)V .registers 10 .prologue const/16 v7, 0xf const/4 v6, 0x4 const/4 v0, 0x0 const/4 v1, 0x1 const/4 v2, 0x0 .line 3307 const-string/jumbo v3, "Service" new-instance v4, Ljava/lang/StringBuilder; invoke-direct {v4}, Ljava/lang/StringBuilder;->()V const-string/jumbo v5, "handle: " invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v4 iget v5, p1, Landroid/os/Message;->what:I invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; move-result-object v4 invoke-virtual {v4}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v4 invoke-static {v3, v4}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I .line 3308 iget v3, p1, Landroid/os/Message;->what:I sparse-switch v3, :sswitch_data_2d0 .line 3516 :cond_27 :goto_27 return-void .line 3312 :sswitch_28 invoke-virtual {p1}, Landroid/os/Message;->getData()Landroid/os/Bundle; move-result-object v0 .line 3313 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v1, v0}, Lconditions/TestComplexIf3;->-wrap27(Lconditions/TestComplexIf3;Landroid/os/Bundle;)V goto :goto_27 .line 3318 :sswitch_32 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-wrap26(Lconditions/TestComplexIf3;)V goto :goto_27 .line 3323 :sswitch_38 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-get18(Lconditions/TestComplexIf3;)Z move-result v0 if-eqz v0, :cond_45 .line 3324 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0, v2}, Lconditions/TestComplexIf3;->-wrap33(Lconditions/TestComplexIf3;Z)V .line 3326 :cond_45 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-wrap9(Lconditions/TestComplexIf3;)Z .line 3327 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-wrap0(Lconditions/TestComplexIf3;)Z .line 3329 new-instance v0, Landroid/os/Bundle; invoke-direct {v0, v1}, Landroid/os/Bundle;->(I)V .line 3330 const-string/jumbo v1, "flag" const/16 v2, 0xb invoke-virtual {v0, v1, v2}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V .line 3331 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v1, v0}, Lconditions/TestComplexIf3;->-wrap28(Lconditions/TestComplexIf3;Landroid/os/Bundle;)V .line 3333 invoke-static {}, Lconditions/TestComplexIf3;->-get28()Lconditions/TestComplexIf3$OnExitListener; move-result-object v0 if-eqz v0, :cond_27 .line 3334 invoke-static {}, Lconditions/TestComplexIf3;->-get28()Lconditions/TestComplexIf3$OnExitListener; move-result-object v0 invoke-interface {v0}, Lconditions/TestComplexIf3$OnExitListener;->onExit()V goto :goto_27 .line 3340 :sswitch_6f invoke-virtual {p1}, Landroid/os/Message;->getData()Landroid/os/Bundle; move-result-object v0 .line 3341 const-string/jumbo v3, "value" invoke-virtual {v0, v3}, Landroid/os/Bundle;->getInt(Ljava/lang/String;)I move-result v3 .line 3347 if-nez v3, :cond_8e .line 3349 const-string/jumbo v2, "flag" invoke-virtual {v0, v2, v6}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V .line 3351 const-string/jumbo v2, "key" invoke-virtual {v0, v2, v1}, Landroid/os/Bundle;->putBoolean(Ljava/lang/String;Z)V .line 3352 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v1, v0}, Lconditions/TestComplexIf3;->-wrap28(Lconditions/TestComplexIf3;Landroid/os/Bundle;)V goto :goto_27 .line 3357 :cond_8e iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v1}, Lconditions/TestComplexIf3;->-get22(Lconditions/TestComplexIf3;)I move-result v1 const/4 v3, 0x7 if-eq v1, v3, :cond_27 .line 3358 const-string/jumbo v1, "flag" invoke-virtual {v0, v1, v6}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V .line 3360 const-string/jumbo v1, "key" invoke-virtual {v0, v1, v2}, Landroid/os/Bundle;->putBoolean(Ljava/lang/String;Z)V .line 3361 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v1, v0}, Lconditions/TestComplexIf3;->-wrap28(Lconditions/TestComplexIf3;Landroid/os/Bundle;)V goto/16 :goto_27 .line 3368 :sswitch_aa invoke-virtual {p1}, Landroid/os/Message;->getData()Landroid/os/Bundle; move-result-object v0 .line 3369 const-string/jumbo v1, "f" invoke-virtual {v0, v1}, Landroid/os/Bundle;->getFloat(Ljava/lang/String;)F move-result v0 .line 3370 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v1, v0}, Lconditions/TestComplexIf3;->-wrap12(Lconditions/TestComplexIf3;F)Z move-result v1 .line 3372 if-nez v1, :cond_c7 .line 3373 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-get7(Lconditions/TestComplexIf3;)I move-result v0 invoke-static {v0}, Ltest/Utils;->computeFrequency(I)F move-result v0 .line 3375 :cond_c7 new-instance v2, Landroid/os/Bundle; const/4 v3, 0x3 invoke-direct {v2, v3}, Landroid/os/Bundle;->(I)V .line 3376 const-string/jumbo v3, "flag" invoke-virtual {v2, v3, v7}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V .line 3378 const-string/jumbo v3, "key" invoke-virtual {v2, v3, v1}, Landroid/os/Bundle;->putBoolean(Ljava/lang/String;Z)V .line 3379 const-string/jumbo v1, "key" invoke-virtual {v2, v1, v0}, Landroid/os/Bundle;->putFloat(Ljava/lang/String;F)V .line 3380 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0, v2}, Lconditions/TestComplexIf3;->-wrap28(Lconditions/TestComplexIf3;Landroid/os/Bundle;)V goto/16 :goto_27 .line 3385 :sswitch_e6 invoke-virtual {p1}, Landroid/os/Message;->getData()Landroid/os/Bundle; move-result-object v0 .line 3386 iget-object v3, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v3, v1}, Lconditions/TestComplexIf3;->-set5(Lconditions/TestComplexIf3;Z)Z .line 3387 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; const-string/jumbo v3, "f" invoke-virtual {v0, v3}, Landroid/os/Bundle;->getFloat(Ljava/lang/String;)F move-result v3 .line 3388 const-string/jumbo v4, "o" invoke-virtual {v0, v4}, Landroid/os/Bundle;->getBoolean(Ljava/lang/String;)Z move-result v0 .line 3387 invoke-static {v1, v3, v0}, Lconditions/TestComplexIf3;->-wrap13(Lconditions/TestComplexIf3;FZ)F move-result v0 .line 3390 invoke-static {v0}, Ltest/Utils;->computeStation(F)I move-result v1 .line 3391 invoke-static {v1}, Ltest/Utils;->isValidStation(I)Z move-result v1 if-eqz v1, :cond_2cd .line 3392 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v1, v0}, Lconditions/TestComplexIf3;->-wrap12(Lconditions/TestComplexIf3;F)Z move-result v1 .line 3395 :goto_113 if-nez v1, :cond_11f .line 3396 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-get7(Lconditions/TestComplexIf3;)I move-result v0 invoke-static {v0}, Ltest/Utils;->computeFrequency(I)F move-result v0 .line 3398 :cond_11f new-instance v3, Landroid/os/Bundle; const/4 v4, 0x2 invoke-direct {v3, v4}, Landroid/os/Bundle;->(I)V .line 3399 const-string/jumbo v4, "flag" invoke-virtual {v3, v4, v7}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V .line 3401 const-string/jumbo v4, "key_is_tune" invoke-virtual {v3, v4, v1}, Landroid/os/Bundle;->putBoolean(Ljava/lang/String;Z)V .line 3402 const-string/jumbo v1, "key_tune_to_station" invoke-virtual {v3, v1, v0}, Landroid/os/Bundle;->putFloat(Ljava/lang/String;F)V .line 3403 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0, v3}, Lconditions/TestComplexIf3;->-wrap28(Lconditions/TestComplexIf3;Landroid/os/Bundle;)V .line 3404 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0, v2}, Lconditions/TestComplexIf3;->-set5(Lconditions/TestComplexIf3;Z)Z goto/16 :goto_27 .line 3414 :sswitch_143 iget-object v3, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v3, v1}, Lconditions/TestComplexIf3;->-set4(Lconditions/TestComplexIf3;Z)Z .line 3418 iget-object v3, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; iget-object v4, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v4}, Lconditions/TestComplexIf3;->-get6(Lconditions/TestComplexIf3;)Landroid/content/Context; move-result-object v4 invoke-static {v4}, Ltest/Station;->getCurrentStation(Landroid/content/Context;)I move-result v4 invoke-static {v3, v4}, Lconditions/TestComplexIf3;->-set0(Lconditions/TestComplexIf3;I)I .line 3419 iget-object v3, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v3}, Lconditions/TestComplexIf3;->-get19(Lconditions/TestComplexIf3;)I move-result v3 sget v4, Lconditions/TestComplexIf3;->POWER_UP:I if-eq v3, v4, :cond_185 .line 3420 iget-object v3, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; iget-object v4, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v4}, Lconditions/TestComplexIf3;->-get7(Lconditions/TestComplexIf3;)I move-result v4 invoke-static {v4}, Ltest/Utils;->computeFrequency(I)F move-result v4 invoke-static {v3, v4}, Lconditions/TestComplexIf3;->-wrap10(Lconditions/TestComplexIf3;F)Z move-result v3 if-eqz v3, :cond_21f .line 3421 iget-object v3, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; iget-object v4, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v4}, Lconditions/TestComplexIf3;->-get7(Lconditions/TestComplexIf3;)I move-result v4 invoke-static {v4}, Ltest/Utils;->computeFrequency(I)F move-result v4 invoke-static {v3, v4}, Lconditions/TestComplexIf3;->-wrap8(Lconditions/TestComplexIf3;F)Z move-result v3 .line 3419 if-eqz v3, :cond_2c9 .line 3422 :cond_185 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-get27(Lconditions/TestComplexIf3;)Landroid/os/PowerManager$WakeLock; move-result-object v0 if-eqz v0, :cond_222 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-get27(Lconditions/TestComplexIf3;)Landroid/os/PowerManager$WakeLock; move-result-object v0 invoke-virtual {v0}, Landroid/os/PowerManager$WakeLock;->isHeld()Z move-result v0 xor-int/lit8 v0, v0, 0x1 if-eqz v0, :cond_2c6 .line 3424 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-get27(Lconditions/TestComplexIf3;)Landroid/os/PowerManager$WakeLock; move-result-object v0 invoke-virtual {v0}, Landroid/os/PowerManager$WakeLock;->acquire()V move v0, v1 .line 3426 :goto_1a5 const-string/jumbo v3, "Service" const-string/jumbo v4, "handle: start" invoke-static {v3, v4}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I .line 3427 iget-object v3, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v3}, Lconditions/TestComplexIf3;->-wrap14(Lconditions/TestComplexIf3;)[I move-result-object v3 .line 3429 :goto_1b4 const-string/jumbo v4, "Service" const-string/jumbo v5, "handle: end" invoke-static {v4, v5}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I .line 3431 if-eqz v3, :cond_224 aget v4, v3, v2 const/16 v5, -0x64 if-ne v4, v5, :cond_224 .line 3434 const/4 v3, -0x1 .line 3433 filled-new-array {v3, v2}, [I move-result-object v3 move-object v4, v3 move v3, v2 .line 3448 :goto_1cc iget-object v5, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v5}, Lconditions/TestComplexIf3;->-get9(Lconditions/TestComplexIf3;)Z move-result v5 if-eqz v5, :cond_1d9 .line 3449 iget-object v5, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-virtual {v5, v2}, Lconditions/TestComplexIf3;->setMute(Z)I .line 3452 :cond_1d9 if-eqz v0, :cond_1f8 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-get27(Lconditions/TestComplexIf3;)Landroid/os/PowerManager$WakeLock; move-result-object v0 if-eqz v0, :cond_1f8 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-get27(Lconditions/TestComplexIf3;)Landroid/os/PowerManager$WakeLock; move-result-object v0 invoke-virtual {v0}, Landroid/os/PowerManager$WakeLock;->isHeld()Z move-result v0 if-eqz v0, :cond_1f8 .line 3453 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-get27(Lconditions/TestComplexIf3;)Landroid/os/PowerManager$WakeLock; move-result-object v0 invoke-virtual {v0}, Landroid/os/PowerManager$WakeLock;->release()V .line 3456 :cond_1f8 new-instance v0, Landroid/os/Bundle; invoke-direct {v0, v6}, Landroid/os/Bundle;->(I)V .line 3457 const-string/jumbo v5, "callback_flag" .line 3458 const/16 v6, 0xd .line 3457 invoke-virtual {v0, v5, v6}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V .line 3460 const-string/jumbo v5, "key_station_num" aget v1, v4, v1 invoke-virtual {v0, v5, v1}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V .line 3461 const-string/jumbo v1, "key_is_scan" invoke-virtual {v0, v1, v3}, Landroid/os/Bundle;->putBoolean(Ljava/lang/String;Z)V .line 3463 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v1, v2}, Lconditions/TestComplexIf3;->-set4(Lconditions/TestComplexIf3;Z)Z .line 3465 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v1, v0}, Lconditions/TestComplexIf3;->-wrap29(Lconditions/TestComplexIf3;Landroid/os/Bundle;)V goto/16 :goto_27 :cond_21f move-object v3, v0 move v0, v2 .line 3421 goto :goto_1b4 :cond_222 move v0, v2 .line 3422 goto :goto_1a5 .line 3437 :cond_224 iget-object v4, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v4, v3}, Lconditions/TestComplexIf3;->-wrap15(Lconditions/TestComplexIf3;[I)[I move-result-object v3 .line 3438 aget v4, v3, v2 .line 3439 iget-object v4, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; iget-object v5, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v5}, Lconditions/TestComplexIf3;->-get7(Lconditions/TestComplexIf3;)I move-result v5 invoke-static {v5}, Ltest/Utils;->computeFrequency(I)F move-result v5 invoke-static {v4, v5}, Lconditions/TestComplexIf3;->-wrap12(Lconditions/TestComplexIf3;F)Z move-result v4 if-eqz v4, :cond_2c2 .line 3440 iget-object v4, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; iget-object v5, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v5}, Lconditions/TestComplexIf3;->-get7(Lconditions/TestComplexIf3;)I move-result v5 invoke-virtual {v4, v5}, Lconditions/TestComplexIf3;->initService(I)V move-object v4, v3 move v3, v1 goto :goto_1cc .line 3470 :sswitch_24c invoke-virtual {p1}, Landroid/os/Message;->getData()Landroid/os/Bundle; move-result-object v0 .line 3471 const-string/jumbo v1, "key_audiofocus_changed" invoke-virtual {v0, v1}, Landroid/os/Bundle;->getInt(Ljava/lang/String;)I move-result v0 .line 3472 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v1, v0}, Lconditions/TestComplexIf3;->-wrap43(Lconditions/TestComplexIf3;I)V goto/16 :goto_27 .line 3476 :sswitch_25e invoke-virtual {p1}, Landroid/os/Message;->getData()Landroid/os/Bundle; move-result-object v0 .line 3477 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; const-string/jumbo v2, "option" invoke-virtual {v0, v2}, Landroid/os/Bundle;->getBoolean(Ljava/lang/String;)Z move-result v0 invoke-static {v1, v0}, Lconditions/TestComplexIf3;->-wrap19(Lconditions/TestComplexIf3;Z)I goto/16 :goto_27 .line 3481 :sswitch_270 invoke-virtual {p1}, Landroid/os/Message;->getData()Landroid/os/Bundle; move-result-object v0 .line 3482 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; const-string/jumbo v2, "option" invoke-virtual {v0, v2}, Landroid/os/Bundle;->getBoolean(Ljava/lang/String;)Z move-result v0 invoke-virtual {v1, v0}, Lconditions/TestComplexIf3;->setMute(Z)I goto/16 :goto_27 .line 3486 :sswitch_282 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-wrap16(Lconditions/TestComplexIf3;)I goto/16 :goto_27 .line 3491 :sswitch_289 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-wrap38(Lconditions/TestComplexIf3;)V goto/16 :goto_27 .line 3495 :sswitch_290 iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-wrap11(Lconditions/TestComplexIf3;)Z goto/16 :goto_27 .line 3499 :sswitch_297 invoke-virtual {p1}, Landroid/os/Message;->getData()Landroid/os/Bundle; move-result-object v0 .line 3500 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; const-string/jumbo v2, "option" invoke-virtual {v0, v2}, Landroid/os/Bundle;->getBoolean(Ljava/lang/String;)Z move-result v0 invoke-static {v1, v0}, Lconditions/TestComplexIf3;->-wrap36(Lconditions/TestComplexIf3;Z)V goto/16 :goto_27 .line 3504 :sswitch_2a9 invoke-virtual {p1}, Landroid/os/Message;->getData()Landroid/os/Bundle; move-result-object v0 .line 3505 iget-object v1, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; const-string/jumbo v2, "name" invoke-virtual {v0, v2}, Landroid/os/Bundle;->getString(Ljava/lang/String;)Ljava/lang/String; move-result-object v0 invoke-static {v1, v0}, Lconditions/TestComplexIf3;->-wrap32(Lconditions/TestComplexIf3;Ljava/lang/String;)V goto/16 :goto_27 .line 3510 :sswitch_2bb iget-object v0, p0, Lconditions/TestComplexIf3$Handler;->this$0:Lconditions/TestComplexIf3; invoke-static {v0}, Lconditions/TestComplexIf3;->-wrap37(Lconditions/TestComplexIf3;)V goto/16 :goto_27 :cond_2c2 move-object v4, v3 move v3, v1 goto/16 :goto_1cc :cond_2c6 move v0, v2 goto/16 :goto_1a5 :cond_2c9 move-object v3, v0 move v0, v2 goto/16 :goto_1b4 :cond_2cd move v1, v2 goto/16 :goto_113 .line 3308 :sswitch_data_2d0 .sparse-switch 0x4 -> :sswitch_6f 0x5 -> :sswitch_25e 0x7 -> :sswitch_270 0x9 -> :sswitch_28 0xa -> :sswitch_32 0xb -> :sswitch_38 0xd -> :sswitch_143 0xf -> :sswitch_aa 0x10 -> :sswitch_e6 0x12 -> :sswitch_282 0x15 -> :sswitch_297 0x16 -> :sswitch_289 0x17 -> :sswitch_290 0x1a -> :sswitch_2a9 0x1e -> :sswitch_24c 0x66 -> :sswitch_2bb .end sparse-switch .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestComplexIf4.smali ================================================ .class public final Lconditions/TestComplexIf4; .super Ljava/lang/Object; # virtual methods .method public final test()V .registers 14 const/4 v1, 0x0 const/16 v6, 0x0 :loop_0 if-ge v1, v1, :cond_0 goto :loop_0 :cond_0 if-lt v1, v6, :cond_1 goto/16 :goto_0 :cond_1 cmp-long v2, v6, v6 if-nez v2, :cond_2 goto/16 :near_end :cond_2 if-le v2, v1, :cond_3 if-lez v1, :cond_4 goto :near_end :cond_4 const/4 v5, 0x0 if-ge v5, v1, :cond_5 :cond_5 cmp-long v5, v6, v6 if-ltz v5, :cond_3 goto :near_end :cond_3 cmp-long v5, v6, v6 :goto_0 if-eqz v1, :near_end const/4 v1, 0x0 goto :cond_6 :near_end const/4 v1, 0x0 :cond_6 if-ne v1, v1, :cond_magic :cond_magic const/4 v1, 0x0 .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestConditions18.smali ================================================ .class public final Lconditions/TestConditions18; .super Ljava/lang/Object; .field private map:Ljava/util/Map; .method public test(Ljava/lang/Object;)Z .locals 1 if-eq p0, p1, :cond_1 instance-of v0, p1, Lconditions/TestConditions18; if-eqz v0, :cond_0 check-cast p1, Lconditions/TestConditions18; iget-object v0, p0, Lconditions/TestConditions18;->map:Ljava/util/Map; iget-object p1, p1, Lconditions/TestConditions18;->map:Ljava/util/Map; invoke-static {v0, p1}, Lconditions/TestConditions18;->st(Ljava/lang/Object;Ljava/lang/Object;)Z move-result p1 if-eqz p1, :cond_0 goto :goto_0 :cond_0 const/4 p1, 0x0 return p1 :cond_1 :goto_0 const/4 p1, 0x1 return p1 .end method .method private static st(Ljava/lang/Object;Ljava/lang/Object;)Z .locals 1 const/4 v0, 0x0 return v0 .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestConditions21.smali ================================================ .class public final Lconditions/TestConditions21; .super Ljava/lang/Object; .method public check(Ljava/lang/Object;)Z .locals 2 if-eq p0, p1, :ret_true instance-of v0, p1, Ljava/util/List; if-eqz v0, :ret_false check-cast p1, Ljava/util/List; invoke-interface {p1}, Ljava/util/List;->isEmpty()Z move-result v0 if-nez v0, :ret_false invoke-interface {p1, p0}, Ljava/util/List;->contains(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :ret_false goto :ret_true :ret_false const/4 p1, 0x0 return p1 :ret_true const/4 p1, 0x1 return p1 .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestIfAndSwitch.smali ================================================ ###### Class conditions.TestIfAndSwitch (conditions.TestIfAndSwitch) .class Lconditions/TestIfAndSwitch; .super Ljava/lang/Object; .source "TestIfAndSwitch.java" # static fields .field private static final ACTION_MOVE:I = 0x2 .field private static final C:I .field private static i:I .field private static final rd:Ljava/util/Random; # direct methods .method static constructor ()V .registers 1 .line 8 new-instance v0, Ljava/util/Random; invoke-direct {v0}, Ljava/util/Random;->()V sput-object v0, Lconditions/TestIfAndSwitch;->rd:Ljava/util/Random; return-void .end method .method constructor ()V .registers 1 .line 3 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public static ifAndSwitch()Z .registers 4 .line 12 nop .line 13 sget-object v0, Lconditions/TestIfAndSwitch;->rd:Ljava/util/Random; invoke-virtual {v0}, Ljava/util/Random;->nextInt()I move-result v0 const/4 v1, 0x2 const/4 v2, 0x1 const/4 v3, 0x0 if-ne v0, v1, :cond_14 .line 14 sget v0, Lconditions/TestIfAndSwitch;->i:I packed-switch v0, :pswitch_data_1a goto :goto_14 .line 16 :pswitch_12 const/4 v0, 0x1 goto :goto_15 .line 20 :cond_14 :goto_14 const/4 v0, 0x0 :goto_15 if-eqz v0, :cond_18 .line 21 return v2 .line 23 :cond_18 return v3 nop :pswitch_data_1a .packed-switch 0x0 :pswitch_12 .end packed-switch .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestIfCodeStyle.smali ================================================ .class public Lconditions/TestIfCodeStyle; .super Ljava/lang/Object; .implements Landroid/os/Parcelable; .field public isActive:Z .field public isFactory:Z .field public moduleName:Ljava/lang/String; .field public modulePath:Ljava/lang/String; .field public preinstalledModulePath:Ljava/lang/String; .field public versionCode:J .field public versionName:Ljava/lang/String; .method public final readFromParcel(Landroid/os/Parcel;)V .registers 9 .param p1, "_aidl_parcel" # Landroid/os/Parcel; .line 44 invoke-virtual {p1}, Landroid/os/Parcel;->dataPosition()I move-result v0 .line 45 .local v0, "_aidl_start_pos":I invoke-virtual {p1}, Landroid/os/Parcel;->readInt()I move-result v1 .line 47 .local v1, "_aidl_parcelable_size":I const-string v2, "Overflow in the size of parcelable" const v3, 0x7fffffff if-gez v1, :cond_1e .line 63 sub-int/2addr v3, v1 if-gt v0, v3, :cond_18 .line 66 add-int v2, v0, v1 invoke-virtual {p1, v2}, Landroid/os/Parcel;->setDataPosition(I)V .line 47 return-void .line 64 :cond_18 new-instance v3, Ljava/lang/RuntimeException; invoke-direct {v3, v2}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V throw v3 .line 48 :cond_1e :try_start_1e invoke-virtual {p1}, Landroid/os/Parcel;->dataPosition()I move-result v4 :try_end_22 .catchall {:try_start_1e .. :try_end_22} :catchall_fd sub-int/2addr v4, v0 if-lt v4, v1, :cond_34 .line 63 sub-int/2addr v3, v1 if-gt v0, v3, :cond_2e .line 66 add-int v2, v0, v1 invoke-virtual {p1, v2}, Landroid/os/Parcel;->setDataPosition(I)V .line 48 return-void .line 64 :cond_2e new-instance v3, Ljava/lang/RuntimeException; invoke-direct {v3, v2}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V throw v3 .line 49 :cond_34 :try_start_34 invoke-virtual {p1}, Landroid/os/Parcel;->readString()Ljava/lang/String; move-result-object v4 iput-object v4, p0, Lconditions/TestIfCodeStyle;->moduleName:Ljava/lang/String; .line 50 invoke-virtual {p1}, Landroid/os/Parcel;->dataPosition()I move-result v4 :try_end_3e .catchall {:try_start_34 .. :try_end_3e} :catchall_fd sub-int/2addr v4, v0 if-lt v4, v1, :cond_50 .line 63 sub-int/2addr v3, v1 if-gt v0, v3, :cond_4a .line 66 add-int v2, v0, v1 invoke-virtual {p1, v2}, Landroid/os/Parcel;->setDataPosition(I)V .line 50 return-void .line 64 :cond_4a new-instance v3, Ljava/lang/RuntimeException; invoke-direct {v3, v2}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V throw v3 .line 51 :cond_50 :try_start_50 invoke-virtual {p1}, Landroid/os/Parcel;->readString()Ljava/lang/String; move-result-object v4 iput-object v4, p0, Lconditions/TestIfCodeStyle;->modulePath:Ljava/lang/String; .line 52 invoke-virtual {p1}, Landroid/os/Parcel;->dataPosition()I move-result v4 :try_end_5a .catchall {:try_start_50 .. :try_end_5a} :catchall_fd sub-int/2addr v4, v0 if-lt v4, v1, :cond_6c .line 63 sub-int/2addr v3, v1 if-gt v0, v3, :cond_66 .line 66 add-int v2, v0, v1 invoke-virtual {p1, v2}, Landroid/os/Parcel;->setDataPosition(I)V .line 52 return-void .line 64 :cond_66 new-instance v3, Ljava/lang/RuntimeException; invoke-direct {v3, v2}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V throw v3 .line 53 :cond_6c :try_start_6c invoke-virtual {p1}, Landroid/os/Parcel;->readString()Ljava/lang/String; move-result-object v4 iput-object v4, p0, Lconditions/TestIfCodeStyle;->preinstalledModulePath:Ljava/lang/String; .line 54 invoke-virtual {p1}, Landroid/os/Parcel;->dataPosition()I move-result v4 :try_end_76 .catchall {:try_start_6c .. :try_end_76} :catchall_fd sub-int/2addr v4, v0 if-lt v4, v1, :cond_88 .line 63 sub-int/2addr v3, v1 if-gt v0, v3, :cond_82 .line 66 add-int v2, v0, v1 invoke-virtual {p1, v2}, Landroid/os/Parcel;->setDataPosition(I)V .line 54 return-void .line 64 :cond_82 new-instance v3, Ljava/lang/RuntimeException; invoke-direct {v3, v2}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V throw v3 .line 55 :cond_88 :try_start_88 invoke-virtual {p1}, Landroid/os/Parcel;->readLong()J move-result-wide v4 iput-wide v4, p0, Lconditions/TestIfCodeStyle;->versionCode:J .line 56 invoke-virtual {p1}, Landroid/os/Parcel;->dataPosition()I move-result v4 :try_end_92 .catchall {:try_start_88 .. :try_end_92} :catchall_fd sub-int/2addr v4, v0 if-lt v4, v1, :cond_a4 .line 63 sub-int/2addr v3, v1 if-gt v0, v3, :cond_9e .line 66 add-int v2, v0, v1 invoke-virtual {p1, v2}, Landroid/os/Parcel;->setDataPosition(I)V .line 56 return-void .line 64 :cond_9e new-instance v3, Ljava/lang/RuntimeException; invoke-direct {v3, v2}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V throw v3 .line 57 :cond_a4 :try_start_a4 invoke-virtual {p1}, Landroid/os/Parcel;->readString()Ljava/lang/String; move-result-object v4 iput-object v4, p0, Lconditions/TestIfCodeStyle;->versionName:Ljava/lang/String; .line 58 invoke-virtual {p1}, Landroid/os/Parcel;->dataPosition()I move-result v4 :try_end_ae .catchall {:try_start_a4 .. :try_end_ae} :catchall_fd sub-int/2addr v4, v0 if-lt v4, v1, :cond_c0 .line 63 sub-int/2addr v3, v1 if-gt v0, v3, :cond_ba .line 66 add-int v2, v0, v1 invoke-virtual {p1, v2}, Landroid/os/Parcel;->setDataPosition(I)V .line 58 return-void .line 64 :cond_ba new-instance v3, Ljava/lang/RuntimeException; invoke-direct {v3, v2}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V throw v3 .line 59 :cond_c0 :try_start_c0 invoke-virtual {p1}, Landroid/os/Parcel;->readInt()I move-result v4 const/4 v5, 0x1 const/4 v6, 0x0 if-eqz v4, :cond_ca move v4, v5 goto :goto_cb :cond_ca move v4, v6 :goto_cb iput-boolean v4, p0, Lconditions/TestIfCodeStyle;->isFactory:Z .line 60 invoke-virtual {p1}, Landroid/os/Parcel;->dataPosition()I move-result v4 :try_end_d1 .catchall {:try_start_c0 .. :try_end_d1} :catchall_fd sub-int/2addr v4, v0 if-lt v4, v1, :cond_e3 .line 63 sub-int/2addr v3, v1 if-gt v0, v3, :cond_dd .line 66 add-int v2, v0, v1 invoke-virtual {p1, v2}, Landroid/os/Parcel;->setDataPosition(I)V .line 60 return-void .line 64 :cond_dd new-instance v3, Ljava/lang/RuntimeException; invoke-direct {v3, v2}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V throw v3 .line 61 :cond_e3 :try_start_e3 invoke-virtual {p1}, Landroid/os/Parcel;->readInt()I move-result v4 if-eqz v4, :cond_ea goto :goto_eb :cond_ea move v5, v6 :goto_eb iput-boolean v5, p0, Lconditions/TestIfCodeStyle;->isActive:Z :try_end_ed .catchall {:try_start_e3 .. :try_end_ed} :catchall_fd .line 63 sub-int/2addr v3, v1 if-gt v0, v3, :cond_f7 .line 66 add-int v2, v0, v1 invoke-virtual {p1, v2}, Landroid/os/Parcel;->setDataPosition(I)V .line 67 nop .line 68 return-void .line 64 :cond_f7 new-instance v3, Ljava/lang/RuntimeException; invoke-direct {v3, v2}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V throw v3 .line 63 :catchall_fd move-exception v4 sub-int/2addr v3, v1 if-le v0, v3, :cond_107 .line 64 new-instance v3, Ljava/lang/RuntimeException; invoke-direct {v3, v2}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V throw v3 .line 66 :cond_107 add-int v2, v0, v1 invoke-virtual {p1, v2}, Landroid/os/Parcel;->setDataPosition(I)V .line 67 throw v4 .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestIfCodeStyle2.smali ================================================ .class public Lconditions/TestIfCodeStyle2; .super Ljava/lang/Object; .method public execute(Ltest/TestSender;Ljava/lang/String;[Ljava/lang/String;)Z .registers 21 .param p1, "sender" # Ltest/TestSender; .param p2, "currentAlias" # Ljava/lang/String; .param p3, "args" # [Ljava/lang/String; .prologue .line 33 invoke-virtual/range {p0 .. p1}, Lconditions/TestIfCodeStyle2;->testPermission(Ltest/TestSender;)Z move-result v4 if-nez v4, :cond_8 const/4 v4, 0x1 .line 165 :goto_7 return v4 .line 35 :cond_8 move-object/from16 v0, p3 array-length v4, v0 const/4 v5, 0x2 if-ge v4, v5, :cond_32 .line 36 new-instance v4, Ljava/lang/StringBuilder; invoke-direct {v4}, Ljava/lang/StringBuilder;->()V sget-object v5, Ltest/ChatColor;->RED:Ltest/ChatColor; invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder; move-result-object v4 const-string v5, "Usage: " invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v4 move-object/from16 v0, p0 iget-object v5, v0, Lconditions/TestIfCodeStyle2;->usageMessage:Ljava/lang/String; invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v4 invoke-virtual {v4}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 37 const/4 v4, 0x0 goto :goto_7 .line 40 :cond_32 const/4 v4, 0x0 aget-object v4, p3, v4 const-string v5, "give" invoke-virtual {v4, v5}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z move-result v4 if-nez v4, :cond_61 .line 41 new-instance v4, Ljava/lang/StringBuilder; invoke-direct {v4}, Ljava/lang/StringBuilder;->()V sget-object v5, Ltest/ChatColor;->RED:Ltest/ChatColor; invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder; move-result-object v4 const-string v5, "Usage: " invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v4 move-object/from16 v0, p0 iget-object v5, v0, Lconditions/TestIfCodeStyle2;->usageMessage:Ljava/lang/String; invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v4 invoke-virtual {v4}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 42 const/4 v4, 0x0 goto :goto_7 .line 45 :cond_61 const/4 v4, 0x1 aget-object v16, p3, v4 .line 46 .local v16, "statisticString":Ljava/lang/String; const/4 v2, 0x0 .line 48 .local v2, "player":Ltest/entity/Player; move-object/from16 v0, p3 array-length v4, v0 const/4 v5, 0x2 if-le v4, v5, :cond_7d .line 49 const/4 v4, 0x1 aget-object v4, p3, v4 invoke-static {v4}, Ltest/Bukkit;->getPlayer(Ljava/lang/String;)Ltest/entity/Player; move-result-object v2 .line 54 :cond_72 :goto_72 if-nez v2, :cond_88 .line 55 const-string v4, "You must specify which player you wish to perform this action on." move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 56 const/4 v4, 0x1 goto :goto_7 .line 50 :cond_7d move-object/from16 v0, p1 instance-of v4, v0, Ltest/entity/Player; if-eqz v4, :cond_72 move-object/from16 v2, p1 .line 51 check-cast v2, Ltest/entity/Player; goto :goto_72 .line 59 :cond_88 const-string v4, "*" move-object/from16 v0, v16 invoke-virtual {v0, v4}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v4 if-eqz v4, :cond_d7 .line 60 invoke-static {}, Ltest/Achievement;->values()[Ltest/Achievement; move-result-object v5 array-length v7, v5 const/4 v4, 0x0 :goto_98 if-ge v4, v7, :cond_bf aget-object v13, v5, v4 .line 61 .local v13, "achievement":Ltest/Achievement; invoke-interface {v2, v13}, Ltest/entity/Player;->hasAchievement(Ltest/Achievement;)Z move-result v8 if-eqz v8, :cond_a5 .line 60 :cond_a2 :goto_a2 add-int/lit8 v4, v4, 0x1 goto :goto_98 .line 64 :cond_a5 new-instance v1, Ltest/event/player/PlayerAchievementAwardedEvent; invoke-direct {v1, v2, v13}, Ltest/event/player/PlayerAchievementAwardedEvent;->(Ltest/entity/Player;Ltest/Achievement;)V .line 65 .local v1, "event":Ltest/event/player/PlayerAchievementAwardedEvent; invoke-static {}, Ltest/Bukkit;->getServer()Ltest/Server; move-result-object v8 invoke-interface {v8}, Ltest/Server;->getPluginManager()Ltest/plugin/PluginManager; move-result-object v8 invoke-interface {v8, v1}, Ltest/plugin/PluginManager;->callEvent(Ltest/event/Event;)V .line 66 invoke-virtual {v1}, Ltest/event/player/PlayerAchievementAwardedEvent;->isCancelled()Z move-result v8 if-nez v8, :cond_a2 .line 67 invoke-interface {v2, v13}, Ltest/entity/Player;->awardAchievement(Ltest/Achievement;)V goto :goto_a2 .line 70 .end local v1 # "event":Ltest/event/player/PlayerAchievementAwardedEvent; .end local v13 # "achievement":Ltest/Achievement; :cond_bf const-string v4, "Successfully given all achievements to %s" const/4 v5, 0x1 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 invoke-interface {v2}, Ltest/entity/Player;->getName()Ljava/lang/String; move-result-object v8 aput-object v8, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-static {v0, v4}, Ltest/command/Command;->broadcastCommandMessage(Ltest/TestSender;Ljava/lang/String;)V .line 71 const/4 v4, 0x1 goto/16 :goto_7 .line 74 :cond_d7 invoke-static {}, Ltest/Bukkit;->getUnsafe()Ltest/UnsafeValues; move-result-object v4 move-object/from16 v0, v16 invoke-interface {v4, v0}, Ltest/UnsafeValues;->getAchievementFromInternalName(Ljava/lang/String;)Ltest/Achievement; move-result-object v13 .line 75 .restart local v13 # "achievement":Ltest/Achievement; invoke-static {}, Ltest/Bukkit;->getUnsafe()Ltest/UnsafeValues; move-result-object v4 move-object/from16 v0, v16 invoke-interface {v4, v0}, Ltest/UnsafeValues;->getStatisticFromInternalName(Ljava/lang/String;)Ltest/Statistic; move-result-object v3 .line 77 .local v3, "statistic":Ltest/Statistic; if-eqz v13, :cond_15d .line 78 invoke-interface {v2, v13}, Ltest/entity/Player;->hasAchievement(Ltest/Achievement;)Z move-result v4 if-eqz v4, :cond_10e .line 79 const-string v4, "%s already has achievement %s" const/4 v5, 0x2 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 invoke-interface {v2}, Ltest/entity/Player;->getName()Ljava/lang/String; move-result-object v8 aput-object v8, v5, v7 const/4 v7, 0x1 aput-object v16, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 80 const/4 v4, 0x1 goto/16 :goto_7 .line 83 :cond_10e new-instance v1, Ltest/event/player/PlayerAchievementAwardedEvent; invoke-direct {v1, v2, v13}, Ltest/event/player/PlayerAchievementAwardedEvent;->(Ltest/entity/Player;Ltest/Achievement;)V .line 84 .restart local v1 # "event":Ltest/event/player/PlayerAchievementAwardedEvent; invoke-static {}, Ltest/Bukkit;->getServer()Ltest/Server; move-result-object v4 invoke-interface {v4}, Ltest/Server;->getPluginManager()Ltest/plugin/PluginManager; move-result-object v4 invoke-interface {v4, v1}, Ltest/plugin/PluginManager;->callEvent(Ltest/event/Event;)V .line 85 invoke-virtual {v1}, Ltest/event/player/PlayerAchievementAwardedEvent;->isCancelled()Z move-result v4 if-eqz v4, :cond_13f .line 86 const-string v4, "Unable to award %s the achievement %s" const/4 v5, 0x2 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 invoke-interface {v2}, Ltest/entity/Player;->getName()Ljava/lang/String; move-result-object v8 aput-object v8, v5, v7 const/4 v7, 0x1 aput-object v16, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 87 const/4 v4, 0x1 goto/16 :goto_7 .line 89 :cond_13f invoke-interface {v2, v13}, Ltest/entity/Player;->awardAchievement(Ltest/Achievement;)V .line 91 const-string v4, "Successfully given %s the stat %s" const/4 v5, 0x2 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 invoke-interface {v2}, Ltest/entity/Player;->getName()Ljava/lang/String; move-result-object v8 aput-object v8, v5, v7 const/4 v7, 0x1 aput-object v16, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-static {v0, v4}, Ltest/command/Command;->broadcastCommandMessage(Ltest/TestSender;Ljava/lang/String;)V .line 92 const/4 v4, 0x1 goto/16 :goto_7 .line 95 .end local v1 # "event":Ltest/event/player/PlayerAchievementAwardedEvent; :cond_15d if-nez v3, :cond_173 .line 96 const-string v4, "Unknown achievement or statistic \'%s\'" const/4 v5, 0x1 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 aput-object v16, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 97 const/4 v4, 0x1 goto/16 :goto_7 .line 100 :cond_173 invoke-virtual {v3}, Ltest/Statistic;->getType()Ltest/Statistic$Type; move-result-object v4 sget-object v5, Ltest/Statistic$Type;->UNTYPED:Ltest/Statistic$Type; if-ne v4, v5, :cond_1d4 .line 101 new-instance v1, Ltest/event/player/PlayerStatisticIncrementEvent; invoke-interface {v2, v3}, Ltest/entity/Player;->getStatistic(Ltest/Statistic;)I move-result v4 invoke-interface {v2, v3}, Ltest/entity/Player;->getStatistic(Ltest/Statistic;)I move-result v5 add-int/lit8 v5, v5, 0x1 invoke-direct {v1, v2, v3, v4, v5}, Ltest/event/player/PlayerStatisticIncrementEvent;->(Ltest/entity/Player;Ltest/Statistic;II)V .line 102 .local v1, "event":Ltest/event/player/PlayerStatisticIncrementEvent; invoke-static {}, Ltest/Bukkit;->getServer()Ltest/Server; move-result-object v4 invoke-interface {v4}, Ltest/Server;->getPluginManager()Ltest/plugin/PluginManager; move-result-object v4 invoke-interface {v4, v1}, Ltest/plugin/PluginManager;->callEvent(Ltest/event/Event;)V .line 103 invoke-virtual {v1}, Ltest/event/player/PlayerStatisticIncrementEvent;->isCancelled()Z move-result v4 if-eqz v4, :cond_1b6 .line 104 const-string v4, "Unable to increment %s for %s" const/4 v5, 0x2 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 aput-object v16, v5, v7 const/4 v7, 0x1 invoke-interface {v2}, Ltest/entity/Player;->getName()Ljava/lang/String; move-result-object v8 aput-object v8, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 105 const/4 v4, 0x1 goto/16 :goto_7 .line 107 :cond_1b6 invoke-interface {v2, v3}, Ltest/entity/Player;->incrementStatistic(Ltest/Statistic;)V .line 108 const-string v4, "Successfully given %s the stat %s" const/4 v5, 0x2 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 invoke-interface {v2}, Ltest/entity/Player;->getName()Ljava/lang/String; move-result-object v8 aput-object v8, v5, v7 const/4 v7, 0x1 aput-object v16, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-static {v0, v4}, Ltest/command/Command;->broadcastCommandMessage(Ltest/TestSender;Ljava/lang/String;)V .line 109 const/4 v4, 0x1 goto/16 :goto_7 .line 112 .end local v1 # "event":Ltest/event/player/PlayerStatisticIncrementEvent; :cond_1d4 invoke-virtual {v3}, Ltest/Statistic;->getType()Ltest/Statistic$Type; move-result-object v4 sget-object v5, Ltest/Statistic$Type;->ENTITY:Ltest/Statistic$Type; if-ne v4, v5, :cond_274 .line 113 const-string v4, "." move-object/from16 v0, v16 invoke-virtual {v0, v4}, Ljava/lang/String;->lastIndexOf(Ljava/lang/String;)I move-result v4 add-int/lit8 v4, v4, 0x1 move-object/from16 v0, v16 invoke-virtual {v0, v4}, Ljava/lang/String;->substring(I)Ljava/lang/String; move-result-object v4 invoke-static {v4}, Ltest/entity/EntityType;->fromName(Ljava/lang/String;)Ltest/entity/EntityType; move-result-object v6 .line 115 .local v6, "entityType":Ltest/entity/EntityType; if-nez v6, :cond_206 .line 116 const-string v4, "Unknown achievement or statistic \'%s\'" const/4 v5, 0x1 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 aput-object v16, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 117 const/4 v4, 0x1 goto/16 :goto_7 .line 120 :cond_206 new-instance v1, Ltest/event/player/PlayerStatisticIncrementEvent; invoke-interface {v2, v3}, Ltest/entity/Player;->getStatistic(Ltest/Statistic;)I move-result v4 invoke-interface {v2, v3}, Ltest/entity/Player;->getStatistic(Ltest/Statistic;)I move-result v5 add-int/lit8 v5, v5, 0x1 invoke-direct/range {v1 .. v6}, Ltest/event/player/PlayerStatisticIncrementEvent;->(Ltest/entity/Player;Ltest/Statistic;IILtest/entity/EntityType;)V .line 121 .restart local v1 # "event":Ltest/event/player/PlayerStatisticIncrementEvent; invoke-static {}, Ltest/Bukkit;->getServer()Ltest/Server; move-result-object v4 invoke-interface {v4}, Ltest/Server;->getPluginManager()Ltest/plugin/PluginManager; move-result-object v4 invoke-interface {v4, v1}, Ltest/plugin/PluginManager;->callEvent(Ltest/event/Event;)V .line 122 invoke-virtual {v1}, Ltest/event/player/PlayerStatisticIncrementEvent;->isCancelled()Z move-result v4 if-eqz v4, :cond_241 .line 123 const-string v4, "Unable to increment %s for %s" const/4 v5, 0x2 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 aput-object v16, v5, v7 const/4 v7, 0x1 invoke-interface {v2}, Ltest/entity/Player;->getName()Ljava/lang/String; move-result-object v8 aput-object v8, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 124 const/4 v4, 0x1 goto/16 :goto_7 .line 128 :cond_241 :try_start_241 invoke-interface {v2, v3, v6}, Ltest/entity/Player;->incrementStatistic(Ltest/Statistic;Ltest/entity/EntityType;)V :try_end_244 .catch Ljava/lang/IllegalArgumentException; {:try_start_241 .. :try_end_244} :catch_25f .line 164 .end local v6 # "entityType":Ltest/entity/EntityType; :goto_244 const-string v4, "Successfully given %s the stat %s" const/4 v5, 0x2 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 invoke-interface {v2}, Ltest/entity/Player;->getName()Ljava/lang/String; move-result-object v8 aput-object v8, v5, v7 const/4 v7, 0x1 aput-object v16, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-static {v0, v4}, Ltest/command/Command;->broadcastCommandMessage(Ltest/TestSender;Ljava/lang/String;)V .line 165 const/4 v4, 0x1 goto/16 :goto_7 .line 129 .restart local v6 # "entityType":Ltest/entity/EntityType; :catch_25f move-exception v14 .line 130 .local v14, "e":Ljava/lang/IllegalArgumentException; const-string v4, "Unknown achievement or statistic \'%s\'" const/4 v5, 0x1 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 aput-object v16, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 131 const/4 v4, 0x1 goto/16 :goto_7 .line 136 .end local v1 # "event":Ltest/event/player/PlayerStatisticIncrementEvent; .end local v6 # "entityType":Ltest/entity/EntityType; .end local v14 # "e":Ljava/lang/IllegalArgumentException; :cond_274 :try_start_274 const-string v4, "." move-object/from16 v0, v16 invoke-virtual {v0, v4}, Ljava/lang/String;->lastIndexOf(Ljava/lang/String;)I move-result v4 add-int/lit8 v4, v4, 0x1 move-object/from16 v0, v16 invoke-virtual {v0, v4}, Ljava/lang/String;->substring(I)Ljava/lang/String; move-result-object v9 const/4 v10, 0x0 const v11, 0x7fffffff const/4 v12, 0x1 move-object/from16 v7, p0 move-object/from16 v8, p1 invoke-virtual/range {v7 .. v12}, Lconditions/TestIfCodeStyle2;->getInteger(Ltest/TestSender;Ljava/lang/String;IIZ)I :try_end_290 .catch Ljava/lang/NumberFormatException; {:try_start_274 .. :try_end_290} :catch_2ab move-result v15 .line 142 .local v15, "id":I invoke-static {v15}, Ltest/Material;->getMaterial(I)Ltest/Material; move-result-object v12 .line 144 .local v12, "material":Ltest/Material; if-nez v12, :cond_2b8 .line 145 const-string v4, "Unknown achievement or statistic \'%s\'" const/4 v5, 0x1 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 aput-object v16, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 146 const/4 v4, 0x1 goto/16 :goto_7 .line 137 .end local v12 # "material":Ltest/Material; .end local v15 # "id":I :catch_2ab move-exception v14 .line 138 .local v14, "e":Ljava/lang/NumberFormatException; invoke-virtual {v14}, Ljava/lang/NumberFormatException;->getMessage()Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 139 const/4 v4, 0x1 goto/16 :goto_7 .line 149 .end local v14 # "e":Ljava/lang/NumberFormatException; .restart local v12 # "material":Ltest/Material; .restart local v15 # "id":I :cond_2b8 new-instance v1, Ltest/event/player/PlayerStatisticIncrementEvent; invoke-interface {v2, v3}, Ltest/entity/Player;->getStatistic(Ltest/Statistic;)I move-result v10 invoke-interface {v2, v3}, Ltest/entity/Player;->getStatistic(Ltest/Statistic;)I move-result v4 add-int/lit8 v11, v4, 0x1 move-object v7, v1 move-object v8, v2 move-object v9, v3 invoke-direct/range {v7 .. v12}, Ltest/event/player/PlayerStatisticIncrementEvent;->(Ltest/entity/Player;Ltest/Statistic;IILtest/Material;)V .line 150 .restart local v1 # "event":Ltest/event/player/PlayerStatisticIncrementEvent; invoke-static {}, Ltest/Bukkit;->getServer()Ltest/Server; move-result-object v4 invoke-interface {v4}, Ltest/Server;->getPluginManager()Ltest/plugin/PluginManager; move-result-object v4 invoke-interface {v4, v1}, Ltest/plugin/PluginManager;->callEvent(Ltest/event/Event;)V .line 151 invoke-virtual {v1}, Ltest/event/player/PlayerStatisticIncrementEvent;->isCancelled()Z move-result v4 if-eqz v4, :cond_2f6 .line 152 const-string v4, "Unable to increment %s for %s" const/4 v5, 0x2 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 aput-object v16, v5, v7 const/4 v7, 0x1 invoke-interface {v2}, Ltest/entity/Player;->getName()Ljava/lang/String; move-result-object v8 aput-object v8, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 153 const/4 v4, 0x1 goto/16 :goto_7 .line 157 :cond_2f6 :try_start_2f6 invoke-interface {v2, v3, v12}, Ltest/entity/Player;->incrementStatistic(Ltest/Statistic;Ltest/Material;)V :try_end_2f9 .catch Ljava/lang/IllegalArgumentException; {:try_start_2f6 .. :try_end_2f9} :catch_2fb goto/16 :goto_244 .line 158 :catch_2fb move-exception v14 .line 159 .local v14, "e":Ljava/lang/IllegalArgumentException; const-string v4, "Unknown achievement or statistic \'%s\'" const/4 v5, 0x1 new-array v5, v5, [Ljava/lang/Object; const/4 v7, 0x0 aput-object v16, v5, v7 invoke-static {v4, v5}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; move-result-object v4 move-object/from16 v0, p1 invoke-interface {v0, v4}, Ltest/TestSender;->sendMessage(Ljava/lang/String;)V .line 160 const/4 v4, 0x1 goto/16 :goto_7 .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestIfElseAndConditionIntermediateInstruction.smali ================================================ ###### Class conditions.TestIfElseAndConditionIntermediateInstruction (conditions.TestIfElseAndConditionIntermediateInstruction) .class public Lconditions/TestIfElseAndConditionIntermediateInstruction; .super Ljava/lang/Object; .source "TestIfElseAndConditionIntermediateInstruction.java" # static fields .field private static final CONST:F = 342.0f # instance fields .field private bool:Z .field private num:F # direct methods .method public constructor ()V .registers 1 .line 3 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method private nothing1()V .registers 1 .line 19 return-void .end method .method private nothing2()V .registers 1 .line 23 return-void .end method # virtual methods .method public function()V .registers 3 .line 9 iget-boolean v0, p0, Lconditions/TestIfElseAndConditionIntermediateInstruction;->bool:Z if-eqz v0, :cond_12 iget v0, p0, Lconditions/TestIfElseAndConditionIntermediateInstruction;->num:F const/high16 v1, 0x3f800000 # 1.0f cmpg-float v1, v0, v1 if-gez v1, :cond_12 .line 10 const/high16 v1, 0x43ab0000 # 342.0f add-float/2addr v0, v1 iput v0, p0, Lconditions/TestIfElseAndConditionIntermediateInstruction;->num:F goto :goto_15 .line 12 :cond_12 invoke-direct {p0}, Lconditions/TestIfElseAndConditionIntermediateInstruction;->nothing2()V .line 14 :goto_15 invoke-direct {p0}, Lconditions/TestIfElseAndConditionIntermediateInstruction;->nothing1()V .line 15 return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestInnerAssign3.smali ================================================ .class public Lconditions/TestInnerAssign3; .super LTestSuper; .source "Test.java" .method public test()V .locals 5 const/4 v0, 0 const/4 v1, 0 const/4 v4, 0 if-eqz v4, :cond_0 const/4 v2, 0 invoke-virtual {v2}, LTestClass1;->testMethod()LTestClass2; move-result-object v0 if-eqz v0, :cond_0 if-eq v1, v0, :cond_0 iget-object v3, v2, LTestClass1;->testField:LTestClass3; if-eqz v3, :cond_0 :cond_0 return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestOutBlock.smali ================================================ .class public Lconditions/TestOutBlock; .super Lcom/eltechs/axs/activities/FrameworkActivity; .method protected onCreate(Landroid/os/Bundle;)V .registers 9 .line 98 invoke-super {p0, p1}, Lcom/eltechs/axs/activities/FrameworkActivity;->onCreate(Landroid/os/Bundle;)V .line 101 invoke-virtual {p0}, Lconditions/TestOutBlock;->getApplicationState()Lcom/eltechs/axs/applicationState/ApplicationStateBase; move-result-object p1 .line 102 invoke-interface {p1}, Lcom/eltechs/axs/applicationState/ApplicationStateBase;->getEnvironment()Lcom/eltechs/axs/environmentService/AXSEnvironment; move-result-object v0 const-class v1, Lcom/eltechs/axs/environmentService/components/XServerComponent; invoke-virtual {v0, v1}, Lcom/eltechs/axs/environmentService/AXSEnvironment;->getComponent(Ljava/lang/Class;)Lcom/eltechs/axs/environmentService/EnvironmentComponent; move-result-object v0 check-cast v0, Lcom/eltechs/axs/environmentService/components/XServerComponent; .line 103 invoke-virtual {p0}, Lconditions/TestOutBlock;->getIntent()Landroid/content/Intent; move-result-object v1 const-string v2, "facadeclass" invoke-virtual {v1, v2}, Landroid/content/Intent;->getSerializableExtra(Ljava/lang/String;)Ljava/io/Serializable; move-result-object v1 check-cast v1, Ljava/lang/Class; if-eqz v1, :cond_46 const/4 v2, 0x2 const/4 v3, 0x0 .line 108 :try_start_23 new-array v4, v2, [Ljava/lang/Class; const-class v5, Lcom/eltechs/axs/xserver/XServer; aput-object v5, v4, v3 const-class v5, Lcom/eltechs/axs/applicationState/ApplicationStateBase; const/4 v6, 0x1 aput-object v5, v4, v6 invoke-virtual {v1, v4}, Ljava/lang/Class;->getDeclaredConstructor([Ljava/lang/Class;)Ljava/lang/reflect/Constructor; move-result-object v1 .line 109 new-array v2, v2, [Ljava/lang/Object; invoke-virtual {v0}, Lcom/eltechs/axs/environmentService/components/XServerComponent;->getXServer()Lcom/eltechs/axs/xserver/XServer; move-result-object v4 aput-object v4, v2, v3 aput-object p1, v2, v6 invoke-virtual {v1, v2}, Ljava/lang/reflect/Constructor;->newInstance([Ljava/lang/Object;)Ljava/lang/Object; move-result-object v1 check-cast v1, Lcom/eltechs/axs/xserver/ViewFacade; :try_end_42 .catch Ljava/lang/Exception; {:try_start_23 .. :try_end_42} :catch_43 goto :goto_47 .line 112 :catch_43 invoke-static {v3}, Lcom/eltechs/axs/helpers/Assert;->state(Z)V :cond_46 const/4 v1, 0x0 .line 118 :goto_47 invoke-virtual {p0}, Lconditions/TestOutBlock;->getWindow()Landroid/view/Window; move-result-object v2 const/16 v3, 0x80 invoke-virtual {v2, v3}, Landroid/view/Window;->addFlags(I)V .line 120 invoke-virtual {p0}, Lconditions/TestOutBlock;->getWindow()Landroid/view/Window; move-result-object v2 const/high16 v3, 0x400000 invoke-virtual {v2, v3}, Landroid/view/Window;->addFlags(I)V .line 125 sget v2, Lcom/eltechs/axs/R$layout;->main:I invoke-virtual {p0, v2}, Lconditions/TestOutBlock;->setContentView(I)V .line 127 invoke-direct {p0}, Lconditions/TestOutBlock;->checkForSuddenDeath()Z move-result v2 if-eqz v2, :cond_65 return-void .line 135 :cond_65 new-instance v2, Lcom/eltechs/axs/widgets/viewOfXServer/ViewOfXServer; invoke-virtual {v0}, Lcom/eltechs/axs/environmentService/components/XServerComponent;->getXServer()Lcom/eltechs/axs/xserver/XServer; move-result-object v0 invoke-interface {p1}, Lcom/eltechs/axs/applicationState/ApplicationStateBase;->getXServerViewConfiguration()Lcom/eltechs/axs/configuration/XServerViewConfiguration; move-result-object p1 invoke-direct {v2, p0, v0, v1, p1}, Lcom/eltechs/axs/widgets/viewOfXServer/ViewOfXServer;->(Landroid/content/Context;Lcom/eltechs/axs/xserver/XServer;Lcom/eltechs/axs/xserver/ViewFacade;Lcom/eltechs/axs/configuration/XServerViewConfiguration;)V iput-object v2, p0, Lconditions/TestOutBlock;->viewOfXServer:Lcom/eltechs/axs/widgets/viewOfXServer/ViewOfXServer; .line 137 iget-object p1, p0, Lconditions/TestOutBlock;->periodicIabCheckTimer:Landroid/os/CountDownTimer; invoke-virtual {p1}, Landroid/os/CountDownTimer;->start()Landroid/os/CountDownTimer; return-void .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestTernary4.smali ================================================ .class public final Lconditions/TestTernary4; .super Ljava/lang/Object; .field private defaultValuesByPath:Ljava/util/Map; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/util/Map<", "Ljava/lang/String;", "Ljava/lang/Object;", ">;" } .end annotation .end field .field private valuesByPath:Ljava/util/Map; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/util/Map<", "Ljava/lang/String;", "Ljava/lang/Object;", ">;" } .end annotation .end field .method private test(Ljava/util/HashMap;)Ljava/util/Set; .registers 10 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/util/HashMap<", "Ljava/lang/String;", "Ljava/lang/Object;", ">;)", "Ljava/util/Set;" } .end annotation .line 278 new-instance v0, Ljava/util/HashSet; invoke-direct {v0}, Ljava/util/HashSet;->()V .line 280 iget-object v1, p0, Lconditions/TestTernary4;->defaultValuesByPath:Ljava/util/Map; monitor-enter v1 .line 281 :try_start_14 iget-object v3, p0, Lconditions/TestTernary4;->defaultValuesByPath:Ljava/util/Map; invoke-interface {v3}, Ljava/util/Map;->keySet()Ljava/util/Set; move-result-object v3 invoke-interface {v3}, Ljava/util/Set;->iterator()Ljava/util/Iterator; move-result-object v3 :cond_1e :goto_1e invoke-interface {v3}, Ljava/util/Iterator;->hasNext()Z move-result v4 if-eqz v4, :cond_4c invoke-interface {v3}, Ljava/util/Iterator;->next()Ljava/lang/Object; move-result-object v4 check-cast v4, Ljava/lang/String; .line 286 invoke-virtual {p1, v4}, Ljava/util/HashMap;->get(Ljava/lang/Object;)Ljava/lang/Object; move-result-object v5 const/4 v6, 0x1 if-eqz v5, :cond_3b .line 287 invoke-direct {p0, v4}, Lconditions/TestTernary4;->getValueObject(Ljava/lang/String;)Ljava/lang/Object; move-result-object v7 invoke-virtual {v7, v5}, Ljava/lang/Object;->equals(Ljava/lang/Object;)Z move-result v5 xor-int/2addr v5, v6 goto :goto_46 .line 289 :cond_3b iget-object v5, p0, Lconditions/TestTernary4;->valuesByPath:Ljava/util/Map; invoke-interface {v5, v4}, Ljava/util/Map;->get(Ljava/lang/Object;)Ljava/lang/Object; move-result-object v5 if-eqz v5, :cond_45 const/4 v5, 0x1 goto :goto_46 :cond_45 const/4 v5, 0x0 :goto_46 if-eqz v5, :cond_1e .line 293 invoke-interface {v0, v4}, Ljava/util/Set;->add(Ljava/lang/Object;)Z goto :goto_1e .line 296 :cond_4c monitor-exit v1 return-object v0 :catchall_4e move-exception p1 monitor-exit v1 :try_end_50 .catchall {:try_start_14 .. :try_end_50} :catchall_4e throw p1 .end method .method private getValueObject(Ljava/lang/String;)Ljava/lang/Object; .locals 1 const/4 v0, 0x0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestTernaryInIf2.smali ================================================ .class public LTestTernaryInIf2; .super Ljava/lang/Object; # instance fields .field private a:Ljava/lang/String; .field private b:Ljava/lang/String; .field private c:Ljava/lang/String; # direct methods .method public constructor ()V .locals 0 .line 10 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method # virtual methods .method public equals(Ljava/lang/Object;)Z .locals 4 const/4 v0, 0x1 if-ne p0, p1, :cond_0 return v0 :cond_0 const/4 v1, 0x0 if-eqz p1, :cond_a .line 110 invoke-virtual {p0}, Ljava/lang/Object;->getClass()Ljava/lang/Class; move-result-object v2 invoke-virtual {p1}, Ljava/lang/Object;->getClass()Ljava/lang/Class; move-result-object v3 if-eq v2, v3, :cond_1 goto :goto_4 .line 112 :cond_1 check-cast p1, LTestTernaryInIf2; .line 114 iget-object v2, p0, LTestTernaryInIf2;->a:Ljava/lang/String; if-eqz v2, :cond_2 iget-object v2, p0, LTestTernaryInIf2;->a:Ljava/lang/String; iget-object v3, p1, LTestTernaryInIf2;->a:Ljava/lang/String; invoke-virtual {v2, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v2 if-nez v2, :cond_3 goto :goto_0 :cond_2 iget-object v2, p1, LTestTernaryInIf2;->a:Ljava/lang/String; if-eqz v2, :cond_3 :goto_0 return v1 .line 116 :cond_3 iget-object v2, p0, LTestTernaryInIf2;->b:Ljava/lang/String; if-eqz v2, :cond_4 iget-object v2, p0, LTestTernaryInIf2;->b:Ljava/lang/String; iget-object v3, p1, LTestTernaryInIf2;->b:Ljava/lang/String; invoke-virtual {v2, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v2 if-nez v2, :cond_5 goto :goto_1 :cond_4 iget-object v2, p1, LTestTernaryInIf2;->b:Ljava/lang/String; if-eqz v2, :cond_5 :goto_1 return v1 .line 118 :cond_5 iget-object v2, p0, LTestTernaryInIf2;->c:Ljava/lang/String; if-eqz v2, :cond_6 iget-object v2, p0, LTestTernaryInIf2;->c:Ljava/lang/String; iget-object v3, p1, LTestTernaryInIf2;->c:Ljava/lang/String; invoke-virtual {v2, v3}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v2 if-nez v2, :cond_7 return v1 :cond_6 iget-object v2, p1, LTestTernaryInIf2;->c:Ljava/lang/String; if-eqz v2, :cond_7 .line 120 :cond_7 :cond_8 const/4 v0, 0x0 :goto_3 return v0 :cond_a :goto_4 return v1 .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestTernaryInIf3.smali ================================================ .class public Lconditions/TestTernaryInIf3; .super Ljava/lang/Object; .method public static final A01(LX/73h;FFZ)Landroid/util/Pair; .registers 7 .line 0 const/4 v1, 0x0 .line 1 cmpl-float v0, p1, v1 .line 2 .line 3 if-eqz v0, :cond_33 .line 4 .line 5 cmpl-float v0, p2, v1 .line 6 .line 7 if-eqz v0, :cond_33 .line 8 .line 9 if-eqz p3, :cond_24 .line 10 .line 11 cmpg-float v0, p2, v1 .line 12 .line 13 if-ltz v0, :cond_26 .line 14 .line 15 :cond_f iget-object v1, p0, LX/73h;->A00:LX/5Yj; .line 16 .line 17 const-string v0, "translationXCurveDownwards" .line 18 .line 19 invoke-virtual {v1, v0}, LX/5Yj;->A03(Ljava/lang/String;)LX/5Wd; .line 20 .line 21 .line 22 move-result-object v2 .line 23 iget-object v1, p0, LX/73h;->A00:LX/5Yj; .line 24 .line 25 const-string v0, "translationYCurveDownwards" .line 26 .line 27 :goto_1b invoke-virtual {v1, v0}, LX/5Yj;->A03(Ljava/lang/String;)LX/5Wd; .line 28 .line 29 .line 30 move-result-object v0 .line 31 invoke-static {v2, v0}, LX/0xz;->A0F(Ljava/lang/Object;Ljava/lang/Object;)Landroid/util/Pair; .line 32 .line 33 .line 34 move-result-object v0 .line 35 return-object v0 .line 36 :cond_24 if-lez v0, :cond_f .line 37 .line 38 :cond_26 iget-object v1, p0, LX/73h;->A00:LX/5Yj; .line 39 .line 40 const-string v0, "translationXCurveUpwards" .line 41 .line 42 invoke-virtual {v1, v0}, LX/5Yj;->A03(Ljava/lang/String;)LX/5Wd; .line 43 .line 44 .line 45 move-result-object v2 .line 46 iget-object v1, p0, LX/73h;->A00:LX/5Yj; .line 47 .line 48 const-string v0, "translationYCurveUpwards" .line 49 .line 50 goto :goto_1b .line 51 :cond_33 iget-object v1, p0, LX/73h;->A00:LX/5Yj; .line 52 .line 53 const-string v0, "translationXLinear" .line 54 .line 55 invoke-virtual {v1, v0}, LX/5Yj;->A03(Ljava/lang/String;)LX/5Wd; .line 56 .line 57 .line 58 move-result-object v2 .line 59 iget-object v1, p0, LX/73h;->A00:LX/5Yj; .line 60 .line 61 const-string v0, "translationYLinear" .line 62 .line 63 goto :goto_1b .line 64 .line 65 .line 66 .line 67 .line 68 .line 69 .line 70 .line 71 .line 72 .line 73 .line 74 .line 75 .line 76 .line 77 .end method ================================================ FILE: jadx-core/src/test/smali/conditions/TestTernaryOneBranchInConstructor2.smali ================================================ .class public final Lconditions/TestTernaryOneBranchInConstructor2; .super Ljava/lang/Object; .method public constructor (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V .locals 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public synthetic constructor (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZII)V .locals 1 and-int/lit8 p6, p5, 0x2 const-string v0, "" if-eqz p6, :cond_0 move-object p2, v0 :cond_0 and-int/lit8 p6, p5, 0x4 if-eqz p6, :cond_1 move-object p3, v0 :cond_1 and-int/lit8 p5, p5, 0x8 if-eqz p5, :cond_2 const/4 p4, 0x0 :cond_2 invoke-direct {p0, p1, p2, p3, p4}, Lconditions/TestTernaryOneBranchInConstructor2;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V return-void .end method ================================================ FILE: jadx-core/src/test/smali/debuginfo/TestVariablesNames.smali ================================================ .class public LTestVariablesNames; .super Ljava/lang/Object; .source "TestVariablesNames.java" .method public test(Ljava/lang/String;I)V .registers 10 .prologue .line 17 invoke-direct {p0, p1}, LTestVariablesNames;->f1(Ljava/lang/String;)V .line 18 add-int/lit8 p1, p2, 0x3 .line 19 new-instance v5, Ljava/lang/StringBuilder; invoke-direct {v5}, Ljava/lang/StringBuilder;->()V const-string v6, "i" invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v5 invoke-virtual {v5, p1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; move-result-object v5 invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v3 .line 20 invoke-direct {p0, p1, v3}, LTestVariablesNames;->f2(ILjava/lang/String;)V .line 21 mul-int/lit8 v5, p1, 0x5 int-to-double p1, v5 .line 22 new-instance v5, Ljava/lang/StringBuilder; invoke-direct {v5}, Ljava/lang/StringBuilder;->()V const-string v6, "d" invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v5 invoke-virtual {v5, p1, v1}, Ljava/lang/StringBuilder;->append(D)Ljava/lang/StringBuilder; move-result-object v5 invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v4 .line 23 invoke-direct {p0, p1, v1, v4}, LTestVariablesNames;->f3(DLjava/lang/String;)V .line 24 return-void .end method .method public constructor ()V .registers 1 .prologue invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method private f1(Ljava/lang/String;)V .registers 2 .prologue return-void .end method .method private f2(ILjava/lang/String;)V .registers 3 .prologue return-void .end method .method private f3(DLjava/lang/String;)V .registers 4 .prologue return-void .end method ================================================ FILE: jadx-core/src/test/smali/enums/TestEnumKotlinEntries.smali ================================================ .class public final enum Lenums/TestEnumKotlinEntries; .super Ljava/lang/Enum; .source "TestEnumKotlinEntries.kt" # annotations .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Enum<", "Lenums/TestEnumKotlinEntries;", ">;" } .end annotation .annotation runtime Lkotlin/Metadata; d1 = { "\u0000\u000c\n\u0002\u0018\u0002\n\u0002\u0010\u0010\n\u0002\u0008\u0005\u0008\u0086\u0081\u0002\u0018\u00002\u0008\u0012\u0004\u0012\u00020\u00000\u0001B\t\u0008\u0002\u00a2\u0006\u0004\u0008\u0002\u0010\u0003j\u0002\u0008\u0004j\u0002\u0008\u0005j\u0002\u0008\u0006" } d2 = { "Lenums/TestEnumKotlinEntries;", "", "", "(Ljava/lang/String;I)V", "ALPHA", "BETA", "GAMMA" } k = 0x1 mv = { 0x2, 0x3, 0x0 } xi = 0x30 .end annotation # static fields .field private static final synthetic $ENTRIES:Lkotlin/enums/EnumEntries; .field private static final synthetic $VALUES:[Lenums/TestEnumKotlinEntries; .field public static final enum ALPHA:Lenums/TestEnumKotlinEntries; .field public static final enum BETA:Lenums/TestEnumKotlinEntries; .field public static final enum GAMMA:Lenums/TestEnumKotlinEntries; # direct methods .method private static final synthetic $values()[Lenums/TestEnumKotlinEntries; .registers 3 const/4 v0, 0x3 new-array v0, v0, [Lenums/TestEnumKotlinEntries; const/4 v1, 0x0 sget-object v2, Lenums/TestEnumKotlinEntries;->ALPHA:Lenums/TestEnumKotlinEntries; aput-object v2, v0, v1 const/4 v1, 0x1 sget-object v2, Lenums/TestEnumKotlinEntries;->BETA:Lenums/TestEnumKotlinEntries; aput-object v2, v0, v1 const/4 v1, 0x2 sget-object v2, Lenums/TestEnumKotlinEntries;->GAMMA:Lenums/TestEnumKotlinEntries; aput-object v2, v0, v1 return-object v0 .end method .method static constructor ()V .registers 3 .line 4 new-instance v0, Lenums/TestEnumKotlinEntries; const-string v1, "ALPHA" const/4 v2, 0x0 invoke-direct {v0, v1, v2}, Lenums/TestEnumKotlinEntries;->(Ljava/lang/String;I)V sput-object v0, Lenums/TestEnumKotlinEntries;->ALPHA:Lenums/TestEnumKotlinEntries; .line 5 new-instance v0, Lenums/TestEnumKotlinEntries; const-string v1, "BETA" const/4 v2, 0x1 invoke-direct {v0, v1, v2}, Lenums/TestEnumKotlinEntries;->(Ljava/lang/String;I)V sput-object v0, Lenums/TestEnumKotlinEntries;->BETA:Lenums/TestEnumKotlinEntries; .line 6 new-instance v0, Lenums/TestEnumKotlinEntries; const-string v1, "GAMMA" const/4 v2, 0x2 invoke-direct {v0, v1, v2}, Lenums/TestEnumKotlinEntries;->(Ljava/lang/String;I)V sput-object v0, Lenums/TestEnumKotlinEntries;->GAMMA:Lenums/TestEnumKotlinEntries; invoke-static {}, Lenums/TestEnumKotlinEntries;->$values()[Lenums/TestEnumKotlinEntries; move-result-object v0 sput-object v0, Lenums/TestEnumKotlinEntries;->$VALUES:[Lenums/TestEnumKotlinEntries; check-cast v0, [Ljava/lang/Enum; invoke-static {v0}, Lkotlin/enums/EnumEntriesKt;->enumEntries([Ljava/lang/Enum;)Lkotlin/enums/EnumEntries; move-result-object v0 sput-object v0, Lenums/TestEnumKotlinEntries;->$ENTRIES:Lkotlin/enums/EnumEntries; return-void .end method .method private constructor (Ljava/lang/String;I)V .registers 3 .param p1, "$enum$name" # Ljava/lang/String; .param p2, "$enum$ordinal" # I .annotation system Ldalvik/annotation/Signature; value = { "()V" } .end annotation .line 3 invoke-direct {p0, p1, p2}, Ljava/lang/Enum;->(Ljava/lang/String;I)V return-void .end method .method public static getEntries()Lkotlin/enums/EnumEntries; .registers 1 .annotation system Ldalvik/annotation/Signature; value = { "()", "Lkotlin/enums/EnumEntries<", "Lenums/TestEnumKotlinEntries;", ">;" } .end annotation sget-object v0, Lenums/TestEnumKotlinEntries;->$ENTRIES:Lkotlin/enums/EnumEntries; return-object v0 .end method .method public static valueOf(Ljava/lang/String;)Lenums/TestEnumKotlinEntries; .registers 2 const-class v0, Lenums/TestEnumKotlinEntries; invoke-static {v0, p0}, Ljava/lang/Enum;->valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; move-result-object v0 check-cast v0, Lenums/TestEnumKotlinEntries; return-object v0 .end method .method public static values()[Lenums/TestEnumKotlinEntries; .registers 1 sget-object v0, Lenums/TestEnumKotlinEntries;->$VALUES:[Lenums/TestEnumKotlinEntries; invoke-virtual {v0}, Ljava/lang/Object;->clone()Ljava/lang/Object; move-result-object v0 check-cast v0, [Lenums/TestEnumKotlinEntries; return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/enums/TestEnumObfuscated.smali ================================================ ###### Class jadx.tests.integration.enums.TestEnums3$TestCls.Numbers (jadx.tests.integration.enums.TestEnums3$TestCls$Numbers) .class public final enum Lenums/TestEnumObfuscated; .super Ljava/lang/Enum; # static fields .field private static final synthetic $VLS:[Lenums/TestEnumObfuscated; .field public static final enum ONE:Lenums/TestEnumObfuscated; .field public static final enum TWO:Lenums/TestEnumObfuscated; # instance fields .field private final num:I # direct methods .method static constructor ()V .registers 7 .prologue const/4 v6, 0x3 const/4 v5, 0x0 const/4 v4, 0x2 const/4 v3, 0x1 new-instance v0, Lenums/TestEnumObfuscated; const-string v1, "ONE" invoke-direct {v0, v1, v5, v3}, Lenums/TestEnumObfuscated;->(Ljava/lang/String;II)V sput-object v0, Lenums/TestEnumObfuscated;->ONE:Lenums/TestEnumObfuscated; new-instance v0, Lenums/TestEnumObfuscated; const-string v1, "TWO" invoke-direct {v0, v1, v3, v4}, Lenums/TestEnumObfuscated;->(Ljava/lang/String;II)V sput-object v0, Lenums/TestEnumObfuscated;->TWO:Lenums/TestEnumObfuscated; const/4 v0, 0x2 new-array v0, v0, [Lenums/TestEnumObfuscated; sget-object v1, Lenums/TestEnumObfuscated;->ONE:Lenums/TestEnumObfuscated; aput-object v1, v0, v5 sget-object v1, Lenums/TestEnumObfuscated;->TWO:Lenums/TestEnumObfuscated; aput-object v1, v0, v3 sput-object v0, Lenums/TestEnumObfuscated;->$VLS:[Lenums/TestEnumObfuscated; return-void .end method .method private constructor (Ljava/lang/String;II)V .registers 4 .prologue invoke-direct {p0, p1, p2}, Ljava/lang/Enum;->(Ljava/lang/String;I)V iput p3, p0, Lenums/TestEnumObfuscated;->num:I return-void .end method .method public static vo(Ljava/lang/String;)Lenums/TestEnumObfuscated; .registers 2 .prologue const-class v0, Lenums/TestEnumObfuscated; invoke-static {v0, p0}, Ljava/lang/Enum;->valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; move-result-object v0 check-cast v0, Lenums/TestEnumObfuscated; return-object v0 .end method .method public static vs()[Lenums/TestEnumObfuscated; .registers 1 .prologue sget-object v0, Lenums/TestEnumObfuscated;->$VLS:[Lenums/TestEnumObfuscated; invoke-virtual {v0}, [Lenums/TestEnumObfuscated;->clone()Ljava/lang/Object; move-result-object v0 check-cast v0, [Lenums/TestEnumObfuscated; return-object v0 .end method .method public static values()[Lenums/TestEnumObfuscated; .registers 1 sget-object v0, Lenums/TestEnumObfuscated;->$VLS:[Lenums/TestEnumObfuscated; return v0 .end method .method public static valuesCount()I .registers 2 invoke-static {v0, p0}, Lenums/TestEnumObfuscated;->vs()[Lenums/TestEnumObfuscated; move-result-object v0 array-length v1, v0 return v1 .end method .method public static valuesFieldUse()I .registers 2 sget-object v0, Lenums/TestEnumObfuscated;->$VLS:[Lenums/TestEnumObfuscated; array-length v1, v0 return v1 .end method .method public synthetic getNum()I .registers 2 .prologue iget v0, p0, Lenums/TestEnumObfuscated;->num:I return v0 .end method ================================================ FILE: jadx-core/src/test/smali/enums/TestEnumUsesOtherEnum.smali ================================================ .class public final enum Lenums/TestEnumUsesOtherEnum; .super Ljava/lang/Enum; .source "" .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Enum<", "Lenums/TestEnumUsesOtherEnum;", ">;" } .end annotation .field public static final enum A:Lenums/TestEnumUsesOtherEnum; .field private static final synthetic B:[Lenums/TestEnumUsesOtherEnum; .field public static final enum i:Lenums/TestEnumUsesOtherEnum; .field public static final enum j:Lenums/TestEnumUsesOtherEnum; .field public static final enum k:Lenums/TestEnumUsesOtherEnum; .field public static final enum l:Lenums/TestEnumUsesOtherEnum; .field public static final enum m:Lenums/TestEnumUsesOtherEnum; .field public static final enum n:Lenums/TestEnumUsesOtherEnum; .field public static final enum o:Lenums/TestEnumUsesOtherEnum; .field public static final enum p:Lenums/TestEnumUsesOtherEnum; .field public static final enum q:Lenums/TestEnumUsesOtherEnum; .field public static final enum r:Lenums/TestEnumUsesOtherEnum; .field public static final enum s:Lenums/TestEnumUsesOtherEnum; .field public static final enum t:Lenums/TestEnumUsesOtherEnum; .field public static final enum u:Lenums/TestEnumUsesOtherEnum; .field public static final enum v:Lenums/TestEnumUsesOtherEnum; .field public static final enum w:Lenums/TestEnumUsesOtherEnum; .field public static final enum x:Lenums/TestEnumUsesOtherEnum; .field public static final enum y:Lenums/TestEnumUsesOtherEnum; .field public static final enum z:Lenums/TestEnumUsesOtherEnum; # instance fields .field public c:Ljava/lang/String; .field public d:Ljava/lang/String; .field public e:Ljava/lang/Integer; .field public f:Z .field public g:Z .field public h:[Ljava/lang/String; .method static constructor ()V .registers 53 new-instance v9, Lenums/TestEnumUsesOtherEnum; const/4 v10, 0x1 invoke-static {v10}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer; move-result-object v11 const/16 v12, 0xc new-array v8, v12, [Ljava/lang/String; const-string v0, "VARCHAR" const/4 v13, 0x0 invoke-static {v13}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer; move-result-object v23 aput-object v0, v8, v13 const-string v0, "VARCHAR2" aput-object v0, v8, v10 const-string v0, "TEXT" const/4 v15, 0x2 aput-object v0, v8, v15 const-string v0, "STRING" const/4 v14, 0x3 aput-object v0, v8, v14 const-string v0, "VARBINARY" const/16 v24, 0x4 aput-object v0, v8, v24 const-string v0, "TINYTEXT" const/4 v7, 0x5 aput-object v0, v8, v7 const-string v0, "MEDIUMTEXT" const/16 v25, 0x6 aput-object v0, v8, v25 const-string v0, "LONGTEXT" const/4 v6, 0x7 aput-object v0, v8, v6 const-string v0, "NVARCHAR" const/16 v5, 0x8 aput-object v0, v8, v5 const-string v0, "NTEXT" const/16 v26, 0x9 aput-object v0, v8, v26 const-string v0, "MEMO" const/16 v27, 0xa aput-object v0, v8, v27 const-string v0, "HYPERLINK" const/16 v28, 0xb aput-object v0, v8, v28 const-string v1, "VARCHAR" const/4 v2, 0x0 const-string v3, "VARCHAR" const-string v4, "100" const/16 v16, 0x0 const/16 v17, 0x1 move-object v0, v9 const/16 v12, 0x8 move-object v5, v11 const/4 v12, 0x7 move/from16 v6, v16 move/from16 v7, v17 invoke-direct/range {v0 .. v8}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V sput-object v9, Lenums/TestEnumUsesOtherEnum;->i:Lenums/TestEnumUsesOtherEnum; new-instance v30, Lenums/TestEnumUsesOtherEnum; new-array v8, v15, [Ljava/lang/String; const-string v0, "CHAR" aput-object v0, v8, v13 const-string v0, "NCHAR" aput-object v0, v8, v10 const-string v1, "CHAR" const/4 v2, 0x1 const-string v3, "CHAR" const-string v4, "1" const/4 v6, 0x0 const/4 v7, 0x1 move-object/from16 v0, v30 invoke-direct/range {v0 .. v8}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V sput-object v30, Lenums/TestEnumUsesOtherEnum;->j:Lenums/TestEnumUsesOtherEnum; new-instance v31, Lenums/TestEnumUsesOtherEnum; new-array v8, v12, [Ljava/lang/String; const-string v0, "INT" aput-object v0, v8, v13 const-string v0, "INTEGER" aput-object v0, v8, v10 const-string v0, "NUMBER" aput-object v0, v8, v15 const-string v0, "SERIAL" aput-object v0, v8, v14 const-string v0, "YEAR" aput-object v0, v8, v24 const-string v0, "BIT" const/4 v7, 0x5 aput-object v0, v8, v7 const-string v0, "AUTONUMBER" aput-object v0, v8, v25 const-string v1, "INT" const/4 v2, 0x2 const-string v3, "INT" const-string v4, "10" const/4 v6, 0x1 move-object/from16 v0, v31 const/4 v11, 0x5 move/from16 v7, v16 invoke-direct/range {v0 .. v8}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V sput-object v31, Lenums/TestEnumUsesOtherEnum;->k:Lenums/TestEnumUsesOtherEnum; new-instance v0, Lenums/TestEnumUsesOtherEnum; new-array v1, v10, [Ljava/lang/String; const-string v2, "TINYINT" aput-object v2, v1, v13 const-string v17, "TINYINT" const/16 v18, 0x3 const-string v19, "TINYINT" move-object/from16 v16, v0 move-object/from16 v20, v31 move-object/from16 v21, v1 invoke-direct/range {v16 .. v21}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Lenums/TestEnumUsesOtherEnum;[Ljava/lang/String;)V sput-object v0, Lenums/TestEnumUsesOtherEnum;->l:Lenums/TestEnumUsesOtherEnum; new-instance v1, Lenums/TestEnumUsesOtherEnum; new-array v2, v15, [Ljava/lang/String; const-string v3, "SMALLINT" aput-object v3, v2, v13 const-string v3, "SMALLSERIAL" aput-object v3, v2, v10 const-string v17, "SMALLINT" const/16 v18, 0x4 const-string v19, "SMALLINT" move-object/from16 v16, v1 move-object/from16 v21, v2 invoke-direct/range {v16 .. v21}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Lenums/TestEnumUsesOtherEnum;[Ljava/lang/String;)V sput-object v1, Lenums/TestEnumUsesOtherEnum;->m:Lenums/TestEnumUsesOtherEnum; new-instance v2, Lenums/TestEnumUsesOtherEnum; new-array v3, v10, [Ljava/lang/String; const-string v4, "MEDIUMINT" aput-object v4, v3, v13 const-string v17, "MEDIUMINT" const/16 v18, 0x5 const-string v19, "MEDIUMINT" move-object/from16 v16, v2 move-object/from16 v21, v3 invoke-direct/range {v16 .. v21}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Lenums/TestEnumUsesOtherEnum;[Ljava/lang/String;)V sput-object v2, Lenums/TestEnumUsesOtherEnum;->n:Lenums/TestEnumUsesOtherEnum; new-instance v3, Lenums/TestEnumUsesOtherEnum; new-array v4, v15, [Ljava/lang/String; const-string v5, "BIGINT" aput-object v5, v4, v13 const-string v5, "BIGSERIAL" aput-object v5, v4, v10 const-string v17, "BIGINT" const/16 v18, 0x6 const-string v19, "BIGINT" move-object/from16 v16, v3 move-object/from16 v21, v4 invoke-direct/range {v16 .. v21}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Lenums/TestEnumUsesOtherEnum;[Ljava/lang/String;)V sput-object v3, Lenums/TestEnumUsesOtherEnum;->o:Lenums/TestEnumUsesOtherEnum; new-instance v4, Lenums/TestEnumUsesOtherEnum; new-array v5, v14, [Ljava/lang/String; const-string v6, "BOOLEAN" aput-object v6, v5, v13 const-string v6, "BOOL" aput-object v6, v5, v10 const-string v6, "YES/NO" aput-object v6, v5, v15 const-string v6, "BOOLEAN" const/16 v16, 0x7 const-string v17, "BOOLEAN" const/16 v18, 0x0 const/16 v20, 0x0 const/16 v21, 0x0 const/4 v7, 0x3 move-object v14, v4 const/4 v8, 0x2 move-object v15, v6 move-object/from16 v19, v23 move-object/from16 v22, v5 invoke-direct/range {v14 .. v22}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V sput-object v4, Lenums/TestEnumUsesOtherEnum;->p:Lenums/TestEnumUsesOtherEnum; new-instance v5, Lenums/TestEnumUsesOtherEnum; new-array v6, v12, [Ljava/lang/String; const-string v14, "BLOB" aput-object v14, v6, v13 const-string v14, "BYTEA" aput-object v14, v6, v10 const-string v14, "BINARY" aput-object v14, v6, v8 const-string v14, "TINYBLOB" aput-object v14, v6, v7 const-string v14, "MEDIUMBLOB" aput-object v14, v6, v24 const-string v14, "LONGBLOB" aput-object v14, v6, v11 const-string v14, "IMAGE" aput-object v14, v6, v25 const-string v15, "BLOB" const/16 v16, 0x8 const-string v17, "BLOB" move-object v14, v5 move-object/from16 v22, v6 invoke-direct/range {v14 .. v22}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V sput-object v5, Lenums/TestEnumUsesOtherEnum;->q:Lenums/TestEnumUsesOtherEnum; new-instance v6, Lenums/TestEnumUsesOtherEnum; invoke-static {v8}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer; move-result-object v37 const/16 v14, 0x8 new-array v15, v14, [Ljava/lang/String; const-string v14, "FLOAT" aput-object v14, v15, v13 const-string v14, "REAL" aput-object v14, v15, v10 const-string v14, "DEC" aput-object v14, v15, v8 const-string v14, "DECIMAL" aput-object v14, v15, v7 const-string v14, "MONEY" aput-object v14, v15, v24 const-string v14, "SMALLMONEY" aput-object v14, v15, v11 const-string v14, "SINGLE" aput-object v14, v15, v25 const-string v14, "CURRENCY" aput-object v14, v15, v12 const-string v33, "FLOAT" const/16 v34, 0x9 const-string v35, "FLOAT" const-string v36, "10, 5" const/16 v38, 0x1 const/16 v39, 0x0 move-object/from16 v32, v6 move-object/from16 v40, v15 invoke-direct/range {v32 .. v40}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V sput-object v6, Lenums/TestEnumUsesOtherEnum;->r:Lenums/TestEnumUsesOtherEnum; new-instance v29, Lenums/TestEnumUsesOtherEnum; new-array v14, v8, [Ljava/lang/String; const-string v15, "DOUBLE" aput-object v15, v14, v13 const-string v15, "DOUBLE PRECISION" aput-object v15, v14, v10 const-string v33, "DOUBLE" const/16 v34, 0xa const-string v35, "DOUBLE" move-object/from16 v32, v29 move-object/from16 v36, v6 move-object/from16 v37, v14 invoke-direct/range {v32 .. v37}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Lenums/TestEnumUsesOtherEnum;[Ljava/lang/String;)V sput-object v29, Lenums/TestEnumUsesOtherEnum;->s:Lenums/TestEnumUsesOtherEnum; new-instance v32, Lenums/TestEnumUsesOtherEnum; new-array v14, v10, [Ljava/lang/String; const-string v15, "SET" aput-object v15, v14, v13 const-string v37, "SET" const/16 v38, 0xb const-string v39, "SET" const-string v40, "a, b, c" const/16 v41, 0x0 const/16 v42, 0x0 const/16 v43, 0x0 move-object/from16 v36, v32 move-object/from16 v44, v14 invoke-direct/range {v36 .. v44}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V sput-object v32, Lenums/TestEnumUsesOtherEnum;->t:Lenums/TestEnumUsesOtherEnum; new-instance v33, Lenums/TestEnumUsesOtherEnum; new-array v14, v10, [Ljava/lang/String; const-string v15, "ENUM" aput-object v15, v14, v13 const-string v45, "ENUM" const/16 v46, 0xc const-string v47, "ENUM" const-string v48, "a, b, c" const/16 v49, 0x0 const/16 v50, 0x0 const/16 v51, 0x0 move-object/from16 v44, v33 move-object/from16 v52, v14 invoke-direct/range {v44 .. v52}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V sput-object v33, Lenums/TestEnumUsesOtherEnum;->u:Lenums/TestEnumUsesOtherEnum; new-instance v34, Lenums/TestEnumUsesOtherEnum; new-array v15, v10, [Ljava/lang/String; const-string v14, "DATE" aput-object v14, v15, v13 const-string v16, "DATE" const/16 v17, 0xd const-string v18, "DATE" const-string v19, "" move-object/from16 v14, v34 move-object/from16 v22, v15 move-object/from16 v15, v16 move/from16 v16, v17 move-object/from16 v17, v18 move-object/from16 v18, v19 move-object/from16 v19, v23 invoke-direct/range {v14 .. v22}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V sput-object v34, Lenums/TestEnumUsesOtherEnum;->v:Lenums/TestEnumUsesOtherEnum; new-instance v35, Lenums/TestEnumUsesOtherEnum; new-array v14, v10, [Ljava/lang/String; const-string v15, "TIME" aput-object v15, v14, v13 const-string v16, "TIME" const/16 v17, 0xe const-string v18, "TIME" move-object/from16 v15, v35 move-object/from16 v19, v34 move-object/from16 v20, v14 invoke-direct/range {v15 .. v20}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Lenums/TestEnumUsesOtherEnum;[Ljava/lang/String;)V sput-object v35, Lenums/TestEnumUsesOtherEnum;->w:Lenums/TestEnumUsesOtherEnum; new-instance v36, Lenums/TestEnumUsesOtherEnum; new-array v14, v11, [Ljava/lang/String; const-string v15, "DATETIME" aput-object v15, v14, v13 const-string v15, "DATETIME2" aput-object v15, v14, v10 const-string v15, "SMALLDATETIME" aput-object v15, v14, v8 const-string v15, "DATETIMEOFFSET" aput-object v15, v14, v7 const-string v15, "DATE/TIME" aput-object v15, v14, v24 const-string v16, "DATETIME" const/16 v17, 0xf const-string v18, "DATETIME" move-object/from16 v15, v36 move-object/from16 v20, v14 invoke-direct/range {v15 .. v20}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Lenums/TestEnumUsesOtherEnum;[Ljava/lang/String;)V sput-object v36, Lenums/TestEnumUsesOtherEnum;->x:Lenums/TestEnumUsesOtherEnum; new-instance v37, Lenums/TestEnumUsesOtherEnum; new-array v14, v10, [Ljava/lang/String; const-string v15, "TIMESTAMP" aput-object v15, v14, v13 const-string v16, "TIMESTAMP" const/16 v17, 0x10 const-string v18, "TIMESTAMP" move-object/from16 v15, v37 move-object/from16 v20, v14 invoke-direct/range {v15 .. v20}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Lenums/TestEnumUsesOtherEnum;[Ljava/lang/String;)V sput-object v37, Lenums/TestEnumUsesOtherEnum;->y:Lenums/TestEnumUsesOtherEnum; new-instance v38, Lenums/TestEnumUsesOtherEnum; new-array v15, v8, [Ljava/lang/String; const-string v14, "JSON" aput-object v14, v15, v13 const-string v14, "JSONB" aput-object v14, v15, v10 const-string v16, "JSON" const/16 v17, 0x11 const-string v18, "JSON" const/16 v19, 0x0 const/16 v20, 0x0 const/16 v21, 0x1 move-object/from16 v14, v38 move-object/from16 v22, v15 move-object/from16 v15, v16 move/from16 v16, v17 move-object/from16 v17, v18 move-object/from16 v18, v19 move-object/from16 v19, v23 invoke-direct/range {v14 .. v22}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V sput-object v38, Lenums/TestEnumUsesOtherEnum;->z:Lenums/TestEnumUsesOtherEnum; new-instance v39, Lenums/TestEnumUsesOtherEnum; new-array v15, v10, [Ljava/lang/String; const-string v14, "UUID" aput-object v14, v15, v13 const-string v16, "UUID" const/16 v17, 0x12 const-string v18, "UUID" const/16 v19, 0x0 move-object/from16 v14, v39 move-object/from16 v22, v15 move-object/from16 v15, v16 move/from16 v16, v17 move-object/from16 v17, v18 move-object/from16 v18, v19 move-object/from16 v19, v23 invoke-direct/range {v14 .. v22}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V sput-object v39, Lenums/TestEnumUsesOtherEnum;->A:Lenums/TestEnumUsesOtherEnum; const/16 v14, 0x13 new-array v14, v14, [Lenums/TestEnumUsesOtherEnum; aput-object v9, v14, v13 aput-object v30, v14, v10 aput-object v31, v14, v8 aput-object v0, v14, v7 aput-object v1, v14, v24 aput-object v2, v14, v11 aput-object v3, v14, v25 aput-object v4, v14, v12 const/16 v0, 0x8 aput-object v5, v14, v0 aput-object v6, v14, v26 aput-object v29, v14, v27 aput-object v32, v14, v28 const/16 v0, 0xc aput-object v33, v14, v0 const/16 v0, 0xd aput-object v34, v14, v0 const/16 v0, 0xe aput-object v35, v14, v0 const/16 v0, 0xf aput-object v36, v14, v0 const/16 v0, 0x10 aput-object v37, v14, v0 const/16 v0, 0x11 aput-object v38, v14, v0 const/16 v0, 0x12 aput-object v39, v14, v0 sput-object v14, Lenums/TestEnumUsesOtherEnum;->B:[Lenums/TestEnumUsesOtherEnum; return-void .end method .method private constructor (Ljava/lang/String;ILjava/lang/String;Lenums/TestEnumUsesOtherEnum;[Ljava/lang/String;)V .registers 15 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/lang/String;", "Lenums/TestEnumUsesOtherEnum;", "[", "Ljava/lang/String;", ")V" } .end annotation iget-object v4, p4, Lenums/TestEnumUsesOtherEnum;->d:Ljava/lang/String; iget-object v5, p4, Lenums/TestEnumUsesOtherEnum;->e:Ljava/lang/Integer; iget-boolean v6, p4, Lenums/TestEnumUsesOtherEnum;->f:Z iget-boolean v7, p4, Lenums/TestEnumUsesOtherEnum;->g:Z move-object v0, p0 move-object v1, p1 move v2, p2 move-object v3, p3 move-object v8, p5 invoke-direct/range {v0 .. v8}, Lenums/TestEnumUsesOtherEnum;->(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V return-void .end method .method private constructor (Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ZZ[Ljava/lang/String;)V .registers 9 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/lang/String;", "Ljava/lang/String;", "Ljava/lang/Integer;", "ZZ[", "Ljava/lang/String;", ")V" } .end annotation invoke-direct {p0, p1, p2}, Ljava/lang/Enum;->(Ljava/lang/String;I)V iput-object p3, p0, Lenums/TestEnumUsesOtherEnum;->c:Ljava/lang/String; iput-object p4, p0, Lenums/TestEnumUsesOtherEnum;->d:Ljava/lang/String; iput-object p5, p0, Lenums/TestEnumUsesOtherEnum;->e:Ljava/lang/Integer; iput-boolean p6, p0, Lenums/TestEnumUsesOtherEnum;->f:Z iput-boolean p7, p0, Lenums/TestEnumUsesOtherEnum;->g:Z iput-object p8, p0, Lenums/TestEnumUsesOtherEnum;->h:[Ljava/lang/String; return-void .end method .method public static valueOf(Ljava/lang/String;)Lenums/TestEnumUsesOtherEnum; .registers 2 const-class v0, Lenums/TestEnumUsesOtherEnum; invoke-static {v0, p0}, Ljava/lang/Enum;->valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; move-result-object p0 check-cast p0, Lenums/TestEnumUsesOtherEnum; return-object p0 .end method .method public static values()[Lenums/TestEnumUsesOtherEnum; .registers 1 sget-object v0, Lenums/TestEnumUsesOtherEnum;->B:[Lenums/TestEnumUsesOtherEnum; invoke-virtual {v0}, [Lenums/TestEnumUsesOtherEnum;->clone()Ljava/lang/Object; move-result-object v0 check-cast v0, [Lenums/TestEnumUsesOtherEnum; return-object v0 .end method # virtual methods .method public c()Z .registers 2 iget-object v0, p0, Lenums/TestEnumUsesOtherEnum;->e:Ljava/lang/Integer; if-eqz v0, :cond_d invoke-virtual {v0}, Ljava/lang/Integer;->intValue()I move-result v0 if-eqz v0, :cond_b goto :goto_d :cond_b const/4 v0, 0x0 goto :goto_e :cond_d :goto_d const/4 v0, 0x1 :goto_e return v0 .end method .method public toString()Ljava/lang/String; .registers 2 iget-object v0, p0, Lenums/TestEnumUsesOtherEnum;->c:Ljava/lang/String; return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/enums/TestEnumWithFields.smali ================================================ .class public final enum Lenums/TestEnumWithFields; .super Ljava/lang/Enum; # annotations .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Enum<", "Lenums/TestEnumWithFields;", ">;" } .end annotation # static fields .field public static final synthetic $VALUES:[Lenums/TestEnumWithFields; .field public static final DEFAULT:Lenums/TestEnumWithFields; .field public static final enum DISABLED:Lenums/TestEnumWithFields; .field public static final enum FIVE_SECONDS:Lenums/TestEnumWithFields; .field public static final MAX:Lenums/TestEnumWithFields; .field public static final enum TWO_AND_A_HALF_SECONDS:Lenums/TestEnumWithFields; .field public static final sValues:[Lenums/TestEnumWithFields; # instance fields .field public final mRawValue:I # direct methods .method public static constructor ()V .locals 6 .line 1 new-instance v0, Lenums/TestEnumWithFields; const/4 v1, 0x0 const-string v2, "DISABLED" invoke-direct {v0, v2, v1, v1}, Lenums/TestEnumWithFields;->(Ljava/lang/String;II)V sput-object v0, Lenums/TestEnumWithFields;->DISABLED:Lenums/TestEnumWithFields; .line 2 new-instance v0, Lenums/TestEnumWithFields; const/4 v2, 0x1 const-string v3, "TWO_AND_A_HALF_SECONDS" invoke-direct {v0, v3, v2, v2}, Lenums/TestEnumWithFields;->(Ljava/lang/String;II)V sput-object v0, Lenums/TestEnumWithFields;->TWO_AND_A_HALF_SECONDS:Lenums/TestEnumWithFields; .line 3 new-instance v0, Lenums/TestEnumWithFields; const/4 v3, 0x2 const-string v4, "FIVE_SECONDS" invoke-direct {v0, v4, v3, v3}, Lenums/TestEnumWithFields;->(Ljava/lang/String;II)V sput-object v0, Lenums/TestEnumWithFields;->FIVE_SECONDS:Lenums/TestEnumWithFields; const/4 v4, 0x3 new-array v4, v4, [Lenums/TestEnumWithFields; .line 4 sget-object v5, Lenums/TestEnumWithFields;->DISABLED:Lenums/TestEnumWithFields; aput-object v5, v4, v1 sget-object v1, Lenums/TestEnumWithFields;->TWO_AND_A_HALF_SECONDS:Lenums/TestEnumWithFields; aput-object v1, v4, v2 aput-object v0, v4, v3 sput-object v4, Lenums/TestEnumWithFields;->$VALUES:[Lenums/TestEnumWithFields; .line 5 sput-object v5, Lenums/TestEnumWithFields;->DEFAULT:Lenums/TestEnumWithFields; .line 6 sput-object v0, Lenums/TestEnumWithFields;->MAX:Lenums/TestEnumWithFields; .line 7 invoke-static {}, Lenums/TestEnumWithFields;->values()[Lenums/TestEnumWithFields; move-result-object v0 sput-object v0, Lenums/TestEnumWithFields;->sValues:[Lenums/TestEnumWithFields; return-void .end method .method public constructor (Ljava/lang/String;II)V .locals 0 .annotation system Ldalvik/annotation/Signature; value = { "(I)V" } .end annotation .line 1 invoke-direct {p0, p1, p2}, Ljava/lang/Enum;->(Ljava/lang/String;I)V iput p3, p0, Lenums/TestEnumWithFields;->mRawValue:I return-void .end method .method public static valueOf(Ljava/lang/String;)Lenums/TestEnumWithFields; .locals 1 .line 1 const-class v0, Lenums/TestEnumWithFields; invoke-static {v0, p0}, Ljava/lang/Enum;->valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; move-result-object p0 check-cast p0, Lenums/TestEnumWithFields; return-object p0 .end method .method public static values()[Lenums/TestEnumWithFields; .locals 1 .line 1 sget-object v0, Lenums/TestEnumWithFields;->$VALUES:[Lenums/TestEnumWithFields; invoke-virtual {v0}, [Lenums/TestEnumWithFields;->clone()Ljava/lang/Object; move-result-object v0 check-cast v0, [Lenums/TestEnumWithFields; return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/enums/TestEnums10.smali ================================================ .class public final enum Lenums/TestEnums10; .super Ljava/lang/Enum; .source "" .field public static final synthetic A02:[Lenums/TestEnums10; .field public static final enum A03:Lenums/TestEnums10; .field public static final enum A04:Lenums/TestEnums10; .field public static final enum A05:Lenums/TestEnums10; .field public static final enum A06:Lenums/TestEnums10; .field public static final enum A07:Lenums/TestEnums10; .field public static final enum A08:Lenums/TestEnums10; .field public static final enum A09:Lenums/TestEnums10; .field public static final enum A0A:Lenums/TestEnums10; .field public static final enum A0B:Lenums/TestEnums10; .field public static final enum A0C:Lenums/TestEnums10; .field public final A00:I .method public static constructor ()V .locals 33 const/16 v28, 0x0 const/16 v27, 0x1 const-string v3, "CONNECT" new-instance v26, Lenums/TestEnums10; move-object/from16 v2, v26 move/from16 v1, v28 move/from16 v0, v27 invoke-direct {v2, v3, v1, v0}, Lenums/TestEnums10;->(Ljava/lang/String;II)V sput-object v26, Lenums/TestEnums10;->A04:Lenums/TestEnums10; const/4 v4, 0x2 const-string v2, "CONNACK" new-instance v25, Lenums/TestEnums10; move-object/from16 v1, v25 invoke-direct {v1, v2, v0, v4}, Lenums/TestEnums10;->(Ljava/lang/String;II)V sput-object v25, Lenums/TestEnums10;->A03:Lenums/TestEnums10; const/4 v6, 0x3 const-string v1, "PUBLISH" new-instance v24, Lenums/TestEnums10; move-object/from16 v0, v24 invoke-direct {v0, v1, v4, v6}, Lenums/TestEnums10;->(Ljava/lang/String;II)V sput-object v24, Lenums/TestEnums10;->A08:Lenums/TestEnums10; const/4 v7, 0x4 const-string v1, "PUBACK" new-instance v23, Lenums/TestEnums10; move-object/from16 v0, v23 invoke-direct {v0, v1, v6, v7}, Lenums/TestEnums10;->(Ljava/lang/String;II)V sput-object v23, Lenums/TestEnums10;->A07:Lenums/TestEnums10; const/4 v8, 0x5 const-string v1, "PUBREC" new-instance v22, Lenums/TestEnums10; move-object/from16 v0, v22 invoke-direct {v0, v1, v7, v8}, Lenums/TestEnums10;->(Ljava/lang/String;II)V const/4 v9, 0x6 const-string v1, "PUBREL" new-instance v21, Lenums/TestEnums10; move-object/from16 v0, v21 invoke-direct {v0, v1, v8, v9}, Lenums/TestEnums10;->(Ljava/lang/String;II)V const/4 v10, 0x7 const-string v1, "PUBCOMP" new-instance v20, Lenums/TestEnums10; move-object/from16 v0, v20 invoke-direct {v0, v1, v9, v10}, Lenums/TestEnums10;->(Ljava/lang/String;II)V const/16 v11, 0x8 const-string v1, "SUBSCRIBE" new-instance v19, Lenums/TestEnums10; move-object/from16 v0, v19 invoke-direct {v0, v1, v10, v11}, Lenums/TestEnums10;->(Ljava/lang/String;II)V sput-object v19, Lenums/TestEnums10;->A0A:Lenums/TestEnums10; const/16 v12, 0x9 const-string v1, "SUBACK" new-instance v18, Lenums/TestEnums10; move-object/from16 v0, v18 invoke-direct {v0, v1, v11, v12}, Lenums/TestEnums10;->(Ljava/lang/String;II)V sput-object v18, Lenums/TestEnums10;->A09:Lenums/TestEnums10; const/16 v13, 0xa const-string v1, "UNSUBSCRIBE" new-instance v17, Lenums/TestEnums10; move-object/from16 v0, v17 invoke-direct {v0, v1, v12, v13}, Lenums/TestEnums10;->(Ljava/lang/String;II)V sput-object v17, Lenums/TestEnums10;->A0C:Lenums/TestEnums10; const/16 v14, 0xb const-string v0, "UNSUBACK" new-instance v5, Lenums/TestEnums10; invoke-direct {v5, v0, v13, v14}, Lenums/TestEnums10;->(Ljava/lang/String;II)V sput-object v5, Lenums/TestEnums10;->A0B:Lenums/TestEnums10; const/16 v15, 0xc const-string v0, "PINGREQ" new-instance v3, Lenums/TestEnums10; invoke-direct {v3, v0, v14, v15}, Lenums/TestEnums10;->(Ljava/lang/String;II)V sput-object v3, Lenums/TestEnums10;->A05:Lenums/TestEnums10; const/16 v2, 0xd const-string v0, "PINGRESP" new-instance v1, Lenums/TestEnums10; invoke-direct {v1, v0, v15, v2}, Lenums/TestEnums10;->(Ljava/lang/String;II)V sput-object v1, Lenums/TestEnums10;->A06:Lenums/TestEnums10; const/16 v15, 0xe const-string v0, "DISCONNECT" new-instance v16, Lenums/TestEnums10; move-object/from16 v29, v16 move-object/from16 v30, v0 move/from16 v31, v2 move/from16 v32, v15 invoke-direct/range {v29 .. v32}, Lenums/TestEnums10;->(Ljava/lang/String;II)V new-array v15, v15, [Lenums/TestEnums10; aput-object v26, v15, v28 aput-object v25, v15, v27 aput-object v24, v15, v4 aput-object v23, v15, v6 aput-object v22, v15, v7 aput-object v21, v15, v8 aput-object v20, v15, v9 aput-object v19, v15, v10 aput-object v18, v15, v11 aput-object v17, v15, v12 aput-object v5, v15, v13 aput-object v3, v15, v14 const/16 v0, 0xc aput-object v1, v15, v0 aput-object v16, v15, v2 sput-object v15, Lenums/TestEnums10;->A02:[Lenums/TestEnums10; return-void .end method .method public constructor (Ljava/lang/String;II)V .locals 0 invoke-direct {p0, p1, p2}, Ljava/lang/Enum;->(Ljava/lang/String;I)V iput p3, p0, Lenums/TestEnums10;->A00:I return-void .end method .method public static valueOf(Ljava/lang/String;)Lenums/TestEnums10; .locals 1 const-class v0, Lenums/TestEnums10; invoke-static {v0, p0}, Ljava/lang/Enum;->valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; move-result-object v0 check-cast v0, Lenums/TestEnums10; return-object v0 .end method .method public static values()[Lenums/TestEnums10; .locals 1 sget-object v0, Lenums/TestEnums10;->A02:[Lenums/TestEnums10; invoke-virtual {v0}, [Ljava/lang/Object;->clone()Ljava/lang/Object; move-result-object v0 check-cast v0, [Lenums/TestEnums10; return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/enums/TestEnums5.smali ================================================ .class final enum Lkotlin/collections/State; .super Ljava/lang/Enum; # annotations .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Enum<", "Lkotlin/collections/State;", ">;" } .end annotation # static fields .field private static final synthetic $VALUES:[Lkotlin/collections/State; .field public static final enum Done:Lkotlin/collections/State; .field public static final enum Failed:Lkotlin/collections/State; .field public static final enum NotReady:Lkotlin/collections/State; .field public static final enum Ready:Lkotlin/collections/State; # direct methods .method static constructor ()V .registers 4 const/4 v0, 0x4 new-array v0, v0, [Lkotlin/collections/State; new-instance v1, Lkotlin/collections/State; const-string v2, "Ready" const/4 v3, 0x0 invoke-direct {v1, v2, v3}, Lkotlin/collections/State;->(Ljava/lang/String;I)V sput-object v1, Lkotlin/collections/State;->Ready:Lkotlin/collections/State; aput-object v1, v0, v3 new-instance v1, Lkotlin/collections/State; const-string v2, "NotReady" const/4 v3, 0x1 invoke-direct {v1, v2, v3}, Lkotlin/collections/State;->(Ljava/lang/String;I)V sput-object v1, Lkotlin/collections/State;->NotReady:Lkotlin/collections/State; aput-object v1, v0, v3 new-instance v1, Lkotlin/collections/State; const-string v2, "Done" const/4 v3, 0x2 invoke-direct {v1, v2, v3}, Lkotlin/collections/State;->(Ljava/lang/String;I)V sput-object v1, Lkotlin/collections/State;->Done:Lkotlin/collections/State; aput-object v1, v0, v3 new-instance v1, Lkotlin/collections/State; const-string v2, "Failed" const/4 v3, 0x3 invoke-direct {v1, v2, v3}, Lkotlin/collections/State;->(Ljava/lang/String;I)V sput-object v1, Lkotlin/collections/State;->Failed:Lkotlin/collections/State; aput-object v1, v0, v3 sput-object v0, Lkotlin/collections/State;->$VALUES:[Lkotlin/collections/State; return-void .end method .method protected constructor (Ljava/lang/String;I)V .registers 3 .param p1, "$enum_name_or_ordinal$0" # Ljava/lang/String; .param p2, "$enum_name_or_ordinal$1" # I .annotation system Ldalvik/annotation/Signature; value = { "()V" } .end annotation .line 4 invoke-direct {p0, p1, p2}, Ljava/lang/Enum;->(Ljava/lang/String;I)V return-void .end method .method public static valueOf(Ljava/lang/String;)Lkotlin/collections/State; .registers 2 const-class v0, Lkotlin/collections/State; invoke-static {v0, p0}, Ljava/lang/Enum;->valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; move-result-object p0 check-cast p0, Lkotlin/collections/State; return-object p0 .end method .method public static values()[Lkotlin/collections/State; .registers 1 sget-object v0, Lkotlin/collections/State;->$VALUES:[Lkotlin/collections/State; invoke-virtual {v0}, [Lkotlin/collections/State;->clone()Ljava/lang/Object; move-result-object v0 check-cast v0, [Lkotlin/collections/State; return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/enums/TestEnums8.smali ================================================ .class public final enum Lenums/TestEnums8; .super Ljava/lang/Enum; .source "SourceFile" # annotations .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Enum<", "Lenums/TestEnums8;", ">;" } .end annotation # static fields .field private static final synthetic $VALUES:[Lenums/TestEnums8; .field private static final FOR_BITS:[Lenums/TestEnums8; .field public static final enum H:Lenums/TestEnums8; .field public static final enum L:Lenums/TestEnums8; .field public static final enum M:Lenums/TestEnums8; .field public static final enum Q:Lenums/TestEnums8; # instance fields .field private final bits:I # direct methods .method static constructor ()V .locals 10 .line 28 new-instance v0, Lenums/TestEnums8; const/4 v1, 0x1 const/4 v2, 0x0 const-string v3, "L" invoke-direct {v0, v3, v2, v1}, Lenums/TestEnums8;->(Ljava/lang/String;II)V sput-object v0, Lenums/TestEnums8;->L:Lenums/TestEnums8; .line 30 new-instance v0, Lenums/TestEnums8; const-string v3, "M" invoke-direct {v0, v3, v1, v2}, Lenums/TestEnums8;->(Ljava/lang/String;II)V sput-object v0, Lenums/TestEnums8;->M:Lenums/TestEnums8; .line 32 new-instance v0, Lenums/TestEnums8; const/4 v3, 0x3 const/4 v4, 0x2 const-string v5, "Q" invoke-direct {v0, v5, v4, v3}, Lenums/TestEnums8;->(Ljava/lang/String;II)V sput-object v0, Lenums/TestEnums8;->Q:Lenums/TestEnums8; .line 34 new-instance v0, Lenums/TestEnums8; const-string v5, "H" invoke-direct {v0, v5, v3, v4}, Lenums/TestEnums8;->(Ljava/lang/String;II)V sput-object v0, Lenums/TestEnums8;->H:Lenums/TestEnums8; const/4 v0, 0x4 new-array v5, v0, [Lenums/TestEnums8; .line 25 sget-object v6, Lenums/TestEnums8;->L:Lenums/TestEnums8; aput-object v6, v5, v2 sget-object v7, Lenums/TestEnums8;->M:Lenums/TestEnums8; aput-object v7, v5, v1 sget-object v8, Lenums/TestEnums8;->Q:Lenums/TestEnums8; aput-object v8, v5, v4 sget-object v9, Lenums/TestEnums8;->H:Lenums/TestEnums8; aput-object v9, v5, v3 sput-object v5, Lenums/TestEnums8;->$VALUES:[Lenums/TestEnums8; new-array v0, v0, [Lenums/TestEnums8; aput-object v7, v0, v2 aput-object v6, v0, v1 aput-object v9, v0, v4 aput-object v8, v0, v3 .line 36 sput-object v0, Lenums/TestEnums8;->FOR_BITS:[Lenums/TestEnums8; return-void .end method .method private constructor (Ljava/lang/String;II)V .locals 0 .annotation system Ldalvik/annotation/Signature; value = { "(I)V" } .end annotation .line 40 invoke-direct {p0, p1, p2}, Ljava/lang/Enum;->(Ljava/lang/String;I)V .line 41 iput p3, p0, Lenums/TestEnums8;->bits:I return-void .end method .method public static forBits(I)Lenums/TestEnums8; .locals 2 if-ltz p0, :cond_0 .line 53 sget-object v0, Lenums/TestEnums8;->FOR_BITS:[Lenums/TestEnums8; array-length v1, v0 if-ge p0, v1, :cond_0 .line 56 aget-object p0, v0, p0 return-object p0 .line 54 :cond_0 new-instance p0, Ljava/lang/IllegalArgumentException; invoke-direct {p0}, Ljava/lang/IllegalArgumentException;->()V throw p0 .end method .method public static valueOf(Ljava/lang/String;)Lenums/TestEnums8; .locals 1 .line 25 const-class v0, Lenums/TestEnums8; invoke-static {v0, p0}, Ljava/lang/Enum;->valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; move-result-object p0 check-cast p0, Lenums/TestEnums8; return-object p0 .end method .method public static values()[Lenums/TestEnums8; .locals 1 .line 25 sget-object v0, Lenums/TestEnums8;->$VALUES:[Lenums/TestEnums8; invoke-virtual {v0}, [Lenums/TestEnums8;->clone()Ljava/lang/Object; move-result-object v0 check-cast v0, [Lenums/TestEnums8; return-object v0 .end method # virtual methods .method public getBits()I .locals 1 .line 45 iget v0, p0, Lenums/TestEnums8;->bits:I return v0 .end method ================================================ FILE: jadx-core/src/test/smali/enums/TestEnumsWithStaticFields.smali ================================================ .class public final enum Lenums/TestEnumsWithStaticFields; .super Ljava/lang/Enum; .source "SourceFile" # interfaces .implements Lx/a/c; # annotations .annotation system Ldalvik/annotation/MemberClasses; value = { Lx/a/d$a; } .end annotation .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Enum", "<", "Lenums/TestEnumsWithStaticFields;", ">;", "Lx/a/c;" } .end annotation # static fields .field public static final enum sA:Lenums/TestEnumsWithStaticFields; .field private static sB:Lx/a/c; .field private static final synthetic sC:[Lenums/TestEnumsWithStaticFields; # direct methods .method static constructor ()V .registers 4 .prologue const v3, 0x23900 const/4 v2, 0x0 invoke-static {v3}, Lx/q;->i(I)V .line 10 new-instance v0, Lenums/TestEnumsWithStaticFields; const-string/jumbo v1, "INSTANCE" invoke-direct {v0, v1}, Lenums/TestEnumsWithStaticFields;->(Ljava/lang/String;)V sput-object v0, Lenums/TestEnumsWithStaticFields;->sA:Lenums/TestEnumsWithStaticFields; .line 9 const/4 v0, 0x1 new-array v0, v0, [Lenums/TestEnumsWithStaticFields; sget-object v1, Lenums/TestEnumsWithStaticFields;->sA:Lenums/TestEnumsWithStaticFields; aput-object v1, v0, v2 sput-object v0, Lenums/TestEnumsWithStaticFields;->sC:[Lenums/TestEnumsWithStaticFields; .line 36 new-instance v0, Lx/a/d$a; invoke-direct {v0, v2}, Lx/a/d$a;->(B)V sput-object v0, Lenums/TestEnumsWithStaticFields;->sB:Lx/a/c; invoke-static {v3}, Lx/q;->o(I)V return-void .end method .method private constructor (Ljava/lang/String;)V .registers 3 .annotation system Ldalvik/annotation/Signature; value = { "()V" } .end annotation .prologue .line 9 const/4 v0, 0x0 invoke-direct {p0, p1, v0}, Ljava/lang/Enum;->(Ljava/lang/String;I)V return-void .end method .method public static a(Lx/a/c;)V .registers 1 .prologue .line 79 if-eqz p0, :cond_4 .line 80 sput-object p0, Lenums/TestEnumsWithStaticFields;->sB:Lx/a/c; .line 82 :cond_4 return-void .end method .method public static valueOf(Ljava/lang/String;)Lenums/TestEnumsWithStaticFields; .registers 3 .prologue const v1, 0x238f8 invoke-static {v1}, Lx/q;->i(I)V .line 9 const-class v0, Lenums/TestEnumsWithStaticFields; invoke-static {v0, p0}, Ljava/lang/Enum;->valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; move-result-object v0 check-cast v0, Lenums/TestEnumsWithStaticFields; invoke-static {v1}, Lx/q;->o(I)V return-object v0 .end method .method public static values()[Lenums/TestEnumsWithStaticFields; .registers 2 .prologue const v1, 0x238f7 invoke-static {v1}, Lx/q;->i(I)V .line 9 sget-object v0, Lenums/TestEnumsWithStaticFields;->sC:[Lenums/TestEnumsWithStaticFields; invoke-virtual {v0}, [Lenums/TestEnumsWithStaticFields;->clone()Ljava/lang/Object; move-result-object v0 check-cast v0, [Lenums/TestEnumsWithStaticFields; invoke-static {v1}, Lx/q;->o(I)V return-object v0 .end method # virtual methods .method public final FR(I)V .registers 4 .prologue const v1, 0x238fb invoke-static {v1}, Lx/q;->i(I)V .line 96 sget-object v0, Lenums/TestEnumsWithStaticFields;->sB:Lx/a/c; invoke-interface {v0, p1}, Lx/a/c;->FR(I)V .line 97 invoke-static {v1}, Lx/q;->o(I)V return-void .end method ================================================ FILE: jadx-core/src/test/smali/enums/TestSwitchOverEnum/Count.smali ================================================ .class public final enum Lenums/TestSwitchOverEnum$Count; .super Ljava/lang/Enum; .field private static final synthetic $VALUES:[Lenums/TestSwitchOverEnum$Count; .field public static final enum ONE:Lenums/TestSwitchOverEnum$Count; .field public static final enum THREE:Lenums/TestSwitchOverEnum$Count; .field public static final enum TWO:Lenums/TestSwitchOverEnum$Count; .method private static synthetic $values()[Lenums/TestSwitchOverEnum$Count; .registers 3 const/4 v0, 0x3 new-array v0, v0, [Lenums/TestSwitchOverEnum$Count; const/4 v1, 0x0 sget-object v2, Lenums/TestSwitchOverEnum$Count;->ONE:Lenums/TestSwitchOverEnum$Count; aput-object v2, v0, v1 const/4 v1, 0x1 sget-object v2, Lenums/TestSwitchOverEnum$Count;->TWO:Lenums/TestSwitchOverEnum$Count; aput-object v2, v0, v1 const/4 v1, 0x2 sget-object v2, Lenums/TestSwitchOverEnum$Count;->THREE:Lenums/TestSwitchOverEnum$Count; aput-object v2, v0, v1 return-object v0 .end method .method static constructor ()V .registers 3 new-instance v0, Lenums/TestSwitchOverEnum$Count; const-string v1, "ONE" const/4 v2, 0x0 invoke-direct {v0, v1, v2}, Lenums/TestSwitchOverEnum$Count;->(Ljava/lang/String;I)V sput-object v0, Lenums/TestSwitchOverEnum$Count;->ONE:Lenums/TestSwitchOverEnum$Count; new-instance v0, Lenums/TestSwitchOverEnum$Count; const-string v1, "TWO" const/4 v2, 0x1 invoke-direct {v0, v1, v2}, Lenums/TestSwitchOverEnum$Count;->(Ljava/lang/String;I)V sput-object v0, Lenums/TestSwitchOverEnum$Count;->TWO:Lenums/TestSwitchOverEnum$Count; new-instance v0, Lenums/TestSwitchOverEnum$Count; const-string v1, "THREE" const/4 v2, 0x2 invoke-direct {v0, v1, v2}, Lenums/TestSwitchOverEnum$Count;->(Ljava/lang/String;I)V sput-object v0, Lenums/TestSwitchOverEnum$Count;->THREE:Lenums/TestSwitchOverEnum$Count; invoke-static {}, Lenums/TestSwitchOverEnum$Count;->$values()[Lenums/TestSwitchOverEnum$Count; move-result-object v0 sput-object v0, Lenums/TestSwitchOverEnum$Count;->$VALUES:[Lenums/TestSwitchOverEnum$Count; return-void .end method .method private constructor (Ljava/lang/String;I)V .registers 3 .annotation system Ldalvik/annotation/Signature; value = { "()V" } .end annotation invoke-direct {p0, p1, p2}, Ljava/lang/Enum;->(Ljava/lang/String;I)V return-void .end method .method public static valueOf(Ljava/lang/String;)Lenums/TestSwitchOverEnum$Count; .registers 2 const-class v0, Lenums/TestSwitchOverEnum$Count; invoke-static {v0, p0}, Ljava/lang/Enum;->valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; move-result-object v0 check-cast v0, Lenums/TestSwitchOverEnum$Count; return-object v0 .end method .method public static values()[Lenums/TestSwitchOverEnum$Count; .registers 1 sget-object v0, Lenums/TestSwitchOverEnum$Count;->$VALUES:[Lenums/TestSwitchOverEnum$Count; invoke-virtual {v0}, [Lenums/TestSwitchOverEnum$Count;->clone()Ljava/lang/Object; move-result-object v0 check-cast v0, [Lenums/TestSwitchOverEnum$Count; return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/enums/TestSwitchOverEnum/TestSwitchOverEnum.smali ================================================ .class public Lenums/TestSwitchOverEnum; .super Ljava/lang/Object; .method public test(Lenums/TestSwitchOverEnum$Count;)I .registers 3 .param p1, "v" invoke-virtual {p1}, Lenums/TestSwitchOverEnum$Count;->ordinal()I move-result v0 packed-switch v0, :pswitch_data const/4 v0, 0x0 :goto_8 return v0 :pswitch_9 const/4 v0, 0x1 goto :goto_8 :pswitch_b const/4 v0, 0x2 goto :goto_8 :pswitch_data .packed-switch 0x0 :pswitch_9 :pswitch_b .end packed-switch .end method ================================================ FILE: jadx-core/src/test/smali/fallback/TestFallbackManyNops.smali ================================================ .class public Lfallback/TestFallbackManyNops; .super Ljava/lang/Object; .method public static test()V .registers 1 nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop return-void .end method ================================================ FILE: jadx-core/src/test/smali/generics/TestClassSignature.smali ================================================ .class public abstract Lgenerics/TestClassSignature; .super Ljava/lang/Object; .source "SourceFile" # interfaces .implements Ljava/util/Iterator; # annotations .annotation system Ldalvik/annotation/Signature; value = { "", "Lgenerics/TestClassSignature<", "TT;>;" } .end annotation # instance fields .field public f:Ljava/lang/Object; .annotation system Ldalvik/annotation/Signature; value = { "TT;" } .end annotation .end field # direct methods .method public constructor (Ljava/lang/Object;)V .registers 2 .annotation system Ldalvik/annotation/Signature; value = { "(TT;)V" } .end annotation invoke-direct {p0}, Ljava/lang/Object;->()V iput-object p1, p0, Lgenerics/TestClassSignature;->f:Ljava/lang/Object; return-void .end method # virtual methods .method public abstract a(Ljava/lang/Object;)Ljava/lang/Object; .annotation system Ldalvik/annotation/Signature; value = { "(TT;)TT;" } .end annotation .end method .method public final hasNext()Z .registers 2 .line 1 iget-object v0, p0, Lgenerics/TestClassSignature;->f:Ljava/lang/Object; if-eqz v0, :cond_6 const/4 v0, 0x1 goto :goto_7 :cond_6 const/4 v0, 0x0 :goto_7 return v0 .end method .method public final next()Ljava/lang/Object; .registers 3 .annotation system Ldalvik/annotation/Signature; value = { "()TT;" } .end annotation invoke-virtual {p0}, Lgenerics/TestClassSignature;->hasNext()Z move-result v0 if-eqz v0, :cond_1b :try_start_6 iget-object v0, p0, Lgenerics/TestClassSignature;->f:Ljava/lang/Object; :try_end_8 .catchall {:try_start_6 .. :try_end_8} :catchall_11 iget-object v1, p0, Lgenerics/TestClassSignature;->f:Ljava/lang/Object; invoke-virtual {p0, v1}, Lgenerics/TestClassSignature;->a(Ljava/lang/Object;)Ljava/lang/Object; move-result-object v1 iput-object v1, p0, Lgenerics/TestClassSignature;->f:Ljava/lang/Object; return-object v0 :catchall_11 move-exception v0 iget-object v1, p0, Lgenerics/TestClassSignature;->f:Ljava/lang/Object; invoke-virtual {p0, v1}, Lgenerics/TestClassSignature;->a(Ljava/lang/Object;)Ljava/lang/Object; move-result-object v1 iput-object v1, p0, Lgenerics/TestClassSignature;->f:Ljava/lang/Object; throw v0 :cond_1b new-instance v0, Ljava/util/NoSuchElementException; invoke-direct {v0}, Ljava/util/NoSuchElementException;->()V throw v0 .end method .method public final remove()V .registers 2 new-instance v0, Ljava/lang/UnsupportedOperationException; invoke-direct {v0}, Ljava/lang/UnsupportedOperationException;->()V throw v0 .end method ================================================ FILE: jadx-core/src/test/smali/generics/TestMethodOverride.smali ================================================ .class public final Lgenerics/TestMethodOverride; .super Ljava/lang/Object; # interfaces .implements Landroid/os/Parcelable$Creator; # annotations .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Object;", "Landroid/os/Parcelable$Creator<", "Ljava/lang/String;", ">;" } .end annotation # direct methods .method public constructor ()V .registers 1 .line 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method # virtual methods .method public final synthetic createFromParcel(Landroid/os/Parcel;)Ljava/lang/Object; .registers 2 const/4 v0, 0x0 return-object v0 .end method .method public final synthetic newArray(I)[Ljava/lang/Object; .registers 2 .line 4 new-array p1, p1, [Ljava/lang/String; return-object p1 .end method ================================================ FILE: jadx-core/src/test/smali/generics/TestMissingGenericsTypes2.smali ================================================ .class public Lgenerics/TestMissingGenericsTypes2; .super Ljava/lang/Object; .source "TestMissingGenericsTypes2.java" # interfaces .implements Ljava/lang/Iterable; # annotations .annotation system Ldalvik/annotation/Signature; value = { "", "Ljava/lang/Object;", "Ljava/lang/Iterable<", "TT;>;" } .end annotation # direct methods .method public constructor ()V .registers 1 .local p0, "this":Lgenerics/TestMissingGenericsTypes2;, "Lgenerics/TestMissingGenericsTypes2;" invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method private doSomething(Ljava/lang/String;)V .registers 2 .param p1, "s" # Ljava/lang/String; .local p0, "this":Lgenerics/TestMissingGenericsTypes2;, "Lgenerics/TestMissingGenericsTypes2;" return-void .end method # virtual methods .method public iterator()Ljava/util/Iterator; .registers 2 .annotation system Ldalvik/annotation/Signature; value = { "()", "Ljava/util/Iterator<", "TT;>;" } .end annotation .local p0, "this":Lgenerics/TestMissingGenericsTypes2;, "Lgenerics/TestMissingGenericsTypes2;" const/4 v0, 0x0 return-object v0 .end method .method public test(Lgenerics/TestMissingGenericsTypes2;)V .registers 4 .annotation system Ldalvik/annotation/Signature; value = { "(", "Lgenerics/TestMissingGenericsTypes2<", "Ljava/lang/String;", ">;)V" } .end annotation .local p0, "this":Lgenerics/TestMissingGenericsTypes2;, "Lgenerics/TestMissingGenericsTypes2;" .local p1, "l":Lgenerics/TestMissingGenericsTypes2;, "Lgenerics/TestMissingGenericsTypes2;" invoke-virtual {p1}, Lgenerics/TestMissingGenericsTypes2;->iterator()Ljava/util/Iterator; move-result-object v0 # original: # .local v0, "i":Ljava/util/Iterator;, "Ljava/util/Iterator;" # manipulated: removed generic type .local v0, "i":Ljava/util/Iterator; :goto_4 invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z move-result v1 if-eqz v1, :cond_14 invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object; move-result-object v1 check-cast v1, Ljava/lang/String; .local v1, "s":Ljava/lang/String; invoke-direct {p0, v1}, Lgenerics/TestMissingGenericsTypes2;->doSomething(Ljava/lang/String;)V .end local v1 # "s":Ljava/lang/String; goto :goto_4 :cond_14 return-void .end method ================================================ FILE: jadx-core/src/test/smali/generics/TestSyntheticOverride/KotlinFunction1.smali ================================================ .class public interface abstract Lkotlin/jvm/functions/Function1; .super Ljava/lang/Object; .source "SourceFile" .implements Lkotlin/Function; .annotation system Ldalvik/annotation/Signature; value = { "", "Ljava/lang/Object;", "Lkotlin/Function<", "TR;>;" } .end annotation .method public abstract invoke(Ljava/lang/Object;)Ljava/lang/Object; .annotation system Ldalvik/annotation/Signature; value = { "(TP1;)TR;" } .end annotation .end method ================================================ FILE: jadx-core/src/test/smali/generics/TestSyntheticOverride/TestSyntheticOverride.smali ================================================ .class final Lgenerics/TestSyntheticOverride; .super Ljava/lang/Object; .implements Lkotlin/jvm/functions/Function1; .annotation system Ldalvik/annotation/Signature; value = { "Lkotlin/jvm/internal/Lambda;", "Lkotlin/jvm/functions/Function1<", "Ljava/lang/String;", "Lkotlin/Unit;", ">;" } .end annotation .field final synthetic this$0:Lgenerics/TestSyntheticOverrideParent; .method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object; .registers 2 check-cast p1, Ljava/lang/String; invoke-virtual {p0, p1}, Lgenerics/TestSyntheticOverride;->invoke(Ljava/lang/String;)V sget-object p1, Lkotlin/Unit;->INSTANCE:Lkotlin/Unit; return-object p1 .end method .method public final invoke(Ljava/lang/String;)V .registers 5 iget-object v0, p0, Lgenerics/TestSyntheticOverride;->this$0:Lgenerics/TestSyntheticOverrideParent; invoke-static {v0}, Lgenerics/TestSyntheticOverride;->access$getDialog$p(Lgenerics/TestSyntheticOverride;)Landroidx/appcompat/app/AlertDialog; move-result-object v0 if-nez v0, :cond_9 goto :goto_c :cond_9 invoke-virtual {v0}, Landroidx/appcompat/app/AppCompatDialog;->dismiss()V :goto_c iget-object v0, p0, Lgenerics/TestSyntheticOverride;->this$0:Lgenerics/TestSyntheticOverrideParent; invoke-virtual {v0}, Landroid/app/Activity;->getIntent()Landroid/content/Intent; move-result-object v1 const-string v2, "intent" invoke-static {v1, v2}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V invoke-static {v0, v1, p1}, Lgenerics/TestSyntheticOverride;->access$getChooserIntent(Lgenerics/TestSyntheticOverride;Landroid/content/Intent;Ljava/lang/String;)Landroid/content/Intent; move-result-object p1 invoke-virtual {v0, p1}, Landroid/app/Activity;->startActivity(Landroid/content/Intent;)V iget-object p1, p0, Lgenerics/TestSyntheticOverride;->this$0:Lgenerics/TestSyntheticOverrideParent; invoke-virtual {p1}, Landroid/app/Activity;->finish()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestGetterInlineNegative.smali ================================================ .class public Linline/TestGetterInlineNegative; .super Ljava/lang/Object; .field public static final field:Ljava/lang/String; = "some string" .method public static synthetic getter()Ljava/lang/String; .locals 1 sget-object v0, Linline/TestGetterInlineNegative;->field:Ljava/lang/String; return-object v0 .end method .method public test()V .locals 1 invoke-static {}, Linline/TestGetterInlineNegative;->getter()Ljava/lang/String; return-void .end method .method public test2()Ljava/lang/String; .locals 2 invoke-static {}, Linline/TestGetterInlineNegative;->getter()Ljava/lang/String; move-result-object v1 return-object v1 .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestInline7.smali ================================================ .class public Linline/TestInline7; .super Ljava/lang/Object; .method public onViewCreated(Landroid/view/View;Landroid/os/Bundle;)V .locals 4 .param p2 # Landroid/os/Bundle; .annotation build Landroid/support/annotation/Nullable; .end annotation .end param .line 42 invoke-super {p0, p1, p2}, Lapp/navigation/fragment/NodeFragment;->onViewCreated(Landroid/view/View;Landroid/os/Bundle;)V .line 43 sget p2, Lapp/wallet/R$id;->done_button_early_release_failure:I invoke-virtual {p1, p2}, Landroid/view/View;->findViewById(I)Landroid/view/View; move-result-object p2 new-instance v0, Lapp/common/utils/SafeClickListener; invoke-direct {v0, p0}, Lapp/common/utils/SafeClickListener;->(Lapp/common/utils/ISafeClickVerifierListener;)V invoke-virtual {p2, v0}, Landroid/view/View;->setOnClickListener(Landroid/view/View$OnClickListener;)V .line 44 invoke-virtual {p0}, Linline/TestInline7;->getArguments()Landroid/os/Bundle; move-result-object p2 if-eqz p2, :cond_0 .line 46 sget v0, Lapp/wallet/R$id;->summary_content_early_release_failure:I invoke-virtual {p1, v0}, Landroid/view/View;->findViewById(I)Landroid/view/View; move-result-object p1 check-cast p1, Landroid/widget/TextView; sget v0, Lapp/wallet/R$string;->withdraw_id_capture_failure_content:I const/4 v1, 0x2 new-array v1, v1, [Ljava/lang/Object; const/4 v2, 0x0 const-string v3, "withdrawAmount" invoke-virtual {p2, v3}, Landroid/os/Bundle;->getString(Ljava/lang/String;)Ljava/lang/String; move-result-object v3 aput-object v3, v1, v2 const/4 v2, 0x1 const-string v3, "withdrawHoldTime" invoke-virtual {p2, v3}, Landroid/os/Bundle;->getString(Ljava/lang/String;)Ljava/lang/String; move-result-object p2 aput-object p2, v1, v2 invoke-virtual {p0, v0, v1}, Linline/TestInline7;->getString(I[Ljava/lang/Object;)Ljava/lang/String; move-result-object p2 invoke-virtual {p1, p2}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V :cond_0 return-void .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestInstanceLambda/Lambda.smali ================================================ .class public Linline/Lambda$1; .super Ljava/lang/Object; .implements Ljava/util/function/Function; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Object;", "Ljava/util/function/Function", ";" } .end annotation .field public static final INSTANCE:Linline/Lambda$1; .method static constructor ()V .registers 1 new-instance v0, Linline/Lambda$1; invoke-direct {v0}, Linline/Lambda$1;->()V sput-object v0, Linline/Lambda$1;->INSTANCE:Linline/Lambda$1; return-void .end method .method private constructor ()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public final apply(Ljava/lang/Object;)Ljava/lang/Object; .registers 2 .annotation system Ldalvik/annotation/Signature; value = { "(TT;)TT;" } .end annotation return-object p1 .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestInstanceLambda/TestCls.smali ================================================ .class public Linline/TestCls; .super Ljava/lang/Object; .method public test(Ljava/util/List;)Ljava/util/Map; .registers 3 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/util/List", "<+TT;>;)", "Ljava/util/Map", ";" } .end annotation sget-object v0, Linline/Lambda$1;->INSTANCE:Linline/Lambda$1; invoke-static {p1, v0}, Linline/TestCls;->toMap(Ljava/util/List;Ljava/util/function/Function;)Ljava/util/Map; move-result-object v0 return-object v0 .end method .method private static toMap(Ljava/util/List;Ljava/util/function/Function;)Ljava/util/Map; .registers 4 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/util/List", "<+TT;>;", "Ljava/util/function/Function", ";)", "Ljava/util/Map", ";" } .end annotation const/4 v0, 0x0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestMethodInline/A.smali ================================================ .class public Linline/A; .super Ljava/lang/Object; .method public static useMth()V .locals 1 invoke-static {}, Linline/other/B;->bridgeMth()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestMethodInline/B.smali ================================================ .class public Linline/other/B; .super Ljava/lang/Object; .method public static bridge synthetic bridgeMth()V .locals 1 invoke-static {}, Linline/other/C;->test()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestMethodInline/C.smali ================================================ .class Linline/other/C; # package-private .super Ljava/lang/Object; .method public static test()V .locals 1 return-void .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestOverlapSyntheticMethods.smali ================================================ .class public Linline/TestOverlapSyntheticMethods; .super Ljava/lang/Object; .method public synthetic a(I)I .registers 2 return p1 .end method .method public synthetic a(I)Ljava/lang/String; .registers 4 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;->()V const-string v1, "i:" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v0 invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; move-result-object v0 invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v0 return-object v0 .end method .method public test(I)Ljava/lang/String; .registers 4 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;->()V invoke-virtual {p0, p1}, Linline/TestOverlapSyntheticMethods;->a(I)I move-result v1 invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; move-result-object v0 const-string v1, "|" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v0 invoke-virtual {p0, p1}, Linline/TestOverlapSyntheticMethods;->a(I)Ljava/lang/String; move-result-object v1 invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v0 invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestOverrideBridgeMerge.smali ================================================ .class public Linline/TestOverrideBridgeMerge; .super Ljava/lang/Object; .implements Ljava/util/function/Function; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Object;", "Ljava/util/function/Function", "<", "Ljava/lang/String;", "Ljava/lang/Integer;", ">;" } .end annotation .method public constructor ()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public bridge synthetic apply(Ljava/lang/Object;)Ljava/lang/Object; .registers 3 check-cast p1, Ljava/lang/String; invoke-virtual {p0, p1}, Linline/TestOverrideBridgeMerge;->test(Ljava/lang/String;)Ljava/lang/Integer; move-result-object v0 return-object v0 .end method .method public test(Ljava/lang/String;)Ljava/lang/Integer; .registers 3 .param p1, "str" # Ljava/lang/String; invoke-virtual {p1}, Ljava/lang/String;->length()I move-result v0 invoke-static {v0}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer; move-result-object v0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestSyntheticClassInline/A.smali ================================================ .class Linline/A; .super Ljava/lang/Object; .source "TestJavaClass.java" .method constructor ()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method static synthetic lambda$test$0(JJ)Ljava/lang/Long; .registers 8 .param p0, "x1" # J .param p2, "x2" # J invoke-static {}, Ljava/lang/System;->currentTimeMillis()J move-result-wide v0 .local v0, "y1":J add-long v2, p0, v0 add-long/2addr v2, p2 invoke-static {v2, v3}, Ljava/lang/Long;->valueOf(J)Ljava/lang/Long; move-result-object v2 return-object v2 .end method .method static test(JJ)Ljava/util/function/Supplier; .registers 5 .param p0, "x1" # J .param p2, "x2" # J .annotation system Ldalvik/annotation/Signature; value = { "(JJ)", "Ljava/util/function/Supplier<", "Ljava/lang/Long;", ">;" } .end annotation new-instance v0, Linline/A$$ExternalSyntheticLambda0; invoke-direct {v0, p0, p1, p2, p3}, Linline/A$$ExternalSyntheticLambda0;->(JJ)V return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestSyntheticClassInline/B.smali ================================================ .class public final synthetic Linline/A$$ExternalSyntheticLambda0; .super Ljava/lang/Object; .source "D8$$SyntheticClass" .implements Ljava/util/function/Supplier; .field public final synthetic f$0:J .field public final synthetic f$1:J .method public synthetic constructor (JJ)V .registers 5 invoke-direct {p0}, Ljava/lang/Object;->()V iput-wide p1, p0, Linline/A$$ExternalSyntheticLambda0;->f$0:J iput-wide p3, p0, Linline/A$$ExternalSyntheticLambda0;->f$1:J return-void .end method .method public final get()Ljava/lang/Object; .registers 5 iget-wide v0, p0, Linline/A$$ExternalSyntheticLambda0;->f$0:J iget-wide v2, p0, Linline/A$$ExternalSyntheticLambda0;->f$1:J invoke-static {v0, v1, v2, v3}, Linline/A;->lambda$test$0(JJ)Ljava/lang/Long; move-result-object v0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestSyntheticInline3/KotlinFunction1.smali ================================================ .class public interface abstract Lkotlin/jvm/functions/Function1; .super Ljava/lang/Object; .source "SourceFile" .implements Lkotlin/Function; .annotation system Ldalvik/annotation/Signature; value = { "", "Ljava/lang/Object;", "Lkotlin/Function<", "TR;>;" } .end annotation .method public abstract invoke(Ljava/lang/Object;)Ljava/lang/Object; .annotation system Ldalvik/annotation/Signature; value = { "(TP1;)TR;" } .end annotation .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestSyntheticInline3/TestSyntheticInline3$onCreate$1.smali ================================================ .class final Linline/TestSyntheticInline3$onCreate$1; .super Lkotlin/jvm/internal/Lambda; .implements Lkotlin/jvm/functions/Function1; .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x18 name = null .end annotation .annotation system Ldalvik/annotation/Signature; value = { "Lkotlin/jvm/internal/Lambda;", "Lkotlin/jvm/functions/Function1<", "Ljava/lang/String;", "Lkotlin/Unit;", ">;" } .end annotation .field final synthetic this$0:Linline/TestSyntheticInline3; .method constructor (Linline/TestSyntheticInline3;)V .registers 2 iput-object p1, p0, Linline/TestSyntheticInline3$onCreate$1;->this$0:Linline/TestSyntheticInline3; const/4 p1, 0x1 invoke-direct {p0, p1}, Lkotlin/jvm/internal/Lambda;->(I)V return-void .end method .method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object; .registers 2 check-cast p1, Ljava/lang/String; invoke-virtual {p0, p1}, Linline/TestSyntheticInline3$onCreate$1;->invoke(Ljava/lang/String;)V sget-object p1, Lkotlin/Unit;->INSTANCE:Lkotlin/Unit; return-object p1 .end method .method public final invoke(Ljava/lang/String;)V .registers 5 iget-object v0, p0, Linline/TestSyntheticInline3$onCreate$1;->this$0:Linline/TestSyntheticInline3; invoke-static {v0}, Linline/TestSyntheticInline3;->access$getDialog$p(Linline/TestSyntheticInline3;)Landroidx/appcompat/app/AlertDialog; move-result-object v0 if-nez v0, :cond_9 goto :goto_c :cond_9 invoke-virtual {v0}, Landroidx/appcompat/app/AppCompatDialog;->dismiss()V :goto_c iget-object v0, p0, Linline/TestSyntheticInline3$onCreate$1;->this$0:Linline/TestSyntheticInline3; invoke-virtual {v0}, Landroid/app/Activity;->getIntent()Landroid/content/Intent; move-result-object v1 const-string v2, "intent" invoke-static {v1, v2}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V invoke-static {v0, v1, p1}, Linline/TestSyntheticInline3;->access$getChooserIntent(Linline/TestSyntheticInline3;Landroid/content/Intent;Ljava/lang/String;)Landroid/content/Intent; move-result-object p1 invoke-virtual {v0, p1}, Landroid/app/Activity;->startActivity(Landroid/content/Intent;)V iget-object p1, p0, Linline/TestSyntheticInline3$onCreate$1;->this$0:Linline/TestSyntheticInline3; invoke-virtual {p1}, Landroid/app/Activity;->finish()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/inline/TestSyntheticInline3/TestSyntheticInline3.smali ================================================ .class public final Linline/TestSyntheticInline3; .super Landroid/app/Activity; .field private dialog:Landroidx/appcompat/app/AlertDialog; .method public static final synthetic access$getChooserIntent(Linline/TestSyntheticInline3;Landroid/content/Intent;Ljava/lang/String;)Landroid/content/Intent; .registers 3 invoke-direct {p0, p1, p2}, Linline/TestSyntheticInline3;->getChooserIntent(Landroid/content/Intent;Ljava/lang/String;)Landroid/content/Intent; move-result-object p0 return-object p0 .end method .method public static final synthetic access$getDialog$p(Linline/TestSyntheticInline3;)Landroidx/appcompat/app/AlertDialog; .registers 1 iget-object p0, p0, Linline/TestSyntheticInline3;->dialog:Landroidx/appcompat/app/AlertDialog; return-object p0 .end method .method protected onCreate(Landroid/os/Bundle;)V .registers 3 invoke-super {p0, p1}, Landroidx/fragment/app/FragmentActivity;->onCreate(Landroid/os/Bundle;)V new-instance v0, Linline/TestSyntheticInline3$onCreate$1; invoke-direct {v0, p0}, Linline/TestSyntheticInline3$onCreate$1;->(Linline/TestSyntheticInline3;)V return-void .end method .method private final getChooserIntent(Landroid/content/Intent;Ljava/lang/String;)Landroid/content/Intent; .registers 5 new-instance v0, Landroid/content/Intent; invoke-direct {v0, p1}, Landroid/content/Intent;->(Landroid/content/Intent;)V return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestAnonymousClass14/OuterCls$1.smali ================================================ .class Linner/OuterCls$1; .super Ljava/lang/Thread; .source "SourceFile" .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x0 name = null .end annotation .field final synthetic this$0:Linner/OuterCls; # direct methods .method constructor (Linner/OuterCls;Ljava/lang/Runnable;)V .locals 0 iput-object p1, p0, Linner/OuterCls$1;->this$0:Linner/OuterCls; return-void .end method # virtual methods .method public someMethod()V .locals 3 return-void .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestAnonymousClass14/OuterCls$TestCls.smali ================================================ .class Linner/OuterCls$TestCls; .super Ljava/lang/Object; .source "SourceFile" # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Linner/OuterCls; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x0 name = "TestCls" .end annotation .field final synthetic this$0:Linner/OuterCls; # direct methods .method private constructor (Linner/OuterCls;)V .locals 0 iput-object p1, p0, Linner/OuterCls$TestCls;->this$0:Linner/OuterCls; new-instance p1, Ljava/util/ArrayList; invoke-direct {p1}, Ljava/util/ArrayList;->()V return-void .end method .method synthetic constructor (Linner/OuterCls;Linner/OuterCls$1;)V .locals 0 invoke-direct {p0, p1}, Linner/OuterCls$TestCls;->(Linner/OuterCls;)V return-void .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestAnonymousClass14/OuterCls.smali ================================================ .class public Linner/OuterCls; .super Ljava/lang/Object; .source "SourceFile" # interfaces .implements Ljava/lang/Runnable; # annotations .annotation system Ldalvik/annotation/MemberClasses; value = { Linner/OuterCls$TestCls; } .end annotation # direct methods .method static constructor ()V .locals 0 return-void .end method .method public constructor ()V .locals 1 return-void .end method .method public makeTestCls()V .locals 2 new-instance v1, Linner/OuterCls$TestCls; const/4 v0, 0x0 invoke-direct {v1, p0, v0}, Linner/OuterCls$TestCls;->(Linner/OuterCls;Linner/OuterCls$1;)V return-void .end method .method public makeAnonymousCls()V .locals 2 new-instance v1, Linner/OuterCls$1; invoke-direct {v1, p0, p0}, Linner/OuterCls$1;->(Linner/OuterCls;Ljava/lang/Runnable;)V invoke-direct {p0, v1}, Linner/OuterCls;->use(Ljava/lang/Thread;)V return-void .end method .method public run()V .locals 2 return-void .end method .method public use(Ljava/lang/Thread;)V .locals 2 return-void .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestAnonymousClass19/ATestCls.smali ================================================ .class public Linner/ATestCls; .super Ljava/lang/Object; .method public constructor ()V .registers 1 .prologue .line 11 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method # virtual methods .method public test(ZZ)V .registers 5 .param p1, "a" # Z .param p2, "b" # Z .prologue .line 14 if-eqz p1, :cond_e if-eqz p2, :cond_e const/4 v0, 0x1 .line 15 .local v0, "c":Z :goto_5 new-instance v1, Linner/Lambda$TestCls$1; invoke-direct {v1, p0, p1, p2, v0}, Linner/Lambda$TestCls$1;->(Linner/ATestCls;ZZZ)V invoke-virtual {p0, v1}, Linner/ATestCls;->use(Ljava/lang/Runnable;)V .line 21 return-void .line 14 .end local v0 # "c":Z :cond_e const/4 v0, 0x0 goto :goto_5 .end method .method public use(Ljava/lang/Runnable;)V .registers 2 return-void .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestAnonymousClass19/Lambda$TestCls$1.smali ================================================ .class public final synthetic Linner/Lambda$TestCls$1; .super Ljava/lang/Object; .implements Ljava/lang/Runnable; .field final synthetic this$0:Linner/ATestCls; .field final synthetic val$a:Z .field final synthetic val$b:Z .field final synthetic val$c:Z .method constructor (Linner/ATestCls;ZZZ)V .registers 5 .param p1, "this$0" .annotation system Ldalvik/annotation/Signature; value = { "()V" } .end annotation .prologue .line 15 iput-object p1, p0, Linner/Lambda$TestCls$1;->this$0:Linner/ATestCls; iput-boolean p2, p0, Linner/Lambda$TestCls$1;->val$a:Z iput-boolean p3, p0, Linner/Lambda$TestCls$1;->val$b:Z iput-boolean p4, p0, Linner/Lambda$TestCls$1;->val$c:Z invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public run()V .registers 4 .prologue .line 18 sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; new-instance v1, Ljava/lang/StringBuilder; invoke-direct {v1}, Ljava/lang/StringBuilder;->()V iget-boolean v2, p0, Linner/Lambda$TestCls$1;->val$a:Z invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder; move-result-object v1 const-string v2, " && " invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1 iget-boolean v2, p0, Linner/Lambda$TestCls$1;->val$b:Z invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder; move-result-object v1 const-string v2, " = " invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1 iget-boolean v2, p0, Linner/Lambda$TestCls$1;->val$c:Z invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder; move-result-object v1 invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v1 invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V .line 19 return-void .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestIncorrectAnonymousClass/TestCls$1.smali ================================================ .class public final Linner/TestCls$1; .super Ljava/lang/Object; # direct methods .method public constructor ()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method # virtual methods .method public invoke()V .registers 2 new-instance v0, Linner/TestCls$1; invoke-direct {v0}, Linner/TestCls$1;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestIncorrectAnonymousClass/TestCls.smali ================================================ .class public Linner/TestCls; .super Ljava/lang/Object; # direct methods .method public constructor ()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public test()V .registers 2 new-instance v0, Linner/TestCls$1; invoke-direct {v0}, Linner/TestCls$1;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestInnerClassFakeSyntheticConstructor.smali ================================================ .class public Ljadx/tests/inner/TestCls; .super Ljava/lang/Object; # direct methods .method public synthetic constructor (Ljava/lang/String;)V .registers 3 .param p1, "a" # Ljava/lang/String; .prologue const/4 v0, 0x1 invoke-direct {p0, p1, v0}, Ljadx/tests/inner/TestCls;->(Ljava/lang/String;Z)V return-void .end method .method public constructor (Ljava/lang/String;Z)V .registers 3 .param p1, "a" # Ljava/lang/String; .param p2, "b" # Z .prologue invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public static build(Ljava/lang/String;)Ljadx/tests/inner/TestCls; .registers 2 .param p0, "str" # Ljava/lang/String; .prologue new-instance v0, Ljadx/tests/inner/TestCls; invoke-direct {v0, p0}, Ljadx/tests/inner/TestCls;->(Ljava/lang/String;)V return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestInnerClassSyntheticRename.smali ================================================ .class Linner/TestInnerClassSyntheticRename; .super Landroid/os/AsyncTask; # annotations .annotation system Ldalvik/annotation/Signature; value = { "Landroid/os/AsyncTask<", "Landroid/net/Uri;", "Landroid/net/Uri;", "Ljava/util/List<", "Landroid/net/Uri;", ">;>;" } .end annotation # direct methods .method private constructor (Lcom/github/skylot/testasync/MainActivity;)V .locals 0 invoke-direct {p0}, Landroid/os/AsyncTask;->()V return-void .end method # virtual methods .method protected varargs a([Landroid/net/Uri;)Ljava/util/List; .locals 1 .annotation system Ldalvik/annotation/Signature; value = { "([", "Landroid/net/Uri;", ")", "Ljava/util/List<", "Landroid/net/Uri;", ">;" } .end annotation const-string p1, "TestCls" const-string v0, "doInBackground" invoke-static {p1, v0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I const/4 p1, 0x0 return-object p1 .end method .method protected a(Ljava/util/List;)V .locals 1 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/util/List<", "Landroid/net/Uri;", ">;)V" } .end annotation const-string p1, "TestCls" const-string v0, "onPostExecute" invoke-static {p1, v0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I return-void .end method .method protected synthetic doInBackground([Ljava/lang/Object;)Ljava/lang/Object; .locals 0 check-cast p1, [Landroid/net/Uri; invoke-virtual {p0, p1}, Linner/TestInnerClassSyntheticRename;->a([Landroid/net/Uri;)Ljava/util/List; move-result-object p1 return-object p1 .end method .method protected synthetic onPostExecute(Ljava/lang/Object;)V .locals 0 check-cast p1, Ljava/util/List; invoke-virtual {p0, p1}, Linner/TestInnerClassSyntheticRename;->a(Ljava/util/List;)V return-void .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestNestedAnonymousClass/A.smali ================================================ .class public Linner/A; .super Ljava/lang/Object; .method public constructor ()V .registers 1 .prologue invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public test()V .registers 2 .prologue new-instance v0, Linner/B; invoke-direct {v0, p0}, Linner/B;->(Linner/A;)V invoke-virtual {p0, v0}, Linner/A;->use(Ljava/util/concurrent/Callable;)V return-void .end method .method public use(Ljava/util/concurrent/Callable;)V .registers 2 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/util/concurrent/Callable", "<", "Ljava/lang/Runnable;", ">;)V" } .end annotation .prologue return-void .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestNestedAnonymousClass/B.smali ================================================ .class synthetic Linner/B; .super Ljava/lang/Object; .implements Ljava/util/concurrent/Callable; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Object;", "Ljava/util/concurrent/Callable", "<", "Ljava/lang/Runnable;", ">;" } .end annotation .field final synthetic this$0:Linner/A; .method constructor (Linner/A;)V .registers 2 .prologue iput-object p1, p0, Linner/B;->this$0:Linner/A; invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public bridge synthetic call()Ljava/lang/Object; .registers 2 .annotation system Ldalvik/annotation/Throws; value = { Ljava/lang/Exception; } .end annotation .prologue invoke-virtual {p0}, Linner/B;->call()Ljava/lang/Runnable; move-result-object v0 return-object v0 .end method .method public call()Ljava/lang/Runnable; .registers 2 .prologue new-instance v0, Linner/C; invoke-direct {v0, p0}, Linner/C;->(Linner/B;)V return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestNestedAnonymousClass/C.smali ================================================ .class synthetic Linner/C; .super Ljava/lang/Object; .implements Ljava/lang/Runnable; .field final synthetic this$1:Linner/B; .method constructor (Linner/B;)V .registers 2 .prologue iput-object p1, p0, Linner/C;->this$1:Linner/B; invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public run()V .registers 3 .prologue sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; const-string v1, "run" invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V return-void .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestSyntheticMthRename/TestCls$A.smali ================================================ .class public final Linner/TestCls$A; .super Ljava/lang/Object; .source "TestCls.java" # interfaces .implements Linner/TestCls$I; # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Linner/TestCls; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x19 name = "A" .end annotation .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Object;", "Linner/TestCls$I", "<", "Ljava/lang/String;", "Ljava/lang/Runnable;", ">;" } .end annotation # direct methods .method public constructor ()V .registers 1 .prologue .line 9 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method private varargs renamedCall([Ljava/lang/Runnable;)Ljava/lang/String; .registers 3 .param p1, "p" # [Ljava/lang/Runnable; .prologue .line 12 const-string v0, "str" return-object v0 .end method # virtual methods .method public synthetic call([Ljava/lang/Object;)Ljava/lang/Object; .registers 3 .prologue .line 9 check-cast p1, [Ljava/lang/Runnable; invoke-virtual {p0, p1}, Linner/TestCls$A;->renamedCall([Ljava/lang/Runnable;)Ljava/lang/String; move-result-object v0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestSyntheticMthRename/TestCls$I.smali ================================================ .class public interface abstract Linner/TestCls$I; .super Ljava/lang/Object; .source "TestCls.java" # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Linner/TestCls; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x609 name = "I" .end annotation .annotation system Ldalvik/annotation/Signature; value = { "", "Ljava/lang/Object;" } .end annotation # virtual methods .method public varargs abstract call([Ljava/lang/Object;)Ljava/lang/Object; .annotation system Ldalvik/annotation/Signature; value = { "([TP;)TR;" } .end annotation .end method ================================================ FILE: jadx-core/src/test/smali/inner/TestSyntheticMthRename/TestCls.smali ================================================ .class public Linner/TestCls; .super Ljava/lang/Object; .source "TestCls.java" # annotations .annotation system Ldalvik/annotation/MemberClasses; value = { Linner/TestCls$A;, Linner/TestCls$I; } .end annotation # direct methods .method public constructor ()V .registers 1 .prologue .line 3 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/invoke/TestCastInOverloadedInvoke2.smali ================================================ .class public Linvoke/TestCastInOverloadedInvoke2; .super Ljava/lang/Object; .method public test()V .locals 3 new-instance v0, Landroid/content/Intent; invoke-direct {v0}, Landroid/content/Intent;->()V const-string v1, "param" const/4 v2, 0x0 invoke-virtual {v0, v1, v2}, Landroid/content/Intent;->putExtra(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent; return-void .end method ================================================ FILE: jadx-core/src/test/smali/invoke/TestConstructorWithMoves.smali ================================================ .class public Linvoke/TestConstructorWithMoves; .super Ljava/lang/Object; .method static public test()Z .registers 11 new-instance v5, Ljava/lang/Boolean; move-object v8, v5 move-object v5, v8 move-object v6, v8 const-string v7, "test" invoke-direct {v6, v7}, Ljava/lang/Boolean;->(Ljava/lang/String;)V check-cast v5, Ljava/lang/Boolean; invoke-virtual {v5}, Ljava/lang/Boolean;->booleanValue()Z move-result v5 move v3, v5 return v3 .end method ================================================ FILE: jadx-core/src/test/smali/invoke/TestPolymorphicInvoke.smali ================================================ .class public Linvoke/TestPolymorphicInvoke; .super Ljava/lang/Object; .method public func(II)Ljava/lang/String; .registers 4 .param p1, "a" # I .param p2, "c" # I .line 23 add-int v0, p1, p2 invoke-static {v0}, Ljava/lang/String;->valueOf(I)Ljava/lang/String; move-result-object v0 return-object v0 .end method .method public test()V .registers 7 .line 32 :try_start_0 invoke-static {}, Ljava/lang/invoke/MethodHandles;->lookup()Ljava/lang/invoke/MethodHandles$Lookup; move-result-object v0 .line 33 .local v0, "lookup":Ljava/lang/invoke/MethodHandles$Lookup; const-class v1, Ljava/lang/String; sget-object v2, Ljava/lang/Integer;->TYPE:Ljava/lang/Class; const/4 v3, 0x1 new-array v3, v3, [Ljava/lang/Class; const/4 v4, 0x0 sget-object v5, Ljava/lang/Integer;->TYPE:Ljava/lang/Class; aput-object v5, v3, v4 invoke-static {v1, v2, v3}, Ljava/lang/invoke/MethodType;->methodType(Ljava/lang/Class;Ljava/lang/Class;[Ljava/lang/Class;)Ljava/lang/invoke/MethodType; move-result-object v1 .line 34 .local v1, "methodType":Ljava/lang/invoke/MethodType; const-class v2, Linvoke/TestPolymorphicInvoke; const-string v3, "func" invoke-virtual {v0, v2, v3, v1}, Ljava/lang/invoke/MethodHandles$Lookup;->findVirtual(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle; move-result-object v2 .line 35 .local v2, "methodHandle":Ljava/lang/invoke/MethodHandle; const/16 v3, 0xa const/16 v4, 0x14 invoke-polymorphic {v2, p0, v3, v4}, Ljava/lang/invoke/MethodHandle;->invoke([Ljava/lang/Object;)Ljava/lang/Object;, (Linvoke/TestPolymorphicInvoke;II)Ljava/lang/String; move-result-object v3 .line 36 .local v3, "ret":Ljava/lang/String; sget-object v4, Ljava/lang/System;->out:Ljava/io/PrintStream; invoke-virtual {v4, v3}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V :try_end_2a .catchall {:try_start_0 .. :try_end_2a} :catchall_2b .line 39 .end local v0 # "lookup":Ljava/lang/invoke/MethodHandles$Lookup; .end local v1 # "methodType":Ljava/lang/invoke/MethodType; .end local v2 # "methodHandle":Ljava/lang/invoke/MethodHandle; .end local v3 # "ret":Ljava/lang/String; goto :goto_2f .line 37 :catchall_2b move-exception v0 .line 38 .local v0, "e":Ljava/lang/Throwable; invoke-virtual {v0}, Ljava/lang/Throwable;->printStackTrace()V .line 40 .end local v0 # "e":Ljava/lang/Throwable; :goto_2f return-void .end method ================================================ FILE: jadx-core/src/test/smali/invoke/TestRawCustomInvoke.smali ================================================ .class public Linvoke/TestRawCustomInvoke; .super Ljava/lang/Object; .method public static func(ID)Ljava/lang/String; .registers 5 int-to-double v0, p0 add-double/2addr v0, p1 invoke-static {v0, v1}, Ljava/lang/String;->valueOf(D)Ljava/lang/String; move-result-object p0 return-object p0 .end method .method private static staticBootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; .registers 5 :try_start_0 new-instance v0, Ljava/lang/invoke/ConstantCallSite; invoke-virtual {p0}, Ljava/lang/invoke/MethodHandles$Lookup;->lookupClass()Ljava/lang/Class; move-result-object v1 invoke-virtual {p0, v1, p1, p2}, Ljava/lang/invoke/MethodHandles$Lookup;->findStatic(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle; move-result-object p0 invoke-direct {v0, p0}, Ljava/lang/invoke/ConstantCallSite;->(Ljava/lang/invoke/MethodHandle;)V :try_end_d .catch Ljava/lang/NoSuchMethodException; {:try_start_0 .. :try_end_d} :catch_e .catch Ljava/lang/IllegalAccessException; {:try_start_0 .. :try_end_d} :catch_e return-object v0 :catch_e move-exception p0 new-instance p1, Ljava/lang/RuntimeException; invoke-direct {p1, p0}, Ljava/lang/RuntimeException;->(Ljava/lang/Throwable;)V throw p1 .end method .method public test()Ljava/lang/String; .registers 3 :try_start_0 const/4 v0, 0x1 const-wide/high16 v1, 0x4000000000000000L # 2.0 invoke-custom {v0, v1}, call_site_0("func", (ID)Ljava/lang/String;)@Linvoke/TestRawCustomInvoke;->staticBootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; move-result-object v0 :try_end_25 .catchall {:try_start_0 .. :try_end_25} :catchall_26 return-object v0 :catchall_26 move-exception v0 invoke-static {v0}, Lorg/junit/jupiter/api/Assertions;->fail(Ljava/lang/Throwable;)Ljava/lang/Object; const/4 v0, 0x0 return-object v0 .end method .method public check()V .registers 3 invoke-virtual {p0}, Linvoke/TestRawCustomInvoke;->test()Ljava/lang/String; move-result-object v0 invoke-static {v0}, Ljadx/tests/api/utils/assertj/JadxAssertions;->assertThat(Ljava/lang/String;)Ljadx/tests/api/utils/assertj/JadxCodeAssertions; move-result-object v0 const-string v1, "3.0" invoke-virtual {v0, v1}, Ljadx/tests/api/utils/assertj/JadxCodeAssertions;->isEqualTo(Ljava/lang/String;)Lorg/assertj/core/api/AbstractStringAssert; return-void .end method ================================================ FILE: jadx-core/src/test/smali/loops/TestBreakInLoop6.smali ================================================ .class public Lloops/TestBreakInLoop6; .super Ljava/lang/Object; .method public test()J .registers 19 move-object/from16 v0, p0 const-wide v1, 0x1L iget-object v3, v0, Ltest/Test;->j:[Ltest/Test; array-length v4, v3 const/4 v6, 0x0 :goto_c if-ge v6, v4, :cond_64 aget-object v7, v3, v6 invoke-interface {v7}, Ltest/Test;->h()J move-result-wide v8 const-wide v11, 0x2L cmp-long v13, v8, v11 if-eqz v13, :cond_4e cmp-long v13, v1, v11 if-nez v13, :cond_4e move-wide v1, v8 iget-object v11, v0, Ltest/Test;->i:[Ltest/Test; array-length v12, v11 const/4 v13, 0x0 :goto_28 if-ge v13, v12, :cond_4e aget-object v14, v11, v13 if-ne v14, v7, :cond_2f goto :cond_4e :cond_2f invoke-interface {v14, v1, v2}, Ltest/Test;->f(J)J move-result-wide v15 cmp-long v17, v15, v1 if-nez v17, :cond_3a add-int/lit8 v13, v13, 0x1 goto :goto_28 :cond_3a new-instance v3, Ljava/lang/IllegalStateException; invoke-direct {v3}, Ljava/lang/IllegalStateException;->()V throw v3 :cond_4e add-int/lit8 v6, v6, 0x1 goto :goto_c :cond_64 return-wide v1 .end method ================================================ FILE: jadx-core/src/test/smali/loops/TestEndlessLoop2.smali ================================================ .class Lloops/TestEndlessLoop2; .super Ljava/lang/Object; .field instanceCount:J .method test([Ljava/lang/String;)V .registers 10 const/16 p1, 0xb invoke-virtual {p0, p1}, Lloops/TestEndlessLoop2;->vMeth(I)V const/16 v0, 0xf1 const-wide/high16 v1, 0x4032000000000000L # 18.0 :goto_a const-wide/high16 v3, 0x4076000000000000L # 352.0 const/4 v5, 0x1 cmpg-double v6, v1, v3 if-gez v6, :cond_1c const/4 v0, 0x1 :goto_12 add-int/2addr v0, v5 const/16 v3, 0x4b if-ge v0, v3, :cond_18 goto :goto_12 :cond_18 const-wide/high16 v3, 0x3ff0000000000000L # 1.0 add-double/2addr v1, v3 goto :goto_a :cond_1c iget-wide v3, p0, Lloops/TestEndlessLoop2;->instanceCount:J long-to-int v4, v3 const/16 v3, 0xb :goto_21 const/16 v6, 0xf3 if-ge v5, v6, :cond_41 rem-int/lit8 v6, v5, 0x9 add-int/lit8 v6, v6, 0x12 if-eq v6, p1, :cond_3e const/16 v7, 0x15 if-eq v6, v7, :cond_36 const/16 v7, 0x16 if-eq v6, v7, :cond_34 goto :goto_3b :cond_34 add-int/2addr v4, v0 goto :goto_3b :cond_36 const v6, 0xeed9 div-int/2addr v3, v6 nop :goto_3b add-int/lit8 v5, v5, 0x1 goto :goto_21 :cond_3e nop :goto_3f nop # endless loop with empty body goto :goto_3f :cond_41 sget-object p1, Ljava/lang/System;->out:Ljava/io/PrintStream; invoke-static {v1, v2}, Ljava/lang/Double;->doubleToLongBits(D)J move-result-wide v0 new-instance v2, Ljava/lang/StringBuilder; invoke-direct {v2}, Ljava/lang/StringBuilder;->()V const-string v5, "i21 d2 i22 = " invoke-virtual {v2, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; const-string v3, "," invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v2, v0, v1}, Ljava/lang/StringBuilder;->append(J)Ljava/lang/StringBuilder; invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v2, v4}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v0 invoke-virtual {p1, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V return-void .end method ================================================ FILE: jadx-core/src/test/smali/loops/TestIfInLoop4.smali ================================================ .class public LTestIfInLoop4; .super Ljava/lang/Object; .method public test()Z .registers 5 move-object/from16 v0, p0 const/4 v2, 0x0 const/4 v3, 0x1 :goto_0 iget v1, v0, LTestIfInLoop4;->x:I if-ge v2, v1, :goto_1 if-gtz v2, :cond if-gez v2, :cond if-ltz v2, :goto_1 if-ge v2, v1, :goto_1 goto :goto_1 :cond goto :goto_0 :goto_1 return v3 .end method ================================================ FILE: jadx-core/src/test/smali/loops/TestLoopCondition5.smali ================================================ .class public LTestLoopCondition5; .super Ljava/lang/Object; .source "TestLoopCondition5.java" .method private static lastIndexOf([IIII)I .locals 1 add-int/lit8 p3, p3, -0x1 :goto_0 const/4 v0, -0x1 if-lt p3, p2, :cond_1 .line 219 aget v0, p0, p3 if-ne v0, p1, :cond_0 return p3 :cond_0 add-int/lit8 p3, p3, -0x1 goto :goto_0 :cond_1 move p3, v0 return p3 .end method ================================================ FILE: jadx-core/src/test/smali/loops/TestLoopRestore.smali ================================================ .class public Lloops/TestLoopRestore; .super Ljava/lang/Object; .source "SourceFile.java" .method private test([B)Ljava/lang/String; .registers 10 const/16 v0, 0x10 new-array v0, v0, [C fill-array-data v0, :array_3c :try_start_7 const-string v1, "MD5" invoke-static {v1}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest; move-result-object v1 invoke-virtual {v1, p1}, Ljava/security/MessageDigest;->update([B)V invoke-virtual {v1}, Ljava/security/MessageDigest;->digest()[B move-result-object p1 array-length v1, p1 mul-int/lit8 v2, v1, 0x2 new-array v2, v2, [C :try_end_19 .catch Ljava/lang/Exception; {:try_start_7 .. :try_end_19} :catch_3a const/4 v3, 0x0 const/4 v4, 0x0 :goto_1b if-ge v3, v1, :cond_34 aget-byte v5, p1, v3 add-int/lit8 v6, v4, 0x1 ushr-int/lit8 v7, v5, 0x4 and-int/lit8 v7, v7, 0xf aget-char v7, v0, v7 aput-char v7, v2, v4 add-int/lit8 v4, v6, 0x1 and-int/lit8 v5, v5, 0xf aget-char v5, v0, v5 aput-char v5, v2, v6 add-int/lit8 v3, v3, 0x1 goto :goto_1b :cond_34 new-instance p1, Ljava/lang/String; invoke-direct {p1, v2}, Ljava/lang/String;->([C)V return-object p1 :catch_3a const/4 p1, 0x0 return-object p1 :array_3c .array-data 2 0x30s 0x31s 0x32s 0x33s 0x34s 0x35s 0x36s 0x37s 0x38s 0x39s 0x61s 0x62s 0x63s 0x64s 0x65s 0x66s .end array-data .end method ================================================ FILE: jadx-core/src/test/smali/loops/TestLoopRestore3.smali ================================================ .class public Lloops/TestLoopRestore3; .super Ljava/lang/Object; .method public final b(Ljava/lang/String;Lb/U53$b;)V .registers 8 iget-object v0, p0, Lb/X53;->e:Ljava/util/concurrent/atomic/AtomicReference; :goto_2 invoke-virtual {v0}, Ljava/util/concurrent/atomic/AtomicReference;->get()Ljava/lang/Object; move-result-object v1 move-object v2, v1 check-cast v2, Ljava/util/List; move-object v3, v2 check-cast v3, Ljava/lang/Iterable; instance-of v4, v3, Ljava/util/Collection; if-eqz v4, :cond_1a move-object v4, v3 check-cast v4, Ljava/util/Collection; invoke-interface {v4}, Ljava/util/Collection;->isEmpty()Z move-result v4 if-eqz v4, :cond_1a goto :goto_33 :cond_1a invoke-interface {v3}, Ljava/lang/Iterable;->iterator()Ljava/util/Iterator; move-result-object v3 :cond_1e invoke-interface {v3}, Ljava/util/Iterator;->hasNext()Z move-result v4 if-eqz v4, :cond_33 invoke-interface {v3}, Ljava/util/Iterator;->next()Ljava/lang/Object; move-result-object v4 check-cast v4, Lb/X53$c; iget-object v4, v4, Lb/X53$c;->b:Ljava/lang/String; invoke-static {v4, p1}, Lkotlin/jvm/internal/Intrinsics;->a(Ljava/lang/Object;Ljava/lang/Object;)Z move-result v4 if-eqz v4, :cond_1e goto :goto_40 :cond_33 :goto_33 check-cast v2, Ljava/util/Collection; new-instance v3, Lb/X53$c; sget-object v4, Lb/Pd2;->a:Lb/Pd2; invoke-direct {v3, p2, p1, v4}, Lb/X53$c;->(Lb/U53$b;Ljava/lang/String;Ljava/util/List;)V invoke-static {v2, v3}, Lb/R31;->a0(Ljava/util/Collection;Ljava/lang/Object;)Ljava/util/ArrayList; move-result-object v2 :cond_40 :goto_40 invoke-virtual {v0, v1, v2}, Ljava/util/concurrent/atomic/AtomicReference;->compareAndSet(Ljava/lang/Object;Ljava/lang/Object;)Z move-result v3 if-eqz v3, :cond_47 return-void :cond_47 invoke-virtual {v0}, Ljava/util/concurrent/atomic/AtomicReference;->get()Ljava/lang/Object; move-result-object v3 if-eq v3, v1, :cond_40 goto :goto_2 .end method ================================================ FILE: jadx-core/src/test/smali/loops/TestMultiEntryLoop.smali ================================================ .class public Lloops/TestMultiEntryLoop; .super Ljava/lang/Object; .field private static arr:[B .method private static test(III)Ljava/lang/String; .registers 9 mul-int/lit8 p1, p1, 0x2 rsub-int/lit8 p1, p1, 0x6f mul-int/lit8 p0, p0, 0x2 add-int/lit8 p0, p0, 0x1c mul-int/lit8 p2, p2, 0x2 add-int/lit8 p2, p2, 0x4 new-instance v0, Ljava/lang/String; const/4 v5, -0x1 sget-object v4, Lloops/TestMultiEntryLoop;->arr:[B new-array v1, p0, [B add-int/lit8 p0, p0, -0x1 if-nez v4, :cond_1e move v2, p1 move v3, p2 :goto_19 add-int/2addr v2, v3 add-int/lit8 p1, v2, -0x8 add-int/lit8 p2, p2, 0x1 :cond_1e add-int/lit8 v5, v5, 0x1 int-to-byte v2, p1 aput-byte v2, v1, v5 if-ne v5, p0, :cond_2a invoke-direct {v0, v1}, Ljava/lang/String;->([B)V return-object v0 :cond_2a move v2, p1 aget-byte v3, v4, p2 goto :goto_19 .end method ================================================ FILE: jadx-core/src/test/smali/loops/TestMultiEntryLoop2.smali ================================================ .class public Lloops/TestMultiEntryLoop2; .super Ljava/lang/Object; .field public list:Ljava/util/List; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/util/List<", "Ljava/lang/String;", ">;" } .end annotation .end field .method private test(Ljava/lang/String;II)V .registers 14 if-ne p2, p3, :cond_3 return-void :cond_3 invoke-virtual {p1, p2}, Ljava/lang/String;->charAt(I)C move-result v0 const/16 v1, 0x2f const-string v2, "" const/4 v3, 0x1 if-eq v0, v1, :cond_1e const/16 v1, 0x5c if-ne v0, v1, :cond_13 goto :goto_1e :cond_13 iget-object v0, p0, Lloops/TestMultiEntryLoop2;->list:Ljava/util/List; invoke-interface {v0}, Ljava/util/List;->size()I move-result v1 sub-int/2addr v1, v3 invoke-interface {v0, v1, v2}, Ljava/util/List;->set(ILjava/lang/Object;)Ljava/lang/Object; goto :goto_29 :cond_1e :goto_1e iget-object v0, p0, Lloops/TestMultiEntryLoop2;->list:Ljava/util/List; invoke-interface {v0}, Ljava/util/List;->clear()V .line 4 iget-object v0, p0, Lloops/TestMultiEntryLoop2;->list:Ljava/util/List; invoke-interface {v0, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z goto :goto_41 :cond_29 :goto_29 move v6, p2 if-ge v6, p3, :cond_44 const-string p2, "/\\" .line 5 invoke-static {p1, v6, p3, p2}, Lloops/TestMultiEntryLoop2;->delimiterOffset(Ljava/lang/String;IILjava/lang/String;)I move-result p2 if-ge p2, p3, :cond_36 move v0, v3 goto :goto_37 :cond_36 const/4 v0, 0x0 :goto_37 const/4 v9, 0x1 move-object v4, p0 move-object v5, p1 move v7, p2 move v8, v0 .line 6 invoke-direct/range {v4 .. v9}, Lloops/TestMultiEntryLoop2;->push(Ljava/lang/String;IIZZ)V if-eqz v0, :cond_29 :goto_41 add-int/lit8 p2, p2, 0x1 goto :goto_29 :cond_44 return-void .end method .method private push(Ljava/lang/String;IIZZ)V .locals 0 return-void .end method .method private delimiterOffset(Ljava/lang/String;IILjava/lang/String;)I .locals 1 const/4 v0, 0x0 return v0 .end method ================================================ FILE: jadx-core/src/test/smali/names/TestCaseSensitiveChecks/1.smali ================================================ .class public LA; .super Ljava/lang/Object; ================================================ FILE: jadx-core/src/test/smali/names/TestCaseSensitiveChecks/2.smali ================================================ .class public La; .super Ljava/lang/Object; ================================================ FILE: jadx-core/src/test/smali/names/TestClassNameWithInvalidChar/a.smali ================================================ .class public Ldo-; .super Ljava/lang/Object; ================================================ FILE: jadx-core/src/test/smali/names/TestClassNameWithInvalidChar/b.smali ================================================ .class public Li-f; .super Ljava/lang/Object; ================================================ FILE: jadx-core/src/test/smali/names/TestDefPkgRename/a.smali ================================================ .class public LA; .super Ljava/lang/Object; ================================================ FILE: jadx-core/src/test/smali/names/TestDefPkgRename/b.smali ================================================ .class public Lpkg/B; .super Ljava/lang/Object; .field public a:LA; ================================================ FILE: jadx-core/src/test/smali/names/TestDuplicatedNames.smali ================================================ .class public LTestDuplicatedNames; .super Ljava/lang/Object; .source "TestDuplicatedNames.java" # instance fields .field public fieldName:Ljava/lang/String; .field public fieldName:Ljava/lang/Object; # direct methods .method public constructor ()V .registers 1 .prologue .line 3 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method # virtual methods .method public run()Ljava/lang/String; .registers 2 .prologue iget-object v0, p0, LTestDuplicatedNames;->fieldName:Ljava/lang/String; return-object v0 .end method .method public run()Ljava/lang/Object; .registers 2 .prologue iget-object v0, p0, LTestDuplicatedNames;->fieldName:Ljava/lang/Object; return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/names/TestFieldCollideWithPackage/1.smali ================================================ .class public Lfirst/A; .super Ljava/lang/Object; .field public first:Lfirst/A; .field public second:Lsecond/A; .method public test()Ljava/lang/String; .registers 2 invoke-static {}, Lsecond/A;->call()Ljava/lang/String; move-result-object v0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/names/TestFieldCollideWithPackage/2.smali ================================================ .class public Lsecond/A; .super Ljava/lang/Object; .method static public call()Ljava/lang/String; .registers 1 const v0, 0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/names/TestLocalVarCollideWithPackage/1.smali ================================================ .class public Lfirst/A; .super Ljava/lang/Object; .method public test()Ljava/lang/String; .registers 2 new-instance v1, Lpkg/Second; invoke-direct {v1}, Lpkg/Second;->()V .local v1, "second":Lpkg/Second; invoke-static {}, Lsecond/A;->call()Ljava/lang/String; iget-object v0, v1, Lpkg/Second;->str:Ljava/lang/String; return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/names/TestLocalVarCollideWithPackage/2.smali ================================================ .class public Lsecond/A; .super Ljava/lang/Object; .method static public call()Ljava/lang/String; .registers 1 const v0, 0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/names/TestLocalVarCollideWithPackage/3.smali ================================================ .class public Lpkg/Second; .super Ljava/lang/Object; .field public str:Ljava/lang/String; ================================================ FILE: jadx-core/src/test/smali/names/TestReservedClassNames.smali ================================================ .class public Ldo; .super Ljava/lang/Object; # direct methods .method public constructor ()V .locals 0 .line 3 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/names/TestReservedNames.smali ================================================ .class public Lnames/TestReservedNames; .super Ljava/lang/Object; .source "TestReservedNames.java" # instance fields .field public do:Ljava/lang/String; # reserved name .field public 0f:Ljava/lang/String; # invalid identifier # direct methods .method public constructor ()V .registers 1 .prologue .line 3 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method # virtual methods .method public try()Ljava/lang/String; .registers 2 .prologue .line 8 iget-object v0, p0, Lnames/TestReservedNames;->do:Ljava/lang/String; return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/names/TestReservedPackageNames/a.smali ================================================ .class public Ldo/if/A; .super Ljava/lang/Object; ================================================ FILE: jadx-core/src/test/smali/others/TestAllNops.smali ================================================ .class public Lothers/TestAllNops; .super Ljava/lang/Object; .method public constructor ()V .registers 1 .line 55 nop nop nop nop .end method .method private test()Z .registers 11 .line 1480 nop nop .line 1481 nop nop nop nop nop nop .line 1485 nop nop .line 1486 nop nop .line 1487 nop .end method .method private testWithTryCatch()Z .registers 11 .line 1480 :try_start_0 nop nop .line 1481 nop nop nop nop nop nop .line 1485 nop nop :try_end_35 .catch Ljava/security/NoSuchAlgorithmException; {:try_start_0 .. :try_end_35} :catch_36 nop .line 1547 :catch_36 nop .line 1487 nop .end method ================================================ FILE: jadx-core/src/test/smali/others/TestBadClassAccessModifiers/A.smali ================================================ .class public Lothers/A; .super Ljava/lang/Object; .source "A.java" # direct methods .method public constructor ()V .registers 1 .line 3 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method # virtual methods .method public call()V .registers 1 .line 5 invoke-static {}, Lothers/B$BB$BBB;->test()V .line 6 return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestBadClassAccessModifiers/B$BB$BBB.smali ================================================ .class public Lothers/B$BB$BBB; .super Ljava/lang/Object; .source "B.java" # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Lothers/B$BB; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x9 name = "BBB" .end annotation # direct methods .method public constructor ()V .registers 1 .line 12 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public static test()V .registers 0 .line 14 return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestBadClassAccessModifiers/B$BB.smali ================================================ .class public Lothers/B$BB; .super Ljava/lang/Object; .source "B.java" # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Lothers/B; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0xa name = "BB" .end annotation .annotation system Ldalvik/annotation/MemberClasses; value = { Lothers/B$BB$BBB; } .end annotation # direct methods .method private constructor ()V .registers 1 .line 11 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestBadClassAccessModifiers/B.smali ================================================ .class public Lothers/B; .super Ljava/lang/Object; .source "B.java" # annotations .annotation system Ldalvik/annotation/MemberClasses; value = { Lothers/B$BB;, Lothers/B$BBpr; } .end annotation # direct methods .method public constructor ()V .registers 1 .line 3 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestBadMethodAccessModifiers/TestCls$A.smali ================================================ .class public abstract Lothers/TestCls$A; .super Ljava/lang/Object; .source "TestCls.java" # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Lothers/TestCls; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x401 name = "A" .end annotation # instance fields .field final synthetic this$0:Lothers/TestCls; # direct methods .method public constructor (Lothers/TestCls;)V .registers 2 .param p1, "this$0" # Lothers/TestCls; .prologue .line 5 iput-object p1, p0, Lothers/TestCls$A;->this$0:Lothers/TestCls; invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method # virtual methods .method public abstract test()V .end method ================================================ FILE: jadx-core/src/test/smali/others/TestBadMethodAccessModifiers/TestCls$B.smali ================================================ .class public Lothers/TestCls$B; .super Lothers/TestCls$A; .source "TestCls.java" # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Lothers/TestCls; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x1 name = "B" .end annotation # instance fields .field final synthetic this$0:Lothers/TestCls; # direct methods .method public constructor (Lothers/TestCls;)V .registers 2 .param p1, "this$0" # Lothers/TestCls; .prologue .line 9 iput-object p1, p0, Lothers/TestCls$B;->this$0:Lothers/TestCls; invoke-direct {p0, p1}, Lothers/TestCls$A;->(Lothers/TestCls;)V return-void .end method # virtual methods .method protected test()V .registers 1 .prologue .line 11 return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestBadMethodAccessModifiers/TestCls.smali ================================================ .class public Lothers/TestCls; .super Ljava/lang/Object; .source "TestCls.java" # annotations .annotation system Ldalvik/annotation/MemberClasses; value = { Lothers/TestCls$B;, Lothers/TestCls$A; } .end annotation # direct methods .method public constructor ()V .registers 1 .prologue .line 3 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestConstructor.smali ================================================ .class public Lothers/TestConstructor; .super Ljava/lang/Object; .method private test(DDLSomeObject;)LSomeObject; .locals 22 .param p1, "arg1" # D .param p3, "arg2" # D .param p5, "arg3" # LSomeObject; .prologue .line 54 new-instance v17, LSomeObject; move-object/from16 v0, v17 move-object/from16 v1, p5 invoke-direct {v0, v1}, LSomeObject;->(LSomeObject;)V .line 59 .local v17, "localSomeObject":LSomeObject; return-object v17 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestConstructor2/A.smali ================================================ .class public final Lothers/TestConstructor2$A; .super Ljava/lang/Object; .field public a:I .field public b:Ljava/util/ArrayDeque; ================================================ FILE: jadx-core/src/test/smali/others/TestConstructor2/TestConstructor2.smali ================================================ .class public Lothers/TestConstructor2; .super Ljava/lang/Object; .field public a:Ljava/util/HashMap; .method public final a(III)V .registers 6 .line 1 .line 2 new-instance v0, Lothers/TestConstructor2$A; .line 3 .line 4 .line 5 invoke-direct {v0}, Ljava/lang/Object;->()V .line 6 .line 7 if-gt p2, p3, :cond_1c .line 8 .line 9 new-instance p2, Ljava/util/ArrayDeque; .line 10 .line 11 iget v1, v0, Lothers/TestConstructor2$A;->a:I .line 12 .line 13 .line 14 invoke-direct {p2, v1}, Ljava/util/ArrayDeque;->(I)V .line 15 .line 16 iput-object p2, v0, Lothers/TestConstructor2$A;->b:Ljava/util/ArrayDeque; .line 17 .line 18 iput p3, v0, Lothers/TestConstructor2$A;->a:I .line 19 .line 20 iget-object p2, p0, Lothers/TestConstructor2;->a:Ljava/util/HashMap; .line 21 .line 22 .line 23 invoke-static {p1}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer; .line 24 move-result-object p1 .line 25 .line 26 .line 27 invoke-virtual {p2, p1, v0}, Ljava/util/HashMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; .line 28 return-void .line 29 .line 30 :cond_1c new-instance p1, Ljava/lang/IllegalArgumentException; .line 31 .line 32 const-string/jumbo p2, "error" .line 33 .line 34 .line 35 invoke-direct {p1, p2}, Ljava/lang/IllegalArgumentException;->(Ljava/lang/String;)V .line 36 throw p1 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestConstructorBranched.smali ================================================ .class public Lothers/TestConstructorBranched; .super Ljava/lang/Object; .method public test(Ljava/util/Collection;)Ljava/util/Set; .registers 4 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/util/Collection", "<", "Ljava/lang/String;", ">;)", "Ljava/util/Set", "<", "Ljava/lang/String;", ">;" } .end annotation new-instance v0, Ljava/util/HashSet; if-nez p1, :cond_d invoke-direct {v0}, Ljava/util/HashSet;->()V goto :goto_7 :cond_d invoke-direct {v0, p1}, Ljava/util/HashSet;->(Ljava/util/Collection;)V :goto_7 const-string v1, "end" invoke-interface {v0, v1}, Ljava/util/Set;->add(Ljava/lang/Object;)Z return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestConstructorBranched2.smali ================================================ .class public Lothers/TestConstructorBranched2; .super Ljava/lang/Object; .method private test(Ljava/util/List;)Ljava/lang/String; .locals 12 iget-boolean v1, p0, Landroidx/gridlayout/widget/GridLayout$Axis;->horizontal:Z const/4 v2, 0x1 if-eqz v1, :cond_0 const-string/jumbo v1, "x" goto :goto_0 :cond_0 const-string/jumbo v1, "y" :goto_0 new-instance v3, Ljava/lang/StringBuilder; invoke-direct {v3}, Ljava/lang/StringBuilder;->()V const/4 v4, 0x1 invoke-interface {p1}, Ljava/util/List;->iterator()Ljava/util/Iterator; move-result-object v5 const/16 v6, 0x98 :goto_1 invoke-interface {v5}, Ljava/util/Iterator;->hasNext()Z move-result v6 if-eqz v6, :cond_3 invoke-interface {v5}, Ljava/util/Iterator;->next()Ljava/lang/Object; move-result-object v6 check-cast v6, Landroidx/gridlayout/widget/GridLayout$Arc; if-eqz v4, :cond_1 const/4 v4, 0x0 goto :goto_2 :cond_1 const-string v7, ", " invoke-virtual {v3, v7}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v3 :goto_2 iget-object v7, v6, Landroidx/gridlayout/widget/GridLayout$Arc;->span:Landroidx/gridlayout/widget/GridLayout$Interval; iget v7, v7, Landroidx/gridlayout/widget/GridLayout$Interval;->min:I iget-object v8, v6, Landroidx/gridlayout/widget/GridLayout$Arc;->span:Landroidx/gridlayout/widget/GridLayout$Interval; iget v8, v8, Landroidx/gridlayout/widget/GridLayout$Interval;->max:I iget-object v9, v6, Landroidx/gridlayout/widget/GridLayout$Arc;->value:Landroidx/gridlayout/widget/GridLayout$MutableInt; iget v9, v9, Landroidx/gridlayout/widget/GridLayout$MutableInt;->value:I const-string v10, "-" new-instance v11, Ljava/lang/StringBuilder; if-ge v7, v8, :cond_2 invoke-direct {v11}, Ljava/lang/StringBuilder;->()V invoke-virtual {v11, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v11 invoke-virtual {v11, v8}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; move-result-object v11 invoke-virtual {v11, v10}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v10 invoke-virtual {v10, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v10 invoke-virtual {v10, v7}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; move-result-object v10 const-string v11, ">=" invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v10 invoke-virtual {v10, v9}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; move-result-object v10 invoke-virtual {v10}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v10 goto :goto_3 :cond_2 invoke-direct {v11}, Ljava/lang/StringBuilder;->()V invoke-virtual {v11, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v11 invoke-virtual {v11, v7}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; move-result-object v11 invoke-virtual {v11, v10}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v10 invoke-virtual {v10, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v10 invoke-virtual {v10, v8}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; move-result-object v10 const-string v11, "<=" invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v10 neg-int v11, v9 invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; move-result-object v10 invoke-virtual {v10}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v10 :goto_3 invoke-virtual {v3, v10}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; goto/16 :goto_1 :cond_3 invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v5 return-object v5 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestConstructorBranched3.smali ================================================ .class public Lothers/TestConstructorBranched3; .super Ljava/lang/Object; .method public static final test(Ljava/lang/Class;)Lml/f; .registers 5 const/4 v0, 0x0 :goto_1 invoke-virtual {p0}, Ljava/lang/Class;->isArray()Z move-result v1 if-eqz v1, :cond_13 add-int/lit8 v0, v0, 0x1 invoke-virtual {p0}, Ljava/lang/Class;->getComponentType()Ljava/lang/Class; move-result-object p0 const-string v1, "currentClass.componentType" invoke-static {p0, v1}, Lve/e;->l(Ljava/lang/Object;Ljava/lang/String;)V goto :goto_1 :cond_13 invoke-virtual {p0}, Ljava/lang/Class;->isPrimitive()Z move-result v1 if-eqz v1, :cond_68 sget-object v1, Ljava/lang/Void;->TYPE:Ljava/lang/Class; invoke-static {p0, v1}, Lve/e;->g(Ljava/lang/Object;Ljava/lang/Object;)Z move-result v1 if-eqz v1, :cond_31 new-instance p0, Lml/f; sget-object v1, Lgk/k$a;->e:Lhl/c; invoke-virtual {v1}, Lhl/c;->i()Lhl/b; move-result-object v1 invoke-static {v1}, Lhl/a;->l(Lhl/b;)Lhl/a; move-result-object v1 invoke-direct {p0, v1, v0}, Lml/f;->(Lhl/a;I)V return-object p0 :cond_31 invoke-virtual {p0}, Ljava/lang/Class;->getName()Ljava/lang/String; move-result-object p0 invoke-static {p0}, Lpl/b;->c(Ljava/lang/String;)Lpl/b; move-result-object p0 invoke-virtual {p0}, Lpl/b;->r()Lgk/i; move-result-object p0 const-string v1, "get(currentClass.name).primitiveType" invoke-static {p0, v1}, Lve/e;->l(Ljava/lang/Object;Ljava/lang/String;)V new-instance v1, Lml/f; if-lez v0, :cond_58 .line 1 iget-object p0, p0, Lgk/i;->d:Ljj/d; invoke-interface {p0}, Ljj/d;->getValue()Ljava/lang/Object; move-result-object p0 check-cast p0, Lhl/b; .line 2 invoke-static {p0}, Lhl/a;->l(Lhl/b;)Lhl/a; move-result-object p0 add-int/lit8 v0, v0, -0x1 invoke-direct {v1, p0, v0}, Lml/f;->(Lhl/a;I)V return-object v1 .line 3 :cond_58 iget-object p0, p0, Lgk/i;->c:Ljj/d; invoke-interface {p0}, Ljj/d;->getValue()Ljava/lang/Object; move-result-object p0 check-cast p0, Lhl/b; .line 4 invoke-static {p0}, Lhl/a;->l(Lhl/b;)Lhl/a; move-result-object p0 invoke-direct {v1, p0, v0}, Lml/f;->(Lhl/a;I)V return-object v1 :cond_68 invoke-static {p0}, Lpk/b;->b(Ljava/lang/Class;)Lhl/a; move-result-object p0 sget-object v1, Lik/c;->a:Lik/c; invoke-virtual {p0}, Lhl/a;->b()Lhl/b; move-result-object v2 const-string v3, "javaClassId.asSingleFqName()" invoke-static {v2, v3}, Lve/e;->l(Ljava/lang/Object;Ljava/lang/String;)V invoke-virtual {v1, v2}, Lik/c;->f(Lhl/b;)Lhl/a; move-result-object v1 if-nez v1, :cond_7e goto :goto_7f :cond_7e move-object p0, v1 :goto_7f new-instance v1, Lml/f; invoke-direct {v1, p0, v0}, Lml/f;->(Lhl/a;I)V return-object v1 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestDeadBlockReferencesStart.smali ================================================ .class Lothers/TestDeadBlockReferencesStart; .super Ljava/lang/Object; .method public test()V .registers 6 :start return-void goto :start .end method ================================================ FILE: jadx-core/src/test/smali/others/TestExplicitOverride.smali ================================================ ###### Class others.TestExplicitOverride (others.TestExplicitOverride) .class public Lothers/TestExplicitOverride; .super Ljava/lang/Exception; .source "TestExplicitOverride.java" # direct methods .method public constructor ()V .registers 1 .prologue .line 3 invoke-direct {p0}, Ljava/lang/Exception;->()V return-void .end method # virtual methods .method public getMessage()Ljava/lang/String; .registers 2 .annotation runtime Ljava/lang/Override; .end annotation .prologue .line 7 invoke-super {p0}, Ljava/lang/Exception;->getMessage()Ljava/lang/String; move-result-object v0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestFieldInitOrder2.smali ================================================ .class public Lothers/TestFieldInitOrder2; .super Ljava/lang/Object; .field private static final VALUE:Ljava/lang/String; .field static final ZPREFIX:Ljava/lang/String; = "SOME_" # direct methods .method static constructor ()V .registers 2 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;->()V sget-object v1, Lothers/TestFieldInitOrder2;->ZPREFIX:Ljava/lang/String; invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v0 const-string v1, "VALUE" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v0 invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v0 sput-object v0, Lothers/TestFieldInitOrder2;->VALUE:Ljava/lang/String; return-void .end method .method public constructor ()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public check()V .registers 3 sget-object v0, Lothers/TestFieldInitOrder2;->VALUE:Ljava/lang/String; invoke-static {v0}, Ljadx/tests/api/utils/assertj/JadxAssertions;->assertThat(Ljava/lang/String;)Ljadx/tests/api/utils/assertj/JadxCodeAssertions; move-result-object v0 const-string v1, "SOME_VALUE" invoke-virtual {v0, v1}, Ljadx/tests/api/utils/assertj/JadxCodeAssertions;->isEqualTo(Ljava/lang/String;)Lorg/assertj/core/api/AbstractStringAssert; return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestFieldUsageMove.smali ================================================ .class public Lothers/TestFieldUsageMove; .super Ljava/lang/Object; .method public static test(Ljava/lang/Object;)V .registers 4 .line 4 instance-of v0, p0, Ljava/lang/Boolean; if-eqz v0, :cond_1a sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; new-instance v1, Ljava/lang/StringBuilder; invoke-direct {v1}, Ljava/lang/StringBuilder;->()V const-string v2, "Boolean: " invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v1, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder; invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v1 invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V .line 5 :cond_1a instance-of v0, p0, Ljava/lang/Float; if-eqz v0, :cond_34 sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; new-instance v1, Ljava/lang/StringBuilder; invoke-direct {v1}, Ljava/lang/StringBuilder;->()V const-string v2, "Float: " invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v1, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder; invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p0 invoke-virtual {v0, p0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V .line 6 :cond_34 return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestFixClassAccessModifiers/Cls.smali ================================================ .class public Lothers/Cls; .super Ljava/lang/Object; .annotation system Ldalvik/annotation/MemberClasses; value = { Lothers/Cls$InnerCls; } .end annotation ================================================ FILE: jadx-core/src/test/smali/others/TestFixClassAccessModifiers/InnerCls.smali ================================================ .class private Lothers/Cls$InnerCls; .super Ljava/lang/Object; .annotation system Ldalvik/annotation/EnclosingClass; value = Lothers/Cls; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0xA name = "InnerCls" .end annotation ================================================ FILE: jadx-core/src/test/smali/others/TestFixClassAccessModifiers/TestCls.smali ================================================ .class public Lothers/TestCls; .super Ljava/lang/Object; .field public field:Lothers/Cls$InnerCls; ================================================ FILE: jadx-core/src/test/smali/others/TestIncorrectFieldSignature.smali ================================================ .class public Lothers/TestIncorrectFieldSignature; .super Ljava/lang/Object; .field public static A:Z .annotation system Ldalvik/annotation/Signature; value = { "Lint;" } .end annotation .end field .field public static B:Ljava/lang/Boolean; .annotation system Ldalvik/annotation/Signature; value = { "Lpkg/int;" } .end annotation .end field ================================================ FILE: jadx-core/src/test/smali/others/TestIncorrectMethodSignature.smali ================================================ .class public Lothers/TestIncorrectMethodSignature; .super Ljava/lang/RuntimeException; .source "TestIncorrectMethodSignature.java" .method public constructor (Ljava/lang/String;)V .registers 2 .annotation system Ldalvik/annotation/Signature; value = { "(J)V" } .end annotation invoke-direct {p0, p1}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestInlineVarArg.smali ================================================ .class public Lothers/TestInlineVarArg; .super Ljava/lang/Object; .method public static varargs f([Ljava/lang/String;)V .registers 1 return-void .end method .method public test()V .registers 5 const/4 v2, 0x3 new-array v1, v2, [Ljava/lang/String; move-object v0, v1 const/4 v2, 0x0 const-string v3, "a" aput-object v3, v0, v2 const/4 v2, 0x1 const-string v3, "b" aput-object v3, v0, v2 const/4 v2, 0x2 const-string v3, "c" aput-object v3, v0, v2 move-object v1, v0 invoke-static {v1}, Lothers/TestInlineVarArg;->f([Ljava/lang/String;)V return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestInsnsBeforeSuper/A.smali ================================================ .class public Lothers/A; .super Ljava/lang/Object; # direct methods .method public constructor (Ljava/lang/String;)V .registers 3 .prologue invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestInsnsBeforeSuper/B.smali ================================================ .class public Lothers/B; .super Lothers/A; # direct methods .method public constructor (Ljava/lang/String;)V .registers 3 .prologue invoke-static {p1}, Lothers/B;->checkNull(Ljava/lang/Object;)V invoke-direct {p0, p1}, Lothers/A;->(Ljava/lang/String;)V return-void .end method .method public static checkNull(Ljava/lang/Object;)V .registers 3 .prologue if-nez p0, :cond_8 new-instance v0, Ljava/lang/NullPointerException; invoke-direct {v0}, Ljava/lang/NullPointerException;->()V throw v0 :cond_8 return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestInsnsBeforeSuper2.smali ================================================ .class public Lothers/TestInsnsBeforeSuper2; .super Ljava/lang/Exception; .source "MyException.java" # instance fields .field private mErrorType:I # direct methods .method public constructor (Ljava/lang/String;I)V .locals 8 .prologue move-object v0, p0 .local v0, "this":Lothers/TestInsnsBeforeSuper2; move-object v1, p1 .local v1, "message":Ljava/lang/String; move v2, p2 .line 39 .local v2, "errorType":I move-object v3, v0 .local v3, "this":Lothers/TestInsnsBeforeSuper2; move-object v4, v1 .local v4, "message":Ljava/lang/String; move v5, v2 .line 51 .end local v0 # "this":Lothers/TestInsnsBeforeSuper2; .end local v1 # "message":Ljava/lang/String; .end local v2 # "errorType":I .local v5, "errorType":I move-object v6, v1 invoke-direct {v0, v6}, Ljava/lang/Exception;->(Ljava/lang/String;)V .line 39 const/4 v7, 0x0 iput v7, v0, Lothers/TestInsnsBeforeSuper2;->mErrorType:I .line 52 iput v2, v0, Lothers/TestInsnsBeforeSuper2;->mErrorType:I .line 53 return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestInsnsBeforeThis.smali ================================================ .class public Lothers/TestInsnsBeforeThis; .super Ljava/lang/Object; .method public constructor (Ljava/lang/String;)V .registers 3 .prologue invoke-static {p1}, Lothers/TestInsnsBeforeThis;->checkNull(Ljava/lang/Object;)V invoke-direct {p1}, Ljava/lang/String;->length()I move-result v0 invoke-direct {p0, v0}, Lothers/TestInsnsBeforeThis;->(I)V return-void .end method .method public constructor (I)V .registers 3 .prologue invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public static checkNull(Ljava/lang/Object;)V .registers 3 .prologue if-nez p0, :cond_8 new-instance v0, Ljava/lang/NullPointerException; invoke-direct {v0}, Ljava/lang/NullPointerException;->()V throw v0 :cond_8 return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestInvalidExceptions.smali ================================================ .class public Lothers/TestInvalidExceptions; .super Ljava/lang/Object; .method private invalidException()V .registers 3 .annotation system Ldalvik/annotation/Throws; value = { Ljava/lang/String; } .end annotation new-instance v0, Ljava/io/FileNotFoundException; const-string v1, "" invoke-direct {v0, v1}, Ljava/io/FileNotFoundException;->(Ljava/lang/String;)V throw v0 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestInvalidExceptions2.smali ================================================ .class public Lothers/TestInvalidExceptions2; .super Ljava/lang/Object; .method private throwPossibleExceptionType()V .registers 3 .annotation system Ldalvik/annotation/Throws; value = { Ljadx/UnknownTypeHierarchyException; } .end annotation new-instance v0, Ljadx/UnknownTypeHierarchyException; const-string v1, "" invoke-direct {v0, v1}, Ljadx/UnknownTypeHierarchyException;->(Ljava/lang/String;)V throw v0 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestMissingExceptions.smali ================================================ .class public Lothers/TestMissingExceptions; .super Ljava/lang/Object; .method private exceptionSource()V .registers 3 new-instance v0, Ljava/io/FileNotFoundException; const-string v1, "" invoke-direct {v0, v1}, Ljava/io/FileNotFoundException;->(Ljava/lang/String;)V throw v0 .end method .method public doSomething1(I)V .registers 3 .param p1, "i" # I const/4 v0, 0x1 if-ne p1, v0, :cond_7 invoke-virtual {p0, p1}, Lothers/TestMissingExceptions;->doSomething2(I)V goto :goto_a :cond_7 invoke-virtual {p0, p1}, Lothers/TestMissingExceptions;->doSomething1(I)V :goto_a return-void .end method .method public doSomething2(I)V .registers 3 .param p1, "i" # I const/4 v0, 0x1 if-ne p1, v0, :cond_7 invoke-direct {p0}, Lothers/TestMissingExceptions;->exceptionSource()V goto :goto_a :cond_7 invoke-virtual {p0, p1}, Lothers/TestMissingExceptions;->doSomething1(I)V :goto_a return-void .end method .method public mergeThrownExcetions()V .registers 1 .annotation system Ldalvik/annotation/Throws; value = { Ljava/io/IOException; } .end annotation invoke-direct {p0}, Lothers/TestMissingExceptions;->exceptionSource()V return-void .end method .method public missingThrowsAnnotation()V .registers 1 invoke-direct {p0}, Lothers/TestMissingExceptions;->exceptionSource()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestMoveInline.smali ================================================ .class public Lothers/TestMoveInline; .super Ljava/lang/Object; .field private h:[B .field private a:Lothers/TestMoveInline; .method public k([BII)V .registers 5 return-void .end method .method public test(I)V .registers 7 const/4 v0, 0x0 move v1, v0 :goto_2 and-int/lit8 v2, p1, -0x80 if-nez v2, :cond_13 .line 1 iget-object v2, p0, Lothers/TestMoveInline;->h:[B add-int/lit8 v3, v1, 0x1 int-to-byte p1, p1 aput-byte p1, v2, v1 .line 2 iget-object p1, p0, Lothers/TestMoveInline;->a:Lothers/TestMoveInline; invoke-virtual {p1, v2, v0, v3}, Lothers/TestMoveInline;->k([BII)V return-void .line 3 :cond_13 iget-object v2, p0, Lothers/TestMoveInline;->h:[B add-int/lit8 v3, v1, 0x1 and-int/lit8 v4, p1, 0x7f or-int/lit16 v4, v4, 0x80 int-to-byte v4, v4 aput-byte v4, v2, v1 ushr-int/lit8 p1, p1, 0x7 move v1, v3 goto :goto_2 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestMultipleNOPs/test.smali ================================================ .class final LseC/dujmehn/Cutyq/e; .super Ljava/lang/Object; # interfaces .implements Ljava/lang/Runnable; .method public static test()Ljava/lang/String; .registers 9 nop nop nop nop .prologue nop const-string v5, "VA==" nop nop .local v5, "x_gfxj":Ljava/lang/String; nop const-string v1, "54b6610e1af242f78e38a5866eaa7c41" nop nop .local v1, "p":Ljava/lang/String; nop invoke-virtual {v5}, Ljava/lang/String;->getBytes()[B nop nop move-result-object v7 nop nop const/4 v8, 0x0 nop nop invoke-static {v7, v8}, Landroid/util/Base64;->decode([BI)[B nop nop move-result-object v3 nop nop .local v3, "wjxzqy":[B nop new-instance v4, Ljava/lang/String; nop nop invoke-direct {v4, v3}, Ljava/lang/String;->([B)V nop nop .local v4, "x":Ljava/lang/String; nop new-instance v6, Ljava/lang/StringBuilder; nop nop invoke-direct {v6}, Ljava/lang/StringBuilder;->()V nop nop .local v6, "xg":Ljava/lang/StringBuilder; nop const/4 v0, 0x0 nop nop .local v0, "n":I nop :goto_3b nop invoke-virtual {v4}, Ljava/lang/String;->length()I nop nop move-result v7 nop nop if-ge v0, v7, :cond_76 nop nop invoke-virtual {v4, v0}, Ljava/lang/String;->charAt(I)C nop nop move-result v7 nop nop invoke-virtual {v1}, Ljava/lang/String;->length()I nop nop move-result v8 nop nop rem-int v8, v0, v8 nop nop invoke-virtual {v1, v8}, Ljava/lang/String;->charAt(I)C nop nop move-result v8 nop nop xor-int/2addr v7, v8 nop nop int-to-char v7, v7 nop nop invoke-virtual {v6, v7}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder; nop nop add-int/lit8 v0, v0, 0x1 nop nop goto :goto_3b nop nop :cond_76 nop invoke-virtual {v6}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; nop nop move-result-object v2 nop nop .local v2, "w":Ljava/lang/String; nop return-object v2 nop .end method ================================================ FILE: jadx-core/src/test/smali/others/TestN21.smali ================================================ .class public Lothers/TestN21; .super Ljava/lang/Object; .method private static test([BI)I .locals 5 const/4 v1, 0x0 const/16 v0, 0xe aget-byte v0, p0, v0 shl-int/lit8 v0, v0, 0x10 move v2, v1 :goto_0 if-nez v2, :cond_1 const/4 v2, 0x3 and-int/lit16 v3, p1, 0xff :try_start_0 aget-byte v3, p0, v3 and-int/lit16 v3, v3, 0xff shr-int/lit8 v4, p1, 0x8 and-int/lit16 v4, v4, 0xff aget-byte v4, p0, v4 and-int/lit16 v4, v4, 0xff shl-int/lit8 v4, v4, 0x8 or-int/2addr v3, v4 shr-int/lit8 v4, p1, 0x10 and-int/lit16 v4, v4, 0xff aget-byte v4, p0, v4 and-int/lit16 v4, v4, 0xff shl-int/lit8 v4, v4, 0x10 or-int/2addr v3, v4 shr-int/lit8 v4, p1, 0x18 and-int/lit16 v4, v4, 0xff aget-byte v0, p0, v4 :try_end_0 .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_1 shl-int/lit8 v0, v0, 0x18 or-int/2addr v0, v3 :cond_0 :goto_1 return v0 :catch_0 move-exception v2 :cond_1 if-nez v1, :cond_0 const/4 v1, 0x2 and-int/lit8 v2, p1, 0x7f :try_start_1 aget-byte v0, p0, v2 :try_end_1 .catch Ljava/lang/Exception; {:try_start_1 .. :try_end_1} :catch_0 shr-int/lit8 v0, v0, 0x8 goto :goto_1 :catch_1 move-exception v3 goto :goto_0 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestOverridePackagePrivateMethod/A.smali ================================================ .class public Ltest/A; .super Ljava/lang/Object; .method a()V # package-private .locals 1 return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestOverridePackagePrivateMethod/B.smali ================================================ .class public Ltest/B; .super Ltest/A; .method public a()V .locals 1 return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestOverridePackagePrivateMethod/C.smali ================================================ .class public Lother/C; .super Ltest/A; .method public a()V .locals 1 return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestOverrideWithSameName/A.smali ================================================ .class interface abstract Ltest/A; .super Ljava/lang/Object; .method public abstract a()Ltest/B; .end method .method public abstract a()Ltest/C; .end method ================================================ FILE: jadx-core/src/test/smali/others/TestOverrideWithSameName/B.smali ================================================ .class abstract Ltest/B; .super Ljava/lang/Object; .implements Ltest/A; .method public a()Ltest/C; .registers 2 const/4 v0, 0x0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestOverrideWithSameName/C.smali ================================================ .class public Ltest/C; .super Ltest/B; .method public a()Ltest/B; .registers 2 const/4 v0, 0x0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/others/TestSuperLoop/A.smali ================================================ .class public LA; .super LB; .field public a:I ================================================ FILE: jadx-core/src/test/smali/others/TestSuperLoop/B.smali ================================================ .class public LB; .super LA; .field public b:I ================================================ FILE: jadx-core/src/test/smali/others/TestSyntheticConstructor/BuggyConstructor.smali ================================================ .class public LBuggyConstructor; .super Ljava/lang/Object; .source "BuggyConstructor.java" #.implements LInterfaceClass; .method public synthetic constructor ()V .locals 0 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/others/TestSyntheticConstructor/Test.smali ================================================ .class public Lothers/Test; .super Ljava/lang/Object; .source "Test.java" .field public static final A00:Ljava/lang/Object; .method public static constructor ()V .locals 1 new-instance v0, LBuggyConstructor; invoke-direct {v0}, LBuggyConstructor;->()V .end method ================================================ FILE: jadx-core/src/test/smali/others/TestUsageApacheHttpClient.smali ================================================ ###### Class Lothers.TestUsageApacheHttpClient (TestUsageApacheHttpClient) .class public Lothers/TestUsageApacheHttpClient; .super Ljava/lang/Object; .source "TestUsageApacheHttpClient.java" # instance fields .field private httpClient:Lorg/apache/http/client/HttpClient; # direct methods .method public constructor ()V .registers 1 .line 3 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/rename/TestUsingSourceFileName/b.smali ================================================ .class Lb; .super Ljava/lang/Object; .source "a.java" .method constructor ()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/special/TestPackageInfoSupport/pkg1.smali ================================================ .class interface abstract synthetic Lspecial/pkg1/package-info; .super Ljava/lang/Object; .source "package-info.java" .annotation runtime Ljava/lang/Deprecated; .end annotation ================================================ FILE: jadx-core/src/test/smali/special/TestPackageInfoSupport/pkg2.smali ================================================ .class interface abstract synthetic Lspecial/pkg2/package-info; .super Ljava/lang/Object; .annotation runtime Lorg/jetbrains/annotations/ApiStatus$Internal; .end annotation ================================================ FILE: jadx-core/src/test/smali/special/TestPackageInfoSupport/pkg3.smali ================================================ .class interface Lspecial/pkg3/package-info; .super Ljava/lang/Object; ================================================ FILE: jadx-core/src/test/smali/switches/TestSwitchOverStrings3.smali ================================================ .class public LTestSwitchOverStrings3; .super Ljava/lang/Object; .method public test3(Ljava/lang/String;)I .registers 5 .line 87 invoke-virtual {p1}, Ljava/lang/String;->hashCode()I move-result v0 const/4 v1, 0x0 const/4 v2, 0x1 packed-switch v0, :pswitch_data_38 :cond_9 goto :goto_32 :pswitch_a const-string v0, "branch4" invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result p1 if-eqz p1, :cond_9 const/4 p1, 0x3 goto :goto_33 :pswitch_14 const-string v0, "branch3" invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result p1 if-eqz p1, :cond_9 const/4 p1, 0x2 goto :goto_33 :pswitch_1e const-string v0, "branch2" invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result p1 if-eqz p1, :cond_9 const/4 p1, 0x1 goto :goto_33 :pswitch_28 const-string v0, "branch1" invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result p1 if-eqz p1, :cond_9 const/4 p1, 0x0 goto :goto_33 :goto_32 const/4 p1, -0x1 :goto_33 packed-switch p1, :pswitch_data_44 .line 94 return v1 .line 90 :pswitch_37 return v2 :pswitch_data_38 .packed-switch 0x8358ecf :pswitch_28 :pswitch_1e :pswitch_14 :pswitch_a .end packed-switch :pswitch_data_44 .packed-switch 0x0 :pswitch_37 :pswitch_37 .end packed-switch .end method ================================================ FILE: jadx-core/src/test/smali/switches/TestSwitchOverStrings4.smali ================================================ .class public LTestSwitchOverStrings4; .super Ljava/lang/Object; .method public static test4(Ljava/lang/String;)I .registers 10 const/16 v3, 0x0 const/16 v4, -0x1 if-nez p0, :cond_26 return v4 :cond_26 .line 202 invoke-virtual {p0}, Ljava/lang/String;->hashCode()I move-result v2 sparse-switch v2, :sswitch_data_222 const/4 v0, -0x1 const/16 v2, 0x13 goto/16 :goto_20a :sswitch_3b const/16 v2, 0x13 const-string/jumbo v1, "video/x-matroska" invoke-virtual {p0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_48 goto/16 :goto_207 :cond_48 const/16 v0, 0x1 goto/16 :goto_20a :sswitch_1fd const/16 v2, 0x13 const-string v1, "audio/eac3-joc" invoke-virtual {p0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-nez v0, :cond_209 :goto_207 const/4 v0, -0x1 goto :goto_20a :cond_209 const/4 v0, 0x0 :goto_20a packed-switch v0, :pswitch_data_29c return v4 :pswitch_216 return v2 :pswitch_221 return v3 :sswitch_data_222 .sparse-switch 0xb269699 -> :sswitch_1fd 0x79909c15 -> :sswitch_3b .end sparse-switch :pswitch_data_29c .packed-switch 0x0 :pswitch_221 :pswitch_216 .end packed-switch .end method ================================================ FILE: jadx-core/src/test/smali/synchronize/TestNestedSynchronize.smali ================================================ .class public final Lsynchronize/TestNestedSynchronize; .super Ljava/lang/Object; .source "TestNestedSynchronize.java" .method public final test()V .locals 2 const/4 v0, 0 const/4 v1, 0 monitor-enter v0 monitor-enter v1 monitor-exit v1 monitor-exit v0 return-void .end method ================================================ FILE: jadx-core/src/test/smali/synchronize/TestSynchronized4.smali ================================================ .class public Lsynchronize/TestSynchronized4; .super Ljava/lang/Object; .field private final obj:Ljava/lang/Object; .method public constructor ()V .registers 2 invoke-direct {p0}, Ljava/lang/Object;->()V new-instance v0, Ljava/lang/Object; invoke-direct {v0}, Ljava/lang/Object;->()V iput-object v0, p0, Lsynchronize/TestSynchronized4;->obj:Ljava/lang/Object; return-void .end method .method public test(I)Z .registers 4 iget-object v1, p0, Lsynchronize/TestSynchronized4;->obj:Ljava/lang/Object; monitor-enter v1 :try_start_3 invoke-direct {p0, p1}, Lsynchronize/TestSynchronized4;->isZero(I)Z move-result v0 if-eqz v0, :cond_11 iget-object v0, p0, Lsynchronize/TestSynchronized4;->obj:Ljava/lang/Object; invoke-direct {p0, v0, p1}, Lsynchronize/TestSynchronized4;->call(Ljava/lang/Object;I)Z move-result v0 monitor-exit v1 :goto_10 return v0 :cond_11 sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; invoke-virtual {v0}, Ljava/io/PrintStream;->println()V invoke-direct {p0}, Lsynchronize/TestSynchronized4;->getField()Ljava/lang/Object; move-result-object v0 if-nez v0, :cond_22 const/4 v0, 0x1 :goto_1d monitor-exit v1 return v0 :catchall_1f move-exception v0 monitor-exit v1 :try_end_21 .catchall {:try_start_3 .. :try_end_21} :catchall_1f throw v0 :cond_22 const/4 v0, 0x0 goto :goto_1d .end method .method private call(Ljava/lang/Object;I)Z .registers 4 const/4 v0, 0x0 return v0 .end method .method private getField()Ljava/lang/Object; .registers 2 const/4 v0, 0x0 return-object v0 .end method .method private isZero(I)Z .registers 3 if-nez p1, :cond_4 const/4 v0, 0x1 :goto_3 return v0 :cond_4 const/4 v0, 0x0 goto :goto_3 .end method ================================================ FILE: jadx-core/src/test/smali/synchronize/TestSynchronized5.smali ================================================ .class public Lsynchronize/TestSynchronized5; .super Ljava/lang/Object; .method public final get()V .registers 6 monitor-enter p0 :try_start_1 const/4 v0, 0 if-eqz v0, :cond_1 monitor-exit p0 return-void :cond_1 monitor-exit p0 :try_end_1 .catchall {:try_start_1 .. :try_end_1} :catchall_1 monitor-enter p0 :try_start_2 const/4 v1, 1 if-eqz v1, :cond_2 invoke-static {}, Ljava/lang/System;->gc()V :cond_2 monitor-exit p0 :try_end_2 .catchall {:try_start_2 .. :try_end_2} :catchall_2 return-void :catchall_2 move-exception v0 :try_start_3 monitor-exit p0 :try_end_3 .catchall {:try_start_3 .. :try_end_3} :catchall_2 throw v0 :catchall_1 move-exception v0 :try_start_4 monitor-exit p0 :try_end_4 .catchall {:try_start_4 .. :try_end_4} :catchall_1 throw v0 .end method ================================================ FILE: jadx-core/src/test/smali/synchronize/TestSynchronized6.smali ================================================ .class public Lsynchronize/TestSynchronized6; .super Ljava/lang/Object; .field private final lock:Ljava/lang/Object; .method public constructor ()V .registers 2 invoke-direct {p0}, Ljava/lang/Object;->()V new-instance v0, Ljava/lang/Object; invoke-direct {v0}, Ljava/lang/Object;->()V iput-object v0, p0, Lsynchronize/TestSynchronized6;->lock:Ljava/lang/Object; return-void .end method .method private test(Ljava/lang/Object;)Z .locals 2 .line 169 iget-object v0, p0, Lsynchronize/TestSynchronized6;->lock:Ljava/lang/Object; monitor-enter v0 .line 170 :try_start_0 invoke-direct {p0, p1}, Lsynchronize/TestSynchronized6;->isA(Ljava/lang/Object;)Z move-result v1 if-nez v1, :cond_1 invoke-direct {p0, p1}, Lsynchronize/TestSynchronized6;->isB(Ljava/lang/Object;)Z move-result p1 if-eqz p1, :cond_0 goto :goto_0 :cond_0 const/4 p1, 0x0 goto :goto_1 :cond_1 :goto_0 const/4 p1, 0x1 :goto_1 monitor-exit v0 return p1 :catchall_0 move-exception p1 .line 171 monitor-exit v0 :try_end_0 .catchall {:try_start_0 .. :try_end_0} :catchall_0 throw p1 .end method .method private isA(Ljava/lang/Object;)Z .registers 3 const/4 v0, 0x0 return v0 .end method .method private isB(Ljava/lang/Object;)Z .registers 3 const/4 v0, 0x0 return v0 .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestEmptyCatch.smali ================================================ .class synthetic Ltrycatch/TestEmptyCatch; .super Ljava/lang/Object; # static fields .field static final synthetic $SwitchMap$Access:[I # direct methods .method static constructor ()V .locals 3 .line 49 invoke-static {}, Lother/EnumAccess;->values()[Lother/EnumAccess; move-result-object v0 array-length v0, v0 new-array v0, v0, [I sput-object v0, Ltrycatch/TestEmptyCatch;->$SwitchMap$Access:[I :try_start_0 sget-object v1, Lother/EnumAccess;->QUERY:Lother/EnumAccess; invoke-virtual {v1}, Ljava/lang/Enum;->ordinal()I move-result v1 const/4 v2, 0x1 aput v2, v0, v1 :try_end_0 .catch Ljava/lang/NoSuchFieldError; {:try_start_0 .. :try_end_0} :catch_0 :catch_0 :try_start_1 sget-object v0, Ltrycatch/TestEmptyCatch;->$SwitchMap$Access:[I sget-object v1, Lother/EnumAccess;->MODIFY:Lother/EnumAccess; invoke-virtual {v1}, Ljava/lang/Enum;->ordinal()I move-result v1 const/4 v2, 0x2 aput v2, v0, v1 :try_end_1 .catch Ljava/lang/NoSuchFieldError; {:try_start_1 .. :try_end_1} :catch_1 :catch_1 :try_start_2 sget-object v0, Ltrycatch/TestEmptyCatch;->$SwitchMap$Access:[I sget-object v1, Lother/EnumAccess;->MODIFY_CONST:Lother/EnumAccess; invoke-virtual {v1}, Ljava/lang/Enum;->ordinal()I move-result v1 const/4 v2, 0x3 aput v2, v0, v1 :try_end_2 .catch Ljava/lang/NoSuchFieldError; {:try_start_2 .. :try_end_2} :catch_2 :catch_2 :try_start_3 sget-object v0, Ltrycatch/TestEmptyCatch;->$SwitchMap$Access:[I sget-object v1, Lother/EnumAccess;->MODIFY_GETTER_SETTER:Lother/EnumAccess; invoke-virtual {v1}, Ljava/lang/Enum;->ordinal()I move-result v1 const/4 v2, 0x4 aput v2, v0, v1 :try_end_3 .catch Ljava/lang/NoSuchFieldError; {:try_start_3 .. :try_end_3} :catch_3 :catch_3 :try_start_4 sget-object v0, Ltrycatch/TestEmptyCatch;->$SwitchMap$Access:[I sget-object v1, Lother/EnumAccess;->CONVERT_ACCESSOR_TO_DATA:Lother/EnumAccess; invoke-virtual {v1}, Ljava/lang/Enum;->ordinal()I move-result v1 const/4 v2, 0x5 aput v2, v0, v1 :try_end_4 .catch Ljava/lang/NoSuchFieldError; {:try_start_4 .. :try_end_4} :catch_4 :catch_4 return-void .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestFinally3.smali ================================================ .class public Ltrycatch/TestFinally3; .super Ljava/lang/Object; .field public bytes:[B .method public test()[B .registers 4 .annotation system Ldalvik/annotation/Throws; value = { Ljava/lang/Exception; } .end annotation const/4 v0, 0x0 :try_start_1 iget-object v1, p0, Ltrycatch/TestFinally3;->bytes:[B if-nez v1, :cond_1a invoke-direct {p0}, Ltrycatch/TestFinally3;->validate()Z :try_end_8 .catchall {:try_start_1 .. :try_end_8} :catchall_24 move-result v1 if-nez v1, :cond_10 invoke-static {v0}, Ltrycatch/TestFinally3;->close(Ljava/io/InputStream;)V return-object v0 :cond_10 :try_start_10 invoke-direct {p0}, Ltrycatch/TestFinally3;->getInputStream()Ljava/io/InputStream; move-result-object v0 invoke-direct {p0, v0}, Ltrycatch/TestFinally3;->read(Ljava/io/InputStream;)[B move-result-object v1 iput-object v1, p0, Ltrycatch/TestFinally3;->bytes:[B :cond_1a iget-object v1, p0, Ltrycatch/TestFinally3;->bytes:[B invoke-direct {p0, v1}, Ltrycatch/TestFinally3;->convert([B)[B :try_end_1f .catchall {:try_start_10 .. :try_end_1f} :catchall_24 move-result-object v1 invoke-static {v0}, Ltrycatch/TestFinally3;->close(Ljava/io/InputStream;)V return-object v1 :catchall_24 move-exception v1 invoke-static {v0}, Ltrycatch/TestFinally3;->close(Ljava/io/InputStream;)V throw v1 .end method .method private convert([B)[B .registers 3 .annotation system Ldalvik/annotation/Throws; value = { Ljava/lang/Exception; } .end annotation const/4 v0, 0x0 new-array v0, v0, [B return-object v0 .end method .method private static close(Ljava/io/InputStream;)V .registers 1 return-void .end method .method private getInputStream()Ljava/io/InputStream; .registers 3 .annotation system Ldalvik/annotation/Throws; value = { Ljava/lang/Exception; } .end annotation new-instance v0, Ljava/io/ByteArrayInputStream; const/4 v1, 0x0 new-array v1, v1, [B invoke-direct {v0, v1}, Ljava/io/ByteArrayInputStream;->([B)V return-object v0 .end method .method private read(Ljava/io/InputStream;)[B .registers 3 .annotation system Ldalvik/annotation/Throws; value = { Ljava/lang/Exception; } .end annotation const/4 v0, 0x0 new-array v0, v0, [B return-object v0 .end method .method private validate()Z .registers 2 .annotation system Ldalvik/annotation/Throws; value = { Ljava/lang/Exception; } .end annotation const/4 v0, 0x0 return v0 .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestLoopInTryCatch.smali ================================================ .class public Ltrycatch/TestLoopInTryCatch; .super Ljava/lang/Object; .source "SourceFile" .method public static test()V .registers 6 :try_start :loop invoke-static {}, Ltrycatch/TestLoopInTryCatch;->getI()I move-result v1 const/4 v2, 0x1 if-eq v1, v2, :cond const/4 v3, 0x2 if-eq v1, v3, :cond goto :loop :cond if-eq v1, v2, :end invoke-static {}, Ltrycatch/TestLoopInTryCatch;->getI()I return-void :try_end .catch Ljava/lang/RuntimeException; {:try_start .. :try_end} :end :end return-void .end method .method public static getI()I .locals 2 const/4 v1, 0x1 return v1 .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestMultiExceptionCatchSameJump.smali ================================================ .class public Ltrycatch/TestMultiExceptionCatchSameJump; .super Ljava/lang/Object; .source "TestMultiExceptionCatchSameJump.java" .method public test()V .locals 2 .line 17 :try_start_0 sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; const-string v1, "Test" invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V :try_end_0 .catch Ljava/security/ProviderException; {:try_start_0 .. :try_end_0} :catch_0 .catch Ljava/time/DateTimeException; {:try_start_0 .. :try_end_0} :catch_0 .line 20 nop .line 22 return-void .line 18 :catch_0 move-exception v0 .line 19 .local v0, "e":Ljava/lang/RuntimeException; new-instance v1, Ljava/lang/RuntimeException; invoke-direct {v1, v0}, Ljava/lang/RuntimeException;->(Ljava/lang/Throwable;)V throw v1 .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestNestedTryCatch4.smali ================================================ .class public Ltrycatch/TestNestedTryCatch4; .super Landroid/app/NativeActivity; .method private test(Landroid/content/Intent;)V .registers 11 .annotation system Ldalvik/annotation/MethodParameters; accessFlags = { 0x0 } names = { "intent" } .end annotation const-string v0, "IOException while closing input stream\n" if-nez p1, :cond_5 return-void :cond_5 const-string v1, "intent_cmd" .line 1740 invoke-virtual {p1, v1}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String; move-result-object v1 const-string v2, "MCPE" if-eqz v1, :cond_80 .line 1741 invoke-virtual {v1}, Ljava/lang/String;->length()I move-result v3 if-lez v3, :cond_80 .line 1743 :try_start_15 new-instance p1, Lorg/json/JSONObject; invoke-direct {p1, v1}, Lorg/json/JSONObject;->(Ljava/lang/String;)V const-string v0, "Command" .line 1744 invoke-virtual {p1, v0}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String; move-result-object v0 const-string v1, "keyboardResult" .line 1745 invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v1 if-eqz v1, :cond_33 const-string v0, "Text" .line 1746 invoke-virtual {p1, v0}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String; move-result-object p1 invoke-virtual {p0, p1}, Ltrycatch/TestNestedTryCatch4;->nativeSetTextboxText(Ljava/lang/String;)V goto/16 :goto_208 :cond_33 const-string v1, "fileDialogResult" .line 1748 invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_208 iget-wide v0, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J const-wide/16 v3, 0x0 cmp-long v5, v0, v3 if-eqz v5, :cond_208 const-string v0, "Result" .line 1749 invoke-virtual {p1, v0}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String; move-result-object v0 const-string v1, "Ok" invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_5d .line 1750 iget-wide v0, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J const-string v5, "Path" invoke-virtual {p1, v5}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String; move-result-object p1 invoke-virtual {p0, v0, v1, p1}, Ltrycatch/TestNestedTryCatch4;->nativeOnPickImageSuccess(JLjava/lang/String;)V goto :goto_62 .line 1753 :cond_5d iget-wide v0, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J invoke-virtual {p0, v0, v1}, Ltrycatch/TestNestedTryCatch4;->nativeOnPickImageCanceled(J)V .line 1755 :goto_62 iput-wide v3, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J :try_end_64 .catch Lorg/json/JSONException; {:try_start_15 .. :try_end_64} :catch_66 goto/16 :goto_208 :catch_66 move-exception p1 .line 1759 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;->()V const-string v1, "JSONObject exception:" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {p1}, Lorg/json/JSONException;->toString()Ljava/lang/String; move-result-object p1 invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p1 invoke-static {v2, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I return-void .line 1765 :cond_80 invoke-virtual {p1}, Landroid/content/Intent;->getAction()Ljava/lang/String; move-result-object v1 .line 1766 invoke-virtual {p1}, Landroid/content/Intent;->getType()Ljava/lang/String; const-string/jumbo v3, "xbox_live_game_invite" .line 1768 invoke-virtual {v3, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v3 if-eqz v3, :cond_b0 const-string/jumbo v0, "xbl" .line 1771 invoke-virtual {p1, v0}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String; move-result-object p1 .line 1772 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;->()V const-string v3, "[XboxLive] Received Invite " invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v0 invoke-static {v2, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I .line 1776 invoke-virtual {p0, v1, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V goto/16 :goto_208 :cond_b0 const-string v3, "android.intent.action.VIEW" .line 1779 invoke-virtual {v3, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v3 if-nez v3, :cond_c0 const-string v3, "org.chromium.arc.intent.action.VIEW" invoke-virtual {v3, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v1 if-eqz v1, :cond_208 .line 1780 :cond_c0 invoke-virtual {p1}, Landroid/content/Intent;->getScheme()Ljava/lang/String; move-result-object v1 .line 1781 invoke-virtual {p1}, Landroid/content/Intent;->getData()Landroid/net/Uri; move-result-object p1 if-nez p1, :cond_cb return-void :cond_cb const-string v3, "minecraft" .line 1787 invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z move-result v3 if-nez v3, :cond_1f9 const-string v3, "minecraftedu" invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z move-result v3 if-eqz v3, :cond_dd goto/16 :goto_1f9 :cond_dd const-string v3, "file" .line 1795 invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z move-result v3 const-string v4, "&" if-eqz v3, :cond_108 .line 1798 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;->()V invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String; move-result-object v1 invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v0, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String; move-result-object p1 invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p1 const-string v0, "fileIntent" invoke-virtual {p0, v0, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V goto/16 :goto_208 :cond_108 const-string v3, "content" .line 1800 invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z move-result v1 if-eqz v1, :cond_208 .line 1803 new-instance v1, Ljava/io/File; invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String; move-result-object v3 invoke-direct {v1, v3}, Ljava/io/File;->(Ljava/lang/String;)V invoke-virtual {v1}, Ljava/io/File;->getName()Ljava/lang/String; move-result-object v1 .line 1804 new-instance v3, Ljava/io/File; new-instance v5, Ljava/lang/StringBuilder; invoke-direct {v5}, Ljava/lang/StringBuilder;->()V invoke-virtual {p0}, Ltrycatch/TestNestedTryCatch4;->getApplicationContext()Landroid/content/Context; move-result-object v6 invoke-virtual {v6}, Landroid/content/Context;->getCacheDir()Ljava/io/File; move-result-object v6 invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder; const-string v6, "/" invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v5, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v1 invoke-direct {v3, v1}, Ljava/io/File;->(Ljava/lang/String;)V .line 1806 invoke-virtual {p0}, Ltrycatch/TestNestedTryCatch4;->getContentResolver()Landroid/content/ContentResolver; move-result-object v1 .line 1810 :try_start_142 invoke-virtual {v1, p1}, Landroid/content/ContentResolver;->openInputStream(Landroid/net/Uri;)Ljava/io/InputStream; move-result-object v1 :try_end_146 .catch Ljava/io/IOException; {:try_start_142 .. :try_end_146} :catch_1df .line 1818 :try_start_146 new-instance v5, Ljava/io/FileOutputStream; invoke-direct {v5, v3}, Ljava/io/FileOutputStream;->(Ljava/io/File;)V const/high16 v6, 0x100000 new-array v6, v6, [B .line 1824 :goto_14f invoke-virtual {v1, v6}, Ljava/io/InputStream;->read([B)I move-result v7 const/4 v8, -0x1 if-eq v7, v8, :cond_15b const/4 v8, 0x0 .line 1825 invoke-virtual {v5, v6, v8, v7}, Ljava/io/OutputStream;->write([BII)V goto :goto_14f .line 1828 :cond_15b invoke-virtual {v5}, Ljava/io/OutputStream;->close()V const-string v5, "contentIntent" .line 1831 new-instance v6, Ljava/lang/StringBuilder; invoke-direct {v6}, Ljava/lang/StringBuilder;->()V invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String; move-result-object p1 invoke-virtual {v6, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v6, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v3}, Ljava/io/File;->getAbsolutePath()Ljava/lang/String; move-result-object p1 invoke-virtual {v6, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v6}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p1 invoke-virtual {p0, v5, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V :try_end_17d .catch Ljava/io/IOException; {:try_start_146 .. :try_end_17d} :catch_18b .catchall {:try_start_146 .. :try_end_17d} :catchall_189 .line 1842 :try_start_17d invoke-virtual {v1}, Ljava/io/InputStream;->close()V :try_end_180 .catch Ljava/io/IOException; {:try_start_17d .. :try_end_180} :catch_182 goto/16 :goto_208 :catch_182 move-exception p1 .line 1845 new-instance v1, Ljava/lang/StringBuilder; invoke-direct {v1}, Ljava/lang/StringBuilder;->()V goto :goto_1b1 :catchall_189 move-exception p1 goto :goto_1c3 :catch_18b move-exception p1 .line 1833 :try_start_18c new-instance v4, Ljava/lang/StringBuilder; invoke-direct {v4}, Ljava/lang/StringBuilder;->()V const-string v5, "IOException while copying file from content intent\n" invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {p1}, Ljava/io/IOException;->toString()Ljava/lang/String; move-result-object p1 invoke-virtual {v4, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v4}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p1 invoke-static {v2, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I :try_end_1a4 .catchall {:try_start_18c .. :try_end_1a4} :catchall_189 .line 1837 :try_start_1a4 invoke-virtual {v3}, Ljava/io/File;->delete()Z :try_end_1a7 .catch Ljava/lang/Exception; {:try_start_1a4 .. :try_end_1a7} :catch_1a7 .catchall {:try_start_1a4 .. :try_end_1a7} :catchall_189 .line 1842 :catch_1a7 :try_start_1a7 invoke-virtual {v1}, Ljava/io/InputStream;->close()V :try_end_1aa .catch Ljava/io/IOException; {:try_start_1a7 .. :try_end_1aa} :catch_1ab goto :goto_208 :catch_1ab move-exception p1 .line 1845 new-instance v1, Ljava/lang/StringBuilder; invoke-direct {v1}, Ljava/lang/StringBuilder;->()V :goto_1b1 invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {p1}, Ljava/io/IOException;->toString()Ljava/lang/String; move-result-object p1 invoke-virtual {v1, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p1 invoke-static {v2, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I goto :goto_208 .line 1842 :goto_1c3 :try_start_1c3 invoke-virtual {v1}, Ljava/io/InputStream;->close()V :try_end_1c6 .catch Ljava/io/IOException; {:try_start_1c3 .. :try_end_1c6} :catch_1c7 goto :goto_1de :catch_1c7 move-exception v1 .line 1845 new-instance v3, Ljava/lang/StringBuilder; invoke-direct {v3}, Ljava/lang/StringBuilder;->()V invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v1}, Ljava/io/IOException;->toString()Ljava/lang/String; move-result-object v0 invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v0 invoke-static {v2, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I .line 1847 :goto_1de throw p1 :catch_1df move-exception p1 .line 1813 new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;->()V const-string v1, "IOException while opening file from content intent\n" invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {p1}, Ljava/io/IOException;->toString()Ljava/lang/String; move-result-object p1 invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p1 invoke-static {v2, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I return-void .line 1788 :cond_1f9 :goto_1f9 invoke-virtual {p1}, Landroid/net/Uri;->getHost()Ljava/lang/String; move-result-object v0 .line 1789 invoke-virtual {p1}, Landroid/net/Uri;->getQuery()Ljava/lang/String; move-result-object p1 if-nez v0, :cond_205 if-eqz p1, :cond_208 .line 1792 :cond_205 invoke-virtual {p0, v0, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V :cond_208 :goto_208 return-void .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestNestedTryCatch5.smali ================================================ .class public Ltrycatch/TestNestedTryCatch5; .super Ljava/lang/Object; .method public test(Landroid/database/sqlite/SQLiteDatabase;)V .registers 13 const/4 v0, 0x0 invoke-static {p1, v0}, LX/7Yz;->A0I(Ljava/lang/Object;I)V iget-boolean v0, p0, LX/00y;->A00:Z if-nez v0, :cond_9e :try_start_8 iget-object v8, p0, LX/00y;->A03:LX/0Ux; iget-object v0, p0, LX/00y;->A04:LX/0Km; invoke-static {p1, v0}, LX/00y;->A01(Landroid/database/sqlite/SQLiteDatabase;LX/0Km;)LX/0g9; move-result-object v10 check-cast v8, LX/0AB; invoke-virtual {v8, v10}, LX/0AB;->A05(LX/0wU;)V iget-object v0, v8, LX/0AB;->A01:LX/0Z4; iget-object v9, v0, LX/0Z4;->A00:Landroidx/work/impl/WorkDatabase_Impl; iput-object v10, v9, LX/0Rt;->A0B:LX/0wU; const-string v0, "PRAGMA foreign_keys = ON" invoke-interface {v10, v0}, LX/0wU;->Auv(Ljava/lang/String;)V iget-object v1, v9, LX/0Rt;->A06:LX/0Uj; iget-object v2, v1, LX/0Uj;->A05:Ljava/lang/Object; monitor-enter v2 :try_end_25 .catchall {:try_start_8 .. :try_end_25} :catchall_95 :try_start_25 iget-boolean v0, v1, LX/0Uj;->A0D:Z if-eqz v0, :cond_31 const-string v1, "ROOM" const-string v0, "Invalidation tracker is initialized twice :/." invoke-static {v1, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I goto :goto_4e :cond_31 const-string v0, "PRAGMA temp_store = MEMORY;" invoke-interface {v10, v0}, LX/0wU;->Auv(Ljava/lang/String;)V const-string v0, "PRAGMA recursive_triggers=\'ON\';" invoke-interface {v10, v0}, LX/0wU;->Auv(Ljava/lang/String;)V const-string v0, "CREATE TEMP TABLE room_table_modification_log (table_id INTEGER PRIMARY KEY, invalidated INTEGER NOT NULL DEFAULT 0)" invoke-interface {v10, v0}, LX/0wU;->Auv(Ljava/lang/String;)V invoke-virtual {v1, v10}, LX/0Uj;->A00(LX/0wU;)V const-string v0, "UPDATE room_table_modification_log SET invalidated = 0 WHERE invalidated = 1" invoke-interface {v10, v0}, LX/0wU;->ArR(Ljava/lang/String;)LX/0wJ; move-result-object v0 iput-object v0, v1, LX/0Uj;->A0C:LX/0wJ; const/4 v0, 0x1 iput-boolean v0, v1, LX/0Uj;->A0D:Z :try_end_4e .catchall {:try_start_25 .. :try_end_4e} :catchall_92 :goto_4e :try_start_4e monitor-exit v2 iget-object v0, v9, LX/0Rt;->A01:Ljava/util/List; if-eqz v0, :cond_8e invoke-interface {v0}, Ljava/util/List;->size()I move-result v7 const/4 v6, 0x0 :goto_58 if-ge v6, v7, :cond_8e iget-object v0, v9, LX/0Rt;->A01:Ljava/util/List; invoke-interface {v0, v6}, Ljava/util/List;->get(I)Ljava/lang/Object; iget-object v5, v10, LX/0g9;->A00:Landroid/database/sqlite/SQLiteDatabase; invoke-virtual {v5}, Landroid/database/sqlite/SQLiteDatabase;->beginTransaction()V :try_end_64 .catchall {:try_start_4e .. :try_end_64} :catchall_95 :try_start_64 invoke-static {}, LX/001;->A0r()Ljava/lang/StringBuilder; move-result-object v4 const-string v0, "DELETE FROM workspec WHERE state IN (2, 3, 5) AND (last_enqueue_time + minimum_retention_duration) < " invoke-virtual {v4, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-static {}, Ljava/lang/System;->currentTimeMillis()J move-result-wide v2 sget-wide v0, LX/0Jm;->A00:J sub-long/2addr v2, v0 invoke-virtual {v4, v2, v3}, Ljava/lang/StringBuilder;->append(J)Ljava/lang/StringBuilder; const-string v0, " AND (SELECT COUNT(*)=0 FROM dependency WHERE prerequisite_id=id AND work_spec_id NOT IN (SELECT id FROM workspec WHERE state IN (2, 3, 5)))" invoke-static {v0, v4}, LX/000;->A0X(Ljava/lang/String;Ljava/lang/StringBuilder;)Ljava/lang/String; move-result-object v0 invoke-interface {v10, v0}, LX/0wU;->Auv(Ljava/lang/String;)V invoke-virtual {v5}, Landroid/database/sqlite/SQLiteDatabase;->setTransactionSuccessful()V :try_end_83 .catchall {:try_start_64 .. :try_end_83} :catchall_89 :try_start_83 invoke-virtual {v5}, Landroid/database/sqlite/SQLiteDatabase;->endTransaction()V add-int/lit8 v6, v6, 0x1 goto :goto_58 :catchall_89 move-exception v0 invoke-virtual {v5}, Landroid/database/sqlite/SQLiteDatabase;->endTransaction()V goto :goto_94 :cond_8e const/4 v0, 0x0 iput-object v0, v8, LX/0AB;->A00:LX/0N8; goto :goto_9e :catchall_92 move-exception v0 monitor-exit v2 :goto_94 throw v0 :try_end_95 .catchall {:try_start_83 .. :try_end_95} :catchall_95 :catchall_95 move-exception v2 sget-object v1, LX/0Fu;->A04:LX/0Fu; new-instance v0, LX/0oe; invoke-direct {v0, v1, v2}, LX/0oe;->(LX/0Fu;Ljava/lang/Throwable;)V throw v0 :cond_9e :goto_9e const/4 v0, 0x1 iput-boolean v0, p0, LX/00y;->A01:Z return-void .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestTryCatch10.smali ================================================ .class public Ltrycatch/TestTryCatch10; .super Ljava/lang/Object; .field public static VERSION:I .method public static test(I)Z .registers 5 sget v0, Ltrycatch/TestTryCatch10;->VERSION:I const/16 v1, 0x1d const/4 v2, 0x0 if-lt v0, v1, :cond_1b const-string v0, "custom" invoke-static {v0}, Ltrycatch/TestTryCatch10;->check(Ljava/lang/String;)Z move-result v0 if-nez v0, :cond_10 goto :goto_1b :cond_10 :try_start_10 invoke-static {p0}, Ltrycatch/TestTryCatch10;->getVar(I)I move-result p0 :try_end_18 .catch Ljava/lang/Exception; {:try_start_10 .. :try_end_18} :catch_1b if-eqz p0, :cond_1b const/4 v2, 0x1 :catch_1b :cond_1b :goto_1b return v2 .end method .method public static getVar(I)I .locals 0 return p0 .end method .method public static check(Ljava/lang/String;)Z .locals 1 const/4 v0, 0x0 return v0 .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestTryCatchFinally10.smali ================================================ .class public Ltrycatch/TestTryCatchFinally10; .super Ljava/lang/Object; # static fields .field private static final l:Llog/DebugLogger; .method public static test(Landroid/content/Context;I)Ljava/lang/String; .locals 2 .line 46 invoke-static {p0}, LCommonContracts;->requireNonNull(Ljava/lang/Object;)V const/4 v0, 0x0 .line 50 :try_start_0 invoke-virtual {p0}, Landroid/content/Context;->getResources()Landroid/content/res/Resources; move-result-object p0 invoke-virtual {p0, p1}, Landroid/content/res/Resources;->openRawResource(I)Ljava/io/InputStream; move-result-object v0 .line 51 new-instance p0, Ljava/util/Scanner; invoke-direct {p0, v0}, Ljava/util/Scanner;->(Ljava/io/InputStream;)V const-string p1, "\\A" invoke-virtual {p0, p1}, Ljava/util/Scanner;->useDelimiter(Ljava/lang/String;)Ljava/util/Scanner; move-result-object p0 .line 52 invoke-virtual {p0}, Ljava/util/Scanner;->hasNext()Z move-result p1 if-eqz p1, :cond_0 invoke-virtual {p0}, Ljava/util/Scanner;->next()Ljava/lang/String; move-result-object p0 goto :goto_0 :cond_0 const-string p0, "" :try_end_0 .catchall {:try_start_0 .. :try_end_0} :catchall_0 :goto_0 if-eqz v0, :cond_1 .line 56 :try_start_1 invoke-virtual {v0}, Ljava/io/InputStream;->close()V :try_end_1 .catch Ljava/io/IOException; {:try_start_1 .. :try_end_1} :catch_0 goto :goto_1 :catch_0 move-exception p1 .line 58 sget-object v0, Ltrycatch/TestTryCatchFinally10;->l:Llog/DebugLogger; sget-object v1, Llog/DebugLogger$LogLevel;->ERROR:Llog/DebugLogger$LogLevel; invoke-virtual {v0, v1, p1}, Llog/DebugLogger;->logException(Llog/DebugLogger$LogLevel;Ljava/lang/Exception;)V :cond_1 :goto_1 return-object p0 :catchall_0 move-exception p0 if-eqz v0, :cond_2 .line 56 :try_start_2 invoke-virtual {v0}, Ljava/io/InputStream;->close()V :try_end_2 .catch Ljava/io/IOException; {:try_start_2 .. :try_end_2} :catch_1 goto :goto_2 :catch_1 move-exception p1 .line 58 sget-object v0, Ltrycatch/TestTryCatchFinally10;->l:Llog/DebugLogger; sget-object v1, Llog/DebugLogger$LogLevel;->ERROR:Llog/DebugLogger$LogLevel; invoke-virtual {v0, v1, p1}, Llog/DebugLogger;->logException(Llog/DebugLogger$LogLevel;Ljava/lang/Exception;)V .line 61 :cond_2 :goto_2 throw p0 .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestTryCatchFinally15.smali ================================================ .class public Ltrycatch/TestTryCatchFinally15; .super Ljava/lang/Object; .implements Landroid/os/IInterface; .field private final zza:Landroid/os/IBinder; .field private final zzb:Ljava/lang/String; .method protected final test(ILandroid/os/Parcel;)Landroid/os/Parcel; .registers 6 .annotation system Ldalvik/annotation/Throws; value = { Landroid/os/RemoteException; } .end annotation .line 1 invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel; move-result-object v0 :try_start_4 iget-object v1, p0, Ltrycatch/TestTryCatchFinally15;->zza:Landroid/os/IBinder; const/4 v2, 0x0 .line 2 invoke-interface {v1, p1, p2, v0, v2}, Landroid/os/IBinder;->transact(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z .line 3 invoke-virtual {v0}, Landroid/os/Parcel;->readException()V :try_end_d .catch Ljava/lang/RuntimeException; {:try_start_4 .. :try_end_d} :catch_13 .catchall {:try_start_4 .. :try_end_d} :catchall_11 .line 6 invoke-virtual {p2}, Landroid/os/Parcel;->recycle()V return-object v0 :catchall_11 move-exception p1 goto :goto_18 .line 5 :catch_13 move-exception p1 .line 4 :try_start_14 invoke-virtual {v0}, Landroid/os/Parcel;->recycle()V .line 5 throw p1 :try_end_18 .catchall {:try_start_14 .. :try_end_18} :catchall_11 .line 6 :goto_18 invoke-virtual {p2}, Landroid/os/Parcel;->recycle()V .line 7 throw p1 .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestTryCatchLastInsn.smali ================================================ .class public Ltrycatch/TestTryCatchLastInsn; .super Ljava/lang/Object; .source "TestTryCatchLastInsn.java" .method public test()Ljava/lang/Exception; .registers 6 .prologue const-string v1, "result" :try_start invoke-direct {p0}, Ltrycatch/TestTryCatchLastInsn;->call()Ljava/lang/Exception; move-result-object v1 :try_end .catch Ljava/lang/Exception; {:try_start .. :try_end} :catch :goto_return return-object v1 :catch move-exception v4 sget-object v3, Ljava/lang/System;->out:Ljava/io/PrintStream; invoke-virtual {v3, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V move-object v1, v4 goto :goto_return .end method .method private call()Ljava/lang/Exception; .registers 2 new-instance v0, Ljava/lang/Exception; invoke-direct {v0}, Ljava/lang/Exception;->()V return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestTryCatchMultiException2.smali ================================================ .class public Ltrycatch/TestTryCatchMultiException2; .super Ljava/lang/Object; .method public static test()Z .registers 5 :try_start_b const-string v0, "c" invoke-static {v0}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class; move-result-object v1 const/4 v0, 0x0 const-string v2, "b" new-array v3, v0, [Ljava/lang/Class; invoke-virtual {v1, v2, v3}, Ljava/lang/Class;->getMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method; move-result-object v2 new-array v3, v0, [Ljava/lang/Object; invoke-virtual {v2, v1, v3}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; move-result-object v1 check-cast v1, Ljava/lang/Boolean; invoke-virtual {v1}, Ljava/lang/Boolean;->booleanValue()Z move-result v1 :try_end_2f .catch Ljava/lang/ClassNotFoundException; {:try_start_b .. :try_end_2f} :catch_30 .catch Ljava/lang/NoSuchMethodException; {:try_start_b .. :try_end_2f} :catch_30 .catch Ljava/lang/Exception; {:try_start_b .. :try_end_2f} :catch_30 .catchall {:try_start_b .. :try_end_2f} :catchall_30 return v1 :catch_30 :catchall_30 return v0 .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestTryCatchNoMoveExc.smali ================================================ .class public Ltrycatch/TestTryCatchNoMoveExc; .super Ljava/lang/Object; .method private static test(Ljava/lang/AutoCloseable;)V .locals 0 if-eqz p0, :cond_0 .line 187 :try_start_0 invoke-interface {p0}, Ljava/lang/AutoCloseable;->close()V :try_end_0 .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0 :catch_0 :cond_0 return-void .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestTryCatchNoMoveExc2.smali ================================================ .class public Ltrycatch/TestTryCatchNoMoveExc2; .super Ljava/lang/Object; .method private static test(Ljava/lang/AutoCloseable;)V .locals 0 :try_start_0 invoke-interface {p0}, Ljava/lang/AutoCloseable;->close()V :try_end_0 .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0 :catch_0 invoke-static {}, Ljava/lang/System;->nanoTime()J return-void .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestTryCatchStartOnMove.smali ================================================ .class public Ltrycatch/TestTryCatchStartOnMove; .super Ljava/lang/Object; # direct methods .method private static test(Ljava/lang/String;)V .registers 5 :try_start move v3, p0 invoke-static {v3}, Ltrycatch/TestTryCatchStartOnMove;->call(Ljava/lang/String;)V :try_end .catch Ljava/lang/Exception; {:try_start .. :try_end} :catch :goto_ret return-void :catch move-exception v0 sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; new-instance v1, Ljava/lang/StringBuilder; invoke-direct {v1}, Ljava/lang/StringBuilder;->()V const-string v2, "Failed call for " invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1 invoke-virtual {v1, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v1 invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v1 invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V goto :goto_ret .end method .method public constructor ()V .registers 1 invoke-direct {p0}, Ljadx/tests/api/SmaliTest;->()V return-void .end method .method private static call(Ljava/lang/String;)V .registers 1 return-void .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestTryWithEmptyCatchTriple.smali ================================================ .class public Ltrycatch/TestTryWithEmptyCatchTriple; .super Ljava/lang/Object; .field static field:[I .method static test()V .registers 3 const/4 v0, 0x1 :try_start_1 sget-object v1, Ltrycatch/TestTryWithEmptyCatchTriple;->field:[I const/4 v2, 0x0 aput v0, v1, v2 :try_end_1 .catch Ljava/lang/Error; {:try_start_1 .. :try_end_1} :catch_1 :catch_1 sget-object v1, Ltrycatch/TestTryWithEmptyCatchTriple;->field:[I array-length v1, v1 new-array v1, v1, [I sput-object v1, Ltrycatch/TestTryWithEmptyCatchTriple;->field:[I :try_start_2 sget-object v1, Ltrycatch/TestTryWithEmptyCatchTriple;->field:[I const/4 v2, 0x0 aput v0, v1, v2 :try_end_2 .catch Ljava/lang/Error; {:try_start_2 .. :try_end_2} :catch_2 :catch_2 :try_start_3 sget-object v0, Ltrycatch/TestTryWithEmptyCatchTriple;->field:[I const/4 v1, 0x0 const/4 v2, 0x2 aput v2, v0, v1 :try_end_3 .catch Ljava/lang/Error; {:try_start_3 .. :try_end_3} :catch_3 :catch_3 return-void .end method ================================================ FILE: jadx-core/src/test/smali/trycatch/TestUnreachableCatch.smali ================================================ .class public Ltrycatch/TestUnreachableCatch; .super Ljava/lang/Object; .method private static prepareFontData(Landroid/content/Context;[Landroid/provider/FontsContract$FontInfo;Landroid/os/CancellationSignal;)Ljava/util/Map; .locals 18 .param p0, "context" # Landroid/content/Context; .param p1, "fonts" # [Landroid/provider/FontsContract$FontInfo; .param p2, "cancellationSignal" # Landroid/os/CancellationSignal; .annotation system Ldalvik/annotation/Signature; value = { "(", "Landroid/content/Context;", "[", "Landroid/provider/FontsContract$FontInfo;", "Landroid/os/CancellationSignal;", ")", "Ljava/util/Map<", "Landroid/net/Uri;", "Ljava/nio/ByteBuffer;", ">;" } .end annotation .line 728 move-object/from16 v1, p1 new-instance v0, Ljava/util/HashMap; invoke-direct {v0}, Ljava/util/HashMap;->()V move-object v2, v0 .line 729 .local v2, "out":Ljava/util/HashMap;, "Ljava/util/HashMap;" invoke-virtual/range {p0 .. p0}, Landroid/content/Context;->getContentResolver()Landroid/content/ContentResolver; move-result-object v3 .line 731 .local v3, "resolver":Landroid/content/ContentResolver; array-length v4, v1 const/4 v0, 0x0 move v5, v0 :goto_0 if-ge v5, v4, :cond_5 aget-object v6, v1, v5 .line 732 .local v6, "font":Landroid/provider/FontsContract$FontInfo; invoke-virtual {v6}, Landroid/provider/FontsContract$FontInfo;->getResultCode()I move-result v0 if-eqz v0, :cond_0 .line 733 move-object/from16 v9, p2 goto/16 :goto_5 .line 736 :cond_0 invoke-virtual {v6}, Landroid/provider/FontsContract$FontInfo;->getUri()Landroid/net/Uri; move-result-object v7 .line 737 .local v7, "uri":Landroid/net/Uri; invoke-virtual {v2, v7}, Ljava/util/HashMap;->containsKey(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_1 .line 738 move-object/from16 v9, p2 goto :goto_5 .line 741 :cond_1 const/4 v8, 0x0 .line 742 .local v8, "buffer":Ljava/nio/ByteBuffer; :try_start_0 const-string/jumbo v0, "r" :try_end_0 .catch Ljava/io/IOException; {:try_start_0 .. :try_end_0} :catch_2 .line 743 move-object/from16 v9, p2 :try_start_1 invoke-virtual {v3, v7, v0, v9}, Landroid/content/ContentResolver;->openFileDescriptor(Landroid/net/Uri;Ljava/lang/String;Landroid/os/CancellationSignal;)Landroid/os/ParcelFileDescriptor; move-result-object v0 :try_end_1 .catch Ljava/io/IOException; {:try_start_1 .. :try_end_1} :catch_1 move-object v10, v0 .line 744 .local v10, "pfd":Landroid/os/ParcelFileDescriptor; if-eqz v10, :cond_3 .line 745 :try_start_2 new-instance v0, Ljava/io/FileInputStream; .line 746 invoke-virtual {v10}, Landroid/os/ParcelFileDescriptor;->getFileDescriptor()Ljava/io/FileDescriptor; move-result-object v11 invoke-direct {v0, v11}, Ljava/io/FileInputStream;->(Ljava/io/FileDescriptor;)V :try_end_2 .catch Ljava/io/IOException; {:try_start_2 .. :try_end_2} :catch_0 .catchall {:try_start_2 .. :try_end_2} :catchall_2 move-object v11, v0 .line 747 .local v11, "fis":Ljava/io/FileInputStream; :try_start_3 invoke-virtual {v11}, Ljava/io/FileInputStream;->getChannel()Ljava/nio/channels/FileChannel; move-result-object v12 .line 748 .local v12, "fileChannel":Ljava/nio/channels/FileChannel; invoke-virtual {v12}, Ljava/nio/channels/FileChannel;->size()J move-result-wide v16 .line 749 .local v16, "size":J sget-object v13, Ljava/nio/channels/FileChannel$MapMode;->READ_ONLY:Ljava/nio/channels/FileChannel$MapMode; const-wide/16 v14, 0x0 invoke-virtual/range {v12 .. v17}, Ljava/nio/channels/FileChannel;->map(Ljava/nio/channels/FileChannel$MapMode;JJ)Ljava/nio/MappedByteBuffer; move-result-object v0 :try_end_3 .catchall {:try_start_3 .. :try_end_3} :catchall_0 move-object v8, v0 .line 750 .end local v12 # "fileChannel":Ljava/nio/channels/FileChannel; .end local v16 # "size":J :try_start_4 invoke-virtual {v11}, Ljava/io/FileInputStream;->close()V :try_end_4 .catch Ljava/io/IOException; {:try_start_4 .. :try_end_4} :catch_0 .catchall {:try_start_4 .. :try_end_4} :catchall_2 .line 752 .end local v11 # "fis":Ljava/io/FileInputStream; goto :goto_3 .line 745 .restart local v11 # "fis":Ljava/io/FileInputStream; :catchall_0 move-exception v0 move-object v12, v0 :try_start_5 invoke-virtual {v11}, Ljava/io/FileInputStream;->close()V :try_end_5 .catchall {:try_start_5 .. :try_end_5} :catchall_1 goto :goto_1 :catchall_1 move-exception v0 move-object v13, v0 :try_start_6 invoke-virtual {v12, v13}, Ljava/lang/Throwable;->addSuppressed(Ljava/lang/Throwable;)V .end local v2 # "out":Ljava/util/HashMap;, "Ljava/util/HashMap;" .end local v3 # "resolver":Landroid/content/ContentResolver; .end local v6 # "font":Landroid/provider/FontsContract$FontInfo; .end local v7 # "uri":Landroid/net/Uri; .end local v8 # "buffer":Ljava/nio/ByteBuffer; .end local v10 # "pfd":Landroid/os/ParcelFileDescriptor; .end local p0 # "context":Landroid/content/Context; .end local p1 # "fonts":[Landroid/provider/FontsContract$FontInfo; .end local p2 # "cancellationSignal":Landroid/os/CancellationSignal; :goto_1 throw v12 :try_end_6 .catch Ljava/io/IOException; {:try_start_6 .. :try_end_6} :catch_0 .catchall {:try_start_6 .. :try_end_6} :catchall_2 .line 742 .end local v11 # "fis":Ljava/io/FileInputStream; .restart local v2 # "out":Ljava/util/HashMap;, "Ljava/util/HashMap;" .restart local v3 # "resolver":Landroid/content/ContentResolver; .restart local v6 # "font":Landroid/provider/FontsContract$FontInfo; .restart local v7 # "uri":Landroid/net/Uri; .restart local v8 # "buffer":Ljava/nio/ByteBuffer; .restart local v10 # "pfd":Landroid/os/ParcelFileDescriptor; .restart local p0 # "context":Landroid/content/Context; .restart local p1 # "fonts":[Landroid/provider/FontsContract$FontInfo; .restart local p2 # "cancellationSignal":Landroid/os/CancellationSignal; :catchall_2 move-exception v0 move-object v11, v0 if-eqz v10, :cond_2 :try_start_7 invoke-virtual {v10}, Landroid/os/ParcelFileDescriptor;->close()V :try_end_7 .catchall {:try_start_7 .. :try_end_7} :catchall_3 goto :goto_2 :catchall_3 move-exception v0 move-object v12, v0 :try_start_8 invoke-virtual {v11, v12}, Ljava/lang/Throwable;->addSuppressed(Ljava/lang/Throwable;)V .end local v2 # "out":Ljava/util/HashMap;, "Ljava/util/HashMap;" .end local v3 # "resolver":Landroid/content/ContentResolver; .end local v6 # "font":Landroid/provider/FontsContract$FontInfo; .end local v7 # "uri":Landroid/net/Uri; .end local v8 # "buffer":Ljava/nio/ByteBuffer; .end local p0 # "context":Landroid/content/Context; .end local p1 # "fonts":[Landroid/provider/FontsContract$FontInfo; .end local p2 # "cancellationSignal":Landroid/os/CancellationSignal; :cond_2 :goto_2 throw v11 .line 750 .restart local v2 # "out":Ljava/util/HashMap;, "Ljava/util/HashMap;" .restart local v3 # "resolver":Landroid/content/ContentResolver; .restart local v6 # "font":Landroid/provider/FontsContract$FontInfo; .restart local v7 # "uri":Landroid/net/Uri; .restart local v8 # "buffer":Ljava/nio/ByteBuffer; .restart local p0 # "context":Landroid/content/Context; .restart local p1 # "fonts":[Landroid/provider/FontsContract$FontInfo; .restart local p2 # "cancellationSignal":Landroid/os/CancellationSignal; :catch_0 move-exception v0 .line 754 :cond_3 :goto_3 if-eqz v10, :cond_4 invoke-virtual {v10}, Landroid/os/ParcelFileDescriptor;->close()V :try_end_8 .catch Ljava/io/IOException; {:try_start_8 .. :try_end_8} :catch_1 .line 756 .end local v10 # "pfd":Landroid/os/ParcelFileDescriptor; :cond_4 goto :goto_4 .line 754 :catch_1 move-exception v0 goto :goto_4 :catch_2 move-exception v0 move-object/from16 v9, p2 .line 760 :goto_4 invoke-virtual {v2, v7, v8}, Ljava/util/HashMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; .line 731 .end local v6 # "font":Landroid/provider/FontsContract$FontInfo; .end local v7 # "uri":Landroid/net/Uri; .end local v8 # "buffer":Ljava/nio/ByteBuffer; :goto_5 add-int/lit8 v5, v5, 0x1 goto :goto_0 .line 762 :cond_5 move-object/from16 v9, p2 invoke-static {v2}, Ljava/util/Collections;->unmodifiableMap(Ljava/util/Map;)Ljava/util/Map; move-result-object v0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/types/TestConstInline.smali ================================================ .class public Ltypes/TestConstInline; .super Ljava/lang/Object; .method private static test(Z)Ljava/lang/String; .registers 4 .param p0, "b" # Z if-eqz p0, :cond_d invoke-static {}, Ltypes/TestConstInline;->list()Ljava/util/List; move-result-object v0 const-string v1, "1" goto :goto_return :cond_d const/4 v2, 0x0 # chained move instead zero const loading move v0, v2 move v1, v0 goto :goto_return :goto_return invoke-static {v0, v1}, Ltypes/TestConstInline;->use(Ljava/util/List;Ljava/lang/String;)Ljava/lang/String; move-result-object v2 return-object v2 .end method .method private static use(Ljava/util/List;Ljava/lang/String;)Ljava/lang/String; .registers 3 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/util/List", "<", "Ljava/lang/String;", ">;", "Ljava/lang/String;", ")", "Ljava/lang/String;" } .end annotation new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;->()V invoke-virtual {v0, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder; move-result-object v0 invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v0 invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v0 return-object v0 .end method .method private static list()Ljava/util/List; .registers 1 .annotation system Ldalvik/annotation/Signature; value = { "()", "Ljava/util/List", "<", "Ljava/lang/String;", ">;" } .end annotation invoke-static {}, Ljava/util/Collections;->emptyList()Ljava/util/List; move-result-object v0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/types/TestGenerics2.smali ================================================ .class public final Ltypes/TestGenerics2; .super Ljava/lang/Object; .source "SourceFile" # instance fields .field private field:Ljava/util/Map; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/util/Map<", "Ljava/lang/Integer;", "Ljava/lang/String;", ">;" } .end annotation .end field .method public test()V .registers 5 iget-object v4, p0, Ltypes/TestGenerics2;->field:Ljava/util/Map; invoke-interface {v4}, Ljava/util/Map;->size()I move-result v0 invoke-static {v0}, Ltypes/TestGenerics2;->useInt(I)V invoke-interface {v4}, Ljava/util/Map;->entrySet()Ljava/util/Set; move-result-object v4 invoke-interface {v4}, Ljava/util/Set;->iterator()Ljava/util/Iterator; move-result-object v4 :goto_16 invoke-interface {v4}, Ljava/util/Iterator;->hasNext()Z move-result v0 if-eqz v0, :ret invoke-interface {v4}, Ljava/util/Iterator;->next()Ljava/lang/Object; move-result-object v0 invoke-interface {v0}, Ljava/util/Map$Entry;->getKey()Ljava/lang/Object; move-result-object v1 check-cast v1, Ljava/lang/Integer; invoke-virtual {v1}, Ljava/lang/Integer;->intValue()I move-result v1 invoke-static {v1}, Ltypes/TestGenerics2;->useInt(I)V invoke-interface {v0}, Ljava/util/Map$Entry;->getValue()Ljava/lang/Object; move-result-object v0 check-cast v0, Ljava/lang/String; invoke-interface {v0, p1}, Ljava/lang/String;->trim()Ljava/lang/String; goto :goto_16 :ret return-void .end method .method public static useInt(I)V .registers 3 return-void .end method ================================================ FILE: jadx-core/src/test/smali/types/TestGenericsInFullInnerCls/FieldCls.smali ================================================ .class public Ltypes/FieldCls; .super Ljava/lang/Object; .field private a:Landroidx/compose/animation/core/bb; .annotation system Ldalvik/annotation/Signature; value = { "Ltypes/test/ba<", "Ltypes/n;", ">.types/test/bb<", "Ltypes/n;", "Ltypes/n;", ">;" } .end annotation .end field ================================================ FILE: jadx-core/src/test/smali/types/TestGenericsInFullInnerCls/ba.smali ================================================ .class public final Ltypes/test/ba; .super Ljava/lang/Object; .annotation system Ldalvik/annotation/Signature; value = { "", "Ljava/lang/Object;" } .end annotation ================================================ FILE: jadx-core/src/test/smali/types/TestGenericsInFullInnerCls/bb.smali ================================================ .class public final Ltypes/test/bb; .super Ljava/lang/Object; # annotations .annotation system Ldalvik/annotation/Signature; value = { "", "Ljava/lang/Object;" } .end annotation .field private b:Ltypes/test/ba; .annotation system Ldalvik/annotation/Signature; value = { "Ltypes/test/ba<", "TS;>;" } .end annotation .end field .field private c:Landroidx/compose/animation/core/bc; .annotation system Ldalvik/annotation/Signature; value = { "Ltypes/test/ba<", "TS;>.types/test/bb.types/test/bc;" } .end annotation .end field ================================================ FILE: jadx-core/src/test/smali/types/TestGenericsInFullInnerCls/bc.smali ================================================ .class public final Ltypes/test/bc; .super Ljava/lang/Object; # annotations .annotation system Ldalvik/annotation/Signature; value = { "", "Ljava/lang/Object;", "Ltypes/test/ca<", "TT;>;" } .end annotation .field private a:Ltypes/test/ba; .annotation system Ldalvik/annotation/Signature; value = { "Ltypes/test/ba<", "TS;>;" } .end annotation .end field ================================================ FILE: jadx-core/src/test/smali/types/TestGenericsInFullInnerCls/n.smali ================================================ .class public Ltypes/n; .super Ljava/lang/Object; ================================================ FILE: jadx-core/src/test/smali/types/TestPrimitiveConversion.smali ================================================ .class public Ltypes/TestPrimitiveConversion; .super Ljava/lang/Object; .method public test(JZ)V .registers 5 invoke-static {p1, p2, p3}, Ltypes/TestPrimitiveConversion;->putByte(JB)V return-void .end method .method private static putByte(JB)V .registers 3 return-void .end method ================================================ FILE: jadx-core/src/test/smali/types/TestPrimitiveConversion2.smali ================================================ .class public Ltypes/TestPrimitiveConversion2; .super Ljava/lang/Object; .method protected test(Landroid/widget/TextView;Lapp/ItemCurrency;Lapp/ItemCurrency;Lapp/ItemCurrency;Lapp/ItemCurrency;ZLapp/SearchListItem;)Z .locals 4 .annotation system Ldalvik/annotation/Signature; value = { "(", "Landroid/widget/TextView;", "Lapp/ItemCurrency;", "Lapp/ItemCurrency;", "Lapp/ItemCurrency;", "Lapp/ItemCurrency;", "ZLapp/SearchListItem;)Z" } .end annotation .line 573 invoke-direct {p0, p2, p3}, Lapp/DefaultItemAdapter;->getConvertedPrice(Lapp/ItemCurrency;Lapp/ItemCurrency;)Lapp/ItemCurrency; move-result-object p3 const/4 v0, 0x0 if-eqz p3, :cond_3 .line 577 iget-object v1, p3, Lapp/ItemCurrency;->code:Ljava/lang/String; iget-object p2, p2, Lapp/ItemCurrency;->code:Ljava/lang/String; invoke-virtual {v1, p2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result p2 const/4 v1, 0x1 xor-int/2addr p2, v1 or-int/lit8 v2, p2, 0x2 .line 581 iget-object v3, p3, Lapp/ItemCurrency;->value:Ljava/lang/String; iget-object p3, p3, Lapp/ItemCurrency;->code:Ljava/lang/String; invoke-virtual {p0, v3, p3, v2}, Lapp/ItemAdapter;->formatCurrency(Ljava/lang/String;Ljava/lang/String;I)Ljava/lang/String; move-result-object p3 if-eqz p2, :cond_0 if-eqz p3, :cond_0 .line 586 new-instance v1, Ljava/lang/StringBuilder; invoke-direct {v1}, Ljava/lang/StringBuilder;->()V invoke-virtual {v1, p3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; const-string p3, " " invoke-virtual {v1, p3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p3 const/4 v1, 0x2 :cond_0 if-eqz p6, :cond_1 if-eqz p7, :cond_1 .line 594 invoke-direct {p0, p4, p5}, Lapp/DefaultItemAdapter;->getConvertedPrice(Lapp/ItemCurrency;Lapp/ItemCurrency;)Lapp/ItemCurrency; move-result-object p5 if-eqz p5, :cond_1 .line 598 iget-object p6, p5, Lapp/ItemCurrency;->code:Ljava/lang/String; iget-object p4, p4, Lapp/ItemCurrency;->code:Ljava/lang/String; invoke-virtual {p6, p4}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result p4 .line 599 iget-object p5, p5, Lapp/ItemCurrency;->code:Ljava/lang/String; invoke-direct {p0, p5, p3, p7, p4}, Lapp/DefaultItemAdapter;->getPrice(Ljava/lang/String;Ljava/lang/String;Lapp/SearchListItem;Z)Landroid/text/Spannable; move-result-object v0 :cond_1 if-nez v0, :cond_2 .line 605 sget-object p4, Landroid/graphics/Typeface;->DEFAULT:Landroid/graphics/Typeface; invoke-virtual {p1, p4, v1}, Landroid/widget/TextView;->setTypeface(Landroid/graphics/Typeface;I)V .line 606 invoke-virtual {p1, p3}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V goto :goto_0 .line 609 :cond_2 invoke-virtual {p1, v0}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V goto :goto_0 .line 612 :cond_3 invoke-virtual {p1, v0}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V const/4 p2, 0x0 :goto_0 return p2 .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver10.smali ================================================ .class public Ltypes/TestTypeResolver10; .super Ljava/lang/Object; .source "SourceFile" .method private test(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object; .locals 2 invoke-virtual {p1}, Ljava/lang/String;->isEmpty()Z move-result v0 const/4 v1, 0x0 if-eqz v0, :cond_0 return-object v1 :cond_0 invoke-virtual {p2}, Ljava/lang/String;->isEmpty()Z move-result v0 if-eqz v0, :cond_1 return-object v1 :cond_1 :try_start_0 invoke-static {p2}, Ljava/lang/Integer;->parseInt(Ljava/lang/String;)I move-result p2 if-eqz p2, :cond_4 const/4 v0, 0x1 if-eq p2, v0, :cond_3 const/4 v0, 0x3 if-eq p2, v0, :cond_2 goto :goto_1 .line 4 :cond_2 invoke-static {p1}, Ljava/lang/Boolean;->parseBoolean(Ljava/lang/String;)Z move-result p1 invoke-static {p1}, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean; move-result-object p1 :cond_3 :goto_0 move-object v1, p1 goto :goto_1 .line 5 :cond_4 invoke-static {p1}, Ljava/lang/Integer;->valueOf(Ljava/lang/String;)Ljava/lang/Integer; move-result-object p1 :try_end_0 .catch Ljava/lang/NumberFormatException; {:try_start_0 .. :try_end_0} :catch_0 goto :goto_0 :catch_0 move-exception p1 .line 6 invoke-virtual {p1}, Ljava/lang/NumberFormatException;->printStackTrace()V :goto_1 return-object v1 .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver14.smali ================================================ .class public Ltypes/TestTypeResolver14; .super Ljava/lang/Object; .source "SourceFile" .method public test()Ljava/util/Date; .registers 5 .annotation system Ldalvik/annotation/Throws; value = { Ljava/lang/Exception; } .end annotation .line 472 const/4 v2, 0x0 const/4 v3, 0x0 invoke-static {v3, v2}, Landroidx/room/util/DBUtil;->query(ZLandroid/os/CancellationSignal;)Landroid/database/Cursor; move-result-object v0 .line 475 :try_start_e invoke-interface {v0}, Landroid/database/Cursor;->moveToFirst()Z move-result v1 if-eqz v1, :cond_2d .line 477 invoke-interface {v0, v3}, Landroid/database/Cursor;->isNull(I)Z move-result v1 if-eqz v1, :cond_1b goto :goto_23 .line 480 :cond_1b invoke-interface {v0, v3}, Landroid/database/Cursor;->getLong(I)J move-result-wide v1 invoke-static {v1, v2}, Ljava/lang/Long;->valueOf(J)Ljava/lang/Long; move-result-object v2 .line 482 :goto_23 iget-object v1, p0, Ltypes/TestTypeResolver14$8;->this$0:Ltypes/TestTypeResolver14; invoke-virtual {v1, v2}, Ltypeconverters/DateTypeConverter;->toDate(Ljava/lang/Long;)Ljava/util/Date; move-result-object v2 :try_end_2d .catchall {:try_start_e .. :try_end_2d} :catchall_31 .line 488 :cond_2d invoke-interface {v0}, Landroid/database/Cursor;->close()V return-object v2 :catchall_31 move-exception v1 invoke-interface {v0}, Landroid/database/Cursor;->close()V .line 489 throw v1 .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver15.smali ================================================ .class public Ltypes/TestTypeResolver15; .super Ljava/lang/Object; .method private test(Z)V .locals 2 if-eqz p1, :cond_0 const/4 v1, 0x0 goto :goto_0 :cond_0 const/16 v1, 0x8 :goto_0 invoke-virtual {p0, v1}, Ltypes/TestTypeResolver15;->useInt(I)V xor-int/lit8 p1, p1, 0x1 invoke-virtual {p0, p1}, Ltypes/TestTypeResolver15;->useInt(I)V return-void .end method .method private useInt(I)V .registers 2 return-void .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver16.smali ================================================ .class public Ltypes/TestTypeResolver16; .super Ljava/lang/Object; .method public final test(Ljava/util/List;Ljava/util/Set;Ljava/util/function/Function;)Ljava/util/List; .locals 1 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/util/List<", "+TT;>;", "Ljava/util/Set<", "+TT;>;", "Ljava/util/function/Function<", "-TT;+TK;>;)", "Ljava/util/List<", "TT;>;" } .end annotation if-eqz p2, :cond_1 if-eqz p1, :cond_0 .line 85 move-object v0, p1 check-cast v0, Ljava/util/Collection; check-cast p2, Ljava/lang/Iterable; invoke-static {v0, p2, p3}, Ltypes/TestTypeResolver16;->union(Ljava/util/Collection;Ljava/lang/Iterable;Ljava/util/function/Function;)Ljava/util/List; move-result-object p2 goto :goto_0 :cond_0 const/4 p2, 0x0 :goto_0 if-eqz p2, :cond_1 move-object p1, p2 :cond_1 if-eqz p1, :cond_2 goto :goto_1 :cond_2 invoke-static {}, Ltypes/TestTypeResolver16;->emptyList()Ljava/util/List; move-result-object p1 :goto_1 return-object p1 .end method .method public static final union(Ljava/util/Collection;Ljava/lang/Iterable;Ljava/util/function/Function;)Ljava/util/List; .locals 4 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/util/Collection<", "+TT;>;", "Ljava/lang/Iterable<", "+TT;>;", "Ljava/util/function/Function<", "-TT;+TK;>;)", "Ljava/util/List<", "TT;>;" } .end annotation const/4 v0, 0x0 return-object v0 .end method .method public static final emptyList()Ljava/util/List; .locals 1 .annotation system Ldalvik/annotation/Signature; value = { "()", "Ljava/util/List<", "TT;>;" } .end annotation const/4 v0, 0x0 return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver17.smali ================================================ .class public Ltypes/TestTypeResolver17; .super Ljava/lang/Object; .method private static closeQuietly(Ljava/lang/AutoCloseable;)V .locals 0 return-void .end method .method private static test(Landroid/content/Context;Landroid/net/Uri;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; .locals 7 .line 159 invoke-virtual {p0}, Landroid/content/Context;->getContentResolver()Landroid/content/ContentResolver; move-result-object v0 const/4 p0, 0x1 const/4 v6, 0x0 :try_start_0 new-array v2, p0, [Ljava/lang/String; const/4 p0, 0x0 aput-object p2, v2, p0 const/4 v3, 0x0 const/4 v4, 0x0 const/4 v5, 0x0 move-object v1, p1 .line 163 invoke-virtual/range {v0 .. v5}, Landroid/content/ContentResolver;->query(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor; move-result-object v6 .line 164 invoke-interface {v6}, Landroid/database/Cursor;->moveToFirst()Z move-result p1 if-eqz p1, :cond_0 invoke-interface {v6, p0}, Landroid/database/Cursor;->isNull(I)Z move-result p1 if-nez p1, :cond_0 .line 165 invoke-interface {v6, p0}, Landroid/database/Cursor;->getString(I)Ljava/lang/String; move-result-object p0 :try_end_0 .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0 .catchall {:try_start_0 .. :try_end_0} :catchall_0 .line 173 invoke-static {v6}, Ltypes/TestTypeResolver17;->closeQuietly(Ljava/lang/AutoCloseable;)V return-object p0 :cond_0 invoke-static {v6}, Ltypes/TestTypeResolver17;->closeQuietly(Ljava/lang/AutoCloseable;)V return-object p3 :catchall_0 move-exception p0 goto :goto_0 :catch_0 move-exception p0 :try_start_1 const-string p1, "DocumentFile" .line 170 new-instance p2, Ljava/lang/StringBuilder; invoke-direct {p2}, Ljava/lang/StringBuilder;->()V const-string v0, "Failed query: " invoke-virtual {p2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {p2, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder; invoke-virtual {p2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p0 invoke-static {p1, p0}, Landroid/util/Log;->w(Ljava/lang/String;Ljava/lang/String;)I :try_end_1 .catchall {:try_start_1 .. :try_end_1} :catchall_0 .line 173 invoke-static {v6}, Ltypes/TestTypeResolver17;->closeQuietly(Ljava/lang/AutoCloseable;)V return-object p3 :goto_0 invoke-static {v6}, Ltypes/TestTypeResolver17;->closeQuietly(Ljava/lang/AutoCloseable;)V throw p0 .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver20/Sequence.smali ================================================ .class public interface abstract Lkotlin/sequences/Sequence; .super Ljava/lang/Object; .source "SourceFile" .annotation system Ldalvik/annotation/Signature; value = { "", "Ljava/lang/Object;" } .end annotation .method public abstract iterator()Ljava/util/Iterator; .annotation system Ldalvik/annotation/Signature; value = { "()", "Ljava/util/Iterator<", "TT;>;" } .end annotation .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver20/TestTypeResolver20.smali ================================================ .class public Ltypes/TestTypeResolver20; .super Ljava/lang/Object; .source "SourceFile" .method public static final max(Lkotlin/sequences/Sequence;)Ljava/lang/Comparable; .registers 4 .annotation system Ldalvik/annotation/Signature; value = { ";>(", "Lkotlin/sequences/Sequence<", "+TT;>;)TT;" } .end annotation .line 1147 invoke-interface {p0}, Lkotlin/sequences/Sequence;->iterator()Ljava/util/Iterator; move-result-object p0 .line 1148 invoke-interface {p0}, Ljava/util/Iterator;->hasNext()Z move-result v0 if-nez v0, :cond_11 const/4 p0, 0x0 return-object p0 .line 1149 :cond_11 invoke-interface {p0}, Ljava/util/Iterator;->next()Ljava/lang/Object; move-result-object v0 check-cast v0, Ljava/lang/Comparable; .line 1150 :cond_17 :goto_17 invoke-interface {p0}, Ljava/util/Iterator;->hasNext()Z move-result v1 if-eqz v1, :cond_2b .line 1151 invoke-interface {p0}, Ljava/util/Iterator;->next()Ljava/lang/Object; move-result-object v1 check-cast v1, Ljava/lang/Comparable; .line 1152 invoke-interface {v0, v1}, Ljava/lang/Comparable;->compareTo(Ljava/lang/Object;)I move-result v2 if-gez v2, :cond_17 move-object v0, v1 goto :goto_17 :cond_2b return-object v0 .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver21.smali ================================================ .class public Ltypes/TestTypeResolver21; .super Ljava/lang/Object; .source "TestTypeResolver21.java" .method public test(Ljava/lang/Object;)Ljava/lang/Number; .registers 4 .param p1, "objectArray" # Ljava/lang/Object; .prologue .line 16 check-cast p1, [Ljava/lang/Object; .end local p1 # "objectArray":Ljava/lang/Object; move-object v0, p1 check-cast v0, [Ljava/lang/Object; .line 17 .local v0, "arr":[Ljava/lang/Object; const/4 v1, 0x0 aget-object v1, v0, v1 check-cast v1, Ljava/lang/Number; return-object v1 .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver24/T1.smali ================================================ .class LT1; .super Ljava/lang/Object; .method public foo1()V .registers 1 return-void .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver24/T2.smali ================================================ .class LT2; .super Ljava/lang/Object; .method public foo2()V .registers 1 return-void .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver24/Test1.smali ================================================ .class public LTest1; .super Ljava/lang/Object; .method public test()V .registers 3 const/4 v0, 0x0 move-object v1, v0 check-cast v1, LT1; invoke-virtual {v0}, LT1;->foo1()V move-object v1, v0 check-cast v1, LT2; invoke-virtual {v0}, LT2;->foo2()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver25.smali ================================================ .class public abstract Ltypes/TestTypeResolver25; .super Ljava/lang/Object; .field public final a:Ljava/util/Map; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/util/Map<", "Ljava/lang/String;", "Ljava/lang/Object;", ">;" } .end annotation .end field .field public volatile b:Z .method public k(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; .registers 6 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/lang/String;", "TT;)TT;" } .end annotation .line 1 iget-object v0, p0, Ltypes/TestTypeResolver25;->a:Ljava/util/Map; monitor-enter v0 .line 2 :try_start_3 iget-object v1, p0, Ltypes/TestTypeResolver25;->a:Ljava/util/Map; invoke-interface {v1, p1}, Ljava/util/Map;->get(Ljava/lang/Object;)Ljava/lang/Object; move-result-object v1 if-nez v1, :cond_10 .line 3 iget-object v2, p0, Ltypes/TestTypeResolver25;->a:Ljava/util/Map; invoke-interface {v2, p1, p2}, Ljava/util/Map;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; .line 4 :cond_10 monitor-exit v0 :try_end_11 .catchall {:try_start_3 .. :try_end_11} :catchall_2c if-nez v1, :cond_14 goto :goto_15 :cond_14 move-object p2, v1 .line 5 :goto_15 iget-boolean p1, p0, Ltypes/TestTypeResolver25;->b:Z if-eqz p1, :cond_2b .line 6 instance-of p1, p2, Ljava/io/Closeable; if-eqz p1, :cond_2b .line 7 :try_start_1d move-object p1, p2 check-cast p1, Ljava/io/Closeable; invoke-interface {p1}, Ljava/io/Closeable;->close()V :try_end_23 .catch Ljava/io/IOException; {:try_start_1d .. :try_end_23} :catch_24 goto :goto_2b :catch_24 move-exception p1 .line 8 new-instance p2, Ljava/lang/RuntimeException; invoke-direct {p2, p1}, Ljava/lang/RuntimeException;->(Ljava/lang/Throwable;)V throw p2 :cond_2b :goto_2b return-object p2 :catchall_2c move-exception p1 .line 9 :try_start_2d monitor-exit v0 :try_end_2e .catchall {:try_start_2d .. :try_end_2e} :catchall_2c throw p1 .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver5.smali ================================================ .class public Ltypes/TestTypeResolver5; .super Landroid/content/Context; .source "SourceFile" .field public static final EXTERNAL_SOURCE:Ljava/lang/String; = "externalsource" .field public static final IS_APPBOY_CAMPAIGN:Ljava/lang/String; = "appBoyCampaign" .field public static final IS_NEWS_FEED:Ljava/lang/String; = "isNewsFeed" # direct methods .method public constructor ()V .locals 0 .prologue .line 35 invoke-direct {p0}, Landroid/content/Context;->()V return-void .end method .method private openNextScreen(Landroid/os/Bundle;)V .locals 3 .prologue const/4 v1, 0x0 .line 56 if-eqz p1, :cond_2 const-string v0, "externalsource" invoke-virtual {p1, v0}, Landroid/os/Bundle;->containsKey(Ljava/lang/String;)Z move-result v0 if-eqz v0, :cond_2 const-string v0, "externalsource" .line 57 invoke-virtual {p1, v0}, Landroid/os/Bundle;->getString(Ljava/lang/String;)Ljava/lang/String; move-result-object v0 move-object v2, v0 .line 58 :goto_0 if-eqz p1, :cond_3 const-string v0, "isNewsFeed" invoke-virtual {p1, v0}, Landroid/os/Bundle;->containsKey(Ljava/lang/String;)Z move-result v0 if-eqz v0, :cond_3 const-string v0, "isNewsFeed" .line 59 invoke-virtual {p1, v0}, Landroid/os/Bundle;->getBoolean(Ljava/lang/String;)Z move-result v0 .line 61 :goto_1 invoke-static {v2}, Landroid/text/TextUtils;->isEmpty(Ljava/lang/CharSequence;)Z move-result v2 if-nez v2, :cond_0 const/4 v1, 0x1 .line 64 :cond_0 if-eqz p1, :cond_1 .line 65 new-instance v2, Landroid/webkit/WebView; invoke-direct {v2, p0}, Landroid/webkit/WebView;->(Landroid/content/Context;)V .line 66 invoke-direct {p0, v2, p1}, Ltypes/TestTypeResolver5;->runJavaScriptForCampaign(Landroid/webkit/WebView;Landroid/os/Bundle;)V .line 70 :cond_1 if-eqz v0, :cond_4 .line 72 invoke-direct {p0, p1}, Ltypes/TestTypeResolver5;->startHomeActivity(Landroid/os/Bundle;)V .line 73 invoke-virtual {p0}, Ltypes/TestTypeResolver5;->finish()V .line 80 :goto_2 return-void .line 57 :cond_2 const-string v0, "" move-object v2, v0 goto :goto_0 :cond_3 move v0, v1 .line 59 goto :goto_1 .line 74 :cond_4 invoke-virtual {p0}, Ltypes/TestTypeResolver5;->isTaskRoot()Z move-result v0 if-nez v0, :cond_5 if-eqz v1, :cond_6 .line 76 :cond_5 invoke-direct {p0, p1}, Ltypes/TestTypeResolver5;->openSplash(Landroid/os/Bundle;)V goto :goto_2 .line 78 :cond_6 invoke-virtual {p0}, Ltypes/TestTypeResolver5;->finish()V goto :goto_2 .end method .method private openSplash(Landroid/os/Bundle;)V .locals 1 return-void .end method .method private runJavaScriptForCampaign(Landroid/webkit/WebView;Landroid/os/Bundle;)V .locals 1 return-void .end method .method private startHomeActivity(Landroid/os/Bundle;)V .locals 1 return-void .end method .method public onBackPressed()V .locals 1 return-void .end method .method public onCreate(Landroid/os/Bundle;)V .locals 1 return-void .end method .method protected onPause()V .locals 1 return-void .end method .method protected onStart()V .locals 1 return-void .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver8/A.smali ================================================ .class public Ltypes/TestCls$A; .super Ljava/lang/Object; # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Ljadx/tests/integration/types/TestTypeResolver8$TestCls; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x9 name = "A" .end annotation # direct methods .method public constructor ()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver8/B.smali ================================================ .class public Ltypes/TestCls$B; .super Ljava/lang/Object; # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Ltypes/TestCls; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x9 name = "B" .end annotation # direct methods .method public constructor (Ltypes/A;)V .registers 2 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method ================================================ FILE: jadx-core/src/test/smali/types/TestTypeResolver8/TestCls.smali ================================================ .class public Ltypes/TestCls; .super Ljava/lang/Object; # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Ljadx/tests/integration/types/TestTypeResolver8; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x9 name = "TestCls" .end annotation .annotation system Ldalvik/annotation/MemberClasses; value = { Ltypes/TestCls$B;, Ltypes/TestCls$A; } .end annotation # instance fields .field private f:Ltypes/TestCls$A; # direct methods .method public constructor ()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method private use(Ltypes/TestCls$B;)V .registers 2 return-void .end method # virtual methods .method public test()V .registers 3 iget-object v1, p0, Ltypes/TestCls;->f:Ltypes/TestCls$A; if-eqz v1, :cond_a new-instance v0, Ltypes/TestCls$B; invoke-direct {v0, v1}, Ltypes/TestCls$B;->(Ltypes/TestCls$A;)V move v1, v0 :cond_a invoke-direct {p0, v1}, Ltypes/TestCls;->use(Ltypes/TestCls$B;)V return-void .end method ================================================ FILE: jadx-core/src/test/smali/variables/TestThisBranchDup.smali ================================================ .class public final Lvariables/TestThisBranchDup; .super Ljava/lang/Object; .method public constructor (ZZZLh3/t;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V .registers 10 and-int/lit8 p8, p7, 0x1 if-eqz p8, :cond_5 const/4 p1, 0x0 :cond_5 and-int/lit8 p8, p7, 0x2 const/4 v0, 0x1 if-eqz p8, :cond_b move p2, v0 :cond_b and-int/lit8 p8, p7, 0x4 if-eqz p8, :cond_10 move p3, v0 :cond_10 and-int/lit8 p8, p7, 0x8 if-eqz p8, :cond_16 .line 11 sget-object p4, Lh3/t;->Inherit:Lh3/t; :cond_16 and-int/lit8 p8, p7, 0x10 if-eqz p8, :cond_1b move p5, v0 :cond_1b and-int/lit8 p7, p7, 0x20 if-eqz p7, :cond_27 move p8, v0 move-object p6, p4 move p7, p5 move p4, p2 move p5, p3 move-object p2, p0 move p3, p1 goto :goto_2e :cond_27 move p8, p6 move p7, p5 move p5, p3 move-object p6, p4 move p3, p1 move p4, p2 move-object p2, p0 .line 12 :goto_2e invoke-direct/range {p2 .. p8}, Lvariables/TestThisBranchDup;->(ZZZLh3/t;ZZ)V return-void .end method ================================================ FILE: jadx-core/src/test/smali/variables/TestVariables6.smali ================================================ .class public LTestVariables6; .super Lcom/paypal/android/p2pmobile/wallet/banksandcards/fragments/BasePaymentFragment; .source "SourceFile" # interfaces .implements Landroid/support/v13/app/FragmentCompat$OnRequestPermissionsResultCallback; .implements Landroid/widget/TextView$OnEditorActionListener; .implements Lcom/paypal/android/p2pmobile/common/utils/ISafeClickVerifierListener; .implements Lcom/paypal/android/p2pmobile/common/widgets/CSCTextWatcher$ICSCTextWatcherListener; # annotations .annotation system Ldalvik/annotation/MemberClasses; value = { Lcom/paypal/android/p2pmobile/wallet/banksandcards/fragments/EnterCardFragment$IEnterCardFragmentListener; } .end annotation .field private static final DATE_SEPARATOR:C = '/' .field mDateFormatOrder:Lcom/paypal/android/p2pmobile/common/utils/ValidatedDateFormatOrder; # direct methods .method static constructor ()V .locals 0 return-void .end method .method public constructor ()V .locals 1 return-void .end method .method private bindStartDateToMutableCredebitCard(Lcom/paypal/android/foundation/wallet/model/MutableCredebitCard;)Z .locals 10 .param p1 # Lcom/paypal/android/foundation/wallet/model/MutableCredebitCard; .annotation build Landroid/support/annotation/NonNull; .end annotation .end param .line 1024 iget-object v0, p0, LTestVariables6;->mFinancialInstrumentMetadataDefinition:Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataDefinition; invoke-virtual {v0}, Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataDefinition;->getStartMonth()Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataAttribute; move-result-object v0 .line 1025 iget-object v1, p0, LTestVariables6;->mFinancialInstrumentMetadataDefinition:Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataDefinition; invoke-virtual {v1}, Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataDefinition;->getStartYear()Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataAttribute; move-result-object v1 const/4 v2, 0x2 .line 1026 new-array v2, v2, [Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataAttribute; const/4 v3, 0x0 aput-object v0, v2, v3 const/4 v0, 0x1 aput-object v1, v2, v0 invoke-static {v2}, Lcom/paypal/android/p2pmobile/wallet/banksandcards/utils/EnterCardFragmentUtils;->attributesAreRequired([Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataAttribute;)Z move-result v2 if-nez v2, :cond_0 return v0 .line 1030 :cond_0 invoke-virtual {p0}, LTestVariables6;->getView()Landroid/view/View; move-result-object v2 if-nez v2, :cond_1 return v3 .line 1035 :cond_1 invoke-virtual {v1}, Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataAttribute;->getMaximumLength()I move-result v6 .line 1036 sget v1, Lcom/paypal/android/p2pmobile/wallet/R$id;->enter_card_start_date:I invoke-virtual {v2, v1}, Landroid/view/View;->findViewById(I)Landroid/view/View; move-result-object v1 check-cast v1, Landroid/widget/TextView; .line 1038 new-instance v2, Lcom/paypal/android/p2pmobile/common/utils/DateStringParser; invoke-virtual {v1}, Landroid/widget/TextView;->getText()Ljava/lang/CharSequence; move-result-object v1 invoke-interface {v1}, Ljava/lang/CharSequence;->toString()Ljava/lang/String; move-result-object v5 iget-object v7, p0, LTestVariables6;->mDateFormatOrder:Lcom/paypal/android/p2pmobile/common/utils/ValidatedDateFormatOrder; const/16 v8, 0x2f const/4 v9, 0x0 move-object v4, v2 invoke-direct/range {v4 .. v9}, Lcom/paypal/android/p2pmobile/common/utils/DateStringParser;->(Ljava/lang/String;ILcom/paypal/android/p2pmobile/common/utils/ValidatedDateFormatOrder;CZ)V .line 1039 invoke-virtual {v2}, Lcom/paypal/android/p2pmobile/common/utils/DateStringParser;->isError()Z move-result v1 if-nez v1, :cond_2 .line 1040 invoke-virtual {v2}, Lcom/paypal/android/p2pmobile/common/utils/DateStringParser;->getDate()Ljava/util/Date; move-result-object v1 invoke-virtual {p1, v1}, Lcom/paypal/android/foundation/wallet/model/MutableCredebitCard;->setIssueDate(Ljava/util/Date;)V return v0 :cond_2 return v3 .end method ================================================ FILE: jadx-core/src/test/smali/variables/TestVariablesGeneric.smali ================================================ .class public Lvariables/TestVariablesGeneric; .super Ljava/lang/Object; .source "SourceFile" .method public static a(Lrx/i;Lrx/c;)Lrx/j; .locals 3 .annotation system Ldalvik/annotation/Signature; value = { "(", "Lrx/i<", "-TT;>;", "Lrx/c<", "TT;>;)", "Lrx/j;" } .end annotation if-nez p0, :cond_0 .line 10325 new-instance p0, Ljava/lang/IllegalArgumentException; const-string p1, "subscriber can not be null" invoke-direct {p0, p1}, Ljava/lang/IllegalArgumentException;->(Ljava/lang/String;)V throw p0 .line 10327 :cond_0 iget-object v0, p1, Lrx/c;->a:Lrx/c$a; if-nez v0, :cond_1 .line 10328 new-instance p0, Ljava/lang/IllegalStateException; const-string p1, "onSubscribe function can not be null." invoke-direct {p0, p1}, Ljava/lang/IllegalStateException;->(Ljava/lang/String;)V throw p0 .line 10336 :cond_1 invoke-virtual {p0}, Lrx/i;->onStart()V .line 10343 instance-of v0, p0, Lrx/c/c; if-nez v0, :cond_2 .line 10345 new-instance v0, Lrx/c/c; invoke-direct {v0, p0}, Lrx/c/c;->(Lrx/i;)V move-object p0, v0 .line 10352 :cond_2 :try_start_0 iget-object v0, p1, Lrx/c;->a:Lrx/c$a; invoke-static {p1, v0}, Lrx/d/c;->a(Lrx/c;Lrx/c$a;)Lrx/c$a; move-result-object p1 invoke-interface {p1, p0}, Lrx/c$a;->call(Ljava/lang/Object;)V .line 10353 invoke-static {p0}, Lrx/d/c;->a(Lrx/j;)Lrx/j; move-result-object p1 :try_end_0 .catch Ljava/lang/Throwable; {:try_start_0 .. :try_end_0} :catch_0 return-object p1 :catch_0 move-exception p1 .line 10356 invoke-static {p1}, Lrx/exceptions/a;->b(Ljava/lang/Throwable;)V .line 10358 invoke-virtual {p0}, Lrx/i;->isUnsubscribed()Z move-result v0 if-eqz v0, :cond_3 .line 10359 invoke-static {p1}, Lrx/d/c;->b(Ljava/lang/Throwable;)Ljava/lang/Throwable; move-result-object p0 invoke-static {p0}, Lrx/d/c;->a(Ljava/lang/Throwable;)V goto :goto_0 .line 10363 :cond_3 :try_start_1 invoke-static {p1}, Lrx/d/c;->b(Ljava/lang/Throwable;)Ljava/lang/Throwable; move-result-object v0 invoke-virtual {p0, v0}, Lrx/i;->onError(Ljava/lang/Throwable;)V :try_end_1 .catch Ljava/lang/Throwable; {:try_start_1 .. :try_end_1} :catch_1 .line 10375 :goto_0 invoke-static {}, Lrx/f/e;->b()Lrx/j; move-result-object p0 return-object p0 :catch_1 move-exception p0 .line 10365 invoke-static {p0}, Lrx/exceptions/a;->b(Ljava/lang/Throwable;)V .line 10368 new-instance v0, Lrx/exceptions/OnErrorFailedException; new-instance v1, Ljava/lang/StringBuilder; const-string v2, "Error occurred attempting to subscribe [" invoke-direct {v1, v2}, Ljava/lang/StringBuilder;->(Ljava/lang/String;)V invoke-virtual {p1}, Ljava/lang/Throwable;->getMessage()Ljava/lang/String; move-result-object p1 invoke-virtual {v1, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; const-string p1, "] and then again while trying to pass to onError." invoke-virtual {v1, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object p1 invoke-direct {v0, p1, p0}, Lrx/exceptions/OnErrorFailedException;->(Ljava/lang/String;Ljava/lang/Throwable;)V .line 10370 invoke-static {v0}, Lrx/d/c;->b(Ljava/lang/Throwable;)Ljava/lang/Throwable; .line 10372 throw v0 .end method ================================================ FILE: jadx-core/src/test/smali/variables/TestVariablesInLoop.smali ================================================ .class public abstract Lvariables/TestVariablesInLoop; .super Ljava/lang/Object; .source "SourceFile" .implements Ljava/util/List; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/lang/Object;", "Ljava/util/List<", "Ljava/lang/Long;", ">;" } .end annotation .method static test(Ljava/util/List;)I .locals 5 .annotation system Ldalvik/annotation/Signature; value = { "(", "Ljava/util/List<", "Ljava/lang/Long;", ">;)I" } .end annotation invoke-interface {p0}, Ljava/util/List;->size()I move-result v0 const/4 v1, 0x0 if-nez v0, :cond_0 return v1 :cond_0 instance-of v2, p0, Lvariables/TestVariablesInLoop; if-eqz v2, :cond_1 check-cast p0, Lvariables/TestVariablesInLoop; const/4 v2, 0x0 :goto_0 if-ge v1, v0, :cond_2 invoke-virtual {p0, v1}, Lvariables/TestVariablesInLoop;->getLong(I)J move-result-wide v3 invoke-static {v3, v4}, Lvariables/TestVariablesInLoop;->mth(J)I move-result v3 add-int/2addr v2, v3 add-int/lit8 v1, v1, 0x1 goto :goto_0 :cond_1 const/4 v2, 0x0 :goto_1 if-ge v1, v0, :cond_2 invoke-interface {p0, v1}, Ljava/util/List;->get(I)Ljava/lang/Object; move-result-object v3 check-cast v3, Ljava/lang/Long; invoke-virtual {v3}, Ljava/lang/Long;->longValue()J move-result-wide v3 invoke-static {v3, v4}, Lvariables/TestVariablesInLoop;->mth(J)I move-result v3 add-int/2addr v2, v3 add-int/lit8 v1, v1, 0x1 goto :goto_1 :cond_2 return v2 .end method .method public final getLong(I)J .locals 2 const/16 v0, 0x0 return-wide v0 .end method .method static mth(J)I .locals 2 const/4 v0, 0x0 return v0 .end method ================================================ FILE: jadx-gui/build.gradle.kts ================================================ plugins { id("jadx-kotlin") id("application") id("jadx-library") id("com.gradleup.shadow") version "8.3.8" id("edu.sc.seis.launch4j") version "4.0.0" id("org.beryx.runtime") version "2.0.1" } dependencies { implementation(project(":jadx-core")) implementation(project(":jadx-cli")) implementation(project(":jadx-plugins-tools")) implementation(project(":jadx-commons:jadx-app-commons")) // import mappings implementation(project(":jadx-plugins:jadx-rename-mappings")) implementation("org.jcommander:jcommander:2.0") implementation("ch.qos.logback:logback-classic:1.5.21") implementation("io.github.oshai:kotlin-logging-jvm:7.0.13") implementation("com.fifesoft:rsyntaxtextarea:3.6.1") implementation("com.fifesoft:autocomplete:3.3.2") implementation("org.drjekyll:fontchooser:3.1.0") implementation("hu.kazocsaba:image-viewer:1.2.3") implementation("com.twelvemonkeys.imageio:imageio-webp:3.12.0") // WebP support for image viewer implementation("com.formdev:flatlaf:3.7") implementation("com.formdev:flatlaf-intellij-themes:3.7") implementation("com.formdev:flatlaf-extras:3.7") implementation("com.formdev:flatlaf-fonts-inter:4.1") implementation("com.formdev:flatlaf-fonts-jetbrains-mono:2.304") implementation("com.google.code.gson:gson:2.13.2") implementation("org.apache.commons:commons-lang3:3.20.0") implementation("org.apache.commons:commons-text:1.15.0") implementation("commons-io:commons-io:2.21.0") implementation("io.reactivex.rxjava3:rxjava:3.1.12") implementation("com.github.akarnokd:rxjava3-swing:3.1.1") implementation("com.android.tools.build:apksig:8.13.1") implementation("io.github.skylot:jdwp:2.0.0") // Library for hex viewing data val bined = "0.2.2" implementation("org.exbin.bined:bined-swing:$bined") implementation("org.exbin.bined:bined-highlight-swing:$bined") implementation("org.exbin.bined:bined-swing-section:$bined") implementation("org.exbin.auxiliary:binary_data:$bined") implementation("org.exbin.auxiliary:binary_data-array:$bined") // Library for rendering GraphViz DOT files implementation("guru.nidi:graphviz-java:0.18.1") implementation("com.eclipsesource.j2v8:j2v8_linux_x86_64:4.6.0") implementation("com.eclipsesource.j2v8:j2v8_win32_x86_64:4.6.0") testImplementation(project.project(":jadx-core").sourceSets.getByName("test").output) } val jadxVersion: String by rootProject.extra tasks.test { exclude("**/tmp/*") } application { applicationName = ("jadx-gui") mainClass.set("jadx.gui.JadxGUI") applicationDefaultJvmArgs = listOf( "-Xms128M", "-XX:MaxRAMPercentage=70.0", "-Dawt.useSystemAAFontSettings=lcd", "-Dswing.aatext=true", "-Djava.util.Arrays.useLegacyMergeSort=true", // disable zip checks (#1962) "-Djdk.util.zip.disableZip64ExtraFieldValidation=true", // needed for ktlint formatter "-XX:+IgnoreUnrecognizedVMOptions", "--add-opens=java.base/java.lang=ALL-UNNAMED", // Foreign API access for 'directories' library (Windows only) "--enable-native-access=ALL-UNNAMED", // flags to fix UI ghosting (#2225) "-Dsun.java2d.noddraw=true", "-Dsun.java2d.d3d=false", "-Dsun.java2d.ddforcevram=true", "-Dsun.java2d.ddblit=false", "-Dswing.useflipBufferStrategy=true", ) applicationDistribution.from("$rootDir") { include("README.md") include("NOTICE") include("LICENSE") } } tasks.jar { manifest { attributes(mapOf("Main-Class" to application.mainClass.get())) } } tasks.shadowJar { isZip64 = true mergeServiceFiles() manifest { from(tasks.jar.get().manifest) } } // workaround to exclude shadowJar 'all' artifact from publishing to maven project.components.withType(AdhocComponentWithVariants::class.java).forEach { c -> c.withVariantsFromConfiguration(project.configurations.shadowRuntimeElements.get()) { skip() } } tasks.startShadowScripts { doLast { val newWindowsScriptContent = windowsScript.readText() .replace("java.exe", "javaw.exe") .replace("\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS%", "start \"jadx-gui\" /B \"%JAVA_EXE%\" %DEFAULT_JVM_OPTS%") // Add launch script path as a property val newUnixScriptContent = unixScript.readText() .replace( Regex("DEFAULT_JVM_OPTS=.+", RegexOption.MULTILINE), { result -> result.value + "\" \\\"-Djadx.launchScript.path=\$(realpath $0)\\\"\"" }, ) windowsScript.writeText(newWindowsScriptContent) unixScript.writeText(newUnixScriptContent) } } launch4j { mainClassName.set(application.mainClass.get()) copyConfigurable.set(listOf()) dontWrapJar.set(true) icon.set("$projectDir/src/main/resources/logos/jadx-logo.ico") outfile.set("jadx-gui-$jadxVersion.exe") version.set(jadxVersion) copyright.set("Skylot") windowTitle.set("jadx") companyName.set("jadx") jreMinVersion.set("11") jvmOptions.set(escapeJVMOptions()) requires64Bit.set(true) downloadUrl.set("https://www.oracle.com/java/technologies/downloads/#jdk21-windows") supportUrl.set("https://github.com/skylot/jadx") bundledJrePath.set(if (project.hasProperty("bundleJRE")) "%EXEDIR%/jre" else "%JAVA_HOME%") classpath.set(tasks.getByName("shadowJar").outputs.files.map { "%EXEDIR%/lib/${it.name}" }.sorted().toList()) println("Launch4J classpath: ${classpath.get()}") chdir.set("") // don't change current dir libraryDir.set("") // don't add any libs } fun escapeJVMOptions(): List { return application.applicationDefaultJvmArgs .toList() .map { if (it.startsWith("-D")) "\"$it\"" else it } } runtime { addOptions("--strip-debug", "--no-header-files", "--no-man-pages") addModules( "java.desktop", "java.naming", "java.xml", // needed for "https" protocol to download plugins and updates "jdk.crypto.cryptoki", "jdk.accessibility", ) jpackage { imageOptions = listOf("--icon", "$projectDir/src/main/resources/logos/jadx-logo.ico") skipInstaller = true targetPlatformName = "win" } launcher { noConsole = true } } val copyDistWin by tasks.registering(Copy::class) { description = "Copy files for Windows bundle" val libTask = tasks.getByName("shadowJar") dependsOn(libTask) from(libTask.outputs) { include("*.jar") into("lib") } val exeTask = tasks.getByName("createExe") dependsOn(exeTask) from(exeTask.outputs) { include("*.exe") } into(layout.buildDirectory.dir("jadx-gui-win")) duplicatesStrategy = DuplicatesStrategy.EXCLUDE } val copyDistWinWithJre by tasks.registering(Copy::class) { description = "Copy files for Windows with JRE bundle" val jreTask = tasks.runtime.get() dependsOn(jreTask) from(jreTask.jreDir) { include("**/*") into("jre") } val libTask = tasks.getByName("shadowJar") dependsOn(libTask) from(libTask.outputs) { include("*.jar") into("lib") } val exeTask = tasks.getByName("createExe") dependsOn(exeTask) from(exeTask.outputs) { include("*.exe") } into(layout.buildDirectory.dir("jadx-gui-with-jre-win")) duplicatesStrategy = DuplicatesStrategy.EXCLUDE } /** * Register and expose distribution artifacts to use in top level packaging tasks */ val distWinConfiguration by configurations.creating { isCanBeResolved = false } val distWinWithJreConfiguration by configurations.creating { isCanBeResolved = false } artifacts { add(distWinConfiguration.name, copyDistWin) add(distWinWithJreConfiguration.name, copyDistWinWithJre) } val syncNLSLines by tasks.registering(JavaExec::class) { group = "jadx-dev" description = "Utility task to sync new/missing translation using EN as a reference" classpath = sourceSets.main.get().runtimeClasspath mainClass.set("jadx.gui.utils.tools.SyncNLSLines") } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/JadxGUI.java ================================================ package jadx.gui; import java.awt.Desktop; import javax.swing.SwingUtilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.cli.JadxCLIArgs; import jadx.cli.config.JadxConfigAdapter; import jadx.commons.app.JadxSystemInfo; import jadx.core.Jadx; import jadx.core.utils.files.FileUtils; import jadx.gui.logs.LogCollector; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsData; import jadx.gui.ui.MainWindow; import jadx.gui.utils.LafManager; import jadx.gui.utils.NLS; public class JadxGUI { private static final Logger LOG = LoggerFactory.getLogger(JadxGUI.class); public static void main(String[] args) { try { JadxConfigAdapter configAdapter = JadxSettings.buildConfigAdapter(); JadxSettingsData settingsData = JadxCLIArgs.processArgs(args, new JadxSettingsData(), configAdapter); if (settingsData == null) { return; } JadxSettings settings = new JadxSettings(configAdapter); settings.loadSettingsData(settingsData); LogCollector.register(); printSystemInfo(); NLS.setLocale(settings.getLangLocale()); SwingUtilities.invokeLater(() -> { LafManager.init(settings); settings.getFontSettings().updateDefaultFont(); MainWindow mw = new MainWindow(settings); registerOpenFileHandler(mw); mw.init(); }); } catch (Exception e) { LOG.error("Error: {}", e.getMessage(), e); System.exit(1); } } private static void registerOpenFileHandler(MainWindow mw) { try { if (Desktop.isDesktopSupported()) { Desktop desktop = Desktop.getDesktop(); if (desktop.isSupported(Desktop.Action.APP_OPEN_FILE)) { desktop.setOpenFileHandler(e -> mw.open(FileUtils.toPaths(e.getFiles()))); } } } catch (Throwable e) { LOG.error("Failed to register open file handler", e); } } private static void printSystemInfo() { if (LOG.isDebugEnabled()) { LOG.debug("Starting jadx-gui. Version: '{}'. JVM: {} {}. OS: {}, version: {}, arch: {}", Jadx.getVersion(), JadxSystemInfo.JAVA_VM, JadxSystemInfo.JAVA_VER, JadxSystemInfo.OS_NAME, JadxSystemInfo.OS_VERSION, JadxSystemInfo.OS_ARCH); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/JadxWrapper.java ================================================ package jadx.gui; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import jadx.api.JavaNode; import jadx.api.JavaPackage; import jadx.api.ResourceFile; import jadx.api.impl.InMemoryCodeCache; import jadx.api.metadata.ICodeNodeRef; import jadx.api.plugins.pass.JadxPassInfo; import jadx.api.plugins.pass.impl.SimpleJadxPassInfo; import jadx.api.plugins.pass.types.JadxPreparePass; import jadx.api.usage.impl.EmptyUsageInfoCache; import jadx.api.usage.impl.InMemoryUsageInfoCache; import jadx.cli.JadxAppCommon; import jadx.cli.plugins.JadxFilesGetter; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ProcessState; import jadx.core.dex.nodes.RootNode; import jadx.core.plugins.AppContext; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.cache.code.CodeCacheMode; import jadx.gui.cache.code.CodeStringCache; import jadx.gui.cache.code.disk.BufferCodeCache; import jadx.gui.cache.code.disk.DiskCodeCache; import jadx.gui.cache.usage.UsageInfoCache; import jadx.gui.plugins.context.CommonGuiPluginsContext; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; import jadx.gui.utils.CacheObject; import jadx.plugins.tools.JadxExternalPluginsLoader; import static jadx.core.dex.nodes.ProcessState.GENERATED_AND_UNLOADED; import static jadx.core.dex.nodes.ProcessState.NOT_LOADED; import static jadx.core.dex.nodes.ProcessState.PROCESS_COMPLETE; @SuppressWarnings("ConstantConditions") public class JadxWrapper { private static final Logger LOG = LoggerFactory.getLogger(JadxWrapper.class); private static final Object DECOMPILER_UPDATE_SYNC = new Object(); private final MainWindow mainWindow; private volatile @Nullable JadxDecompiler decompiler; private CommonGuiPluginsContext guiPluginsContext; public JadxWrapper(MainWindow mainWindow) { this.mainWindow = mainWindow; } public void open() { close(); try { synchronized (DECOMPILER_UPDATE_SYNC) { JadxProject project = getProject(); JadxArgs jadxArgs = getSettings().toJadxArgs(); jadxArgs.setPluginLoader(new JadxExternalPluginsLoader()); jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE); project.fillJadxArgs(jadxArgs); JadxAppCommon.applyEnvVars(jadxArgs); decompiler = new JadxDecompiler(jadxArgs); guiPluginsContext = initGuiPluginsContext(decompiler, mainWindow); initUsageCache(jadxArgs); registerCodeCache(decompiler); decompiler.setEventsImpl(mainWindow.events()); decompiler.load(); } } catch (Exception e) { LOG.error("Jadx decompiler wrapper init error", e); close(); } } // TODO: check and move into core package public void unloadClasses() { getCurrentDecompiler().ifPresent(decompiler -> { for (ClassNode cls : decompiler.getRoot().getClasses()) { ProcessState clsState = cls.getState(); cls.unload(); cls.setState(clsState == PROCESS_COMPLETE ? GENERATED_AND_UNLOADED : NOT_LOADED); } }); } public void close() { try { synchronized (DECOMPILER_UPDATE_SYNC) { if (decompiler != null) { decompiler.close(); decompiler = null; } if (guiPluginsContext != null) { resetGuiPluginsContext(); guiPluginsContext = null; } } } catch (Exception e) { LOG.error("Jadx decompiler close error", e); } finally { mainWindow.getCacheObject().reset(); } } /** * Disk cache require loaded classes to operate, but cache should be set before 'after load' event * to allow plugins decompile classes with cache enabled. * To resolve this, register last 'prepare' pass for cache initialization. */ private void registerCodeCache(JadxDecompiler jadxDecompiler) { CodeCacheMode codeCacheMode = getSettings().getCodeCacheMode(); if (codeCacheMode == CodeCacheMode.MEMORY) { jadxDecompiler.getArgs().setCodeCache(new InMemoryCodeCache()); return; } jadxDecompiler.addCustomPass(new JadxPreparePass() { @Override public JadxPassInfo getInfo() { return new SimpleJadxPassInfo("CacheInit"); } @Override public void init(RootNode root) { switch (getSettings().getCodeCacheMode()) { case DISK_WITH_CACHE: root.getArgs().setCodeCache(new CodeStringCache(buildBufferedDiskCache(root))); break; case DISK: root.getArgs().setCodeCache(buildBufferedDiskCache(root)); break; } } }); } private BufferCodeCache buildBufferedDiskCache(RootNode root) { DiskCodeCache diskCache = new DiskCodeCache(root, getProject().getCacheDir()); return new BufferCodeCache(diskCache); } private void initUsageCache(JadxArgs jadxArgs) { switch (getSettings().getUsageCacheMode()) { case NONE: jadxArgs.setUsageInfoCache(new EmptyUsageInfoCache()); break; case MEMORY: jadxArgs.setUsageInfoCache(new InMemoryUsageInfoCache()); break; case DISK: jadxArgs.setUsageInfoCache(new UsageInfoCache(getProject().getCacheDir(), jadxArgs.getInputFiles())); break; } } public static CommonGuiPluginsContext initGuiPluginsContext(JadxDecompiler decompiler, MainWindow mainWindow) { CommonGuiPluginsContext guiPluginsContext = new CommonGuiPluginsContext(mainWindow); decompiler.getPluginManager().registerAddPluginListener(pluginContext -> { AppContext appContext = new AppContext(); appContext.setGuiContext(guiPluginsContext.buildForPlugin(pluginContext)); appContext.setFilesGetter(decompiler.getArgs().getFilesGetter()); pluginContext.setAppContext(appContext); }); return guiPluginsContext; } public CommonGuiPluginsContext getGuiPluginsContext() { return guiPluginsContext; } public void resetGuiPluginsContext() { guiPluginsContext.reset(); } public void reloadPasses() { resetGuiPluginsContext(); decompiler.reloadPasses(); } /** * Get the complete list of classes */ public List getClasses() { return getDecompiler().getClasses(); } /** * Get all classes that are not excluded by the excluded packages settings */ public List getIncludedClasses() { List classList = getDecompiler().getClasses(); List excludedPackages = getExcludedPackages(); if (excludedPackages.isEmpty()) { return classList; } return classList.stream() .filter(cls -> isClassIncluded(excludedPackages, cls)) .collect(Collectors.toList()); } /** * Get all classes that are not excluded by the excluded packages settings including inner classes */ public List getIncludedClassesWithInners() { List classes = getDecompiler().getClassesWithInners(); List excludedPackages = getExcludedPackages(); if (excludedPackages.isEmpty()) { return classes; } return classes.stream() .filter(cls -> isClassIncluded(excludedPackages, cls)) .collect(Collectors.toList()); } private static boolean isClassIncluded(List excludedPackages, JavaClass cls) { for (String exclude : excludedPackages) { String clsFullName = cls.getFullName(); if (clsFullName.equals(exclude) || clsFullName.startsWith(exclude + '.')) { return false; } } return true; } public List> buildDecompileBatches(List classes) { return getDecompiler().getDecompileScheduler().buildBatches(classes); } // TODO: move to CLI and filter classes in JadxDecompiler public List getExcludedPackages() { String excludedPackages = getSettings().getExcludedPackages().trim(); if (excludedPackages.isEmpty()) { return Collections.emptyList(); } return Arrays.asList(excludedPackages.split(" +")); } public void setExcludedPackages(List packagesToExclude) { getSettings().setExcludedPackages(String.join(" ", packagesToExclude).trim()); getSettings().sync(); } public void addExcludedPackage(String packageToExclude) { String newExclusion = getSettings().getExcludedPackages() + ' ' + packageToExclude; getSettings().setExcludedPackages(newExclusion.trim()); getSettings().sync(); } public void removeExcludedPackage(String packageToRemoveFromExclusion) { List list = new ArrayList<>(getExcludedPackages()); list.remove(packageToRemoveFromExclusion); getSettings().setExcludedPackages(String.join(" ", list)); getSettings().sync(); } public Optional getCurrentDecompiler() { synchronized (DECOMPILER_UPDATE_SYNC) { return Optional.ofNullable(decompiler); } } /** * TODO: make method private * Do not store JadxDecompiler in fields to not leak old instances */ public @NotNull JadxDecompiler getDecompiler() { if (decompiler == null || decompiler.getRoot() == null) { throw new JadxRuntimeException("Decompiler not yet loaded"); } return decompiler; } // TODO: forbid usage of this method public RootNode getRootNode() { return getDecompiler().getRoot(); } public void reloadCodeData() { getDecompiler().reloadCodeData(); } public JavaNode getJavaNodeByRef(ICodeNodeRef nodeRef) { return getDecompiler().getJavaNodeByRef(nodeRef); } public @Nullable JavaNode getEnclosingNode(ICodeInfo codeInfo, int pos) { return getDecompiler().getEnclosingNode(codeInfo, pos); } public List getPackages() { return getDecompiler().getPackages(); } public List getResources() { return getDecompiler().getResources(); } public JadxArgs getArgs() { return getDecompiler().getArgs(); } public JadxProject getProject() { return mainWindow.getProject(); } public JadxSettings getSettings() { return mainWindow.getSettings(); } public CacheObject getCache() { return mainWindow.getCacheObject(); } /** * @param fullName Full name of an outer class. Inner classes are not supported. */ public @Nullable JavaClass searchJavaClassByFullAlias(String fullName) { return getDecompiler().getClasses().stream() .filter(cls -> cls.getFullName().equals(fullName)) .findFirst() .orElse(null); } public @Nullable JavaClass searchJavaClassByOrigClassName(String fullName) { return getDecompiler().searchJavaClassByOrigFullName(fullName); } /** * @param rawName Full raw name of an outer class. Inner classes are not supported. */ public @Nullable JavaClass searchJavaClassByRawName(String rawName) { return getDecompiler().getClasses().stream() .filter(cls -> cls.getRawName().equals(rawName)) .findFirst() .orElse(null); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/CodeCacheMode.java ================================================ package jadx.gui.cache.code; import java.util.stream.Collectors; import java.util.stream.Stream; import jadx.gui.utils.NLS; public enum CodeCacheMode { MEMORY("preferences.codeCacheMode.memory", "preferences.codeCacheMode.memory.desc"), DISK_WITH_CACHE("preferences.codeCacheMode.diskWithCache", "preferences.codeCacheMode.diskWithCache.desc"), DISK("preferences.codeCacheMode.disk", "preferences.codeCacheMode.disk.desc"); private final String labelKey; private final String descKey; CodeCacheMode(String labelKey, String descKey) { this.labelKey = labelKey; this.descKey = descKey; } public String getLocalizedName() { return NLS.str(labelKey); } public String getDesc() { return NLS.str(descKey); } @Override public String toString() { return getLocalizedName(); } public static String buildToolTip() { return Stream.of(values()) .map(v -> v.getLocalizedName() + " - " + v.getDesc()) .collect(Collectors.joining("\n")); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/CodeStringCache.java ================================================ package jadx.gui.cache.code; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.reactivestreams.Subscriber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.processors.PublishProcessor; import jadx.api.ICodeCache; import jadx.api.ICodeInfo; import jadx.api.impl.DelegateCodeCache; import jadx.gui.utils.UiUtils; /** * Keep code strings for faster search */ public class CodeStringCache extends DelegateCodeCache { private static final Logger LOG = LoggerFactory.getLogger(CodeStringCache.class); private final Map codeCache = new ConcurrentHashMap<>(); private final Subscriber subscriber; private final Disposable disposable; public CodeStringCache(ICodeCache backCache) { super(backCache); // reset cache if free memory is low // check only on changes (with debounce) to reduce background checks if app not used PublishProcessor processor = PublishProcessor.create(); subscriber = processor; disposable = processor.debounce(3, TimeUnit.SECONDS) .map(v -> UiUtils.isFreeMemoryAvailable()) .filter(v -> !v) .subscribe(v -> { LOG.warn("Free memory is low! Reset code strings cache. Cache size {}", codeCache.size()); codeCache.clear(); System.gc(); }); } @Override @Nullable public String getCode(String clsFullName) { subscriber.onNext(Boolean.TRUE); String code = codeCache.get(clsFullName); if (code != null) { return code; } String backCode = backCache.getCode(clsFullName); if (backCode != null) { codeCache.put(clsFullName, backCode); } return backCode; } @Override public @NotNull ICodeInfo get(String clsFullName) { subscriber.onNext(Boolean.TRUE); return super.get(clsFullName); } @Override public void add(String clsFullName, ICodeInfo codeInfo) { subscriber.onNext(Boolean.TRUE); codeCache.put(clsFullName, codeInfo.getCodeStr()); backCache.add(clsFullName, codeInfo); } @Override public void remove(String clsFullName) { codeCache.remove(clsFullName); backCache.remove(clsFullName); } @Override public void close() throws IOException { try { backCache.close(); } finally { codeCache.clear(); subscriber.onComplete(); disposable.dispose(); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/FixedCodeCache.java ================================================ package jadx.gui.cache.code; import jadx.api.ICodeCache; import jadx.api.ICodeInfo; import jadx.api.impl.DelegateCodeCache; /** * Code cache with fixed size of wrapped code cache ('remove' and 'add' methods will do nothing). */ public class FixedCodeCache extends DelegateCodeCache { public FixedCodeCache(ICodeCache codeCache) { super(codeCache); } @Override public void remove(String clsFullName) { // no op } @Override public void add(String clsFullName, ICodeInfo codeInfo) { // no op } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/BufferCodeCache.java ================================================ package jadx.gui.cache.code.disk; import java.io.IOException; import java.util.Deque; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeCache; import jadx.api.ICodeInfo; public class BufferCodeCache implements ICodeCache { private static final int BUFFER_SIZE = 20; private final ICodeCache backCache; private final Map cache = new ConcurrentHashMap<>(); private final Deque buffer = new ConcurrentLinkedDeque<>(); public BufferCodeCache(ICodeCache backCache) { this.backCache = backCache; } private void addInternal(String clsFullName, ICodeInfo codeInfo) { cache.put(clsFullName, codeInfo); buffer.addLast(clsFullName); if (buffer.size() > BUFFER_SIZE) { String removedKey = buffer.removeFirst(); cache.remove(removedKey); } } @Override public boolean contains(String clsFullName) { if (cache.containsKey(clsFullName)) { return true; } return backCache.contains(clsFullName); } @Override public void add(String clsFullName, ICodeInfo codeInfo) { addInternal(clsFullName, codeInfo); backCache.add(clsFullName, codeInfo); } @Override public @NotNull ICodeInfo get(String clsFullName) { ICodeInfo codeInfo = cache.get(clsFullName); if (codeInfo != null) { return codeInfo; } ICodeInfo backCodeInfo = backCache.get(clsFullName); if (backCodeInfo != ICodeInfo.EMPTY) { addInternal(clsFullName, backCodeInfo); } return backCodeInfo; } @Override public @Nullable String getCode(String clsFullName) { ICodeInfo codeInfo = cache.get(clsFullName); if (codeInfo != null) { return codeInfo.getCodeStr(); } return backCache.getCode(clsFullName); } @Override public void remove(String clsFullName) { cache.remove(clsFullName); backCache.remove(clsFullName); } @Override public void close() throws IOException { cache.clear(); buffer.clear(); backCache.close(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/CodeMetadataAdapter.java ================================================ package jadx.gui.cache.code.disk; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.Map; import jadx.api.ICodeInfo; import jadx.api.impl.AnnotatedCodeInfo; import jadx.api.impl.SimpleCodeInfo; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeMetadata; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.files.FileUtils; import jadx.gui.cache.code.disk.adapters.CodeAnnotationAdapter; import jadx.gui.cache.code.disk.adapters.DataAdapterHelper; import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; public class CodeMetadataAdapter { private static final byte[] JADX_METADATA_HEADER = "jadxmd".getBytes(StandardCharsets.US_ASCII); private final CodeAnnotationAdapter codeAnnotationAdapter; public CodeMetadataAdapter(RootNode root) { codeAnnotationAdapter = new CodeAnnotationAdapter(root); } public void write(Path metadataFile, ICodeMetadata metadata) { FileUtils.makeDirsForFile(metadataFile); try (OutputStream fileOutput = Files.newOutputStream(metadataFile, WRITE, CREATE, TRUNCATE_EXISTING); DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fileOutput))) { out.write(JADX_METADATA_HEADER); writeLines(out, metadata.getLineMapping()); writeAnnotations(out, metadata.getAsMap()); } catch (Exception e) { throw new RuntimeException("Failed to write metadata file", e); } } public ICodeInfo readAndBuild(Path metadataFile, String code) { if (!Files.exists(metadataFile)) { return new SimpleCodeInfo(code); } try (InputStream fileInput = Files.newInputStream(metadataFile); DataInputStream in = new DataInputStream(new BufferedInputStream(fileInput))) { in.skipBytes(JADX_METADATA_HEADER.length); Map lines = readLines(in); Map annotations = readAnnotations(in); return new AnnotatedCodeInfo(code, lines, annotations); } catch (Exception e) { throw new RuntimeException("Failed to parse code annotations", e); } } private void writeLines(DataOutput out, Map lines) throws IOException { out.writeInt(lines.size()); for (Map.Entry entry : lines.entrySet()) { DataAdapterHelper.writeUVInt(out, entry.getKey()); DataAdapterHelper.writeUVInt(out, entry.getValue()); } } private Map readLines(DataInput in) throws IOException { int size = in.readInt(); if (size == 0) { return Collections.emptyMap(); } Map lines = new HashMap<>(size); for (int i = 0; i < size; i++) { int key = DataAdapterHelper.readUVInt(in); int value = DataAdapterHelper.readUVInt(in); lines.put(key, value); } return lines; } private void writeAnnotations(DataOutputStream out, Map annotations) throws IOException { out.writeInt(annotations.size()); for (Map.Entry entry : annotations.entrySet()) { DataAdapterHelper.writeUVInt(out, entry.getKey()); codeAnnotationAdapter.write(out, entry.getValue()); } } private Map readAnnotations(DataInputStream in) throws IOException { int size = in.readInt(); if (size == 0) { return Collections.emptyMap(); } Map map = new HashMap<>(size); for (int i = 0; i < size; i++) { int pos = DataAdapterHelper.readUVInt(in); ICodeAnnotation ann = codeAnnotationAdapter.read(in); if (ann != null) { map.put(pos, ann); } } return map; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java ================================================ package jadx.gui.cache.code.disk; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.BitSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeCache; import jadx.api.ICodeInfo; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.core.Jadx; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; public class DiskCodeCache implements ICodeCache { private static final Logger LOG = LoggerFactory.getLogger(DiskCodeCache.class); private static final int DATA_FORMAT_VERSION = 15; private final Path baseDir; private final Path srcDir; private final Path metaDir; private final Path codeVersionFile; private final String codeVersion; private final CodeMetadataAdapter codeMetadataAdapter; private final ExecutorService writePool; private final Map clsDataMap; public DiskCodeCache(RootNode root, Path projectCacheDir) { baseDir = projectCacheDir.resolve("code"); srcDir = baseDir.resolve("sources"); metaDir = baseDir.resolve("metadata"); codeVersionFile = baseDir.resolve("code-version"); JadxArgs args = root.getArgs(); codeVersion = buildCodeVersion(args, root.getDecompiler()); writePool = Executors.newFixedThreadPool(args.getThreadsCount()); codeMetadataAdapter = new CodeMetadataAdapter(root); clsDataMap = buildClassDataMap(root.getClasses()); if (checkCodeVersion()) { loadCachedSet(); } else { reset(); } } private boolean checkCodeVersion() { try { if (!Files.exists(codeVersionFile)) { return false; } String currentCodeVer = FileUtils.readFile(codeVersionFile); return currentCodeVer.equals(codeVersion); } catch (Exception e) { LOG.warn("Failed to load code version file", e); return false; } } private void reset() { try { long start = System.currentTimeMillis(); LOG.info("Resetting disk code cache, base dir: {}", baseDir.toAbsolutePath()); FileUtils.deleteDirIfExists(baseDir); if (Files.exists(baseDir.getParent().resolve(codeVersionFile.getFileName()))) { // remove old version cache files FileUtils.deleteDirIfExists(baseDir.getParent()); } FileUtils.makeDirs(srcDir); FileUtils.makeDirs(metaDir); FileUtils.writeFile(codeVersionFile, codeVersion); if (LOG.isDebugEnabled()) { LOG.info("Reset done in: {}ms", System.currentTimeMillis() - start); } } catch (Exception e) { throw new JadxRuntimeException("Failed to reset code cache", e); } finally { clsDataMap.values().forEach(d -> d.setCached(false)); } } /** * Async writes backed by in-memory store */ @Override public void add(String clsFullName, ICodeInfo codeInfo) { CacheData clsData = getClsData(clsFullName); clsData.setTmpCodeInfo(codeInfo); clsData.setCached(true); writePool.execute(() -> { try { int clsId = clsData.getClsId(); ICodeInfo code = clsData.getTmpCodeInfo(); if (code != null) { FileUtils.writeFile(getJavaFile(clsId), code.getCodeStr()); codeMetadataAdapter.write(getMetadataFile(clsId), code.getCodeMetadata()); } } catch (Exception e) { LOG.error("Failed to write code cache for " + clsFullName, e); remove(clsFullName); } finally { clsData.setTmpCodeInfo(null); } }); } @Override public @Nullable String getCode(String clsFullName) { try { if (!contains(clsFullName)) { return null; } CacheData clsData = getClsData(clsFullName); ICodeInfo tmpCodeInfo = clsData.getTmpCodeInfo(); if (tmpCodeInfo != null) { return tmpCodeInfo.getCodeStr(); } Path javaFile = getJavaFile(clsData.getClsId()); if (!Files.exists(javaFile)) { return null; } return FileUtils.readFile(javaFile); } catch (Exception e) { LOG.error("Failed to read class code for {}", clsFullName, e); return null; } } @Override public @NotNull ICodeInfo get(String clsFullName) { try { if (!contains(clsFullName)) { return ICodeInfo.EMPTY; } CacheData clsData = getClsData(clsFullName); ICodeInfo tmpCodeInfo = clsData.getTmpCodeInfo(); if (tmpCodeInfo != null) { return tmpCodeInfo; } int clsId = clsData.getClsId(); Path javaFile = getJavaFile(clsId); if (!Files.exists(javaFile)) { return ICodeInfo.EMPTY; } String code = FileUtils.readFile(javaFile); return codeMetadataAdapter.readAndBuild(getMetadataFile(clsId), code); } catch (Exception e) { LOG.error("Failed to read code cache for {}", clsFullName, e); return ICodeInfo.EMPTY; } } @Override public boolean contains(String clsFullName) { return getClsData(clsFullName).isCached(); } @Override public void remove(String clsFullName) { try { CacheData clsData = getClsData(clsFullName); if (clsData.isCached()) { clsData.setCached(false); if (clsData.getTmpCodeInfo() == null) { LOG.debug("Removing class info from disk: {}", clsFullName); int clsId = clsData.getClsId(); Files.deleteIfExists(getJavaFile(clsId)); Files.deleteIfExists(getMetadataFile(clsId)); } else { // class info not yet written to disk clsData.setTmpCodeInfo(null); } } } catch (Exception e) { throw new JadxRuntimeException("Failed to remove code cache for " + clsFullName, e); } } private String buildCodeVersion(JadxArgs args, @Nullable JadxDecompiler decompiler) { List inputFiles = new ArrayList<>(args.getInputFiles()); if (args.getGeneratedRenamesMappingFileMode().shouldRead() && args.getGeneratedRenamesMappingFile() != null && args.getGeneratedRenamesMappingFile().exists()) { inputFiles.add(args.getGeneratedRenamesMappingFile()); } return DATA_FORMAT_VERSION + ":" + Jadx.getVersion() + ":" + args.makeCodeArgsHash(decompiler) + ":" + FileUtils.buildInputsHash(Utils.collectionMap(inputFiles, File::toPath)); } private CacheData getClsData(String clsFullName) { CacheData clsData = clsDataMap.get(clsFullName); if (clsData == null) { throw new JadxRuntimeException("Unknown class name: " + clsFullName); } return clsData; } private void loadCachedSet() { long start = System.currentTimeMillis(); BitSet cachedSet = new BitSet(clsDataMap.size()); try (Stream stream = Files.walk(metaDir)) { stream.forEach(file -> { String fileName = file.getFileName().toString(); if (fileName.endsWith(".jadxmd")) { String idStr = StringUtils.removeSuffix(fileName, ".jadxmd"); int clsId = Integer.parseInt(idStr, 16); cachedSet.set(clsId); } }); } catch (Exception e) { throw new JadxRuntimeException("Failed to enumerate cached classes", e); } int count = 0; for (CacheData data : clsDataMap.values()) { int clsId = data.getClsId(); if (cachedSet.get(clsId)) { data.setCached(true); count++; } } LOG.info("Found {} classes in disk cache, time: {}ms, dir: {}", count, System.currentTimeMillis() - start, metaDir.getParent()); } private Path getJavaFile(int clsId) { return srcDir.resolve(getPathForClsId(clsId, ".java")); } private Path getMetadataFile(int clsId) { return metaDir.resolve(getPathForClsId(clsId, ".jadxmd")); } private Path getPathForClsId(int clsId, String ext) { // all classes divided between 256 top level folders String firstByte = FileUtils.byteToHex(clsId); return Paths.get(firstByte, FileUtils.intToHex(clsId) + ext); } private Map buildClassDataMap(List classes) { int clsCount = classes.size(); Map map = new HashMap<>(clsCount); for (int i = 0; i < clsCount; i++) { ClassNode cls = classes.get(i); map.put(cls.getRawName(), new CacheData(i)); } return map; } @Override public void close() throws IOException { synchronized (this) { try { writePool.shutdown(); boolean completed = writePool.awaitTermination(1, TimeUnit.MINUTES); if (!completed) { LOG.warn("Disk code cache closing terminated by timeout"); } } catch (InterruptedException e) { LOG.error("Failed to close disk code cache", e); } } } private static final class CacheData { private final int clsId; private boolean cached; private @Nullable ICodeInfo tmpCodeInfo; public CacheData(int clsId) { this.clsId = clsId; } public int getClsId() { return clsId; } public boolean isCached() { return cached; } public void setCached(boolean cached) { this.cached = cached; } public @Nullable ICodeInfo getTmpCodeInfo() { return tmpCodeInfo; } public void setTmpCodeInfo(@Nullable ICodeInfo tmpCodeInfo) { this.tmpCodeInfo = tmpCodeInfo; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/ArgTypeAdapter.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import jadx.core.dex.instructions.args.ArgType; import jadx.core.utils.exceptions.JadxRuntimeException; public class ArgTypeAdapter implements DataAdapter { public static final ArgTypeAdapter INSTANCE = new ArgTypeAdapter(); private enum Types { NULL, UNKNOWN, PRIMITIVE, ARRAY, OBJECT, WILDCARD, GENERIC, TYPE_VARIABLE, OUTER_GENERIC } @Override public void write(DataOutput out, ArgType value) throws IOException { if (value == null) { writeType(out, Types.NULL); return; } if (!value.isTypeKnown()) { writeType(out, Types.UNKNOWN); return; } if (value.isPrimitive()) { writeType(out, Types.PRIMITIVE); out.writeByte(value.getPrimitiveType().getShortName().charAt(0)); return; } if (value.getOuterType() != null) { writeType(out, Types.OUTER_GENERIC); write(out, value.getOuterType()); write(out, value.getInnerType()); return; } if (value.getWildcardType() != null) { writeType(out, Types.WILDCARD); ArgType.WildcardBound bound = value.getWildcardBound(); out.writeByte(bound.getNum()); if (bound != ArgType.WildcardBound.UNBOUND) { write(out, value.getWildcardType()); } return; } if (value.isGeneric()) { writeType(out, Types.GENERIC); out.writeUTF(value.getObject()); writeTypesList(out, value.getGenericTypes()); return; } if (value.isGenericType()) { writeType(out, Types.TYPE_VARIABLE); out.writeUTF(value.getObject()); writeTypesList(out, value.getExtendTypes()); return; } if (value.isObject()) { writeType(out, Types.OBJECT); out.writeUTF(value.getObject()); return; } if (value.isArray()) { writeType(out, Types.ARRAY); out.writeByte(value.getArrayDimension()); write(out, value.getArrayRootElement()); return; } throw new JadxRuntimeException("Cannot save type: " + value + ", cls: " + value.getClass()); } private void writeType(DataOutput out, Types type) throws IOException { out.writeByte(type.ordinal()); } @Override public ArgType read(DataInput in) throws IOException { byte typeOrdinal = in.readByte(); Types type = Types.values()[typeOrdinal]; switch (type) { case NULL: return null; case UNKNOWN: return ArgType.UNKNOWN; case PRIMITIVE: char shortName = (char) in.readByte(); return ArgType.parse(shortName); case OUTER_GENERIC: ArgType outerType = read(in); ArgType innerType = read(in); return ArgType.outerGeneric(outerType, innerType); case WILDCARD: ArgType.WildcardBound bound = ArgType.WildcardBound.getByNum(in.readByte()); if (bound == ArgType.WildcardBound.UNBOUND) { return ArgType.WILDCARD; } ArgType objType = read(in); return ArgType.wildcard(objType, bound); case GENERIC: String clsType = in.readUTF(); return ArgType.generic(clsType, readTypesList(in)); case TYPE_VARIABLE: String typeVar = in.readUTF(); List extendTypes = readTypesList(in); return ArgType.genericType(typeVar, extendTypes); case OBJECT: return ArgType.object(in.readUTF()); case ARRAY: int dim = in.readByte(); ArgType rootType = read(in); return ArgType.array(rootType, dim); default: throw new RuntimeException("Unexpected arg type: " + type); } } private void writeTypesList(DataOutput out, List types) throws IOException { out.writeByte(types.size()); for (ArgType type : types) { write(out, type); } } private List readTypesList(DataInput in) throws IOException { byte size = in.readByte(); if (size == 0) { return Collections.emptyList(); } List list = new ArrayList<>(size); for (int i = 0; i < size; i++) { list.add(read(in)); } return list; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/ClassNodeAdapter.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; public class ClassNodeAdapter implements DataAdapter { private final RootNode root; public ClassNodeAdapter(RootNode root) { this.root = root; } @Override public void write(DataOutput out, ClassNode value) throws IOException { out.writeUTF(value.getClassInfo().getRawName()); } @Override public ClassNode read(DataInput in) throws IOException { return root.resolveRawClass(in.readUTF()); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/CodeAnnotationAdapter.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.util.EnumMap; import java.util.Map; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeAnnotation.AnnType; import jadx.core.dex.nodes.RootNode; public class CodeAnnotationAdapter implements DataAdapter { private final Map adaptersByCls; private final TypeInfo[] adaptersByTag; public CodeAnnotationAdapter(RootNode root) { Map> map = registerAdapters(root); int size = map.size(); adaptersByCls = new EnumMap<>(AnnType.class); adaptersByTag = new TypeInfo[size + 1]; int tag = 1; for (Map.Entry> entry : map.entrySet()) { TypeInfo typeInfo = new TypeInfo(tag, entry.getValue()); adaptersByCls.put(entry.getKey(), typeInfo); adaptersByTag[tag] = typeInfo; tag++; } } private Map> registerAdapters(RootNode root) { Map> map = new EnumMap<>(AnnType.class); MethodNodeAdapter mthAdapter = new MethodNodeAdapter(root); map.put(AnnType.CLASS, new ClassNodeAdapter(root)); map.put(AnnType.FIELD, new FieldNodeAdapter(root)); map.put(AnnType.METHOD, mthAdapter); map.put(AnnType.DECLARATION, new NodeDeclareRefAdapter(this)); map.put(AnnType.VAR, new VarNodeAdapter(mthAdapter)); map.put(AnnType.VAR_REF, VarRefAdapter.INSTANCE); map.put(AnnType.OFFSET, InsnCodeOffsetAdapter.INSTANCE); map.put(AnnType.END, new NodeEndAdapter()); return map; } @SuppressWarnings("unchecked") @Override public void write(DataOutput out, ICodeAnnotation value) throws IOException { if (value == null) { out.writeByte(0); return; } TypeInfo typeInfo = adaptersByCls.get(value.getAnnType()); if (typeInfo == null) { throw new RuntimeException("Unexpected code annotation type: " + value.getClass().getSimpleName()); } out.writeByte(typeInfo.getTag()); typeInfo.getAdapter().write(out, value); } @Override public ICodeAnnotation read(DataInput in) throws IOException { int tag = in.readByte(); if (tag == 0) { return null; } TypeInfo typeInfo = adaptersByTag[tag]; if (typeInfo == null) { throw new RuntimeException("Unknown type tag: " + tag); } return (ICodeAnnotation) typeInfo.getAdapter().read(in); } @SuppressWarnings("rawtypes") private static class TypeInfo { private final int tag; private final DataAdapter adapter; private TypeInfo(int tag, DataAdapter adapter) { this.tag = tag; this.adapter = adapter; } public int getTag() { return tag; } public DataAdapter getAdapter() { return adapter; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/DataAdapter.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; public interface DataAdapter { void write(DataOutput out, T value) throws IOException; T read(DataInput in) throws IOException; } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/DataAdapterHelper.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import org.jetbrains.annotations.Nullable; public class DataAdapterHelper { public static void writeNullableUTF(DataOutput out, @Nullable String str) throws IOException { if (str == null) { out.writeByte(0); } else { out.writeByte(1); out.writeUTF(str); } } public static @Nullable String readNullableUTF(DataInput in) throws IOException { if (in.readByte() == 0) { return null; } return in.readUTF(); } /** * Write unsigned variable length integer (ULEB128 encoding) */ public static void writeUVInt(DataOutput out, int val) throws IOException { if (val < 0) { throw new IllegalArgumentException("Expect value >= 0, got: " + val); } int current = val; int next = val; while (true) { next >>>= 7; if (next == 0) { // last byte out.writeByte(current & 0x7f); return; } out.writeByte((current & 0x7f) | 0x80); current = next; } } /** * Read unsigned variable length integer (ULEB128 encoding) */ public static int readUVInt(DataInput in) throws IOException { int result = 0; int shift = 0; while (true) { byte v = in.readByte(); result |= (v & (byte) 0x7f) << shift; shift += 7; if ((v & 0x80) != 0x80) { return result; } } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/FieldNodeAdapter.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.RootNode; public class FieldNodeAdapter implements DataAdapter { private final RootNode root; public FieldNodeAdapter(RootNode root) { this.root = root; } @Override public void write(DataOutput out, FieldNode value) throws IOException { FieldInfo fieldInfo = value.getFieldInfo(); out.writeUTF(fieldInfo.getDeclClass().getRawName()); out.writeUTF(fieldInfo.getShortId()); } @Override public FieldNode read(DataInput in) throws IOException { String cls = in.readUTF(); String sign = in.readUTF(); ClassNode clsNode = root.resolveRawClass(cls); if (clsNode == null) { throw new RuntimeException("Class not found: " + cls); } FieldNode fieldNode = clsNode.searchFieldByShortId(sign); if (fieldNode == null) { throw new RuntimeException("Field not found: " + cls + "." + sign); } return fieldNode; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/InsnCodeOffsetAdapter.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import jadx.api.metadata.annotations.InsnCodeOffset; public class InsnCodeOffsetAdapter implements DataAdapter { public static final InsnCodeOffsetAdapter INSTANCE = new InsnCodeOffsetAdapter(); @Override public void write(DataOutput out, InsnCodeOffset value) throws IOException { DataAdapterHelper.writeUVInt(out, value.getOffset()); } @Override public InsnCodeOffset read(DataInput in) throws IOException { return new InsnCodeOffset(DataAdapterHelper.readUVInt(in)); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/MethodNodeAdapter.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; public class MethodNodeAdapter implements DataAdapter { private final RootNode root; public MethodNodeAdapter(RootNode root) { this.root = root; } @Override public void write(DataOutput out, MethodNode value) throws IOException { MethodInfo methodInfo = value.getMethodInfo(); out.writeUTF(methodInfo.getDeclClass().getRawName()); out.writeUTF(methodInfo.getShortId()); } @Override public MethodNode read(DataInput in) throws IOException { String cls = in.readUTF(); String sign = in.readUTF(); return root.resolveDirectMethod(cls, sign); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/NodeDeclareRefAdapter.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.NodeDeclareRef; public class NodeDeclareRefAdapter implements DataAdapter { private final CodeAnnotationAdapter refAdapter; public NodeDeclareRefAdapter(CodeAnnotationAdapter refAdapter) { this.refAdapter = refAdapter; } @Override public void write(DataOutput out, NodeDeclareRef value) throws IOException { ICodeNodeRef node = value.getNode(); if (node == null) { throw new RuntimeException("Null node in NodeDeclareRef"); } refAdapter.write(out, node); DataAdapterHelper.writeUVInt(out, value.getDefPos()); } @Override public NodeDeclareRef read(DataInput in) throws IOException { ICodeNodeRef ref = (ICodeNodeRef) refAdapter.read(in); int defPos = DataAdapterHelper.readUVInt(in); NodeDeclareRef nodeDeclareRef = new NodeDeclareRef(ref); nodeDeclareRef.setDefPos(defPos); // restore def position if loading metadata without actual decompilation ref.setDefPosition(defPos); return nodeDeclareRef; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/NodeEndAdapter.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import jadx.api.metadata.annotations.NodeEnd; public class NodeEndAdapter implements DataAdapter { @Override public void write(DataOutput out, NodeEnd value) throws IOException { } @Override public NodeEnd read(DataInput in) throws IOException { return NodeEnd.VALUE; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/VarNodeAdapter.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import jadx.api.metadata.annotations.VarNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.MethodNode; import static jadx.gui.cache.code.disk.adapters.DataAdapterHelper.readNullableUTF; import static jadx.gui.cache.code.disk.adapters.DataAdapterHelper.readUVInt; import static jadx.gui.cache.code.disk.adapters.DataAdapterHelper.writeNullableUTF; import static jadx.gui.cache.code.disk.adapters.DataAdapterHelper.writeUVInt; public class VarNodeAdapter implements DataAdapter { private final MethodNodeAdapter mthAdapter; public VarNodeAdapter(MethodNodeAdapter mthAdapter) { this.mthAdapter = mthAdapter; } @Override public void write(DataOutput out, VarNode value) throws IOException { mthAdapter.write(out, value.getMth()); writeUVInt(out, value.getReg()); writeUVInt(out, value.getSsa()); ArgTypeAdapter.INSTANCE.write(out, value.getType()); writeNullableUTF(out, value.getName()); } @Override public VarNode read(DataInput in) throws IOException { MethodNode mth = mthAdapter.read(in); int reg = readUVInt(in); int ssa = readUVInt(in); ArgType type = ArgTypeAdapter.INSTANCE.read(in); String name = readNullableUTF(in); return new VarNode(mth, reg, ssa, type, name); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/code/disk/adapters/VarRefAdapter.java ================================================ package jadx.gui.cache.code.disk.adapters; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import jadx.api.metadata.annotations.VarRef; public class VarRefAdapter implements DataAdapter { public static final VarRefAdapter INSTANCE = new VarRefAdapter(); @Override public void write(DataOutput out, VarRef value) throws IOException { int refPos = value.getRefPos(); DataAdapterHelper.writeUVInt(out, refPos); } @Override public VarRef read(DataInput in) throws IOException { int refPos = DataAdapterHelper.readUVInt(in); return VarRef.fromPos(refPos); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/manager/CacheEntry.java ================================================ package jadx.gui.cache.manager; import org.jetbrains.annotations.NotNull; public class CacheEntry implements Comparable { private String project; private String cache; private long timestamp; public String getProject() { return project; } public void setProject(String project) { this.project = project; } public String getCache() { return cache; } public void setCache(String cache) { this.cache = cache; } public long getTimestamp() { return timestamp; } public void setTimestamp(long timestamp) { this.timestamp = timestamp; } @Override public int compareTo(@NotNull CacheEntry other) { // recent entries first return -Long.compare(timestamp, other.timestamp); } @Override public String toString() { return "CacheEntry{project=" + project + ", cache=" + cache + "}"; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/manager/CacheManager.java ================================================ package jadx.gui.cache.manager; import java.io.BufferedReader; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import jadx.api.plugins.utils.CommonFileUtils; import jadx.core.utils.GsonUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.data.ProjectData; import jadx.gui.utils.files.JadxFiles; public class CacheManager { private static final Logger LOG = LoggerFactory.getLogger(CacheManager.class); private static final Gson GSON = GsonUtils.buildGson(); private static final Type CACHES_TYPE = new TypeToken>() { }.getType(); private final Map cacheMap; private final JadxSettings settings; public CacheManager(JadxSettings settings) { this.settings = settings; this.cacheMap = loadCaches(); } /** * If project cache is set -> check if cache entry exists for this project. * If not -> calculate new and add entry. */ public Path getCacheDir(JadxProject project, @Nullable String cacheDirStr) { if (cacheDirStr == null) { Path newProjectCacheDir = buildCacheDir(project); addEntry(projectToKey(project), newProjectCacheDir); return newProjectCacheDir; } Path cacheDir = resolveCacheDirStr(cacheDirStr, project.getProjectPath()); return verifyEntry(project, cacheDir); } public void projectPathUpdate(JadxProject project, Path newPath) { if (Objects.equals(project.getProjectPath(), newPath)) { return; } String key = projectToKey(project); CacheEntry prevEntry = cacheMap.remove(key); if (prevEntry == null) { return; } CacheEntry newEntry = new CacheEntry(); newEntry.setProject(pathToString(newPath)); newEntry.setCache(prevEntry.getCache()); addEntry(newEntry); } public List getCachesList() { List list = new ArrayList<>(cacheMap.values()); Collections.sort(list); return list; } public synchronized void removeCacheEntry(CacheEntry entry) { try { cacheMap.remove(entry.getProject()); saveCaches(cacheMap); FileUtils.deleteDirIfExists(Paths.get(entry.getCache())); } catch (Exception e) { LOG.error("Failed to remove cache entry: " + entry.getCache(), e); } } private Path resolveCacheDirStr(String cacheDirStr, Path projectPath) { Path path = Paths.get(cacheDirStr); if (path.isAbsolute() || projectPath == null) { return path; } return projectPath.resolveSibling(path); } public String buildCacheDirStr(Path dir) { if (Objects.equals(settings.getCacheDir(), ".")) { return dir.getFileName().toString(); } return pathToString(dir); } private Path buildCacheDir(JadxProject project) { String cacheDirValue = settings.getCacheDir(); if (Objects.equals(cacheDirValue, ".")) { return buildLocalCacheDir(project); } Path cacheBaseDir = cacheDirValue == null ? JadxFiles.PROJECTS_CACHE_DIR : Paths.get(cacheDirValue); return cacheBaseDir.resolve(buildProjectUniqName(project)); } private static Path buildLocalCacheDir(JadxProject project) { Path projectPath = project.getProjectPath(); if (projectPath != null) { return projectPath.resolveSibling(projectPath.getFileName() + ".cache"); } List files = project.getFilePaths(); if (files.isEmpty()) { throw new JadxRuntimeException("Failed to build local cache dir"); } Path path = files.stream() .filter(p -> !p.getFileName().toString().endsWith(".jadx.kts")) .findFirst() .orElseGet(() -> files.get(0)); String name = CommonFileUtils.removeFileExtension(path.getFileName().toString()); return path.resolveSibling(name + ".jadx.cache"); } private Path verifyEntry(JadxProject project, Path cacheDir) { boolean cacheExists = Files.exists(cacheDir); String key = projectToKey(project); CacheEntry entry = cacheMap.get(key); if (entry == null) { Path newCacheDir = cacheExists ? cacheDir : buildCacheDir(project); addEntry(key, newCacheDir); return newCacheDir; } if (entry.getCache().equals(pathToString(cacheDir)) && cacheExists) { // same and exists return cacheDir; } // remove previous cache dir FileUtils.deleteDirIfExists(Paths.get(entry.getCache())); Path newCacheDir = cacheExists ? cacheDir : buildCacheDir(project); entry.setCache(pathToString(newCacheDir)); entry.setTimestamp(System.currentTimeMillis()); saveCaches(cacheMap); return newCacheDir; } private void addEntry(String projectKey, Path cacheDir) { CacheEntry entry = new CacheEntry(); entry.setProject(projectKey); entry.setCache(pathToString(cacheDir)); addEntry(entry); } private void addEntry(CacheEntry entry) { entry.setTimestamp(System.currentTimeMillis()); cacheMap.put(entry.getProject(), entry); saveCaches(cacheMap); } private String projectToKey(JadxProject project) { Path projectPath = project.getProjectPath(); if (projectPath != null) { return pathToString(projectPath); } return "tmp:" + buildProjectUniqName(project); } private static String buildProjectUniqName(JadxProject project) { return project.getName() + "-" + FileUtils.buildInputsHash(project.getFilePaths()); } public static String pathToString(Path path) { try { return path.toAbsolutePath().normalize().toString(); } catch (Exception e) { throw new JadxRuntimeException("Failed to expand path: " + path, e); } } private synchronized Map loadCaches() { List list = null; if (Files.exists(JadxFiles.CACHES_LIST)) { try (BufferedReader reader = Files.newBufferedReader(JadxFiles.CACHES_LIST)) { list = GSON.fromJson(reader, CACHES_TYPE); } catch (Exception e) { LOG.warn("Failed to load caches list", e); } } else { return initFromRecentProjects(); } if (Utils.isEmpty(list)) { return new HashMap<>(); } Map map = new HashMap<>(list.size()); for (CacheEntry entry : list) { map.put(entry.getProject(), entry); } return map; } private synchronized void saveCaches(Map map) { List list = new ArrayList<>(map.values()); Collections.sort(list); String json = GSON.toJson(list, CACHES_TYPE); try { Files.writeString(JadxFiles.CACHES_LIST, json, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } catch (Exception e) { throw new JadxRuntimeException("Failed to write caches file", e); } } /** * Load caches info from recent projects list. * Help for initial migration. */ private Map initFromRecentProjects() { try { Map map = new HashMap<>(); long t = System.currentTimeMillis(); for (Path project : settings.getRecentProjects()) { try { ProjectData data = JadxProject.loadProjectData(project); String cacheDir = data.getCacheDir(); if (cacheDir == null) { // no cache dir, ignore continue; } Path cachePath = resolveCacheDirStr(cacheDir, project); if (!Files.isDirectory(cachePath)) { continue; } String key = pathToString(project); CacheEntry entry = new CacheEntry(); entry.setProject(key); entry.setCache(pathToString(cachePath)); entry.setTimestamp(t++); // keep projects order map.put(key, entry); } catch (Exception e) { LOG.warn("Failed to load project file: {}", project, e); } } saveCaches(map); return map; } catch (Exception e) { LOG.warn("Failed to fill cache list from recent projects", e); return new HashMap<>(); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/CachedMethodRef.java ================================================ package jadx.gui.cache.usage; import java.util.List; import jadx.api.plugins.input.data.IMethodRef; public class CachedMethodRef implements IMethodRef { private String parentClassType; private String name; private String returnType; private List argTypes; public CachedMethodRef(String parentClassType, String name, String returnType, List argTypes) { this.parentClassType = parentClassType; this.name = name; this.returnType = returnType; this.argTypes = argTypes; } @Override public String getParentClassType() { return parentClassType; } public void setParentClassType(String parentClassType) { this.parentClassType = parentClassType; } @Override public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String getReturnType() { return returnType; } public void setReturnType(String returnType) { this.returnType = returnType; } @Override public List getArgTypes() { return argTypes; } public void setArgTypes(List argTypes) { this.argTypes = argTypes; } @Override public int getUniqId() { throw new UnsupportedOperationException("Unimplemented method 'getUniqId'"); } @Override public void load() { throw new UnsupportedOperationException("Unimplemented method 'load'"); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/ClsUsageData.java ================================================ package jadx.gui.cache.usage; import java.util.HashMap; import java.util.List; import java.util.Map; final class ClsUsageData { private final String rawName; private List clsDeps; private List clsUsage; private List clsUseInMth; private final Map fldUsage = new HashMap<>(); private final Map mthUsage = new HashMap<>(); ClsUsageData(String rawName) { this.rawName = rawName; } public String getRawName() { return rawName; } public List getClsDeps() { return clsDeps; } public void setClsDeps(List clsDeps) { this.clsDeps = clsDeps; } public List getClsUsage() { return clsUsage; } public void setClsUsage(List clsUsage) { this.clsUsage = clsUsage; } public List getClsUseInMth() { return clsUseInMth; } public void setClsUseInMth(List clsUseInMth) { this.clsUseInMth = clsUseInMth; } public Map getFldUsage() { return fldUsage; } public Map getMthUsage() { return mthUsage; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/CollectUsageData.java ================================================ package jadx.gui.cache.usage; import java.util.List; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.usage.IUsageInfoVisitor; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.Utils; final class CollectUsageData implements IUsageInfoVisitor { private final RawUsageData data; public CollectUsageData(RawUsageData usageData) { data = usageData; } @Override public void visitClassDeps(ClassNode cls, List deps) { data.getClassData(cls).setClsDeps(clsNodesRef(deps)); } @Override public void visitClassUsage(ClassNode cls, List usage) { data.getClassData(cls).setClsUsage(clsNodesRef(usage)); } @Override public void visitClassUseInMethods(ClassNode cls, List methods) { data.getClassData(cls).setClsUseInMth(mthNodesRef(methods)); } @Override public void visitFieldsUsage(FieldNode fld, List methods) { data.getFieldData(fld).setUsage(mthNodesRef(methods)); } @Override public void visitMethodsUsage(MethodNode mth, List methods) { data.getMethodData(mth).setUsage(mthNodesRef(methods)); } @Override public void visitMethodsUses(MethodNode mth, List methods) { data.getMethodData(mth).setUses(mthNodesRef(methods)); } @Override public void visitUnresolvedMethodsUsage(MethodNode mth, List methods) { data.getMethodData(mth).setUnresolvedUsage(methods); } @Override public void visitIsSelfCall(MethodNode mth, boolean isSelfCall) { data.getMethodData(mth).setCallsSelf(isSelfCall); } @Override public void visitComplete() { data.collectClassesWithoutData(); } private List clsNodesRef(List usage) { return Utils.collectionMap(usage, ClassNode::getRawName); } private List mthNodesRef(List methods) { return Utils.collectionMap(methods, m -> data.getMethodData(m).getMthRef()); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/FldRef.java ================================================ package jadx.gui.cache.usage; final class FldRef { private final String cls; private final String shortId; FldRef(String cls, String shortId) { this.cls = cls; this.shortId = shortId; } public String getCls() { return cls; } public String getShortId() { return shortId; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/FldUsageData.java ================================================ package jadx.gui.cache.usage; import java.util.List; final class FldUsageData { private final FldRef fldRef; private List usage; public FldUsageData(FldRef fldRef) { this.fldRef = fldRef; } public FldRef getFldRef() { return fldRef; } public List getUsage() { return usage; } public void setUsage(List usage) { this.usage = usage; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/MthRef.java ================================================ package jadx.gui.cache.usage; final class MthRef { private final String cls; private final String shortId; MthRef(String cls, String shortId) { this.cls = cls; this.shortId = shortId; } public String getCls() { return cls; } public String getShortId() { return shortId; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof MthRef)) { return false; } MthRef other = (MthRef) o; return cls.equals(other.cls) && shortId.equals(other.shortId); } @Override public int hashCode() { return 31 * cls.hashCode() + shortId.hashCode(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/MthUsageData.java ================================================ package jadx.gui.cache.usage; import java.util.List; import jadx.api.plugins.input.data.IMethodRef; final class MthUsageData { private final MthRef mthRef; private List usage; private List uses; private List unresolvedUsage; private boolean callsSelf; public MthUsageData(MthRef mthRef) { this.mthRef = mthRef; } public MthRef getMthRef() { return mthRef; } public List getUsage() { return usage; } public void setUsage(List usage) { this.usage = usage; } public List getUses() { return uses; } public void setUses(List uses) { this.uses = uses; } public List getUnresolvedUsage() { return unresolvedUsage; } public void setUnresolvedUsage(List unresolvedUsage) { this.unresolvedUsage = unresolvedUsage; } public boolean callsSelf() { return callsSelf; } public void setCallsSelf(boolean callsSelf) { this.callsSelf = callsSelf; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/RawUsageData.java ================================================ package jadx.gui.cache.usage; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; class RawUsageData { private final Map clsMap = new HashMap<>(); private List classesWithoutData = Collections.emptyList(); public Map getClsMap() { return clsMap; } public List getClassesWithoutData() { return classesWithoutData; } public ClsUsageData getClassData(ClassNode cls) { return getClassData(cls.getRawName()); } public ClsUsageData getClassData(String clsRawName) { return clsMap.computeIfAbsent(clsRawName, ClsUsageData::new); } public MthUsageData getMethodData(MethodNode mth) { ClassNode parentClass = mth.getParentClass(); String shortId = mth.getMethodInfo().getShortId(); return getClassData(parentClass).getMthUsage().computeIfAbsent(shortId, m -> new MthUsageData(new MthRef(parentClass.getRawName(), shortId))); } public FldUsageData getFieldData(FieldNode fld) { ClassNode parentClass = fld.getParentClass(); String shortId = fld.getFieldInfo().getShortId(); return getClassData(parentClass).getFldUsage().computeIfAbsent(shortId, m -> new FldUsageData(new FldRef(parentClass.getRawName(), shortId))); } public void collectClassesWithoutData() { Set allClasses = new HashSet<>(clsMap.size() * 2); for (ClsUsageData usageData : clsMap.values()) { List deps = usageData.getClsDeps(); if (deps != null) { allClasses.addAll(deps); } List usage = usageData.getClsUsage(); if (usage != null) { allClasses.addAll(usage); } } allClasses.removeAll(clsMap.keySet()); classesWithoutData = new ArrayList<>(allClasses); Collections.sort(classesWithoutData); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/UsageCacheMode.java ================================================ package jadx.gui.cache.usage; public enum UsageCacheMode { NONE, MEMORY, DISK } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/UsageData.java ================================================ package jadx.gui.cache.usage; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.usage.IUsageInfoData; import jadx.api.usage.IUsageInfoVisitor; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; class UsageData implements IUsageInfoData { private static final Logger LOG = LoggerFactory.getLogger(UsageData.class); private final RootNode root; private final RawUsageData rawUsageData; public UsageData(RootNode root, RawUsageData rawUsageData) { this.root = root; this.rawUsageData = rawUsageData; } @Override public void apply() { Map clsMap = rawUsageData.getClsMap(); for (ClassNode cls : root.getClasses()) { String clsRawName = cls.getRawName(); ClsUsageData clsUsageData = clsMap.get(clsRawName); if (clsUsageData != null) { applyForClass(clsUsageData, cls); } } } @Override public void applyForClass(ClassNode cls) { String clsRawName = cls.getRawName(); ClsUsageData clsUsageData = rawUsageData.getClsMap().get(clsRawName); if (clsUsageData == null) { LOG.debug("No usage data for class: {}", clsRawName); return; } applyForClass(clsUsageData, cls); } private void applyForClass(ClsUsageData clsUsageData, ClassNode cls) { cls.setDependencies(resolveClsList(clsUsageData.getClsDeps())); cls.setUseIn(resolveClsList(clsUsageData.getClsUsage())); cls.setUseInMth(resolveMthList(clsUsageData.getClsUseInMth())); Map mthUsage = clsUsageData.getMthUsage(); for (MethodNode mth : cls.getMethods()) { MthUsageData mthUsageData = mthUsage.get(mth.getMethodInfo().getShortId()); if (mthUsageData != null) { mth.setUseIn(resolveMthList(mthUsageData.getUsage())); mth.setUsed(resolveMthList(mthUsageData.getUses())); mth.setUnresolvedUsed(mthUsageData.getUnresolvedUsage()); mth.setCallsSelf(mthUsageData.callsSelf()); } } Map fldUsage = clsUsageData.getFldUsage(); for (FieldNode fld : cls.getFields()) { FldUsageData fldUsageData = fldUsage.get(fld.getFieldInfo().getShortId()); if (fldUsageData != null) { fld.setUseIn(resolveMthList(fldUsageData.getUsage())); } } } @Override public void visitUsageData(IUsageInfoVisitor visitor) { throw new JadxRuntimeException("Not implemented"); } private List resolveClsList(List clsList) { return Utils.collectionMap(clsList, root::resolveRawClass); } private List resolveMthList(List mthRefList) { return Utils.collectionMap(mthRefList, m -> root.resolveDirectMethod(m.getCls(), m.getShortId())); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/UsageFileAdapter.java ================================================ package jadx.gui.cache.usage; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.usage.IUsageInfoData; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.gui.cache.code.disk.adapters.DataAdapterHelper; import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; public class UsageFileAdapter extends DataAdapterHelper { private static final Logger LOG = LoggerFactory.getLogger(UsageFileAdapter.class); private static final int USAGE_DATA_VERSION = 2; private static final byte[] JADX_USAGE_HEADER = "jadx.usage".getBytes(StandardCharsets.US_ASCII); public static synchronized @Nullable RawUsageData load(Path usageFile, List inputs) { if (!Files.isRegularFile(usageFile)) { return null; } long start = System.currentTimeMillis(); try (DataInputStream in = new DataInputStream(new BufferedInputStream(Files.newInputStream(usageFile)))) { in.skipBytes(JADX_USAGE_HEADER.length); int dataVersion = in.readInt(); if (dataVersion != USAGE_DATA_VERSION) { LOG.debug("Found old usage data format"); FileUtils.deleteFileIfExists(usageFile); return null; } String inputsHash = buildInputsHash(inputs); String fileInputsHash = in.readUTF(); if (!inputsHash.equals(fileInputsHash)) { LOG.debug("Found usage data with different inputs hash"); FileUtils.deleteFileIfExists(usageFile); return null; } RawUsageData data = readData(in); if (LOG.isDebugEnabled()) { LOG.debug("Loaded usage data from disk cache, classes count: {}, time: {}ms, file: {}", data.getClsMap().size(), System.currentTimeMillis() - start, usageFile); } return data; } catch (Exception e) { try { FileUtils.deleteFileIfExists(usageFile); } catch (IOException ex) { // ignore } LOG.error("Failed to load usage data file", e); return null; } } public static synchronized void save(IUsageInfoData data, Path usageFile, List inputs) { long start = System.currentTimeMillis(); FileUtils.makeDirsForFile(usageFile); String inputsHash = buildInputsHash(inputs); RawUsageData usageData = new RawUsageData(); data.visitUsageData(new CollectUsageData(usageData)); try (OutputStream fileOutput = Files.newOutputStream(usageFile, WRITE, CREATE, TRUNCATE_EXISTING); DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fileOutput))) { out.write(JADX_USAGE_HEADER); out.writeInt(USAGE_DATA_VERSION); out.writeUTF(inputsHash); writeData(out, usageData); } catch (Exception e) { LOG.error("Failed to save usage data file", e); try { FileUtils.deleteFileIfExists(usageFile); } catch (IOException ex) { LOG.error("Failed to delete usage data file: {}", usageFile, ex); } } if (LOG.isDebugEnabled()) { LOG.debug("Usage data saved, time: {}ms, file: {}", System.currentTimeMillis() - start, usageFile); } } private static RawUsageData readData(DataInputStream in) throws IOException { RawUsageData data = new RawUsageData(); int clsCount = readUVInt(in); int clsWithoutDataCount = readUVInt(in); // Class information String[] clsNames = new String[clsCount + clsWithoutDataCount]; ClsUsageData[] classes = new ClsUsageData[clsCount]; int c = 0; for (int i = 0; i < clsCount; i++) { String clsRawName = in.readUTF(); classes[i] = data.getClassData(clsRawName); clsNames[c++] = clsRawName; } for (int i = 0; i < clsWithoutDataCount; i++) { clsNames[c++] = in.readUTF(); } // Method information int mthCount = readUVInt(in); MthRef[] methods = new MthRef[mthCount]; for (int i = 0; i < mthCount; i++) { int clsId = readUVInt(in); String mthShortId = in.readUTF(); ClsUsageData cls = classes[clsId]; MthRef mthRef = new MthRef(cls.getRawName(), mthShortId); cls.getMthUsage().put(mthShortId, new MthUsageData(mthRef)); methods[i] = mthRef; } // Unresolved method information int uMthCount = readUVInt(in); IMethodRef[] unresolvedMethods = new IMethodRef[uMthCount]; for (int i = 0; i < uMthCount; i++) { String name = in.readUTF(); String parentClassType = in.readUTF(); String returnType = in.readUTF(); int argCount = in.readInt(); String[] args = new String[argCount]; for (int j = 0; j < argCount; j++) { args[j] = in.readUTF(); } IMethodRef iMethodRef = new CachedMethodRef(parentClassType, name, returnType, Arrays.asList(args)); unresolvedMethods[i] = iMethodRef; } // Usage data for (int i = 0; i < clsCount; i++) { ClsUsageData cls = data.getClassData(clsNames[i]); cls.setClsDeps(readClsList(in, clsNames)); cls.setClsUsage(readClsList(in, clsNames)); cls.setClsUseInMth(readMthList(in, methods)); int mCount = readUVInt(in); for (int m = 0; m < mCount; m++) { MthRef mthRef = methods[readUVInt(in)]; MthUsageData mthUsageData = cls.getMthUsage().get(mthRef.getShortId()); mthUsageData.setUsage(readMthList(in, methods)); mthUsageData.setUses(readMthList(in, methods)); mthUsageData.setUnresolvedUsage(readUnresolvedMthList(in, unresolvedMethods)); mthUsageData.setCallsSelf(in.readBoolean()); } int fCount = readUVInt(in); for (int f = 0; f < fCount; f++) { String fldShortId = in.readUTF(); cls.getFldUsage().computeIfAbsent(fldShortId, fldId -> new FldUsageData(new FldRef(cls.getRawName(), fldId))) .setUsage(readMthList(in, methods)); } } return data; } private static void writeData(DataOutputStream out, RawUsageData usageData) throws IOException { Map clsMap = new HashMap<>(); Map mthMap = new HashMap<>(); Map uMthMap = new HashMap<>(); Map clsDataMap = usageData.getClsMap(); List classes = new ArrayList<>(clsDataMap.keySet()); Collections.sort(classes); List classesWithoutData = usageData.getClassesWithoutData(); // Class information writeUVInt(out, classes.size()); writeUVInt(out, classesWithoutData.size()); int i = 0; for (String cls : classes) { out.writeUTF(cls); clsMap.put(cls, i++); } for (String cls : classesWithoutData) { out.writeUTF(cls); clsMap.put(cls, i++); } // Method information List methods = clsDataMap.values().stream() .flatMap(c -> c.getMthUsage().values().stream()) .map(MthUsageData::getMthRef) .collect(Collectors.toList()); writeUVInt(out, methods.size()); int j = 0; for (MthRef mth : methods) { writeUVInt(out, clsMap.get(mth.getCls())); out.writeUTF(mth.getShortId()); mthMap.put(mth, j++); } // Unresolved method information Set unresolvedMethods = clsDataMap.values().stream() .flatMap(classUsageData -> classUsageData.getMthUsage().values().stream()) .flatMap(methodUsageData -> { List unresolvedUsageList = methodUsageData.getUnresolvedUsage(); return (unresolvedUsageList == null) ? null : unresolvedUsageList.stream(); }) .filter(Objects::nonNull) .collect(Collectors.toSet()); writeUVInt(out, unresolvedMethods.size()); int k = 0; for (IMethodRef uMth : unresolvedMethods) { String name = uMth.getName(); out.writeUTF((name == null) ? "" : name); String parentClassType = uMth.getParentClassType(); out.writeUTF((parentClassType == null) ? "" : parentClassType); String returnType = uMth.getReturnType(); out.writeUTF((returnType == null) ? "" : returnType); List argTypes = uMth.getArgTypes(); if (argTypes == null) { out.writeInt(0); } else { out.writeInt(argTypes.size()); for (String arg : argTypes) { out.writeUTF(arg); } } uMthMap.put(uMth, k++); } // Usage data for (String cls : classes) { ClsUsageData clsData = clsDataMap.get(cls); writeClsList(out, clsMap, clsData.getClsDeps()); writeClsList(out, clsMap, clsData.getClsUsage()); writeMthList(out, mthMap, clsData.getClsUseInMth()); writeUVInt(out, clsData.getMthUsage().size()); for (MthUsageData mthData : clsData.getMthUsage().values()) { writeUVInt(out, mthMap.get(mthData.getMthRef())); writeMthList(out, mthMap, mthData.getUsage()); writeMthList(out, mthMap, mthData.getUses()); writeUnresolvedMthList(out, uMthMap, mthData.getUnresolvedUsage()); out.writeBoolean(mthData.callsSelf()); } writeUVInt(out, clsData.getFldUsage().size()); for (FldUsageData fldData : clsData.getFldUsage().values()) { out.writeUTF(fldData.getFldRef().getShortId()); writeMthList(out, mthMap, fldData.getUsage()); } } } private static List readClsList(DataInputStream in, String[] classes) throws IOException { int count = readUVInt(in); if (count == 0) { return Collections.emptyList(); } List list = new ArrayList<>(count); for (int i = 0; i < count; i++) { list.add(classes[readUVInt(in)]); } return list; } private static void writeClsList(DataOutputStream out, Map clsMap, List clsList) throws IOException { if (Utils.isEmpty(clsList)) { writeUVInt(out, 0); return; } writeUVInt(out, clsList.size()); for (String cls : clsList) { Integer clsId = clsMap.get(cls); if (clsId == null) { throw new JadxRuntimeException("Unknown class in usage: " + cls); } writeUVInt(out, clsId); } } private static List readMthList(DataInputStream in, MthRef[] methods) throws IOException { int count = readUVInt(in); if (count == 0) { return Collections.emptyList(); } List list = new ArrayList<>(count); for (int i = 0; i < count; i++) { list.add(methods[readUVInt(in)]); } return list; } private static void writeMthList(DataOutputStream out, Map mthMap, List mthList) throws IOException { if (Utils.isEmpty(mthList)) { writeUVInt(out, 0); return; } writeUVInt(out, mthList.size()); for (MthRef mth : mthList) { writeUVInt(out, mthMap.get(mth)); } } private static List readUnresolvedMthList(DataInputStream in, IMethodRef[] methods) throws IOException { int count = readUVInt(in); if (count == 0) { return Collections.emptyList(); } List list = new ArrayList<>(count); for (int i = 0; i < count; i++) { list.add(methods[readUVInt(in)]); } return list; } private static void writeUnresolvedMthList(DataOutputStream out, Map uMthMap, List mthList) throws IOException { if (Utils.isEmpty(mthList)) { writeUVInt(out, 0); return; } writeUVInt(out, mthList.size()); for (IMethodRef mth : mthList) { writeUVInt(out, uMthMap.get(mth)); } } private static String buildInputsHash(List inputs) { List paths = inputs.stream() .filter(f -> !f.getName().endsWith(".jadx.kts")) .map(File::toPath) .collect(Collectors.toList()); return FileUtils.buildInputsHash(paths); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/cache/usage/UsageInfoCache.java ================================================ package jadx.gui.cache.usage; import java.io.File; import java.nio.file.Path; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.usage.IUsageInfoCache; import jadx.api.usage.IUsageInfoData; import jadx.api.usage.impl.InMemoryUsageInfoCache; import jadx.core.dex.nodes.RootNode; public class UsageInfoCache implements IUsageInfoCache { private static final Object LOAD_DATA_SYNC = new Object(); private final Path usageFile; private final List inputs; private final InMemoryUsageInfoCache memCache = new InMemoryUsageInfoCache(); private @Nullable RawUsageData rawUsageData; public UsageInfoCache(Path cacheDir, List inputFiles) { usageFile = cacheDir.resolve("usage"); inputs = inputFiles; } @Override public @Nullable IUsageInfoData get(RootNode root) { IUsageInfoData memData = memCache.get(root); if (memData != null) { return memData; } synchronized (LOAD_DATA_SYNC) { if (rawUsageData == null) { rawUsageData = UsageFileAdapter.load(usageFile, inputs); } if (rawUsageData != null) { UsageData data = new UsageData(root, rawUsageData); memCache.set(root, data); return data; } } return null; } @Override public void set(RootNode root, IUsageInfoData data) { memCache.set(root, data); UsageFileAdapter.save(data, usageFile, inputs); } @Override public void close() { rawUsageData = null; memCache.close(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/ArtAdapter.java ================================================ package jadx.gui.device.debugger; public class ArtAdapter { public interface IArtAdapter { int getRuntimeRegNum(int smaliNum, int regCount, int paramStart); boolean readNullObject(); String typeForNull(); } public static IArtAdapter getAdapter(int androidReleaseVer) { if (androidReleaseVer <= 8) { return new AndroidOreoAndBelow(); } else { return new AndroidPieAndAbove(); } } public static class AndroidOreoAndBelow implements IArtAdapter { @Override public int getRuntimeRegNum(int smaliNum, int regCount, int paramStart) { int localRegCount = regCount - paramStart; return (smaliNum + localRegCount) % regCount; } @Override public boolean readNullObject() { return true; } @Override public String typeForNull() { return ""; } } public static class AndroidPieAndAbove implements IArtAdapter { @Override public int getRuntimeRegNum(int smaliNum, int regCount, int paramStart) { return smaliNum; } @Override public boolean readNullObject() { return false; } @Override public String typeForNull() { return "zero value"; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/BreakpointManager.java ================================================ package jadx.gui.device.debugger; 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.nio.file.Paths; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import jadx.core.dex.nodes.ClassNode; import jadx.gui.device.debugger.smali.Smali; import jadx.gui.treemodel.JClass; import static jadx.core.utils.GsonUtils.buildGson; public class BreakpointManager { private static final Logger LOG = LoggerFactory.getLogger(BreakpointManager.class); private static final Gson GSON = buildGson(); private static final Type TYPE_TOKEN = new TypeToken>>() { }.getType(); private static @NotNull Map> bpm = Collections.emptyMap(); private static @Nullable Path savePath; private static DebugController debugController; private static Map> listeners = Collections.emptyMap(); // class full name as key public static void saveAndExit() { sync(); bpm = Collections.emptyMap(); savePath = null; listeners = Collections.emptyMap(); } public static void init(@Nullable Path baseDir) { Path saveDir = baseDir != null ? baseDir : Paths.get("."); savePath = saveDir.resolve("breakpoints.json"); // TODO: move into project file or same dir as project file if (Files.exists(savePath)) { try (Reader reader = Files.newBufferedReader(savePath, StandardCharsets.UTF_8)) { bpm = GSON.fromJson(reader, TYPE_TOKEN); } catch (Exception e) { LOG.error("Failed to read breakpoints config: {}", savePath, e); } } } /** * @param listener When breakpoint is failed to set during debugging, this listener will be called. */ public static void addListener(JClass topCls, Listener listener) { if (listeners.isEmpty()) { listeners = new HashMap<>(); } listeners.put(DbgUtils.getRawFullName(topCls), new SimpleEntry<>(topCls.getCls().getClassNode(), listener)); } public static void removeListener(JClass topCls) { listeners.remove(DbgUtils.getRawFullName(topCls)); } public static List getPositions(JClass topCls) { List bps = bpm.get(DbgUtils.getRawFullName(topCls)); if (bps != null && bps.size() > 0) { Smali smali = DbgUtils.getSmali(topCls.getCls().getClassNode()); if (smali != null) { List posList = new ArrayList<>(bps.size()); for (FileBreakpoint bp : bps) { int pos = smali.getInsnPosByCodeOffset(bp.getFullMthRawID(), bp.codeOffset); if (pos > -1) { posList.add(pos); } } return posList; } } return Collections.emptyList(); } public static boolean set(JClass topCls, int line) { Entry lineInfo = DbgUtils.getCodeOffsetInfoByLine(topCls, line); if (lineInfo != null) { if (bpm.isEmpty()) { bpm = new HashMap<>(); } String name = DbgUtils.getRawFullName(topCls); List list = bpm.computeIfAbsent(name, k -> new ArrayList<>()); FileBreakpoint bkp = list.stream() .filter(bp -> bp.codeOffset == lineInfo.getValue() && bp.getFullMthRawID().equals(lineInfo.getKey())) .findFirst() .orElse(null); boolean ok = true; if (bkp == null) { String[] sigs = DbgUtils.sepClassAndMthSig(lineInfo.getKey()); if (sigs != null && sigs.length == 2) { FileBreakpoint bp = new FileBreakpoint(sigs[0], sigs[1], lineInfo.getValue()); list.add(bp); if (debugController != null) { ok = debugController.setBreakpoint(bp); } } } return ok; } return false; } public static boolean remove(JClass topCls, int line) { Entry lineInfo = DbgUtils.getCodeOffsetInfoByLine(topCls, line); if (lineInfo != null) { List bps = bpm.get(DbgUtils.getRawFullName(topCls)); for (Iterator it = bps.iterator(); it.hasNext();) { FileBreakpoint bp = it.next(); if (bp.codeOffset == lineInfo.getValue() && bp.getFullMthRawID().equals(lineInfo.getKey())) { it.remove(); if (debugController != null) { return debugController.removeBreakpoint(bp); } break; } } } return true; } private static void sync() { if (savePath == null) { return; } if (bpm.isEmpty() && !Files.exists(savePath)) { // user didn't do anything with breakpoint so don't output breakpoint file. return; } try { Files.write(savePath, GSON.toJson(bpm).getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { LOG.error("Failed to write breakpoints config: {}", savePath, e); } } public interface Listener { void breakpointDisabled(int codeOffset); } protected static class FileBreakpoint { public String cls; public String mth; public long codeOffset; public FileBreakpoint() { // needed for JSON deserialization } private FileBreakpoint(String cls, String mth, long codeOffset) { this.cls = cls; this.mth = mth; this.codeOffset = codeOffset; } protected String getFullMthRawID() { return cls + "." + mth; } @Override public int hashCode() { return Objects.hash(codeOffset, cls, mth); } @Override public boolean equals(Object obj) { if (obj instanceof FileBreakpoint) { if (obj == this) { return true; } FileBreakpoint fbp = (FileBreakpoint) obj; return fbp.codeOffset == codeOffset && fbp.cls.equals(cls) && fbp.mth.equals(mth); } return false; } } protected static List getAllBreakpoints() { List bpList = new ArrayList<>(); for (Entry> entry : bpm.entrySet()) { bpList.addAll(entry.getValue()); } return bpList; } protected static void failBreakpoint(FileBreakpoint bp) { Entry entry = listeners.get(bp.cls); if (entry != null) { int pos = DbgUtils.getSmali(entry.getKey()) .getInsnPosByCodeOffset(bp.getFullMthRawID(), bp.codeOffset); pos = Math.max(0, pos); entry.getValue().breakpointDisabled(pos); } } protected static void setDebugController(DebugController controller) { debugController = controller; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java ================================================ package jadx.gui.device.debugger; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import jadx.core.deobf.NameMapper; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.utils.android.AndroidManifestParser; import jadx.core.utils.android.AppAttribute; import jadx.core.utils.android.ApplicationParams; import jadx.gui.device.debugger.smali.Smali; import jadx.gui.device.debugger.smali.SmaliMethodNode; import jadx.gui.treemodel.JClass; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class DbgUtils { private static final Logger LOG = LoggerFactory.getLogger(DbgUtils.class); private static Map smaliCache = Collections.emptyMap(); protected static Smali getSmali(ClassNode topCls) { if (smaliCache == Collections.EMPTY_MAP) { smaliCache = new HashMap<>(); } return smaliCache.computeIfAbsent(topCls.getTopParentClass().getClassInfo(), c -> Smali.disassemble(topCls)); } public static String getSmaliCode(ClassNode topCls) { Smali smali = getSmali(topCls); if (smali != null) { return smali.getCode(); } return null; } public static Entry getCodeOffsetInfoByLine(JClass cls, int line) { Smali smali = getSmali(cls.getCls().getClassNode().getTopParentClass()); if (smali != null) { return smali.getMthFullIDAndCodeOffsetByLine(line); } return null; } @Nullable public static SmaliMethodNode getSmaliMethodNode(JClass cls, String mthRawFullID) { Smali smali = getSmali(cls.getCls().getClassNode().getTopParentClass()); if (smali != null) { return smali.getMethodNode(mthRawFullID); } return null; } public static void printSmaliLineMapping(SmaliMethodNode smn) { for (Map.Entry lineToCodeOffset : smn.getLineMapping().entrySet()) { LOG.debug("line={} -> codeOffset={}", lineToCodeOffset.getKey(), lineToCodeOffset.getValue()); } } public static String[] sepClassAndMthSig(String fullSig) { int pos = fullSig.indexOf('('); if (pos != -1) { pos = fullSig.lastIndexOf('.', pos); if (pos != -1) { String[] sigs = new String[2]; sigs[0] = fullSig.substring(0, pos); sigs[1] = fullSig.substring(pos + 1); return sigs; } } return null; } // doesn't replace $ public static String classSigToRawFullName(String clsSig) { if (clsSig != null && clsSig.startsWith("L") && clsSig.endsWith(";")) { clsSig = clsSig.substring(1, clsSig.length() - 1) .replace("/", "."); } return clsSig; } // replaces $ public static String classSigToFullName(String clsSig) { if (clsSig != null && clsSig.startsWith("L") && clsSig.endsWith(";")) { clsSig = clsSig.substring(1, clsSig.length() - 1) .replace("/", ".") .replace("$", "."); } return clsSig; } public static String getRawFullName(JClass topCls) { return topCls.getCls().getClassNode().getClassInfo().makeRawFullName(); } public static boolean isStringObjectSig(String objectSig) { return objectSig.equals("Ljava/lang/String;"); } public static JClass getTopClassBySig(String clsSig, MainWindow mainWindow) { clsSig = DbgUtils.classSigToFullName(clsSig); JavaClass cls = mainWindow.getWrapper().getDecompiler().searchJavaClassOrItsParentByOrigFullName(clsSig); if (cls != null) { JClass jc = mainWindow.getCacheObject().getNodeCache().makeFrom(cls); return jc.getRootClass(); } return null; } public static JClass getJClass(JavaClass cls, MainWindow mainWindow) { return mainWindow.getCacheObject().getNodeCache().makeFrom(cls); } public static ClassNode getClassNodeBySig(String clsSig, MainWindow mainWindow) { clsSig = DbgUtils.classSigToFullName(clsSig); return mainWindow.getWrapper().getDecompiler().searchClassNodeByOrigFullName(clsSig); } public static boolean isPrintableChar(int c) { return 32 <= c && c <= 126; } public static final class AppData { private final String appPackage; private final JavaClass mainActivityCls; public AppData(String appPackage, JavaClass mainActivityCls) { this.appPackage = appPackage; this.mainActivityCls = mainActivityCls; } public String getAppPackage() { return appPackage; } public JavaClass getMainActivityCls() { return mainActivityCls; } public String getProcessName() { return appPackage + '/' + mainActivityCls.getClassNode().getClassInfo().getFullName(); } } public static @Nullable AppData parseAppData(MainWindow mw) { JadxDecompiler decompiler = mw.getWrapper().getDecompiler(); String appPkg = decompiler.getRoot().getAppPackage(); if (appPkg == null) { UiUtils.errorMessage(mw, NLS.str("error_dialog.not_found", "App package")); return null; } AndroidManifestParser parser = new AndroidManifestParser( AndroidManifestParser.getAndroidManifest(decompiler.getResources()), EnumSet.of(AppAttribute.MAIN_ACTIVITY), decompiler.getArgs().getSecurity()); if (!parser.isManifestFound()) { UiUtils.errorMessage(mw, NLS.str("error_dialog.not_found", "AndroidManifest.xml")); return null; } ApplicationParams results = parser.parse(); String mainActivityName = results.getMainActivity(); if (mainActivityName == null) { UiUtils.errorMessage(mw, NLS.str("adb_dialog.msg_read_mani_fail")); return null; } if (!NameMapper.isValidFullIdentifier(mainActivityName)) { UiUtils.errorMessage(mw, "Invalid main activity name"); return null; } JavaClass mainActivityClass = results.getMainActivityJavaClass(decompiler); if (mainActivityClass == null) { UiUtils.errorMessage(mw, NLS.str("error_dialog.not_found", "Main activity class")); return null; } return new AppData(appPkg, mainActivityClass); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/DebugController.java ================================================ package jadx.gui.device.debugger; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.swing.JOptionPane; import javax.swing.tree.DefaultMutableTreeNode; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.device.debugger.BreakpointManager.FileBreakpoint; import jadx.gui.device.debugger.SmaliDebugger.Frame; import jadx.gui.device.debugger.SmaliDebugger.RuntimeBreakpoint; import jadx.gui.device.debugger.SmaliDebugger.RuntimeDebugInfo; import jadx.gui.device.debugger.SmaliDebugger.RuntimeField; import jadx.gui.device.debugger.SmaliDebugger.RuntimeRegister; import jadx.gui.device.debugger.SmaliDebugger.RuntimeValue; import jadx.gui.device.debugger.SmaliDebugger.RuntimeVarInfo; import jadx.gui.device.debugger.smali.Smali; import jadx.gui.device.debugger.smali.SmaliRegister; import jadx.gui.treemodel.JClass; import jadx.gui.ui.panel.IDebugController; import jadx.gui.ui.panel.JDebuggerPanel; import jadx.gui.ui.panel.JDebuggerPanel.IListElement; import jadx.gui.ui.panel.JDebuggerPanel.ValueTreeNode; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public final class DebugController implements SmaliDebugger.SuspendListener, IDebugController { private static final Logger LOG = LoggerFactory.getLogger(DebugController.class); private static final String ONCREATE_SIGNATURE = "onCreate(Landroid/os/Bundle;)V"; private static final Map TYPE_MAP = new HashMap<>(); private static final RuntimeType[] POSSIBLE_TYPES = { RuntimeType.OBJECT, RuntimeType.INT, RuntimeType.LONG }; private static final int DEFAULT_CACHE_SIZE = 512; private JDebuggerPanel debuggerPanel; private SmaliDebugger debugger; private ArtAdapter.IArtAdapter art; private final CurrentInfo cur = new CurrentInfo(); private BreakpointStore bpStore; private boolean updateAllFldAndReg = false; // update all fields and registers private ValueTreeNode toBeUpdatedTreeNode; // a field or register number. private volatile boolean isSuspended = true; private boolean hasResumed; private ResumeCmd run; private ResumeCmd stepOver; private ResumeCmd stepInto; private ResumeCmd stepOut; private StateListener stateListener; private final Map regAdaMap = new ConcurrentHashMap<>(); private final ExecutorService updateQueue = Executors.newSingleThreadExecutor(); private final ExecutorService lazyQueue = Executors.newSingleThreadExecutor(); @Override public boolean startDebugger(JDebuggerPanel debuggerPanel, String adbHost, int adbPort, int androidVer) { if (TYPE_MAP.isEmpty()) { initTypeMap(); } this.debuggerPanel = debuggerPanel; UiUtils.uiRunAndWait(debuggerPanel::resetUI); try { debugger = SmaliDebugger.attach(adbHost, adbPort, this); } catch (SmaliDebuggerException e) { JOptionPane.showMessageDialog(debuggerPanel.getMainWindow(), e.getMessage(), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); logErr(e); return false; } art = ArtAdapter.getAdapter(androidVer); resetAllInfo(); hasResumed = false; run = debugger::resume; stepOver = debugger::stepOver; stepInto = debugger::stepInto; stepOut = debugger::stepOut; stopAtOnCreate(); if (bpStore == null) { bpStore = new BreakpointStore(); } else { bpStore.reset(); } BreakpointManager.setDebugController(this); initBreakpoints(BreakpointManager.getAllBreakpoints()); return true; } private void openMainActivityTab(JClass mainActivity) { String fullID = DbgUtils.getRawFullName(mainActivity) + "." + ONCREATE_SIGNATURE; Smali smali = DbgUtils.getSmali(mainActivity.getCls().getClassNode()); int pos = smali.getMethodDefPos(fullID); int finalPos = Math.max(1, pos); debuggerPanel.scrollToSmaliLine(mainActivity, finalPos, true); } private void stopAtOnCreate() { DbgUtils.AppData appData = DbgUtils.parseAppData(debuggerPanel.getMainWindow()); if (appData == null) { debuggerPanel.log("Failed to set breakpoint at onCreate, you have to do it yourself."); return; } JClass mainActivity = DbgUtils.getJClass(appData.getMainActivityCls(), debuggerPanel.getMainWindow()); lazyQueue.execute(() -> openMainActivityTab(mainActivity)); String clsSig = DbgUtils.getRawFullName(mainActivity); try { long id = debugger.getClassID(clsSig, true); if (id != -1) { return; // this app is running, we can't stop at onCreate anymore. } debuggerPanel.log(String.format("Breakpoint will set at %s.%s", clsSig, ONCREATE_SIGNATURE)); debugger.regMethodEntryEventSync(clsSig, ONCREATE_SIGNATURE::equals); } catch (SmaliDebuggerException e) { logErr(e, String.format("Failed set breakpoint at %s.%s", clsSig, ONCREATE_SIGNATURE)); } } @Override public boolean isSuspended() { return isSuspended; } @Override public boolean isDebugging() { return debugger != null; } @Override public boolean run() { return execResumeCmd(run); } @Override public boolean stepInto() { return execResumeCmd(stepInto); } @Override public boolean stepOver() { return execResumeCmd(stepOver); } @Override public boolean stepOut() { return execResumeCmd(stepOut); } @Override public boolean pause() { if (isDebugging()) { try { debugger.suspend(); } catch (SmaliDebuggerException e) { logErr(e); return false; } setDebuggerState(true, false); resetAllInfo(); } return true; } @Override public boolean stop() { if (isDebugging()) { try { debugger.exit(); } catch (SmaliDebuggerException e) { logErr(e); return false; } } return true; } @Override public boolean exit() { if (isDebugging()) { setDebuggerState(true, true); stop(); debugger = null; } BreakpointManager.setDebugController(null); debuggerPanel.getMainWindow().destroyDebuggerPanel(); debuggerPanel = null; return true; } /** * @param type must be one of int, long, float, double, string or object. */ @Override public boolean modifyRegValue(ValueTreeNode valNode, ArgType type, Object value) { checkType(type, value); if (isDebugging() && isSuspended()) { return modifyValueInternal(valNode, castType(type), value); } return false; } @Override public String getProcessName() { DbgUtils.AppData appData = DbgUtils.parseAppData(debuggerPanel.getMainWindow()); if (appData == null) { return ""; } return appData.getProcessName(); } private RuntimeType castType(ArgType type) { if (type == ArgType.INT) { return RuntimeType.INT; } if (type == ArgType.STRING) { return RuntimeType.STRING; } if (type == ArgType.LONG) { return RuntimeType.LONG; } if (type == ArgType.FLOAT) { return RuntimeType.FLOAT; } if (type == ArgType.DOUBLE) { return RuntimeType.DOUBLE; } if (type == ArgType.OBJECT) { return RuntimeType.OBJECT; } throw new JadxRuntimeException("Unexpected type: " + type); } protected static RuntimeType castType(String type) { RuntimeType rt = null; if (!StringUtils.isEmpty(type)) { rt = TYPE_MAP.get(type); } if (rt == null) { rt = POSSIBLE_TYPES[0]; } return rt; } private void checkType(ArgType type, Object value) { if (!(type == ArgType.INT && value instanceof Integer) && !(type == ArgType.STRING && value instanceof String) && !(type == ArgType.LONG && value instanceof Long) && !(type == ArgType.FLOAT && value instanceof Float) && !(type == ArgType.DOUBLE && value instanceof Double) && !(type == ArgType.OBJECT && value instanceof Long)) { throw new JadxRuntimeException("Type must be one of int, long, float, double, String or Object."); } } private boolean modifyValueInternal(ValueTreeNode valNode, RuntimeType type, Object value) { if (valNode instanceof RegTreeNode) { try { RegTreeNode regNode = (RegTreeNode) valNode; debugger.setValueSync( regNode.getRuntimeRegNum(), type, value, cur.frame.getThreadID(), cur.frame.getFrame().getID()); lazyQueue.execute(() -> { setRegsNotUpdated(); updateRegister((RegTreeNode) valNode, type, true); }); } catch (SmaliDebuggerException e) { logErr(e); return false; } } else if (valNode instanceof FieldTreeNode) { // TODO: check type. FieldTreeNode fldNode = (FieldTreeNode) valNode; try { debugger.setValueSync( fldNode.getObjectID(), ((RuntimeField) fldNode.getRuntimeValue()).getFieldID(), fldNode.getRuntimeField().getType(), value); lazyQueue.execute(() -> { updateField((FieldTreeNode) valNode); }); } catch (SmaliDebuggerException e) { logErr(e); return false; } } return true; } private interface ResumeCmd { void exec() throws SmaliDebuggerException; } private boolean execResumeCmd(ResumeCmd cmd) { if (!hasResumed) { if (cmd != run) { return false; } hasResumed = true; } if (isDebugging() && isSuspended()) { updateAllFldAndReg = cmd == run; setDebuggerState(false, false); try { cmd.exec(); return true; } catch (SmaliDebuggerException e) { logErr(e); setDebuggerState(true, false); } } return false; } /** * @param suspended suspended by step, breakpoint, etc.. * @param stopped remote app had been terminated, it's used to * change icons only, to check if it's running use isDebugging() instead. */ private void setDebuggerState(boolean suspended, boolean stopped) { isSuspended = suspended; if (stopped) { hasResumed = false; } if (stateListener != null) { stateListener.onStateChanged(suspended, stopped); } } @Override public void setStateListener(StateListener listener) { stateListener = listener; } @Override public void onSuspendEvent(SuspendInfo info) { if (!isDebugging()) { return; } if (info.isTerminated()) { debuggerPanel.log("Debugger exited."); setDebuggerState(true, true); debugger = null; return; } setDebuggerState(true, false); long threadID = info.getThreadID(); int refreshLevel = 2; // update all threads, stack frames, registers and fields. if (cur.frame != null) { if (threadID == cur.frame.getThreadID() && info.getClassID() == cur.frame.getClsID() && info.getMethodID() == cur.frame.getMthID()) { refreshLevel = 1; // relevant registers or fields. } else { cur.frame.getClsID(); } setRegsNotUpdated(); } if (refreshLevel == 2) { updateAllInfo(threadID, info.getOffset()); } else { if (cur.smali != null && cur.frame != null) { refreshRegInfo(info.getOffset()); refreshCurFrame(threadID, info.getOffset()); if (updateAllFldAndReg) { debuggerPanel.resetRegTreeNodes(); updateAllRegisters(cur.frame); } else if (toBeUpdatedTreeNode != null) { lazyQueue.execute(() -> updateRegOrField(toBeUpdatedTreeNode)); } markCodeOffset(info.getOffset()); } else { debuggerPanel.resetRegTreeNodes(); } if (cur.frame != null) { // update current code offset in stack frame. cur.frame.updateCodeOffset(info.getOffset()); debuggerPanel.refreshStackFrameList(Collections.emptyList()); } } } private void refreshRegInfo(long codeOffset) { List list = cur.regAdapter.getInfoAt(codeOffset); for (RegisterObserver.Info info : list) { RegTreeNode reg = cur.frame.getRegNodes().get(info.getSmaliRegNum()); if (info.isLoad()) { applyDbgInfo(reg, info.getName(), info.getType()); } else { reg.setAlias(""); reg.setAbsoluteType(false); } } if (list.size() > 0) { debuggerPanel.refreshRegisterTree(); } } private void updateRegOrField(ValueTreeNode valTreeNode) { if (valTreeNode instanceof RegTreeNode) { updateRegister((RegTreeNode) valTreeNode, null, true); return; } if (valTreeNode instanceof FieldTreeNode) { updateField((FieldTreeNode) valTreeNode); return; } } public void updateField(FieldTreeNode node) { try { setFieldsNotUpdated(); debugger.getValueSync(node.getObjectID(), node.getRuntimeField()); decodeRuntimeValue(node); debuggerPanel.updateThisTree(node); } catch (SmaliDebuggerException e) { logErr(e); } } public boolean updateRegister(RegTreeNode regNode, RuntimeType type, boolean retry) { if (type == null) { if (regNode.isAbsoluteType()) { type = castType(regNode.getType()); } else { type = POSSIBLE_TYPES[0]; } } boolean ok = false; RuntimeRegister register = null; try { register = debugger.getRegisterSync( cur.frame.getThreadID(), cur.frame.getFrame().getID(), regNode.getRuntimeRegNum(), type); } catch (SmaliDebuggerException e) { if (retry) { if (debugger.errIsTypeMismatched(e.getErrCode())) { RuntimeType[] types = getPossibleTypes(type); for (RuntimeType nextType : types) { ok = updateRegister(regNode, nextType, false); if (ok) { regNode.updateType(nextType.getDesc()); break; } } } else { logErr(e.getMessage() + " for " + regNode.getName()); regNode.updateType(null); regNode.updateValue(null); } } } if (register != null) { regNode.updateReg(register); decodeRuntimeValue(regNode); } debuggerPanel.updateRegTree(regNode); return ok; } private RuntimeType[] getPossibleTypes(RuntimeType cur) { RuntimeType[] types = new RuntimeType[2]; for (int i = 0, j = 0; i < POSSIBLE_TYPES.length; i++) { if (cur != POSSIBLE_TYPES[i]) { types[j++] = POSSIBLE_TYPES[i]; } } return types; } // when single stepping we can detect which reg need to be updated. private void markNextToBeUpdated(long codeOffset) { if (codeOffset != -1) { Object rst = cur.smali.getResultRegOrField(cur.mthFullID, codeOffset); toBeUpdatedTreeNode = null; if (cur.frame != null) { if (rst instanceof Integer) { int regNum = (int) rst; if (cur.frame.getRegNodes().size() > regNum) { toBeUpdatedTreeNode = cur.frame.getRegNodes().get(regNum); } return; } if (rst instanceof FieldInfo) { FieldInfo info = (FieldInfo) rst; toBeUpdatedTreeNode = cur.frame.getFieldNodes() .stream() .filter(f -> f.getName().equals(info.getName())) .findFirst() .orElse(null); } } } } private void updateAllThreads() { List threads; try { threads = debugger.getAllThreadsSync(); } catch (SmaliDebuggerException e) { logErr(e); return; } List threadEleList = new ArrayList<>(threads.size()); for (Long thread : threads) { ThreadBoxElement ele = new ThreadBoxElement(thread); threadEleList.add(ele); } debuggerPanel.refreshThreadBox(threadEleList); lazyQueue.execute(() -> { for (ThreadBoxElement ele : threadEleList) { // get thread names try { ele.setName(debugger.getThreadNameSync(ele.getThreadID())); } catch (SmaliDebuggerException e) { logErr(e); } } debuggerPanel.refreshThreadBox(Collections.emptyList()); }); } private FrameNode updateAllStackFrames(long threadID) { List frames = Collections.emptyList(); try { frames = debugger.getFramesSync(threadID); } catch (SmaliDebuggerException e) { logErr(e); } if (frames.isEmpty()) { return null; } List frameEleList = new ArrayList<>(frames.size()); for (SmaliDebugger.Frame frame : frames) { FrameNode ele = new FrameNode(threadID, frame); frameEleList.add(ele); } FrameNode curEle = frameEleList.get(0); fetchStackFrameNames(curEle); debuggerPanel.refreshStackFrameList(frameEleList); lazyQueue.execute(() -> { // get class & method names for frames for (int i = 1; i < frameEleList.size(); i++) { fetchStackFrameNames(frameEleList.get(i)); } debuggerPanel.refreshStackFrameList(Collections.emptyList()); }); return frameEleList.get(0); } private void fetchStackFrameNames(FrameNode ele) { try { long clsID = ele.getFrame().getClassID(); String clsSig = debugger.getClassSignatureSync(clsID); String mthSig = debugger.getMethodSignatureSync(clsID, ele.getFrame().getMethodID()); ele.setSignatures(clsSig, mthSig); } catch (SmaliDebuggerException e) { logErr(e); } } private Smali decodeSmali(FrameNode frame) { if (cur.frame.getClsSig() != null) { JClass jClass = DbgUtils.getTopClassBySig(frame.getClsSig(), debuggerPanel.getMainWindow()); if (jClass != null) { ClassNode cNode = jClass.getCls().getClassNode(); cur.clsNode = jClass; cur.mthFullID = DbgUtils.classSigToRawFullName(frame.getClsSig()) + "." + frame.getMthSig(); return DbgUtils.getSmali(cNode); } } return null; } private void refreshCurFrame(long threadID, long codeOffset) { try { Frame frame = debugger.getCurrentFrame(threadID); cur.frame.setFrame(frame); cur.frame.updateCodeOffset(codeOffset); } catch (SmaliDebuggerException e) { logErr(e); } } private void updateAllFields(FrameNode frame) { List fldNodes = Collections.emptyList(); String clsSig = frame.getClsSig(); if (clsSig != null) { ClassNode clsNode = DbgUtils.getClassNodeBySig(clsSig, debuggerPanel.getMainWindow()); if (clsNode != null) { fldNodes = clsNode.getFields(); } } try { long thisID = debugger.getThisID(frame.getThreadID(), frame.getFrame().getID()); List flds = debugger.getAllFieldsSync(frame.getClsID()); List nodes = new ArrayList<>(flds.size()); for (RuntimeField fld : flds) { FieldTreeNode fldNode = new FieldTreeNode(fld, thisID); fldNodes.stream() .filter(f -> f.getName().equals(fldNode.getName())) .findFirst() .ifPresent(smaliFld -> fldNode.setAlias(smaliFld.getAlias())); nodes.add(fldNode); } debuggerPanel.updateThisFieldNodes(nodes); frame.setFieldNodes(nodes); if (thisID > 0 && nodes.size() > 0) { lazyQueue.execute(() -> updateAllFieldValues(thisID, frame)); } } catch (SmaliDebuggerException e) { logErr(e); } } private void updateAllFieldValues(long thisID, FrameNode frame) { List nodes = frame.getFieldNodes(); if (nodes.size() > 0) { List flds = new ArrayList<>(nodes.size()); List rts = new ArrayList<>(nodes.size()); nodes.forEach(n -> { RuntimeField f = n.getRuntimeField(); if (f.isBelongToThis()) { flds.add(n); rts.add(f); } }); try { debugger.getAllFieldValuesSync(thisID, rts); flds.forEach(n -> decodeRuntimeValue(n)); debuggerPanel.refreshThisFieldTree(); } catch (SmaliDebuggerException e) { logErr(e); } } } private void updateAllRegisters(FrameNode frame) { UiUtils.uiRun(() -> { if (!buildRegTreeNodes(frame).isEmpty()) { fetchAllRegisters(frame); } }); } private void fetchAllRegisters(FrameNode frame) { List regs = cur.regAdapter.getInitializedList(frame.getCodeOffset()); for (SmaliRegister reg : regs) { RuntimeVarInfo info = cur.regAdapter.getInfo(reg.getRuntimeRegNum(), frame.getCodeOffset()); RegTreeNode regNode = frame.getRegNodes().get(reg.getRegNum()); if (info != null) { applyDbgInfo(regNode, info); } updateRegister(regNode, null, true); } } private void applyDbgInfo(RegTreeNode rn, RuntimeVarInfo info) { applyDbgInfo(rn, info.getName(), info.getType()); } private void applyDbgInfo(RegTreeNode rn, String alias, String type) { rn.setAlias(alias); rn.updateType(type); rn.setAbsoluteType(true); } private void setRegsNotUpdated() { if (cur.frame != null) { for (RegTreeNode regNode : cur.frame.getRegNodes()) { regNode.setUpdated(false); } } } private void setFieldsNotUpdated() { if (cur.frame != null) { for (FieldTreeNode node : cur.frame.getFieldNodes()) { node.setUpdated(false); } } } private List buildRegTreeNodes(FrameNode frame) { List regs = cur.smali.getRegisterList(cur.mthFullID); List regNodes = new ArrayList<>(regs.size()); List inRtOrder = new ArrayList<>(regs.size()); regs.forEach(r -> { RegTreeNode rn = new RegTreeNode(r); regNodes.add(rn); inRtOrder.add(rn); }); inRtOrder.sort(Comparator.comparingInt(RegTreeNode::getRuntimeRegNum)); frame.setRegNodes(regNodes); debuggerPanel.updateRegTreeNodes(inRtOrder); debuggerPanel.refreshRegisterTree(); return regNodes; } private boolean decodeRuntimeValue(RuntimeValueTreeNode valNode) { RuntimeValue rValue = valNode.getRuntimeValue(); RuntimeType type = rValue.getType(); if (!valNode.isAbsoluteType()) { valNode.updateType(null); } try { switch (type) { case OBJECT: return decodeObject(valNode); case STRING: String str = "\"" + debugger.readStringSync(rValue) + "\""; valNode.updateType("java.lang.String") .updateTypeID(debugger.readID(rValue)) .updateValue(str); break; case INT: valNode.updateValue(Integer.toString(debugger.readInt(rValue))); break; case LONG: valNode.updateValue(Long.toString(debugger.readAll(rValue))); break; case ARRAY: decodeArrayVal(valNode); break; case BOOLEAN: { int b = debugger.readByte(rValue); valNode.updateValue(b == 1 ? "true" : "false"); break; } case SHORT: valNode.updateValue(Short.toString(debugger.readShort(rValue))); break; case CHAR: case BYTE: { int b = (int) debugger.readAll(rValue); if (DbgUtils.isPrintableChar(b)) { valNode.updateValue(type == RuntimeType.CHAR ? String.valueOf((char) b) : String.valueOf((byte) b)); } else { valNode.updateValue(String.valueOf(b)); } break; } case DOUBLE: double d = debugger.readDouble(rValue); valNode.updateValue(Double.toString(d)); break; case FLOAT: float f = debugger.readFloat(rValue); valNode.updateValue(Float.toString(f)); break; case VOID: valNode.updateType("void"); break; case THREAD: valNode.updateType("thread").updateTypeID(debugger.readID(rValue)); break; case THREAD_GROUP: valNode.updateType("thread_group").updateTypeID(debugger.readID(rValue)); break; case CLASS_LOADER: valNode.updateType("class_loader").updateTypeID(debugger.readID(rValue)); break; case CLASS_OBJECT: valNode.updateType("class_object").updateTypeID(debugger.readID(rValue)); break; } } catch (SmaliDebuggerException e) { logErr(e); return false; } return true; } private boolean decodeObject(RuntimeValueTreeNode valNode) { RuntimeValue rValue = valNode.getRuntimeValue(); boolean ok = true; if (debugger.readID(rValue) == 0) { if (valNode.isAbsoluteType()) { valNode.updateValue("null"); return ok; } else if (!art.readNullObject()) { valNode.updateType(art.typeForNull()); valNode.updateValue("0"); return ok; } } String sig; try { sig = debugger.readObjectSignatureSync(rValue); valNode.updateType(String.format("%s@%d", DbgUtils.classSigToRawFullName(sig), debugger.readID(rValue))); } catch (SmaliDebuggerException e) { ok = debugger.errIsInvalidObject(e.getErrCode()) && valNode instanceof RegTreeNode; if (ok) { try { RegTreeNode reg = (RegTreeNode) valNode; RuntimeRegister rr = debugger.getRegisterSync( cur.frame.getThreadID(), cur.frame.getFrame().getID(), reg.getRuntimeRegNum(), RuntimeType.INT); reg.updateReg(rr); rValue = rr; valNode.updateType(RuntimeType.INT.getDesc()); valNode.updateValue(Long.toString((int) debugger.readAll(rValue))); } catch (SmaliDebuggerException except) { logErr(except, String.format("Update %s failed, %s", valNode.getName(), except.getMessage())); valNode.updateValue(except.getMessage()); ok = false; } } else { logErr(e); } } return ok; } private void decodeArrayVal(RuntimeValueTreeNode valNode) throws SmaliDebuggerException { String type = debugger.readObjectSignatureSync(valNode.getRuntimeValue()); ArgType argType = ArgType.parse(type); String javaType = argType.toString(); Entry> ret = debugger.readArray(valNode.getRuntimeValue(), 0, 0); javaType = javaType.substring(0, javaType.length() - 1) + ret.getKey() + "]"; valNode.updateType(javaType + "@" + debugger.readID(valNode.getRuntimeValue())); if (argType.getArrayElement().isPrimitive()) { for (Long aLong : ret.getValue()) { valNode.add(new DefaultMutableTreeNode(Long.toString(aLong))); } return; } String typeSig = type.substring(1); if (DbgUtils.isStringObjectSig(typeSig)) { for (Long aLong : ret.getValue()) { valNode.add(new DefaultMutableTreeNode(debugger.readStringSync(aLong))); } return; } typeSig = DbgUtils.classSigToRawFullName(typeSig); for (Long aLong : ret.getValue()) { valNode.add(new DefaultMutableTreeNode(String.format("%s@%d", typeSig, aLong))); } } private void updateAllInfo(long threadID, long codeOffset) { updateQueue.execute(() -> { resetAllInfo(); cur.frame = updateAllStackFrames(threadID); if (cur.frame != null) { lazyQueue.execute(() -> updateAllFields(cur.frame)); if (cur.frame.getClsSig() == null || cur.frame.getMthSig() == null) { fetchStackFrameNames(cur.frame); } cur.smali = decodeSmali(cur.frame); if (cur.smali != null) { cur.regAdapter = regAdaMap.computeIfAbsent(cur.mthFullID, k -> RegisterObserver.merge( getRuntimeDebugInfo(cur.frame), getSmaliRegisterList(), art, cur.mthFullID)); if (cur.smali.getRegCount(cur.mthFullID) > 0) { updateAllRegisters(cur.frame); } markCodeOffset(codeOffset); } } updateAllThreads(); }); } private List getSmaliRegisterList() { int regCount = cur.smali.getRegCount(cur.mthFullID); int paramStart = cur.smali.getParamRegStart(cur.mthFullID); List srs = cur.smali.getRegisterList(cur.mthFullID); for (SmaliRegister sr : srs) { sr.setRuntimeRegNum(art.getRuntimeRegNum(sr.getRegNum(), regCount, paramStart)); } return srs; } private void resetAllInfo() { isSuspended = true; toBeUpdatedTreeNode = null; cur.reset(); UiUtils.uiRun(debuggerPanel::resetAllDebuggingInfo); } private List getRuntimeDebugInfo(FrameNode frame) { try { RuntimeDebugInfo dbgInfo = debugger.getRuntimeDebugInfo(frame.getClsID(), frame.getMthID()); if (dbgInfo != null) { return dbgInfo.getInfoList(); } } catch (SmaliDebuggerException ignore) { // logErr(e); } return Collections.emptyList(); } private void markCodeOffset(long codeOffset) { scrollToPos(codeOffset); markNextToBeUpdated(codeOffset); } private void logErr(Exception e, String extra) { debuggerPanel.log(e.getMessage()); debuggerPanel.log(extra); LOG.error(extra, e); } private void logErr(Exception e) { debuggerPanel.log(e.getMessage()); LOG.error("Debug error", e); } private void logErr(String e) { debuggerPanel.log(e); LOG.error("Debug error: {}", e); } private void scrollToPos(long codeOffset) { int pos = -1; if (codeOffset > -1) { pos = cur.smali.getInsnPosByCodeOffset(cur.mthFullID, codeOffset); } if (pos == -1) { pos = cur.smali.getMethodDefPos(cur.mthFullID); if (pos == -1) { debuggerPanel.log("Can't scroll to " + cur.mthFullID); return; } } debuggerPanel.scrollToSmaliLine(cur.clsNode, pos, true); } private void initBreakpoints(List fbps) { if (fbps.size() == 0) { return; } boolean fetch = true; for (FileBreakpoint fbp : fbps) { try { long id = debugger.getClassID(fbp.cls, fetch); // only fetch classes from JVM once, // if this time this class hasn't been loaded then it won't load next time, cuz JVM is freezed. fetch = false; if (id > -1) { setBreakpoint(id, fbp); } else { setDelayBreakpoint(fbp); } } catch (SmaliDebuggerException e) { logErr(e); failBreakpoint(fbp, e.getMessage()); } } } protected boolean setBreakpoint(FileBreakpoint bp) { if (!isDebugging()) { return true; } try { long cid = debugger.getClassID(bp.cls, true); if (cid > -1) { setBreakpoint(cid, bp); } else { setDelayBreakpoint(bp); } } catch (SmaliDebuggerException e) { logErr(e); BreakpointManager.failBreakpoint(bp); return false; } return true; } private void setDelayBreakpoint(FileBreakpoint bp) { boolean hasSet = bpStore.hasSetDelaied(bp.cls); bpStore.add(bp, null); if (!hasSet) { updateQueue.execute(() -> { try { debugger.regClassPrepareEventForBreakpoint(bp.cls, id -> { List list = bpStore.get(bp.cls); for (FileBreakpoint fbp : list) { setBreakpoint(id, fbp); } }); } catch (SmaliDebuggerException e) { logErr(e); failBreakpoint(bp, ""); } }); } } protected void setBreakpoint(long cid, FileBreakpoint fbp) { try { long mid = debugger.getMethodID(cid, fbp.mth); if (mid > -1) { RuntimeBreakpoint rbp = debugger.makeBreakpoint(cid, mid, fbp.codeOffset); debugger.setBreakpoint(rbp); bpStore.add(fbp, rbp); return; } } catch (SmaliDebuggerException e) { logErr(e); } failBreakpoint(fbp, "Failed to get method for breakpoint, " + fbp.mth + ":" + fbp.codeOffset); } private void failBreakpoint(FileBreakpoint fbp, String msg) { if (!msg.isEmpty()) { debuggerPanel.log(msg); } bpStore.removeBreakpoint(fbp); BreakpointManager.failBreakpoint(fbp); } protected boolean removeBreakpoint(FileBreakpoint fbp) { if (!isDebugging()) { return true; } RuntimeBreakpoint rbp = bpStore.removeBreakpoint(fbp); if (rbp != null) { try { debugger.removeBreakpoint(rbp); } catch (SmaliDebuggerException e) { logErr(e); return false; } } return true; } private static RuntimeBreakpoint delayBP = null; private class BreakpointStore { Map bpm = Collections.emptyMap(); BreakpointStore() { if (delayBP == null) { delayBP = debugger.makeBreakpoint(-1, -1, -1); } } void reset() { bpm.clear(); } boolean hasSetDelaied(String cls) { for (Entry entry : bpm.entrySet()) { if (entry.getValue() == delayBP && entry.getKey().cls.equals(cls)) { return true; } } return false; } List get(String cls) { List fbps = new ArrayList<>(); bpm.forEach((k, v) -> { if (v == delayBP && k.cls.equals(cls)) { fbps.add(k); bpm.remove(k); } }); return fbps; } void add(FileBreakpoint fbp, RuntimeBreakpoint rbp) { if (bpm == Collections.EMPTY_MAP) { bpm = new ConcurrentHashMap<>(); } bpm.put(fbp, rbp == null ? delayBP : rbp); } RuntimeBreakpoint removeBreakpoint(FileBreakpoint fbp) { return bpm.remove(fbp); } } public class FrameNode implements IListElement { private SmaliDebugger.Frame frame; private final long threadID; private String clsSig; private String mthSig; private StringBuilder cache; private long codeOffset = -1; private List regNodes; private List thisNodes; private long thisID; public FrameNode(long threadID, SmaliDebugger.Frame frame) { cache = new StringBuilder(DEFAULT_CACHE_SIZE); this.frame = frame; this.threadID = threadID; regNodes = Collections.emptyList(); thisNodes = Collections.emptyList(); } public SmaliDebugger.Frame getFrame() { return frame; } public void setFrame(SmaliDebugger.Frame frame) { this.frame = frame; } public long getClsID() { return frame.getClassID(); } public long getMthID() { return frame.getMethodID(); } public long getThreadID() { return threadID; } public long getThisID() { return thisID; } public void setThisID(long thisID) { this.thisID = thisID; } public void setSignatures(String clsSig, String mthSig) { this.clsSig = clsSig; this.mthSig = mthSig; resetCache(); } public String getClsSig() { return clsSig; } public String getMthSig() { return mthSig; } public void updateCodeOffset(long codeOffset) { this.codeOffset = codeOffset; if (this.codeOffset > -1) { resetCache(); } } public long getCodeOffset() { return codeOffset == -1 ? frame.getCodeIndex() : codeOffset; } public void setRegNodes(List regNodes) { this.regNodes = regNodes; } public List getRegNodes() { return regNodes; } public List getFieldNodes() { return thisNodes; } public void setFieldNodes(List thisNodes) { this.thisNodes = thisNodes; } @Override public void onSelected() { if (clsSig != null) { JClass cls = DbgUtils.getTopClassBySig(clsSig, debuggerPanel.getMainWindow()); if (cls != null) { Smali smali = DbgUtils.getSmali(cls.getCls().getClassNode()); if (smali != null) { int pos = smali.getInsnPosByCodeOffset( DbgUtils.classSigToRawFullName(clsSig) + "." + mthSig, getCodeOffset()); debuggerPanel.scrollToSmaliLine(cls, Math.max(0, pos), true); return; } } debuggerPanel.log("Can't open smali panel for " + clsSig + "->" + mthSig); } } private void resetCache() { // Do not reuse thee existing cache instance as this can result in // multi-threading access issues in case toString() method is active this.cache = new StringBuilder(DEFAULT_CACHE_SIZE); } @Override public String toString() { StringBuilder sbCache = cache; if (sbCache.length() == 0) { long off = getCodeOffset(); if (off < 0) { sbCache.append(String.format("index: %-4d ", off)); } else { sbCache.append(String.format("index: %04x ", off)); } if (clsSig == null) { sbCache.append("clsID: ").append(frame.getClassID()); } else { sbCache.append(clsSig).append("->"); } if (mthSig == null) { sbCache.append(" mthID: ").append(frame.getMethodID()); } else { sbCache.append(mthSig); } } return sbCache.toString(); } } private static class ThreadBoxElement implements IListElement { private long threadID; private String name; public ThreadBoxElement(long threadID) { this.threadID = threadID; } public void setName(String name) { this.name = name; } public long getThreadID() { return threadID; } @Override public String toString() { if (name == null) { return "thread id: " + threadID; } return "thread id: " + threadID + " name:" + name; } @Override public void onSelected() { } } private static class RegTreeNode extends RuntimeValueTreeNode { private static final long serialVersionUID = -1111111202103122234L; private final SmaliRegister smaliReg; private RuntimeRegister runtimeReg; private String value; private String type; private String alias; private boolean absType; public RegTreeNode(SmaliRegister smaliReg) { this.smaliReg = smaliReg; } public void updateReg(RuntimeRegister reg) { runtimeReg = reg; } public void setAlias(String alias) { this.alias = alias; } @Override public RegTreeNode updateValue(String value) { setUpdated(true); this.value = value; removeAllChildren(); return this; } @Override public RegTreeNode updateType(String type) { if (this.type == null || !this.type.equals(type)) { this.type = type; reset(); } return this; } private void reset() { value = null; removeAllChildren(); setUpdated(true); this.absType = false; updateTypeID(0); } @Override public String getName() { if (!StringUtils.isEmpty(alias)) { return String.format("%s (%s)", smaliReg.getName(), alias); } return String.format("%-3s", smaliReg.getName()); } @Override @Nullable public String getValue() { return value; } public RuntimeRegister getRuntimeReg() { return runtimeReg; } public int getRuntimeRegNum() { return smaliReg.getRuntimeRegNum(); } @Override public String getType() { if (type != null) { return type; } if (runtimeReg != null) { return runtimeReg.getType().getDesc(); } return null; } @Override public RuntimeValue getRuntimeValue() { return getRuntimeReg(); } @Override public boolean isAbsoluteType() { return absType; } public void setAbsoluteType(boolean abs) { absType = abs; } } static class FieldTreeNode extends RuntimeValueTreeNode { private static final long serialVersionUID = -1111111202103122235L; private final RuntimeField field; private String value; private String alias; private long objectID; private FieldTreeNode(RuntimeField field, long id) { this.field = field; objectID = id; } public long getObjectID() { return objectID; } public void setObjectID(long id) { this.objectID = id; } public RuntimeField getRuntimeField() { return this.field; } public void setAlias(String alias) { this.alias = alias; } @Override public FieldTreeNode updateValue(String val) { setUpdated(true); value = val; removeAllChildren(); return this; } @Override public FieldTreeNode updateType(String val) { return this; } @Override public String getName() { if (StringUtils.isEmpty(alias) || alias.equals(field.getName())) { return field.getName(); } return field.getName() + " (" + alias + ")"; } @Override public String getValue() { return value; } @Override public String getType() { return ArgType.parse(field.getFieldType()).toString(); } @Override public RuntimeValue getRuntimeValue() { return field; } @Override public boolean isAbsoluteType() { return true; } } private abstract static class RuntimeValueTreeNode extends ValueTreeNode { private static final long serialVersionUID = -1111111202103260222L; private long typeID; @Override public ValueTreeNode updateTypeID(long id) { this.typeID = id; return this; } @Override public long getTypeID() { return this.typeID; } public abstract RuntimeValue getRuntimeValue(); public abstract boolean isAbsoluteType(); } private class CurrentInfo { JClass clsNode; String mthFullID; Smali smali; FrameNode frame; RegisterObserver regAdapter; public void reset() { frame = null; smali = null; clsNode = null; regAdapter = null; mthFullID = ""; } } private static void initTypeMap() { TYPE_MAP.put("I", RuntimeType.INT); TYPE_MAP.put("Z", RuntimeType.INT); TYPE_MAP.put("B", RuntimeType.INT); TYPE_MAP.put("C", RuntimeType.INT); TYPE_MAP.put("F", RuntimeType.INT); TYPE_MAP.put("S", RuntimeType.INT); TYPE_MAP.put("V", RuntimeType.INT); TYPE_MAP.put("int", RuntimeType.INT); TYPE_MAP.put("boolean", RuntimeType.INT); TYPE_MAP.put("byte", RuntimeType.INT); TYPE_MAP.put("short", RuntimeType.INT); TYPE_MAP.put("char", RuntimeType.INT); TYPE_MAP.put("float", RuntimeType.INT); TYPE_MAP.put("void", RuntimeType.INT); TYPE_MAP.put("L", RuntimeType.LONG); TYPE_MAP.put("D", RuntimeType.LONG); TYPE_MAP.put("long", RuntimeType.LONG); TYPE_MAP.put("double", RuntimeType.LONG); TYPE_MAP.put("java.lang.String", RuntimeType.STRING); TYPE_MAP.put("Ljava/lang/String;", RuntimeType.STRING); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/DebugSettings.java ================================================ package jadx.gui.device.debugger; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.StringUtils; import jadx.gui.device.protocol.ADB; import jadx.gui.device.protocol.ADBDevice; import jadx.gui.utils.NLS; public class DebugSettings { private static final Logger LOG = LoggerFactory.getLogger(DebugSettings.class); private static final int FORWARD_TCP_PORT = 33233; public static final DebugSettings INSTANCE = new DebugSettings(); private int ver; private String pid; private String name; private ADBDevice device; private int forwardTcpPort = FORWARD_TCP_PORT; private String expectPkg = ""; private boolean autoAttachPkg = false; private DebugSettings() { } public void set(ADBDevice device, int ver, String pid, String name) { this.ver = ver; this.pid = pid; this.name = name; this.device = device; this.autoAttachPkg = false; this.expectPkg = ""; } public DebugSettings setPid(String pid) { this.pid = pid; return this; } public DebugSettings setName(String name) { this.name = name; return this; } public String forwardJDWP() { int localPort = forwardTcpPort; String resultDesc = ""; try { do { ADBDevice.ForwardResult rst = device.forwardJDWP(localPort + "", pid); if (rst.state == 0) { forwardTcpPort = localPort; return ""; } if (rst.state == 1) { if (rst.desc.contains("Only one usage of each socket address")) { // port is taken by other process if (localPort < 65536) { localPort++; // retry continue; } } } resultDesc = rst.desc; break; } while (true); } catch (Exception e) { LOG.error("JDWP forward error", e); } if (StringUtils.isEmpty(resultDesc)) { resultDesc = NLS.str("adb_dialog.forward_fail"); } return resultDesc; } // we have to remove all ports that forwarding the jdwp:pid, otherwise our JDWP handshake may fail. public void clearForward() { String jdwpPid = " jdwp:" + pid; String tcpPort = " tcp:" + forwardTcpPort; try { List list = ADB.listForward(device.getDeviceInfo().getAdbHost(), device.getDeviceInfo().getAdbPort()); for (String s : list) { if (s.startsWith(device.getSerial()) && s.endsWith(jdwpPid) && !s.contains(tcpPort)) { String[] fields = s.split("\\s+"); for (String field : fields) { if (field.startsWith("tcp:")) { try { device.removeForward(field.substring("tcp:".length())); } catch (Exception e) { LOG.error("JDWP remove forward error", e); } } } } } } catch (Exception e) { LOG.error("JDWP clear forward error", e); } } public boolean isBeingDebugged() { String jdwpPid = " jdwp:" + pid; String tcpPort = " tcp:" + forwardTcpPort; try { List list = ADB.listForward(device.getDeviceInfo().getAdbHost(), device.getDeviceInfo().getAdbPort()); for (String s : list) { if (s.startsWith(device.getSerial()) && s.endsWith(jdwpPid)) { return !s.contains(tcpPort); } } } catch (Exception e) { LOG.error("ADB list forward error", e); } return false; } public int getVer() { return ver; } public String getPid() { return pid; } public String getName() { return name; } public ADBDevice getDevice() { return device; } public int getForwardTcpPort() { return forwardTcpPort; } public String getExpectPkg() { return expectPkg; } public void setExpectPkg(String expectPkg) { this.expectPkg = expectPkg; } public boolean isAutoAttachPkg() { return autoAttachPkg; } public void setAutoAttachPkg(boolean autoAttachPkg) { this.autoAttachPkg = autoAttachPkg; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/EventListenerAdapter.java ================================================ package jadx.gui.device.debugger; import io.github.skylot.jdwp.JDWP.Event.Composite.BreakpointEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.ClassPrepareEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.ClassUnloadEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.ExceptionEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.FieldAccessEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.FieldModificationEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MethodEntryEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitWithReturnValueEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnterEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnteredEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitedEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.SingleStepEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadDeathEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadStartEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.VMDeathEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.VMStartEvent; abstract class EventListenerAdapter { void onVMStart(VMStartEvent event) { } void onVMDeath(VMDeathEvent event) { } void onSingleStep(SingleStepEvent event) { } void onBreakpoint(BreakpointEvent event) { } void onMethodEntry(MethodEntryEvent event) { } void onMethodExit(MethodExitEvent event) { } void onMethodExitWithReturnValue(MethodExitWithReturnValueEvent event) { } void onMonitorContendedEnter(MonitorContendedEnterEvent event) { } void onMonitorContendedEntered(MonitorContendedEnteredEvent event) { } void onMonitorWait(MonitorWaitEvent event) { } void onMonitorWaited(MonitorWaitedEvent event) { } void onException(ExceptionEvent event) { } void onThreadStart(ThreadStartEvent event) { } void onThreadDeath(ThreadDeathEvent event) { } void onClassPrepare(ClassPrepareEvent event) { } void onClassUnload(ClassUnloadEvent event) { } void onFieldAccess(FieldAccessEvent event) { } void onFieldModification(FieldModificationEvent event) { } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/LogcatController.java ================================================ package jadx.gui.device.debugger; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.TreeSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.device.protocol.ADBDevice; import jadx.gui.ui.panel.LogcatPanel; public class LogcatController { private static final Logger LOG = LoggerFactory.getLogger(LogcatController.class); private final ADBDevice adbDevice; private final LogcatPanel logcatPanel; private Timer timer; private final String timezone; private LogcatInfo recent = null; private List events = new ArrayList<>(); private LogcatFilter filter = new LogcatFilter(); private String status = "null"; public LogcatController(LogcatPanel logcatPanel, ADBDevice adbDevice) throws IOException { this.adbDevice = adbDevice; this.logcatPanel = logcatPanel; this.timezone = adbDevice.getTimezone(); this.startLogcat(); } public void startLogcat() { timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { getLog(); } }, 0, 1000); this.status = "running"; } public void stopLogcat() { timer.cancel(); this.status = "stopped"; } public String getStatus() { return this.status; } public void clearLogcat() { try { adbDevice.clearLogcat(); clearEvents(); } catch (IOException e) { LOG.error("Failed to clear Logcat", e); } } private void getLog() { if (!logcatPanel.isReady()) { return; } try { byte[] buf; if (recent == null) { buf = adbDevice.getBinaryLogcat(); } else { buf = adbDevice.getBinaryLogcat(recent.getAfterTimestamp()); } if (buf == null) { return; } ByteBuffer in = ByteBuffer.wrap(buf); in.order(ByteOrder.LITTLE_ENDIAN); while (in.remaining() > 20) { LogcatInfo eInfo = null; byte[] msgBuf; short eLen = in.getShort(); short eHdrLen = in.getShort(); if (eLen + eHdrLen > in.remaining()) { return; } switch (eHdrLen) { case 20: // header length 20 == version 1 eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.get()); msgBuf = new byte[eLen]; in.get(msgBuf, 0, eLen - 1); eInfo.setMsg(msgBuf); break; case 24: // header length 24 == version 2 / 3 eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.get()); msgBuf = new byte[eLen]; in.get(msgBuf, 0, eLen - 1); eInfo.setMsg(msgBuf); break; case 28: // header length 28 == version 4 eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.get()); msgBuf = new byte[eLen]; in.get(msgBuf, 0, eLen - 1); eInfo.setMsg(msgBuf); break; default: break; } if (eInfo == null) { return; } if (recent == null) { recent = eInfo; } else if (recent.getInstant().isBefore(eInfo.getInstant())) { recent = eInfo; } if (filter.doFilter(eInfo)) { logcatPanel.log(eInfo); } events.add(eInfo); } } catch (Exception e) { LOG.error("Failed to get logcat message", e); } } public boolean reload() { stopLogcat(); boolean ok = logcatPanel.clearLogcatArea(); if (ok) { events.forEach((eInfo) -> { if (filter.doFilter(eInfo)) { logcatPanel.log(eInfo); } }); startLogcat(); } return true; } public void clearEvents() { this.recent = null; this.events = new ArrayList<>(); } public void exit() { stopLogcat(); filter = new LogcatFilter(); recent = null; } public LogcatFilter getFilter() { return this.filter; } public class LogcatFilter { private final Set pid; private final Set msgType; public LogcatFilter() { this(new TreeSet<>(), new TreeSet<>(List.of((byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5, (byte) 6, (byte) 7, (byte) 8))); } public LogcatFilter(Set pid, Set msgType) { this.pid = pid; this.msgType = msgType; } public void addPid(int pid) { this.pid.add(pid); } public void removePid(int pid) { this.pid.remove(pid); } public void togglePid(int pid, boolean state) { if (state) { addPid(pid); } else { removePid(pid); } } public void addMsgType(byte msgType) { this.msgType.add(msgType); } public void removeMsgType(byte msgType) { this.msgType.remove(msgType); } public void toggleMsgType(byte msgType, boolean state) { if (state) { addMsgType(msgType); } else { removeMsgType(msgType); } } public boolean doFilter(LogcatInfo inInfo) { return (pid.contains(inInfo.getPid())) && msgType.contains(inInfo.getMsgType()); } public List getFilteredList(List inInfoList) { List outInfoList = new ArrayList<>(); inInfoList.forEach((inInfo) -> { if (doFilter(inInfo)) { outInfoList.add(inInfo); } }); return outInfoList; } } public class LogcatInfo { private String msg; private final byte msgType; private final int nsec; private final int pid; private final int sec; private final int tid; private final short hdrSize; private final short len; private final short version; private int lid; private int uid; public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, byte msgType) { this.hdrSize = hdrSize; this.len = len; this.msgType = msgType; this.nsec = nsec; this.pid = pid; this.sec = sec; this.tid = tid; this.version = 1; } // Version 2 and 3 both have the same arguments public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, int lid, byte msgType) { this.hdrSize = hdrSize; this.len = len; this.lid = lid; this.msgType = msgType; this.nsec = nsec; this.pid = pid; this.sec = sec; this.tid = tid; this.version = 3; } public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, int lid, int uid, byte msgType) { this.hdrSize = hdrSize; this.len = len; this.lid = lid; this.msgType = msgType; this.nsec = nsec; this.pid = pid; this.sec = sec; this.tid = tid; this.uid = uid; this.version = 4; } public void setMsg(byte[] msg) { this.msg = new String(msg); } public short getVersion() { return this.version; } public short getLen() { return this.len; } public short getHeaderLen() { return this.hdrSize; } public int getPid() { return this.pid; } public int getTid() { return this.tid; } public int getSec() { return this.sec; } public int getNSec() { return this.nsec; } public int getLid() { return this.lid; } public int getUid() { return this.uid; } public Instant getInstant() { return Instant.ofEpochSecond(getSec(), getNSec()); } public String getTimestamp() { DateTimeFormatter dtFormat = DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.of(timezone)); return dtFormat.format(getInstant()); } public String getAfterTimestamp() { DateTimeFormatter dtFormat = DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.of(timezone)); return dtFormat.format(getInstant().plusMillis(1)); } public byte getMsgType() { return this.msgType; } public String getMsgTypeString() { switch (getMsgType()) { case 0: return "Unknown"; case 1: return "Default"; case 2: return "Verbose"; case 3: return "Debug"; case 4: return "Info"; case 5: return "Warn"; case 6: return "Error"; case 7: return "Fatal"; case 8: return "Silent"; default: return "Unknown"; } } public String getMsg() { return this.msg; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/RegisterObserver.java ================================================ package jadx.gui.device.debugger; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.core.dex.instructions.args.ArgType; import jadx.gui.device.debugger.SmaliDebugger.RuntimeVarInfo; import jadx.gui.device.debugger.smali.SmaliRegister; public class RegisterObserver { private Map> infoMap; private final List regList; private final ArtAdapter.IArtAdapter art; private final String mthFullID; private boolean hasDbgInfo = false; private RegisterObserver(ArtAdapter.IArtAdapter art, String mthFullID) { this.regList = new ArrayList<>(); this.infoMap = Collections.emptyMap(); this.art = art; this.mthFullID = mthFullID; } @NotNull public static RegisterObserver merge(List rtRegs, List smaliRegs, ArtAdapter.IArtAdapter art, String mthFullID) { RegisterObserver adapter = new RegisterObserver(art, mthFullID); adapter.hasDbgInfo = !rtRegs.isEmpty(); if (adapter.hasDbgInfo) { adapter.infoMap = new HashMap<>(); } for (SmaliRegister sr : smaliRegs) { adapter.regList.add(new SmaliRegisterMapping(sr)); } adapter.regList.sort(Comparator.comparingInt(r -> r.getSmaliRegister().getRuntimeRegNum())); for (RuntimeVarInfo rt : rtRegs) { final SmaliRegisterMapping smaliRegMapping = adapter.getRegListEntry(rt.getRegNum()); final SmaliRegister smaliReg = smaliRegMapping.getSmaliRegister(); smaliRegMapping.addRuntimeVarInfo(rt); String type = rt.getSignature(); if (type.isEmpty()) { type = rt.getType(); } ArgType at = ArgType.parse(type); if (at != null) { type = at.toString(); } Info load = new Info(smaliReg.getRegNum(), true, rt.getName(), type); Info unload = new Info(smaliReg.getRegNum(), false, null, null); adapter.infoMap.computeIfAbsent((long) rt.getStartOffset(), k -> new ArrayList<>()) .add(load); adapter.infoMap.computeIfAbsent((long) rt.getEndOffset(), k -> new ArrayList<>()) .add(unload); } return adapter; } @NotNull public List getInitializedList(long codeOffset) { List ret = Collections.emptyList(); for (SmaliRegisterMapping smaliRegisterMapping : regList) { if (smaliRegisterMapping.getSmaliRegister().isInitialized(codeOffset)) { if (ret.isEmpty()) { ret = new ArrayList<>(); } ret.add(smaliRegisterMapping.getSmaliRegister()); } } return ret; } @Nullable public RuntimeVarInfo getInfo(int runtimeNum, long codeOffset) { SmaliRegisterMapping list = getRegListEntry(runtimeNum); for (RuntimeVarInfo info : list.getRuntimeVarInfoList()) { if (info.getStartOffset() > codeOffset) { break; } if (info.isInitialized(codeOffset)) { return info; } } return null; } private SmaliRegisterMapping getRegListEntry(int regNum) { try { return regList.get(regNum); } catch (IndexOutOfBoundsException e) { throw new RuntimeException( String.format("Register %d does not exist (size: %d).\n %s\n Method: %s", regNum, regList.size(), buildDeviceInfo(), mthFullID), e); } } private String buildDeviceInfo() { DebugSettings debugSettings = DebugSettings.INSTANCE; return "Device: " + debugSettings.getDevice().getDeviceInfo() + ", Android: " + debugSettings.getVer() + ", ArtAdapter: " + art.getClass().getSimpleName(); } @NotNull public List getInfoAt(long codeOffset) { if (hasDbgInfo) { List list = infoMap.get(codeOffset); if (list != null) { return list; } } return Collections.emptyList(); } public static class SmaliRegisterMapping { private final SmaliRegister smaliRegister; private List rtList = Collections.emptyList(); public SmaliRegisterMapping(SmaliRegister smaliRegister) { this.smaliRegister = smaliRegister; } public SmaliRegister getSmaliRegister() { return smaliRegister; } @NotNull public List getRuntimeVarInfoList() { return rtList; } public void addRuntimeVarInfo(RuntimeVarInfo rt) { if (rtList.isEmpty()) { rtList = new ArrayList<>(); } rtList.add(rt); } } public static class Info { private final int smaliRegNum; private final boolean load; private final String name; private final String type; private Info(int smaliRegNum, boolean load, String name, String type) { this.smaliRegNum = smaliRegNum; this.load = load; this.name = name; this.type = type; } public int getSmaliRegNum() { return smaliRegNum; } public boolean isLoad() { return load; } public String getName() { return name; } public String getType() { return type; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/RuntimeType.java ================================================ package jadx.gui.device.debugger; import io.github.skylot.jdwp.JDWP; public enum RuntimeType { ARRAY(91, "[]"), BYTE(66, "byte"), CHAR(67, "char"), OBJECT(76, "object"), FLOAT(70, "float"), DOUBLE(68, "double"), INT(73, "int"), LONG(74, "long"), SHORT(83, "short"), VOID(86, "void"), BOOLEAN(90, "boolean"), STRING(115, "string"), THREAD(116, "thread"), THREAD_GROUP(103, "thread_group"), CLASS_LOADER(108, "class_loader"), CLASS_OBJECT(99, "class_object"); private final int jdwpTag; private final String desc; RuntimeType(int tag, String desc) { this.jdwpTag = tag; this.desc = desc; } public int getTag() { return jdwpTag; } public String getDesc() { return this.desc; } /** * Converts a JDWP.Tag to a {@link RuntimeType} * * @param tag * @return * @throws SmaliDebuggerException */ public static RuntimeType fromJdwpTag(int tag) throws SmaliDebuggerException { switch (tag) { case JDWP.Tag.ARRAY: return RuntimeType.ARRAY; case JDWP.Tag.BYTE: return RuntimeType.BYTE; case JDWP.Tag.CHAR: return RuntimeType.CHAR; case JDWP.Tag.OBJECT: return RuntimeType.OBJECT; case JDWP.Tag.FLOAT: return RuntimeType.FLOAT; case JDWP.Tag.DOUBLE: return RuntimeType.DOUBLE; case JDWP.Tag.INT: return RuntimeType.INT; case JDWP.Tag.LONG: return RuntimeType.LONG; case JDWP.Tag.SHORT: return RuntimeType.SHORT; case JDWP.Tag.VOID: return RuntimeType.VOID; case JDWP.Tag.BOOLEAN: return RuntimeType.BOOLEAN; case JDWP.Tag.STRING: return RuntimeType.STRING; case JDWP.Tag.THREAD: return RuntimeType.THREAD; case JDWP.Tag.THREAD_GROUP: return RuntimeType.THREAD_GROUP; case JDWP.Tag.CLASS_LOADER: return RuntimeType.CLASS_LOADER; case JDWP.Tag.CLASS_OBJECT: return RuntimeType.CLASS_OBJECT; default: throw new SmaliDebuggerException("Unexpected value: " + tag); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/SmaliDebugger.java ================================================ package jadx.gui.device.debugger; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.atomic.AtomicInteger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.github.skylot.jdwp.JDWP; import io.github.skylot.jdwp.JDWP.ArrayReference.Length.LengthReplyData; import io.github.skylot.jdwp.JDWP.ByteBuffer; import io.github.skylot.jdwp.JDWP.Event.Composite.BreakpointEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.ClassPrepareEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.ClassUnloadEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.EventData; import io.github.skylot.jdwp.JDWP.Event.Composite.ExceptionEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.FieldAccessEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.FieldModificationEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MethodEntryEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MethodExitWithReturnValueEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnterEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorContendedEnteredEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.MonitorWaitedEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.SingleStepEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadDeathEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.ThreadStartEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.VMDeathEvent; import io.github.skylot.jdwp.JDWP.Event.Composite.VMStartEvent; import io.github.skylot.jdwp.JDWP.EventRequest.Set.ClassMatchRequest; import io.github.skylot.jdwp.JDWP.EventRequest.Set.CountRequest; import io.github.skylot.jdwp.JDWP.EventRequest.Set.LocationOnlyRequest; import io.github.skylot.jdwp.JDWP.EventRequest.Set.StepRequest; import io.github.skylot.jdwp.JDWP.Method.VariableTableWithGeneric.VarTableWithGenericData; import io.github.skylot.jdwp.JDWP.Method.VariableTableWithGeneric.VarWithGenericSlot; import io.github.skylot.jdwp.JDWP.ObjectReference; import io.github.skylot.jdwp.JDWP.ObjectReference.ReferenceType.ReferenceTypeReplyData; import io.github.skylot.jdwp.JDWP.ObjectReference.SetValues.FieldValueSetter; import io.github.skylot.jdwp.JDWP.Packet; import io.github.skylot.jdwp.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericData; import io.github.skylot.jdwp.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericReplyData; import io.github.skylot.jdwp.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericData; import io.github.skylot.jdwp.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericReplyData; import io.github.skylot.jdwp.JDWP.ReferenceType.Signature.SignatureReplyData; import io.github.skylot.jdwp.JDWP.StackFrame.GetValues.GetValuesReplyData; import io.github.skylot.jdwp.JDWP.StackFrame.GetValues.GetValuesSlots; import io.github.skylot.jdwp.JDWP.StackFrame.SetValues.SlotValueSetter; import io.github.skylot.jdwp.JDWP.StackFrame.ThisObject.ThisObjectReplyData; import io.github.skylot.jdwp.JDWP.StringReference.Value.ValueReplyData; import io.github.skylot.jdwp.JDWP.ThreadReference.Frames.FramesReplyData; import io.github.skylot.jdwp.JDWP.ThreadReference.Frames.FramesReplyDataFrames; import io.github.skylot.jdwp.JDWP.ThreadReference.Name.NameReplyData; import io.github.skylot.jdwp.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericData; import io.github.skylot.jdwp.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericReplyData; import io.github.skylot.jdwp.JDWP.VirtualMachine.AllThreads.AllThreadsReplyData; import io.github.skylot.jdwp.JDWP.VirtualMachine.AllThreads.AllThreadsReplyDataThreads; import io.github.skylot.jdwp.JDWP.VirtualMachine.CreateString.CreateStringReplyData; import jadx.api.plugins.input.data.AccessFlags; import jadx.gui.device.debugger.smali.RegisterInfo; import jadx.gui.utils.IOUtils; import jadx.gui.utils.ObjectPool; // TODO: Finish error notification, inner errors should be logged let user notice. public class SmaliDebugger { private static final Logger LOG = LoggerFactory.getLogger(SmaliDebugger.class); private final JDWP jdwp; private final int localTcpPort; private final InputStream inputStream; private final OutputStream outputStream; // All event callbacks will be called in this queue, e.g. class prepare/unload private static final Executor EVENT_LISTENER_QUEUE = Executors.newSingleThreadExecutor(); // Handle callbacks of single step, breakpoint and watchpoint private static final Executor SUSPEND_LISTENER_QUEUE = Executors.newSingleThreadExecutor(); private final Map callbackMap = new ConcurrentHashMap<>(); private final Map eventListenerMap = new ConcurrentHashMap<>(); private final Map classMap = new ConcurrentHashMap<>(); private final Map classIDMap = new ConcurrentHashMap<>(); private final Map> clsMethodMap = new ConcurrentHashMap<>(); private final Map> clsFieldMap = new ConcurrentHashMap<>(); private Map> varMap = Collections.emptyMap(); // cls id: private final CountRequest oneOffEventReq; private final AtomicInteger idGenerator = new AtomicInteger(1); private final SuspendInfo suspendInfo = new SuspendInfo(); private final SuspendListener suspendListener; private ObjectPool> slotsPool; private ObjectPool> stepReqPool; private ObjectPool> syncQueuePool; private ObjectPool> fieldIdPool; private final Map syncQueueMap = new ConcurrentHashMap<>(); private final AtomicInteger syncQueueID = new AtomicInteger(0); private static final ICommandResult SKIP_RESULT = res -> { }; private SmaliDebugger(SuspendListener suspendListener, int localTcpPort, JDWP jdwp, InputStream inputStream, OutputStream outputStream) { this.jdwp = jdwp; this.localTcpPort = localTcpPort; this.suspendListener = suspendListener; this.inputStream = inputStream; this.outputStream = outputStream; oneOffEventReq = jdwp.eventRequest().cmdSet().newCountRequest(); oneOffEventReq.count = 1; } /** * After a successful attach the remote app will be suspended, so we have times to * set breakpoints or do any other things, after that call resume() to activate the app. */ public static SmaliDebugger attach(String host, int port, SuspendListener suspendListener) throws SmaliDebuggerException { try { byte[] bytes = JDWP.IDSizes.encode().getBytes(); JDWP.setPacketID(bytes, 1); LOG.debug("Connecting to ADB {}:{}", host, port); Socket socket = new Socket(host, port); InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); socket.setSoTimeout(5000); JDWP jdwp = initJDWP(outputStream, inputStream); socket.setSoTimeout(0); // set back to 0 so the decodingLoop won't break for timeout. SmaliDebugger debugger = new SmaliDebugger(suspendListener, port, jdwp, inputStream, outputStream); debugger.decodingLoop(); debugger.listenClassUnloadEvent(); debugger.initPools(); return debugger; } catch (IOException e) { throw new SmaliDebuggerException("Attach failed", e); } } private void onSuspended(long thread, long clazz, long mth, long offset) { suspendInfo.update() .updateThread(thread) .updateClass(clazz) .updateMethod(mth) .updateOffset(offset); if (suspendInfo.isAnythingChanged()) { SUSPEND_LISTENER_QUEUE.execute(() -> suspendListener.onSuspendEvent(suspendInfo)); } } public void stepInto() throws SmaliDebuggerException { sendStepRequest(suspendInfo.getThreadID(), JDWP.StepDepth.INTO); } public void stepOver() throws SmaliDebuggerException { sendStepRequest(suspendInfo.getThreadID(), JDWP.StepDepth.OVER); } public void stepOut() throws SmaliDebuggerException { sendStepRequest(suspendInfo.getThreadID(), JDWP.StepDepth.OUT); } public void exit() throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.virtualMachine().cmdExit().encode(-1)); tryThrowError(res); } public void detach() throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.virtualMachine().cmdDispose().encode()); tryThrowError(res); } private void initPools() { slotsPool = new ObjectPool<>(() -> { List slots = new ArrayList<>(1); GetValuesSlots slot = jdwp.stackFrame().cmdGetValues().newValuesSlots(); slot.slot = 0; slot.sigbyte = JDWP.Tag.OBJECT; slots.add(slot); return slots; }); stepReqPool = new ObjectPool<>(() -> { List eventEncoders = new ArrayList<>(2); eventEncoders.add(jdwp.eventRequest().cmdSet().newStepRequest()); eventEncoders.add(oneOffEventReq); return eventEncoders; }); syncQueuePool = new ObjectPool<>(SynchronousQueue::new); fieldIdPool = new ObjectPool<>(() -> { List ids = new ArrayList<>(1); ids.add((long) -1); return ids; }); } /** * @param regNum If it's an argument, just pass its index, non-static method should be index + 1. * e.g. void a(int b, int c), you want the value of b, then should pass 1 (0 + 1), * this is a virtual method, so 0 is for the this object and 1 is the real index of b. *

* If it's a variable then should be the reg number + number of arguments and + 1 * if it's in a non-static method. * e.g. to get the value of v3 in a virtual method with 2 arguments, should pass * 6 (3 + 2 + 1 = 6). */ public RuntimeRegister getRegisterSync(long threadID, long frameID, int regNum, RuntimeType type) throws SmaliDebuggerException { List slots = slotsPool.get(); GetValuesSlots slot = slots.get(0); slot.slot = regNum; slot.sigbyte = (byte) type.getTag(); Packet res = sendCommandSync(jdwp.stackFrame().cmdGetValues().encode(threadID, frameID, slots)); tryThrowError(res); slotsPool.put(slots); GetValuesReplyData val = jdwp.stackFrame().cmdGetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); return buildRegister(regNum, val.values.get(0).slotValue.tag, val.values.get(0).slotValue.idOrValue); } public long getThisID(long threadID, long frameID) throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.stackFrame().cmdThisObject().encode(threadID, frameID)); tryThrowError(res); ThisObjectReplyData data = jdwp.stackFrame().cmdThisObject().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); return data.objectThis.objectID; } public List getAllFieldsSync(long clsID) throws SmaliDebuggerException { return getAllFields(clsID); } public void getFieldValueSync(long clsID, RuntimeField fld) throws SmaliDebuggerException { List list = new ArrayList<>(1); list.add(fld); getAllFieldValuesSync(clsID, list); } public void getAllFieldValuesSync(long thisID, List flds) throws SmaliDebuggerException { List ids = new ArrayList<>(flds.size()); flds.forEach(f -> ids.add(f.getFieldID())); Packet res = sendCommandSync(jdwp.objectReference().cmdGetValues().encode(thisID, ids)); tryThrowError(res); ObjectReference.GetValues.GetValuesReplyData data = jdwp.objectReference().cmdGetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); List values = data.values; for (int i = 0; i < values.size(); i++) { ObjectReference.GetValues.GetValuesReplyDataValues value = values.get(i); flds.get(i).setValue(value.value.idOrValue) .setType(RuntimeType.fromJdwpTag(value.value.tag)); } } public Frame getCurrentFrame(long threadID) throws SmaliDebuggerException { return getCurrentFrameInternal(threadID); } public List getFramesSync(long threadID) throws SmaliDebuggerException { return getAllFrames(threadID); } public List getAllThreadsSync() throws SmaliDebuggerException { return getAllThreads(); } @Nullable public String getThreadNameSync(long threadID) throws SmaliDebuggerException { return sendThreadNameReq(threadID); } @Nullable public String getClassSignatureSync(long classID) throws SmaliDebuggerException { return getClassSignatureInternal(classID); } @Nullable public String getMethodSignatureSync(long classID, long methodID) throws SmaliDebuggerException { return getMethodSignatureInternal(classID, methodID); } public boolean errIsTypeMismatched(int errCode) { return errCode == JDWP.Error.TYPE_MISMATCH; } public boolean errIsInvalidSlot(int errCode) { return errCode == JDWP.Error.INVALID_SLOT; } public boolean errIsInvalidObject(int errCode) { return errCode == JDWP.Error.INVALID_OBJECT; } private static class ClassListenerInfo { int prepareReqID; int unloadReqID; ClassListener listener; void reset(ClassListener l) { this.listener = l; this.prepareReqID = -1; this.unloadReqID = -1; } } private ClassListenerInfo clsListener; /** * Listens for class preparation and unload events. */ public void setClassListener(ClassListener listener) throws SmaliDebuggerException { if (clsListener != null) { if (listener != clsListener.listener) { unregisterEventSync(JDWP.EventKind.CLASS_PREPARE, clsListener.prepareReqID); unregisterEventSync(JDWP.EventKind.CLASS_UNLOAD, clsListener.unloadReqID); } } else { clsListener = new ClassListenerInfo(); } clsListener.reset(listener); regClassPrepareEvent(clsListener); regClassUnloadEvent(clsListener); } private void regClassUnloadEvent(ClassListenerInfo info) throws SmaliDebuggerException { Packet res = sendCommandSync( jdwp.eventRequest().cmdSet().newClassExcludeRequest((byte) JDWP.EventKind.CLASS_UNLOAD, (byte) JDWP.SuspendPolicy.NONE, "java.*")); tryThrowError(res); info.unloadReqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); eventListenerMap.put(info.unloadReqID, new EventListenerAdapter() { @Override void onClassUnload(ClassUnloadEvent event) { info.listener.onUnloaded(DbgUtils.classSigToRawFullName(event.signature)); } }); } private void regClassPrepareEvent(ClassListenerInfo info) throws SmaliDebuggerException { Packet res = sendCommandSync( jdwp.eventRequest().cmdSet().newClassExcludeRequest((byte) JDWP.EventKind.CLASS_PREPARE, (byte) JDWP.SuspendPolicy.NONE, "java.*")); tryThrowError(res); info.prepareReqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); eventListenerMap.put(info.prepareReqID, new EventListenerAdapter() { @Override void onClassPrepare(ClassPrepareEvent event) { info.listener.onPrepared(DbgUtils.classSigToRawFullName(event.signature), event.typeID); } }); } public void regClassPrepareEventForBreakpoint(String clsSig, ClassPrepareListener l) throws SmaliDebuggerException { Packet res = sendCommandSync(buildClassMatchReqForBreakpoint(clsSig, JDWP.EventKind.CLASS_PREPARE)); tryThrowError(res); int reqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); eventListenerMap.put(reqID, new EventListenerAdapter() { @Override void onClassPrepare(ClassPrepareEvent event) { EVENT_LISTENER_QUEUE.execute(() -> { try { l.onPrepared(event.typeID); } finally { eventListenerMap.remove(reqID); try { resume(); } catch (SmaliDebuggerException e) { LOG.error("Resume failed", e); } } }); } }); } public interface MethodEntryListener { /** * return true to remove */ boolean entry(String mthSig); } public void regMethodEntryEventSync(String clsSig, MethodEntryListener l) throws SmaliDebuggerException { Packet res = sendCommandSync( jdwp.eventRequest().cmdSet().newClassMatchRequest((byte) JDWP.EventKind.METHOD_ENTRY, (byte) JDWP.SuspendPolicy.ALL, clsSig)); tryThrowError(res); int reqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); eventListenerMap.put(reqID, new EventListenerAdapter() { @Override void onMethodEntry(MethodEntryEvent event) { EVENT_LISTENER_QUEUE.execute(() -> { boolean removeListener = false; try { String sig = getMethodSignatureInternal(event.location.classID, event.location.methodID); removeListener = l.entry(sig); if (removeListener) { sendCommand(jdwp.eventRequest().cmdClear().encode((byte) JDWP.EventKind.METHOD_ENTRY, reqID), SKIP_RESULT); onSuspended(event.thread, event.location.classID, event.location.methodID, -1); eventListenerMap.remove(reqID); } } catch (SmaliDebuggerException e) { LOG.error("Method entry failed", e); } finally { if (!removeListener) { try { resume(); } catch (SmaliDebuggerException e) { LOG.error("Resume failed", e); } } } }); } }); } private void unregisterEventSync(int eventKind, int reqID) throws SmaliDebuggerException { eventListenerMap.remove(reqID); Packet rst = sendCommandSync(jdwp.eventRequest().cmdClear().encode((byte) eventKind, reqID)); tryThrowError(rst); } public String readObjectSignatureSync(RuntimeValue val) throws SmaliDebuggerException { long objID = readID(val); // get type reference by object id. Packet res = sendCommandSync(jdwp.objectReference().cmdReferenceType().encode(objID)); tryThrowError(res); ReferenceTypeReplyData data = jdwp.objectReference().cmdReferenceType().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); // get signature by type reference id. res = sendCommandSync(jdwp.referenceType().cmdSignature().encode(data.typeID)); tryThrowError(res); SignatureReplyData sigData = jdwp.referenceType().cmdSignature().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); return sigData.signature; } public String readStringSync(RuntimeValue val) throws SmaliDebuggerException { return readStringSync(readID(val)); } public String readStringSync(long id) throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.stringReference().cmdValue().encode(id)); tryThrowError(res); ValueReplyData strData = jdwp.stringReference().cmdValue().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); return strData.stringValue; } public boolean setValueSync(int runtimeRegNum, RuntimeType type, Object val, long threadID, long frameID) throws SmaliDebuggerException { if (type == RuntimeType.STRING) { long newID = createString((String) val); if (newID == -1) { return false; } val = newID; type = RuntimeType.OBJECT; } List setters = buildRegValueSetter(type.getTag(), runtimeRegNum); JDWP.encodeAny(setters.get(0).slotValue.idOrValue, val); Packet res = sendCommandSync(jdwp.stackFrame().cmdSetValues().encode(threadID, frameID, setters)); tryThrowError(res); return jdwp.stackFrame().cmdSetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); } public boolean setValueSync(long objID, long fldID, RuntimeType type, Object val) throws SmaliDebuggerException { if (type == RuntimeType.STRING) { long newID = createString((String) val); if (newID == -1) { return false; } val = newID; } List setters = buildFieldValueSetter(); FieldValueSetter setter = setters.get(0); setter.fieldID = fldID; JDWP.encodeAny(setter.value.idOrValue, val); Packet res = sendCommandSync(jdwp.objectReference().cmdSetValues().encode(objID, setters)); tryThrowError(res); return jdwp.objectReference().cmdSetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); } public void getValueSync(long objID, RuntimeField fld) throws SmaliDebuggerException { List ids = fieldIdPool.get(); ids.set(0, fld.getFieldID()); Packet res = sendCommandSync(jdwp.objectReference().cmdGetValues().encode(objID, ids)); tryThrowError(res); ObjectReference.GetValues.GetValuesReplyData data = jdwp.objectReference().cmdGetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); fld.setValue(data.values.get(0).value.idOrValue) .setType(RuntimeType.fromJdwpTag(data.values.get(0).value.tag)); } private long createString(String localStr) throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.virtualMachine().cmdCreateString().encode(localStr)); tryThrowError(res); CreateStringReplyData id = jdwp.virtualMachine().cmdCreateString().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); return id.stringObject; } public long readID(RuntimeValue val) { return JDWP.decodeBySize(val.getRawVal().getBytes(), 0, val.getRawVal().size()); } public String readArraySignature(RuntimeValue val) throws SmaliDebuggerException { return readObjectSignatureSync(val); } public int readArrayLength(RuntimeValue val) throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.arrayReference().cmdLength().encode(readID(val))); tryThrowError(res); LengthReplyData data = jdwp.arrayReference().cmdLength().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); return data.arrayLength; } /** * @param startIndex less than 0 means 0 * @param len less than or equals 0 means the maximum value 99 or the rest of the elements. * @return An entry, The key is the total length of this array when len is <= 0, otherwise 0, * the value, if this array is an object array then it's object ids. */ public Entry> readArray(RuntimeValue reg, int startIndex, int len) throws SmaliDebuggerException { long id = readID(reg); Entry> ret; if (len <= 0) { Packet res = sendCommandSync(jdwp.arrayReference().cmdLength().encode(id)); tryThrowError(res); LengthReplyData data = jdwp.arrayReference().cmdLength().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); len = Math.min(99, data.arrayLength); ret = new SimpleEntry<>(data.arrayLength, null); } else { ret = new SimpleEntry<>(0, null); } startIndex = Math.max(0, startIndex); Packet res = sendCommandSync(jdwp.arrayReference().cmdGetValues().encode(id, startIndex, len)); tryThrowError(res); JDWP.ArrayReference.GetValues.GetValuesReplyData valData = jdwp.arrayReference().cmdGetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); ret.setValue(valData.values.idOrValues); return ret; } public byte readByte(RuntimeValue val) { return JDWP.decodeByte(val.getRawVal().getBytes(), 0); } public char readChar(RuntimeValue val) { return JDWP.decodeChar(val.getRawVal().getBytes(), 0); } public short readShort(RuntimeValue val) { return JDWP.decodeShort(val.getRawVal().getBytes(), 0); } public int readInt(RuntimeValue val) { return JDWP.decodeInt(val.getRawVal().getBytes(), 0); } public float readFloat(RuntimeValue val) { return JDWP.decodeFloat(val.getRawVal().getBytes(), 0); } /** * @param val has 8 bytes mostly */ public long readAll(RuntimeValue val) { return JDWP.decodeBySize(val.getRawVal().getBytes(), 0, Math.min(val.getRawVal().size(), 8)); } public double readDouble(RuntimeValue val) { return JDWP.decodeDouble(val.getRawVal().getBytes(), 0); } @Nullable public RuntimeDebugInfo getRuntimeDebugInfo(long clsID, long mthID) throws SmaliDebuggerException { Map secMap = varMap.get(clsID); RuntimeDebugInfo info = null; if (secMap != null) { info = secMap.get(mthID); } if (info == null) { info = initDebugInfo(clsID, mthID); } return info; } private RuntimeDebugInfo initDebugInfo(long clsID, long mthID) throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.method().cmdVariableTableWithGeneric.encode(clsID, mthID)); tryThrowError(res); VarTableWithGenericData data = jdwp.method().cmdVariableTableWithGeneric.decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); if (varMap == Collections.EMPTY_MAP) { varMap = new ConcurrentHashMap<>(); } RuntimeDebugInfo info = new RuntimeDebugInfo(data); varMap.computeIfAbsent(clsID, k -> new HashMap<>()).put(mthID, info); return info; } private static JDWP initJDWP(OutputStream outputStream, InputStream inputStream) throws SmaliDebuggerException { try { handShake(outputStream, inputStream); outputStream.write(JDWP.Suspend.encode().setPacketID(1).getBytes()); // suspend all threads Packet res = readPacket(inputStream); tryThrowError(res); if (res.isReplyPacket() && res.getID() == 1) { // get id sizes for decoding & encoding of jdwp packets. outputStream.write(JDWP.IDSizes.encode().setPacketID(1).getBytes()); res = readPacket(inputStream); tryThrowError(res); if (res.isReplyPacket() && res.getID() == 1) { JDWP.IDSizes.IDSizesReplyData sizes = JDWP.IDSizes.decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); return new JDWP(sizes); } } } catch (IOException e) { throw new SmaliDebuggerException(e); } throw new SmaliDebuggerException("Failed to init JDWP."); } private static void handShake(OutputStream outputStream, InputStream inputStream) throws SmaliDebuggerException { byte[] buf; try { outputStream.write(JDWP.encodeHandShakePacket()); buf = IOUtils.readNBytes(inputStream, 14); } catch (Exception e) { throw new SmaliDebuggerException("jdwp handshake failed", e); } if (buf == null || !JDWP.decodeHandShakePacket(buf)) { throw new SmaliDebuggerException("jdwp handshake bad reply"); } } private MethodsWithGenericData getMethodBySig(long classID, String sig) { List methods = clsMethodMap.get(classID); if (methods != null) { for (MethodsWithGenericData method : methods) { if (sig.startsWith(method.name + "(") && sig.endsWith(method.signature)) { return method; } } } return null; } private int genID() { return idGenerator.getAndAdd(1); } /** * Read & decode packets from Socket connection */ private void decodingLoop() { Executors.newSingleThreadExecutor().execute(() -> { boolean errFromCallback; while (true) { errFromCallback = false; try { Packet res = readPacket(inputStream); if (res == null) { break; } suspendInfo.nextRound(); ICommandResult callback = callbackMap.remove(res.getID()); if (callback != null) { if (callback != SKIP_RESULT) { errFromCallback = true; callback.onCommandReply(res); } continue; } if (res.getCommandSetID() == 64 && res.getCommandID() == 100) { // command from JVM errFromCallback = true; decodeCompositeEvents(res); } else { printUnexpectedID(res.getID()); } } catch (SmaliDebuggerException e) { LOG.error("Error in debugger decoding loop", e); if (!errFromCallback) { // fatal error break; } } } suspendInfo.setTerminated(); clearWaitingSyncQueue(); suspendListener.onSuspendEvent(suspendInfo); }); } private void sendCommand(ByteBuffer buf, ICommandResult callback) throws SmaliDebuggerException { int id = genID(); callbackMap.put(id, callback); try { outputStream.write(buf.setPacketID(id).getBytes()); } catch (IOException e) { throw new SmaliDebuggerException(e); } } /** * Do not use this method inside a ICommandResult callback, it will cause deadlock. * It should be used in a thread. */ private Packet sendCommandSync(ByteBuffer buf) throws SmaliDebuggerException { SynchronousQueue store = syncQueuePool.get(); sendCommand(buf, res -> { try { store.put(res); } catch (Exception e) { LOG.error("Command send failed", e); } }); Integer id = syncQueueID.getAndAdd(1); try { syncQueueMap.put(id, Thread.currentThread()); return store.take(); } catch (InterruptedException e) { throw new SmaliDebuggerException(e); } finally { syncQueueMap.remove(id); syncQueuePool.put(store); } } // called by decodingLoop() when fatal error occurred, // if don't do so the store.take() may block forever. private void clearWaitingSyncQueue() { syncQueueMap.keySet().forEach(k -> { Thread t = syncQueueMap.remove(k); if (t != null) { t.interrupt(); } }); } private void printUnexpectedID(int id) throws SmaliDebuggerException { throw new SmaliDebuggerException("Missing handler for this id: " + id); } private void decodeCompositeEvents(Packet res) throws SmaliDebuggerException { EventData data = jdwp.event().cmdComposite().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); for (JDWP.EventRequestDecoder event : data.events) { EventListenerAdapter listener = eventListenerMap.get(event.getRequestID()); if (listener == null) { LOG.error("Missing handler for id: {}", event.getRequestID()); continue; } if (event instanceof VMStartEvent) { listener.onVMStart((VMStartEvent) event); return; } if (event instanceof VMDeathEvent) { listener.onVMDeath((VMDeathEvent) event); return; } if (event instanceof SingleStepEvent) { listener.onSingleStep((SingleStepEvent) event); return; } if (event instanceof BreakpointEvent) { listener.onBreakpoint((BreakpointEvent) event); return; } if (event instanceof MethodEntryEvent) { listener.onMethodEntry((MethodEntryEvent) event); return; } if (event instanceof MethodExitEvent) { listener.onMethodExit((MethodExitEvent) event); return; } if (event instanceof MethodExitWithReturnValueEvent) { listener.onMethodExitWithReturnValue((MethodExitWithReturnValueEvent) event); return; } if (event instanceof MonitorContendedEnterEvent) { listener.onMonitorContendedEnter((MonitorContendedEnterEvent) event); return; } if (event instanceof MonitorContendedEnteredEvent) { listener.onMonitorContendedEntered((MonitorContendedEnteredEvent) event); return; } if (event instanceof MonitorWaitEvent) { listener.onMonitorWait((MonitorWaitEvent) event); return; } if (event instanceof MonitorWaitedEvent) { listener.onMonitorWaited((MonitorWaitedEvent) event); return; } if (event instanceof ExceptionEvent) { listener.onException((ExceptionEvent) event); return; } if (event instanceof ThreadStartEvent) { listener.onThreadStart((ThreadStartEvent) event); return; } if (event instanceof ThreadDeathEvent) { listener.onThreadDeath((ThreadDeathEvent) event); return; } if (event instanceof ClassPrepareEvent) { listener.onClassPrepare((ClassPrepareEvent) event); return; } if (event instanceof ClassUnloadEvent) { listener.onClassUnload((ClassUnloadEvent) event); return; } if (event instanceof FieldAccessEvent) { listener.onFieldAccess((FieldAccessEvent) event); return; } if (event instanceof FieldModificationEvent) { listener.onFieldModification((FieldModificationEvent) event); return; } throw new SmaliDebuggerException("Unexpected event: " + event); } } private final EventListenerAdapter stepListener = new EventListenerAdapter() { @Override void onSingleStep(SingleStepEvent event) { onSuspended(event.thread, event.location.classID, event.location.methodID, event.location.index); } }; private void sendStepRequest(long threadID, int depth) throws SmaliDebuggerException { List stepReq = buildStepRequest(threadID, JDWP.StepSize.MIN, depth); ByteBuffer stepEncodedBuf = jdwp.eventRequest().cmdSet().encode( (byte) JDWP.EventKind.SINGLE_STEP, (byte) JDWP.SuspendPolicy.ALL, stepReq); stepReqPool.put(stepReq); sendCommand(stepEncodedBuf, res -> { tryThrowError(res); int reqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); eventListenerMap.put(reqID, stepListener); }); resume(); } public void resume() throws SmaliDebuggerException { sendCommand(JDWP.Resume.encode(), SKIP_RESULT); } public void suspend() throws SmaliDebuggerException { sendCommand(JDWP.Suspend.encode(), SKIP_RESULT); } public void setBreakpoint(RuntimeBreakpoint bp) throws SmaliDebuggerException { sendCommand(buildBreakpointRequest(bp), res -> { tryThrowError(res); bp.reqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); eventListenerMap.put(bp.reqID, new EventListenerAdapter() { @Override void onBreakpoint(BreakpointEvent event) { onSuspended(event.thread, event.location.classID, event.location.methodID, event.location.index); } }); }); } public long getClassID(String clsSig, boolean fetch) throws SmaliDebuggerException { do { AllClassesWithGenericData data = classMap.get(clsSig); if (data == null) { if (fetch) { getAllClasses(); fetch = false; continue; } break; } else { return data.typeID; } } while (true); return -1; } public long getMethodID(long cid, String mthSig) throws SmaliDebuggerException { initClassCache(cid); MethodsWithGenericData data = getMethodBySig(cid, mthSig); if (data != null) { return data.methodID; } return -1; } public void initClassCache(long clsID) throws SmaliDebuggerException { initFields(clsID); initMethods(clsID); } public void removeBreakpoint(RuntimeBreakpoint bp) throws SmaliDebuggerException { sendCommand(jdwp.eventRequest().cmdClear().encode((byte) JDWP.EventKind.BREAKPOINT, bp.reqID), SKIP_RESULT); } private ByteBuffer buildBreakpointRequest(RuntimeBreakpoint bp) { LocationOnlyRequest req = jdwp.eventRequest().cmdSet().newLocationOnlyRequest(); req.loc.classID = bp.clsID; req.loc.methodID = bp.mthID; req.loc.index = bp.offset; req.loc.tag = JDWP.TypeTag.CLASS; List list = new ArrayList<>(1); list.add(req); return jdwp.eventRequest().cmdSet().encode((byte) JDWP.EventKind.BREAKPOINT, (byte) JDWP.SuspendPolicy.ALL, list); } /** * Builds a one-off class prepare event for setting up breakpoints. */ private ByteBuffer buildClassMatchReqForBreakpoint(String cls, int eventKind) { List encoders = new ArrayList<>(2); ClassMatchRequest match = jdwp.eventRequest().cmdSet().newClassMatchRequest(); encoders.add(match); encoders.add(oneOffEventReq); match.classPattern = cls; return jdwp.eventRequest().cmdSet().encode((byte) eventKind, (byte) JDWP.SuspendPolicy.ALL, encoders); } private List buildStepRequest(long threadID, int stepSize, int stepDepth) { List eventEncoders = stepReqPool.get(); StepRequest req = (StepRequest) eventEncoders.get(0); req.size = stepSize; req.depth = stepDepth; req.thread = threadID; return eventEncoders; } private List buildFieldValueSetter() { FieldValueSetter setter = jdwp.objectReference().cmdSetValues().new FieldValueSetter(); setter.value = jdwp.new UntaggedValuePacket(); setter.value.idOrValue = new ByteBuffer(); List setters = new ArrayList<>(1); setters.add(setter); return setters; } private List buildRegValueSetter(int tag, int regNum) { List setters = new ArrayList<>(1); SlotValueSetter setter = jdwp.stackFrame().cmdSetValues().new SlotValueSetter(); setters.add(setter); setter.slot = regNum; setter.slotValue = jdwp.new ValuePacket(); setter.slotValue.tag = tag; setter.slotValue.idOrValue = new ByteBuffer(); return setters; } private String getClassSignatureInternal(long id) throws SmaliDebuggerException { AllClassesWithGenericData data = classIDMap.get(id); if (data == null) { getAllClasses(); } data = classIDMap.get(id); if (data != null) { return data.signature; } return null; } private String getMethodSignatureInternal(long clsID, long mthID) throws SmaliDebuggerException { List mthData = clsMethodMap.get(clsID); if (mthData == null) { Packet res = sendCommandSync(jdwp.referenceType().cmdMethodsWithGeneric().encode(clsID)); tryThrowError(res); MethodsWithGenericReplyData data = jdwp.referenceType().cmdMethodsWithGeneric().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); mthData = data.declared; clsMethodMap.put(clsID, mthData); } if (mthData != null) { for (MethodsWithGenericData data : mthData) { if (data.methodID == mthID) { return data.name + data.signature; } } } return null; } private String sendThreadNameReq(long id) throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.threadReference().cmdName().encode(id)); tryThrowError(res); NameReplyData nameData = jdwp.threadReference().cmdName().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); return nameData.threadName; } private List getAllFields(long clsID) throws SmaliDebuggerException { initFields(clsID); List flds = clsFieldMap.get(clsID); if (flds != null && flds.size() > 0) { List rfs = new ArrayList<>(flds.size()); for (FieldsWithGenericData fld : flds) { String type = fld.signature; if (fld.genericSignature != null && !fld.genericSignature.trim().isEmpty()) { type += "<" + fld.genericSignature + ">"; } rfs.add(new RuntimeField(fld.name, type, fld.fieldID, fld.modBits)); } return rfs; } return Collections.emptyList(); } public Frame getCurrentFrameInternal(long threadID) throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.threadReference().cmdFrames().encode(threadID, 0, 1)); tryThrowError(res); FramesReplyData frameData = jdwp.threadReference().cmdFrames().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); FramesReplyDataFrames frame = frameData.frames.get(0); return new Frame(frame.frameID, frame.location.classID, frame.location.methodID, frame.location.index); } private List getAllFrames(long threadID) throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.threadReference().cmdFrames().encode(threadID, 0, -1)); tryThrowError(res); FramesReplyData frameData = jdwp.threadReference().cmdFrames().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); List frames = new ArrayList<>(); for (FramesReplyDataFrames frame : frameData.frames) { frames.add(new Frame(frame.frameID, frame.location.classID, frame.location.methodID, frame.location.index)); } return frames; } private List getAllThreads() throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.virtualMachine().cmdAllThreads().encode()); tryThrowError(res); AllThreadsReplyData data; data = jdwp.virtualMachine().cmdAllThreads().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); List threads = new ArrayList<>(data.threads.size()); for (AllThreadsReplyDataThreads thread : data.threads) { threads.add(thread.thread); } return threads; } private void getAllClasses() throws SmaliDebuggerException { Packet res = sendCommandSync(jdwp.virtualMachine().cmdAllClassesWithGeneric().encode()); tryThrowError(res); AllClassesWithGenericReplyData classData = jdwp.virtualMachine().cmdAllClassesWithGeneric().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); for (AllClassesWithGenericData aClass : classData.classes) { classMap.put(DbgUtils.classSigToRawFullName(aClass.signature), aClass); classIDMap.put(aClass.typeID, aClass); } } private void initFields(long clsID) throws SmaliDebuggerException { if (clsFieldMap.get(clsID) == null) { Packet res = sendCommandSync(jdwp.referenceType().cmdFieldsWithGeneric().encode(clsID)); tryThrowError(res); FieldsWithGenericReplyData data = jdwp.referenceType().cmdFieldsWithGeneric().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); clsFieldMap.put(clsID, data.declared); } } private void initMethods(long clsID) throws SmaliDebuggerException { if (clsMethodMap.get(clsID) == null) { Packet res = sendCommandSync(jdwp.referenceType().cmdMethodsWithGeneric().encode(clsID)); tryThrowError(res); MethodsWithGenericReplyData data = jdwp.referenceType().cmdMethodsWithGeneric().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); clsMethodMap.put(clsID, data.declared); } } /** * Removes class cache when it's unloaded from JVM. */ private void listenClassUnloadEvent() throws SmaliDebuggerException { sendCommand( jdwp.eventRequest().cmdSet().encode((byte) JDWP.EventKind.CLASS_UNLOAD, (byte) JDWP.SuspendPolicy.NONE, Collections.emptyList()), res -> { int reqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); eventListenerMap.put(reqID, new EventListenerAdapter() { @Override void onClassUnload(ClassUnloadEvent event) { EVENT_LISTENER_QUEUE.execute(() -> { System.out.printf("ClassUnloaded: %s%n", event.signature); AllClassesWithGenericData clsData = classMap.remove(event.signature); if (clsData != null) { classIDMap.remove(clsData.typeID); clsFieldMap.remove(clsData.typeID); clsMethodMap.remove(clsData.typeID); varMap.remove(clsData.typeID); } }); } }); }); } /** * Reads a JDWP packet. */ @Nullable private static Packet readPacket(InputStream inputStream) throws SmaliDebuggerException { try { byte[] header = IOUtils.readNBytes(inputStream, JDWP.PACKET_HEADER_SIZE); if (header == null) { // stream ended return null; } int bodyLength = JDWP.getPacketLength(header, 0) - JDWP.PACKET_HEADER_SIZE; if (bodyLength <= 0) { return Packet.make(header); } byte[] body = IOUtils.readNBytes(inputStream, bodyLength); if (body == null) { throw new SmaliDebuggerException("Stream truncated"); } return Packet.make(concatBytes(header, body)); } catch (IOException e) { throw new SmaliDebuggerException("Read packer error", e); } } private static byte[] concatBytes(byte[] buf1, byte[] buf2) { byte[] tempBuf = new byte[buf1.length + buf2.length]; System.arraycopy(buf1, 0, tempBuf, 0, buf1.length); System.arraycopy(buf2, 0, tempBuf, buf1.length, buf2.length); return tempBuf; } private static void tryThrowError(@Nullable Packet res) throws SmaliDebuggerException { if (res == null) { throw new SmaliDebuggerException("Stream ended"); } if (res.isError()) { throw new SmaliDebuggerException("(JDWP Error Code:" + res.getErrorCode() + ") " + res.getErrorText(), res.getErrorCode()); } } private interface ICommandResult { void onCommandReply(Packet res) throws SmaliDebuggerException; } public static class RuntimeField extends RuntimeValue { private final String name; private final String fldType; private final long fieldID; private final int modBits; private RuntimeField(String name, String type, long fieldID, int modBits) { super(null, null); this.name = name; this.fldType = type; this.fieldID = fieldID; this.modBits = modBits; } public String getFieldType() { return fldType; } public String getName() { return name; } public long getFieldID() { return fieldID; } private RuntimeField setValue(ByteBuffer rawVal) { super.rawVal = rawVal; return this; } public boolean isBelongToThis() { return !AccessFlags.hasFlag(modBits, AccessFlags.STATIC) && !AccessFlags.hasFlag(modBits, AccessFlags.SYNTHETIC); } } public static class RuntimeBreakpoint { private long clsID; private long mthID; private long offset; private int reqID; public long getCodeOffset() { return offset; } } public RuntimeBreakpoint makeBreakpoint(long cid, long mid, long offset) { RuntimeBreakpoint bp = new RuntimeBreakpoint(); bp.clsID = cid; bp.mthID = mid; bp.offset = offset; return bp; } private RuntimeRegister buildRegister(int num, int tag, ByteBuffer buf) throws SmaliDebuggerException { return new RuntimeRegister(num, RuntimeType.fromJdwpTag(tag), buf); } public static class RuntimeValue { protected ByteBuffer rawVal; protected RuntimeType type; RuntimeValue(RuntimeType type, ByteBuffer rawVal) { this.rawVal = rawVal; this.type = type; } public RuntimeType getType() { return type; } public void setType(RuntimeType type) { this.type = type; } private ByteBuffer getRawVal() { return rawVal; } } public static class RuntimeRegister extends RuntimeValue { private final int num; private RuntimeRegister(int num, RuntimeType type, ByteBuffer rawVal) { super(type, rawVal); this.num = num; } public int getRegNum() { return num; } } public static class RuntimeVarInfo extends RegisterInfo { private final VarWithGenericSlot slot; private RuntimeVarInfo(VarWithGenericSlot slot) { this.slot = slot; } @Override public String getName() { return slot.name; } @Override public int getRegNum() { return slot.slot; } @Override public String getType() { String gen = getSignature(); if (gen == null || gen.isEmpty()) { return this.slot.signature; } return gen; } @NotNull @Override public String getSignature() { return this.slot.genericSignature.trim(); } @Override public int getStartOffset() { return (int) slot.codeIndex; } @Override public int getEndOffset() { return (int) (slot.codeIndex + slot.length); } @Override public boolean isMarkedAsParameter() { return false; } } public static class RuntimeDebugInfo { private final List infoList; private RuntimeDebugInfo(VarTableWithGenericData data) { infoList = new ArrayList<>(data.slots.size()); for (VarWithGenericSlot slot : data.slots) { infoList.add(new RuntimeVarInfo(slot)); } } public List getInfoList() { return infoList; } } public static class Frame { private final long id; private final long clsID; private final long mthID; private final long index; private Frame(long id, long clsID, long mthID, long index) { this.id = id; this.clsID = clsID; this.mthID = mthID; this.index = index; } public long getID() { return id; } public long getClassID() { return clsID; } public long getMethodID() { return mthID; } public long getCodeIndex() { return index; } } public interface ClassPrepareListener { void onPrepared(long id); } public interface ClassListener { void onPrepared(String cls, long id); void onUnloaded(String cls); } /** * Listener for breakpoint, watch, step, etc. */ public interface SuspendListener { /** * For step, breakpoint, watchpoint, and any other events that suspend the JVM. * This method will be called in stateListenQueue. */ void onSuspendEvent(SuspendInfo current); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/SmaliDebuggerException.java ================================================ package jadx.gui.device.debugger; public class SmaliDebuggerException extends Exception { private final int errCode; private static final long serialVersionUID = -1111111202102191403L; public SmaliDebuggerException(Exception e) { super(e); errCode = -1; } public SmaliDebuggerException(String msg) { super(msg); this.errCode = -1; } public SmaliDebuggerException(String msg, Exception e) { super(msg, e); errCode = -1; } public SmaliDebuggerException(String msg, int errCode) { super(msg); this.errCode = errCode; } public int getErrCode() { return errCode; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/SuspendInfo.java ================================================ package jadx.gui.device.debugger; public class SuspendInfo { private boolean terminated; private boolean newRound; private final InfoSetter updater = new InfoSetter(); public long getThreadID() { return updater.thread; } public long getClassID() { return updater.clazz; } public long getMethodID() { return updater.method; } public long getOffset() { return updater.offset; } InfoSetter update() { updater.changed = false; updater.nextRound(newRound); this.newRound = false; return updater; } // called by decodingLoop, to tell the updater even though the values are the same, // they are decoded from another packet, they should be treated as new. void nextRound() { newRound = true; } // according to JDWP document it's legal to fire two or more events on a same location, // e.g. one for single step and the other for breakpoint, so when this happened we only // want one of them. boolean isAnythingChanged() { return updater.changed; } public boolean isTerminated() { return terminated; } void setTerminated() { terminated = true; } static class InfoSetter { private long thread; private long clazz; private long method; private long offset; // code offset; private boolean changed; void nextRound(boolean newRound) { if (!changed) { changed = newRound; } } InfoSetter updateThread(long thread) { if (!changed) { changed = this.thread != thread; } this.thread = thread; return this; } InfoSetter updateClass(long clazz) { if (!changed) { changed = this.clazz != clazz; } this.clazz = clazz; return this; } InfoSetter updateMethod(long method) { if (!changed) { changed = this.method != method; } this.method = method; return this; } InfoSetter updateOffset(long offset) { if (!changed) { changed = this.offset != offset; } this.offset = offset; return this; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/smali/RegisterInfo.java ================================================ package jadx.gui.device.debugger.smali; import jadx.api.plugins.input.data.ILocalVar; public abstract class RegisterInfo implements ILocalVar { public boolean isInitialized(long codeOffset) { return codeOffset >= getStartOffset() && codeOffset < getEndOffset(); } public boolean isUnInitialized(long codeOffset) { return codeOffset < getStartOffset() || codeOffset >= getEndOffset(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/smali/Smali.java ================================================ package jadx.gui.device.debugger.smali; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.JadxArgs; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.AccessFlagsScope; import jadx.api.plugins.input.data.ICatch; import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IDebugInfo; import jadx.api.plugins.input.data.IFieldData; import jadx.api.plugins.input.data.ILocalVar; import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.ITry; import jadx.api.plugins.input.data.annotations.AnnotationVisibility; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.api.plugins.input.insns.InsnData; import jadx.api.plugins.input.insns.InsnIndexType; import jadx.api.plugins.input.insns.Opcode; import jadx.api.plugins.input.insns.custom.ISwitchPayload; import jadx.core.dex.attributes.AttributeStorage; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnDecoder; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.api.plugins.input.data.AccessFlagsScope.FIELD; import static jadx.api.plugins.input.data.AccessFlagsScope.METHOD; import static jadx.api.plugins.input.insns.Opcode.CONST; import static jadx.api.plugins.input.insns.Opcode.CONST_METHOD_HANDLE; import static jadx.api.plugins.input.insns.Opcode.CONST_METHOD_TYPE; import static jadx.api.plugins.input.insns.Opcode.CONST_WIDE; import static jadx.api.plugins.input.insns.Opcode.FILLED_NEW_ARRAY; import static jadx.api.plugins.input.insns.Opcode.FILLED_NEW_ARRAY_RANGE; import static jadx.api.plugins.input.insns.Opcode.FILL_ARRAY_DATA_PAYLOAD; import static jadx.api.plugins.input.insns.Opcode.INVOKE_CUSTOM; import static jadx.api.plugins.input.insns.Opcode.INVOKE_CUSTOM_RANGE; import static jadx.api.plugins.input.insns.Opcode.INVOKE_POLYMORPHIC; import static jadx.api.plugins.input.insns.Opcode.INVOKE_POLYMORPHIC_RANGE; import static jadx.api.plugins.input.insns.Opcode.PACKED_SWITCH; import static jadx.api.plugins.input.insns.Opcode.PACKED_SWITCH_PAYLOAD; import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH; import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH_PAYLOAD; public class Smali { private static final Logger LOG = LoggerFactory.getLogger(Smali.class); private static SmaliInsnDecoder insnDecoder = null; private ICodeInfo codeInfo; private final Map insnMap = new HashMap<>(); // fullRawId of method as key private final boolean printFileOffset = true; private final boolean printBytecode = true; private boolean isJavaBytecode; private Smali() { } public static Smali disassemble(ClassNode cls) { cls = cls.getTopParentClass(); SmaliWriter code = new SmaliWriter(cls); Smali smali = new Smali(); smali.isJavaBytecode = cls.getInputFileName().endsWith(".class"); // TODO: add flag to api smali.writeClass(code, cls); smali.codeInfo = code.finish(); return smali; } public String getCode() { return codeInfo.getCodeStr(); } public int getMethodDefPos(String mthFullRawID) { SmaliMethodNode info = insnMap.get(mthFullRawID); if (info != null) { return info.getDefPos(); } return -1; } @Nullable public SmaliMethodNode getMethodNode(String mthFullRawID) { return insnMap.get(mthFullRawID); } public int getRegCount(String mthFullRawID) { SmaliMethodNode info = insnMap.get(mthFullRawID); if (info != null) { return info.getRegCount(); } return -1; } public int getParamRegStart(String mthFullRawID) { SmaliMethodNode info = insnMap.get(mthFullRawID); if (info != null) { return info.getParamRegStart(); } return -1; } public int getInsnPosByCodeOffset(String mthFullRawID, long codeOffset) { SmaliMethodNode info = insnMap.get(mthFullRawID); if (info != null) { return info.getInsnPos(codeOffset); } return -1; } @Nullable public Entry getMthFullIDAndCodeOffsetByLine(int line) { for (Entry entry : insnMap.entrySet()) { Integer codeOffset = entry.getValue().getLineMapping().get(line); if (codeOffset != null) { return new SimpleEntry<>(entry.getKey(), codeOffset); } } return null; } public List getRegisterList(String mthFullRawID) { SmaliMethodNode node = insnMap.get(mthFullRawID); if (node != null) { return node.getRegList(); } return Collections.emptyList(); } /** * @return null for no result, FieldInfo for field, Integer for register. */ @Nullable public Object getResultRegOrField(String mthFullRawID, long codeOffset) { SmaliMethodNode info = insnMap.get(mthFullRawID); if (info != null) { InsnNode insn = info.getInsnNode(codeOffset); if (insn != null) { if (insn.getType() == InsnType.IPUT) { return ((IndexInsnNode) insn).getIndex(); } if (insn.getType() == InsnType.INVOKE) { if (insn instanceof InvokeNode) { if (insn.getArgsCount() > 0) { return ((RegisterArg) insn.getArg(0)).getRegNum(); } } } RegisterArg regArg = insn.getResult(); if (regArg != null) { return regArg.getRegNum(); } } } return null; } private void writeClass(SmaliWriter smali, ClassNode cls) { IClassData clsData = cls.getClsData(); if (clsData == null) { smali.startLine(String.format("###### Class %s is created by jadx", cls.getFullName())); return; } AttributeStorage clsAttributes = AttributeStorage.fromList(clsData.getAttributes()); smali.startLine("Class: " + clsData.getType()) .startLine("AccessFlags: " + AccessFlags.format(clsData.getAccessFlags(), AccessFlagsScope.CLASS)) .startLine("SuperType: " + clsData.getSuperType()) .startLine("Interfaces: " + clsData.getInterfacesTypes()) .startLine("SourceFile: " + clsAttributes.get(JadxAttrType.SOURCE_FILE)); AnnotationsAttr annotationsAttr = clsAttributes.get(JadxAttrType.ANNOTATION_LIST); if (annotationsAttr != null) { Collection annos = annotationsAttr.getList(); if (!annos.isEmpty()) { smali.startLine(String.format("# %d annotations", annos.size())); writeAnnotations(smali, new ArrayList<>(annos)); smali.startLine(); } } List fields = new ArrayList<>(); int[] colWidths = new int[] { 0, 0 }; // first is access flag, second is name int[] mthIndex = new int[] { 0 }; LineInfo line = new LineInfo(); clsData.visitFieldsAndMethods( f -> { RawField fld = RawField.make(f); fields.add(fld); if (fld.accessFlag.length() > colWidths[0]) { colWidths[0] = fld.accessFlag.length(); } if (fld.name.length() > colWidths[1]) { colWidths[1] = fld.name.length(); } }, m -> { if (!fields.isEmpty()) { writeFields(smali, clsData, fields, colWidths); fields.clear(); } try { writeMethod(smali, cls.getMethods().get(mthIndex[0]++), m, line); } catch (Throwable e) { IMethodRef methodRef = m.getMethodRef(); String mthFullName = methodRef.getParentClassType() + "->" + methodRef.getName(); smali.setIndent(0); smali.startLine("Failed to write method: " + mthFullName + "\n" + Utils.getStackTrace(e)); LOG.error("Failed to write smali code for method: {}", mthFullName, e); } line.reset(); }); if (!fields.isEmpty()) { // in case there's no methods. writeFields(smali, clsData, fields, colWidths); } for (ClassNode innerClass : cls.getInnerClasses()) { writeClass(smali, innerClass); } } private void writeFields(SmaliWriter smali, IClassData classData, List fields, int[] colWidths) { int staticIdx = 0; smali.startLine().startLine("# fields"); String whites = new String(new byte[Math.max(colWidths[0], colWidths[1])]).replace("\0", " "); for (RawField fld : fields) { smali.startLine(); int pad = colWidths[0] - fld.accessFlag.length(); if (pad > 0) { fld.accessFlag += whites.substring(0, pad); } smali.add(".field ").add(fld.accessFlag); pad = colWidths[1] - fld.name.length(); if (pad > 0) { fld.name += whites.substring(0, pad); } smali.add(fld.name).add(" "); smali.add(": ").add(fld.type); if (fld.isStatic) { EncodedValue constVal = fld.attributes.get(JadxAttrType.CONSTANT_VALUE); if (constVal != null) { smali.add(" # init val = "); writeEncodedValue(smali, constVal, false); } } AnnotationsAttr annotationsAttr = fld.attributes.get(JadxAttrType.ANNOTATION_LIST); if (annotationsAttr != null) { smali.incIndent(); writeAnnotations(smali, annotationsAttr.getList()); smali.decIndent(); } } smali.startLine(); } private void writeMethod(SmaliWriter smali, MethodNode methodNode, IMethodData mth, LineInfo line) { if (insnDecoder == null) { insnDecoder = new SmaliInsnDecoder(methodNode); } smali.startLine().startLine(".method "); writeMethodDef(smali, mth, line); ICodeReader codeReader = mth.getCodeReader(); if (codeReader != null) { int regsCount = codeReader.getRegistersCount(); line.smaliMthNode.setParamRegStart(getParamStartRegNum(mth)); line.smaliMthNode.setRegCount(regsCount); Map nodes = new HashMap<>(codeReader.getUnitsCount() / 2); line.smaliMthNode.setInsnNodes(nodes, codeReader.getUnitsCount()); line.smaliMthNode.initRegInfoList(regsCount, codeReader.getUnitsCount()); smali.incIndent(); smali.startLine(".registers ").add(Integer.toString(regsCount)); writeTries(codeReader, line); IDebugInfo debugInfo = codeReader.getDebugInfo(); List localVars = debugInfo != null ? debugInfo.getLocalVars() : Collections.emptyList(); formatMthParamInfo(mth, smali, line, regsCount, localVars); if (debugInfo != null) { formatDbgInfo(debugInfo, localVars, line); } smali.newLine(); smali.startLine(); // first pass to fill payload offsets for switch instructions codeReader.visitInstructions(insn -> { Opcode opcode = insn.getOpcode(); if (opcode == PACKED_SWITCH || opcode == SPARSE_SWITCH) { insn.decode(); line.addPayloadOffset(insn.getOffset(), insn.getTarget()); } }); codeReader.visitInstructions(insn -> { InsnNode node = decodeInsn(insn, line); nodes.put((long) insn.getOffset(), node); }); line.write(smali); insnMap.put(methodNode.getMethodInfo().getRawFullId(), line.smaliMthNode); smali.decIndent(); } smali.startLine(".end method"); } private void writeTries(ICodeReader codeReader, LineInfo line) { List tries = codeReader.getTries(); for (ITry aTry : tries) { int end = aTry.getEndOffset(); String tryEndTip = String.format(FMT_TRY_END_TAG, end); String tryStartTip = String.format(FMT_TRY_TAG, aTry.getStartOffset()); String tryStartTipExtra = " # :" + tryStartTip.substring(0, tryStartTip.length() - 1); line.addTip(aTry.getStartOffset(), tryStartTip, " # :" + tryEndTip.substring(0, tryEndTip.length() - 1)); line.addTip(end, tryEndTip, tryStartTipExtra); ICatch iCatch = aTry.getCatch(); int[] addresses = iCatch.getHandlers(); int addr; for (int i = 0; i < addresses.length; i++) { addr = addresses[i]; String catchTip = String.format(FMT_CATCH_TAG, addr); line.addTip(addr, catchTip, " # " + iCatch.getTypes()[i]); line.addTip(addr, catchTip, tryStartTipExtra); line.addTip(aTry.getStartOffset(), tryStartTip, " # :" + catchTip.substring(0, catchTip.length() - 1)); } addr = iCatch.getCatchAllHandler(); if (addr > -1) { String catchAllTip = String.format(FMT_CATCH_ALL_TAG, addr); line.addTip(addr, catchAllTip, tryStartTipExtra); line.addTip(aTry.getStartOffset(), tryStartTip, " # :" + catchAllTip.substring(0, catchAllTip.length() - 1)); } } } private InsnNode decodeInsn(InsnData insn, LineInfo lineInfo) { insn.decode(); InsnNode node = insnDecoder.decode(insn); formatInsn(insn, node, lineInfo); return node; } private void formatInsn(InsnData insn, InsnNode node, LineInfo line) { StringBuilder lw = line.getLineWriter(); lw.delete(0, lw.length()); fmtCols(insn, line); if (fmtPayloadInsn(insn, line)) { return; } lw.append(formatInsnName(insn)).append(" "); fmtRegs(insn, node.getType(), line); if (!tryFormatTargetIns(insn, node.getType(), line)) { if (hasLiteral(insn)) { lw.append(", ").append(literal(insn)); } else if (node.getType() == InsnType.INVOKE) { lw.append(", ").append(method(insn)); } else if (insn.getIndexType() == InsnIndexType.FIELD_REF) { lw.append(", ").append(field(insn)); } else if (insn.getIndexType() == InsnIndexType.STRING_REF) { lw.append(", ").append(str(insn)); } else if (insn.getIndexType() == InsnIndexType.TYPE_REF) { lw.append(", ").append(type(insn)); } else if (insn.getOpcode() == CONST_METHOD_HANDLE) { lw.append(", ").append(methodHandle(insn)); } else if (insn.getOpcode() == CONST_METHOD_TYPE) { lw.append(", ").append(proto(insn, insn.getIndex())); } } line.addInsnLine(insn.getOffset(), lw.toString()); } private String formatInsnName(InsnData insn) { if (isJavaBytecode) { // add api opcode, because registers not used return String.format("%-" + INSN_COL_WIDTH + "s | %-15s", insn.getOpcodeMnemonic(), insn.getOpcode().name().toLowerCase(Locale.ROOT).replace('_', '-')); } return String.format(FMT_INSN_COL, insn.getOpcodeMnemonic()); } private boolean tryFormatTargetIns(InsnData insn, InsnType insnType, LineInfo line) { switch (insnType) { case IF: { int target = insn.getTarget(); line.addTip(target, String.format(FMT_COND_TAG, target), ""); line.getLineWriter().append(", ").append(String.format(FMT_COND, target)); return true; } case GOTO: { int target = insn.getTarget(); line.addTip(target, String.format(FMT_GOTO_TAG, target), ""); line.getLineWriter().append(String.format(FMT_GOTO, target)); return true; } case FILL_ARRAY: { int target = insn.getTarget(); line.addTip(target, String.format(FMT_DATA_TAG, target), ""); line.getLineWriter().append(", ").append(String.format(FMT_DATA, target)); return true; } case SWITCH: { int target = insn.getTarget(); if (insn.getOpcode() == Opcode.PACKED_SWITCH) { line.addTip(target, String.format(FMT_P_SWITCH_TAG, target), ""); line.getLineWriter().append(", ").append(String.format(FMT_P_SWITCH, target)); } else { line.addTip(target, String.format(FMT_S_SWITCH_TAG, target), ""); line.getLineWriter().append(", ").append(String.format(FMT_S_SWITCH, target)); } return true; } } return false; } private static boolean hasStaticFlag(int flag) { return (flag & AccessFlags.STATIC) != 0; } private void writeMethodDef(SmaliWriter smali, IMethodData mth, LineInfo lineInfo) { smali.add(AccessFlags.format(mth.getAccessFlags(), METHOD)); IMethodRef methodRef = mth.getMethodRef(); methodRef.load(); lineInfo.smaliMthNode.setDefPos(smali.getLength()); smali.add(methodRef.getName()) .add('('); methodRef.getArgTypes().forEach(smali::add); smali.add(')'); smali.add(methodRef.getReturnType()); AttributeStorage mthAttributes = AttributeStorage.fromList(mth.getAttributes()); AnnotationsAttr annotationsAttr = mthAttributes.get(JadxAttrType.ANNOTATION_LIST); if (annotationsAttr != null && !annotationsAttr.isEmpty()) { smali.incIndent(); writeAnnotations(smali, annotationsAttr.getList()); smali.decIndent(); smali.startLine(); } } private void formatMthParamInfo(IMethodData mth, SmaliWriter smali, LineInfo line, int regsCount, List localVars) { List types = mth.getMethodRef().getArgTypes(); if (types.isEmpty()) { return; } int paramStart = 0; int regNum = line.smaliMthNode.getParamRegStart(); if (!hasStaticFlag(mth.getAccessFlags())) { // add 'this' register line.addRegName(regNum, "p0"); line.smaliMthNode.setParamReg(regNum, "p0"); regNum++; paramStart++; } if (localVars.isEmpty()) { return; } ILocalVar[] params = new ILocalVar[regsCount]; for (ILocalVar var : localVars) { if (var.isMarkedAsParameter()) { params[var.getRegNum()] = var; } } smali.newLine(); for (String paramType : types) { ILocalVar param = params[regNum]; if (param != null) { String name = Utils.getOrElse(param.getName(), ""); String type = Utils.getOrElse(param.getSignature(), paramType); String varName = "p" + paramStart; smali.startLine(String.format(".param %s, \"%s\" # %s", varName, name, type)); line.addRegName(regNum, varName); line.smaliMthNode.setParamReg(regNum, varName); } int regSize = isWideType(paramType) ? 2 : 1; regNum += regSize; paramStart += regSize; } } private static int getParamStartRegNum(IMethodData mth) { ICodeReader codeReader = mth.getCodeReader(); if (codeReader != null) { int startNum = codeReader.getRegistersCount(); if (startNum > 0) { for (String argType : mth.getMethodRef().getArgTypes()) { if (isWideType(argType)) { startNum -= 2; } else { startNum -= 1; } } if (!hasStaticFlag(mth.getAccessFlags())) { startNum--; } return startNum; } } return -1; } private static boolean isWideType(String type) { return type.equals("D") || type.equals("J"); } private void writeAnnotations(SmaliWriter smali, List annoList) { if (annoList.size() > 0) { for (int i = 0; i < annoList.size(); i++) { smali.startLine(); writeAnnotation(smali, annoList.get(i)); if (i != annoList.size() - 1) { smali.startLine(); } } } } private void writeAnnotation(SmaliWriter smali, IAnnotation anno) { smali.add(".annotation") .add(" "); AnnotationVisibility vby = anno.getVisibility(); if (vby != null) { smali.add(vby.toString().toLowerCase()).add(" "); } smali.add(anno.getAnnotationClass()); anno.getValues().forEach((k, v) -> { smali.incIndent(); smali.startLine(k).add(" = "); writeEncodedValue(smali, v, true); smali.decIndent(); }); smali.startLine(".end annotation"); } private void formatDbgInfo(IDebugInfo dbgInfo, List localVars, LineInfo line) { dbgInfo.getSourceLineMapping().forEach((codeOffset, srcLine) -> { if (codeOffset > -1) { line.addDebugLineTip(codeOffset, String.format(".line %d", srcLine), ""); } }); for (ILocalVar localVar : localVars) { if (localVar.isMarkedAsParameter()) { continue; } String type = localVar.getType(); String sign = localVar.getSignature(); String longTypeStr; if (sign == null || sign.trim().isEmpty()) { longTypeStr = String.format(", \"%s\":%s", localVar.getName(), type); } else { longTypeStr = String.format(", \"%s\":%s, \"%s\"", localVar.getName(), type, localVar.getSignature()); } line.addTip( localVar.getStartOffset(), ".local " + formatVarName(line.smaliMthNode, localVar), longTypeStr); line.addTip( localVar.getEndOffset(), ".end local " + formatVarName(line.smaliMthNode, localVar), String.format(" # \"%s\":%s", localVar.getName(), type)); } } private String formatVarName(SmaliMethodNode smaliMthNode, ILocalVar localVar) { int paramRegStart = smaliMthNode.getParamRegStart(); int regNum = localVar.getRegNum(); if (regNum < paramRegStart) { return "v" + regNum; } return "p" + (regNum - paramRegStart); } private void writeEncodedValue(SmaliWriter smali, EncodedValue value, boolean wrapArray) { StringUtils stringUtils = smali.getClassNode().root().getStringUtils(); switch (value.getType()) { case ENCODED_ARRAY: smali.add("{"); if (wrapArray) { smali.incIndent(); smali.startLine(); } List values = (List) value.getValue(); for (int i = 0; i < values.size(); i++) { writeEncodedValue(smali, values.get(i), wrapArray); if (i != values.size() - 1) { smali.add(","); if (wrapArray) { smali.startLine(); } else { smali.add(" "); } } } if (wrapArray) { smali.decIndent(); smali.startLine("}"); } break; case ENCODED_NULL: smali.add("null"); break; case ENCODED_ANNOTATION: writeAnnotation(smali, (IAnnotation) value.getValue()); break; case ENCODED_BYTE: smali.add(stringUtils.formatByte((Byte) value.getValue(), false)); break; case ENCODED_SHORT: smali.add(stringUtils.formatShort((Short) value.getValue(), false)); break; case ENCODED_CHAR: smali.add(stringUtils.unescapeChar((Character) value.getValue())); break; case ENCODED_INT: smali.add(stringUtils.formatInteger((Integer) value.getValue(), false)); break; case ENCODED_LONG: smali.add(stringUtils.formatLong((Long) value.getValue(), false)); break; case ENCODED_FLOAT: smali.add(StringUtils.formatFloat((Float) value.getValue())); break; case ENCODED_DOUBLE: smali.add(StringUtils.formatDouble((Double) value.getValue())); break; case ENCODED_STRING: smali.add(stringUtils.unescapeString((String) value.getValue())); break; case ENCODED_TYPE: smali.add(ArgType.parse((String) value.getValue()) + ".class"); break; default: smali.add(String.valueOf(value.getValue())); } } private static final int CODE_OFFSET_COLUMN_WIDTH = 4; private static final int BYTECODE_COLUMN_WIDTH = 20 + 3; // 3 for ellipses. private static final String FMT_BYTECODE_COL = "%-" + (BYTECODE_COLUMN_WIDTH - 3) + "s"; private static final int INSN_COL_WIDTH = "const-method-handle".length(); private static final String FMT_INSN_COL = "%-" + INSN_COL_WIDTH + "s"; private static final String FMT_FILE_OFFSET = "%08x:"; private static final String FMT_CODE_OFFSET = "%04x:"; private static final String FMT_TARGET_OFFSET = "%04x"; private static final String FMT_GOTO = ":goto_" + FMT_TARGET_OFFSET; private static final String FMT_COND = ":cond_" + FMT_TARGET_OFFSET; private static final String FMT_DATA = ":array_" + FMT_TARGET_OFFSET; private static final String FMT_P_SWITCH = ":p_switch_" + FMT_TARGET_OFFSET; private static final String FMT_S_SWITCH = ":s_switch_" + FMT_TARGET_OFFSET; private static final String FMT_P_SWITCH_CASE = ":p_case_" + FMT_TARGET_OFFSET; private static final String FMT_S_SWITCH_CASE = ":s_case_" + FMT_TARGET_OFFSET; private static final String FMT_TRY_TAG = "try_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_TRY_END_TAG = "try_end_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_CATCH_TAG = "catch_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_CATCH_ALL_TAG = "catch_all_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_GOTO_TAG = "goto_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_COND_TAG = "cond_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_DATA_TAG = "array_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_P_SWITCH_TAG = "p_switch_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_S_SWITCH_TAG = "s_switch_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_P_SWITCH_CASE_TAG = "p_case_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_S_SWITCH_CASE_TAG = "s_case_" + FMT_TARGET_OFFSET + ":"; private void fmtRegs(InsnData insn, InsnType insnType, LineInfo line) { boolean appendBrace = insnType == InsnType.INVOKE || isRegList(insn); StringBuilder lw = line.getLineWriter(); if (insnType == InsnType.INVOKE) { int resultReg = insn.getResultReg(); if (resultReg != -1) { lw.append(line.getRegName(resultReg)).append(" <= "); } } if (appendBrace) { lw.append("{"); } if (isRangeRegIns(insn)) { lw.append(line.getRegName(insn.getReg(0))) .append(" .. ") .append(line.getRegName(insn.getReg(insn.getRegsCount() - 1))); } else if (insn.getRegsCount() > 0) { for (int i = 0; i < insn.getRegsCount(); i++) { if (i > 0) { lw.append(", "); } lw.append(line.getRegName(insn.getReg(i))); } } if (appendBrace) { lw.append("}"); } } private int getInsnColStart() { int start = 0; if (printFileOffset) { start += 8 + 1 + 1; // plus 1s for space and the ':' } if (printBytecode) { start += BYTECODE_COLUMN_WIDTH + 1; // plus 1 for space } return start; } private void fmtCols(InsnData insn, LineInfo line) { if (printFileOffset) { line.getLineWriter().append(String.format(FMT_FILE_OFFSET + " ", insn.getFileOffset())); } if (printBytecode) { formatByteCode(line.getLineWriter(), insn.getByteCode()); line.getLineWriter().append(" "); line.getLineWriter().append(String.format(FMT_CODE_OFFSET + " ", insn.getOffset())); } } private void formatByteCode(StringBuilder smali, byte[] bytes) { int maxLen = Math.min(bytes.length, 4 * 2); // limit to 4 units StringBuilder inHex = new StringBuilder(); for (int i = 0; i < maxLen; i++) { inHex.append(String.format("%02x", bytes[i])); if (i % 2 == 1) { inHex.append(' '); } } smali.append(String.format(FMT_BYTECODE_COL, inHex)); if (maxLen < bytes.length) { smali.append("..."); } else { smali.append(" "); } } private boolean fmtPayloadInsn(InsnData insn, LineInfo line) { Opcode opcode = insn.getOpcode(); if (opcode == PACKED_SWITCH_PAYLOAD) { line.getLineWriter().append("packed-switch-payload"); line.addInsnLine(insn.getOffset(), line.getLineWriter().toString()); ISwitchPayload payload = (ISwitchPayload) insn.getPayload(); if (payload != null) { fmtSwitchPayload(insn, FMT_P_SWITCH_CASE, FMT_P_SWITCH_CASE_TAG, line, payload); } return true; } if (opcode == SPARSE_SWITCH_PAYLOAD) { line.getLineWriter().append("sparse-switch-payload"); line.addInsnLine(insn.getOffset(), line.getLineWriter().toString()); ISwitchPayload payload = (ISwitchPayload) insn.getPayload(); if (payload != null) { fmtSwitchPayload(insn, FMT_S_SWITCH_CASE, FMT_S_SWITCH_CASE_TAG, line, payload); } return true; } if (opcode == FILL_ARRAY_DATA_PAYLOAD) { line.getLineWriter().append("fill-array-data-payload"); line.addInsnLine(insn.getOffset(), line.getLineWriter().toString()); return true; } return false; } private void fmtSwitchPayload(InsnData insn, String fmtTarget, String fmtTag, LineInfo line, ISwitchPayload payload) { int lineStart = getInsnColStart(); lineStart += CODE_OFFSET_COLUMN_WIDTH + 1 + 1; // plus 1s for space and the ':' String basicIndent = new String(new byte[lineStart]).replace("\0", " "); String indent = JadxArgs.DEFAULT_INDENT_STR + basicIndent; int[] keys = payload.getKeys(); int[] targets = payload.getTargets(); Integer switchOffset = line.payloadOffsetMap.get(insn.getOffset()); if (switchOffset == null) { throw new JadxRuntimeException("Unknown switch insn for payload at " + insn.getOffset()); } for (int i = 0; i < keys.length; i++) { int target = switchOffset + targets[i]; line.addInsnLine(insn.getOffset(), String.format("%scase %d: -> " + fmtTarget, indent, keys[i], target)); line.addTip(target, String.format(fmtTag, target), String.format(" # case %d", keys[i])); } line.addInsnLine(insn.getOffset(), basicIndent + ".end payload"); } private static String literal(InsnData insn) { long it = insn.getLiteral(); String tip = ""; if (it > Integer.MAX_VALUE) { if (isWideIns(insn)) { tip = " # double: " + Double.longBitsToDouble(it); } else if (getOpenCodeByte(insn) == 0x15) { // CONST_HIGH16 = 0x15; tip = " # float: " + Float.intBitsToFloat((int) it); } } else if (it <= 0) { return "" + it + tip; } return "0x" + Long.toHexString(it) + tip; } private static String str(InsnData insn) { return String.format("\"%s\" # string@%04x", insn.getIndexAsString() .replace("\n", "\\n") .replace("\t", "\\t"), insn.getIndex()); } private static String type(InsnData insn) { return String.format("%s # type@%04x", insn.getIndexAsType(), insn.getIndex()); } private static String field(InsnData insn) { return String.format("%s # field@%04x", insn.getIndexAsField().toString(), insn.getIndex()); } private static String method(InsnData insn) { Opcode op = insn.getOpcode(); if (op == INVOKE_CUSTOM || op == INVOKE_CUSTOM_RANGE) { insn.getIndexAsCallSite().load(); return String.format("%s # call_site@%04x", insn.getIndexAsCallSite().toString(), insn.getIndex()); } IMethodRef mthRef = insn.getIndexAsMethod(); mthRef.load(); if (op == INVOKE_POLYMORPHIC || op == INVOKE_POLYMORPHIC_RANGE) { return String.format("%s, %s # method@%04x, proto@%04x", mthRef.toString(), insn.getIndexAsProto(insn.getTarget()).toString(), insn.getIndex(), insn.getTarget()); } return String.format("%s # method@%04x", mthRef.toString(), insn.getIndex()); } private static String proto(InsnData insn, int protoIndex) { return String.format("%s # proto@%04x", insn.getIndexAsProto(protoIndex).toString(), protoIndex); } private static String methodHandle(InsnData insn) { return String.format("%s # method_handle@%04x", insn.getIndexAsMethodHandle().toString(), insn.getIndex()); } protected static boolean isRangeRegIns(InsnData insn) { switch (insn.getOpcode()) { case INVOKE_VIRTUAL_RANGE: case INVOKE_SUPER_RANGE: case INVOKE_DIRECT_RANGE: case INVOKE_STATIC_RANGE: case INVOKE_INTERFACE_RANGE: case FILLED_NEW_ARRAY_RANGE: case INVOKE_CUSTOM_RANGE: case INVOKE_POLYMORPHIC_RANGE: return true; } return false; } private static int getOpenCodeByte(InsnData insn) { return insn.getRawOpcodeUnit() & 0xff; } private static boolean isWideIns(InsnData insn) { return insn.getOpcode() == CONST_WIDE; } private static boolean hasLiteral(InsnData insn) { int opcode = getOpenCodeByte(insn); return insn.getOpcode() == CONST || insn.getOpcode() == CONST_WIDE || (opcode >= 0xd0 && opcode <= 0xe2); // add-int/lit16 to ushr-int/lit8 } private static boolean isRegList(InsnData insn) { return insn.getOpcode() == FILLED_NEW_ARRAY || insn.getOpcode() == FILLED_NEW_ARRAY_RANGE; } private class LineInfo { private SmaliMethodNode smaliMthNode = new SmaliMethodNode(); private final StringBuilder lineWriter = new StringBuilder(50); private String lastDebugTip = ""; private final Map> insnOffsetMap = new LinkedHashMap<>(); private final Map regNameMap = new HashMap<>(); private Map> tipMap = Collections.emptyMap(); private Map payloadOffsetMap = Collections.emptyMap(); public LineInfo() { } public StringBuilder getLineWriter() { return lineWriter; } public void reset() { lastDebugTip = ""; payloadOffsetMap = Collections.emptyMap(); tipMap = Collections.emptyMap(); insnOffsetMap.clear(); regNameMap.clear(); smaliMthNode = new SmaliMethodNode(); } public void addRegName(int regNum, String name) { regNameMap.put(regNum, name); } public String getRegName(int regNum) { String name = regNameMap.get(regNum); if (name == null) { name = "v" + regNum; } return name; } public void addInsnLine(int codeOffset, String insnLine) { List insnList = insnOffsetMap.computeIfAbsent(codeOffset, k -> new ArrayList<>(1)); insnList.add(insnLine); } public void addTip(int offset, String tip, String extra) { if (tipMap.isEmpty()) { tipMap = new LinkedHashMap<>(); } Map innerMap = tipMap.computeIfAbsent(offset, k -> new LinkedHashMap<>()); Object obj = innerMap.get(tip); if (obj != null) { if (obj instanceof String) { if (obj.equals("")) { innerMap.put(tip, 2); } else { List extras = new ArrayList<>(2); extras.add((String) obj); extras.add(extra); innerMap.put(tip, extras); } } else if (obj instanceof Integer) { innerMap.put(tip, ((int) obj) + 1); } else if (obj instanceof List) { if (!extra.isEmpty()) { List extras = (List) obj; extras.add(extra); } } } else { innerMap.put(tip, extra); } } public void addDebugLineTip(int offset, String tip, String extra) { if (tip.equals(lastDebugTip)) { return; } lastDebugTip = tip; if (tipMap.isEmpty()) { tipMap = new LinkedHashMap<>(); } Map innerMap = tipMap.computeIfAbsent(offset, k -> new LinkedHashMap<>()); innerMap.put(tip, extra); } public void addPayloadOffset(int curOffset, int payloadOffset) { if (payloadOffsetMap.isEmpty()) { payloadOffsetMap = new HashMap<>(); } payloadOffsetMap.put(payloadOffset, curOffset); } public void write(SmaliWriter smali) { int lineOffset = getInsnColStart(); for (Entry> entry : insnOffsetMap.entrySet()) { writeTip(smali, entry.getKey(), lineOffset); smaliMthNode.setInsnInfo(entry.getKey(), lineOffset + smali.getLength()); smaliMthNode.attachLine(smali.getLine(), entry.getKey()); smali.attachSourceLine(entry.getKey()); for (String s : entry.getValue()) { smali.add(s).startLine(); } } } private void writeTip(SmaliWriter smali, int codeOffset, int lineOffset) { Map tip = tipMap.get(codeOffset); if (tip != null) { for (Entry entry : tip.entrySet()) { int start = Math.max(0, lineOffset - entry.getKey().length()); if (start > 0) { smali.add(new String(new byte[start]).replace("\0", " ")); } if (entry.getValue() instanceof Integer) { smali.add(String.format("%s # %d refs", entry.getKey(), entry.getValue())) .startLine(); } else if (entry.getValue() instanceof String) { smali.add(String.format("%s%s", entry.getKey(), entry.getValue())) .startLine(); } else if (entry.getValue() instanceof List) { List extras = (List) entry.getValue(); smali.add(String.format("%s%s", entry.getKey(), extras.get(0))) .startLine(); String pad = new String(new byte[lineOffset]).replace("\0", " "); for (int i = 1; i < extras.size(); i++) { smali.add(String.format("%s%s", pad, extras.get(i))) .startLine(); } } else { smali.add(String.format("%s%s", entry.getKey(), entry.getValue())) .startLine(); } } } } } private static class SmaliInsnDecoder extends InsnDecoder { @Override protected @NotNull InsnNode decode(InsnData insn) { try { return super.decode(insn); } catch (Exception e) { switch (insn.getOpcode()) { case INVOKE_CUSTOM: case INVOKE_CUSTOM_RANGE: case INVOKE_POLYMORPHIC: case INVOKE_POLYMORPHIC_RANGE: case CONST_METHOD_HANDLE: case CONST_METHOD_TYPE: return new InsnNode(InsnType.INVOKE, insn.getRegsCount()); default: throw new RuntimeException(e); } } } public SmaliInsnDecoder(MethodNode mthNode) { super(mthNode); } @Override public InsnNode[] process(ICodeReader codeReader) { return null; } } private static class RawField { boolean isStatic; String accessFlag; String name; String type; AttributeStorage attributes; private static RawField make(IFieldData f) { RawField field = new RawField(); field.isStatic = hasStaticFlag(f.getAccessFlags()); field.accessFlag = AccessFlags.format(f.getAccessFlags(), FIELD); field.name = f.getName(); field.type = f.getType(); field.attributes = AttributeStorage.fromList(f.getAttributes()); return field; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliMethodNode.java ================================================ package jadx.gui.device.debugger.smali; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; public class SmaliMethodNode { private Map nodes; // codeOffset: InsnNode private List regList; private int[] insnPos; private int defPos; private Map lineMapping = Collections.emptyMap(); // line: codeOffset private int paramRegStart; private int regCount; public int getParamRegStart() { return this.paramRegStart; } public int getRegCount() { return this.regCount; } /** * Returns the line mapping of the: * 'output disassembled smali file line index' to 'dex instruction position code offset' * The value is the same as {@link InsnNode#getOffset()} * * @return the line mapping */ public Map getLineMapping() { return lineMapping; } public void initRegInfoList(int regCount, int insnCount) { regList = new ArrayList<>(regCount); for (int i = 0; i < regCount; i++) { regList.add(new SmaliRegister(i, insnCount)); } } public int getInsnPos(long codeOffset) { if (insnPos != null && codeOffset < insnPos.length) { return insnPos[(int) codeOffset]; } return -1; } public int getDefPos() { return defPos; } public InsnNode getInsnNode(long codeOffset) { return nodes.get(codeOffset); } public List getRegList() { return regList; } protected SmaliMethodNode() { } protected void setRegCount(int regCount) { this.regCount = regCount; } protected void attachLine(int line, int codeOffset) { if (lineMapping.isEmpty()) { lineMapping = new HashMap<>(); } lineMapping.put(line, codeOffset); } protected void setInsnInfo(int codeOffset, int pos) { if (insnPos != null && codeOffset < insnPos.length) { insnPos[codeOffset] = pos; } InsnNode insn = getInsnNode(codeOffset); RegisterArg r = insn.getResult(); if (r != null) { regList.get(r.getRegNum()).setStartOffset(codeOffset); } for (InsnArg arg : insn.getArguments()) { if (arg instanceof RegisterArg) { regList.get(((RegisterArg) arg).getRegNum()).setStartOffset(codeOffset); } } } protected void setDefPos(int pos) { defPos = pos; } protected void setParamReg(int regNum, String name) { SmaliRegister r = regList.get(regNum); r.setParam(name); } protected void setParamRegStart(int paramRegStart) { this.paramRegStart = paramRegStart; } protected void setInsnNodes(Map nodes, int insnCount) { this.nodes = nodes; insnPos = new int[insnCount]; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliRegister.java ================================================ package jadx.gui.device.debugger.smali; public class SmaliRegister extends RegisterInfo { private final int num; private String paramName; private final int endOffset; private int startOffset; private boolean isParam; private int runtimeNum; public SmaliRegister(int num, int insnCount) { this.num = num; this.endOffset = insnCount; this.startOffset = insnCount; } public int getRuntimeRegNum() { return runtimeNum; } public void setRuntimeRegNum(int runtimeNum) { this.runtimeNum = runtimeNum; } @Override public boolean isInitialized(long codeOffset) { return codeOffset > getStartOffset() && codeOffset < getEndOffset(); } protected void setParam(String name) { paramName = name; isParam = true; } protected void setStartOffset(int off) { if (off < startOffset) { startOffset = off; } } @Override public String getName() { return paramName != null ? paramName : "v" + num; } @Override public int getRegNum() { return num; } @Override public String getType() { return ""; } @Override public String getSignature() { return null; } @Override public int getStartOffset() { return startOffset; } @Override public int getEndOffset() { return endOffset; } @Override public boolean isMarkedAsParameter() { return isParam; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliWriter.java ================================================ package jadx.gui.device.debugger.smali; import jadx.api.ICodeInfo; import jadx.api.impl.SimpleCodeInfo; import jadx.api.impl.SimpleCodeWriter; import jadx.core.dex.nodes.ClassNode; public class SmaliWriter extends SimpleCodeWriter { private int line = 0; private final ClassNode cls; public SmaliWriter(ClassNode cls) { super(cls.root().getArgs()); this.cls = cls; } public ClassNode getClassNode() { return cls; } @Override protected void addLine() { super.addLine(); line++; } @Override public int getLine() { return line; } @Override public ICodeInfo finish() { return new SimpleCodeInfo(buf.toString()); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/protocol/ADB.java ================================================ package jadx.gui.device.protocol; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.net.SocketException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.StringJoiner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.log.LogUtils; import jadx.gui.utils.IOUtils; public class ADB { public static final Charset ADB_CHARSET = StandardCharsets.UTF_8; private static final Logger LOG = LoggerFactory.getLogger(ADB.class); private static final int DEFAULT_PORT = 5037; private static final String DEFAULT_ADDR = "localhost"; private static final String CMD_FEATURES = "000dhost:features"; private static final String CMD_TRACK_DEVICES = "0014host:track-devices-l"; private static final byte[] OKAY = "OKAY".getBytes(ADB_CHARSET); private static final byte[] FAIL = "FAIL".getBytes(ADB_CHARSET); static boolean isOkay(InputStream stream, String command) throws IOException { byte[] buf = IOUtils.readNBytes(stream, 4); if (Arrays.equals(buf, OKAY)) { return true; } if (Arrays.equals(buf, FAIL)) { // Observed that after FAIL the length in hex follows and afterwards an error message, // but it is unclear if this is true for all cases where isOkay is used. // int msgLen = Integer.parseInt(new String(IOUtils.readNBytes(stream, 4)), 16); // byte[] errorMsg = IOUtils.readNBytes(stream, msgLen); // LOG.error("isOkay failed: received error message: {}", new String(errorMsg)); LOG.error("isOkay failed for command: {}", command); return false; } if (buf == null) { throw new IOException("isOkay failed - steam ended"); } throw new IOException("isOkay failed - unexpected response " + new String(buf, ADB_CHARSET)); } public static byte[] exec(String cmd, OutputStream outputStream, InputStream inputStream) throws IOException { return execCommandSync(outputStream, inputStream, cmd); } public static byte[] exec(String cmd) throws IOException { try (Socket socket = connect()) { return exec(cmd, socket.getOutputStream(), socket.getInputStream()); } } public static Socket connect() throws IOException { return connect(DEFAULT_ADDR, DEFAULT_PORT); } public static Socket connect(String host, int port) throws IOException { return new Socket(host, port); } static boolean execCommandAsync(OutputStream outputStream, InputStream inputStream, String cmd) throws IOException { outputStream.write(cmd.getBytes(ADB_CHARSET)); return isOkay(inputStream, "execCommandAsync"); } private static byte[] execCommandSync(OutputStream outputStream, InputStream inputStream, String cmd) throws IOException { outputStream.write(cmd.getBytes(ADB_CHARSET)); if (isOkay(inputStream, "execCommandSync")) { return readServiceProtocol(inputStream); } return null; } static byte[] readServiceProtocol(InputStream stream) { try { byte[] buf = IOUtils.readNBytes(stream, 4); if (buf == null) { return null; } int len = hexToInt(buf); byte[] result; if (len == 0) { result = new byte[0]; } else { result = IOUtils.readNBytes(stream, len); } if (LOG.isTraceEnabled()) { LOG.trace("readServiceProtocol result: {}", LogUtils.escape(result)); } return result; } catch (SocketException e) { LOG.warn("Aborting readServiceProtocol: {}", e.toString()); } catch (IOException e) { LOG.error("Failed to read readServiceProtocol", e); } return null; } static boolean setSerial(String serial, OutputStream outputStream, InputStream inputStream) throws IOException { checkSerial(serial); LOG.trace("setSerial({})", serial); String setSerialCmd = String.format("host:tport:serial:%s", serial); setSerialCmd = String.format("%04x%s", setSerialCmd.length(), setSerialCmd); outputStream.write(setSerialCmd.getBytes(ADB_CHARSET)); boolean ok = isOkay(inputStream, setSerialCmd); if (ok) { // skip the shell-state-id returned by ADB server, it's not important for the following actions. inputStream.readNBytes(8); } else { LOG.error("setSerial command {} failed", LogUtils.escape(setSerialCmd)); } return ok; } private static byte[] execShellCommandRaw(String cmd, OutputStream outputStream, InputStream inputStream) throws IOException { cmd = String.format("shell,v2,TERM=xterm-256color,raw:%s", cmd); cmd = String.format("%04x%s", cmd.length(), cmd); outputStream.write(cmd.getBytes(ADB_CHARSET)); if (isOkay(inputStream, cmd)) { return ShellProtocol.readStdout(inputStream); } return null; } static byte[] execShellCommandRaw(String serial, String cmd, OutputStream outputStream, InputStream inputStream) throws IOException { if (setSerial(serial, outputStream, inputStream)) { return execShellCommandRaw(cmd, outputStream, inputStream); } return null; } public static List getFeatures() throws IOException { byte[] rst = exec(CMD_FEATURES); if (rst != null) { return Arrays.asList(new String(rst, ADB_CHARSET).trim().split(",")); } return Collections.emptyList(); } public static boolean startServer(String adbPath, int port) throws IOException { String tcpPort = String.format("tcp:%d", port); List command = Arrays.asList(adbPath, "-L", tcpPort, "start-server"); java.lang.Process proc = new ProcessBuilder(command) .redirectErrorStream(true) .start(); try { // Wait for the adb server to start. On Windows even on a fast system 6 seconds are not unusual. proc.waitFor(10, TimeUnit.SECONDS); proc.exitValue(); } catch (Exception e) { LOG.error("ADB start server failed with command: {}", String.join(" ", command), e); proc.destroyForcibly(); return false; } ByteArrayOutputStream out = new ByteArrayOutputStream(); try (InputStream in = proc.getInputStream()) { int read; byte[] buf = new byte[1024]; while ((read = in.read(buf)) >= 0) { out.write(buf, 0, read); } } return out.toString().contains(tcpPort); } public static boolean isServerRunning(String host, int port) { try (Socket sock = new Socket(host, port)) { return true; } catch (Exception e) { return false; } } /** * @return a socket connected to adb server, otherwise null */ public static Socket listenForDeviceState(DeviceStateListener listener, String host, int port) throws IOException { Socket socket = connect(host, port); InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); if (!execCommandAsync(outputStream, inputStream, CMD_TRACK_DEVICES)) { socket.close(); return null; } ExecutorService listenThread = Executors.newFixedThreadPool(1); listenThread.execute(() -> { while (true) { byte[] res = readServiceProtocol(inputStream); if (res == null) { break; // socket disconnected } if (listener != null) { String payload = new String(res, ADB_CHARSET); String[] deviceLines = payload.split("\n"); List deviceInfoList = new ArrayList<>(deviceLines.length); for (String deviceLine : deviceLines) { if (!deviceLine.trim().isEmpty()) { deviceInfoList.add(new ADBDeviceInfo(deviceLine, host, port)); } } listener.onDeviceStatusChange(deviceInfoList); } } if (listener != null) { listener.adbDisconnected(); } }); return socket; } public static List listForward(String host, int port) throws IOException { try (Socket socket = connect(host, port)) { String cmd = "0011host:list-forward"; InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); outputStream.write(cmd.getBytes(ADB_CHARSET)); if (isOkay(inputStream, "listForward")) { byte[] bytes = readServiceProtocol(inputStream); if (bytes != null) { String[] forwards = new String(bytes, ADB_CHARSET).split("\n"); return Stream.of(forwards).map(String::trim).collect(Collectors.toList()); } } } return Collections.emptyList(); } public static boolean removeForward(String host, int port, String serial, String localPort) throws IOException { try (Socket socket = connect(host, port)) { String cmd = String.format("host:killforward:tcp:%s", localPort); cmd = String.format("%04x%s", cmd.length(), cmd); InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); if (setSerial(serial, outputStream, inputStream)) { outputStream.write(cmd.getBytes(ADB_CHARSET)); return isOkay(inputStream, "removeForward1") && isOkay(inputStream, "removeForward2"); } } return false; } // Little endian private static int readInt(byte[] bytes, int start) { int result = (bytes[start] & 0xff); result += ((bytes[start + 1] & 0xff) << 8); result += ((bytes[start + 2] & 0xff) << 16); result += (bytes[start + 3] & 0xff) << 24; return result; } private static byte[] appendBytes(byte[] dest, byte[] src, int realSrcSize) { byte[] rst = new byte[dest.length + realSrcSize]; System.arraycopy(dest, 0, rst, 0, dest.length); System.arraycopy(src, 0, rst, dest.length, realSrcSize); return rst; } private static final Pattern SERIAL_PATTERN = Pattern.compile("^[\\w-]{10,20}$"); private static void checkSerial(String serial) { if (!SERIAL_PATTERN.matcher(serial).matches()) { throw new IllegalArgumentException("Invalid serial: " + serial); } } /** * Convert 4 hex characters to int * * @param hex * @return */ private static int hexToInt(byte[] hex) { int n = 0; byte b; for (int i = 0; i < 4; i++) { b = hex[i]; switch (b) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': b -= '0'; break; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': b = (byte) (b - 'a' + 10); break; case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': b = (byte) (b - 'A' + 10); break; default: return -1; } n = (n << 4) | (b & 0xff); } return n; } public interface JDWPProcessListener { void jdwpProcessOccurred(ADBDevice device, Set id); void jdwpListenerClosed(ADBDevice device); } public interface DeviceStateListener { void onDeviceStatusChange(List deviceInfoList); void adbDisconnected(); } public static class Process { public String user; public String pid; public String ppid; public String name; public static Process make(String processLine) { String[] fields = processLine.split("\\s+"); if (fields.length >= 4) { // 0 for user, 1 for pid, 2 for ppid, the last one for name Process proc = new Process(); proc.user = fields[0]; proc.pid = fields[1]; proc.ppid = fields[2]; proc.name = fields[fields.length - 1]; return proc; } return null; } @Override public String toString() { return new StringJoiner(", ", Process.class.getSimpleName() + "[", "]") .add("user='" + user + "'") .add("pid='" + pid + "'") .add("ppid='" + ppid + "'") .add("name='" + name + "'") .toString(); } } private static class ShellProtocol { private static final int ID_STD_IN = 0; private static final int ID_STD_OUT = 1; private static final int ID_STD_ERR = 2; private static final int ID_EXIT = 3; // Close subprocess stdin if possible. private static final int ID_CLOSE_STDIN = 4; // Window size change (an ASCII version of struct winsize). private static final int ID_WINDOW_SIZE_CHANGE = 5; // Indicates an invalid or unknown packet. private static final int ID_INVALID = 255; public static byte[] readStdout(InputStream inputStream) throws IOException { byte[] header = new byte[5]; ByteArrayOutputStream payload = new ByteArrayOutputStream(); byte[] tempBuf = new byte[1024]; for (boolean exit = false; !exit;) { IOUtils.read(inputStream, header); exit = header[0] == ID_EXIT; int payloadSize = readInt(header, 1); if (tempBuf.length < payloadSize) { tempBuf = new byte[payloadSize]; } int readSize = IOUtils.read(inputStream, tempBuf, 0, payloadSize); if (readSize != payloadSize) { LOG.error("Failed to read ShellProtocol data"); return null; // we don't want corrupted data. } payload.write(tempBuf, 0, readSize); } return payload.toByteArray(); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/protocol/ADBDevice.java ================================================ package jadx.gui.device.protocol; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.StringUtils; import jadx.core.utils.log.LogUtils; import jadx.gui.device.protocol.ADB.JDWPProcessListener; import jadx.gui.device.protocol.ADB.Process; import static jadx.gui.device.protocol.ADB.ADB_CHARSET; public class ADBDevice { private static final Logger LOG = LoggerFactory.getLogger(ADBDevice.class); private static final String CMD_TRACK_JDWP = "000atrack-jdwp"; private static final Pattern TIMESTAMP_FORMAT = Pattern.compile("^[0-9]{2}\\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}$"); ADBDeviceInfo info; String androidReleaseVer; volatile Socket jdwpListenerSock; public ADBDevice(ADBDeviceInfo info) { this.info = info; } public ADBDeviceInfo getDeviceInfo() { return info; } public boolean updateDeviceInfo(ADBDeviceInfo info) { if (info.getSerial() == null || info.getSerial().isEmpty()) { return false; } boolean matched = this.info.getSerial().equals(info.getSerial()); if (matched) { this.info = info; } return matched; } public String getSerial() { return info.getSerial(); } public boolean removeForward(String localPort) throws IOException { return ADB.removeForward(info.getAdbHost(), info.getAdbPort(), info.getSerial(), localPort); } public ForwardResult forwardJDWP(String localPort, String jdwpPid) throws IOException { try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) { String cmd = String.format("host:forward:tcp:%s;jdwp:%s", localPort, jdwpPid); cmd = String.format("%04x%s", cmd.length(), cmd); InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); ForwardResult rst; if (ADB.setSerial(info.getSerial(), outputStream, inputStream)) { outputStream.write(cmd.getBytes(ADB_CHARSET)); if (!ADB.isOkay(inputStream, "forwardJDWP1")) { rst = new ForwardResult(1, ADB.readServiceProtocol(inputStream)); } else if (!ADB.isOkay(inputStream, "forwardJDWP2")) { rst = new ForwardResult(2, ADB.readServiceProtocol(inputStream)); } else { rst = new ForwardResult(0, null); } } else { rst = new ForwardResult(1, "Unknown error.".getBytes(ADB_CHARSET)); } return rst; } } public static class ForwardResult { /** * 0 for success, 1 for failed at binding to local tcp, 2 for failed at remote. */ public int state; public String desc; public ForwardResult(int state, byte[] desc) { if (desc != null) { this.desc = new String(desc, ADB_CHARSET); } else { this.desc = ""; } this.state = state; } } /** * @return pid otherwise -1 */ public int launchApp(String fullAppName) throws IOException, InterruptedException { byte[] res; try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) { String cmd = "am start -D -n " + fullAppName; res = ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream()); if (res == null) { return -1; } } String rst = new String(res, ADB_CHARSET).trim(); if (rst.startsWith("Starting: Intent {") && rst.endsWith(fullAppName + " }")) { Thread.sleep(40); String pkg = fullAppName.split("/")[0]; for (Process process : getProcessByPkg(pkg)) { return Integer.parseInt(process.pid); } } return -1; } /** * @return binary output of logcat */ public byte[] getBinaryLogcat() throws IOException { try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) { String cmd = "logcat -dB"; return ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream()); } } /** * @return binary output of logcat after provided timestamp * Timestamp is in the format 09-08 02:18:03.131 */ public byte[] getBinaryLogcat(String timestamp) throws IOException { try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) { Matcher matcher = TIMESTAMP_FORMAT.matcher(timestamp); if (!matcher.find()) { LOG.error("Invalid Logcat Timestamp {}", timestamp); } String cmd = "logcat -dB -t \"" + timestamp + "\""; return ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream()); } } /** * Binary output of logcat -c */ public void clearLogcat() throws IOException { try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) { String cmd = "logcat -c"; ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream()); } } /** * @return Timezone for the attached android device */ public String getTimezone() throws IOException { try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) { String cmd = "getprop persist.sys.timezone"; byte[] tz = ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream()); if (tz == null) { throw new IOException("Failed to get timezone"); } return new String(tz, ADB_CHARSET).trim(); } } public String getAndroidReleaseVersion() { if (!StringUtils.isEmpty(androidReleaseVer)) { return androidReleaseVer; } try { List list = getProp("ro.build.version.release"); if (!list.isEmpty()) { return list.get(0); } LOG.error("Failed to get android release version - no result"); } catch (Exception e) { LOG.error("Failed to get android release version", e); androidReleaseVer = ""; } return androidReleaseVer; } public List getProp(String entry) throws IOException { LOG.debug("ADB getProp({})", entry); try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) { List props = Collections.emptyList(); String cmd = "getprop"; if (!StringUtils.isEmpty(entry)) { cmd += " " + entry; } byte[] payload = ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream()); if (payload != null) { props = new ArrayList<>(); String[] lines = new String(payload, ADB_CHARSET).split("\n"); for (String line : lines) { line = line.trim(); if (!line.isEmpty()) { props.add(line); } } } LOG.trace("ADB getProp({}) = {}", entry, props); return props; } } public List getProcessByPkg(String pkg) throws IOException { return getProcessList("ps | grep " + pkg); } public List getProcessList() throws IOException { return getProcessList("ps"); } private List getProcessList(String cmd) throws IOException { try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) { List procs = new ArrayList<>(); byte[] payload = ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream()); if (payload != null) { String ps = new String(payload, ADB_CHARSET); // LOG.trace("ADB getProcessList({}) = {}", cmd, ps); String[] psLines = ps.split("\n"); for (String line : psLines) { line = line.trim(); if (line.isEmpty()) { continue; } Process proc = Process.make(line); if (proc != null) { procs.add(proc); } else { LOG.error("Unexpected process info data received: \"{}\"", LogUtils.escape(line)); } } } return procs; } } public boolean listenForJDWP(JDWPProcessListener listener) throws IOException { if (this.jdwpListenerSock != null) { return false; } jdwpListenerSock = ADB.connect(this.info.getAdbHost(), this.info.getAdbPort()); InputStream inputStream = jdwpListenerSock.getInputStream(); OutputStream outputStream = jdwpListenerSock.getOutputStream(); if (ADB.setSerial(info.getSerial(), outputStream, inputStream) && ADB.execCommandAsync(outputStream, inputStream, CMD_TRACK_JDWP)) { new Thread(() -> { for (;;) { byte[] res = ADB.readServiceProtocol(inputStream); if (res != null) { if (listener != null) { String payload = new String(res, ADB_CHARSET); String[] ids = payload.split("\n"); Set idList = new HashSet<>(ids.length); for (String id : ids) { if (!id.trim().isEmpty()) { idList.add(id); } } if (idList.isEmpty()) { LOG.info("No debuggable app process found on device {}", info.getSerial()); } listener.jdwpProcessOccurred(this, idList); } } else { // socket disconnected break; } } if (listener != null) { this.jdwpListenerSock = null; listener.jdwpListenerClosed(this); } }).start(); return true; } else { jdwpListenerSock.close(); jdwpListenerSock = null; return false; } } public void stopListenForJDWP() { if (jdwpListenerSock != null) { try { jdwpListenerSock.close(); } catch (Exception e) { LOG.error("JDWP socket close failed", e); } } this.jdwpListenerSock = null; } @Override public int hashCode() { return info.getSerial().hashCode(); } @Override public boolean equals(Object obj) { if (obj instanceof ADBDevice) { String otherSerial = ((ADBDevice) obj).getDeviceInfo().getSerial(); return otherSerial.equals(info.getSerial()); } return false; } @Override public String toString() { return info.getAllInfo(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/device/protocol/ADBDeviceInfo.java ================================================ package jadx.gui.device.protocol; import java.util.Map; import java.util.TreeMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.log.LogUtils; public class ADBDeviceInfo { private static final Logger LOG = LoggerFactory.getLogger(ADBDeviceInfo.class); private final String adbHost; private final int adbPort; private final String serial; private final String state; private final String model; private final String allInfo; /** * Store the device info property values like "device" "model" "product" or "transport_id" */ private final Map propertiesMap = new TreeMap<>(); ADBDeviceInfo(String info, String host, int port) { String[] infoFields = info.trim().split("\\s+"); allInfo = String.join(" ", infoFields); if (infoFields.length > 2) { serial = infoFields[0]; state = infoFields[1]; for (int i = 2; i < infoFields.length; i++) { String field = infoFields[i]; int idx = field.indexOf(':'); if (idx > 0) { String key = field.substring(0, idx); String value = field.substring(idx + 1); if (!value.isEmpty()) { propertiesMap.put(key, value); } } } model = propertiesMap.getOrDefault("model", serial); } else { LOG.error("Unable to extract device information from {}", LogUtils.escape(info)); serial = ""; state = "unknown"; model = "unknown"; } adbHost = host; adbPort = port; } public boolean isOnline() { return state.equals("device"); } public String getAdbHost() { return adbHost; } public int getAdbPort() { return adbPort; } public String getSerial() { return serial; } public String getState() { return state; } public String getModel() { return model; } public String getAllInfo() { return allInfo; } public String getProperty(String key) { return this.propertiesMap.get(key); } @Override public String toString() { return allInfo; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/events/JadxGuiEvents.java ================================================ package jadx.gui.events; import jadx.api.plugins.events.JadxEventType; import jadx.gui.events.types.TreeUpdate; import static jadx.api.plugins.events.JadxEventType.create; public class JadxGuiEvents { public static final JadxEventType TREE_UPDATE = create("TREE_UPDATE"); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/events/services/RenameService.java ================================================ package jadx.gui.events.services; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxDecompiler; import jadx.api.JavaNode; import jadx.api.data.ICodeRename; import jadx.api.data.impl.JadxCodeData; import jadx.api.plugins.events.JadxEvents; import jadx.api.plugins.events.types.NodeRenamedByUser; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.jobs.TaskStatus; import jadx.gui.settings.JadxProject; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JRenameNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.ClassCodeContentPanel; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.CacheObject; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; /** * Rename service listen for user rename events. * For each event: * - add/update rename entry in project code data * - update code and/or invalidate cache for related classes * - apply all needed UI updates (tabs, classes tree) */ public class RenameService { private static final Logger LOG = LoggerFactory.getLogger(RenameService.class); public static void init(MainWindow mainWindow) { RenameService renameService = new RenameService(mainWindow); mainWindow.events().global().addListener(JadxEvents.NODE_RENAMED_BY_USER, renameService::process); } private final MainWindow mainWindow; private RenameService(MainWindow mainWindow) { this.mainWindow = mainWindow; } private void process(NodeRenamedByUser event) { try { LOG.debug("Applying rename event: {}", event); long timeStarted = System.nanoTime(); JRenameNode node = getRenameNode(event); updateCodeRenames(set -> processRename(node, event, set)); refreshState(node, timeStarted); } catch (Exception e) { LOG.error("Rename failed", e); UiUtils.errorMessage(mainWindow, "Rename failed:\n" + Utils.getStackTrace(e)); } } private @NotNull JRenameNode getRenameNode(NodeRenamedByUser event) { Object renameNode = event.getRenameNode(); if (renameNode instanceof JRenameNode) { return (JRenameNode) renameNode; } JadxDecompiler decompiler = mainWindow.getWrapper().getDecompiler(); JavaNode javaNode = decompiler.getJavaNodeByRef(event.getNode()); if (javaNode != null) { JNode node = mainWindow.getCacheObject().getNodeCache().makeFrom(javaNode); if (node instanceof JRenameNode) { return (JRenameNode) node; } } throw new JadxRuntimeException("Failed to resolve node: " + event.getNode()); } private void processRename(JRenameNode node, NodeRenamedByUser event, Set renames) { ICodeRename rename = node.buildCodeRename(event.getNewName(), renames); renames.remove(rename); if (event.isResetName() || event.getNewName().isEmpty()) { node.removeAlias(); } else { renames.add(rename); } } private void updateCodeRenames(Consumer> updater) { JadxProject project = mainWindow.getProject(); JadxCodeData codeData = project.getCodeData(); if (codeData == null) { codeData = new JadxCodeData(); } Set set = new HashSet<>(codeData.getRenames()); updater.accept(set); List list = new ArrayList<>(set); Collections.sort(list); codeData.setRenames(list); project.setCodeData(codeData); } private void refreshState(JRenameNode node, long timeStarted) { List toUpdate = new ArrayList<>(); node.addUpdateNodes(toUpdate); JNodeCache nodeCache = mainWindow.getCacheObject().getNodeCache(); Set updatedTopClasses = toUpdate .stream() .map(JavaNode::getTopParentClass) .map(nodeCache::makeFrom) .filter(Objects::nonNull) .collect(Collectors.toSet()); LOG.debug("Classes to update: {}", updatedTopClasses); if (updatedTopClasses.isEmpty()) { return; } mainWindow.getBackgroundExecutor().execute("Refreshing", () -> { mainWindow.getWrapper().reloadCodeData(); // Reload all the classes in the background process, rather than using the UI thread for // decompilation. We don't just use codeArea.backgroundRefreshClass because it would spawn a // separate background process, whereas we would like it to happen in this one. for (ContentPanel tab : mainWindow.getTabbedPane().getTabs()) { JClass rootClass = tab.getNode().getRootClass(); if (updatedTopClasses.contains(rootClass)) { rootClass.reload(mainWindow.getCacheObject()); } } UiUtils.uiRunAndWait(() -> refreshTabs(mainWindow.getTabbedPane(), updatedTopClasses)); refreshClasses(updatedTopClasses); LOG.debug("Finished rename, took " + (System.nanoTime() - timeStarted) + " ns"); }, (status) -> { if (status == TaskStatus.CANCEL_BY_MEMORY) { mainWindow.showHeapUsageBar(); UiUtils.errorMessage(mainWindow, NLS.str("message.memoryLow")); } node.reload(mainWindow); }); } private void refreshClasses(Set updatedTopClasses) { CacheObject cache = mainWindow.getCacheObject(); if (updatedTopClasses.size() < 10) { // small batch => reload LOG.debug("Classes to reload: {}", updatedTopClasses.size()); for (JClass cls : updatedTopClasses) { try { cls.reload(cache); } catch (Exception e) { LOG.error("Failed to reload class: {}", cls.getFullName(), e); } } } else { // big batch => unload LOG.debug("Classes to unload: {}", updatedTopClasses.size()); for (JClass cls : updatedTopClasses) { try { cls.unload(cache); } catch (Exception e) { LOG.error("Failed to unload class: {}", cls.getFullName(), e); } } } } private void refreshTabs(TabbedPane tabbedPane, Set updatedClasses) { for (ContentPanel tab : tabbedPane.getTabs()) { JClass rootClass = tab.getNode().getRootClass(); if (updatedClasses.remove(rootClass)) { ClassCodeContentPanel contentPanel = (ClassCodeContentPanel) tab; CodeArea codeArea = (CodeArea) contentPanel.getJavaCodePanel().getCodeArea(); codeArea.refreshClass(true); } } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/events/types/JadxGuiEventsImpl.java ================================================ package jadx.gui.events.types; import java.util.function.Consumer; import jadx.api.plugins.events.IJadxEvent; import jadx.api.plugins.events.IJadxEvents; import jadx.api.plugins.events.JadxEventType; import jadx.core.plugins.events.JadxEventsImpl; /** * Special events implementation to operate on both: global UI and project events. * Project events hold listeners only while a project opened and reset them on close. */ public class JadxGuiEventsImpl implements IJadxEvents { private final IJadxEvents global = new JadxEventsImpl(); private final IJadxEvents project = new JadxEventsImpl(); public IJadxEvents global() { return global; } @Override public void send(IJadxEvent event) { global.send(event); project.send(event); } @Override public void addListener(JadxEventType eventType, Consumer listener) { project.addListener(eventType, listener); } @Override public void removeListener(JadxEventType eventType, Consumer listener) { project.removeListener(eventType, listener); } @Override public void reset() { project.reset(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/events/types/TreeUpdate.java ================================================ package jadx.gui.events.types; import jadx.api.plugins.events.IJadxEvent; import jadx.api.plugins.events.JadxEventType; import jadx.gui.events.JadxGuiEvents; import jadx.gui.treemodel.JRoot; public class TreeUpdate implements IJadxEvent { private final JRoot jRoot; public TreeUpdate(JRoot jRoot) { this.jRoot = jRoot; } public JRoot getJRoot() { return jRoot; } @Override public JadxEventType getType() { return JadxGuiEvents.TREE_UPDATE; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java ================================================ package jadx.gui.jobs; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.utils.tasks.ITaskExecutor; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.ProgressPanel; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; /** * Run tasks in the background with progress bar indication. * Use instance created in {@link MainWindow}. */ public class BackgroundExecutor { private static final Logger LOG = LoggerFactory.getLogger(BackgroundExecutor.class); private final JadxSettings settings; private final ProgressUpdater progressUpdater; private ThreadPoolExecutor taskQueueExecutor; private final Map taskRunning = new ConcurrentHashMap<>(); private final AtomicLong idSupplier = new AtomicLong(0); public BackgroundExecutor(JadxSettings settings, ProgressPanel progressPane) { this.settings = Objects.requireNonNull(settings); this.progressUpdater = new ProgressUpdater(progressPane, this::taskCanceled); reset(); } public synchronized void execute(IBackgroundTask task) { InternalTask internalTask = buildTask(task); taskQueueExecutor.execute(() -> runTask(internalTask)); } public synchronized Future executeWithFuture(IBackgroundTask task) { InternalTask internalTask = buildTask(task); return taskQueueExecutor.submit(() -> { runTask(internalTask); return internalTask.getStatus(); }); } public synchronized void cancelAll() { try { taskRunning.values().forEach(this::cancelTask); taskQueueExecutor.shutdownNow(); boolean complete = taskQueueExecutor.awaitTermination(3, TimeUnit.SECONDS); if (complete) { LOG.debug("Background task executor canceled successfully"); } else { String taskNames = taskRunning.values().stream() .map(t -> t.getBgTask().getTitle()) .collect(Collectors.joining(", ")); LOG.debug("Background task executor cancel failed. Running tasks: {}", taskNames); } } catch (Exception e) { LOG.error("Error terminating task executor", e); } finally { reset(); } } public synchronized void waitForComplete() { try { // add empty task and wait its completion taskQueueExecutor.submit(UiUtils.EMPTY_RUNNABLE).get(); } catch (Exception e) { throw new JadxRuntimeException("Failed to wait tasks completion", e); } } public void execute(String title, List backgroundJobs, Consumer onFinishUiRunnable) { execute(new SimpleTask(title, backgroundJobs, onFinishUiRunnable)); } public void execute(String title, Runnable backgroundRunnable, Consumer onFinishUiRunnable) { execute(new SimpleTask(title, Collections.singletonList(backgroundRunnable), onFinishUiRunnable)); } public void execute(String title, Runnable backgroundRunnable) { execute(new SimpleTask(title, Collections.singletonList(backgroundRunnable))); } public void startLoading(Runnable backgroundRunnable, Runnable onFinishUiRunnable) { execute(new SimpleTask(NLS.str("progress.load"), backgroundRunnable, onFinishUiRunnable)); } public void startLoading(Runnable backgroundRunnable) { execute(new SimpleTask(NLS.str("progress.load"), backgroundRunnable)); } private synchronized void reset() { taskQueueExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1, Utils.simpleThreadFactory("bg")); taskRunning.clear(); idSupplier.set(0); } private InternalTask buildTask(IBackgroundTask task) { long id = idSupplier.incrementAndGet(); InternalTask internalTask = new InternalTask(id, task); taskRunning.put(id, internalTask); return internalTask; } private void runTask(InternalTask internalTask) { try { IBackgroundTask task = internalTask.getBgTask(); ITaskExecutor taskExecutor = task.scheduleTasks(); taskExecutor.setThreadsCount(settings.getThreadsCount()); int tasksCount = taskExecutor.getTasksCount(); internalTask.setTaskExecutor(taskExecutor); internalTask.setJobsCount(tasksCount); if (UiUtils.JADX_GUI_DEBUG) { LOG.debug("Starting background task '{}', jobs count: {}, time limit: {} ms, memory check: {}", task.getTitle(), tasksCount, task.timeLimit(), task.checkMemoryUsage()); } long startTime = System.currentTimeMillis(); Supplier cancelCheck = buildCancelCheck(internalTask, startTime); internalTask.taskStart(startTime, cancelCheck); progressUpdater.addTask(internalTask); taskExecutor.execute(); taskExecutor.awaitTermination(); } catch (Exception e) { LOG.error("Task failed", e); internalTask.setStatus(TaskStatus.ERROR); } finally { taskComplete(internalTask); } } private void taskComplete(InternalTask internalTask) { try { IBackgroundTask task = internalTask.getBgTask(); internalTask.setJobsComplete(internalTask.getTaskExecutor().getProgress()); internalTask.setStatus(TaskStatus.COMPLETE); internalTask.updateExecTime(); task.onDone(internalTask); // treat UI task operations as part of the task to not mix with others UiUtils.uiRunAndWait(() -> { try { task.onFinish(internalTask); } catch (Exception e) { LOG.error("Task onFinish failed", e); internalTask.setStatus(TaskStatus.ERROR); } }); } catch (Exception e) { LOG.error("Task complete failed", e); internalTask.setStatus(TaskStatus.ERROR); } finally { internalTask.taskComplete(); progressUpdater.taskComplete(internalTask); removeTask(internalTask); } } private void removeTask(InternalTask internalTask) { taskRunning.remove(internalTask.getId()); } private void cancelTask(InternalTask internalTask) { try { IBackgroundTask task = internalTask.getBgTask(); if (!internalTask.isRunning()) { // task complete or not yet started task.cancel(); removeTask(internalTask); return; } ITaskExecutor taskExecutor = internalTask.getTaskExecutor(); // force termination task.cancel(); taskExecutor.terminate(); ExecutorService executor = taskExecutor.getInternalExecutor(); if (executor == null) { return; } int cancelTimeout = task.getCancelTimeoutMS(); if (cancelTimeout != 0) { if (executor.awaitTermination(cancelTimeout, TimeUnit.MILLISECONDS)) { LOG.debug("Task cancel complete"); return; } } LOG.debug("Forcing tasks cancel"); executor.shutdownNow(); boolean complete = executor.awaitTermination(task.getShutdownTimeoutMS(), TimeUnit.MILLISECONDS); LOG.debug("Forced task cancel status: {}", complete ? "success" : "fail, still active: " + (taskExecutor.getTasksCount() - taskExecutor.getProgress())); } catch (Exception e) { LOG.error("Failed to cancel task: {}", internalTask, e); } } /** * Task cancel notification from progress updater */ private void taskCanceled(InternalTask task) { cancelTask(task); } private Supplier buildCancelCheck(InternalTask internalTask, long startTime) { IBackgroundTask task = internalTask.getBgTask(); int timeLimit = task.timeLimit(); long waitUntilTime = timeLimit == 0 ? 0 : startTime + timeLimit; boolean checkMemoryUsage = task.checkMemoryUsage(); return () -> { if (task.isCanceled() || Thread.currentThread().isInterrupted()) { return TaskStatus.CANCEL_BY_USER; } if (waitUntilTime != 0 && waitUntilTime < System.currentTimeMillis()) { LOG.warn("Task '{}' execution timeout, force cancel", task.getTitle()); return TaskStatus.CANCEL_BY_TIMEOUT; } if (checkMemoryUsage && !UiUtils.isFreeMemoryAvailable()) { LOG.warn("High memory usage: {}", UiUtils.memoryInfo()); if (internalTask.getTaskExecutor().getThreadsCount() == 1) { LOG.warn("Task '{}' memory limit reached, force cancel", task.getTitle()); return TaskStatus.CANCEL_BY_MEMORY; } LOG.warn("Low free memory, reduce processing threads count to 1"); // reduce threads count and continue internalTask.getTaskExecutor().setThreadsCount(1); System.gc(); UiUtils.sleep(1000); // wait GC if (!UiUtils.isFreeMemoryAvailable()) { LOG.error("Task '{}' memory limit reached (after GC), force cancel", task.getTitle()); return TaskStatus.CANCEL_BY_MEMORY; } } return null; }; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/Cancelable.java ================================================ package jadx.gui.jobs; public interface Cancelable { boolean isCanceled(); void cancel(); default int getCancelTimeoutMS() { return 2000; } default int getShutdownTimeoutMS() { return 5000; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/CancelableBackgroundTask.java ================================================ package jadx.gui.jobs; import java.util.concurrent.atomic.AtomicBoolean; public abstract class CancelableBackgroundTask implements IBackgroundTask { private final AtomicBoolean cancel = new AtomicBoolean(false); @Override public boolean isCanceled() { return cancel.get(); } @Override public void cancel() { cancel.set(true); } public void resetCancel() { cancel.set(false); } @Override public boolean canBeCanceled() { return true; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java ================================================ package jadx.gui.jobs; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import javax.swing.JOptionPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeCache; import jadx.api.JavaClass; import jadx.api.utils.tasks.ITaskExecutor; import jadx.commons.app.JadxCommonEnv; import jadx.core.utils.tasks.TaskExecutor; import jadx.gui.JadxWrapper; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; public class DecompileTask extends CancelableBackgroundTask { private static final Logger LOG = LoggerFactory.getLogger(DecompileTask.class); private static final int CLS_LIMIT = JadxCommonEnv.getInt("JADX_CLS_PROCESS_LIMIT", 50); public static int calcDecompileTimeLimit(int classCount) { return classCount * CLS_LIMIT + 5000; } private final MainWindow mainWindow; private final JadxWrapper wrapper; private final AtomicInteger complete = new AtomicInteger(0); private int expectedCompleteCount; private ProcessResult result; public DecompileTask(MainWindow mainWindow) { this.mainWindow = mainWindow; this.wrapper = mainWindow.getWrapper(); } @Override public String getTitle() { return NLS.str("progress.decompile"); } @Override public ITaskExecutor scheduleTasks() { TaskExecutor executor = new TaskExecutor(); executor.addParallelTasks(scheduleJobs()); return executor; } public List scheduleJobs() { if (mainWindow.getCacheObject().isFullDecompilationFinished()) { return Collections.emptyList(); } List classes = wrapper.getIncludedClasses(); expectedCompleteCount = classes.size(); complete.set(0); List> batches; try { batches = wrapper.buildDecompileBatches(classes); } catch (Exception e) { LOG.error("Decompile batches build error", e); return Collections.emptyList(); } return getJobs(batches); } private List getJobs(List> batches) { ICodeCache codeCache = wrapper.getArgs().getCodeCache(); List jobs = new ArrayList<>(batches.size()); for (List batch : batches) { jobs.add(() -> { for (JavaClass cls : batch) { if (isCanceled()) { return; } try { if (!codeCache.contains(cls.getRawName())) { cls.decompile(); } } catch (Throwable e) { LOG.error("Failed to decompile class: {}", cls, e); } finally { complete.incrementAndGet(); } } }); } return jobs; } @Override public void onDone(ITaskInfo taskInfo) { long taskTime = taskInfo.getTime(); long avgPerCls = taskTime / Math.max(expectedCompleteCount, 1); int timeLimit = timeLimit(); int skippedCls = expectedCompleteCount - complete.get(); if (LOG.isInfoEnabled()) { LOG.info("Decompile and index task complete in " + taskTime + " ms (avg " + avgPerCls + " ms per class)" + ", classes: " + expectedCompleteCount + ", skipped: " + skippedCls + ", time limit:{ total: " + timeLimit + "ms, per cls: " + CLS_LIMIT + "ms }" + ", status: " + taskInfo.getStatus()); } result = new ProcessResult(skippedCls, taskInfo.getStatus(), timeLimit); wrapper.unloadClasses(); processDecompilationResults(); System.gc(); mainWindow.getCacheObject().setFullDecompilationFinished(skippedCls == 0); } private void processDecompilationResults() { int skippedCls = result.getSkipped(); if (skippedCls == 0) { return; } TaskStatus status = result.getStatus(); LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status); switch (status) { case CANCEL_BY_USER: { String reason = NLS.str("message.userCancelTask"); String message = NLS.str("message.indexIncomplete", reason, skippedCls); JOptionPane.showMessageDialog(mainWindow, message); break; } case CANCEL_BY_TIMEOUT: { String reason = NLS.str("message.taskTimeout", result.getTimeLimit()); String message = NLS.str("message.indexIncomplete", reason, skippedCls); JOptionPane.showMessageDialog(mainWindow, message); break; } case CANCEL_BY_MEMORY: { mainWindow.showHeapUsageBar(); JOptionPane.showMessageDialog(mainWindow, NLS.str("message.indexingClassesSkipped", skippedCls)); break; } } } @Override public boolean canBeCanceled() { return true; } @Override public int timeLimit() { return calcDecompileTimeLimit(expectedCompleteCount); } @Override public boolean checkMemoryUsage() { return true; } public ProcessResult getResult() { return result; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/ExportTask.java ================================================ package jadx.gui.jobs; import java.io.File; import javax.swing.JOptionPane; import jadx.api.ICodeCache; import jadx.api.utils.tasks.ITaskExecutor; import jadx.gui.JadxWrapper; import jadx.gui.cache.code.CodeCacheMode; import jadx.gui.cache.code.FixedCodeCache; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; public class ExportTask extends CancelableBackgroundTask { private final MainWindow mainWindow; private final JadxWrapper wrapper; private final File saveDir; private int timeLimit; private ICodeCache uiCodeCache; public ExportTask(MainWindow mainWindow, JadxWrapper wrapper, File saveDir) { this.mainWindow = mainWindow; this.wrapper = wrapper; this.saveDir = saveDir; } @Override public String getTitle() { return NLS.str("msg.saving_sources"); } @Override public ITaskExecutor scheduleTasks() { wrapCodeCache(); wrapper.getArgs().setRootDir(saveDir); ITaskExecutor saveTasks = wrapper.getDecompiler().getSaveTaskExecutor(); this.timeLimit = DecompileTask.calcDecompileTimeLimit(saveTasks.getTasksCount()); return saveTasks; } private void wrapCodeCache() { uiCodeCache = wrapper.getArgs().getCodeCache(); if (mainWindow.getSettings().getCodeCacheMode() != CodeCacheMode.DISK) { // do not save newly decompiled code in cache to not increase memory usage // TODO: maybe make memory limited cache? wrapper.getArgs().setCodeCache(new FixedCodeCache(uiCodeCache)); } } @Override public void onFinish(ITaskInfo taskInfo) { // restore initial code cache wrapper.getArgs().setCodeCache(uiCodeCache); if (taskInfo.getJobsSkipped() == 0) { return; } String reason = getIncompleteReason(taskInfo.getStatus()); if (reason != null) { JOptionPane.showMessageDialog(mainWindow, NLS.str("message.saveIncomplete", reason, taskInfo.getJobsSkipped()), NLS.str("message.errorTitle"), JOptionPane.ERROR_MESSAGE); } } private String getIncompleteReason(TaskStatus status) { switch (status) { case CANCEL_BY_USER: return NLS.str("message.userCancelTask"); case CANCEL_BY_TIMEOUT: return NLS.str("message.taskTimeout", timeLimit()); case CANCEL_BY_MEMORY: mainWindow.showHeapUsageBar(); return NLS.str("message.memoryLow"); case ERROR: return NLS.str("message.taskError"); } return null; } @Override public int timeLimit() { return timeLimit; } @Override public boolean canBeCanceled() { return true; } @Override public boolean checkMemoryUsage() { return true; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java ================================================ package jadx.gui.jobs; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import jadx.api.utils.tasks.ITaskExecutor; public interface IBackgroundTask extends Cancelable { String getTitle(); ITaskExecutor scheduleTasks(); /** * Called on executor thread after the all jobs finished. */ default void onDone(ITaskInfo taskInfo) { } /** * Executed on the Event Dispatch Thread after the all jobs finished. */ default void onFinish(ITaskInfo taskInfo) { } default boolean canBeCanceled() { return false; } /** * Global (for all jobs) time limit in milliseconds (0 - to disable). */ default int timeLimit() { return 0; } /** * Executor will check memory usage on every tick and cancel job if no free memory available. */ default boolean checkMemoryUsage() { return false; } /** * Get task progress (Optional) */ default @Nullable ITaskProgress getTaskProgress() { return null; } /** * Return progress notifications listener (use executor tick rate and thread) (Optional) */ default @Nullable Consumer getProgressListener() { return null; } /** * Silent task: don't show progress */ default boolean isSilent() { return false; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/ITaskInfo.java ================================================ package jadx.gui.jobs; public interface ITaskInfo { TaskStatus getStatus(); long getJobsCount(); long getJobsComplete(); long getJobsSkipped(); long getTime(); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/ITaskProgress.java ================================================ package jadx.gui.jobs; public interface ITaskProgress { int progress(); int total(); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/InternalTask.java ================================================ package jadx.gui.jobs; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; import org.jetbrains.annotations.NotNull; import jadx.api.utils.tasks.ITaskExecutor; public class InternalTask implements Delayed, ITaskInfo { private final long id; private final IBackgroundTask bgTask; private final AtomicBoolean running = new AtomicBoolean(false); private final AtomicLong nextUpdate = new AtomicLong(0); private final AtomicBoolean firstUpdate = new AtomicBoolean(true); private long startTime; private long execTime; private Supplier cancelCheck; private TaskStatus status = TaskStatus.WAIT; private ITaskExecutor taskExecutor; private long jobsCount; private long jobsComplete; public InternalTask(long id, IBackgroundTask task) { this.id = id; this.bgTask = task; } public void taskStart(long startTime, Supplier cancelCheck) { this.startTime = startTime; this.cancelCheck = cancelCheck; this.status = TaskStatus.STARTED; this.running.set(true); } public void taskComplete() { this.running.set(false); if (status == TaskStatus.STARTED) { // might be already set to error or cancel this.status = TaskStatus.COMPLETE; } updateExecTime(); } public long getId() { return id; } public IBackgroundTask getBgTask() { return bgTask; } public void setNextUpdate(long nextUpdate) { this.nextUpdate.set(nextUpdate); } public boolean isRunning() { return running.get(); } public boolean checkForFirstUpdate() { return firstUpdate.compareAndExchange(true, false); } public Supplier getCancelCheck() { return cancelCheck; } public long getStartTime() { return startTime; } @Override public TaskStatus getStatus() { return status; } public void setStatus(TaskStatus taskStatus) { this.status = taskStatus; } public ITaskExecutor getTaskExecutor() { return taskExecutor; } public void setTaskExecutor(ITaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } @Override public long getJobsComplete() { return jobsComplete; } public void setJobsComplete(long jobsComplete) { this.jobsComplete = jobsComplete; } @Override public long getJobsCount() { return jobsCount; } public void setJobsCount(long jobsCount) { this.jobsCount = jobsCount; } @Override public long getJobsSkipped() { return jobsCount - jobsComplete; } @Override public long getTime() { return execTime; } public void updateExecTime() { this.execTime = System.currentTimeMillis() - startTime; } @Override public long getDelay(@NotNull TimeUnit unit) { return unit.convert(nextUpdate.get() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } @Override public int compareTo(@NotNull Delayed o) { return Long.compare(nextUpdate.get(), ((InternalTask) o).nextUpdate.get()); } @Override public String toString() { return "InternalTask{" + bgTask.getTitle() + ", status=" + status + ", progress=" + jobsComplete + " of " + jobsCount + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/LoadTask.java ================================================ package jadx.gui.jobs; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; import jadx.api.utils.tasks.ITaskExecutor; import jadx.core.utils.tasks.TaskExecutor; import jadx.gui.utils.NLS; /** * Load task: prepare data in background task and use that data in UI task */ public class LoadTask extends CancelableBackgroundTask { private final String title; private final AtomicReference taskData; private final Runnable bgTask; private final Runnable uiTask; public LoadTask(Supplier loadBgTask, Consumer uiTask) { this(NLS.str("progress.load"), loadBgTask, uiTask); } public LoadTask(String title, Supplier loadBgTask, Consumer uiTask) { this.title = title; this.taskData = new AtomicReference<>(); this.bgTask = () -> taskData.set(loadBgTask.get()); this.uiTask = () -> uiTask.accept(taskData.get()); } @Override public String getTitle() { return title; } @Override public ITaskExecutor scheduleTasks() { TaskExecutor executor = new TaskExecutor(); executor.addSequentialTask(bgTask); return executor; } @Override public void onFinish(ITaskInfo taskInfo) { uiTask.run(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/ProcessResult.java ================================================ package jadx.gui.jobs; public class ProcessResult { private final int skipped; private final TaskStatus status; private final int timeLimit; public ProcessResult(int skipped, TaskStatus status, int timeLimit) { this.skipped = skipped; this.status = status; this.timeLimit = timeLimit; } public int getSkipped() { return skipped; } public TaskStatus getStatus() { return status; } public int getTimeLimit() { return timeLimit; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/ProgressUpdater.java ================================================ package jadx.gui.jobs; import java.util.concurrent.BlockingQueue; import java.util.concurrent.DelayQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.Utils; import jadx.gui.ui.panel.ProgressPanel; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; @SuppressWarnings({ "FieldCanBeLocal", "InfiniteLoopStatement" }) public class ProgressUpdater { private static final Logger LOG = LoggerFactory.getLogger(ProgressUpdater.class); private static final int UPDATE_INTERVAL_MS = 1000; private final ProgressPanel progressPane; private final Consumer cancelCallback; private final ExecutorService bgExecutor = Executors.newSingleThreadExecutor(Utils.simpleThreadFactory("jadx-progress")); private final BlockingQueue tasks = new DelayQueue<>(); public ProgressUpdater(ProgressPanel progressPane, Consumer cancelCallback) { this.progressPane = progressPane; this.cancelCallback = cancelCallback; this.bgExecutor.execute(this::updateLoop); } public void addTask(InternalTask task) { if (task.getBgTask().isSilent()) { return; } scheduleNextUpdate(task); } public void taskComplete(InternalTask task) { task.setNextUpdate(0); updateProgress(task); } private void scheduleNextUpdate(InternalTask task) { task.setNextUpdate(System.currentTimeMillis() + UPDATE_INTERVAL_MS); tasks.add(task); } private void updateLoop() { while (true) { try { InternalTask task = tasks.take(); if (task.isRunning()) { updateProgress(task); cancelCheck(task); scheduleNextUpdate(task); } } catch (Exception e) { LOG.warn("Error in ProgressUpdater loop", e); } } } private void updateProgress(InternalTask internalTask) { UiUtils.uiRun(() -> { IBackgroundTask bgTask = internalTask.getBgTask(); if (internalTask.isRunning()) { if (internalTask.checkForFirstUpdate()) { progressPane.setLabel(bgTask.getTitle() + "… "); progressPane.setCancelButtonVisible(bgTask.canBeCanceled()); progressPane.setVisible(true); } ITaskProgress taskProgress = bgTask.getTaskProgress(); if (taskProgress == null) { int progress = internalTask.getTaskExecutor().getProgress(); taskProgress = new TaskProgress(progress, internalTask.getJobsCount()); } progressPane.setProgress(taskProgress); Consumer onProgressListener = bgTask.getProgressListener(); if (onProgressListener != null) { onProgressListener.accept(taskProgress); } } else { progressPane.reset(); progressPane.setVisible(false); } }); } private void cancelCheck(InternalTask task) { TaskStatus taskStatus = task.getCancelCheck().get(); if (taskStatus == null) { return; } task.setStatus(taskStatus); UiUtils.uiRun(() -> { IBackgroundTask bgTask = task.getBgTask(); progressPane.setLabel(bgTask.getTitle() + " (" + NLS.str("progress.canceling") + ")… "); progressPane.setCancelButtonVisible(false); progressPane.setIndeterminate(true); }); cancelCallback.accept(task); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/SilentTask.java ================================================ package jadx.gui.jobs; import jadx.api.utils.tasks.ITaskExecutor; import jadx.core.utils.tasks.TaskExecutor; /** * Simple and short task, will not show progress */ public class SilentTask extends CancelableBackgroundTask { private final Runnable task; public SilentTask(Runnable backgroundTask) { this.task = backgroundTask; } @Override public boolean isSilent() { return true; } @Override public String getTitle() { return ""; } @Override public ITaskExecutor scheduleTasks() { TaskExecutor executor = new TaskExecutor(); executor.addSequentialTask(task); return executor; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/SimpleTask.java ================================================ package jadx.gui.jobs; import java.util.Collections; import java.util.List; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import jadx.api.utils.tasks.ITaskExecutor; import jadx.core.utils.tasks.TaskExecutor; /** * Simple not cancelable task with memory check */ public class SimpleTask implements IBackgroundTask { private final String title; private final List jobs; private final @Nullable Consumer onFinish; public SimpleTask(String title, Runnable run) { this(title, Collections.singletonList(run), null); } public SimpleTask(String title, Runnable run, Runnable onFinish) { this(title, Collections.singletonList(run), s -> onFinish.run()); } public SimpleTask(String title, List jobs) { this(title, jobs, null); } public SimpleTask(String title, List jobs, @Nullable Consumer onFinish) { this.title = title; this.jobs = jobs; this.onFinish = onFinish; } @Override public String getTitle() { return title; } public List getJobs() { return jobs; } public @Nullable Consumer getOnFinish() { return onFinish; } @Override public ITaskExecutor scheduleTasks() { TaskExecutor executor = new TaskExecutor(); executor.addParallelTasks(jobs); return executor; } @Override public void onFinish(ITaskInfo taskInfo) { if (onFinish != null) { onFinish.accept(taskInfo.getStatus()); } } @Override public boolean checkMemoryUsage() { return true; } @Override public boolean canBeCanceled() { return false; } @Override public boolean isCanceled() { return false; } @Override public void cancel() { } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/TaskProgress.java ================================================ package jadx.gui.jobs; import jadx.gui.utils.UiUtils; public class TaskProgress implements ITaskProgress { private int progress; private int total; public TaskProgress() { this(0, 100); } public TaskProgress(long progress, long total) { this(UiUtils.calcProgress(progress, total), 100); } public TaskProgress(int progress, int total) { this.progress = progress; this.total = total; } @Override public int progress() { return progress; } @Override public int total() { return total; } public void updateProgress(int progress) { this.progress = progress; } public void updateTotal(int total) { this.total = total; } @Override public String toString() { return "TaskProgress{" + progress + " of " + total + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/TaskStatus.java ================================================ package jadx.gui.jobs; public enum TaskStatus { WAIT, STARTED, COMPLETE, CANCEL_BY_USER, CANCEL_BY_TIMEOUT, CANCEL_BY_MEMORY, ERROR } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/jobs/TaskWithExtraOnFinish.java ================================================ package jadx.gui.jobs; import java.util.Objects; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import jadx.api.utils.tasks.ITaskExecutor; /** * Add additional `onFinish` action to the existing task */ public class TaskWithExtraOnFinish implements IBackgroundTask { private final IBackgroundTask task; private final Consumer extraOnFinish; public TaskWithExtraOnFinish(IBackgroundTask task, Runnable extraOnFinish) { this(task, s -> extraOnFinish.run()); } public TaskWithExtraOnFinish(IBackgroundTask task, Consumer extraOnFinish) { this.task = Objects.requireNonNull(task); this.extraOnFinish = Objects.requireNonNull(extraOnFinish); } @Override public void onFinish(ITaskInfo taskInfo) { task.onFinish(taskInfo); extraOnFinish.accept(taskInfo.getStatus()); } @Override public String getTitle() { return task.getTitle(); } @Override public ITaskExecutor scheduleTasks() { return task.scheduleTasks(); } @Override public void onDone(ITaskInfo taskInfo) { task.onDone(taskInfo); } @Override public @Nullable Consumer getProgressListener() { return task.getProgressListener(); } @Override public @Nullable ITaskProgress getTaskProgress() { return task.getTaskProgress(); } @Override public boolean canBeCanceled() { return task.canBeCanceled(); } @Override public boolean isCanceled() { return task.isCanceled(); } @Override public void cancel() { task.cancel(); } @Override public int timeLimit() { return task.timeLimit(); } @Override public boolean checkMemoryUsage() { return task.checkMemoryUsage(); } @Override public int getCancelTimeoutMS() { return task.getCancelTimeoutMS(); } @Override public int getShutdownTimeoutMS() { return task.getShutdownTimeoutMS(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/logs/ILogListener.java ================================================ package jadx.gui.logs; public interface ILogListener { void onAppend(LogEvent logEvent); void onReload(); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/logs/IssuesListener.java ================================================ package jadx.gui.logs; import javax.swing.SwingUtilities; import ch.qos.logback.classic.Level; import jadx.gui.ui.panel.IssuesPanel; import jadx.gui.utils.rx.DebounceUpdate; public class IssuesListener implements ILogListener { private final IssuesPanel issuesPanel; private final DebounceUpdate updater; private int errors = 0; private int warnings = 0; public IssuesListener(IssuesPanel issuesPanel) { this.issuesPanel = issuesPanel; this.updater = new DebounceUpdate(500, this::onUpdate); } private void onUpdate() { SwingUtilities.invokeLater(() -> issuesPanel.onUpdate(errors, warnings)); } @Override public void onAppend(LogEvent logEvent) { switch (logEvent.getLevel().toInt()) { case Level.ERROR_INT: errors++; updater.requestUpdate(); break; case Level.WARN_INT: warnings++; updater.requestUpdate(); break; } } @Override public void onReload() { errors = 0; warnings = 0; updater.requestUpdate(); } public int getErrors() { return errors; } public int getWarnings() { return warnings; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/logs/LimitedQueue.java ================================================ package jadx.gui.logs; import java.util.AbstractQueue; import java.util.ArrayDeque; import java.util.Deque; import java.util.Iterator; public class LimitedQueue extends AbstractQueue { private final Deque deque = new ArrayDeque<>(); private final int limit; public LimitedQueue(int limit) { this.limit = limit; } @Override public Iterator iterator() { return deque.iterator(); } @Override public int size() { return deque.size(); } @Override public boolean offer(T t) { deque.addLast(t); if (deque.size() > limit) { deque.removeFirst(); } return true; } @Override public T poll() { return deque.poll(); } @Override public T peek() { return deque.peek(); } @Override public void clear() { deque.clear(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/logs/LogAppender.java ================================================ package jadx.gui.logs; import org.apache.commons.lang3.StringUtils; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.utils.UiUtils; class LogAppender implements ILogListener { private final LogOptions options; private final RSyntaxTextArea textArea; public LogAppender(LogOptions options, RSyntaxTextArea textArea) { this.options = options; this.textArea = textArea; } @Override public void onAppend(LogEvent logEvent) { if (accept(logEvent)) { UiUtils.uiRun(() -> textArea.append(logEvent.getMsg())); } } @Override public void onReload() { UiUtils.uiRunAndWait(() -> textArea.append(StringUtils.repeat('=', 100) + '\n')); } private boolean accept(LogEvent logEvent) { boolean byLevel = logEvent.getLevel().isGreaterOrEqual(options.getLogLevel()); if (!byLevel) { return false; } switch (options.getMode()) { case ALL: return true; case ALL_SCRIPTS: return logEvent.getLoggerName().startsWith("JadxScript:"); case CURRENT_SCRIPT: return logEvent.getLoggerName().equals(options.getFilter()); default: throw new JadxRuntimeException("Unexpected log mode: " + options.getMode()); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/logs/LogCollector.java ================================================ package jadx.gui.logs; import java.util.ArrayList; import java.util.List; import java.util.Queue; import org.jetbrains.annotations.Nullable; import org.slf4j.LoggerFactory; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.PatternLayout; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; import ch.qos.logback.core.Layout; public class LogCollector extends AppenderBase { public static final int BUFFER_SIZE = 5000; private static final LogCollector INSTANCE = new LogCollector(); public static LogCollector getInstance() { return INSTANCE; } public static void register() { Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); LoggerContext loggerContext = rootLogger.getLoggerContext(); PatternLayout layout = new PatternLayout(); layout.setContext(loggerContext); layout.setPattern("%-5level: %msg%n"); layout.start(); INSTANCE.setContext(loggerContext); INSTANCE.setLayout(layout); INSTANCE.start(); rootLogger.addAppender(INSTANCE); } private final List listeners = new ArrayList<>(); private final Queue buffer = new LimitedQueue<>(BUFFER_SIZE); private Layout layout; public LogCollector() { setName("LogCollector"); } @Override protected synchronized void append(ILoggingEvent event) { String msg = layout.doLayout(event); LogEvent logEvent = new LogEvent(event.getLevel(), event.getLoggerName(), msg); buffer.offer(logEvent); listeners.forEach(l -> l.onAppend(logEvent)); } private void setLayout(Layout layout) { this.layout = layout; } public synchronized void registerListener(ILogListener listener) { listeners.add(listener); buffer.forEach(listener::onAppend); } public synchronized boolean removeListener(@Nullable ILogListener listener) { if (listener == null) { return false; } return this.listeners.removeIf(l -> l == listener); } public synchronized boolean removeListenerByClass(Class listenerCls) { return this.listeners.removeIf(l -> l.getClass().equals(listenerCls)); } public synchronized void reset() { buffer.clear(); listeners.forEach(ILogListener::onReload); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/logs/LogEvent.java ================================================ package jadx.gui.logs; import ch.qos.logback.classic.Level; public final class LogEvent { private final Level level; private final String loggerName; private final String msg; LogEvent(Level level, String loggerName, String msg) { this.level = level; this.loggerName = loggerName; this.msg = msg; } public Level getLevel() { return level; } public String getLoggerName() { return loggerName; } public String getMsg() { return msg; } @Override public String toString() { return level + ": " + loggerName + " - " + msg; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/logs/LogMode.java ================================================ package jadx.gui.logs; import org.apache.commons.lang3.StringUtils; import jadx.gui.utils.NLS; public enum LogMode { ALL, ALL_SCRIPTS, CURRENT_SCRIPT; private static final String[] NLS_STRINGS = StringUtils.split(NLS.str("log_viewer.modes"), '|'); public String getLocalizedName() { return NLS_STRINGS[this.ordinal()]; } @Override public String toString() { return getLocalizedName(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/logs/LogOptions.java ================================================ package jadx.gui.logs; import org.jetbrains.annotations.Nullable; import ch.qos.logback.classic.Level; import jadx.core.utils.Utils; public class LogOptions { /** * Store latest requested log options */ private static LogOptions current = new LogOptions(LogMode.ALL, Level.INFO, null); public static LogOptions allWithLevel(@Nullable Level logLevel) { Level level = Utils.getOrElse(logLevel, current.getLogLevel()); return store(new LogOptions(LogMode.ALL, level, null)); } public static LogOptions forLevel(@Nullable Level logLevel) { Level level = Utils.getOrElse(logLevel, current.getLogLevel()); return store(new LogOptions(current.getMode(), level, current.getFilter())); } public static LogOptions forMode(LogMode mode) { return store(new LogOptions(mode, current.getLogLevel(), current.getFilter())); } public static LogOptions forScript(String scriptName) { String filter = "JadxScript:" + scriptName; return store(new LogOptions(LogMode.CURRENT_SCRIPT, current.getLogLevel(), filter)); } public static LogOptions current() { return current; } private static LogOptions store(LogOptions logOptions) { current = logOptions; return logOptions; } private final LogMode mode; private final Level logLevel; private final @Nullable String filter; private LogOptions(LogMode mode, Level logLevel, @Nullable String filter) { this.mode = mode; this.logLevel = logLevel; this.filter = filter; } public LogMode getMode() { return mode; } public Level getLogLevel() { return logLevel; } public @Nullable String getFilter() { return filter; } @Override public String toString() { return "LogOptions{mode=" + mode + ", logLevel=" + logLevel + ", filter='" + filter + '\'' + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/logs/LogPanel.java ================================================ package jadx.gui.logs; import java.awt.BorderLayout; import java.awt.Dimension; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.event.ChangeListener; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.jetbrains.annotations.Nullable; import ch.qos.logback.classic.Level; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.tab.TabBlueprint; import jadx.gui.utils.NLS; public class LogPanel extends JPanel { private static final long serialVersionUID = -8077649118322056081L; private static final Level[] LEVEL_ITEMS = { Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.OFF }; private final MainWindow mainWindow; private final Runnable dockAction; private final Runnable hideAction; private RSyntaxTextArea textPane; private JComboBox modeCb; private JComboBox levelCb; private ChangeListener activeTabListener; public LogPanel(MainWindow mainWindow, LogOptions logOptions, Runnable dockAction, Runnable hideAction) { this.mainWindow = mainWindow; this.dockAction = dockAction; this.hideAction = hideAction; initUI(logOptions); applyLogOptions(logOptions); } public void applyLogOptions(LogOptions logOptions) { if (logOptions.getMode() == LogMode.CURRENT_SCRIPT) { String scriptName = getCurrentScriptName(); if (scriptName != null) { logOptions = LogOptions.forScript(scriptName); } registerActiveTabListener(); } else { removeActiveTabListener(); } if (modeCb.getSelectedItem() != logOptions.getMode()) { modeCb.setSelectedItem(logOptions.getMode()); } if (levelCb.getSelectedItem() != logOptions.getLogLevel()) { levelCb.setSelectedItem(logOptions.getLogLevel()); } registerLogListener(logOptions); } public void loadSettings() { AbstractCodeArea.loadCommonSettings(mainWindow, textPane); } private void initUI(LogOptions logOptions) { JadxSettings settings = mainWindow.getSettings(); textPane = AbstractCodeArea.getDefaultArea(mainWindow); textPane.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); modeCb = new JComboBox<>(LogMode.values()); modeCb.setSelectedItem(logOptions.getMode()); modeCb.addActionListener(e -> applyLogOptions(LogOptions.forMode((LogMode) modeCb.getSelectedItem()))); JLabel modeLabel = new JLabel(NLS.str("log_viewer.mode")); modeLabel.setLabelFor(modeCb); levelCb = new JComboBox<>(LEVEL_ITEMS); levelCb.setSelectedItem(logOptions.getLogLevel()); levelCb.addActionListener(e -> applyLogOptions(LogOptions.forLevel((Level) levelCb.getSelectedItem()))); JLabel levelLabel = new JLabel(NLS.str("log_viewer.log_level")); levelLabel.setLabelFor(levelCb); JButton clearBtn = new JButton(NLS.str("log_viewer.clear")); clearBtn.addActionListener(ev -> { LogCollector.getInstance().reset(); textPane.setText(""); }); JButton dockBtn = new JButton(NLS.str(settings.isDockLogViewer() ? "log_viewer.undock" : "log_viewer.dock")); dockBtn.addActionListener(ev -> dockAction.run()); JButton hideBtn = new JButton(NLS.str("log_viewer.hide")); hideBtn.addActionListener(ev -> hideAction.run()); JPanel start = new JPanel(); start.setLayout(new BoxLayout(start, BoxLayout.LINE_AXIS)); start.add(modeLabel); start.add(Box.createRigidArea(new Dimension(5, 0))); start.add(modeCb); start.add(Box.createRigidArea(new Dimension(15, 0))); start.add(levelLabel); start.add(Box.createRigidArea(new Dimension(5, 0))); start.add(levelCb); start.add(Box.createRigidArea(new Dimension(5, 0))); JPanel end = new JPanel(); end.setLayout(new BoxLayout(end, BoxLayout.LINE_AXIS)); end.add(clearBtn); end.add(Box.createRigidArea(new Dimension(15, 0))); end.add(dockBtn); end.add(Box.createRigidArea(new Dimension(15, 0))); end.add(hideBtn); JPanel controlPane = new JPanel(); controlPane.setLayout(new BorderLayout()); controlPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); controlPane.add(start, BorderLayout.LINE_START); controlPane.add(end, BorderLayout.LINE_END); JScrollPane scrollPane = new JScrollPane(textPane); setLayout(new BorderLayout(5, 5)); add(controlPane, BorderLayout.PAGE_START); add(scrollPane, BorderLayout.CENTER); } private void registerLogListener(LogOptions logOptions) { LogCollector logCollector = LogCollector.getInstance(); logCollector.removeListenerByClass(LogAppender.class); textPane.setText(""); logCollector.registerListener(new LogAppender(logOptions, textPane)); } private @Nullable String getCurrentScriptName() { TabBlueprint selectedTab = mainWindow.getTabsController().getSelectedTab(); if (selectedTab != null) { JNode node = selectedTab.getNode(); if (node.getClass().getSimpleName().equals("JInputScript")) { // TODO: register custom log filters return node.getName(); } } return null; } private synchronized void registerActiveTabListener() { removeActiveTabListener(); activeTabListener = e -> { String scriptName = getCurrentScriptName(); if (scriptName != null) { applyLogOptions(LogOptions.forScript(scriptName)); } }; mainWindow.getTabbedPane().addChangeListener(activeTabListener); } private synchronized void removeActiveTabListener() { if (activeTabListener != null) { mainWindow.getTabbedPane().removeChangeListener(activeTabListener); activeTabListener = null; } } public void dispose() { LogCollector.getInstance().removeListenerByClass(LogAppender.class); removeActiveTabListener(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java ================================================ package jadx.gui.plugins.context; import java.util.function.Consumer; import java.util.function.Function; import javax.swing.KeyStroke; import org.jetbrains.annotations.Nullable; import jadx.api.metadata.ICodeNodeRef; import jadx.gui.treemodel.JNode; import jadx.gui.ui.action.JNodeAction; import jadx.gui.ui.codearea.CodeArea; public class CodePopupAction { private final String name; private final Function enabledCheck; private final String keyBinding; private final Consumer action; public CodePopupAction(String name, Function enabled, String keyBinding, Consumer action) { this.name = name; this.enabledCheck = enabled; this.keyBinding = keyBinding; this.action = action; } public JNodeAction buildAction(CodeArea codeArea) { return new NodeAction(this, codeArea); } private static class NodeAction extends JNodeAction { private final CodePopupAction data; public NodeAction(CodePopupAction data, CodeArea codeArea) { super(data.name, codeArea); setName(data.name); setShortcutComponent(codeArea); if (data.keyBinding != null) { KeyStroke key = KeyStroke.getKeyStroke(data.keyBinding); if (key == null) { throw new IllegalArgumentException("Failed to parse key stroke: " + data.keyBinding); } setKeyBinding(key); } this.data = data; } @Override public boolean isActionEnabled(@Nullable JNode node) { if (node == null) { return false; } ICodeNodeRef codeNode = node.getCodeNodeRef(); if (codeNode == null) { return false; } return data.enabledCheck.apply(codeNode); } @Override public void runAction(JNode node) { Runnable r = () -> data.action.accept(node.getCodeNodeRef()); getCodeArea().getMainWindow().getBackgroundExecutor().execute(data.name, r); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/context/CommonGuiPluginsContext.java ================================================ package jadx.gui.plugins.context; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.plugins.PluginContext; import jadx.gui.settings.data.ITabStatePersist; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.codearea.JNodePopupBuilder; import jadx.gui.utils.ui.ActionHandler; public class CommonGuiPluginsContext { private static final Logger LOG = LoggerFactory.getLogger(CommonGuiPluginsContext.class); private final MainWindow mainWindow; private final Map pluginsMap = new HashMap<>(); private final List codePopupActionList = new ArrayList<>(); private final List treePopupMenuEntries = new ArrayList<>(); private final List treeInputCategories = new ArrayList<>(); private final List tabStatePersistAdapters = new ArrayList<>(); public CommonGuiPluginsContext(MainWindow mainWindow) { this.mainWindow = mainWindow; } public GuiPluginContext buildForPlugin(PluginContext pluginContext) { GuiPluginContext guiPluginContext = new GuiPluginContext(this, pluginContext); pluginsMap.put(pluginContext, guiPluginContext); return guiPluginContext; } public @Nullable GuiPluginContext getPluginGuiContext(PluginContext pluginContext) { return pluginsMap.get(pluginContext); } public @Nullable GuiPluginContext getGuiPluginContextById(String pluginId) { for (GuiPluginContext guiPluginContext : pluginsMap.values()) { if (guiPluginContext.getPluginContext().getPluginId().equals(pluginId)) { return guiPluginContext; } } return null; } public void reset() { codePopupActionList.clear(); treePopupMenuEntries.clear(); treeInputCategories.clear(); mainWindow.resetPluginsMenu(); } public MainWindow getMainWindow() { return mainWindow; } public List getCodePopupActionList() { return codePopupActionList; } public List getTreePopupMenuEntries() { return treePopupMenuEntries; } public List getTreeInputCategories() { return treeInputCategories; } public List getTabStatePersistAdapters() { return tabStatePersistAdapters; } public void addMenuAction(String name, Runnable action) { ActionHandler item = new ActionHandler(ev -> { try { mainWindow.getBackgroundExecutor().execute(name, action); } catch (Exception e) { LOG.error("Error running action for menu item: {}", name, e); } }); item.setNameAndDesc(name); mainWindow.addToPluginsMenu(item); } public void appendPopupMenus(CodeArea codeArea, JNodePopupBuilder popup) { if (codePopupActionList.isEmpty()) { return; } popup.addSeparator(); for (CodePopupAction codePopupAction : codePopupActionList) { popup.add(codePopupAction.buildAction(codeArea)); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/context/GuiPluginContext.java ================================================ package jadx.gui.plugins.context; import java.awt.Container; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.KeyStroke; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import jadx.api.JavaNode; import jadx.api.gui.tree.ITreeNode; import jadx.api.metadata.ICodeNodeRef; import jadx.api.plugins.events.IJadxEvents; import jadx.api.plugins.events.types.NodeRenamedByUser; import jadx.api.plugins.gui.ISettingsGroup; import jadx.api.plugins.gui.JadxGuiContext; import jadx.api.plugins.gui.JadxGuiSettings; import jadx.core.plugins.PluginContext; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.settings.data.ITabStatePersist; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.codearea.AbstractCodeContentPanel; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.UsageDialog; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.utils.IconsCache; import jadx.gui.utils.UiUtils; public class GuiPluginContext implements JadxGuiContext { private static final Logger LOG = LoggerFactory.getLogger(GuiPluginContext.class); private final CommonGuiPluginsContext commonContext; private final PluginContext pluginContext; private @Nullable ISettingsGroup customSettingsGroup; public GuiPluginContext(CommonGuiPluginsContext commonContext, PluginContext pluginContext) { this.commonContext = commonContext; this.pluginContext = pluginContext; } public CommonGuiPluginsContext getCommonContext() { return commonContext; } public PluginContext getPluginContext() { return pluginContext; } @Override public JFrame getMainFrame() { return commonContext.getMainWindow(); } @Override public void uiRun(Runnable runnable) { UiUtils.uiRun(runnable); } @Override public void addMenuAction(String name, Runnable action) { commonContext.addMenuAction(name, action); } @Override public void addPopupMenuAction(String name, @Nullable Function enabled, @Nullable String keyBinding, Consumer action) { commonContext.getCodePopupActionList().add(new CodePopupAction(name, enabled, keyBinding, action)); } @Override public void addTreePopupMenuEntry(String name, Predicate addPredicate, Consumer action) { commonContext.getTreePopupMenuEntries().add(new TreePopupMenuEntry(name, addPredicate, action)); } public void registerTreeInputCategory(ITreeInputCategory inputCategory) { commonContext.getTreeInputCategories().add(inputCategory); } public void registerTabStatePersistAdapter(ITabStatePersist tabStatePersist) { commonContext.getTabStatePersistAdapters().add(tabStatePersist); } @Override public boolean registerGlobalKeyBinding(String id, String keyBinding, Runnable action) { KeyStroke keyStroke = KeyStroke.getKeyStroke(keyBinding); if (keyStroke == null) { throw new IllegalArgumentException("Failed to parse key binding: " + keyBinding); } JPanel mainPanel = (JPanel) commonContext.getMainWindow().getContentPane(); Object prevBinding = mainPanel.getInputMap().get(keyStroke); if (prevBinding != null) { return false; } UiUtils.addKeyBinding(mainPanel, keyStroke, id, action); return true; } @Override public void copyToClipboard(String str) { UiUtils.copyToClipboard(str); } @Override public JadxGuiSettings settings() { return new GuiSettingsContext(this); } void setCustomSettings(ISettingsGroup customSettingsGroup) { this.customSettingsGroup = customSettingsGroup; } public @Nullable ISettingsGroup getCustomSettingsGroup() { return customSettingsGroup; } @Nullable private CodeArea getCodeArea() { Container contentPane = commonContext.getMainWindow().getTabbedPane().getSelectedContentPanel(); if (contentPane instanceof AbstractCodeContentPanel) { AbstractCodeArea codeArea = ((AbstractCodeContentPanel) contentPane).getCodeArea(); if (codeArea instanceof CodeArea) { return (CodeArea) codeArea; } } return null; } @Override public ImageIcon getSVGIcon(String name) { try { return IconsCache.getSVGIcon(name); } catch (Exception e) { LOG.error("Failed to load icon: {}", name, e); return IconsCache.getSVGIcon("ui/error"); } } @Override public ICodeNodeRef getNodeUnderCaret() { CodeArea codeArea = getCodeArea(); if (codeArea != null) { JNode nodeUnderCaret = codeArea.getNodeUnderCaret(); if (nodeUnderCaret != null) { return nodeUnderCaret.getCodeNodeRef(); } } return null; } @Override public ICodeNodeRef getNodeUnderMouse() { CodeArea codeArea = getCodeArea(); if (codeArea != null) { JNode nodeUnderMouse = codeArea.getNodeUnderMouse(); if (nodeUnderMouse != null) { return nodeUnderMouse.getCodeNodeRef(); } } return null; } @Override public ICodeNodeRef getEnclosingNodeUnderCaret() { CodeArea codeArea = getCodeArea(); if (codeArea != null) { JNode nodeUnderMouse = codeArea.getEnclosingNodeUnderCaret(); if (nodeUnderMouse != null) { return nodeUnderMouse.getCodeNodeRef(); } } return null; } @Override public ICodeNodeRef getEnclosingNodeUnderMouse() { CodeArea codeArea = getCodeArea(); if (codeArea != null) { JNode nodeUnderMouse = codeArea.getEnclosingNodeUnderMouse(); if (nodeUnderMouse != null) { return nodeUnderMouse.getCodeNodeRef(); } } return null; } @Override public boolean open(ICodeNodeRef ref) { commonContext.getMainWindow().getTabsController().codeJump(getJNodeFromRef(ref)); return true; } @Override public void openUsageDialog(ICodeNodeRef ref) { UsageDialog.open(commonContext.getMainWindow(), getJNodeFromRef(ref)); } private JNode getJNodeFromRef(ICodeNodeRef ref) { return commonContext.getMainWindow().getCacheObject().getNodeCache().makeFrom(ref); } @Override public void reloadActiveTab() { UiUtils.uiRun(() -> { CodeArea codeArea = getCodeArea(); if (codeArea != null) { codeArea.refreshClass(); } }); } @Override public void reloadAllTabs() { UiUtils.uiRun(() -> { for (ContentPanel contentPane : commonContext.getMainWindow().getTabbedPane().getTabs()) { if (contentPane instanceof AbstractCodeContentPanel) { AbstractCodeArea codeArea = ((AbstractCodeContentPanel) contentPane).getCodeArea(); if (codeArea instanceof CodeArea) { ((CodeArea) codeArea).refreshClass(); } } } }); } @Override public void applyNodeRename(ICodeNodeRef nodeRef) { JadxDecompiler decompiler = commonContext.getMainWindow().getWrapper().getDecompiler(); JavaNode javaNode = decompiler.getJavaNodeByRef(nodeRef); if (javaNode == null) { throw new JadxRuntimeException("Failed to resolve node ref: " + nodeRef); } String newName; if (javaNode instanceof JavaClass) { // package can have alias newName = javaNode.getFullName(); } else { newName = javaNode.getName(); } IJadxEvents events = commonContext.getMainWindow().events(); events.send(new NodeRenamedByUser(nodeRef, "", newName)); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/context/GuiSettingsContext.java ================================================ package jadx.gui.plugins.context; import java.util.List; import jadx.api.plugins.gui.ISettingsGroup; import jadx.api.plugins.gui.JadxGuiSettings; import jadx.api.plugins.options.OptionDescription; import jadx.gui.settings.ui.SubSettingsGroup; import jadx.gui.settings.ui.plugins.PluginSettings; import jadx.gui.ui.MainWindow; public class GuiSettingsContext implements JadxGuiSettings { private final GuiPluginContext guiPluginContext; public GuiSettingsContext(GuiPluginContext guiPluginContext) { this.guiPluginContext = guiPluginContext; } @Override public void setCustomSettingsGroup(ISettingsGroup group) { guiPluginContext.setCustomSettings(group); } @Override public ISettingsGroup buildSettingsGroupForOptions(String title, List options) { MainWindow mainWindow = guiPluginContext.getCommonContext().getMainWindow(); PluginSettings pluginsSettings = new PluginSettings(mainWindow, mainWindow.getSettings()); SubSettingsGroup settingsGroup = new SubSettingsGroup(title); pluginsSettings.addOptions(settingsGroup, options); return settingsGroup; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/context/ITreeInputCategory.java ================================================ package jadx.gui.plugins.context; import java.nio.file.Path; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import jadx.gui.treemodel.JNode; /** * Custom category for 'Inputs' tree section */ @ApiStatus.Experimental public interface ITreeInputCategory { /** * Check if file should be moved into this category */ boolean filesFilter(Path file); /** * Build node for filtered files. * Can be called with empty list (empty category might be useful) * * @return category node or null if not needed */ @Nullable JNode buildInputNode(List files); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/context/TreePopupMenuEntry.java ================================================ package jadx.gui.plugins.context; import java.util.function.Consumer; import java.util.function.Predicate; import javax.swing.JMenuItem; import org.jetbrains.annotations.Nullable; import jadx.api.gui.tree.ITreeNode; public class TreePopupMenuEntry { private final String name; private final Predicate addPredicate; private final Consumer action; public TreePopupMenuEntry(String name, Predicate addPredicate, Consumer action) { this.name = name; this.addPredicate = addPredicate; this.action = action; } public @Nullable JMenuItem buildEntry(ITreeNode node) { if (!addPredicate.test(node)) { return null; } JMenuItem menuItem = new JMenuItem(name); menuItem.addActionListener(ev -> action.accept(node)); return menuItem; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/mappings/JInputMapping.java ================================================ package jadx.gui.plugins.mappings; import java.nio.file.Path; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JPopupMenu; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.impl.SimpleCodeInfo; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JEditableNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.CodeContentPanel; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.SimpleMenuItem; public class JInputMapping extends JEditableNode { private static final Logger LOG = LoggerFactory.getLogger(JInputMapping.class); private static final ImageIcon MAPPING_ICON = UiUtils.openSvgIcon("nodes/abbreviatePackageNames"); private final Path mappingPath; private final String name; public JInputMapping(Path mappingPath) { this.mappingPath = mappingPath; this.name = mappingPath.getFileName().toString(); } @Override public boolean hasContent() { return true; } @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new CodeContentPanel(tabbedPane, this); } @Override public @NotNull ICodeInfo getCodeInfo() { try { return new SimpleCodeInfo(FileUtils.readFile(mappingPath)); } catch (Exception e) { throw new JadxRuntimeException("Failed to read mapping file: " + mappingPath.toAbsolutePath(), e); } } @Override public void save(String newContent) { try { FileUtils.writeFile(mappingPath, newContent); LOG.debug("Mapping saved: {}", mappingPath.toAbsolutePath()); } catch (Exception e) { throw new JadxRuntimeException("Failed to write mapping file: " + mappingPath.toAbsolutePath(), e); } } @Override public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { JPopupMenu menu = new JPopupMenu(); menu.add(new SimpleMenuItem(NLS.str("popup.remove"), () -> mainWindow.getRenameMappings().closeMappingsAndRemoveFromProject())); return menu; } @Override public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_NONE; } @Override public JClass getJParent() { return null; } @Override public Icon getIcon() { return MAPPING_ICON; } @Override public String getName() { return name; } @Override public String makeString() { return name; } @Override public String getTooltip() { return mappingPath.normalize().toAbsolutePath().toString(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/mappings/RenameMappingsGui.java ================================================ package jadx.gui.plugins.mappings; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.function.Consumer; import java.util.stream.Stream; import javax.swing.Action; import javax.swing.JFileChooser; import javax.swing.JMenu; import javax.swing.JOptionPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.fabricmc.mappingio.MappingReader; import net.fabricmc.mappingio.format.MappingFormat; import jadx.api.args.UserRenamesMappingsMode; import jadx.api.plugins.utils.CommonFileUtils; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; import jadx.gui.jobs.TaskStatus; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JRoot; import jadx.gui.ui.MainWindow; import jadx.gui.ui.filedialog.FileDialogWrapper; import jadx.gui.ui.filedialog.FileOpenMode; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.ActionHandler; import jadx.plugins.mappings.RenameMappingsOptions; import jadx.plugins.mappings.save.MappingExporter; public class RenameMappingsGui { private static final Logger LOG = LoggerFactory.getLogger(RenameMappingsGui.class); private final MainWindow mainWindow; private boolean renamesChanged = false; private JInputMapping mappingNode; private transient JMenu openMappingsMenu; private transient Action saveMappingsAction; private transient JMenu saveMappingsAsMenu; private transient Action closeMappingsAction; public RenameMappingsGui(MainWindow mainWindow) { this.mainWindow = mainWindow; mainWindow.addLoadListener(this::onLoad); mainWindow.addTreeUpdateListener(this::treeUpdate); } public void addMenuActions(JMenu menu) { openMappingsMenu = new JMenu(NLS.str("file.open_mappings")); openMappingsMenu.add(new ActionHandler(ev -> openMappings(MappingFormat.PROGUARD_FILE, true)) .withNameAndDesc("Proguard (inverted)")); openMappingsMenu.add(new ActionHandler(ev -> openMappings(MappingFormat.PROGUARD_FILE, false)).withNameAndDesc("Proguard")); saveMappingsAction = new ActionHandler(this::saveMappings).withNameAndDesc(NLS.str("file.save_mappings")); saveMappingsAsMenu = new JMenu(NLS.str("file.save_mappings_as")); for (MappingFormat mappingFormat : MappingFormat.values()) { if (mappingFormat != MappingFormat.PROGUARD_FILE) { openMappingsMenu.add(new ActionHandler(ev -> openMappings(mappingFormat, false)) .withNameAndDesc(mappingFormat.name)); } saveMappingsAsMenu.add(new ActionHandler(ev -> saveMappingsAs(mappingFormat)) .withNameAndDesc(mappingFormat.name)); } closeMappingsAction = new ActionHandler(ev -> closeMappingsAndRemoveFromProject()) .withNameAndDesc(NLS.str("file.close_mappings")); menu.addSeparator(); menu.add(openMappingsMenu); menu.add(saveMappingsAction); menu.add(saveMappingsAsMenu); menu.add(closeMappingsAction); } private boolean onLoad(boolean loaded) { renamesChanged = false; mappingNode = null; if (loaded) { RootNode rootNode = mainWindow.getWrapper().getRootNode(); rootNode.registerCodeDataUpdateListener(codeData -> onRename()); } else { // project or window close JadxProject project = mainWindow.getProject(); JadxSettings settings = mainWindow.getSettings(); if (project.getMappingsPath() != null && settings.getUserRenamesMappingsMode() == UserRenamesMappingsMode.READ_AND_AUTOSAVE_BEFORE_CLOSING) { saveMappings(); } } return false; } private void onRename() { JadxProject project = mainWindow.getProject(); JadxSettings settings = mainWindow.getSettings(); if (project.getMappingsPath() != null && settings.getUserRenamesMappingsMode() == UserRenamesMappingsMode.READ_AND_AUTOSAVE_EVERY_CHANGE) { saveMappings(); } else { renamesChanged = true; UiUtils.uiRun(mainWindow::update); } } public void onUpdate(boolean loaded) { JadxProject project = mainWindow.getProject(); openMappingsMenu.setEnabled(loaded); saveMappingsAction.setEnabled(loaded && renamesChanged && project.getMappingsPath() != null); saveMappingsAsMenu.setEnabled(loaded); closeMappingsAction.setEnabled(project.getMappingsPath() != null); } private void treeUpdate(JRoot treeRoot) { if (mappingNode != null) { // already added return; } Path mappingsPath = mainWindow.getProject().getMappingsPath(); if (mappingsPath == null) { return; } JNode node = treeRoot.followStaticPath("JInputs"); JNode currentNode = node.removeNode(n -> n.getClass().equals(JInputMapping.class)); if (currentNode != null) { // close opened tab TabbedPane tabbedPane = mainWindow.getTabbedPane(); ContentPanel openedTab = tabbedPane.getTabByNode(currentNode); if (openedTab != null) { tabbedPane.closeCodePanel(openedTab); } } mappingNode = new JInputMapping(mappingsPath); node.add(mappingNode); } private void openMappings(MappingFormat mappingFormat, boolean inverted) { FileDialogWrapper fileDialog = new FileDialogWrapper(mainWindow, FileOpenMode.CUSTOM_OPEN); fileDialog.setTitle(NLS.str("file.open_mappings")); if (mappingFormat.hasSingleFile()) { fileDialog.setFileExtList(Collections.singletonList(mappingFormat.fileExt)); fileDialog.setSelectionMode(JFileChooser.FILES_ONLY); } else { fileDialog.setSelectionMode(JFileChooser.DIRECTORIES_ONLY); } List selectedPaths = fileDialog.show(); if (selectedPaths.size() != 1) { return; } Path filePath = selectedPaths.get(0); LOG.info("Loading mappings from: {}", filePath.toAbsolutePath()); JadxProject project = mainWindow.getProject(); project.setMappingsPath(filePath); project.updatePluginOptions(options -> { options.put(RenameMappingsOptions.FORMAT_OPT, mappingFormat.name()); options.put(RenameMappingsOptions.INVERT_OPT, inverted ? "yes" : "no"); }); mainWindow.reopen(); } public void closeMappingsAndRemoveFromProject() { mainWindow.getProject().setMappingsPath(null); mainWindow.reopen(); } private void saveMappings() { renamesChanged = false; saveInBackground(getCurrentMappingFormat(), mainWindow.getProject().getMappingsPath(), s -> mainWindow.update()); } private void saveMappingsAs(MappingFormat mappingFormat) { FileDialogWrapper fileDialog = new FileDialogWrapper(mainWindow, FileOpenMode.CUSTOM_SAVE); fileDialog.setTitle(NLS.str("file.save_mappings_as")); if (mappingFormat.hasSingleFile()) { Path currentDir = Utils.getOrElse(fileDialog.getCurrentDir(), CommonFileUtils.CWD_PATH); fileDialog.setSelectedFile(currentDir.resolve("mappings." + mappingFormat.fileExt)); fileDialog.setFileExtList(Collections.singletonList(mappingFormat.fileExt)); fileDialog.setSelectionMode(JFileChooser.FILES_ONLY); } else { fileDialog.setSelectionMode(JFileChooser.DIRECTORIES_ONLY); } List selectedPaths = fileDialog.show(); if (selectedPaths.size() != 1) { return; } Path selectedPath = selectedPaths.get(0); Path savePath; // Append file extension if missing if (mappingFormat.hasSingleFile() && !selectedPath.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(mappingFormat.fileExt)) { savePath = selectedPath.resolveSibling(selectedPath.getFileName() + "." + mappingFormat.fileExt); } else { savePath = selectedPath; } // If the target file already exists (and it's not an empty directory), show an overwrite // confirmation if (Files.exists(savePath)) { boolean emptyDir = false; try (Stream entries = Files.list(savePath)) { emptyDir = entries.findFirst().isEmpty(); } catch (IOException ignored) { } if (!emptyDir) { int res = JOptionPane.showConfirmDialog( mainWindow, NLS.str("confirm.save_as_message", savePath.getFileName()), NLS.str("confirm.save_as_title"), JOptionPane.YES_NO_OPTION); if (res == JOptionPane.NO_OPTION) { return; } } } LOG.info("Saving mappings to: {}", savePath.toAbsolutePath()); JadxProject project = mainWindow.getProject(); project.setMappingsPath(savePath); project.updatePluginOptions(options -> { options.put(RenameMappingsOptions.FORMAT_OPT, mappingFormat.name()); options.put(RenameMappingsOptions.INVERT_OPT, "no"); }); saveInBackground(mappingFormat, savePath, s -> { mappingNode = null; mainWindow.reloadTree(); }); } private void saveInBackground(MappingFormat mappingFormat, Path savePath, Consumer onFinishUiRunnable) { mainWindow.getBackgroundExecutor().execute(NLS.str("progress.save_mappings"), () -> new MappingExporter(mainWindow.getWrapper().getRootNode()) .exportMappings(savePath, mainWindow.getProject().getCodeData(), mappingFormat), onFinishUiRunnable); } private MappingFormat getCurrentMappingFormat() { JadxProject project = mainWindow.getProject(); String fmtStr = project.getPluginOption(RenameMappingsOptions.FORMAT_OPT); if (fmtStr != null) { return MappingFormat.valueOf(fmtStr); } Path mappingsPath = project.getMappingsPath(); try { return MappingReader.detectFormat(mappingsPath); } catch (IOException e) { throw new RuntimeException("Failed to detect mapping format for: " + mappingsPath); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkDialog.java ================================================ package jadx.gui.plugins.quark; import java.awt.BorderLayout; import java.awt.Container; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.List; import java.util.stream.Collectors; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.WindowConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.NodeLabel; public class QuarkDialog extends JDialog { private static final long serialVersionUID = 4855753773520368215L; private static final Logger LOG = LoggerFactory.getLogger(QuarkDialog.class); private final transient JadxSettings settings; private final transient MainWindow mainWindow; private final List files; private JComboBox fileSelectCombo; public QuarkDialog(MainWindow mainWindow) { this.mainWindow = mainWindow; this.settings = mainWindow.getSettings(); this.files = filterOpenFiles(mainWindow); if (files.isEmpty()) { UiUtils.errorMessage(mainWindow, "Quark is unable to analyze loaded files"); LOG.error("Quark: The files cannot be analyzed: {}", mainWindow.getProject().getFilePaths()); return; } initUI(); } private List filterOpenFiles(MainWindow mainWindow) { PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**.{apk,dex}"); return mainWindow.getProject().getFilePaths() .stream() .filter(matcher::matches) .collect(Collectors.toList()); } private void initUI() { JLabel description = new JLabel("Analyzing apk using Quark-Engine"); JLabel selectApkText = new JLabel("Select Apk/Dex"); description.setAlignmentX(0.5f); fileSelectCombo = new JComboBox<>(files.toArray(new Path[0])); fileSelectCombo.setRenderer((list, value, index, isSelected, cellHasFocus) -> new NodeLabel(value.getFileName().toString())); JPanel textPane = new JPanel(); textPane.add(description); JPanel selectApkPanel = new JPanel(); selectApkPanel.add(selectApkText); selectApkPanel.add(fileSelectCombo); JPanel buttonPane = new JPanel(); JButton start = new JButton("Start"); JButton close = new JButton("Close"); close.addActionListener(event -> close()); start.addActionListener(event -> startQuarkTasks()); buttonPane.add(start); buttonPane.add(close); getRootPane().setDefaultButton(close); JPanel centerPane = new JPanel(); centerPane.add(selectApkPanel); Container contentPane = getContentPane(); contentPane.add(textPane, BorderLayout.PAGE_START); contentPane.add(centerPane); contentPane.add(buttonPane, BorderLayout.PAGE_END); setTitle("Quark Engine"); pack(); if (!mainWindow.getSettings().loadWindowPos(this)) { setSize(300, 140); } setLocationRelativeTo(null); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setModalityType(ModalityType.APPLICATION_MODAL); UiUtils.addEscapeShortCutToDispose(this); } private void startQuarkTasks() { Path apkFile = (Path) fileSelectCombo.getSelectedItem(); new QuarkManager(mainWindow, apkFile).start(); close(); } private void close() { dispose(); } @Override public void dispose() { settings.saveWindowPos(this); super.dispose(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkManager.java ================================================ package jadx.gui.plugins.quark; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import javax.swing.JOptionPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ch.qos.logback.classic.Level; import jadx.commons.app.JadxSystemInfo; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.jobs.BackgroundExecutor; import jadx.gui.logs.LogOptions; import jadx.gui.treemodel.JRoot; import jadx.gui.ui.MainWindow; import jadx.gui.utils.UiUtils; public class QuarkManager { private static final Logger LOG = LoggerFactory.getLogger(QuarkManager.class); private static final Path QUARK_DIR_PATH = Paths.get(System.getProperty("user.home"), ".quark-engine"); private static final Path VENV_PATH = QUARK_DIR_PATH.resolve("quark_venv"); private static final int LARGE_APK_SIZE = 30; private final MainWindow mainWindow; private final Path apkPath; private boolean useVEnv; private boolean installComplete; private Path reportFile; public QuarkManager(MainWindow mainWindow, Path apkPath) { this.mainWindow = mainWindow; this.apkPath = apkPath; } public void start() { if (!checkFileSize(LARGE_APK_SIZE)) { int result = JOptionPane.showConfirmDialog(mainWindow, "The selected file size is too large (over 30M) that may take a long time to analyze, do you want to continue", "Quark: Warning", JOptionPane.YES_NO_OPTION); if (result == JOptionPane.NO_OPTION) { return; } } BackgroundExecutor executor = mainWindow.getBackgroundExecutor(); executor.execute("Quark install", this::checkInstall, installStatus -> executor.execute("Quark analysis", this::startAnalysis, analysisStatus -> loadReport())); } private void checkInstall() { try { if (checkCommand("quark")) { useVEnv = false; installComplete = true; return; } useVEnv = true; if (checkVEnvCommand("quark")) { installComplete = true; installQuark(); // upgrade quark return; } int result = JOptionPane.showConfirmDialog(mainWindow, "Quark is not installed, do you want to install it from PyPI?", "Warning", JOptionPane.YES_NO_OPTION); if (result == JOptionPane.NO_OPTION) { installComplete = false; return; } createVirtualEnv(); installQuark(); installComplete = true; } catch (Exception e) { UiUtils.errorMessage(mainWindow, e.getMessage()); LOG.error("Failed to install quark", e); installComplete = false; } } private void startAnalysis() { if (!installComplete) { return; } try { updateQuarkRules(); reportFile = Files.createTempFile("QuarkReport-", ".json").toAbsolutePath(); List cmd = new ArrayList<>(); cmd.add(getCommand("quark")); cmd.add("-a"); cmd.add(apkPath.toString()); cmd.add("-o"); cmd.add(reportFile.toString()); runCommand(cmd); } catch (Exception e) { UiUtils.errorMessage(mainWindow, "Failed to execute Quark"); LOG.error("Failed to execute Quark", e); } } private void loadReport() { try { QuarkReportNode quarkNode = new QuarkReportNode(reportFile); JRoot root = mainWindow.getTreeRoot(); root.replaceCustomNode(quarkNode); root.update(); mainWindow.reloadTree(); mainWindow.getTabsController().selectTab(quarkNode); } catch (Exception e) { UiUtils.errorMessage(mainWindow, "Failed to load Quark report."); LOG.error("Failed to load Quark report.", e); } } private void createVirtualEnv() { if (Files.exists(getVenvPath("activate"))) { return; } File directory = QUARK_DIR_PATH.toFile(); if (!directory.isDirectory()) { if (!directory.mkdirs()) { throw new JadxRuntimeException("Failed create directory: " + directory); } } List cmd = new ArrayList<>(); if (JadxSystemInfo.IS_WINDOWS) { cmd.add("python"); cmd.add("-m"); cmd.add("venv"); } else { cmd.add("virtualenv"); } cmd.add(VENV_PATH.toString()); try { runCommand(cmd); } catch (Exception e) { throw new JadxRuntimeException("Failed to create virtual environment", e); } } private void installQuark() { List cmd = new ArrayList<>(); cmd.add(getCommand("pip3")); cmd.add("install"); cmd.add("setuptools"); cmd.add("quark-engine"); cmd.add("--upgrade"); try { runCommand(cmd); } catch (Exception e) { throw new JadxRuntimeException("Failed to install quark-engine", e); } } private void updateQuarkRules() { List cmd = new ArrayList<>(); cmd.add(getCommand("freshquark")); try { runCommand(cmd); } catch (Exception e) { throw new JadxRuntimeException("Failed to update quark rules", e); } } public boolean checkFileSize(int sizeThreshold) { try { int fileSize = (int) Files.size(apkPath) / 1024 / 1024; if (fileSize > sizeThreshold) { return false; } } catch (Exception e) { LOG.error("Failed to calculate file: {}", e.getMessage(), e); return false; } return true; } private String getCommand(String cmd) { if (useVEnv) { return getVenvPath(cmd).toAbsolutePath().toString(); } return cmd; } private boolean checkVEnvCommand(String cmd) { Path venvPath = getVenvPath(cmd); return checkCommand(venvPath.toAbsolutePath().toString()); } private Path getVenvPath(String cmd) { if (JadxSystemInfo.IS_WINDOWS) { return VENV_PATH.resolve("Scripts").resolve(cmd + ".exe"); } return VENV_PATH.resolve("bin").resolve(cmd); } private void runCommand(List cmd) throws Exception { mainWindow.showLogViewer(LogOptions.forLevel(Level.INFO)); LOG.info("Running command: {}", String.join(" ", cmd)); ProcessBuilder builder = new ProcessBuilder(cmd); builder.redirectErrorStream(true); Process process = builder.start(); try (BufferedReader buf = new BufferedReader(new InputStreamReader(process.getInputStream()))) { buf.lines().forEach(msg -> LOG.info("# {}", msg)); } finally { process.waitFor(); } if (process.exitValue() != 0) { throw new RuntimeException("Execution failed (exit code " + process.exitValue() + ") - command " + String.join(" ", cmd) + "\nPlease see command log output what was going wrong."); } } private boolean checkCommand(String... cmd) { try { Process process = Runtime.getRuntime().exec(cmd); process.waitFor(); } catch (Exception e) { return false; } return true; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportData.java ================================================ package jadx.gui.plugins.quark; import java.util.List; import java.util.Map; import com.google.gson.JsonElement; import com.google.gson.annotations.SerializedName; import jadx.core.utils.Utils; @SuppressWarnings("MemberName") public class QuarkReportData { public static class Crime { public String crime; public String confidence; public List permissions; List native_api; List combination; List> register; public int parseConfidence() { return Integer.parseInt(confidence.replace("%", "")); } @Override public String toString() { final StringBuffer sb = new StringBuffer("Crime{"); sb.append("crime='").append(crime).append('\''); sb.append(", confidence='").append(confidence).append('\''); sb.append(", permissions=").append(permissions); sb.append(", native_api=").append(native_api); sb.append(", combination=").append(combination); sb.append(", register=").append(register); sb.append('}'); return sb.toString(); } } public static class Method { @SerializedName("class") String cls; String method; String descriptor; @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(Utils.cleanObjectName(cls)).append(".").append(method); if (descriptor != null) { sb.append(descriptor); } return sb.toString(); } } public static class InvokePlace { List first; List second; } String apk_filename; String threat_level; int total_score; List crimes; public void validate() { if (crimes == null) { throw new RuntimeException("Invalid data: \"crimes\" list missing"); } for (Crime crime : crimes) { if (crime.confidence == null) { throw new RuntimeException("Confidence value missing: " + crime); } try { crime.parseConfidence(); } catch (Exception e) { throw new RuntimeException("Invalid crime entry: " + crime); } } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportNode.java ================================================ package jadx.gui.plugins.quark; import java.io.BufferedReader; import java.nio.file.Files; import java.nio.file.Path; import javax.swing.Icon; import javax.swing.ImageIcon; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.text.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.impl.SimpleCodeInfo; import jadx.core.utils.GsonUtils; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.panel.HtmlPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.UiUtils; public class QuarkReportNode extends JNode { private static final long serialVersionUID = -766800957202637021L; private static final Logger LOG = LoggerFactory.getLogger(QuarkReportNode.class); private static final ImageIcon ICON = UiUtils.openSvgIcon("ui/quark"); private final Path reportFile; private ICodeInfo errorContent; public QuarkReportNode(Path reportFile) { this.reportFile = reportFile; } @Override public JClass getJParent() { return null; } @Override public Icon getIcon() { return ICON; } @Override public String makeString() { return "Quark analysis report"; } @Override public boolean hasContent() { return true; } @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { try { QuarkReportData data; try (BufferedReader reader = Files.newBufferedReader(reportFile)) { data = GsonUtils.buildGson().fromJson(reader, QuarkReportData.class); } data.validate(); return new QuarkReportPanel(tabbedPane, this, data); } catch (Exception e) { LOG.error("Quark report parse error", e); StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); builder.append("

"); builder.escape("Quark analysis failed!"); builder.append("

"); builder.append("

"); builder.append("Error: ").escape(e.getMessage()); builder.append("

"); builder.append("
");
			builder.escape(ExceptionUtils.getStackTrace(e));
			builder.append("
"); errorContent = new SimpleCodeInfo(builder.toString()); return new HtmlPanel(tabbedPane, this); } } @Override public ICodeInfo getCodeInfo() { return errorContent; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkReportPanel.java ================================================ package jadx.gui.plugins.quark; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Font; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Comparator; import java.util.Enumeration; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.JEditorPane; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.ScrollPaneConstants; import javax.swing.SwingUtilities; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeExpansionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.MutableTreeNode; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import org.apache.commons.text.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.beust.jcommander.Strings; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import jadx.api.JavaClass; import jadx.api.JavaMethod; import jadx.core.utils.Utils; import jadx.gui.JadxWrapper; import jadx.gui.treemodel.JMethod; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.ui.NodeLabel; public class QuarkReportPanel extends ContentPanel { private static final long serialVersionUID = -242266836695889206L; private static final Logger LOG = LoggerFactory.getLogger(QuarkReportPanel.class); private final QuarkReportData data; private final JNodeCache nodeCache; private JEditorPane header; private JTree tree; private DefaultMutableTreeNode treeRoot; private Font font; private Font boldFont; private CachingTreeCellRenderer cellRenderer; protected QuarkReportPanel(TabbedPane panel, QuarkReportNode node, QuarkReportData data) { super(panel, node); this.data = data; this.nodeCache = panel.getMainWindow().getCacheObject().getNodeCache(); prepareData(); initUI(); loadSettings(); } private void prepareData() { data.crimes.sort(Comparator.comparingInt(c -> -c.parseConfidence())); } private void initUI() { setLayout(new BorderLayout()); header = new JEditorPane(); header.setContentType("text/html"); header.setEditable(false); header.setText(buildHeader()); cellRenderer = new CachingTreeCellRenderer(); treeRoot = new TextTreeNode("Potential Malicious Activities:").bold(); tree = buildTree(); for (QuarkReportData.Crime crime : data.crimes) { treeRoot.add(new CrimeTreeNode(crime)); } tree.expandRow(0); tree.expandRow(1); JScrollPane tableScroll = new JScrollPane(tree); tableScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout()); mainPanel.add(header, BorderLayout.PAGE_START); mainPanel.add(tableScroll, BorderLayout.CENTER); add(mainPanel); } private JTree buildTree() { JTree tree = new JTree(treeRoot); tree.setLayout(new BorderLayout()); tree.setBorder(BorderFactory.createEmptyBorder()); tree.setShowsRootHandles(false); tree.setScrollsOnExpand(false); tree.setSelectionModel(null); tree.setCellRenderer(cellRenderer); tree.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent event) { if (SwingUtilities.isLeftMouseButton(event)) { Object node = getNodeUnderMouse(tree, event); if (node instanceof MethodTreeNode) { JMethod method = ((MethodTreeNode) node).getJMethod(); tabbedPane.getTabsController().codeJump(method); } } } }); tree.addTreeExpansionListener(new TreeExpansionListener() { @Override public void treeExpanded(TreeExpansionEvent event) { TreePath path = event.getPath(); Object leaf = path.getLastPathComponent(); if (leaf instanceof CrimeTreeNode) { CrimeTreeNode node = (CrimeTreeNode) leaf; Enumeration children = node.children(); while (children.hasMoreElements()) { TreeNode child = children.nextElement(); tree.expandPath(path.pathByAddingChild(child)); } } } @Override public void treeCollapsed(TreeExpansionEvent event) { } }); return tree; } private String buildHeader() { StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); builder.append("

Quark Analysis Report

"); builder.append("

"); builder.append("File: ").append(data.apk_filename); builder.append("
"); builder.append("Treat level: ").append(data.threat_level); builder.append("
"); builder.append("Total score: ").append(Integer.toString(data.total_score)); builder.append("

"); return builder.toString(); } @Override public void loadSettings() { Font settingsFont = getMainWindow().getSettings().getCodeFont(); this.font = settingsFont.deriveFont(settingsFont.getSize2D() + 1.f); this.boldFont = font.deriveFont(Font.BOLD); header.setFont(font); tree.setFont(font); cellRenderer.clearCache(); } private static Object getNodeUnderMouse(JTree tree, MouseEvent mouseEvent) { TreePath path = tree.getPathForLocation(mouseEvent.getX(), mouseEvent.getY()); return path != null ? path.getLastPathComponent() : null; } private static class CachingTreeCellRenderer implements TreeCellRenderer { private final Map cache = new IdentityHashMap<>(); @Override public Component getTreeCellRendererComponent(JTree tr, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean focus) { return cache.computeIfAbsent((BaseTreeNode) value, BaseTreeNode::render); } public void clearCache() { cache.clear(); } } private abstract static class BaseTreeNode extends DefaultMutableTreeNode { private static final long serialVersionUID = 7197501219150495889L; public BaseTreeNode(Object userObject) { super(userObject); } public abstract Component render(); } private class TextTreeNode extends BaseTreeNode { private static final long serialVersionUID = 6763410122501083453L; private boolean bold; public TextTreeNode(String text) { super(text); } public TextTreeNode bold() { bold = true; return this; } @Override public Component render() { JLabel label = new NodeLabel(((String) getUserObject())); label.setFont(bold ? boldFont : font); label.setIcon(null); label.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); return label; } } private class CrimeTreeNode extends TextTreeNode { private static final long serialVersionUID = -1464310215237483911L; private final QuarkReportData.Crime crime; public CrimeTreeNode(QuarkReportData.Crime crime) { super(crime.crime); this.crime = crime; bold(); addDetails(); } private void addDetails() { add(new TextTreeNode("Confidence: " + crime.confidence)); if (Utils.notEmpty(crime.permissions)) { add(new TextTreeNode("Permissions: " + Strings.join(", ", crime.permissions))); } if (Utils.notEmpty(crime.native_api)) { TextTreeNode node = new TextTreeNode("Native API"); for (QuarkReportData.Method method : crime.native_api) { node.add(new TextTreeNode(method.toString())); } add(node); } List combination = crime.combination; if (Utils.notEmpty(combination) && combination.get(0) instanceof JsonArray) { TextTreeNode node = new TextTreeNode("Combination"); int size = combination.size(); for (int i = 0; i < size; i++) { TextTreeNode set = new TextTreeNode("Set " + i); JsonArray array = (JsonArray) combination.get(i); for (JsonElement ele : array) { String mth = ele.getAsString(); set.add(resolveMethod(mth)); } node.add(set); } add(node); } if (Utils.notEmpty(crime.register)) { TextTreeNode node = new TextTreeNode("Invocations"); for (Map invokeMap : crime.register) { invokeMap.forEach((key, value) -> node.add(resolveMethod(key))); } add(node); } } @Override public String toString() { return crime.crime; } } public MutableTreeNode resolveMethod(String descr) { try { String[] parts = removeQuotes(descr).split(" ", 3); String cls = Utils.cleanObjectName(parts[0].replace('$', '.')); String mth = parts[1] + parts[2].replace(" ", ""); MainWindow mainWindow = getMainWindow(); JadxWrapper wrapper = mainWindow.getWrapper(); JavaClass javaClass = wrapper.searchJavaClassByRawName(cls); if (javaClass == null) { return new TextTreeNode(cls + "." + mth); } JavaMethod javaMethod = javaClass.searchMethodByShortId(mth); if (javaMethod == null) { return new TextTreeNode(javaClass.getFullName() + "." + mth); } return new MethodTreeNode(javaMethod); } catch (Exception e) { LOG.error("Failed to parse method descriptor string: {}", descr, e); return new TextTreeNode(descr); } } private static String removeQuotes(String descr) { if (descr.charAt(0) == '\'') { return descr.substring(1, descr.length() - 1); } return descr; } private class MethodTreeNode extends BaseTreeNode { private static final long serialVersionUID = 4350343915220068508L; private final JavaMethod mth; private final JMethod jnode; public MethodTreeNode(JavaMethod mth) { super(mth); this.mth = mth; this.jnode = (JMethod) nodeCache.makeFrom(mth); } public JMethod getJMethod() { return jnode; } @Override public Component render() { JLabel label = new NodeLabel(mth.toString()); label.setFont(font); label.setIcon(jnode.getIcon()); label.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); return label; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/report/ExceptionData.java ================================================ package jadx.gui.report; final class ExceptionData { private final Throwable exception; private final String githubProject; ExceptionData(Throwable exception, String githubProject) { this.exception = exception; this.githubProject = githubProject; } public Throwable getException() { return exception; } public String getGithubProject() { return githubProject; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/report/ExceptionDialog.java ================================================ package jadx.gui.report; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.KeyEvent; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.management.ManagementFactory; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.KeyStroke; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxDecompiler; import jadx.cli.config.JadxConfigAdapter; import jadx.commons.app.JadxSystemInfo; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsData; import jadx.gui.ui.MainWindow; import jadx.gui.utils.LafManager; import jadx.gui.utils.Link; import jadx.gui.utils.TextStandardActions; public class ExceptionDialog extends JDialog { private static final Logger LOG = LoggerFactory.getLogger(ExceptionDialog.class); private static final String FMT_DETAIL_LENGTH = "-13"; ExceptionDialog(MainWindow mainWindow, ExceptionData data) { super(mainWindow, "Jadx Error"); this.getContentPane().setLayout(new BorderLayout()); JPanel titlePanel = new JPanel(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.CENTER; c.gridx = 0; c.weightx = 1.0; c.insets = new Insets(2, 5, 5, 5); JLabel titleLabel = new JLabel("

An error occurred

Jadx encountered an unexpected error.

"); Map details = new LinkedHashMap<>(); details.put("Jadx version", JadxDecompiler.getVersion()); details.put("Java version", JadxSystemInfo.JAVA_VER); details.put("Java VM", String.format("%s %s", System.getProperty("java.vm.vendor", "?"), System.getProperty("java.vm.name", "?"))); details.put("Platform", String.format("%s (%s %s)", JadxSystemInfo.OS_NAME, JadxSystemInfo.OS_VERSION, JadxSystemInfo.OS_ARCH)); Runtime runtime = Runtime.getRuntime(); details.put("Max heap size", String.format("%d MB", runtime.maxMemory() / (1024 * 1024))); try { // TODO: Use ProcessHandle.current().info().commandLine() once min Java is 9+ List args = ManagementFactory.getRuntimeMXBean().getInputArguments(); details.put("Program args", String.join(" ", args)); } catch (Throwable t) { LOG.error("failed to get program arguments", t); } Throwable ex = data.getException(); StringWriter stackTraceWriter = new StringWriter(1024); ex.printStackTrace(new PrintWriter(stackTraceWriter)); final String stackTrace = stackTraceWriter.toString(); String issueTitle; try { issueTitle = URLEncoder.encode(ex.toString(), StandardCharsets.UTF_8); } catch (Exception e) { LOG.error("URL encoding of title failed", e); issueTitle = ex.getClass().getSimpleName(); } String message = "Please describe what you did before the error occurred.\n\n"; message += "**IMPORTANT!** If the error occurs with a specific APK file please attach or provide link to apk file!\n\n"; StringBuilder detailsIssueBuilder = new StringBuilder(); details.forEach((key, value) -> detailsIssueBuilder.append(String.format("* %s: %s\n", key, value))); String body = String.format("%s%s\n```\n%s\n```", message, detailsIssueBuilder, stackTrace); String issueBody; try { issueBody = URLEncoder.encode(body, StandardCharsets.UTF_8); } catch (Exception e) { LOG.error("URL encoding of body failed", e); issueBody = "Please copy the displayed text in the Jadx error dialog and paste it here"; } c.gridy = 0; titlePanel.add(titleLabel, c); String project = data.getGithubProject(); if (!project.isEmpty()) { String url = String.format("https://github.com/%s/issues/new?labels=bug&title=%s&body=%s", project, issueTitle, issueBody); Link issueLink = new Link("Create a new issue at GitHub", url); c.gridy = 1; titlePanel.add(issueLink, c); } JTextArea messageArea = new JTextArea(); TextStandardActions.attach(messageArea); messageArea.setEditable(false); messageArea.setFont(mainWindow.getSettings().getCodeFont().deriveFont(12f)); messageArea.setForeground(Color.BLACK); messageArea.setBackground(Color.WHITE); StringBuilder detailsTextBuilder = new StringBuilder(); details.forEach((key, value) -> detailsTextBuilder.append(String.format("%" + FMT_DETAIL_LENGTH + "s: %s\n", key, value))); messageArea.setText(detailsTextBuilder + "\n" + stackTrace); JPanel buttonPanel = new JPanel(); JButton exitButton = new JButton("Terminate Jadx"); exitButton.addActionListener(event -> System.exit(1)); buttonPanel.add(exitButton); JButton closeButton = new JButton("Go back to Jadx"); closeButton.addActionListener(event -> setVisible(false)); buttonPanel.add(closeButton); JScrollPane messageAreaScroller = new JScrollPane(messageArea); messageAreaScroller.setMinimumSize(new Dimension(600, 400)); messageAreaScroller.setPreferredSize(new Dimension(600, 400)); this.add(titlePanel, BorderLayout.NORTH); this.add(messageAreaScroller, BorderLayout.CENTER); this.add(buttonPanel, BorderLayout.SOUTH); this.pack(); javax.swing.SwingUtilities.invokeLater(() -> messageAreaScroller.getVerticalScrollBar().setValue(0)); final Toolkit toolkit = Toolkit.getDefaultToolkit(); final Dimension screenSize = toolkit.getScreenSize(); final int x = (screenSize.width - getWidth()) / 2; final int y = (screenSize.height - getHeight()) / 2; setLocation(x, y); getRootPane().registerKeyboardAction(event -> setVisible(false), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); this.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); this.setVisible(true); } public static void throwTestException() { try { throw new RuntimeException("Inner exception message"); } catch (Exception e) { throw new JadxRuntimeException("Outer exception message", e); } } public static void showTestExceptionDialog() { try { throwTestException(); } catch (Exception e) { new ExceptionDialog(null, new ExceptionData(e, JadxExceptionHandler.MAIN_PROJECT_STRING)); } } public static void main(String[] args) { JadxConfigAdapter configAdapter = JadxSettings.buildConfigAdapter(); configAdapter.useConfigRef(""); JadxSettingsData settingsData = configAdapter.load(); if (settingsData != null) { JadxSettings settings = new JadxSettings(configAdapter); settings.loadSettingsData(settingsData); LafManager.init(settings); } showTestExceptionDialog(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/report/JadxExceptionHandler.java ================================================ package jadx.gui.report; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.ui.MainWindow; import jadx.plugins.tools.JadxPluginsTools; import jadx.plugins.tools.data.JadxPluginMetadata; import static jadx.plugins.tools.JadxExternalPluginsLoader.JADX_PLUGIN_CLASSLOADER_PREFIX; public class JadxExceptionHandler implements Thread.UncaughtExceptionHandler { private static final Logger LOG = LoggerFactory.getLogger(JadxExceptionHandler.class); public static final String MAIN_PROJECT_STRING = "skylot/jadx"; public static void register(MainWindow mainWindow) { Thread.setDefaultUncaughtExceptionHandler(new JadxExceptionHandler(mainWindow)); } private final MainWindow mainWindow; private JadxExceptionHandler(MainWindow mainWindow) { this.mainWindow = mainWindow; } @Override public void uncaughtException(Thread thread, Throwable ex) { LOG.error("Exception was thrown", ex); new ExceptionDialog(mainWindow, buildExceptionData(ex)); } private ExceptionData buildExceptionData(Throwable ex) { for (StackTraceElement stackTraceElement : ex.getStackTrace()) { String classLoaderName = stackTraceElement.getClassLoaderName(); if (classLoaderName != null && classLoaderName.startsWith(JADX_PLUGIN_CLASSLOADER_PREFIX)) { // plugin exception String jarName = classLoaderName.substring(JADX_PLUGIN_CLASSLOADER_PREFIX.length()); String pluginProject = resolvePluginByJarName(jarName); LOG.debug("Report exception in plugin: {}", pluginProject); return new ExceptionData(ex, pluginProject); } } return new ExceptionData(ex, MAIN_PROJECT_STRING); } private String resolvePluginByJarName(String jarName) { for (JadxPluginMetadata jadxPluginMetadata : JadxPluginsTools.getInstance().getInstalled()) { if (jadxPluginMetadata.getJar().equals(jarName)) { String githubProject = getGithubProject(jadxPluginMetadata.getLocationId()); return githubProject != null ? githubProject : ""; } } return ""; } private static @Nullable String getGithubProject(String locationId) { if (locationId.startsWith("github:")) { return locationId.substring("github:".length()).replace(':', '/'); } return null; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/ISearchMethod.java ================================================ package jadx.gui.search; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; public interface ISearchMethod { int find(String input, String subStr, int start); static ISearchMethod build(SearchSettings searchSettings) { if (searchSettings.isUseRegex()) { Pattern pattern = searchSettings.getPattern(); return (input, subStr, start) -> { Matcher matcher = pattern.matcher(input); if (matcher.find(start)) { return matcher.start(); } return -1; }; } if (searchSettings.isIgnoreCase()) { return StringUtils::indexOfIgnoreCase; } return String::indexOf; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/ISearchProvider.java ================================================ package jadx.gui.search; import org.jetbrains.annotations.Nullable; import jadx.gui.jobs.Cancelable; import jadx.gui.jobs.ITaskProgress; import jadx.gui.treemodel.JNode; public interface ISearchProvider extends ITaskProgress { /** * Return next result or null if search complete */ @Nullable JNode next(Cancelable cancelable); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/SearchJob.java ================================================ package jadx.gui.search; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.treemodel.JNode; public class SearchJob implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(SearchJob.class); private final SearchTask searchTask; private final ISearchProvider provider; public SearchJob(SearchTask task, ISearchProvider provider) { this.searchTask = task; this.provider = provider; } @Override public void run() { while (true) { try { JNode result = provider.next(searchTask); if (result == null) { return; } if (searchTask.addResult(result)) { return; } } catch (Exception e) { LOG.warn("Search error, provider: {}", provider.getClass().getSimpleName(), e); return; } } } public ISearchProvider getProvider() { return provider; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java ================================================ package jadx.gui.search; import java.util.regex.Pattern; import org.jetbrains.annotations.Nullable; import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import jadx.api.JavaPackage; import jadx.core.dex.nodes.PackageNode; import jadx.core.utils.exceptions.InvalidDataException; import jadx.gui.search.providers.ResourceFilter; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JResource; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; public class SearchSettings { private final String searchString; private boolean useRegex; private boolean ignoreCase; private String searchPkgStr; private String resFilterStr; private int resSizeLimit; // in MB private JClass activeCls; private JResource activeResource; private Pattern regexPattern; private ISearchMethod searchMethod; private JavaPackage searchPackage; private ResourceFilter resourceFilter; public SearchSettings(String searchString) { this.searchString = searchString; } public @Nullable String prepare(MainWindow mainWindow) { if (useRegex) { try { int flags = ignoreCase ? Pattern.CASE_INSENSITIVE : 0; this.regexPattern = Pattern.compile(searchString, flags); } catch (Exception e) { return "Invalid Regex: " + e.getMessage(); } } if (!searchPkgStr.isBlank()) { JadxDecompiler decompiler = mainWindow.getWrapper().getDecompiler(); PackageNode pkg = decompiler.getRoot().resolvePackage(searchPkgStr); if (pkg == null) { return NLS.str("search_dialog.package_not_found"); } searchPackage = pkg.getJavaNode(); } searchMethod = ISearchMethod.build(this); try { resourceFilter = ResourceFilter.parse(resFilterStr); } catch (InvalidDataException e) { return "Invalid resource file filter: " + e.getMessage(); } return null; } public boolean isMatch(String searchArea) { return searchMethod.find(searchArea, this.searchString, 0) != -1; } public boolean isUseRegex() { return this.useRegex; } public void setUseRegex(boolean useRegex) { this.useRegex = useRegex; } public boolean isIgnoreCase() { return this.ignoreCase; } public void setIgnoreCase(boolean ignoreCase) { this.ignoreCase = ignoreCase; } public JavaPackage getSearchPackage() { return this.searchPackage; } public boolean isInSearchPkg(JavaClass cls) { return cls.getJavaPackage().isDescendantOf(searchPackage); } public void setSearchPkgStr(String searchPkgStr) { this.searchPkgStr = searchPkgStr; } public String getSearchString() { return this.searchString; } public Pattern getPattern() { return this.regexPattern; } public JClass getActiveCls() { return activeCls; } public void setActiveCls(JClass activeCls) { this.activeCls = activeCls; } public JResource getActiveResource() { return activeResource; } public void setActiveResource(JResource activeResource) { this.activeResource = activeResource; } public ISearchMethod getSearchMethod() { return searchMethod; } public void setResFilterStr(String resFilterStr) { this.resFilterStr = resFilterStr; } public ResourceFilter getResourceFilter() { return resourceFilter; } public int getResSizeLimit() { return resSizeLimit; } public void setResSizeLimit(int resSizeLimit) { this.resSizeLimit = resSizeLimit; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/SearchTask.java ================================================ package jadx.gui.search; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Consumer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.utils.tasks.ITaskExecutor; import jadx.core.utils.tasks.TaskExecutor; import jadx.gui.jobs.BackgroundExecutor; import jadx.gui.jobs.CancelableBackgroundTask; import jadx.gui.jobs.ITaskInfo; import jadx.gui.jobs.ITaskProgress; import jadx.gui.jobs.TaskProgress; import jadx.gui.jobs.TaskStatus; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; public class SearchTask extends CancelableBackgroundTask { private static final Logger LOG = LoggerFactory.getLogger(SearchTask.class); private final BackgroundExecutor backgroundExecutor; private final Consumer resultsListener; private final BiConsumer onFinish; private final List jobs = new ArrayList<>(); private final TaskProgress taskProgress = new TaskProgress(); private final AtomicInteger resultsCount = new AtomicInteger(0); private int resultsLimit; private Future future; private Consumer progressListener; public SearchTask(MainWindow mainWindow, Consumer results, BiConsumer onFinish) { this.backgroundExecutor = mainWindow.getBackgroundExecutor(); this.resultsListener = results; this.onFinish = onFinish; } public void addProviderJob(ISearchProvider provider) { jobs.add(new SearchJob(this, provider)); } public void setResultsLimit(int limit) { this.resultsLimit = limit; } public synchronized void fetchResults() { if (future != null) { throw new IllegalStateException("Previous task not yet finished"); } resetCancel(); resultsCount.set(0); taskProgress.updateTotal(jobs.stream().mapToInt(s -> s.getProvider().total()).sum()); future = backgroundExecutor.executeWithFuture(this); } public synchronized boolean addResult(JNode resultNode) { if (isCanceled()) { // ignore new results after cancel return true; } this.resultsListener.accept(resultNode); if (resultsLimit != 0 && resultsCount.incrementAndGet() >= resultsLimit) { cancel(); return true; } return false; } public synchronized void waitTask() { if (future == null) { return; } try { future.get(200, TimeUnit.MILLISECONDS); } catch (Exception e) { LOG.warn("Search task wait error", e); future.cancel(true); } finally { future = null; } } @Override public String getTitle() { return NLS.str("search_dialog.tip_searching"); } @Override public ITaskExecutor scheduleTasks() { TaskExecutor executor = new TaskExecutor(); executor.addParallelTasks(jobs); return executor; } @Override public void onFinish(ITaskInfo task) { boolean complete = !isCanceled() && task.getStatus() == TaskStatus.COMPLETE && task.getJobsComplete() == task.getJobsCount(); this.onFinish.accept(task, complete); } @Override public boolean checkMemoryUsage() { return true; } @Override public @NotNull ITaskProgress getTaskProgress() { taskProgress.updateProgress(jobs.stream().mapToInt(s -> s.getProvider().progress()).sum()); return taskProgress; } public void setProgressListener(Consumer progressListener) { this.progressListener = progressListener; } @Override public @Nullable Consumer getProgressListener() { return this.progressListener; } @Override public int getCancelTimeoutMS() { return 0; } @Override public int getShutdownTimeoutMS() { return 10; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/providers/BaseSearchProvider.java ================================================ package jadx.gui.search.providers; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import jadx.api.JavaNode; import jadx.core.dex.nodes.ICodeNode; import jadx.gui.search.ISearchMethod; import jadx.gui.search.ISearchProvider; import jadx.gui.search.SearchSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.utils.JNodeCache; public abstract class BaseSearchProvider implements ISearchProvider { private final JNodeCache nodeCache; private final JadxDecompiler decompiler; protected final ISearchMethod searchMth; protected final String searchStr; protected final List classes; protected final SearchSettings searchSettings; public BaseSearchProvider(MainWindow mw, SearchSettings searchSettings, List classes) { this.nodeCache = mw.getCacheObject().getNodeCache(); this.decompiler = mw.getWrapper().getDecompiler(); this.searchMth = searchSettings.getSearchMethod(); this.searchStr = searchSettings.getSearchString(); if (searchSettings.getSearchPackage() != null) { this.classes = classes .stream() .filter(c -> c.getJavaPackage().isDescendantOf(searchSettings.getSearchPackage())) .collect(Collectors.toList()); } else { this.classes = classes; } this.searchSettings = searchSettings; } protected boolean isMatch(String str) { return searchMth.find(str, searchStr, 0) != -1; } protected JNode convert(JavaNode node) { return nodeCache.makeFrom(node); } protected JClass convert(JavaClass cls) { return nodeCache.makeFrom(cls); } protected JNode convert(ICodeNode codeNode) { JavaNode node = Objects.requireNonNull(decompiler.getJavaNodeByRef(codeNode)); return Objects.requireNonNull(nodeCache.makeFrom(node)); } @Override public int total() { return classes.size(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/providers/ClassSearchProvider.java ================================================ package jadx.gui.search.providers; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.JavaClass; import jadx.core.dex.info.ClassInfo; import jadx.gui.jobs.Cancelable; import jadx.gui.search.SearchSettings; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; public final class ClassSearchProvider extends BaseSearchProvider { private int clsNum = 0; public ClassSearchProvider(MainWindow mw, SearchSettings searchSettings, List classes) { super(mw, searchSettings, classes); } @Override public @Nullable JNode next(Cancelable cancelable) { while (true) { if (cancelable.isCanceled() || clsNum >= classes.size()) { return null; } JavaClass curCls = classes.get(clsNum++); if (checkCls(curCls)) { return convert(curCls); } } } private boolean checkCls(JavaClass cls) { ClassInfo clsInfo = cls.getClassNode().getClassInfo(); return isMatch(clsInfo.getShortName()) || isMatch(clsInfo.getFullName()) || isMatch(clsInfo.getAliasFullName()) || isMatch(clsInfo.getRawName()); } @Override public int progress() { return clsNum; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/providers/CodeSearchProvider.java ================================================ package jadx.gui.search.providers; import java.util.List; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeCache; import jadx.api.JavaClass; import jadx.api.JavaNode; import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.ICodeNodeRef; import jadx.api.utils.CodeUtils; import jadx.gui.JadxWrapper; import jadx.gui.jobs.Cancelable; import jadx.gui.search.SearchSettings; import jadx.gui.treemodel.CodeNode; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import static jadx.core.utils.Utils.getOrElse; public final class CodeSearchProvider extends BaseSearchProvider { private static final Logger LOG = LoggerFactory.getLogger(CodeSearchProvider.class); private final ICodeCache codeCache; private final JadxWrapper wrapper; private final @Nullable Set includedClasses; private @Nullable String code; private int clsNum = 0; private int pos = 0; public CodeSearchProvider(MainWindow mw, SearchSettings searchSettings, List classes, @Nullable Set includedClasses) { super(mw, searchSettings, classes); this.codeCache = mw.getWrapper().getArgs().getCodeCache(); this.wrapper = mw.getWrapper(); this.includedClasses = includedClasses; } @Override public @Nullable JNode next(Cancelable cancelable) { Set inclCls = includedClasses; while (true) { if (cancelable.isCanceled() || clsNum >= classes.size()) { return null; } JavaClass cls = classes.get(clsNum); if (inclCls == null || inclCls.contains(cls)) { String clsCode = code; if (clsCode == null && !cls.isInner() && !cls.isNoCode()) { clsCode = getClassCode(cls, codeCache); } if (clsCode != null) { JNode newResult = searchNext(cls, clsCode); if (newResult != null) { code = clsCode; return newResult; } } } else { // force decompilation for not included classes cls.decompile(); } clsNum++; pos = 0; code = null; } } private @Nullable JNode searchNext(JavaClass javaClass, String clsCode) { int newPos = searchMth.find(clsCode, searchStr, pos); if (newPos == -1) { return null; } int lineStart = 1 + CodeUtils.getNewLinePosBefore(clsCode, newPos); int lineEnd = CodeUtils.getNewLinePosAfter(clsCode, newPos); int end = lineEnd == -1 ? clsCode.length() : lineEnd; String line = clsCode.substring(lineStart, end); this.pos = end; JClass rootCls = convert(javaClass); JNode enclosingNode = getOrElse(getEnclosingNode(javaClass, end), rootCls); return new CodeNode(rootCls, enclosingNode, line.trim(), newPos); } private @Nullable JNode getEnclosingNode(JavaClass javaCls, int pos) { try { ICodeMetadata metadata = javaCls.getCodeInfo().getCodeMetadata(); ICodeNodeRef nodeRef = metadata.getNodeAt(pos); JavaNode encNode = wrapper.getJavaNodeByRef(nodeRef); if (encNode != null) { return convert(encNode); } } catch (Exception e) { LOG.debug("Failed to resolve enclosing node", e); } return null; } private String getClassCode(JavaClass javaClass, ICodeCache codeCache) { try { // quick check for if code already in cache String code = codeCache.getCode(javaClass.getRawName()); if (code != null) { return code; } // start decompilation return javaClass.getCode(); } catch (Exception e) { LOG.warn("Failed to get class code: {}", javaClass, e); return ""; } } @Override public int progress() { return clsNum; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/providers/CommentSearchProvider.java ================================================ package jadx.gui.search.providers; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import javax.swing.Icon; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.JavaClass; import jadx.api.JavaField; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.api.data.ICodeComment; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.gui.JadxWrapper; import jadx.gui.jobs.Cancelable; import jadx.gui.search.ISearchProvider; import jadx.gui.search.SearchSettings; import jadx.gui.settings.JadxProject; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.utils.CacheObject; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.JumpPosition; public class CommentSearchProvider implements ISearchProvider { private static final Logger LOG = LoggerFactory.getLogger(CommentSearchProvider.class); private final JadxWrapper wrapper; private final CacheObject cacheObject; private final JadxProject project; private final SearchSettings searchSettings; private final Set searchClsSet; private int progress = 0; public CommentSearchProvider(MainWindow mw, SearchSettings searchSettings, List searchClasses) { this.wrapper = mw.getWrapper(); this.cacheObject = mw.getCacheObject(); this.project = mw.getProject(); this.searchSettings = searchSettings; this.searchClsSet = new HashSet<>(searchClasses); } @Override public @Nullable JNode next(Cancelable cancelable) { while (!cancelable.isCanceled()) { List comments = project.getCodeData().getComments(); if (progress >= comments.size()) { return null; } ICodeComment comment = comments.get(progress++); JNode result = isMatch(searchSettings, comment); if (result != null) { return result; } } return null; } @Nullable private JNode isMatch(SearchSettings searchSettings, ICodeComment comment) { boolean all = searchSettings.getSearchString().isEmpty(); if (all || searchSettings.isMatch(comment.getComment())) { JNode refNode = getRefNode(comment); if (refNode == null) { LOG.warn("Failed to get ref node for comment: {}", comment); return null; } if (searchClsSet.contains(refNode.getRootClass().getCls())) { return getCommentNode(comment, refNode); } } return null; } private @NotNull RefCommentNode getCommentNode(ICodeComment comment, JNode refNode) { IJavaNodeRef nodeRef = comment.getNodeRef(); if (nodeRef.getType() == IJavaNodeRef.RefType.METHOD && comment.getCodeRef() != null) { return new CodeCommentNode((JMethod) refNode, comment); } return new RefCommentNode(refNode, comment.getComment()); } @Nullable private JNode getRefNode(ICodeComment comment) { IJavaNodeRef nodeRef = comment.getNodeRef(); JavaClass javaClass = wrapper.searchJavaClassByOrigClassName(nodeRef.getDeclaringClass()); if (javaClass == null) { return null; } JNodeCache nodeCache = cacheObject.getNodeCache(); switch (nodeRef.getType()) { case CLASS: return nodeCache.makeFrom(javaClass); case FIELD: for (JavaField field : javaClass.getFields()) { if (field.getFieldNode().getFieldInfo().getShortId().equals(nodeRef.getShortId())) { return nodeCache.makeFrom(field); } } break; case METHOD: for (JavaMethod mth : javaClass.getMethods()) { if (mth.getMethodNode().getMethodInfo().getShortId().equals(nodeRef.getShortId())) { return nodeCache.makeFrom(mth); } } break; } return null; } private static final class CodeCommentNode extends RefCommentNode { private static final long serialVersionUID = 6208192811789176886L; private final int offset; private JumpPosition pos; public CodeCommentNode(JMethod node, ICodeComment comment) { super(node, comment.getComment()); IJavaCodeRef codeRef = Objects.requireNonNull(comment.getCodeRef(), "Null comment code ref"); this.offset = codeRef.getIndex(); } @Override public int getPos() { return getCachedPos().getPos(); } private synchronized JumpPosition getCachedPos() { if (pos == null) { pos = getJumpPos(); } return pos; } /** * Lazy decompilation to get comment location if requested */ private JumpPosition getJumpPos() { JavaMethod javaMethod = ((JMethod) node).getJavaMethod(); ICodeInfo codeInfo = javaMethod.getTopParentClass().getCodeInfo(); int methodDefPos = javaMethod.getDefPos(); JumpPosition jump = codeInfo.getCodeMetadata().searchDown(methodDefPos, (pos, ann) -> ann instanceof InsnCodeOffset && ((InsnCodeOffset) ann).getOffset() == offset ? new JumpPosition(node, pos) : null); if (jump != null) { return jump; } return new JumpPosition(node); } } private static class RefCommentNode extends JNode { private static final long serialVersionUID = 3887992236082515752L; protected final JNode node; protected final String comment; public RefCommentNode(JNode node, String comment) { this.node = node; this.comment = comment; } @Override public JClass getRootClass() { return node.getRootClass(); } @Override public JavaNode getJavaNode() { return node.getJavaNode(); } @Override public JClass getJParent() { return node.getJParent(); } @Override public Icon getIcon() { return node.getIcon(); } @Override public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_NONE; // comment is always plain text } @Override public String makeString() { return node.makeString(); } @Override public String makeLongString() { return node.makeLongString(); } @Override public String makeStringHtml() { return node.makeStringHtml(); } @Override public String makeLongStringHtml() { return node.makeLongStringHtml(); } @Override public boolean disableHtml() { return node.disableHtml(); } @Override public int getPos() { return node.getPos(); } @Override public String getTooltip() { return node.getTooltip(); } @Override public String makeDescString() { return comment; } @Override public boolean hasDescString() { return true; } } @Override public int progress() { return progress; } @Override public int total() { return project.getCodeData().getComments().size(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/providers/FieldSearchProvider.java ================================================ package jadx.gui.search.providers; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.JavaClass; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.nodes.FieldNode; import jadx.gui.jobs.Cancelable; import jadx.gui.search.SearchSettings; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; public final class FieldSearchProvider extends BaseSearchProvider { private int clsNum = 0; private int fldNum = 0; public FieldSearchProvider(MainWindow mw, SearchSettings searchSettings, List classes) { super(mw, searchSettings, classes); } @Override public @Nullable JNode next(Cancelable cancelable) { while (true) { if (cancelable.isCanceled()) { return null; } JavaClass cls = classes.get(clsNum); List fields = cls.getClassNode().getFields(); if (fldNum < fields.size()) { FieldNode fld = fields.get(fldNum++); if (checkField(fld.getFieldInfo())) { return convert(fld); } } else { clsNum++; fldNum = 0; if (clsNum >= classes.size()) { return null; } } } } private boolean checkField(FieldInfo fieldInfo) { return isMatch(fieldInfo.getName()) || isMatch(fieldInfo.getAlias()) || isMatch(fieldInfo.getFullId()); } @Override public int progress() { return clsNum; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/providers/MergedSearchProvider.java ================================================ package jadx.gui.search.providers; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.gui.jobs.Cancelable; import jadx.gui.search.ISearchProvider; import jadx.gui.treemodel.JNode; /** * Search provider for sequential execution of nested search providers */ public class MergedSearchProvider implements ISearchProvider { private final List list = new ArrayList<>(); private int current; private int total; public void add(ISearchProvider provider) { list.add(provider); } public boolean isEmpty() { return list.isEmpty(); } public void prepare() { current = list.isEmpty() ? -1 : 0; total = list.stream().mapToInt(ISearchProvider::total).sum(); } @Override public @Nullable JNode next(Cancelable cancelable) { if (current == -1) { return null; } while (true) { JNode next = list.get(current).next(cancelable); if (next != null) { return next; } current++; if (current >= list.size() || cancelable.isCanceled()) { // search complete current = -1; return null; } } } @Override public int progress() { return list.stream().mapToInt(ISearchProvider::progress).sum(); } @Override public int total() { return total; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/providers/MethodSearchProvider.java ================================================ package jadx.gui.search.providers; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.JavaClass; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.nodes.MethodNode; import jadx.gui.jobs.Cancelable; import jadx.gui.search.SearchSettings; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; public final class MethodSearchProvider extends BaseSearchProvider { private int clsNum = 0; private int mthNum = 0; public MethodSearchProvider(MainWindow mw, SearchSettings searchSettings, List classes) { super(mw, searchSettings, classes); } @Override public @Nullable JNode next(Cancelable cancelable) { if (classes.isEmpty()) { return null; } while (true) { if (cancelable.isCanceled()) { return null; } JavaClass cls = classes.get(clsNum); List methods = cls.getClassNode().getMethods(); if (mthNum < methods.size()) { MethodNode mth = methods.get(mthNum++); if (checkMth(mth.getMethodInfo())) { return convert(mth); } } else { clsNum++; mthNum = 0; if (clsNum >= classes.size()) { return null; } } } } private boolean checkMth(MethodInfo mthInfo) { return isMatch(mthInfo.getShortId()) || isMatch(mthInfo.getAlias()) || isMatch(mthInfo.getFullId()) || isMatch(mthInfo.getAliasFullName()); } @Override public int progress() { return clsNum; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/providers/ResourceFilter.java ================================================ package jadx.gui.search.providers; import java.util.ArrayList; import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import jadx.api.resources.ResourceContentType; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.InvalidDataException; import static jadx.api.resources.ResourceContentType.CONTENT_BINARY; import static jadx.api.resources.ResourceContentType.CONTENT_TEXT; public class ResourceFilter { private static final ResourceFilter ANY = new ResourceFilter(Set.of(), Set.of()); private static final String VAR_TEXT = "$TEXT"; private static final String VAR_BIN = "$BIN"; public static final String DEFAULT_STR = VAR_TEXT; public static ResourceFilter parse(String filterStr) { String str = filterStr.trim(); if (str.isEmpty() || str.equals("*")) { return ANY; } Set contentTypes = EnumSet.noneOf(ResourceContentType.class); Set extSet = new LinkedHashSet<>(); String[] parts = filterStr.split("[|, ]"); for (String part : parts) { if (part.isEmpty()) { continue; } if (part.startsWith("$")) { switch (part) { case VAR_TEXT: contentTypes.add(CONTENT_TEXT); break; case VAR_BIN: contentTypes.add(CONTENT_BINARY); break; default: throw new InvalidDataException("Unknown var name: " + part); } } else { extSet.add(part); } } return new ResourceFilter(contentTypes, extSet); } public static String format(ResourceFilter filter) { if (filter.isAnyFile()) { return "*"; } List list = new ArrayList<>(); Set types = filter.getContentTypes(); if (types.contains(CONTENT_TEXT)) { list.add(VAR_TEXT); } if (types.contains(CONTENT_BINARY)) { list.add(VAR_BIN); } list.addAll(filter.getExtSet()); return Utils.listToString(list, "|"); } public static String withContentType(String filterStr, Set contentTypes) { ResourceFilter filter = parse(filterStr); return format(new ResourceFilter(contentTypes, filter.getExtSet())); } private final boolean anyFile; private final Set contentTypes; private final Set extSet; private ResourceFilter(Set contentTypes, Set extSet) { this.anyFile = contentTypes.isEmpty() && extSet.isEmpty(); this.contentTypes = contentTypes.isEmpty() ? Set.of() : contentTypes; this.extSet = extSet.isEmpty() ? Set.of() : extSet; } public boolean isAnyFile() { return anyFile; } public Set getContentTypes() { return contentTypes; } public Set getExtSet() { return extSet; } @Override public String toString() { return format(this); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java ================================================ package jadx.gui.search.providers; import java.util.ArrayDeque; import java.util.Collections; import java.util.Deque; import java.util.Enumeration; import javax.swing.tree.TreeNode; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.api.plugins.utils.CommonFileUtils; import jadx.api.resources.ResourceContentType; import jadx.api.utils.CodeUtils; import jadx.gui.jobs.Cancelable; import jadx.gui.search.ISearchProvider; import jadx.gui.search.SearchSettings; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResSearchNode; import jadx.gui.treemodel.JResource; import jadx.gui.treemodel.JRoot; import jadx.gui.ui.MainWindow; import jadx.gui.ui.dialog.SearchDialog; import jadx.gui.utils.NLS; public class ResourceSearchProvider implements ISearchProvider { private static final Logger LOG = LoggerFactory.getLogger(ResourceSearchProvider.class); private final SearchSettings searchSettings; private final SearchDialog searchDialog; private final ResourceFilter resourceFilter; private final int sizeLimit; /** * Resources queue for process. Using UI nodes to reuse loading cache */ private final Deque resQueue; private int pos; private int loadErrors = 0; private int skipBySize = 0; public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings, SearchDialog searchDialog) { this.searchSettings = searchSettings; this.resourceFilter = searchSettings.getResourceFilter(); this.sizeLimit = searchSettings.getResSizeLimit() * 1024 * 1024; this.searchDialog = searchDialog; JResource activeResource = searchSettings.getActiveResource(); if (activeResource != null) { this.resQueue = new ArrayDeque<>(Collections.singleton(activeResource)); } else { this.resQueue = initResQueue(mw); } } @Override public @Nullable JNode next(Cancelable cancelable) { while (true) { if (cancelable.isCanceled()) { return null; } JResource resNode = getNextResFile(cancelable); if (resNode == null) { return null; } JNode newResult = search(resNode); if (newResult != null) { return newResult; } pos = 0; resQueue.removeLast(); addChildren(resNode); if (resQueue.isEmpty()) { return null; } } } private JNode search(JResource resNode) { String content; try { content = resNode.getCodeInfo().getCodeStr(); } catch (Exception e) { LOG.error("Failed to load resource node content", e); return null; } String searchString = searchSettings.getSearchString(); int newPos = searchSettings.getSearchMethod().find(content, searchString, pos); if (newPos == -1) { return null; } if (resNode.getContentType() == ResourceContentType.CONTENT_TEXT) { int lineStart = 1 + CodeUtils.getNewLinePosBefore(content, newPos); int lineEnd = CodeUtils.getNewLinePosAfter(content, newPos); int end = lineEnd == -1 ? content.length() : lineEnd; String line = content.substring(lineStart, end); this.pos = end; return new JResSearchNode(resNode, line.trim(), newPos); } else { int start = Math.max(0, newPos - 30); int end = Math.min(newPos + 50, content.length()); String line = content.substring(start, end); this.pos = newPos + searchString.length() + 1; return new JResSearchNode(resNode, line, newPos); } } private @Nullable JResource getNextResFile(Cancelable cancelable) { while (true) { JResource node = resQueue.peekLast(); if (node == null || cancelable.isCanceled()) { return null; } if (node.getType() == JResource.JResType.FILE) { if (shouldProcess(node) && loadResNode(node)) { return node; } resQueue.removeLast(); } else { // dir resQueue.removeLast(); loadResNode(node); addChildren(node); } } } private void updateProgressInfo() { StringBuilder sb = new StringBuilder(); if (loadErrors != 0) { sb.append(" ").append(NLS.str("search_dialog.resources_load_errors", loadErrors)); } if (skipBySize != 0) { sb.append(" ").append(NLS.str("search_dialog.resources_skip_by_size", skipBySize)); } if (sb.length() != 0) { sb.append(" ").append(NLS.str("search_dialog.resources_check_logs")); } searchDialog.updateProgressLabel(sb.toString()); } private boolean loadResNode(JResource node) { try { node.loadNode(); return true; } catch (Exception e) { LOG.error("Error load resource node: {}", node, e); loadErrors++; updateProgressInfo(); return false; } } private void addChildren(JResource resNode) { resQueue.addAll(resNode.getSubNodes()); } private static Deque initResQueue(MainWindow mw) { JRoot jRoot = mw.getTreeRoot(); Deque deque = new ArrayDeque<>(jRoot.getChildCount()); Enumeration children = jRoot.children(); while (children.hasMoreElements()) { TreeNode node = children.nextElement(); if (node instanceof JResource) { JResource resNode = (JResource) node; deque.add(resNode); } } return deque; } private boolean shouldProcess(JResource resNode) { if (resNode.getResFile().getType() == ResourceType.ARSC) { // don't check the size of generated resource table, it will also skip all subfiles return resourceFilter.isAnyFile() || resourceFilter.getContentTypes().contains(ResourceContentType.CONTENT_TEXT) || resourceFilter.getExtSet().contains("xml"); } if (!isAllowedFileType(resNode)) { return false; } return isAllowedFileSize(resNode); } private boolean isAllowedFileType(JResource resNode) { ResourceFile resFile = resNode.getResFile(); if (resourceFilter.isAnyFile()) { return true; } ResourceContentType resContentType = resNode.getContentType(); if (resourceFilter.getContentTypes().contains(resContentType)) { return true; } String fileExt = CommonFileUtils.getFileExtension(resFile.getOriginalName()); if (fileExt != null && resourceFilter.getExtSet().contains(fileExt)) { return true; } if (resContentType == ResourceContentType.CONTENT_UNKNOWN && resourceFilter.getContentTypes().contains(ResourceContentType.CONTENT_BINARY)) { // treat unknown file type as binary return true; } return false; } private boolean isAllowedFileSize(JResource resNode) { if (sizeLimit <= 0) { return true; } try { int charsCount = resNode.getCodeInfo().getCodeStr().length(); long size = charsCount * 8L; if (size > sizeLimit) { LOG.info("Resource search skipped because of size limit. Resource '{}' size {} bytes, limit: {}", resNode.getName(), size, sizeLimit); skipBySize++; updateProgressInfo(); return false; } return true; } catch (Exception e) { LOG.warn("Resource load error: {}", resNode, e); loadErrors++; updateProgressInfo(); return false; } } @Override public int progress() { return 0; } @Override public int total() { return 0; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/JadxConfigExcludeExport.java ================================================ package jadx.gui.settings; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Annotation to mark fields excluded from config export/copy actions. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface JadxConfigExcludeExport { } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/JadxGUIArgs.java ================================================ package jadx.gui.settings; import com.beust.jcommander.Parameter; import jadx.cli.JadxCLIArgs; import jadx.cli.config.JadxConfigExclude; public class JadxGUIArgs extends JadxCLIArgs { @JadxConfigExclude @Parameter( names = { "-sc", "--select-class" }, description = "GUI: Open the selected class and show the decompiled code" ) private String cmdSelectClass = null; public String getCmdSelectClass() { return cmdSelectClass; } public void setCmdSelectClass(String cmdSelectClass) { this.cmdSelectClass = cmdSelectClass; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java ================================================ package jadx.gui.settings; import java.io.Reader; import java.io.Writer; 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.Objects; import java.util.StringJoiner; import java.util.function.Consumer; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import jadx.api.JadxArgs; import jadx.api.data.ICodeComment; import jadx.api.data.ICodeRename; import jadx.api.data.IJavaCodeRef; import jadx.api.data.IJavaNodeRef; import jadx.api.data.impl.JadxCodeComment; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxCodeRef; import jadx.api.data.impl.JadxCodeRename; import jadx.api.data.impl.JadxNodeRef; import jadx.api.plugins.utils.CommonFileUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.gui.cache.manager.CacheManager; import jadx.gui.settings.data.ProjectData; import jadx.gui.settings.data.SaveOptionEnum; import jadx.gui.settings.data.TabViewState; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.EditorViewState; import jadx.gui.utils.RelativePathTypeAdapter; import static jadx.core.utils.GsonUtils.defaultGsonBuilder; import static jadx.core.utils.GsonUtils.interfaceReplace; public class JadxProject { private static final Logger LOG = LoggerFactory.getLogger(JadxProject.class); public static final String PROJECT_EXTENSION = "jadx"; private static final int SEARCH_HISTORY_LIMIT = 30; private final transient MainWindow mainWindow; private final transient TabStateViewAdapter tabStateViewAdapter = new TabStateViewAdapter(); private transient String name = "New Project"; private transient @Nullable Path projectPath; private transient boolean initial = true; private transient boolean saved; private final ProjectData data; public JadxProject(MainWindow mainWindow) { this(mainWindow, new ProjectData()); } private JadxProject(MainWindow mainWindow, ProjectData projectData) { this.mainWindow = mainWindow; this.data = Objects.requireNonNull(projectData); } public void fillJadxArgs(JadxArgs jadxArgs) { jadxArgs.setInputFiles(FileUtils.toFiles(getFilePaths())); if (jadxArgs.getUserRenamesMappingsPath() == null) { jadxArgs.setUserRenamesMappingsPath(getMappingsPath()); } jadxArgs.setCodeData(getCodeData()); jadxArgs.getPluginOptions().putAll(data.getPluginOptions()); } public @Nullable Path getWorkingDir() { if (projectPath != null) { return projectPath.toAbsolutePath().getParent(); } List files = data.getFiles(); if (!files.isEmpty()) { Path path = files.get(0); return path.toAbsolutePath().getParent(); } return null; } /** * @return null if project not saved */ public @Nullable Path getProjectPath() { return projectPath; } private void setProjectPath(@NotNull Path projectPath) { this.projectPath = projectPath; this.name = CommonFileUtils.removeFileExtension(projectPath.getFileName().toString()); changed(); } public List getFilePaths() { return data.getFiles(); } public void setFilePaths(List files) { if (files.equals(getFilePaths())) { return; } if (files.isEmpty()) { data.setFiles(files); name = ""; } else { Collections.sort(files); data.setFiles(files); StringJoiner joiner = new StringJoiner("_"); for (Path p : files) { Path fileNamePart = p.getFileName(); if (fileNamePart == null) { joiner.add(p.toString()); continue; } String fileName = fileNamePart.toString(); if (!fileName.endsWith(".jadx.kts")) { joiner.add(CommonFileUtils.removeFileExtension(fileName)); } } String joinedName = joiner.toString(); name = StringUtils.abbreviate(joinedName, 100); } changed(); } public void setTreeExpansions(List list) { if (list.equals(data.getTreeExpansionsV2())) { return; } data.setTreeExpansionsV2(list); changed(); } public List getTreeExpansions() { return data.getTreeExpansionsV2(); } public JadxCodeData getCodeData() { return data.getCodeData(); } public void setCodeData(JadxCodeData codeData) { data.setCodeData(codeData); changed(); } public void saveOpenTabs(List tabs) { List tabStateList = tabs.stream() .map(tabStateViewAdapter::build) .filter(Objects::nonNull) .collect(Collectors.toList()); if (data.setOpenTabs(tabStateList)) { changed(); } } public List getOpenTabs(MainWindow mw) { tabStateViewAdapter.setCustomAdapters(mw.getWrapper().getGuiPluginsContext().getTabStatePersistAdapters()); return data.getOpenTabs().stream() .map(s -> tabStateViewAdapter.load(mw, s)) .filter(Objects::nonNull) .collect(Collectors.toList()); } public Path getMappingsPath() { return data.getMappingsPath(); } public void setMappingsPath(Path mappingsPath) { data.setMappingsPath(mappingsPath); changed(); } /** * Do not expose options map directly to be able to intercept changes */ public void updatePluginOptions(Consumer> update) { update.accept(data.getPluginOptions()); changed(); } public @Nullable String getPluginOption(String key) { return data.getPluginOptions().get(key); } private Path cacheDir; public Path getCacheDir() { if (cacheDir == null) { cacheDir = resolveCachePath(data.getCacheDir()); } return cacheDir; } public void resetCacheDir() { cacheDir = resolveCachePath(null); } private Path resolveCachePath(@Nullable String cacheDirStr) { CacheManager cacheManager = mainWindow.getCacheManager(); Path newCacheDir = cacheManager.getCacheDir(this, cacheDirStr); String newCacheStr = cacheManager.buildCacheDirStr(newCacheDir); if (!newCacheStr.equals(cacheDirStr)) { data.setCacheDir(newCacheStr); changed(); } return newCacheDir; } public boolean isEnableLiveReload() { return data.isEnableLiveReload(); } public void setEnableLiveReload(boolean newValue) { if (newValue != data.isEnableLiveReload()) { data.setEnableLiveReload(newValue); changed(); } } public List getSearchHistory() { return data.getSearchHistory(); } public void addToSearchHistory(String str) { if (str == null || str.isEmpty()) { return; } List list = data.getSearchHistory(); if (!list.isEmpty() && list.get(0).equals(str)) { return; } list.remove(str); list.add(0, str); if (list.size() > SEARCH_HISTORY_LIMIT) { list.remove(list.size() - 1); } data.setSearchHistory(list); changed(); } public void setSearchResourcesFilter(String searchResourcesFilter) { data.setSearchResourcesFilter(searchResourcesFilter); } public String getSearchResourcesFilter() { return data.getSearchResourcesFilter(); } public void setSearchResourcesSizeLimit(int searchResourcesSizeLimit) { data.setSearchResourcesSizeLimit(searchResourcesSizeLimit); } public int getSearchResourcesSizeLimit() { return data.getSearchResourcesSizeLimit(); } private void changed() { JadxSettings settings = mainWindow.getSettings(); if (settings != null && settings.getSaveOption() == SaveOptionEnum.ALWAYS) { save(); } else { saved = false; } initial = false; mainWindow.updateProject(this); } public String getName() { return name; } public boolean isSaveFileSelected() { return projectPath != null; } public boolean isSaved() { return saved; } public boolean isInitial() { return initial; } public void saveAs(Path path) { mainWindow.getCacheManager().projectPathUpdate(this, path); setProjectPath(path); save(); } public void save() { Path savePath = getProjectPath(); if (savePath != null) { Path basePath = savePath.toAbsolutePath().getParent(); try (Writer writer = Files.newBufferedWriter(savePath, StandardCharsets.UTF_8)) { buildGson(basePath).toJson(data, writer); saved = true; } catch (Exception e) { throw new RuntimeException("Error saving project", e); } } } public static JadxProject load(MainWindow mainWindow, Path path) { ProjectData projectData = loadProjectData(path); JadxProject project = new JadxProject(mainWindow, projectData); project.saved = true; project.setProjectPath(path); return project; } public static ProjectData loadProjectData(Path path) { Path basePath = path.toAbsolutePath().getParent(); try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { return buildGson(basePath).fromJson(reader, ProjectData.class); } catch (Exception e) { throw new JadxRuntimeException("Failed to load project file: " + path, e); } } private static Gson buildGson(Path basePath) { return defaultGsonBuilder() .registerTypeHierarchyAdapter(Path.class, new RelativePathTypeAdapter(basePath)) .registerTypeAdapter(ICodeComment.class, interfaceReplace(JadxCodeComment.class)) .registerTypeAdapter(ICodeRename.class, interfaceReplace(JadxCodeRename.class)) .registerTypeAdapter(IJavaNodeRef.class, interfaceReplace(JadxNodeRef.class)) .registerTypeAdapter(IJavaCodeRef.class, interfaceReplace(JadxCodeRef.class)) .create(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java ================================================ package jadx.gui.settings; import java.awt.Font; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Rectangle; import java.awt.Window; import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.JFrame; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.ExclusionStrategy; import com.google.gson.FieldAttributes; import com.google.gson.Gson; import jadx.api.CommentsLevel; import jadx.api.DecompilationMode; import jadx.api.JadxArgs; import jadx.api.args.GeneratedRenamesMappingFileMode; import jadx.api.args.IntegerFormat; import jadx.api.args.ResourceNameSource; import jadx.api.args.UseSourceNameAsClassNameAlias; import jadx.api.args.UserRenamesMappingsMode; import jadx.cli.config.JadxConfigAdapter; import jadx.cli.config.JadxConfigExclude; import jadx.core.utils.GsonUtils; import jadx.gui.cache.code.CodeCacheMode; import jadx.gui.cache.usage.UsageCacheMode; import jadx.gui.settings.data.SaveOptionEnum; import jadx.gui.settings.data.ShortcutsWrapper; import jadx.gui.settings.font.FontSettings; import jadx.gui.ui.MainWindow; import jadx.gui.ui.tab.dnd.TabDndGhostType; import jadx.gui.utils.LangLocale; import jadx.gui.utils.PathTypeAdapter; import jadx.gui.utils.RectangleTypeAdapter; import static jadx.gui.settings.JadxSettingsData.CURRENT_SETTINGS_VERSION; public class JadxSettings { private static final Logger LOG = LoggerFactory.getLogger(JadxSettings.class); private static final int RECENT_PROJECTS_COUNT = 30; private final JadxConfigAdapter configAdapter; private final Object dataWriteSync = new Object(); private final ShortcutsWrapper shortcutsWrapper = new ShortcutsWrapper(); private final FontSettings fontSettings = new FontSettings(); private JadxSettingsData settingsData; public JadxSettings(JadxConfigAdapter configAdapter) { this.configAdapter = configAdapter; } public static JadxConfigAdapter buildConfigAdapter() { return new JadxConfigAdapter<>(JadxSettingsData.class, "gui", gsonBuilder -> { gsonBuilder.registerTypeHierarchyAdapter(Path.class, PathTypeAdapter.singleton()); gsonBuilder.registerTypeHierarchyAdapter(Rectangle.class, RectangleTypeAdapter.singleton()); }); } public String getSettingsJsonString() { return configAdapter.objectToJsonString(settingsData); } public void loadSettingsFromJsonString(String jsonStr) { loadSettingsData(configAdapter.jsonStringToObject(jsonStr)); } public void loadSettingsData(JadxSettingsData settingsData) { this.settingsData = settingsData; upgradeSettings(settingsData.getSettingsVersion()); fixOnLoad(); // update custom fields shortcutsWrapper.updateShortcuts(settingsData.getShortcuts()); fontSettings.bindData(settingsData); } private void upgradeSettings(int fromVersion) { if (settingsData.getSettingsVersion() == CURRENT_SETTINGS_VERSION) { return; } LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION); if (fromVersion <= 22) { fromVersion++; } if (fromVersion != CURRENT_SETTINGS_VERSION) { LOG.warn("Incorrect settings upgrade. Expected version: {}, got: {}", CURRENT_SETTINGS_VERSION, fromVersion); } settingsData.setSettingsVersion(CURRENT_SETTINGS_VERSION); sync(); } private void fixOnLoad() { if (settingsData.getThreadsCount() <= 0) { settingsData.setThreadsCount(JadxArgs.DEFAULT_THREADS_COUNT); } if (settingsData.getDeobfuscationMinLength() < 0) { settingsData.setDeobfuscationMinLength(0); } if (settingsData.getDeobfuscationMaxLength() < 0) { settingsData.setDeobfuscationMaxLength(0); } } public void sync() { synchronized (dataWriteSync) { configAdapter.save(settingsData); } } public String exportSettingsString() { Gson gson = GsonUtils.defaultGsonBuilder() .setExclusionStrategies(new ExclusionStrategy() { @Override public boolean shouldSkipField(FieldAttributes f) { return f.getAnnotation(JadxConfigExclude.class) != null || f.getAnnotation(JadxConfigExcludeExport.class) != null; } @Override public boolean shouldSkipClass(Class clazz) { return false; } }) .create(); return gson.toJson(settingsData); } public JadxArgs toJadxArgs() { return settingsData.toJadxArgs(); } public List getFiles() { return settingsData.getFiles(); } public String getCmdSelectClass() { return settingsData.getCmdSelectClass(); } public Path getLastOpenFilePath() { return settingsData.getLastOpenFilePath(); } public void setLastOpenFilePath(Path lastOpenFilePath) { settingsData.setLastOpenFilePath(lastOpenFilePath); } public Path getLastSaveProjectPath() { return settingsData.getLastSaveProjectPath(); } public void setLastSaveProjectPath(Path lastSaveProjectPath) { settingsData.setLastSaveProjectPath(lastSaveProjectPath); } public Path getLastSaveFilePath() { return settingsData.getLastSaveFilePath(); } public void setLastSaveFilePath(Path lastSaveFilePath) { settingsData.setLastSaveFilePath(lastSaveFilePath); } public boolean isFlattenPackage() { return settingsData.isFlattenPackage(); } public void setFlattenPackage(boolean flattenPackage) { settingsData.setFlattenPackage(flattenPackage); } public boolean isCheckForUpdates() { return settingsData.isCheckForUpdates(); } public void setCheckForUpdates(boolean checkForUpdates) { settingsData.setCheckForUpdates(checkForUpdates); sync(); } public boolean isDisableTooltipOnHover() { return settingsData.isDisableTooltipOnHover(); } public void setDisableTooltipOnHover(boolean disableTooltipOnHover) { settingsData.setDisableTooltipOnHover(disableTooltipOnHover); } public List getRecentProjects() { return Collections.unmodifiableList(settingsData.getRecentProjects()); } public void addRecentProject(@Nullable Path projectPath) { if (projectPath == null) { return; } List recentProjects = settingsData.getRecentProjects(); Path normPath = projectPath.toAbsolutePath().normalize(); recentProjects.remove(normPath); recentProjects.add(0, normPath); int count = recentProjects.size(); if (count > RECENT_PROJECTS_COUNT) { recentProjects.subList(RECENT_PROJECTS_COUNT, count).clear(); } } public void removeRecentProject(Path projectPath) { List recentProjects = settingsData.getRecentProjects(); recentProjects.remove(projectPath); } private static String makeWindowId(Window window) { return window.getClass().getSimpleName(); } @SuppressWarnings("ConstantValue") public void saveWindowPos(Window window) { if (window == null) { return; } synchronized (dataWriteSync) { Rectangle bounds = window.getBounds(); if (bounds != null) { WindowLocation pos = new WindowLocation(makeWindowId(window), bounds); settingsData.getWindowPos().put(pos.getWindowId(), pos); } } } public boolean loadWindowPos(Window window) { String windowId = makeWindowId(window); WindowLocation pos = settingsData.getWindowPos().get(windowId); if (pos == null) { return false; } Rectangle bounds = pos.getBounds(); if (bounds == null || !isAccessibleInAnyScreen(windowId, bounds)) { return false; } window.setBounds(bounds); if (window instanceof MainWindow) { ((JFrame) window).setExtendedState(getMainWindowExtendedState()); } return true; } private static boolean isAccessibleInAnyScreen(String windowId, Rectangle windowBounds) { for (GraphicsDevice gd : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { Rectangle screenBounds = gd.getDefaultConfiguration().getBounds(); if (screenBounds.intersects(windowBounds)) { return true; } } LOG.debug("Window saved position was ignored: {}, bounds: {}", windowId, windowBounds); return false; } public int getMainWindowExtendedState() { return settingsData.getMainWindowExtendedState(); } public void setMainWindowExtendedState(int mainWindowExtendedState) { settingsData.setMainWindowExtendedState(mainWindowExtendedState); } public boolean isShowHeapUsageBar() { return settingsData.isShowHeapUsageBar(); } public void setShowHeapUsageBar(boolean showHeapUsageBar) { settingsData.setShowHeapUsageBar(showHeapUsageBar); } public boolean isAlwaysSelectOpened() { return settingsData.isAlwaysSelectOpened(); } public void setAlwaysSelectOpened(boolean alwaysSelectOpened) { settingsData.setAlwaysSelectOpened(alwaysSelectOpened); } public boolean isEnablePreviewTab() { return settingsData.isEnablePreviewTab(); } public void setEnablePreviewTab(boolean enablePreviewTab) { settingsData.setEnablePreviewTab(enablePreviewTab); } public boolean isUseAlternativeFileDialog() { return settingsData.isUseAlternativeFileDialog(); } public void setUseAlternativeFileDialog(boolean useAlternativeFileDialog) { settingsData.setUseAlternativeFileDialog(useAlternativeFileDialog); } public String getExcludedPackages() { return settingsData.getExcludedPackages(); } public void setExcludedPackages(String excludedPackages) { settingsData.setExcludedPackages(excludedPackages); } public LangLocale getLangLocale() { return settingsData.getLangLocale(); } public void setLangLocale(LangLocale langLocale) { settingsData.setLangLocale(langLocale); } public boolean isAutoStartJobs() { return settingsData.isAutoStartJobs(); } public void setAutoStartJobs(boolean autoStartJobs) { settingsData.setAutoStartJobs(autoStartJobs); } public ShortcutsWrapper getShortcuts() { return shortcutsWrapper; } public int getTreeWidth() { return settingsData.getTreeWidth(); } public void setTreeWidth(int treeWidth) { settingsData.setTreeWidth(treeWidth); } public float getUiZoom() { return settingsData.getUiZoom(); } public void setUiZoom(float uiZoom) { settingsData.setUiZoom(uiZoom); fontSettings.applyUiZoom(uiZoom, isApplyUiZoomToFonts()); } public boolean isApplyUiZoomToFonts() { return settingsData.isApplyUiZoomToFonts(); } public void setApplyUiZoomToFonts(boolean applyUiZoomToFonts) { settingsData.setApplyUiZoomToFonts(applyUiZoomToFonts); fontSettings.applyUiZoom(getUiZoom(), applyUiZoomToFonts); } public FontSettings getFontSettings() { return fontSettings; } public Font getUiFont() { return fontSettings.getUiFontAdapter().getEffectiveFont(); } public void setUiFont(Font font) { fontSettings.getUiFontAdapter().setFont(font); } public Font getCodeFont() { return fontSettings.getCodeFontAdapter().getEffectiveFont(); } public void setCodeFont(Font font) { fontSettings.getCodeFontAdapter().setFont(font); } public Font getSmaliFont() { return fontSettings.getSmaliFontAdapter().getEffectiveFont(); } public void setSmaliFont(Font font) { fontSettings.getSmaliFontAdapter().setFont(font); } public String getEditorTheme() { return settingsData.getEditorTheme(); } public void setEditorTheme(String editorTheme) { settingsData.setEditorTheme(editorTheme); } public String getLafTheme() { return settingsData.getLafTheme(); } public void setLafTheme(String lafTheme) { settingsData.setLafTheme(lafTheme); } public boolean isCodeAreaLineWrap() { return settingsData.isCodeAreaLineWrap(); } public void setCodeAreaLineWrap(boolean lineWrap) { settingsData.setCodeAreaLineWrap(lineWrap); } public int getSearchResultsPerPage() { return settingsData.getSearchResultsPerPage(); } public void setSearchResultsPerPage(int searchResultsPerPage) { settingsData.setSearchResultsPerPage(searchResultsPerPage); } public boolean isUseAutoSearch() { return settingsData.isUseAutoSearch(); } public void saveUseAutoSearch(boolean useAutoSearch) { settingsData.setUseAutoSearch(useAutoSearch); sync(); } public void saveKeepCommonDialogOpen(boolean keepCommonDialogOpen) { settingsData.setKeepCommonDialogOpen(keepCommonDialogOpen); sync(); } public boolean isKeepCommonDialogOpen() { return settingsData.isKeepCommonDialogOpen(); } public int getMainWindowVerticalSplitterLoc() { return settingsData.getMainWindowVerticalSplitterLoc(); } public void setMainWindowVerticalSplitterLoc(int location) { settingsData.setMainWindowVerticalSplitterLoc(location); } public int getDebuggerStackFrameSplitterLoc() { return settingsData.getDebuggerStackFrameSplitterLoc(); } public void setDebuggerStackFrameSplitterLoc(int location) { settingsData.setDebuggerStackFrameSplitterLoc(location); } public int getDebuggerVarTreeSplitterLoc() { return settingsData.getDebuggerVarTreeSplitterLoc(); } public void setDebuggerVarTreeSplitterLoc(int location) { settingsData.setDebuggerVarTreeSplitterLoc(location); } public String getAdbDialogHost() { return settingsData.getAdbDialogHost(); } public void setAdbDialogHost(String adbDialogHost) { settingsData.setAdbDialogHost(adbDialogHost); } public String getAdbDialogPath() { return settingsData.getAdbDialogPath(); } public void setAdbDialogPath(String adbDialogPath) { settingsData.setAdbDialogPath(adbDialogPath); } public String getAdbDialogPort() { return settingsData.getAdbDialogPort(); } public void setAdbDialogPort(String adbDialogPort) { settingsData.setAdbDialogPort(adbDialogPort); } public CommentsLevel getCommentsLevel() { return settingsData.getCommentsLevel(); } public void setCommentsLevel(CommentsLevel level) { settingsData.setCommentsLevel(level); } public int getTypeUpdatesLimitCount() { return settingsData.getTypeUpdatesLimitCount(); } public void setTypeUpdatesLimitCount(int typeUpdatesLimitCount) { settingsData.setTypeUpdatesLimitCount(typeUpdatesLimitCount); } public LineNumbersMode getLineNumbersMode() { return settingsData.getLineNumbersMode(); } public void setLineNumbersMode(LineNumbersMode lineNumbersMode) { settingsData.setLineNumbersMode(lineNumbersMode); } public CodeCacheMode getCodeCacheMode() { return settingsData.getCodeCacheMode(); } public void setCodeCacheMode(CodeCacheMode codeCacheMode) { settingsData.setCodeCacheMode(codeCacheMode); } public UsageCacheMode getUsageCacheMode() { return settingsData.getUsageCacheMode(); } public void setUsageCacheMode(UsageCacheMode usageCacheMode) { settingsData.setUsageCacheMode(usageCacheMode); } public @Nullable String getCacheDir() { return settingsData.getCacheDir(); } public void setCacheDir(@Nullable String cacheDir) { settingsData.setCacheDir(cacheDir); } public boolean isJumpOnDoubleClick() { return settingsData.isJumpOnDoubleClick(); } public void setJumpOnDoubleClick(boolean jumpOnDoubleClick) { settingsData.setJumpOnDoubleClick(jumpOnDoubleClick); } public boolean isDockLogViewer() { return settingsData.isDockLogViewer(); } public void saveDockLogViewer(boolean dockLogViewer) { settingsData.setDockLogViewer(dockLogViewer); sync(); } public boolean isDockQuickTabs() { return settingsData.isDockQuickTabs(); } public void saveDockQuickTabs(boolean dockQuickTabs) { settingsData.setDockQuickTabs(dockQuickTabs); sync(); } public XposedCodegenLanguage getXposedCodegenLanguage() { return settingsData.getXposedCodegenLanguage(); } public void setXposedCodegenLanguage(XposedCodegenLanguage language) { settingsData.setXposedCodegenLanguage(language); } public JadxUpdateChannel getJadxUpdateChannel() { return settingsData.getJadxUpdateChannel(); } public void setJadxUpdateChannel(JadxUpdateChannel channel) { settingsData.setJadxUpdateChannel(channel); } public TabDndGhostType getTabDndGhostType() { return settingsData.getTabDndGhostType(); } public void setTabDndGhostType(TabDndGhostType tabDndGhostType) { settingsData.setTabDndGhostType(tabDndGhostType); } public boolean isRestoreSwitchOverString() { return settingsData.isRestoreSwitchOverString(); } public void setRestoreSwitchOverString(boolean restoreSwitchOverString) { settingsData.setRestoreSwitchOverString(restoreSwitchOverString); } public boolean isRenamePrintable() { return settingsData.isRenamePrintable(); } public UserRenamesMappingsMode getUserRenamesMappingsMode() { return settingsData.getUserRenamesMappingsMode(); } public void setUserRenamesMappingsMode(UserRenamesMappingsMode userRenamesMappingsMode) { settingsData.setUserRenamesMappingsMode(userRenamesMappingsMode); } public boolean isInlineAnonymousClasses() { return settingsData.isInlineAnonymousClasses(); } public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) { settingsData.setInlineAnonymousClasses(inlineAnonymousClasses); } public boolean isRespectBytecodeAccessModifiers() { return settingsData.isRespectBytecodeAccessModifiers(); } public void setRespectBytecodeAccessModifiers(boolean respectBytecodeAccessModifiers) { settingsData.setRespectBytecodeAccessModifiers(respectBytecodeAccessModifiers); } public boolean isRenameCaseSensitive() { return settingsData.isRenameCaseSensitive(); } public DecompilationMode getDecompilationMode() { return settingsData.getDecompilationMode(); } public void setDecompilationMode(DecompilationMode decompilationMode) { settingsData.setDecompilationMode(decompilationMode); } public boolean isInlineMethods() { return settingsData.isInlineMethods(); } public void setInlineMethods(boolean inlineMethods) { settingsData.setInlineMethods(inlineMethods); } public boolean isFsCaseSensitive() { return settingsData.isFsCaseSensitive(); } public void setFsCaseSensitive(boolean fsCaseSensitive) { settingsData.setFsCaseSensitive(fsCaseSensitive); } public boolean isExtractFinally() { return settingsData.isExtractFinally(); } public void setExtractFinally(boolean extractFinally) { settingsData.setExtractFinally(extractFinally); } public int getSourceNameRepeatLimit() { return settingsData.getSourceNameRepeatLimit(); } public void setSourceNameRepeatLimit(int sourceNameRepeatLimit) { settingsData.setSourceNameRepeatLimit(sourceNameRepeatLimit); } public boolean isRenameValid() { return settingsData.isRenameValid(); } public boolean isSkipXmlPrettyPrint() { return settingsData.isSkipXmlPrettyPrint(); } public void setSkipXmlPrettyPrint(boolean skipXmlPrettyPrint) { settingsData.setSkipXmlPrettyPrint(skipXmlPrettyPrint); } public UseSourceNameAsClassNameAlias getUseSourceNameAsClassNameAlias() { return settingsData.getUseSourceNameAsClassNameAlias(); } public void setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias useSourceNameAsClassNameAlias) { settingsData.setUseSourceNameAsClassNameAlias(useSourceNameAsClassNameAlias); } public boolean isShowInconsistentCode() { return settingsData.isShowInconsistentCode(); } public void setShowInconsistentCode(boolean showInconsistentCode) { settingsData.setShowInconsistentCode(showInconsistentCode); } public boolean isCfgOutput() { return settingsData.isCfgOutput(); } public void setCfgOutput(boolean cfgOutput) { settingsData.setCfgOutput(cfgOutput); } public boolean isEscapeUnicode() { return settingsData.isEscapeUnicode(); } public void setEscapeUnicode(boolean escapeUnicode) { settingsData.setEscapeUnicode(escapeUnicode); } public JadxArgs.UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() { return settingsData.getUseKotlinMethodsForVarNames(); } public void setUseKotlinMethodsForVarNames(JadxArgs.UseKotlinMethodsForVarNames useKotlinMethodsForVarNames) { settingsData.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames); } public String getDeobfuscationWhitelistStr() { return settingsData.getDeobfuscationWhitelistStr(); } public void setDeobfuscationWhitelistStr(String deobfuscationWhitelistStr) { settingsData.setDeobfuscationWhitelistStr(deobfuscationWhitelistStr); } public String getGeneratedRenamesMappingFile() { return settingsData.getGeneratedRenamesMappingFile(); } public boolean isRawCfgOutput() { return settingsData.isRawCfgOutput(); } public void setRawCfgOutput(boolean rawCfgOutput) { settingsData.setRawCfgOutput(rawCfgOutput); } public boolean isMoveInnerClasses() { return settingsData.isMoveInnerClasses(); } public void setMoveInnerClasses(boolean moveInnerClasses) { settingsData.setMoveInnerClasses(moveInnerClasses); } public boolean isUseDx() { return settingsData.isUseDx(); } public void setUseDx(boolean useDx) { settingsData.setUseDx(useDx); } public boolean isAddDebugLines() { return settingsData.isAddDebugLines(); } public boolean isUseHeadersForDetectResourceExtensions() { return settingsData.isUseHeadersForDetectResourceExtensions(); } public void setUseHeadersForDetectResourceExtensions(boolean useHeadersForDetectResourceExtensions) { settingsData.setUseHeadersForDetectResourceExtensions(useHeadersForDetectResourceExtensions); } public Map getPluginOptions() { return settingsData.getPluginOptions(); } public boolean isDeobfuscationOn() { return settingsData.isDeobfuscationOn(); } public void setDeobfuscationOn(boolean deobfuscationOn) { settingsData.setDeobfuscationOn(deobfuscationOn); } public boolean isReplaceConsts() { return settingsData.isReplaceConsts(); } public void setReplaceConsts(boolean replaceConsts) { settingsData.setReplaceConsts(replaceConsts); } public boolean isAllowInlineKotlinLambda() { return settingsData.isAllowInlineKotlinLambda(); } public void setAllowInlineKotlinLambda(boolean allowInlineKotlinLambda) { settingsData.setAllowInlineKotlinLambda(allowInlineKotlinLambda); } public void setDeobfuscationUseSourceNameAsAlias(Boolean deobfuscationUseSourceNameAsAlias) { settingsData.setDeobfuscationUseSourceNameAsAlias(deobfuscationUseSourceNameAsAlias); } public void setRenameFlags(Set renameFlags) { settingsData.setRenameFlags(renameFlags); } public void updateRenameFlag(JadxArgs.RenameEnum flag, boolean enabled) { if (enabled) { settingsData.getRenameFlags().add(flag); } else { settingsData.getRenameFlags().remove(flag); } } public void setUserRenamesMappingsPath(Path userRenamesMappingsPath) { settingsData.setUserRenamesMappingsPath(userRenamesMappingsPath); } public boolean isSkipSources() { return settingsData.isSkipSources(); } public boolean isDebugInfo() { return settingsData.isDebugInfo(); } public void setDebugInfo(boolean debugInfo) { settingsData.setDebugInfo(debugInfo); } public boolean isSkipResources() { return settingsData.isSkipResources(); } public void setSkipResources(boolean skipResources) { settingsData.setSkipResources(skipResources); } public ResourceNameSource getResourceNameSource() { return settingsData.getResourceNameSource(); } public void setResourceNameSource(ResourceNameSource resourceNameSource) { settingsData.setResourceNameSource(resourceNameSource); } public IntegerFormat getIntegerFormat() { return settingsData.getIntegerFormat(); } public void setIntegerFormat(IntegerFormat format) { settingsData.setIntegerFormat(format); } public boolean isFallbackMode() { return settingsData.isFallbackMode(); } public boolean isUseImports() { return settingsData.isUseImports(); } public void setUseImports(boolean useImports) { settingsData.setUseImports(useImports); } public int getDeobfuscationMinLength() { return settingsData.getDeobfuscationMinLength(); } public void setDeobfuscationMinLength(int deobfuscationMinLength) { settingsData.setDeobfuscationMinLength(deobfuscationMinLength); } public GeneratedRenamesMappingFileMode getGeneratedRenamesMappingFileMode() { return settingsData.getGeneratedRenamesMappingFileMode(); } public void setGeneratedRenamesMappingFileMode(GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode) { settingsData.setGeneratedRenamesMappingFileMode(generatedRenamesMappingFileMode); } public int getDeobfuscationMaxLength() { return settingsData.getDeobfuscationMaxLength(); } public void setDeobfuscationMaxLength(int deobfuscationMaxLength) { settingsData.setDeobfuscationMaxLength(deobfuscationMaxLength); } public int getThreadsCount() { return settingsData.getThreadsCount(); } public void setThreadsCount(int threadsCount) { settingsData.setThreadsCount(threadsCount); } public SaveOptionEnum getSaveOption() { return settingsData.getSaveOption(); } public void setSaveOption(SaveOptionEnum saveOption) { settingsData.setSaveOption(saveOption); } public boolean isSmaliAreaShowBytecode() { return settingsData.isSmaliAreaShowBytecode(); } public void setSmaliAreaShowBytecode(boolean smaliAreaShowBytecode) { settingsData.setSmaliAreaShowBytecode(smaliAreaShowBytecode); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsData.java ================================================ package jadx.gui.settings; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.JFrame; import org.jetbrains.annotations.Nullable; import com.google.gson.annotations.SerializedName; import jadx.cli.LogHelper; import jadx.gui.cache.code.CodeCacheMode; import jadx.gui.cache.usage.UsageCacheMode; import jadx.gui.settings.data.SaveOptionEnum; import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.tab.dnd.TabDndGhostType; import jadx.gui.utils.LafManager; import jadx.gui.utils.LangLocale; import jadx.gui.utils.NLS; import jadx.gui.utils.shortcut.Shortcut; /** * Data class to hold all jadx-gui settings. * Also inherit all options from jadx-cli. * Serialized/deserialized as JSON in {@link jadx.cli.config.JadxConfigAdapter}. * Annotation {@link JadxConfigExcludeExport} used to exclude environment (files, window states) * fields from copy/export. */ public class JadxSettingsData extends JadxGUIArgs { public static final int CURRENT_SETTINGS_VERSION = 23; private static final Path USER_HOME = Paths.get(System.getProperty("user.home")); @JadxConfigExcludeExport private Path lastSaveProjectPath = USER_HOME; @JadxConfigExcludeExport private Path lastOpenFilePath = USER_HOME; @JadxConfigExcludeExport private Path lastSaveFilePath = USER_HOME; @JadxConfigExcludeExport private List recentProjects = new ArrayList<>(); @JadxConfigExcludeExport private Map windowPos = new HashMap<>(); @JadxConfigExcludeExport private int mainWindowExtendedState = JFrame.NORMAL; private boolean flattenPackage = false; private boolean checkForUpdates = true; private JadxUpdateChannel jadxUpdateChannel = JadxUpdateChannel.STABLE; private float uiZoom = 1.0f; private boolean applyUiZoomToFonts = true; private String uiFontStr = ""; @SerializedName(value = "codeFontStr", alternate = "fontStr") private String codeFontStr = ""; private String smaliFontStr = ""; private String editorTheme = ""; private String lafTheme = LafManager.INITIAL_THEME_NAME; private LangLocale langLocale = NLS.defaultLocale(); private boolean autoStartJobs = false; private String excludedPackages = ""; private SaveOptionEnum saveOption = SaveOptionEnum.ASK; private Map shortcuts = new HashMap<>(); private boolean showHeapUsageBar = false; private boolean alwaysSelectOpened = false; private boolean enablePreviewTab = false; private boolean useAlternativeFileDialog = false; private boolean codeAreaLineWrap = false; private int searchResultsPerPage = 50; private boolean useAutoSearch = true; private boolean keepCommonDialogOpen = false; private boolean smaliAreaShowBytecode = false; private LineNumbersMode lineNumbersMode = LineNumbersMode.AUTO; private int mainWindowVerticalSplitterLoc = 300; private int debuggerStackFrameSplitterLoc = 300; private int debuggerVarTreeSplitterLoc = 700; private String adbDialogPath = ""; private String adbDialogHost = "localhost"; private String adbDialogPort = "5037"; private CodeCacheMode codeCacheMode = CodeCacheMode.DISK; private UsageCacheMode usageCacheMode = UsageCacheMode.DISK; /** * Cache dir option values: * null - default (system) * "." - at project dir * other - custom */ private @Nullable String cacheDir = null; private boolean jumpOnDoubleClick = true; private boolean disableTooltipOnHover = false; private XposedCodegenLanguage xposedCodegenLanguage = XposedCodegenLanguage.JAVA; private int treeWidth = 130; private boolean dockLogViewer = true; private boolean dockQuickTabs = false; private TabDndGhostType tabDndGhostType = TabDndGhostType.OUTLINE; private int settingsVersion = CURRENT_SETTINGS_VERSION; public JadxSettingsData() { this.logLevel = LogHelper.LogLevelEnum.INFO; } public String getAdbDialogHost() { return adbDialogHost; } public void setAdbDialogHost(String adbDialogHost) { this.adbDialogHost = adbDialogHost; } public String getAdbDialogPath() { return adbDialogPath; } public void setAdbDialogPath(String adbDialogPath) { this.adbDialogPath = adbDialogPath; } public String getAdbDialogPort() { return adbDialogPort; } public void setAdbDialogPort(String adbDialogPort) { this.adbDialogPort = adbDialogPort; } public boolean isAlwaysSelectOpened() { return alwaysSelectOpened; } public void setAlwaysSelectOpened(boolean alwaysSelectOpened) { this.alwaysSelectOpened = alwaysSelectOpened; } public boolean isAutoStartJobs() { return autoStartJobs; } public void setAutoStartJobs(boolean autoStartJobs) { this.autoStartJobs = autoStartJobs; } public @Nullable String getCacheDir() { return cacheDir; } public void setCacheDir(@Nullable String cacheDir) { this.cacheDir = cacheDir; } public boolean isCheckForUpdates() { return checkForUpdates; } public void setCheckForUpdates(boolean checkForUpdates) { this.checkForUpdates = checkForUpdates; } public boolean isCodeAreaLineWrap() { return codeAreaLineWrap; } public void setCodeAreaLineWrap(boolean codeAreaLineWrap) { this.codeAreaLineWrap = codeAreaLineWrap; } public CodeCacheMode getCodeCacheMode() { return codeCacheMode; } public void setCodeCacheMode(CodeCacheMode codeCacheMode) { this.codeCacheMode = codeCacheMode; } public int getDebuggerStackFrameSplitterLoc() { return debuggerStackFrameSplitterLoc; } public void setDebuggerStackFrameSplitterLoc(int debuggerStackFrameSplitterLoc) { this.debuggerStackFrameSplitterLoc = debuggerStackFrameSplitterLoc; } public int getDebuggerVarTreeSplitterLoc() { return debuggerVarTreeSplitterLoc; } public void setDebuggerVarTreeSplitterLoc(int debuggerVarTreeSplitterLoc) { this.debuggerVarTreeSplitterLoc = debuggerVarTreeSplitterLoc; } public boolean isDisableTooltipOnHover() { return disableTooltipOnHover; } public void setDisableTooltipOnHover(boolean disableTooltipOnHover) { this.disableTooltipOnHover = disableTooltipOnHover; } public boolean isDockLogViewer() { return dockLogViewer; } public void setDockLogViewer(boolean dockLogViewer) { this.dockLogViewer = dockLogViewer; } public boolean isDockQuickTabs() { return dockQuickTabs; } public void setDockQuickTabs(boolean dockQuickTabs) { this.dockQuickTabs = dockQuickTabs; } public String getEditorTheme() { return editorTheme; } public void setEditorTheme(String editorTheme) { this.editorTheme = editorTheme; } public boolean isEnablePreviewTab() { return enablePreviewTab; } public void setEnablePreviewTab(boolean enablePreviewTab) { this.enablePreviewTab = enablePreviewTab; } public String getExcludedPackages() { return excludedPackages; } public void setExcludedPackages(String excludedPackages) { this.excludedPackages = excludedPackages; } public boolean isFlattenPackage() { return flattenPackage; } public void setFlattenPackage(boolean flattenPackage) { this.flattenPackage = flattenPackage; } public JadxUpdateChannel getJadxUpdateChannel() { return jadxUpdateChannel; } public void setJadxUpdateChannel(JadxUpdateChannel jadxUpdateChannel) { this.jadxUpdateChannel = jadxUpdateChannel; } public boolean isJumpOnDoubleClick() { return jumpOnDoubleClick; } public void setJumpOnDoubleClick(boolean jumpOnDoubleClick) { this.jumpOnDoubleClick = jumpOnDoubleClick; } public boolean isKeepCommonDialogOpen() { return keepCommonDialogOpen; } public void setKeepCommonDialogOpen(boolean keepCommonDialogOpen) { this.keepCommonDialogOpen = keepCommonDialogOpen; } public String getLafTheme() { return lafTheme; } public void setLafTheme(String lafTheme) { this.lafTheme = lafTheme; } public LangLocale getLangLocale() { return langLocale; } public void setLangLocale(LangLocale langLocale) { this.langLocale = langLocale; } public Path getLastOpenFilePath() { return lastOpenFilePath; } public void setLastOpenFilePath(Path lastOpenFilePath) { this.lastOpenFilePath = lastOpenFilePath; } public Path getLastSaveFilePath() { return lastSaveFilePath; } public void setLastSaveFilePath(Path lastSaveFilePath) { this.lastSaveFilePath = lastSaveFilePath; } public Path getLastSaveProjectPath() { return lastSaveProjectPath; } public void setLastSaveProjectPath(Path lastSaveProjectPath) { this.lastSaveProjectPath = lastSaveProjectPath; } public LineNumbersMode getLineNumbersMode() { return lineNumbersMode; } public void setLineNumbersMode(LineNumbersMode lineNumbersMode) { this.lineNumbersMode = lineNumbersMode; } public int getMainWindowExtendedState() { return mainWindowExtendedState; } public void setMainWindowExtendedState(int mainWindowExtendedState) { this.mainWindowExtendedState = mainWindowExtendedState; } public int getMainWindowVerticalSplitterLoc() { return mainWindowVerticalSplitterLoc; } public void setMainWindowVerticalSplitterLoc(int mainWindowVerticalSplitterLoc) { this.mainWindowVerticalSplitterLoc = mainWindowVerticalSplitterLoc; } public List getRecentProjects() { return recentProjects; } public void setRecentProjects(List recentProjects) { this.recentProjects = recentProjects; } public SaveOptionEnum getSaveOption() { return saveOption; } public void setSaveOption(SaveOptionEnum saveOption) { this.saveOption = saveOption; } public int getSearchResultsPerPage() { return searchResultsPerPage; } public void setSearchResultsPerPage(int searchResultsPerPage) { this.searchResultsPerPage = searchResultsPerPage; } public int getSettingsVersion() { return settingsVersion; } public void setSettingsVersion(int settingsVersion) { this.settingsVersion = settingsVersion; } public Map getShortcuts() { return shortcuts; } public void setShortcuts(Map shortcuts) { this.shortcuts = shortcuts; } public boolean isShowHeapUsageBar() { return showHeapUsageBar; } public void setShowHeapUsageBar(boolean showHeapUsageBar) { this.showHeapUsageBar = showHeapUsageBar; } public boolean isSmaliAreaShowBytecode() { return smaliAreaShowBytecode; } public void setSmaliAreaShowBytecode(boolean smaliAreaShowBytecode) { this.smaliAreaShowBytecode = smaliAreaShowBytecode; } public String getUiFontStr() { return uiFontStr; } public void setUiFontStr(String uiFontStr) { this.uiFontStr = uiFontStr; } public String getCodeFontStr() { return codeFontStr; } public void setCodeFontStr(String codeFontStr) { this.codeFontStr = codeFontStr; } public String getSmaliFontStr() { return smaliFontStr; } public void setSmaliFontStr(String smaliFontStr) { this.smaliFontStr = smaliFontStr; } public TabDndGhostType getTabDndGhostType() { return tabDndGhostType; } public void setTabDndGhostType(TabDndGhostType tabDndGhostType) { this.tabDndGhostType = tabDndGhostType; } public int getTreeWidth() { return treeWidth; } public void setTreeWidth(int treeWidth) { this.treeWidth = treeWidth; } public float getUiZoom() { return uiZoom; } public void setUiZoom(float uiZoom) { this.uiZoom = uiZoom; } public boolean isApplyUiZoomToFonts() { return applyUiZoomToFonts; } public void setApplyUiZoomToFonts(boolean applyUiZoomToFonts) { this.applyUiZoomToFonts = applyUiZoomToFonts; } public UsageCacheMode getUsageCacheMode() { return usageCacheMode; } public void setUsageCacheMode(UsageCacheMode usageCacheMode) { this.usageCacheMode = usageCacheMode; } public boolean isUseAlternativeFileDialog() { return useAlternativeFileDialog; } public void setUseAlternativeFileDialog(boolean useAlternativeFileDialog) { this.useAlternativeFileDialog = useAlternativeFileDialog; } public boolean isUseAutoSearch() { return useAutoSearch; } public void setUseAutoSearch(boolean useAutoSearch) { this.useAutoSearch = useAutoSearch; } public Map getWindowPos() { return windowPos; } public void setWindowPos(Map windowPos) { this.windowPos = windowPos; } public XposedCodegenLanguage getXposedCodegenLanguage() { return xposedCodegenLanguage; } public void setXposedCodegenLanguage(XposedCodegenLanguage xposedCodegenLanguage) { this.xposedCodegenLanguage = xposedCodegenLanguage; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/JadxUpdateChannel.java ================================================ package jadx.gui.settings; public enum JadxUpdateChannel { STABLE, UNSTABLE, } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/LineNumbersMode.java ================================================ package jadx.gui.settings; public enum LineNumbersMode { DISABLE, NORMAL, DEBUG, AUTO } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/TabStateViewAdapter.java ================================================ package jadx.gui.settings; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JavaClass; import jadx.gui.plugins.mappings.JInputMapping; import jadx.gui.settings.data.ITabStatePersist; import jadx.gui.settings.data.TabViewState; import jadx.gui.settings.data.ViewPoint; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResource; import jadx.gui.treemodel.JSubResource; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.EditorViewState; import jadx.gui.utils.UiUtils; public class TabStateViewAdapter { private static final Logger LOG = LoggerFactory.getLogger(TabStateViewAdapter.class); private final Map customAdaptersMap = new HashMap<>(); public @Nullable TabViewState build(EditorViewState viewState) { TabViewState tvs = new TabViewState(); tvs.setSubPath(viewState.getSubPath()); if (!saveJNode(tvs, viewState.getNode())) { if (UiUtils.JADX_GUI_DEBUG) { LOG.warn("Can't save view state: {}", viewState); } return null; } tvs.setCaret(viewState.getCaretPos()); tvs.setView(new ViewPoint(viewState.getViewPoint())); tvs.setActive(viewState.isActive()); tvs.setPinned(viewState.isPinned()); tvs.setBookmarked(viewState.isBookmarked()); tvs.setHidden(viewState.isHidden()); tvs.setPreviewTab(viewState.isPreviewTab()); return tvs; } public @Nullable EditorViewState load(MainWindow mw, TabViewState tvs) { try { JNode node = loadJNode(mw, tvs); if (node == null) { if (UiUtils.JADX_GUI_DEBUG) { LOG.warn("Can't restore view for {}", tvs); } return null; } EditorViewState viewState = new EditorViewState(node, tvs.getSubPath(), tvs.getCaret(), tvs.getView().toPoint()); viewState.setActive(tvs.isActive()); viewState.setPinned(tvs.isPinned()); viewState.setBookmarked(tvs.isBookmarked()); viewState.setHidden(tvs.isHidden()); viewState.setPreviewTab(tvs.isPreviewTab()); return viewState; } catch (Exception e) { LOG.error("Failed to load tab state: {}", tvs, e); return null; } } public void setCustomAdapters(List customAdapters) { customAdaptersMap.clear(); for (ITabStatePersist customAdapter : customAdapters) { customAdaptersMap.put(customAdapter.getNodeClass().getName(), customAdapter); } } @Nullable private JNode loadJNode(MainWindow mw, TabViewState tvs) { switch (tvs.getType()) { case "class": JavaClass javaClass = mw.getWrapper().searchJavaClassByRawName(tvs.getTabPath()); if (javaClass != null) { return mw.getCacheObject().getNodeCache().makeFrom(javaClass); } break; case "resource": return mw.getTreeRoot().searchResourceByName(tvs.getTabPath()); case "sub-resource": String[] parts = tvs.getTabPath().split(JSubResource.SUB_RES_PREFIX); JResource baseRes = mw.getTreeRoot().searchResourceByName(parts[0]); if (baseRes != null) { String subName = parts[1]; return baseRes.searchDepthNode(n -> n.getName().equals(subName)); // will load node before search } return null; case "mapping": return mw.getTreeRoot().followStaticPath("JInputs").searchNode(node -> node instanceof JInputMapping); } ITabStatePersist statePersist = customAdaptersMap.get(tvs.getType()); if (statePersist != null) { try { return statePersist.load(tvs.getTabPath()); } catch (Exception e) { LOG.error("Failed to restore tab for custom node adapter: {}", tvs.getType(), e); } } return null; } private boolean saveJNode(TabViewState tvs, JNode node) { if (node instanceof JClass) { tvs.setType("class"); tvs.setTabPath(((JClass) node).getCls().getRawName()); return true; } if (node instanceof JSubResource) { JSubResource subRes = (JSubResource) node; tvs.setType("sub-resource"); tvs.setTabPath(subRes.getBaseRes().getName() + JSubResource.SUB_RES_PREFIX + subRes.getName()); return true; } if (node instanceof JResource) { tvs.setType("resource"); tvs.setTabPath(node.getName()); return true; } if (node instanceof JInputMapping) { tvs.setType("mapping"); return true; } String typeName = node.getClass().getName(); ITabStatePersist statePersist = customAdaptersMap.get(typeName); if (statePersist != null) { try { tvs.setTabPath(statePersist.save(node)); tvs.setType(statePersist.getNodeClass().getName()); return true; } catch (Exception e) { LOG.error("Failed to save state for custom node: {}", typeName, e); } } return false; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/WindowLocation.java ================================================ package jadx.gui.settings; import java.awt.Rectangle; import java.util.Objects; import org.jetbrains.annotations.Nullable; @SuppressWarnings("unused") public class WindowLocation { private String windowId; private @Nullable Rectangle bounds; // Don't remove. Used in JSON serialization public WindowLocation() { } public WindowLocation(String windowId, @Nullable Rectangle bounds) { this.windowId = windowId; this.bounds = bounds; } public String getWindowId() { return windowId; } public void setWindowId(String windowId) { this.windowId = windowId; } public @Nullable Rectangle getBounds() { return bounds; } public void setBounds(@Nullable Rectangle bounds) { this.bounds = bounds; } @Override public int hashCode() { return Objects.hashCode(windowId); } @Override public final boolean equals(Object o) { if (o instanceof WindowLocation) { WindowLocation that = (WindowLocation) o; return Objects.equals(windowId, that.windowId) && Objects.equals(bounds, that.bounds); } return false; } @Override public String toString() { return "WindowLocation{id=" + windowId + ", bounds=" + bounds + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/XposedCodegenLanguage.java ================================================ package jadx.gui.settings; public enum XposedCodegenLanguage { JAVA, KOTLIN, } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/data/ITabStatePersist.java ================================================ package jadx.gui.settings.data; import org.jetbrains.annotations.Nullable; import jadx.gui.treemodel.JNode; /** * Adapter interface to allow save/load state of opened tabs */ public interface ITabStatePersist { Class getNodeClass(); String save(JNode node); @Nullable JNode load(String stateStr); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java ================================================ package jadx.gui.settings.data; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import org.jetbrains.annotations.Nullable; import jadx.api.data.impl.JadxCodeData; import jadx.gui.search.providers.ResourceFilter; public class ProjectData { private int projectVersion = 2; private List files = new ArrayList<>(); private List treeExpansionsV2 = new ArrayList<>(); private JadxCodeData codeData = new JadxCodeData(); private List openTabs = Collections.emptyList(); private @Nullable Path mappingsPath; private @Nullable String cacheDir; // don't use relative path adapter private boolean enableLiveReload = false; private List searchHistory = new ArrayList<>(); private String searchResourcesFilter = ResourceFilter.DEFAULT_STR; private int searchResourcesSizeLimit = 0; // in MB protected Map pluginOptions = new HashMap<>(); public List getFiles() { return files; } public void setFiles(List files) { this.files = Objects.requireNonNull(files); } public List getTreeExpansionsV2() { return treeExpansionsV2; } public void setTreeExpansionsV2(List treeExpansionsV2) { this.treeExpansionsV2 = treeExpansionsV2; } public JadxCodeData getCodeData() { return codeData; } public void setCodeData(JadxCodeData codeData) { this.codeData = codeData; } public int getProjectVersion() { return projectVersion; } public void setProjectVersion(int projectVersion) { this.projectVersion = projectVersion; } public List getOpenTabs() { return openTabs; } public boolean setOpenTabs(List openTabs) { if (this.openTabs.equals(openTabs)) { return false; } this.openTabs = openTabs; return true; } @Nullable public Path getMappingsPath() { return mappingsPath; } public void setMappingsPath(Path mappingsPath) { this.mappingsPath = mappingsPath; } public @Nullable String getCacheDir() { return cacheDir; } public void setCacheDir(@Nullable String cacheDir) { this.cacheDir = cacheDir; } public boolean isEnableLiveReload() { return enableLiveReload; } public void setEnableLiveReload(boolean enableLiveReload) { this.enableLiveReload = enableLiveReload; } public List getSearchHistory() { return searchHistory; } public void setSearchHistory(List searchHistory) { this.searchHistory = searchHistory; } public String getSearchResourcesFilter() { return searchResourcesFilter; } public void setSearchResourcesFilter(String searchResourcesFilter) { this.searchResourcesFilter = searchResourcesFilter; } public int getSearchResourcesSizeLimit() { return searchResourcesSizeLimit; } public void setSearchResourcesSizeLimit(int searchResourcesSizeLimit) { this.searchResourcesSizeLimit = searchResourcesSizeLimit; } public Map getPluginOptions() { return pluginOptions; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/data/SaveOptionEnum.java ================================================ package jadx.gui.settings.data; public enum SaveOptionEnum { ASK, NEVER, ALWAYS } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/data/ShortcutsWrapper.java ================================================ package jadx.gui.settings.data; import java.util.Map; import jadx.gui.ui.action.ActionModel; import jadx.gui.utils.shortcut.Shortcut; public class ShortcutsWrapper { private Map shortcuts; public void updateShortcuts(Map shortcuts) { this.shortcuts = shortcuts; } public Shortcut get(ActionModel actionModel) { return shortcuts.getOrDefault(actionModel, actionModel.getDefaultShortcut()); } public void put(ActionModel actionModel, Shortcut shortcut) { shortcuts.put(actionModel, shortcut); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/data/TabViewState.java ================================================ package jadx.gui.settings.data; public class TabViewState { private String type; private String tabPath; private String subPath; private int caret; private ViewPoint view; boolean active; boolean pinned; boolean bookmarked; boolean hidden; boolean previewTab; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getTabPath() { return tabPath; } public void setTabPath(String tabPath) { this.tabPath = tabPath; } public String getSubPath() { return subPath; } public void setSubPath(String subPath) { this.subPath = subPath; } public int getCaret() { return caret; } public void setCaret(int caret) { this.caret = caret; } public ViewPoint getView() { return view; } public void setView(ViewPoint view) { this.view = view; } public boolean isActive() { return active; } public void setActive(boolean active) { this.active = active; } public boolean isPinned() { return pinned; } public void setPinned(boolean pinned) { this.pinned = pinned; } public boolean isBookmarked() { return bookmarked; } public void setBookmarked(boolean bookmarked) { this.bookmarked = bookmarked; } public boolean isHidden() { return hidden; } public void setHidden(boolean hidden) { this.hidden = hidden; } public boolean isPreviewTab() { return previewTab; } public void setPreviewTab(boolean previewTab) { this.previewTab = previewTab; } @Override public String toString() { return "TabViewState{type=" + type + ", tabPath=" + tabPath + ", subPath=" + subPath + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/data/ViewPoint.java ================================================ package jadx.gui.settings.data; import java.awt.Point; public class ViewPoint { private int x; private int y; public ViewPoint() { this(0, 0); } public ViewPoint(Point p) { this(p.x, p.y); } public ViewPoint(int x, int y) { this.x = x; this.y = y; } public Point toPoint() { return new Point(x, y); } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } @Override public String toString() { return "ViewPoint{" + x + ", " + y + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/font/FontAdapter.java ================================================ package jadx.gui.settings.font; import java.awt.Font; import java.util.Objects; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.Utils; import jadx.gui.utils.FontUtils; import jadx.gui.utils.UiUtils; /** * Common handler for font updates and sync with settings data. */ public class FontAdapter { private static final Logger LOG = LoggerFactory.getLogger(FontAdapter.class); private Font defaultFont; private Font font; private Font effectiveFont; private Consumer fontSetter; private float uiZoom; public FontAdapter(Font defaultFont) { Objects.requireNonNull(defaultFont); this.defaultFont = defaultFont; this.font = defaultFont; } /** * Load current font from data, and save font setter to future sync */ public void bindData(String fontStr, Consumer fontStrSetter) { font = loadFromStr(fontStr); fontSetter = fontStrSetter; } public void setDefaultFont(Font newDefaultFont) { Objects.requireNonNull(newDefaultFont); Font prevDefaultFont = defaultFont; defaultFont = newDefaultFont; if (font == prevDefaultFont) { // font was set to default => update it also setFont(newDefaultFont); } } public Font getFont() { return font; } public Font getEffectiveFont() { return effectiveFont; } public void setFont(@Nullable Font newFont) { font = Utils.getOrElse(newFont, defaultFont); fontSetter.accept(getFontStr()); applyFontZoom(); } public void setUiZoom(float uiZoom) { this.uiZoom = uiZoom; applyFontZoom(); } private Font loadFromStr(String fontStr) { if (fontStr != null && !fontStr.isEmpty()) { try { return FontUtils.loadByStr(fontStr); } catch (Exception e) { LOG.warn("Failed to load font: {}, reset to default", fontStr, e); } } return defaultFont; } private String getFontStr() { if (font == defaultFont) { return ""; } return FontUtils.convertToStr(font); } private void applyFontZoom() { if (UiUtils.nearlyEqual(uiZoom, 1.0f)) { effectiveFont = font; } else { effectiveFont = font.deriveFont(font.getSize2D() * uiZoom); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/font/FontSettings.java ================================================ package jadx.gui.settings.font; import java.awt.Font; import javax.swing.UIManager; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.fonts.inter.FlatInterFont; import com.formdev.flatlaf.fonts.jetbrains_mono.FlatJetBrainsMonoFont; import com.formdev.flatlaf.util.FontUtils; import jadx.gui.settings.JadxSettingsData; import jadx.gui.utils.UiUtils; /** * Handle all font related settings */ public class FontSettings { static { FlatInterFont.install(); FlatJetBrainsMonoFont.install(); FlatLaf.setPreferredMonospacedFontFamily(FlatJetBrainsMonoFont.FAMILY); } private final FontAdapter uiFontAdapter; private final FontAdapter codeFontAdapter; private final FontAdapter smaliFontAdapter; private float uiZoom; private boolean applyUiZoomToFonts; public FontSettings() { int defFontSize = 13; Font defUiFont = FontUtils.getCompositeFont(FlatInterFont.FAMILY, Font.PLAIN, defFontSize); Font defCodeFont = FontUtils.getCompositeFont(FlatJetBrainsMonoFont.FAMILY, Font.PLAIN, defFontSize); uiFontAdapter = new FontAdapter(defUiFont); codeFontAdapter = new FontAdapter(defCodeFont); smaliFontAdapter = new FontAdapter(defCodeFont); } public void bindData(JadxSettingsData data) { uiFontAdapter.bindData(data.getUiFontStr(), data::setUiFontStr); codeFontAdapter.bindData(data.getCodeFontStr(), data::setCodeFontStr); smaliFontAdapter.bindData(data.getSmaliFontStr(), data::setSmaliFontStr); applyUiZoom(data.getUiZoom(), data.isApplyUiZoomToFonts()); } /** * Fetch and apply default font settings after FlatLaf init. */ public void updateDefaultFont() { Font defaultFont = UIManager.getFont("defaultFont"); if (defaultFont != null) { uiFontAdapter.setDefaultFont(defaultFont); } } public synchronized void applyUiZoom(float newUiZoom, boolean newApplyUiZoomToFonts) { if (UiUtils.nearlyEqual(uiZoom, newUiZoom) && applyUiZoomToFonts == newApplyUiZoomToFonts) { return; } uiZoom = newUiZoom; applyUiZoomToFonts = newApplyUiZoomToFonts; float effectiveFontZoom = newApplyUiZoomToFonts ? newUiZoom : 1.0f; uiFontAdapter.setUiZoom(effectiveFontZoom); codeFontAdapter.setUiZoom(effectiveFontZoom); smaliFontAdapter.setUiZoom(effectiveFontZoom); } public FontAdapter getUiFontAdapter() { return uiFontAdapter; } public FontAdapter getCodeFontAdapter() { return codeFontAdapter; } public FontAdapter getSmaliFontAdapter() { return smaliFontAdapter; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java ================================================ package jadx.gui.settings.ui; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.event.ItemEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.function.Consumer; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSpinner; import javax.swing.JSplitPane; import javax.swing.KeyStroke; import javax.swing.ScrollPaneConstants; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.CommentsLevel; import jadx.api.DecompilationMode; import jadx.api.JadxArgs; import jadx.api.JadxArgs.UseKotlinMethodsForVarNames; import jadx.api.JadxDecompiler; import jadx.api.args.GeneratedRenamesMappingFileMode; import jadx.api.args.IntegerFormat; import jadx.api.args.ResourceNameSource; import jadx.api.args.UseSourceNameAsClassNameAlias; import jadx.api.plugins.events.JadxEvents; import jadx.api.plugins.events.types.ReloadSettingsWindow; import jadx.api.plugins.gui.ISettingsGroup; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsData; import jadx.gui.settings.JadxUpdateChannel; import jadx.gui.settings.LineNumbersMode; import jadx.gui.settings.XposedCodegenLanguage; import jadx.gui.settings.data.SaveOptionEnum; import jadx.gui.settings.font.FontAdapter; import jadx.gui.settings.font.FontSettings; import jadx.gui.settings.ui.cache.CacheSettingsGroup; import jadx.gui.settings.ui.font.JadxFontDialog; import jadx.gui.settings.ui.plugins.PluginSettings; import jadx.gui.settings.ui.shortcut.ShortcutsSettingsGroup; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.theme.EditorThemeManager; import jadx.gui.ui.codearea.theme.ThemeIdAndName; import jadx.gui.ui.tab.dnd.TabDndGhostType; import jadx.gui.utils.FontUtils; import jadx.gui.utils.LafManager; import jadx.gui.utils.LangLocale; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.ActionHandler; public class JadxSettingsWindow extends JDialog { private static final long serialVersionUID = -1804570470377354148L; private static final Logger LOG = LoggerFactory.getLogger(JadxSettingsWindow.class); private final transient MainWindow mainWindow; private final transient JadxSettings settings; private final transient String startSettings; private final transient String startSettingsHash; private final transient LangLocale prevLang; private final transient Consumer reloadListener; private transient boolean needReload = false; private transient SettingsTree tree; private List groups; private JPanel wrapGroupPanel; public JadxSettingsWindow(MainWindow mainWindow, JadxSettings settings) { this.mainWindow = mainWindow; this.settings = settings; this.startSettings = settings.getSettingsJsonString(); this.startSettingsHash = calcSettingsHash(); this.prevLang = settings.getLangLocale(); initUI(); setTitle(NLS.str("preferences.title")); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setModalityType(ModalityType.APPLICATION_MODAL); pack(); UiUtils.setWindowIcons(this); setLocationRelativeTo(null); if (!mainWindow.getSettings().loadWindowPos(this)) { setSize(700, 800); } reloadListener = ev -> UiUtils.uiRun(this::reloadUI); mainWindow.events().global().addListener(JadxEvents.RELOAD_SETTINGS_WINDOW, reloadListener); } private void reloadUI() { int[] selection = tree.getSelectionRows(); closeGroups(false); getContentPane().removeAll(); initUI(); // wait for other events to process UiUtils.uiRun(() -> { tree.setSelectionRows(selection); SwingUtilities.updateComponentTreeUI(this); }); } private void initUI() { wrapGroupPanel = new JPanel(new BorderLayout(10, 10)); groups = new ArrayList<>(); groups.add(makeDecompilationGroup()); groups.add(makeDeobfuscationGroup()); groups.add(makeRenameGroup()); groups.add(new CacheSettingsGroup(this)); groups.add(makeAppearanceGroup()); groups.add(new ShortcutsSettingsGroup(this, settings)); groups.add(makeProjectGroup()); groups.add(new PluginSettings(mainWindow, settings).build()); groups.add(makeOtherGroup()); tree = new SettingsTree(this); tree.init(groups); tree.setFocusable(true); JScrollPane leftPane = new JScrollPane(tree); leftPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 3, 3)); JScrollPane rightPane = new JScrollPane(wrapGroupPanel); rightPane.getVerticalScrollBar().setUnitIncrement(16); rightPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); rightPane.setBorder(BorderFactory.createEmptyBorder(10, 3, 3, 10)); JSplitPane splitPane = new JSplitPane(); splitPane.setResizeWeight(0.2); splitPane.setLeftComponent(leftPane); splitPane.setRightComponent(rightPane); Container contentPane = getContentPane(); contentPane.add(splitPane, BorderLayout.CENTER); contentPane.add(buildButtonsPane(), BorderLayout.PAGE_END); KeyStroke strokeEsc = KeyStroke.getKeyStroke("ESCAPE"); InputMap inputMap = getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); inputMap.put(strokeEsc, "ESCAPE"); getRootPane().getActionMap().put("ESCAPE", new ActionHandler(this::cancel)); } private JPanel buildButtonsPane() { JButton saveBtn = new JButton(NLS.str("preferences.save")); saveBtn.addActionListener(event -> save()); JButton cancelButton = new JButton(NLS.str("preferences.cancel")); cancelButton.addActionListener(event -> cancel()); JButton resetBtn = new JButton(NLS.str("preferences.reset")); resetBtn.addActionListener(event -> reset()); JButton copyBtn = new JButton(NLS.str("preferences.copy")); copyBtn.addActionListener(event -> copySettings()); JPanel buttonPane = new JPanel(); buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); buttonPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); buttonPane.add(resetBtn); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(copyBtn); buttonPane.add(Box.createHorizontalGlue()); buttonPane.add(saveBtn); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(cancelButton); getRootPane().setDefaultButton(saveBtn); return buttonPane; } /** * Activate the settings page by location. * * @param location - can be title of a settings group or settings group class implementation (end * with .class) */ public void activatePage(String location) { if (location.endsWith(".class")) { String clsName = StringUtils.removeSuffix(location, ".class"); for (ISettingsGroup group : groups) { String groupCls = group.getClass().getSimpleName(); if (groupCls.equals(clsName)) { selectGroup(group); return; } } throw new JadxRuntimeException("No setting group class: " + location); } else { for (ISettingsGroup group : groups) { if (group.getTitle().equals(location)) { selectGroup(group); return; } } throw new JadxRuntimeException("No setting group with title: " + location); } } public void selectGroup(ISettingsGroup group) { tree.selectGroup(group); } public void activateGroup(@Nullable ISettingsGroup group) { wrapGroupPanel.removeAll(); if (group != null) { wrapGroupPanel.add(group.buildComponent()); } wrapGroupPanel.updateUI(); } private static void enableComponents(Container container, boolean enable) { for (Component component : container.getComponents()) { if (component instanceof Container) { enableComponents((Container) component, enable); } component.setEnabled(enable); } } private SettingsGroup makeDeobfuscationGroup() { JCheckBox deobfOn = new JCheckBox(); deobfOn.setSelected(settings.isDeobfuscationOn()); deobfOn.addItemListener(e -> { settings.setDeobfuscationOn(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); SpinnerNumberModel minLenModel = new SpinnerNumberModel(settings.getDeobfuscationMinLength(), 0, Integer.MAX_VALUE, 1); JSpinner minLenSpinner = new JSpinner(minLenModel); minLenSpinner.addChangeListener(e -> { settings.setDeobfuscationMinLength((Integer) minLenSpinner.getValue()); needReload(); }); SpinnerNumberModel maxLenModel = new SpinnerNumberModel(settings.getDeobfuscationMaxLength(), 0, Integer.MAX_VALUE, 1); JSpinner maxLenSpinner = new JSpinner(maxLenModel); maxLenSpinner.addChangeListener(e -> { settings.setDeobfuscationMaxLength((Integer) maxLenSpinner.getValue()); needReload(); }); JComboBox resNamesSource = new JComboBox<>(ResourceNameSource.values()); resNamesSource.setSelectedItem(settings.getResourceNameSource()); resNamesSource.addActionListener(e -> { settings.setResourceNameSource((ResourceNameSource) resNamesSource.getSelectedItem()); needReload(); }); JCheckBox useHeaders = new JCheckBox(); useHeaders.setSelected(settings.isUseHeadersForDetectResourceExtensions()); useHeaders.addItemListener(e -> { settings.setUseHeadersForDetectResourceExtensions(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JComboBox generatedRenamesMappingFileModeCB = new JComboBox<>(GeneratedRenamesMappingFileMode.values()); generatedRenamesMappingFileModeCB.setSelectedItem(settings.getGeneratedRenamesMappingFileMode()); generatedRenamesMappingFileModeCB.addActionListener(e -> { GeneratedRenamesMappingFileMode newValue = (GeneratedRenamesMappingFileMode) generatedRenamesMappingFileModeCB.getSelectedItem(); if (newValue != settings.getGeneratedRenamesMappingFileMode()) { settings.setGeneratedRenamesMappingFileMode(newValue); needReload(); } }); JButton editWhitelistedEntities = new JButton(NLS.str("preferences.excludedPackages.button")); editWhitelistedEntities.addActionListener(event -> { String prevWhitelistedEntities = settings.getDeobfuscationWhitelistStr(); String result = JOptionPane.showInputDialog(this, NLS.str("preferences.deobfuscation_whitelist.editDialog"), prevWhitelistedEntities); if (result != null) { settings.setDeobfuscationWhitelistStr(result); if (!prevWhitelistedEntities.equals(result)) { needReload(); } } }); SettingsGroup deobfGroup = new SettingsGroup(NLS.str("preferences.deobfuscation")); deobfGroup.addRow(NLS.str("preferences.deobfuscation_on"), deobfOn); deobfGroup.addRow(NLS.str("preferences.deobfuscation_min_len"), minLenSpinner); deobfGroup.addRow(NLS.str("preferences.deobfuscation_max_len"), maxLenSpinner); deobfGroup.addRow(NLS.str("preferences.deobfuscation_res_name_source"), resNamesSource); deobfGroup.addRow(NLS.str("preferences.deobfuscation_res_use_headers"), useHeaders); deobfGroup.addRow(NLS.str("preferences.generated_renames_mapping_file_mode"), generatedRenamesMappingFileModeCB); deobfGroup.addRow(NLS.str("preferences.deobfuscation_whitelist"), NLS.str("preferences.deobfuscation_whitelist.tooltip"), editWhitelistedEntities); deobfGroup.end(); Collection connectedComponents = Arrays.asList(minLenSpinner, maxLenSpinner); deobfOn.addItemListener(e -> enableComponentList(connectedComponents, e.getStateChange() == ItemEvent.SELECTED)); enableComponentList(connectedComponents, settings.isDeobfuscationOn()); return deobfGroup; } private SettingsGroup makeRenameGroup() { JCheckBox renameCaseSensitive = new JCheckBox(); renameCaseSensitive.setSelected(settings.isRenameCaseSensitive()); renameCaseSensitive.addItemListener(e -> { settings.updateRenameFlag(JadxArgs.RenameEnum.CASE, e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox renameValid = new JCheckBox(); renameValid.setSelected(settings.isRenameValid()); renameValid.addItemListener(e -> { settings.updateRenameFlag(JadxArgs.RenameEnum.VALID, e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox renamePrintable = new JCheckBox(); renamePrintable.setSelected(settings.isRenamePrintable()); renamePrintable.addItemListener(e -> { settings.updateRenameFlag(JadxArgs.RenameEnum.PRINTABLE, e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JComboBox useSourceNameAsClassNameAlias = new JComboBox<>(UseSourceNameAsClassNameAlias.values()); useSourceNameAsClassNameAlias.setSelectedItem(settings.getUseSourceNameAsClassNameAlias()); useSourceNameAsClassNameAlias.addActionListener(e -> { settings.setUseSourceNameAsClassNameAlias((UseSourceNameAsClassNameAlias) useSourceNameAsClassNameAlias.getSelectedItem()); needReload(); }); JSpinner repeatLimit = new JSpinner(new SpinnerNumberModel(settings.getSourceNameRepeatLimit(), 1, Integer.MAX_VALUE, 1)); repeatLimit.addChangeListener(e -> { settings.setSourceNameRepeatLimit((Integer) repeatLimit.getValue()); needReload(); }); SettingsGroup group = new SettingsGroup(NLS.str("preferences.rename")); group.addRow(NLS.str("preferences.rename_case"), renameCaseSensitive); group.addRow(NLS.str("preferences.rename_valid"), renameValid); group.addRow(NLS.str("preferences.rename_printable"), renamePrintable); group.addRow(NLS.str("preferences.rename_use_source_name_as_class_name_alias"), useSourceNameAsClassNameAlias); group.addRow(NLS.str("preferences.rename_source_name_repeat_limit"), repeatLimit); return group; } private void enableComponentList(Collection connectedComponents, boolean enabled) { connectedComponents.forEach(comp -> comp.setEnabled(enabled)); } private SettingsGroup makeProjectGroup() { JComboBox dropdown = new JComboBox<>(SaveOptionEnum.values()); dropdown.setSelectedItem(settings.getSaveOption()); dropdown.addActionListener(e -> { settings.setSaveOption((SaveOptionEnum) dropdown.getSelectedItem()); needReload(); }); SettingsGroup group = new SettingsGroup(NLS.str("preferences.project")); group.addRow(NLS.str("preferences.saveOption"), dropdown); return group; } private SettingsGroup makeAppearanceGroup() { JComboBox languageCbx = new JComboBox<>(NLS.getLangLocales()); for (LangLocale locale : NLS.getLangLocales()) { if (locale.equals(settings.getLangLocale())) { languageCbx.setSelectedItem(locale); break; } } languageCbx.addActionListener(e -> settings.setLangLocale((LangLocale) languageCbx.getSelectedItem())); EditorThemeManager editorThemeManager = mainWindow.getEditorThemeManager(); JComboBox themesCbx = new JComboBox<>(editorThemeManager.getThemeIdNameArray()); themesCbx.setSelectedItem(editorThemeManager.getCurrentThemeIdName()); themesCbx.addActionListener(evt -> { ThemeIdAndName selected = (ThemeIdAndName) themesCbx.getSelectedItem(); if (selected != null) { settings.setEditorTheme(selected.getId()); mainWindow.loadSettings(); } }); JComboBox lafCbx = new JComboBox<>(LafManager.getThemes()); lafCbx.setSelectedItem(settings.getLafTheme()); lafCbx.addActionListener(e -> { settings.setLafTheme((String) lafCbx.getSelectedItem()); mainWindow.loadSettings(); }); JSpinner uiZoomSpinner = new JSpinner(new SpinnerNumberModel(settings.getUiZoom(), 0.1, 10.0, 0.25)); uiZoomSpinner.addChangeListener(e -> { float zoomValue = ((Double) uiZoomSpinner.getValue()).floatValue(); settings.setUiZoom(zoomValue); mainWindow.loadSettings(); }); JCheckBox applyUiZoomToFontsChB = new JCheckBox(); applyUiZoomToFontsChB.setSelected(settings.isApplyUiZoomToFonts()); applyUiZoomToFontsChB.addItemListener(e -> { settings.setApplyUiZoomToFonts(e.getStateChange() == ItemEvent.SELECTED); mainWindow.loadSettings(); }); SettingsGroup group = new SettingsGroup(NLS.str("preferences.appearance")); group.addRow(NLS.str("preferences.language"), languageCbx); group.addRow(NLS.str("preferences.ui_zoom"), uiZoomSpinner); group.addRow(NLS.str("preferences.apply_ui_zoom_to_fonts"), applyUiZoomToFontsChB); FontSettings fontSettings = settings.getFontSettings(); addFontEditor(group, NLS.str("preferences.ui_font"), fontSettings.getUiFontAdapter(), false); addFontEditor(group, NLS.str("preferences.code_font"), fontSettings.getCodeFontAdapter(), false); addFontEditor(group, NLS.str("preferences.smali_font"), fontSettings.getSmaliFontAdapter(), true); group.addRow(NLS.str("preferences.laf_theme"), lafCbx); group.addRow(NLS.str("preferences.theme"), themesCbx); JComboBox tabDndGhostTypeCbx = new JComboBox<>(TabDndGhostType.values()); tabDndGhostTypeCbx.setSelectedItem(settings.getTabDndGhostType()); tabDndGhostTypeCbx.addActionListener(e -> { settings.setTabDndGhostType((TabDndGhostType) tabDndGhostTypeCbx.getSelectedItem()); mainWindow.loadSettings(); }); group.addRow(NLS.str("preferences.tab_dnd_appearance"), tabDndGhostTypeCbx); return group; } private void addFontEditor(SettingsGroup group, String title, FontAdapter fontAdapter, boolean monospace) { JLabel fontLabel = new JLabel(getFontLabelStr(fontAdapter.getFont())); JButton fontBtn = new JButton(NLS.str("preferences.select_font")); fontBtn.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { Font font = new JadxFontDialog(JadxSettingsWindow.this, settings, title) .select(fontAdapter.getFont(), monospace); if (font != null) { fontLabel.setText(getFontLabelStr(font)); fontAdapter.setFont(font); mainWindow.loadSettings(); } } }); JPanel fontPanel = new JPanel(); fontPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); fontPanel.add(fontLabel); fontPanel.add(fontBtn); group.addRow(title, fontPanel); } private static String getFontLabelStr(Font font) { return font.getFamily() + ' ' + FontUtils.convertFontStyleToString(font.getStyle()) + ' ' + font.getSize(); } private SettingsGroup makeDecompilationGroup() { JCheckBox useDx = new JCheckBox(); useDx.setSelected(settings.isUseDx()); useDx.addItemListener(e -> { settings.setUseDx(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JComboBox decompilationModeComboBox = new JComboBox<>(DecompilationMode.values()); decompilationModeComboBox.setSelectedItem(settings.getDecompilationMode()); decompilationModeComboBox.addActionListener(e -> { settings.setDecompilationMode((DecompilationMode) decompilationModeComboBox.getSelectedItem()); needReload(); }); JCheckBox showInconsistentCode = new JCheckBox(); showInconsistentCode.setSelected(settings.isShowInconsistentCode()); showInconsistentCode.addItemListener(e -> { settings.setShowInconsistentCode(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox resourceDecode = new JCheckBox(); resourceDecode.setSelected(settings.isSkipResources()); resourceDecode.addItemListener(e -> { settings.setSkipResources(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); // fix for #1331 int threadsCountValue = settings.getThreadsCount(); int threadsCountMax = Math.max(2, Math.max(threadsCountValue, Runtime.getRuntime().availableProcessors() * 2)); SpinnerNumberModel spinnerModel = new SpinnerNumberModel(threadsCountValue, 1, threadsCountMax, 1); JSpinner threadsCount = new JSpinner(spinnerModel); threadsCount.addChangeListener(e -> { settings.setThreadsCount((Integer) threadsCount.getValue()); needReload(); }); JButton editExcludedPackages = new JButton(NLS.str("preferences.excludedPackages.button")); editExcludedPackages.addActionListener(event -> { String oldExcludedPackages = settings.getExcludedPackages(); String result = JOptionPane.showInputDialog(this, NLS.str("preferences.excludedPackages.editDialog"), settings.getExcludedPackages()); if (result != null) { settings.setExcludedPackages(result); if (!oldExcludedPackages.equals(result)) { needReload(); } } }); JCheckBox autoStartJobs = new JCheckBox(); autoStartJobs.setSelected(settings.isAutoStartJobs()); autoStartJobs.addItemListener(e -> settings.setAutoStartJobs(e.getStateChange() == ItemEvent.SELECTED)); JCheckBox escapeUnicode = new JCheckBox(); escapeUnicode.setSelected(settings.isEscapeUnicode()); escapeUnicode.addItemListener(e -> { settings.setEscapeUnicode(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox replaceConsts = new JCheckBox(); replaceConsts.setSelected(settings.isReplaceConsts()); replaceConsts.addItemListener(e -> { settings.setReplaceConsts(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox respectBytecodeAccessModifiers = new JCheckBox(); respectBytecodeAccessModifiers.setSelected(settings.isRespectBytecodeAccessModifiers()); respectBytecodeAccessModifiers.addItemListener(e -> { settings.setRespectBytecodeAccessModifiers(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox useImports = new JCheckBox(); useImports.setSelected(settings.isUseImports()); useImports.addItemListener(e -> { settings.setUseImports(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox useDebugInfo = new JCheckBox(); useDebugInfo.setSelected(settings.isDebugInfo()); useDebugInfo.addItemListener(e -> { settings.setDebugInfo(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox inlineAnonymous = new JCheckBox(); inlineAnonymous.setSelected(settings.isInlineAnonymousClasses()); inlineAnonymous.addItemListener(e -> { settings.setInlineAnonymousClasses(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox inlineMethods = new JCheckBox(); inlineMethods.setSelected(settings.isInlineMethods()); inlineMethods.addItemListener(e -> { settings.setInlineMethods(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox inlineKotlinLambdas = new JCheckBox(); inlineKotlinLambdas.setSelected(settings.isAllowInlineKotlinLambda()); inlineKotlinLambdas.addItemListener(e -> { settings.setAllowInlineKotlinLambda(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox moveInnerClasses = new JCheckBox(); moveInnerClasses.setSelected(settings.isMoveInnerClasses()); moveInnerClasses.addItemListener(e -> { settings.setMoveInnerClasses(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox extractFinally = new JCheckBox(); extractFinally.setSelected(settings.isExtractFinally()); extractFinally.addItemListener(e -> { settings.setExtractFinally(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox restoreSwitchOverString = new JCheckBox(); restoreSwitchOverString.setSelected(settings.isRestoreSwitchOverString()); restoreSwitchOverString.addItemListener(e -> { settings.setRestoreSwitchOverString(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox fsCaseSensitive = new JCheckBox(); fsCaseSensitive.setSelected(settings.isFsCaseSensitive()); fsCaseSensitive.addItemListener(e -> { settings.setFsCaseSensitive(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JComboBox kotlinRenameVars = new JComboBox<>(UseKotlinMethodsForVarNames.values()); kotlinRenameVars.setSelectedItem(settings.getUseKotlinMethodsForVarNames()); kotlinRenameVars.addActionListener(e -> { settings.setUseKotlinMethodsForVarNames((UseKotlinMethodsForVarNames) kotlinRenameVars.getSelectedItem()); needReload(); }); JComboBox commentsLevel = new JComboBox<>(CommentsLevel.values()); commentsLevel.setSelectedItem(settings.getCommentsLevel()); commentsLevel.addActionListener(e -> { settings.setCommentsLevel((CommentsLevel) commentsLevel.getSelectedItem()); needReload(); }); JComboBox integerFormat = new JComboBox<>(IntegerFormat.values()); integerFormat.setSelectedItem(settings.getIntegerFormat()); integerFormat.addActionListener(e -> { settings.setIntegerFormat((IntegerFormat) integerFormat.getSelectedItem()); needReload(); }); JSpinner typeUpdatesLimitCount = new JSpinner( new SpinnerNumberModel(settings.getTypeUpdatesLimitCount(), 1, Short.MAX_VALUE, 1)); typeUpdatesLimitCount.addChangeListener(e -> { int newValue = (Integer) typeUpdatesLimitCount.getValue(); if (newValue < 1) { UiUtils.uiRun(() -> typeUpdatesLimitCount.setValue(1)); } else { settings.setTypeUpdatesLimitCount(newValue); needReload(); } }); SettingsGroup other = new SettingsGroup(NLS.str("preferences.decompile")); other.addRow(NLS.str("preferences.threads"), threadsCount); other.addRow(NLS.str("preferences.excludedPackages"), NLS.str("preferences.excludedPackages.tooltip"), editExcludedPackages); other.addRow(NLS.str("preferences.start_jobs"), autoStartJobs); other.addRow(NLS.str("preferences.decompilationMode"), decompilationModeComboBox); other.addRow(NLS.str("preferences.showInconsistentCode"), showInconsistentCode); other.addRow(NLS.str("preferences.escapeUnicode"), escapeUnicode); other.addRow(NLS.str("preferences.replaceConsts"), replaceConsts); other.addRow(NLS.str("preferences.respectBytecodeAccessModifiers"), respectBytecodeAccessModifiers); other.addRow(NLS.str("preferences.useImports"), useImports); other.addRow(NLS.str("preferences.useDebugInfo"), useDebugInfo); other.addRow(NLS.str("preferences.inlineAnonymous"), inlineAnonymous); other.addRow(NLS.str("preferences.inlineMethods"), inlineMethods); other.addRow(NLS.str("preferences.inlineKotlinLambdas"), inlineKotlinLambdas); other.addRow(NLS.str("preferences.moveInnerClasses"), moveInnerClasses); other.addRow(NLS.str("preferences.extractFinally"), extractFinally); other.addRow(NLS.str("preferences.restoreSwitchOverString"), restoreSwitchOverString); other.addRow(NLS.str("preferences.fsCaseSensitive"), fsCaseSensitive); other.addRow(NLS.str("preferences.useDx"), useDx); other.addRow(NLS.str("preferences.skipResourcesDecode"), resourceDecode); other.addRow(NLS.str("preferences.useKotlinMethodsForVarNames"), kotlinRenameVars); other.addRow(NLS.str("preferences.commentsLevel"), commentsLevel); other.addRow(NLS.str("preferences.integerFormat"), integerFormat); other.addRow(NLS.str("preferences.typeUpdatesCountLimit"), typeUpdatesLimitCount); return other; } private SettingsGroup makeOtherGroup() { JComboBox lineNumbersMode = new JComboBox<>(LineNumbersMode.values()); lineNumbersMode.setSelectedItem(settings.getLineNumbersMode()); lineNumbersMode.addActionListener(e -> { settings.setLineNumbersMode((LineNumbersMode) lineNumbersMode.getSelectedItem()); mainWindow.loadSettings(); }); JCheckBox jumpOnDoubleClick = new JCheckBox(); jumpOnDoubleClick.setSelected(settings.isJumpOnDoubleClick()); jumpOnDoubleClick.addItemListener(e -> settings.setJumpOnDoubleClick(e.getStateChange() == ItemEvent.SELECTED)); JSpinner resultsPerPage = new JSpinner( new SpinnerNumberModel(settings.getSearchResultsPerPage(), 0, Integer.MAX_VALUE, 1)); resultsPerPage.addChangeListener(ev -> settings.setSearchResultsPerPage((Integer) resultsPerPage.getValue())); JCheckBox useAltFileDialog = new JCheckBox(); useAltFileDialog.setSelected(settings.isUseAlternativeFileDialog()); useAltFileDialog.addItemListener(e -> settings.setUseAlternativeFileDialog(e.getStateChange() == ItemEvent.SELECTED)); JCheckBox update = new JCheckBox(); update.setSelected(settings.isCheckForUpdates()); update.addItemListener(e -> settings.setCheckForUpdates(e.getStateChange() == ItemEvent.SELECTED)); JCheckBox disableTooltipOnHover = new JCheckBox(); disableTooltipOnHover.setSelected(settings.isDisableTooltipOnHover()); disableTooltipOnHover.addItemListener(e -> settings.setDisableTooltipOnHover(e.getStateChange() == ItemEvent.SELECTED)); JCheckBox cfg = new JCheckBox(); cfg.setSelected(settings.isCfgOutput()); cfg.addItemListener(e -> { settings.setCfgOutput(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JCheckBox rawCfg = new JCheckBox(); rawCfg.setSelected(settings.isRawCfgOutput()); rawCfg.addItemListener(e -> { settings.setRawCfgOutput(e.getStateChange() == ItemEvent.SELECTED); needReload(); }); JComboBox xposedCodegenLanguage = new JComboBox<>(XposedCodegenLanguage.values()); xposedCodegenLanguage.setSelectedItem(settings.getXposedCodegenLanguage()); xposedCodegenLanguage.addActionListener(e -> { settings.setXposedCodegenLanguage((XposedCodegenLanguage) xposedCodegenLanguage.getSelectedItem()); mainWindow.loadSettings(); }); JComboBox updateChannel = new JComboBox<>(JadxUpdateChannel.values()); updateChannel.setSelectedItem(settings.getJadxUpdateChannel()); updateChannel.addActionListener(e -> { settings.setJadxUpdateChannel((JadxUpdateChannel) updateChannel.getSelectedItem()); mainWindow.loadSettings(); }); SettingsGroup group = new SettingsGroup(NLS.str("preferences.other")); group.addRow(NLS.str("preferences.lineNumbersMode"), lineNumbersMode); group.addRow(NLS.str("preferences.jumpOnDoubleClick"), jumpOnDoubleClick); group.addRow(NLS.str("preferences.disable_tooltip_on_hover"), disableTooltipOnHover); group.addRow(NLS.str("preferences.search_results_per_page"), resultsPerPage); group.addRow(NLS.str("preferences.useAlternativeFileDialog"), useAltFileDialog); group.addRow(NLS.str("preferences.cfg"), cfg); group.addRow(NLS.str("preferences.raw_cfg"), rawCfg); group.addRow(NLS.str("preferences.xposed_codegen_language"), xposedCodegenLanguage); group.addRow(NLS.str("preferences.check_for_updates"), update); group.addRow(NLS.str("preferences.update_channel"), updateChannel); return group; } private void closeGroups(boolean save) { for (ISettingsGroup group : groups) { group.close(save); } } private void save() { closeGroups(true); settings.sync(); enableComponents(this, false); SwingUtilities.invokeLater(() -> { if (shouldReload()) { mainWindow.getShortcutsController().loadSettings(); mainWindow.reopen(); } if (!settings.getLangLocale().equals(prevLang)) { JOptionPane.showMessageDialog( this, NLS.str("msg.language_changed", settings.getLangLocale()), NLS.str("msg.language_changed_title", settings.getLangLocale()), JOptionPane.INFORMATION_MESSAGE); } dispose(); }); } private void cancel() { closeGroups(false); settings.loadSettingsFromJsonString(startSettings); mainWindow.loadSettings(); dispose(); } private void reset() { int res = JOptionPane.showConfirmDialog( JadxSettingsWindow.this, NLS.str("preferences.reset_message"), NLS.str("preferences.reset_title"), JOptionPane.YES_NO_OPTION); if (res == JOptionPane.YES_OPTION) { settings.loadSettingsData(new JadxSettingsData()); mainWindow.loadSettings(); needReload(); getContentPane().removeAll(); initUI(); pack(); repaint(); } } private void copySettings() { String settingsText = settings.exportSettingsString(); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); StringSelection selection = new StringSelection(settingsText); clipboard.setContents(selection, selection); JOptionPane.showMessageDialog( JadxSettingsWindow.this, NLS.str("preferences.copy_message")); } public void needReload() { needReload = true; } private boolean shouldReload() { return needReload || !startSettingsHash.equals(calcSettingsHash()); } @SuppressWarnings("resource") private String calcSettingsHash() { JadxDecompiler decompiler = mainWindow.getWrapper().getCurrentDecompiler().orElse(null); return settings.toJadxArgs().makeCodeArgsHash(decompiler); } public MainWindow getMainWindow() { return mainWindow; } @Override public void dispose() { mainWindow.events().global().removeListener(JadxEvents.RELOAD_SETTINGS_WINDOW, reloadListener); settings.saveWindowPos(this); super.dispose(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsGroup.java ================================================ package jadx.gui.settings.ui; import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SwingConstants; import jadx.api.plugins.gui.ISettingsGroup; public class SettingsGroup implements ISettingsGroup { private final String title; private final JPanel panel; private final JPanel gridPanel; private final GridBagConstraints c; private int row; public SettingsGroup(String title) { this.title = title; gridPanel = new JPanel(new GridBagLayout()); c = new GridBagConstraints(); c.insets = new Insets(5, 5, 5, 5); c.weighty = 1.0; panel = new JPanel(); panel.setLayout(new BorderLayout(5, 5)); panel.setBorder(BorderFactory.createTitledBorder(title)); panel.add(gridPanel, BorderLayout.PAGE_START); } public JLabel addRow(String label, JComponent comp) { return addRow(label, null, comp); } public JLabel addRow(String label, String tooltip, JComponent comp) { JLabel rowLbl = new JLabel(label); rowLbl.setLabelFor(comp); rowLbl.setHorizontalAlignment(SwingConstants.LEFT); if (tooltip != null) { rowLbl.setToolTipText(tooltip); comp.setToolTipText(tooltip); } else { comp.setToolTipText(label); } comp.getAccessibleContext().setAccessibleName(label); c.gridy = row++; c.gridx = 0; c.gridwidth = 1; c.anchor = GridBagConstraints.LINE_START; c.weightx = 0.1; c.fill = GridBagConstraints.LINE_START; gridPanel.add(rowLbl, c); c.gridx = 1; c.gridwidth = GridBagConstraints.REMAINDER; c.anchor = GridBagConstraints.LINE_START; c.weightx = 0.7; c.fill = GridBagConstraints.LINE_START; gridPanel.add(comp, c); comp.addPropertyChangeListener("enabled", evt -> rowLbl.setEnabled((boolean) evt.getNewValue())); return rowLbl; } public void end() { gridPanel.add(Box.createVerticalGlue()); } @Override public JComponent buildComponent() { return panel; } @Override public String getTitle() { return title; } public JPanel getGridPanel() { return gridPanel; } @Override public String toString() { return title; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsTree.java ================================================ package jadx.gui.settings.ui; import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Objects; import javax.swing.JTree; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeWillExpandListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.ExpandVetoException; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.gui.ISettingsGroup; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.utils.NLS; public class SettingsTree extends JTree { private final JadxSettingsWindow settingsWindow; public SettingsTree(JadxSettingsWindow settingsWindow) { this.settingsWindow = settingsWindow; } public void init(List groups) { DefaultMutableTreeNode treeRoot = new DefaultMutableTreeNode(NLS.str("preferences.title")); addGroups(treeRoot, groups); setModel(new DefaultTreeModel(treeRoot)); getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); setFocusable(false); addTreeSelectionListener(ev -> switchGroup()); // expand all nodes and disallow collapsing setNodeExpandedState(this, treeRoot, true); addTreeWillExpandListener(new DisableRootCollapseListener(treeRoot)); addSelectionRow(1); } private static void addGroups(DefaultMutableTreeNode base, List groups) { for (ISettingsGroup group : groups) { SettingsTreeNode node = new SettingsTreeNode(group); base.add(node); addGroups(node, group.getSubGroups()); } } public void selectGroup(ISettingsGroup group) { SettingsTreeNode node = searchTreeNode(group); if (node == null) { throw new JadxRuntimeException("Settings group not found: " + group); } setSelectionPath(new TreePath(node.getPath())); } private @Nullable SettingsTreeNode searchTreeNode(ISettingsGroup group) { DefaultMutableTreeNode root = (DefaultMutableTreeNode) getModel().getRoot(); Enumeration enumeration = root.children(); while (enumeration.hasMoreElements()) { SettingsTreeNode node = (SettingsTreeNode) enumeration.nextElement(); if (node.getGroup() == group) { return node; } } return null; } private void switchGroup() { Object selected = getLastSelectedPathComponent(); if (selected instanceof SettingsTreeNode) { ISettingsGroup group = ((SettingsTreeNode) selected).getGroup(); settingsWindow.activateGroup(group); } else { settingsWindow.activateGroup(null); } } private static void setNodeExpandedState(JTree tree, TreeNode node, boolean expanded) { List list = Collections.list(node.children()); for (TreeNode treeNode : list) { setNodeExpandedState(tree, treeNode, expanded); } DefaultMutableTreeNode mutableTreeNode = (DefaultMutableTreeNode) node; if (!expanded && mutableTreeNode.isRoot()) { return; } TreePath path = new TreePath(mutableTreeNode.getPath()); if (expanded) { tree.expandPath(path); } else { tree.collapsePath(path); } } private static class DisableRootCollapseListener implements TreeWillExpandListener { private final DefaultMutableTreeNode treeRoot; public DisableRootCollapseListener(DefaultMutableTreeNode treeRoot) { this.treeRoot = treeRoot; } @Override public void treeWillExpand(TreeExpansionEvent event) { } @Override public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { Object current = event.getPath().getLastPathComponent(); if (Objects.equals(current, treeRoot)) { throw new ExpandVetoException(event, "Root collapsing not allowed"); } } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/SettingsTreeNode.java ================================================ package jadx.gui.settings.ui; import javax.swing.tree.DefaultMutableTreeNode; import jadx.api.plugins.gui.ISettingsGroup; public class SettingsTreeNode extends DefaultMutableTreeNode { private final ISettingsGroup group; public SettingsTreeNode(ISettingsGroup group) { this.group = group; } public ISettingsGroup getGroup() { return group; } @Override public String toString() { return group.getTitle(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/SubSettingsGroup.java ================================================ package jadx.gui.settings.ui; import java.util.ArrayList; import java.util.List; import jadx.api.plugins.gui.ISettingsGroup; public class SubSettingsGroup extends SettingsGroup { private final List groups = new ArrayList<>(); public SubSettingsGroup(String title) { super(title); } @Override public List getSubGroups() { return groups; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/cache/CacheSettingsGroup.java ================================================ package jadx.gui.settings.ui.cache; import java.awt.BorderLayout; import java.awt.GridLayout; import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Objects; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.UIManager; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.gui.ISettingsGroup; import jadx.gui.cache.code.CodeCacheMode; import jadx.gui.cache.usage.UsageCacheMode; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.ui.JadxSettingsWindow; import jadx.gui.settings.ui.SettingsGroup; import jadx.gui.ui.filedialog.FileDialogWrapper; import jadx.gui.ui.filedialog.FileOpenMode; import jadx.gui.utils.NLS; import jadx.gui.utils.files.JadxFiles; import jadx.gui.utils.ui.DocumentUpdateListener; public class CacheSettingsGroup implements ISettingsGroup { private final String title = NLS.str("preferences.cache"); private final JadxSettingsWindow settingsWindow; private JTextField customDirField; private JButton selectDirBtn; public CacheSettingsGroup(JadxSettingsWindow settingsWindow) { this.settingsWindow = settingsWindow; } @Override public String getTitle() { return title; } @Override public JComponent buildComponent() { JPanel options = new JPanel(); options.setLayout(new BoxLayout(options, BoxLayout.PAGE_AXIS)); options.add(buildBaseOptions()); options.add(buildLocationSelector()); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout()); mainPanel.add(options, BorderLayout.PAGE_START); mainPanel.add(buildCachesView(), BorderLayout.CENTER); return mainPanel; } private JPanel buildCachesView() { CachesTable cachesTable = new CachesTable(settingsWindow.getMainWindow()); JScrollPane scrollPane = new JScrollPane(cachesTable); cachesTable.setFillsViewportHeight(true); cachesTable.updateData(); JButton calcUsage = new JButton(NLS.str("preferences.cache.btn.usage")); calcUsage.addActionListener(ev -> cachesTable.updateSizes()); JButton deleteSelected = new JButton(NLS.str("preferences.cache.btn.delete_selected")); deleteSelected.addActionListener(ev -> cachesTable.deleteSelected()); JButton deleteAll = new JButton(NLS.str("preferences.cache.btn.delete_all")); deleteAll.addActionListener(ev -> cachesTable.deleteAll()); JPanel buttons = new JPanel(); buttons.setLayout(new BoxLayout(buttons, BoxLayout.LINE_AXIS)); buttons.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10)); buttons.add(calcUsage); buttons.add(Box.createHorizontalGlue()); buttons.add(deleteSelected); buttons.add(deleteAll); JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.setBorder(BorderFactory.createTitledBorder(NLS.str("preferences.cache.table.title"))); panel.add(scrollPane, BorderLayout.CENTER); panel.add(buttons, BorderLayout.PAGE_END); return panel; } private JComponent buildLocationSelector() { JPanel panel = new JPanel(); panel.setLayout(new GridLayout(0, 1)); panel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createTitledBorder(NLS.str("preferences.cache.location")), BorderFactory.createEmptyBorder(10, 10, 10, 10))); customDirField = new JTextField(); customDirField.setColumns(10); customDirField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> { settingsWindow.getMainWindow().getSettings().setCacheDir(customDirField.getText()); })); selectDirBtn = new JButton(); selectDirBtn.setIcon(UIManager.getIcon("Tree.closedIcon")); selectDirBtn.addActionListener(e -> { FileDialogWrapper fd = new FileDialogWrapper(settingsWindow.getMainWindow(), FileOpenMode.CUSTOM_OPEN); fd.setFileExtList(Collections.emptyList()); fd.setSelectionMode(JFileChooser.DIRECTORIES_ONLY); List paths = fd.show(); if (!paths.isEmpty()) { String dir = paths.get(0).toAbsolutePath().toString(); customDirField.setText(dir); settingsWindow.getMainWindow().getSettings().setCacheDir(dir); } }); JRadioButton defOpt = new JRadioButton(NLS.str("preferences.cache.location_default")); defOpt.setToolTipText(JadxFiles.CACHE_DIR.toString()); defOpt.addActionListener(e -> changeCacheLocation(null)); JRadioButton localOpt = new JRadioButton(NLS.str("preferences.cache.location_local")); localOpt.addActionListener(e -> changeCacheLocation(".")); JRadioButton customOpt = new JRadioButton(NLS.str("preferences.cache.location_custom")); customOpt.addActionListener(e -> changeCacheLocation("")); ButtonGroup group = new ButtonGroup(); group.add(defOpt); group.add(localOpt); group.add(customOpt); panel.add(defOpt); panel.add(localOpt); JPanel custom = new JPanel(); custom.setLayout(new BoxLayout(custom, BoxLayout.LINE_AXIS)); custom.add(customOpt); custom.add(Box.createHorizontalStrut(15)); custom.add(customDirField); custom.add(selectDirBtn); panel.add(custom); String cacheDir = settingsWindow.getMainWindow().getSettings().getCacheDir(); if (cacheDir == null) { defOpt.setSelected(true); changeCacheLocation(null); } else if (cacheDir.equals(".")) { localOpt.setSelected(true); changeCacheLocation(cacheDir); } else { customOpt.setSelected(true); customDirField.setText(cacheDir); changeCacheLocation(""); } JLabel notice = new JLabel(NLS.str("preferences.cache.change_notice")); notice.setEnabled(false); panel.add(notice); return panel; } private void changeCacheLocation(@Nullable String locValue) { boolean custom = Objects.equals(locValue, ""); customDirField.setEnabled(custom); selectDirBtn.setEnabled(custom); if (!custom) { settingsWindow.getMainWindow().getSettings().setCacheDir(locValue); } } private JComponent buildBaseOptions() { JadxSettings settings = settingsWindow.getMainWindow().getSettings(); JComboBox codeCacheModeComboBox = new JComboBox<>(CodeCacheMode.values()); codeCacheModeComboBox.setSelectedItem(settings.getCodeCacheMode()); codeCacheModeComboBox.addActionListener(e -> { settings.setCodeCacheMode((CodeCacheMode) codeCacheModeComboBox.getSelectedItem()); settingsWindow.needReload(); }); JComboBox usageCacheModeComboBox = new JComboBox<>(UsageCacheMode.values()); usageCacheModeComboBox.setSelectedItem(settings.getUsageCacheMode()); usageCacheModeComboBox.addActionListener(e -> { settings.setUsageCacheMode((UsageCacheMode) usageCacheModeComboBox.getSelectedItem()); settingsWindow.needReload(); }); SettingsGroup group = new SettingsGroup(title); group.addRow(NLS.str("preferences.codeCacheMode"), CodeCacheMode.buildToolTip(), codeCacheModeComboBox); group.addRow(NLS.str("preferences.usageCacheMode"), usageCacheModeComboBox); return group.buildComponent(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/cache/CachesTable.java ================================================ package jadx.gui.settings.ui.cache; import java.awt.Dimension; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.swing.JTable; import javax.swing.ListSelectionModel; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.events.types.ReloadProject; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.gui.cache.manager.CacheManager; import jadx.gui.settings.JadxProject; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.MousePressedHandler; public class CachesTable extends JTable { private static final long serialVersionUID = 5984107298264276049L; private static final Logger LOG = LoggerFactory.getLogger(CachesTable.class); private final MainWindow mainWindow; private final CachesTableModel dataModel; public CachesTable(MainWindow mainWindow) { this.mainWindow = mainWindow; this.dataModel = new CachesTableModel(); setModel(dataModel); setDefaultRenderer(Object.class, new CachesTableRenderer()); setSelectionMode(ListSelectionModel.SINGLE_SELECTION); setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); setShowHorizontalLines(true); setDragEnabled(false); setColumnSelectionAllowed(false); setAutoscrolls(true); setFocusable(false); addMouseListener(new MousePressedHandler(ev -> { int row = rowAtPoint(ev.getPoint()); if (row != -1) { dataModel.changeSelection(row); UiUtils.uiRun(this::updateUI); } })); } public void updateData() { List rows = mainWindow.getCacheManager().getCachesList().stream() .map(TableRow::new) .collect(Collectors.toList()); updateRows(rows); } public void reloadData() { Map prevUsageMap = dataModel.getRows().stream() .collect(Collectors.toMap(TableRow::getProject, TableRow::getUsage)); List rows = mainWindow.getCacheManager().getCachesList().stream() .map(TableRow::new) .peek(r -> r.setUsage(Utils.getOrElse(prevUsageMap.get(r.getProject()), "-"))) .collect(Collectors.toList()); updateRows(rows); } private void updateRows(List rows) { dataModel.setRows(rows); // fix allocated space for default 20 rows int width = getPreferredSize().width; int height = rows.size() * getRowHeight(); setPreferredScrollableViewportSize(new Dimension(width, height)); UiUtils.uiRun(this::updateUI); } public void updateSizes() { List list = dataModel.getRows().stream() .map(row -> (Runnable) () -> calcSize(row)) .collect(Collectors.toList()); mainWindow.getBackgroundExecutor().execute( NLS.str("preferences.cache.task.usage"), list, status -> updateUI()); } private void calcSize(TableRow row) { String cacheDir = row.getCacheEntry().getCache(); try { Path dir = Paths.get(cacheDir); if (Files.isDirectory(dir)) { long size = calcSizeOfDirectory(dir); row.setUsage(FileUtils.byteCountToDisplaySize(size)); } else { row.setUsage("not found"); } } catch (Exception e) { LOG.warn("Failed to calculate size of directory: {}", cacheDir, e); row.setUsage("error"); } } private static long calcSizeOfDirectory(Path dir) { try (Stream stream = Files.walk(dir)) { long blockSize = Files.getFileStore(dir).getBlockSize(); return stream.mapToLong(p -> { if (Files.isRegularFile(p)) { try { long fileSize = Files.size(p); // ceil round to blockSize return (fileSize / blockSize + 1L) * blockSize; } catch (Exception e) { LOG.error("Failed to get file size: {}", p, e); } } return 0; }).sum(); } catch (Exception e) { LOG.error("Failed to calculate directory size: {}", dir, e); return 0; } } public void deleteSelected() { delete(ListUtils.filter(dataModel.getRows(), TableRow::isSelected)); } public void deleteAll() { delete(dataModel.getRows()); } private void delete(List rows) { // force reload if cache for current project is deleted boolean reload = searchCurrentProject(rows); List list = rows.stream() .map(TableRow::getCacheEntry) .map(entry -> (Runnable) () -> mainWindow.getCacheManager().removeCacheEntry(entry)) .collect(Collectors.toList()); mainWindow.getBackgroundExecutor().execute( NLS.str("preferences.cache.task.delete"), list, status -> { reloadData(); if (reload) { mainWindow.events().send(ReloadProject.EVENT); } }); } private boolean searchCurrentProject(List rows) { JadxProject project = mainWindow.getProject(); if (!project.getFilePaths().isEmpty()) { String cacheStr = CacheManager.pathToString(project.getCacheDir()); for (TableRow row : rows) { if (row.getCacheEntry().getCache().equals(cacheStr)) { project.resetCacheDir(); LOG.debug("Found current project in cache delete list -> request full reload"); return true; } } } return false; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/cache/CachesTableModel.java ================================================ package jadx.gui.settings.ui.cache; import java.util.Collections; import java.util.List; import javax.swing.table.AbstractTableModel; import jadx.gui.utils.NLS; public class CachesTableModel extends AbstractTableModel { private static final long serialVersionUID = -7725573085995496397L; private static final String[] COLUMN_NAMES = { NLS.str("preferences.cache.table.project"), NLS.str("preferences.cache.table.size") }; private transient List rows = Collections.emptyList(); public void setRows(List list) { this.rows = list; } public List getRows() { return rows; } @Override public int getRowCount() { return rows.size(); } @Override public int getColumnCount() { return 2; } @Override public String getColumnName(int index) { return COLUMN_NAMES[index]; } @Override public Class getColumnClass(int columnIndex) { return TableRow.class; } @Override public TableRow getValueAt(int rowIndex, int columnIndex) { return rows.get(rowIndex); } public void changeSelection(int idx) { TableRow row = rows.get(idx); row.setSelected(!row.isSelected()); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/cache/CachesTableRenderer.java ================================================ package jadx.gui.settings.ui.cache; import java.awt.Component; import javax.swing.JLabel; import javax.swing.JTable; import javax.swing.table.TableCellRenderer; public class CachesTableRenderer implements TableCellRenderer { private final JLabel label; public CachesTableRenderer() { label = new JLabel(); label.setOpaque(true); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { TableRow obj = (TableRow) value; switch (column) { case 0: label.setText(obj.getProject()); break; case 1: label.setText(obj.getUsage()); break; } label.setToolTipText(obj.getCacheEntry().getCache()); if (obj.isSelected()) { label.setBackground(table.getSelectionBackground()); label.setForeground(table.getSelectionForeground()); } else { label.setBackground(table.getBackground()); label.setForeground(table.getForeground()); } return label; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/cache/TableRow.java ================================================ package jadx.gui.settings.ui.cache; import java.nio.file.Paths; import jadx.api.plugins.utils.CommonFileUtils; import jadx.gui.cache.manager.CacheEntry; final class TableRow { private final CacheEntry cacheEntry; private final String project; private String usage; private boolean selected = false; public TableRow(CacheEntry cacheEntry) { this.cacheEntry = cacheEntry; this.project = cutProjectName(cacheEntry.getProject()); this.usage = "-"; } private String cutProjectName(String project) { if (project.startsWith("tmp:")) { int hashStart = project.lastIndexOf('-'); int endIdx = hashStart != -1 ? hashStart : project.length(); return project.substring(4, endIdx) + " (Temp)"; } return CommonFileUtils.removeFileExtension(Paths.get(project).getFileName().toString()); } public CacheEntry getCacheEntry() { return cacheEntry; } public String getProject() { return project; } public String getUsage() { return usage; } public void setUsage(String usage) { this.usage = usage; } public boolean isSelected() { return selected; } public void setSelected(boolean selected) { this.selected = selected; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/font/FontChooserHack.java ================================================ package jadx.gui.settings.ui.font; import java.lang.reflect.Field; import javax.swing.JCheckBox; import javax.swing.JPanel; import org.drjekyll.fontchooser.FontChooser; import org.drjekyll.fontchooser.panes.FamilyPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FontChooserHack { private static final Logger LOG = LoggerFactory.getLogger(FontChooserHack.class); public static void setOnlyMonospace(FontChooser fontChooser) { try { FamilyPane familyPane = (FamilyPane) getPrivateField(fontChooser, "familyPane"); JCheckBox monospacedCheckBox = (JCheckBox) getPrivateField(familyPane, "monospacedCheckBox"); monospacedCheckBox.setSelected(true); monospacedCheckBox.setEnabled(false); } catch (Throwable e) { LOG.debug("Failed to set only monospace check box", e); } } public static void hidePreview(FontChooser fontChooser) { try { JPanel previewPanel = (JPanel) getPrivateField(fontChooser, "previewPanel"); previewPanel.setVisible(false); } catch (Throwable e) { LOG.debug("Failed to hide preview panel", e); } } private static Object getPrivateField(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException { Field f = obj.getClass().getDeclaredField(fieldName); f.setAccessible(true); return f.get(obj); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/font/JadxFontDialog.java ================================================ package jadx.gui.settings.ui.font; import java.awt.BorderLayout; import java.awt.Dialog; import java.awt.FlowLayout; import java.awt.Font; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JPanel; import javax.swing.WindowConstants; import org.drjekyll.fontchooser.FontChooser; import org.jetbrains.annotations.Nullable; import jadx.gui.settings.JadxSettings; import jadx.gui.utils.FontUtils; import jadx.gui.utils.NLS; public class JadxFontDialog extends JDialog { private static final long serialVersionUID = 7609857698785777587L; private final FontChooser fontChooser = new FontChooser(); private final JadxSettings settings; private boolean selected = false; public JadxFontDialog(Dialog parent, JadxSettings settings, String title) { super(parent, title, true); this.settings = settings; initComponents(); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); if (!settings.loadWindowPos(this)) { pack(); } } public @Nullable Font select(Font currentFont, boolean onlyMonospace) { fontChooser.setSelectedFont(currentFont); if (onlyMonospace) { FontChooserHack.setOnlyMonospace(fontChooser); } setVisible(true); Font selectedFont = fontChooser.getSelectedFont(); if (selected && !selectedFont.equals(currentFont)) { return FontUtils.getCompositeFont(selectedFont.getFamily(), selectedFont.getStyle(), selectedFont.getSize()); } return null; } private void initComponents() { JPanel chooserPanel = new JPanel(); chooserPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 0, 10)); chooserPanel.setLayout(new BorderLayout(0, 10)); chooserPanel.add(fontChooser); JPanel controlPanel = new JPanel(); controlPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); controlPanel.setLayout(new FlowLayout(FlowLayout.TRAILING)); JButton okBtn = new JButton(); okBtn.setText(NLS.str("common_dialog.ok")); okBtn.setMnemonic('o'); okBtn.addActionListener(event -> { selected = true; dispose(); }); JButton cancelBtn = new JButton(); cancelBtn.setText(NLS.str("common_dialog.cancel")); cancelBtn.setMnemonic('c'); cancelBtn.addActionListener(event -> dispose()); controlPanel.add(okBtn); controlPanel.add(cancelBtn); add(chooserPanel); add(controlPanel, BorderLayout.PAGE_END); getRootPane().setDefaultButton(okBtn); } @Override public void dispose() { settings.saveWindowPos(this); super.dispose(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/AvailablePluginNode.java ================================================ package jadx.gui.settings.ui.plugins; import jadx.plugins.tools.data.JadxPluginListEntry; public class AvailablePluginNode extends BasePluginListNode { private final JadxPluginListEntry metadata; public AvailablePluginNode(JadxPluginListEntry metadata) { this.metadata = metadata; } @Override public String getTitle() { return metadata.getName(); } @Override public boolean hasDetails() { return true; } @Override public String getPluginId() { return metadata.getPluginId(); } @Override public String getDescription() { return metadata.getDescription(); } @Override public String getHomepage() { return metadata.getHomepage(); } @Override public String getLocationId() { return metadata.getLocationId(); } @Override public PluginAction getAction() { return PluginAction.INSTALL; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/BasePluginListNode.java ================================================ package jadx.gui.settings.ui.plugins; import org.jetbrains.annotations.Nullable; abstract class BasePluginListNode { public abstract String getTitle(); public abstract boolean hasDetails(); public String getPluginId() { return null; } public String getDescription() { return null; } public String getHomepage() { return null; } public @Nullable String getLocationId() { return null; } public @Nullable String getVersion() { return null; } public boolean isDisabled() { return false; } public PluginAction getAction() { return PluginAction.NONE; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/InstallPluginDialog.java ================================================ package jadx.gui.settings.ui.plugins; import java.awt.BorderLayout; import java.awt.Dimension; import java.nio.file.Path; import java.util.List; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.WindowConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.formdev.flatlaf.FlatClientProperties; import jadx.gui.ui.MainWindow; import jadx.gui.ui.filedialog.FileDialogWrapper; import jadx.gui.ui.filedialog.FileOpenMode; import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; import jadx.gui.utils.UiUtils; public class InstallPluginDialog extends JDialog { private static final Logger LOG = LoggerFactory.getLogger(InstallPluginDialog.class); private static final long serialVersionUID = 5304314264730563853L; private final MainWindow mainWindow; private final PluginSettings pluginsSettings; private JTextField locationFld; public InstallPluginDialog(MainWindow mainWindow, PluginSettings pluginsSettings) { super(mainWindow, NLS.str("preferences.plugins.install")); this.mainWindow = mainWindow; this.pluginsSettings = pluginsSettings; init(); } private void init() { locationFld = new JTextField(); locationFld.setAlignmentX(LEFT_ALIGNMENT); locationFld.setColumns(50); TextStandardActions.attach(locationFld); locationFld.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true); JLabel locationLbl = new JLabel(NLS.str("preferences.plugins.location_id_label")); locationLbl.setLabelFor(locationFld); JPanel locationPanel = new JPanel(); locationPanel.setLayout(new BoxLayout(locationPanel, BoxLayout.LINE_AXIS)); locationPanel.add(locationLbl); locationPanel.add(Box.createRigidArea(new Dimension(5, 0))); locationPanel.add(locationFld); JButton fileBtn = new JButton(NLS.str("preferences.plugins.plugin_jar")); fileBtn.addActionListener(ev -> openPluginFile()); JLabel fileLbl = new JLabel(NLS.str("preferences.plugins.plugin_jar_label")); fileLbl.setLabelFor(fileBtn); JPanel filePanel = new JPanel(); filePanel.setLayout(new BoxLayout(filePanel, BoxLayout.LINE_AXIS)); filePanel.add(fileLbl); filePanel.add(Box.createRigidArea(new Dimension(5, 0))); filePanel.add(fileBtn); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); mainPanel.add(locationPanel); mainPanel.add(Box.createRigidArea(new Dimension(0, 5))); mainPanel.add(filePanel); JButton installBtn = new JButton(NLS.str("preferences.plugins.install_btn")); installBtn.addActionListener(ev -> install()); JButton cancelBtn = new JButton(NLS.str("preferences.cancel")); cancelBtn.addActionListener(ev -> dispose()); JPanel buttonPane = new JPanel(); buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); // TODO: add operation progress buttonPane.add(Box.createHorizontalGlue()); buttonPane.add(installBtn); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(cancelBtn); getRootPane().setDefaultButton(installBtn); JPanel contentPanel = new JPanel(); contentPanel.setLayout(new BorderLayout(5, 5)); contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); contentPanel.add(mainPanel, BorderLayout.PAGE_START); contentPanel.add(buttonPane, BorderLayout.PAGE_END); getContentPane().add(contentPanel); pack(); setLocationRelativeTo(null); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setModalityType(ModalityType.APPLICATION_MODAL); UiUtils.addEscapeShortCutToDispose(this); } private void openPluginFile() { FileDialogWrapper fd = new FileDialogWrapper(mainWindow, FileOpenMode.CUSTOM_OPEN); fd.setTitle(NLS.str("preferences.plugins.plugin_jar")); fd.setFileExtList(List.of("jar", "zip")); fd.setSelectionMode(JFileChooser.FILES_ONLY); List files = fd.show(); if (files.size() == 1) { locationFld.setText("file:" + files.get(0).toAbsolutePath()); } } private void install() { pluginsSettings.install(locationFld.getText()); dispose(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/InstalledPluginNode.java ================================================ package jadx.gui.settings.ui.plugins; import org.jetbrains.annotations.Nullable; import jadx.plugins.tools.data.JadxPluginMetadata; public class InstalledPluginNode extends BasePluginListNode { private final JadxPluginMetadata metadata; public InstalledPluginNode(JadxPluginMetadata metadata) { this.metadata = metadata; } @Override public @Nullable String getTitle() { return metadata.getName(); } @Override public boolean hasDetails() { return true; } @Override public String getPluginId() { return metadata.getPluginId(); } @Override public String getDescription() { return metadata.getDescription(); } @Override public String getHomepage() { return metadata.getHomepage(); } @Override public PluginAction getAction() { return PluginAction.UNINSTALL; } @Override public @Nullable String getVersion() { return metadata.getVersion(); } @Override public boolean isDisabled() { return metadata.isDisabled(); } @Override public String toString() { return metadata.getName(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/LoadedPluginNode.java ================================================ package jadx.gui.settings.ui.plugins; import org.jetbrains.annotations.Nullable; import jadx.core.plugins.PluginContext; public class LoadedPluginNode extends BasePluginListNode { private final PluginContext plugin; public LoadedPluginNode(PluginContext plugin) { this.plugin = plugin; } @Override public @Nullable String getTitle() { return plugin.getPluginInfo().getName(); } @Override public boolean hasDetails() { return true; } @Override public String getPluginId() { return plugin.getPluginId(); } @Override public String getDescription() { return plugin.getPluginInfo().getDescription(); } @Override public String getHomepage() { return plugin.getPluginInfo().getHomepage(); } @Override public String toString() { return plugin.getPluginInfo().getName(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginAction.java ================================================ package jadx.gui.settings.ui.plugins; public enum PluginAction { NONE, INSTALL, UNINSTALL } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java ================================================ package jadx.gui.settings.ui.plugins; import java.awt.event.ItemEvent; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import java.util.function.IntSupplier; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JSpinner; import javax.swing.JTextField; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ch.qos.logback.classic.Level; import jadx.api.plugins.events.types.ReloadProject; import jadx.api.plugins.gui.ISettingsGroup; import jadx.api.plugins.gui.JadxGuiContext; import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.options.OptionDescription; import jadx.api.plugins.options.OptionFlag; import jadx.api.plugins.options.OptionType; import jadx.core.plugins.PluginContext; import jadx.core.utils.Utils; import jadx.gui.logs.LogOptions; import jadx.gui.plugins.context.GuiPluginContext; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.ui.SettingsGroup; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.plugins.CloseablePlugins; import jadx.gui.utils.plugins.CollectPlugins; import jadx.gui.utils.plugins.SettingsGroupPluginWrap; import jadx.gui.utils.ui.DocumentUpdateListener; import jadx.plugins.tools.JadxPluginsTools; import jadx.plugins.tools.data.JadxPluginMetadata; import jadx.plugins.tools.data.JadxPluginUpdate; public class PluginSettings { private static final Logger LOG = LoggerFactory.getLogger(PluginSettings.class); private final MainWindow mainWindow; private final JadxSettings settings; public PluginSettings(MainWindow mainWindow, JadxSettings settings) { this.mainWindow = mainWindow; this.settings = settings; } public ISettingsGroup build() { CloseablePlugins collectedPlugins = new CollectPlugins(mainWindow).build(); ISettingsGroup pluginsGroup = new PluginSettingsGroup(this, mainWindow, collectedPlugins); for (PluginContext context : collectedPlugins.getList()) { ISettingsGroup pluginGroup = addPluginGroup(context); if (pluginGroup != null) { pluginsGroup.getSubGroups().add(new SettingsGroupPluginWrap(context.getPluginId(), pluginGroup)); } } return pluginsGroup; } public void addPlugin() { new InstallPluginDialog(mainWindow, this).setVisible(true); } private void requestReload() { mainWindow.events().send(ReloadProject.EVENT); } public void install(String locationId) { mainWindow.getBackgroundExecutor().execute(NLS.str("preferences.plugins.task.installing"), () -> { try { JadxPluginMetadata metadata = JadxPluginsTools.getInstance().install(locationId); LOG.info("Plugin installed: {}", metadata); requestReload(); } catch (Exception e) { LOG.error("Plugin install failed", e); mainWindow.showLogViewer(LogOptions.forLevel(Level.ERROR)); } }); } public void uninstall(String pluginId) { mainWindow.getBackgroundExecutor().execute(NLS.str("preferences.plugins.task.uninstalling"), () -> { boolean success = JadxPluginsTools.getInstance().uninstall(pluginId); if (success) { LOG.info("Uninstall complete"); requestReload(); } else { LOG.warn("Uninstall failed"); } }); } public void changeDisableStatus(String pluginId, boolean disabled) { mainWindow.getBackgroundExecutor().execute( NLS.str("preferences.plugins.task.status"), () -> JadxPluginsTools.getInstance().changeDisabledStatus(pluginId, disabled), s -> requestReload()); } void updateAll() { mainWindow.getBackgroundExecutor().execute(NLS.str("preferences.plugins.task.updating"), () -> { List updates = JadxPluginsTools.getInstance().updateAll(); if (!updates.isEmpty()) { LOG.info("Updates: {}\n ", Utils.listToString(updates, "\n ")); requestReload(); } else { LOG.info("No updates found"); } }); } private ISettingsGroup addPluginGroup(PluginContext context) { JadxGuiContext guiContext = context.getGuiContext(); if (guiContext instanceof GuiPluginContext) { GuiPluginContext pluginGuiContext = (GuiPluginContext) guiContext; ISettingsGroup customSettingsGroup = pluginGuiContext.getCustomSettingsGroup(); if (customSettingsGroup != null) { return customSettingsGroup; } } JadxPluginOptions options = context.getOptions(); if (options == null) { return null; } List optionsDescriptions = options.getOptionsDescriptions(); if (optionsDescriptions.isEmpty()) { return null; } SettingsGroup settingsGroup = new SettingsGroup(context.getPluginInfo().getName()); addOptions(settingsGroup, optionsDescriptions); return settingsGroup; } public void addOptions(SettingsGroup pluginGroup, List optionsDescriptions) { for (OptionDescription opt : optionsDescriptions) { if (opt.getFlags().contains(OptionFlag.HIDE_IN_GUI)) { continue; } String optName = opt.name(); String title = opt.description(); Consumer updateFunc; String curValue; if (opt.getFlags().contains(OptionFlag.PER_PROJECT)) { JadxProject project = mainWindow.getProject(); updateFunc = value -> project.updatePluginOptions(m -> m.put(optName, value)); curValue = project.getPluginOption(optName); } else { Map optionsMap = settings.getPluginOptions(); updateFunc = value -> optionsMap.put(optName, value); curValue = optionsMap.get(optName); } String value = curValue != null ? curValue : opt.defaultValue(); JComponent editor = null; if (opt.values().isEmpty() || opt.getType() == OptionType.BOOLEAN) { try { editor = getPluginOptionEditor(opt, value, updateFunc); } catch (Exception e) { LOG.error("Failed to add editor for plugin option: {}", optName, e); } } else { JComboBox combo = new JComboBox<>(opt.values().toArray(new String[0])); combo.setSelectedItem(value); combo.addActionListener(e -> updateFunc.accept((String) combo.getSelectedItem())); editor = combo; } if (editor != null) { JLabel label = pluginGroup.addRow(title, editor); boolean enabled = !opt.getFlags().contains(OptionFlag.DISABLE_IN_GUI); if (!enabled) { label.setEnabled(false); editor.setEnabled(false); } } } } private JComponent getPluginOptionEditor(OptionDescription opt, String value, Consumer updateFunc) { switch (opt.getType()) { case STRING: JTextField textField = new JTextField(); textField.setText(value == null ? "" : value); textField.getDocument().addDocumentListener( new DocumentUpdateListener(event -> updateFunc.accept(textField.getText()))); return textField; case NUMBER: JSpinner numberField = new JSpinner(); numberField.setValue(safeStringToInt(value, () -> safeStringToInt(opt.defaultValue(), () -> { throw new IllegalArgumentException("Failed to parse integer default value: " + opt.defaultValue()); }))); numberField.addChangeListener(e -> updateFunc.accept(numberField.getValue().toString())); return numberField; case BOOLEAN: JCheckBox boolField = new JCheckBox(); boolField.setSelected(Objects.equals(value, "yes") || Objects.equals(value, "true")); boolField.addItemListener(e -> { boolean editorValue = e.getStateChange() == ItemEvent.SELECTED; updateFunc.accept(editorValue ? "yes" : "no"); }); return boolField; } return null; } private static int safeStringToInt(String value, IntSupplier defValueSupplier) { if (value == null) { return defValueSupplier.getAsInt(); } try { return Integer.parseInt(value); } catch (Exception e) { LOG.warn("Failed parse string to int: {}", value, e); return defValueSupplier.getAsInt(); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java ================================================ package jadx.gui.settings.ui.plugins; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTextPane; import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.gui.ISettingsGroup; import jadx.core.plugins.PluginContext; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.gui.ui.MainWindow; import jadx.gui.utils.Link; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.plugins.CloseablePlugins; import jadx.plugins.tools.JadxPluginsList; import jadx.plugins.tools.JadxPluginsTools; import jadx.plugins.tools.data.JadxPluginMetadata; class PluginSettingsGroup implements ISettingsGroup { private static final Logger LOG = LoggerFactory.getLogger(PluginSettingsGroup.class); private final PluginSettings pluginsSettings; private final MainWindow mainWindow; private final String title; private final List subGroups = new ArrayList<>(); private final CloseablePlugins collectedPlugins; private JPanel detailsPanel; public PluginSettingsGroup(PluginSettings pluginSettings, MainWindow mainWindow, CloseablePlugins collectedPlugins) { this.pluginsSettings = pluginSettings; this.mainWindow = mainWindow; this.title = NLS.str("preferences.plugins"); this.collectedPlugins = collectedPlugins; } @Override public String getTitle() { return title; } @Override public List getSubGroups() { return subGroups; } @Override public JComponent buildComponent() { // lazy load main page return buildMainSettingsPage(); } @Override public void close(boolean save) { subGroups.forEach(subGroup -> subGroup.close(save)); collectedPlugins.close(); } private JPanel buildMainSettingsPage() { JButton installPluginBtn = new JButton(NLS.str("preferences.plugins.install")); installPluginBtn.addActionListener(ev -> pluginsSettings.addPlugin()); JButton updateAllBtn = new JButton(NLS.str("preferences.plugins.update_all")); updateAllBtn.addActionListener(ev -> pluginsSettings.updateAll()); JPanel actionsPanel = new JPanel(); actionsPanel.setLayout(new BoxLayout(actionsPanel, BoxLayout.LINE_AXIS)); actionsPanel.add(installPluginBtn); actionsPanel.add(Box.createRigidArea(new Dimension(5, 0))); actionsPanel.add(updateAllBtn); DefaultListModel listModel = new DefaultListModel<>(); JList pluginList = new JList<>(listModel); pluginList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); pluginList.setCellRenderer(new PluginsListCellRenderer()); pluginList.addListSelectionListener(ev -> onSelection(pluginList.getSelectedValue())); pluginList.setFocusable(true); JScrollPane scrollPane = new JScrollPane(pluginList); scrollPane.setMinimumSize(new Dimension(80, 120)); detailsPanel = new JPanel(new BorderLayout(5, 5)); detailsPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createTitledBorder(NLS.str("preferences.plugins.details")), BorderFactory.createEmptyBorder(10, 10, 10, 10))); detailsPanel.setLayout(new BoxLayout(detailsPanel, BoxLayout.PAGE_AXIS)); JSplitPane splitPanel = new JSplitPane(); splitPanel.setBorder(BorderFactory.createEmptyBorder(10, 2, 2, 2)); splitPanel.setLeftComponent(scrollPane); splitPanel.setRightComponent(detailsPanel); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout(5, 5)); mainPanel.setBorder(BorderFactory.createTitledBorder(title)); mainPanel.add(actionsPanel, BorderLayout.PAGE_START); mainPanel.add(splitPanel, BorderLayout.CENTER); applyData(listModel); return mainPanel; } private void applyData(DefaultListModel listModel) { List installed = JadxPluginsTools.getInstance().getInstalled(); List nodes = new ArrayList<>(installed.size() + collectedPlugins.getList().size()); Set installedSet = new HashSet<>(installed.size()); for (JadxPluginMetadata pluginMetadata : installed) { installedSet.add(pluginMetadata.getPluginId()); nodes.add(new InstalledPluginNode(pluginMetadata)); } for (PluginContext plugin : collectedPlugins.getList()) { if (!installedSet.contains(plugin.getPluginId())) { nodes.add(new LoadedPluginNode(plugin)); } } nodes.sort(Comparator.comparing(BasePluginListNode::getTitle)); fillListModel(listModel, nodes, Collections.emptyList()); loadAvailablePlugins(listModel, nodes, installedSet); } private static void fillListModel(DefaultListModel listModel, List nodes, List available) { listModel.clear(); listModel.addElement(new TitleNode("Installed")); nodes.stream().filter(n -> n.getAction() == PluginAction.UNINSTALL).forEach(listModel::addElement); listModel.addElement(new TitleNode("Available")); listModel.addAll(available); listModel.addElement(new TitleNode("Bundled")); nodes.stream().filter(n -> n.getAction() == PluginAction.NONE).forEach(listModel::addElement); } private void loadAvailablePlugins(DefaultListModel listModel, List nodes, Set installedSet) { mainWindow.getBackgroundExecutor().execute( NLS.str("preferences.plugins.task.downloading_list"), () -> { try { JadxPluginsList.getInstance().get(availablePlugins -> { List availableNodes = availablePlugins.stream() .filter(availablePlugin -> !installedSet.contains(availablePlugin.getPluginId())) .map(AvailablePluginNode::new) .collect(Collectors.toList()); UiUtils.uiRunAndWait(() -> fillListModel(listModel, nodes, availableNodes)); }); } catch (Exception e) { LOG.warn("Failed to load available plugins list", e); } }); } private void onSelection(BasePluginListNode node) { detailsPanel.removeAll(); if (node.hasDetails()) { JLabel nameLbl = new JLabel(node.getTitle()); Font baseFont = nameLbl.getFont(); nameLbl.setFont(baseFont.deriveFont(Font.BOLD, baseFont.getSize2D() + 2)); JLabel homeLink = null; String homepage = node.getHomepage(); if (StringUtils.notBlank(homepage)) { homeLink = new Link("Homepage: " + homepage, homepage); homeLink.setHorizontalAlignment(SwingConstants.LEFT); } JTextPane descArea = new JTextPane(); descArea.setText(node.getDescription()); descArea.setFont(baseFont.deriveFont(baseFont.getSize2D() + 1)); descArea.setEditable(false); descArea.setBorder(BorderFactory.createEmptyBorder()); descArea.setOpaque(true); JPanel top = new JPanel(); top.setLayout(new BoxLayout(top, BoxLayout.LINE_AXIS)); top.setBorder(BorderFactory.createEmptyBorder(10, 2, 10, 2)); top.add(nameLbl); top.add(Box.createHorizontalGlue()); JButton actionBtn = makeActionButton(node); if (actionBtn != null) { top.add(actionBtn); } if (node.getAction() == PluginAction.UNINSTALL) { // TODO: allow disable bundled plugins boolean disabled = node.isDisabled(); String statusChangeLabel = disabled ? NLS.str("preferences.plugins.enable_btn") : NLS.str("preferences.plugins.disable_btn"); JButton statusBtn = new JButton(statusChangeLabel); statusBtn.addActionListener(ev -> pluginsSettings.changeDisableStatus(node.getPluginId(), !disabled)); top.add(Box.createHorizontalStrut(10)); top.add(statusBtn); } JPanel center = new JPanel(); center.setLayout(new BoxLayout(center, BoxLayout.PAGE_AXIS)); center.setBorder(BorderFactory.createEmptyBorder(10, 2, 10, 2)); center.add(descArea); if (homeLink != null) { JPanel link = new JPanel(); link.setLayout(new BoxLayout(link, BoxLayout.LINE_AXIS)); link.add(homeLink); link.add(Box.createHorizontalGlue()); center.add(link); } center.add(Box.createVerticalGlue()); detailsPanel.add(top, BorderLayout.PAGE_START); detailsPanel.add(center, BorderLayout.CENTER); } detailsPanel.updateUI(); } private @Nullable JButton makeActionButton(BasePluginListNode node) { switch (node.getAction()) { case NONE: return null; case INSTALL: { JButton installBtn = new JButton(NLS.str("preferences.plugins.install_btn")); installBtn.addActionListener(ev -> pluginsSettings.install(node.getLocationId())); return installBtn; } case UNINSTALL: { JButton uninstallBtn = new JButton(NLS.str("preferences.plugins.uninstall_btn")); uninstallBtn.addActionListener(ev -> pluginsSettings.uninstall(node.getPluginId())); return uninstallBtn; } } return null; } private static class PluginsListCellRenderer implements ListCellRenderer { private final JPanel panel; private final JLabel nameLbl; private final JLabel versionLbl; private final JLabel titleLbl; public PluginsListCellRenderer() { panel = new JPanel(); panel.setOpaque(true); panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); panel.setBorder(BorderFactory.createEmptyBorder(2, 10, 2, 10)); nameLbl = new JLabel(""); nameLbl.setFont(nameLbl.getFont().deriveFont(Font.BOLD)); nameLbl.setOpaque(true); versionLbl = new JLabel(""); versionLbl.setOpaque(true); versionLbl.setPreferredSize(new Dimension(40, 10)); panel.add(nameLbl); panel.add(Box.createHorizontalStrut(20)); panel.add(Box.createHorizontalGlue()); panel.add(versionLbl); panel.add(Box.createHorizontalStrut(10)); titleLbl = new JLabel(); titleLbl.setHorizontalAlignment(SwingConstants.CENTER); titleLbl.setPreferredSize(new Dimension(40, 10)); } @Override public Component getListCellRendererComponent(JList list, BasePluginListNode plugin, int index, boolean isSelected, boolean cellHasFocus) { if (!plugin.hasDetails()) { titleLbl.setText(plugin.getTitle()); return titleLbl; } nameLbl.setText(plugin.getTitle()); nameLbl.setToolTipText(plugin.getLocationId()); versionLbl.setText(Utils.getOrElse(plugin.getVersion(), "")); panel.getAccessibleContext().setAccessibleName(plugin.getTitle()); boolean enabled = !plugin.isDisabled(); nameLbl.setEnabled(enabled); versionLbl.setEnabled(enabled); if (isSelected) { panel.setBackground(list.getSelectionBackground()); nameLbl.setBackground(list.getSelectionBackground()); nameLbl.setForeground(list.getSelectionForeground()); versionLbl.setBackground(list.getSelectionBackground()); } else { panel.setBackground(list.getBackground()); nameLbl.setBackground(list.getBackground()); nameLbl.setForeground(list.getForeground()); versionLbl.setBackground(list.getBackground()); } return panel; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/TitleNode.java ================================================ package jadx.gui.settings.ui.plugins; public class TitleNode extends BasePluginListNode { private final String title; public TitleNode(String title) { this.title = title; } @Override public String getTitle() { return title; } @Override public boolean hasDetails() { return false; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/shortcut/ShortcutEdit.java ================================================ package jadx.gui.settings.ui.shortcut; import java.awt.AWTEvent; import java.awt.KeyboardFocusManager; import java.awt.Toolkit; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import javax.swing.BoxLayout; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.UIManager; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.ui.JadxSettingsWindow; import jadx.gui.ui.action.ActionModel; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.shortcut.Shortcut; public class ShortcutEdit extends JPanel { private static final Icon CLEAR_ICON = UiUtils.openSvgIcon("ui/close"); private final ActionModel actionModel; private final JadxSettingsWindow settingsWindow; private final JadxSettings settings; private final TextField textField; public Shortcut shortcut; public ShortcutEdit(ActionModel actionModel, JadxSettingsWindow settingsWindow, JadxSettings settings) { this.actionModel = actionModel; this.settings = settings; this.settingsWindow = settingsWindow; textField = new TextField(); JButton clearButton = new JButton(CLEAR_ICON); setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); add(textField); add(clearButton); clearButton.addActionListener(e -> { setShortcut(Shortcut.none()); saveShortcut(); }); } public void setShortcut(Shortcut shortcut) { this.shortcut = shortcut; textField.reload(); } private void saveShortcut() { settings.getShortcuts().put(actionModel, shortcut); settingsWindow.needReload(); } private boolean verifyShortcut(Shortcut shortcut) { ActionModel otherAction = null; for (ActionModel a : ActionModel.values()) { if (actionModel != a && shortcut.equals(settings.getShortcuts().get(a))) { otherAction = a; break; } } if (otherAction != null) { int dialogResult = JOptionPane.showConfirmDialog( this, NLS.str("msg.duplicate_shortcut", shortcut, otherAction.getName(), otherAction.getCategory().getName()), NLS.str("msg.warning_title"), JOptionPane.YES_NO_OPTION); if (dialogResult != 0) { return false; } } return true; } private class TextField extends JTextField { private Shortcut tempShortcut; public TextField() { KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(ev -> { if (!isListening()) { return false; } if (ev.getID() == KeyEvent.KEY_PRESSED) { Shortcut pressedShortcut = Shortcut.keyboard(ev.getKeyCode(), ev.getModifiersEx()); if (pressedShortcut.isValidKeyboard()) { tempShortcut = pressedShortcut; refresh(tempShortcut); } else { tempShortcut = null; } } else if (ev.getID() == KeyEvent.KEY_RELEASED) { removeFocus(); } ev.consume(); return true; }); addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent ev) { } @Override public void focusLost(FocusEvent ev) { if (tempShortcut != null) { if (verifyShortcut(tempShortcut)) { shortcut = tempShortcut; saveShortcut(); } else { reload(); } tempShortcut = null; } } }); Toolkit.getDefaultToolkit().addAWTEventListener(event -> { if (!isListening()) { return; } if (event instanceof MouseEvent) { MouseEvent mouseEvent = (MouseEvent) event; if (mouseEvent.getID() == MouseEvent.MOUSE_PRESSED) { int mouseButton = mouseEvent.getButton(); if (mouseButton <= MouseEvent.BUTTON1) { return; } if (mouseButton <= MouseEvent.BUTTON3) { int dialogResult = JOptionPane.showConfirmDialog( this, NLS.str("msg.common_mouse_shortcut"), NLS.str("msg.warning_title"), JOptionPane.YES_NO_OPTION); if (dialogResult != 0) { ((MouseEvent) event).consume(); tempShortcut = null; removeFocus(); return; } } ((MouseEvent) event).consume(); tempShortcut = Shortcut.mouse(mouseButton); refresh(tempShortcut); removeFocus(); } } }, AWTEvent.MOUSE_EVENT_MASK); } public void reload() { refresh(shortcut); } private void refresh(Shortcut displayedShortcut) { if (displayedShortcut == null || displayedShortcut.isNone()) { setText("None"); setForeground(UIManager.getColor("TextArea.inactiveForeground")); return; } setText(displayedShortcut.toString()); setForeground(UIManager.getColor("TextArea.foreground")); } private void removeFocus() { // triggers focusLost getRootPane().requestFocus(); } private boolean isListening() { return isFocusOwner(); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/settings/ui/shortcut/ShortcutsSettingsGroup.java ================================================ package jadx.gui.settings.ui.shortcut; import java.awt.BorderLayout; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import jadx.api.plugins.gui.ISettingsGroup; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.ui.JadxSettingsWindow; import jadx.gui.settings.ui.SettingsGroup; import jadx.gui.ui.action.ActionCategory; import jadx.gui.ui.action.ActionModel; import jadx.gui.utils.NLS; import jadx.gui.utils.shortcut.Shortcut; public class ShortcutsSettingsGroup implements ISettingsGroup { private final JadxSettingsWindow settingsWindow; private final JadxSettings settings; public ShortcutsSettingsGroup(JadxSettingsWindow settingsWindow, JadxSettings settings) { this.settingsWindow = settingsWindow; this.settings = settings; } @Override public String getTitle() { return NLS.str("preferences.shortcuts"); } @Override public JComponent buildComponent() { JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.add(new JLabel(NLS.str("preferences.select_shortcuts")), BorderLayout.NORTH); return panel; } @Override public List getSubGroups() { return Arrays.stream(ActionCategory.values()) .map(this::makeShortcutsGroup) .collect(Collectors.toUnmodifiableList()); } private SettingsGroup makeShortcutsGroup(ActionCategory category) { SettingsGroup group = new SettingsGroup(category.getName()); for (ActionModel actionModel : ActionModel.select(category)) { Shortcut shortcut = settings.getShortcuts().get(actionModel); ShortcutEdit edit = new ShortcutEdit(actionModel, settingsWindow, settings); edit.setShortcut(shortcut); group.addRow(actionModel.getName(), edit); } return group; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/tree/TreeExpansionService.java ================================================ package jadx.gui.tree; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.List; import java.util.stream.Collectors; import javax.swing.JTree; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.metadata.ICodeNodeRef; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.jobs.LoadTask; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JPackage; import jadx.gui.treemodel.JRoot; import jadx.gui.ui.MainWindow; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.UiUtils; public class TreeExpansionService { private static final Logger LOG = LoggerFactory.getLogger(TreeExpansionService.class); private static final boolean DEBUG = false; private static final Comparator PATH_LENGTH_REVERSE = Comparator.comparingInt(p -> -p.getPathCount()); private final MainWindow mainWindow; private final JTree tree; private final JNodeCache nodeCache; public TreeExpansionService(MainWindow mainWindow, JTree tree) { this.mainWindow = mainWindow; this.tree = tree; this.nodeCache = mainWindow.getCacheObject().getNodeCache(); } public List save() { if (tree.getRowCount() == 0 || mainWindow.getWrapper().getCurrentDecompiler().isEmpty()) { return Collections.emptyList(); } List expandedPaths = collectExpandedPaths(tree); List list = new ArrayList<>(); for (TreePath expandedPath : expandedPaths) { list.add(savePath(expandedPath)); } if (DEBUG) { LOG.debug("Saving tree expansions:\n {}", Utils.listToString(list, "\n ")); } return list; } public void load(List treeExpansions) { mainWindow.getBackgroundExecutor().execute(new LoadTask<>( () -> { List expandedPaths = new ArrayList<>(); loadPaths(treeExpansions, expandedPaths); // send expand event to load sub-nodes and wait for completion UiUtils.uiRunAndWait(() -> expandedPaths.forEach(path -> { try { tree.fireTreeWillExpand(path); } catch (Exception e) { throw new JadxRuntimeException("Tree expand error", e); } })); return expandedPaths; }, expandedPaths -> { // expand paths after a loading task is finished expandedPaths.forEach(tree::expandPath); })); } private void loadPaths(List treeExpansions, List expandedPaths) { if (DEBUG) { LOG.debug("Restoring tree expansions:\n {}", Utils.listToString(treeExpansions, "\n ")); } for (String treeExpansion : treeExpansions) { try { TreePath treePath = loadPath(treeExpansion); if (treePath != null) { expandedPaths.add(treePath); } } catch (Exception e) { LOG.warn("Failed to load tree expansion entry: {}", treeExpansion, e); } } if (DEBUG) { LOG.debug("Restored expanded tree paths:\n {}", Utils.listToString(expandedPaths, "\n ")); } } private String savePath(TreePath path) { JNode node = (JNode) path.getLastPathComponent(); if (node instanceof JPackage) { return "p:" + ((JPackage) node).getPkg().getRawFullName(); } if (node instanceof JClass) { return "c:" + ((JClass) node).getCls().getRawName(); } return Arrays.stream(path.getPath()) .map(p -> ((JNode) p).getID()) .skip(1) // skip root .collect(Collectors.joining("//", "t:", "")); } private @Nullable TreePath loadPath(String pathStr) { String pathData = pathStr.substring(2); switch (pathStr.charAt(0)) { case 'c': return getTreePathForRef(getRoot().resolveRawClass(pathData)); case 'p': return getTreePathForRef(getRoot().resolvePackage(pathData)); case 't': return resolveTreePath(pathData.split("//")); default: throw new JadxRuntimeException("Unknown tree expansion path type: " + pathStr); } } private @Nullable TreePath resolveTreePath(String[] pathArr) { JNode current = (JNode) tree.getModel().getRoot(); for (String nodeStr : pathArr) { JNode node = current.searchNode(n -> n.getID().equals(nodeStr)); if (node == null) { if (DEBUG) { List children = current.childrenList().stream() .map(n -> ((JNode) n).getID()) .collect(Collectors.toList()); LOG.warn("Failed to restore path: {}, node '{}' not found in '{}' children: {}", Arrays.toString(pathArr), nodeStr, current, children); } return null; } current = node; } return new TreePath(current.getPath()); } private @Nullable TreePath getTreePathForRef(@Nullable ICodeNodeRef ref) { if (ref == null) { return null; } JNode node = nodeCache.makeFrom(ref); if (node.getParent() == null) { if (DEBUG) { LOG.warn("Resolving node not from tree: {}", node); } JNode treeNode = ((JRoot) tree.getModel().getRoot()).searchNode(node); if (treeNode == null) { if (DEBUG) { LOG.error("Node not found in tree: {}", node); } return null; } node = treeNode; } TreeNode[] pathNodes = ((DefaultTreeModel) tree.getModel()).getPathToRoot(node); if (pathNodes == null) { return null; } return new TreePath(pathNodes); } private static List collectExpandedPaths(JTree tree) { TreePath root = tree.getPathForRow(0); Enumeration expandedDescendants = tree.getExpandedDescendants(root); if (expandedDescendants == null) { return Collections.emptyList(); } List expandedPaths = new ArrayList<>(); while (expandedDescendants.hasMoreElements()) { TreePath path = expandedDescendants.nextElement(); if (path.getPathCount() > 1) { expandedPaths.add(path); } } // filter out sub-paths expandedPaths.sort(PATH_LENGTH_REVERSE); // put the longest paths before sub-paths List result = new ArrayList<>(); for (TreePath path : expandedPaths) { if (!isSubPath(result, path)) { result.add(path); } } return result; } private static boolean isSubPath(List paths, TreePath path) { for (TreePath addedPath : paths) { if (path.isDescendant(addedPath)) { return true; } } return false; } private RootNode getRoot() { return mainWindow.getWrapper().getDecompiler().getRoot(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignatureNode.java ================================================ package jadx.gui.treemodel; import java.io.File; import java.security.cert.Certificate; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.text.StringEscapeUtils; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.android.apksig.ApkVerifier; import jadx.api.ICodeInfo; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.api.impl.SimpleCodeInfo; import jadx.gui.JadxWrapper; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.panel.HtmlPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.CertificateManager; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.zip.IZipEntry; public class ApkSignatureNode extends JNode { private static final long serialVersionUID = -9121321926113143407L; private static final Logger LOG = LoggerFactory.getLogger(ApkSignatureNode.class); private static final ImageIcon CERTIFICATE_ICON = UiUtils.openSvgIcon("nodes/styleKeyPack"); private final transient File openFile; private ICodeInfo content; private volatile boolean loadingStarted = false; private static TabbedPane tabbedPane; @Nullable public static ApkSignatureNode getApkSignature(JadxWrapper wrapper) { // Only show the ApkSignature node if an AndroidManifest.xml is present. // Without a manifest the Google ApkVerifier refuses to work. File apkFile = null; for (ResourceFile resFile : wrapper.getResources()) { if (resFile.getType() == ResourceType.MANIFEST) { IZipEntry zipEntry = resFile.getZipEntry(); if (zipEntry != null) { apkFile = zipEntry.getZipFile(); break; } } } if (apkFile == null) { return null; } return new ApkSignatureNode(apkFile); } public ApkSignatureNode(File openFile) { this.openFile = openFile; } @Override public JClass getJParent() { return null; } @Override public Icon getIcon() { return CERTIFICATE_ICON; } @Override public String makeString() { return "APK signature"; } @Override public boolean hasContent() { return true; } @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { ApkSignatureNode.tabbedPane = tabbedPane; return new HtmlPanel(tabbedPane, this); } @Override public ICodeInfo getCodeInfo() { if (content != null) { return content; } // If loading hasn't started yet, start it. if (!loadingStarted) { loadingStarted = true; SwingUtilities.invokeLater(() -> { new ApkSignatureWorker(this).execute(); }); return new SimpleCodeInfo(StringEscapeUtils.escapeHtml4(NLS.str("apkSignature.loading"))); } return new SimpleCodeInfo(StringEscapeUtils.escapeHtml4(NLS.str("apkSignature.loading"))); } private void writeCertificate(StringEscapeUtils.Builder builder, Certificate cert) { CertificateManager certMgr = new CertificateManager(cert); builder.append("
");
		builder.escape(certMgr.generateHeader());
		builder.append("
");
		builder.escape(certMgr.generatePublicKey());
		builder.append("
");
		builder.escape(certMgr.generateSignature());
		builder.append("
");
		builder.append(certMgr.generateFingerprint());
		builder.append("
"); } private static void writeIssues(StringEscapeUtils.Builder builder, String issueType, List issueList) { if (!issueList.isEmpty()) { builder.append("

"); builder.escape(issueType); builder.append("

"); builder.append("
"); // Unprotected Zip entry issues are very common, handle them separately List unprotIssues = issueList.stream() .filter(i -> i.getIssue() == ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY) .collect(Collectors.toList()); if (!unprotIssues.isEmpty()) { builder.append("

"); builder.escape(NLS.str("apkSignature.unprotectedEntry")); builder.append("

"); for (ApkVerifier.IssueWithParams issue : unprotIssues) { builder.escape((String) issue.getParams()[0]); builder.append("
"); } builder.append("
"); } List remainingIssues = issueList.stream() .filter(i -> i.getIssue() != ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY) .collect(Collectors.toList()); if (!remainingIssues.isEmpty()) { builder.append("
\n");
				for (ApkVerifier.IssueWithParams issue : remainingIssues) {
					builder.escape(issue.toString());
					builder.append("\n");
				}
				builder.append("
\n"); } builder.append("
"); } } private static class ApkSignatureWorker extends SwingWorker { private final ApkSignatureNode node; public ApkSignatureWorker(ApkSignatureNode node) { this.node = node; } @Override protected ICodeInfo doInBackground() { LOG.debug("Starting APK signature verification for {}", node.openFile); ApkVerifier verifier = new ApkVerifier.Builder(node.openFile).build(); try { ApkVerifier.Result result = verifier.verify(); // Build HTML content StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); builder.append("

APK signature verification result:

"); builder.append("

"); if (result.isVerified()) { builder.escape(NLS.str("apkSignature.verificationSuccess")); } else { builder.escape(NLS.str("apkSignature.verificationFailed")); } builder.append("

"); final String err = NLS.str("apkSignature.errors"); final String warn = NLS.str("apkSignature.warnings"); final String sigSuccKey = "apkSignature.signatureSuccess"; final String sigFailKey = "apkSignature.signatureFailed"; ApkSignatureNode parentNode = node; writeIssues(builder, err, result.getErrors()); if (!result.getV1SchemeSigners().isEmpty()) { builder.append("

"); builder.escape(NLS.str(result.isVerifiedUsingV1Scheme() ? sigSuccKey : sigFailKey, 1)); builder.append("

\n"); builder.append("
"); for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) { builder.append("

"); builder.escape(NLS.str("apkSignature.signer")); builder.append(" "); builder.escape(signer.getName()); builder.append(" ("); builder.escape(signer.getSignatureFileName()); builder.append(")"); builder.append("

"); parentNode.writeCertificate(builder, signer.getCertificate()); writeIssues(builder, err, signer.getErrors()); writeIssues(builder, warn, signer.getWarnings()); } builder.append("
"); } if (!result.getV2SchemeSigners().isEmpty()) { builder.append("

"); builder.escape(NLS.str(result.isVerifiedUsingV2Scheme() ? sigSuccKey : sigFailKey, 2)); builder.append("

\n"); builder.append("
"); for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) { builder.append("

"); builder.escape(NLS.str("apkSignature.signer")); builder.append(" "); builder.append(Integer.toString(signer.getIndex() + 1)); builder.append("

"); parentNode.writeCertificate(builder, signer.getCertificate()); writeIssues(builder, err, signer.getErrors()); writeIssues(builder, warn, signer.getWarnings()); } builder.append("
"); } if (!result.getV3SchemeSigners().isEmpty()) { builder.append("

"); builder.escape(NLS.str(result.isVerifiedUsingV3Scheme() ? sigSuccKey : sigFailKey, 3)); builder.append("

\n"); builder.append("
"); for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) { builder.append("

"); builder.escape(NLS.str("apkSignature.signer")); builder.append(" "); builder.append(Integer.toString(signer.getIndex() + 1)); builder.append("

"); parentNode.writeCertificate(builder, signer.getCertificate()); writeIssues(builder, err, signer.getErrors()); writeIssues(builder, warn, signer.getWarnings()); } builder.append("
"); } if (!result.getV31SchemeSigners().isEmpty()) { builder.append("

"); builder.escape(NLS.str(result.isVerifiedUsingV31Scheme() ? sigSuccKey : sigFailKey, 31)); builder.append("

\n"); builder.append("
"); for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) { builder.append("

"); builder.escape(NLS.str("apkSignature.signer")); builder.append(" "); builder.append(Integer.toString(signer.getIndex() + 1)); builder.append("

"); parentNode.writeCertificate(builder, signer.getCertificate()); writeIssues(builder, err, signer.getErrors()); writeIssues(builder, warn, signer.getWarnings()); } builder.append("
"); } writeIssues(builder, warn, result.getWarnings()); return new SimpleCodeInfo(builder.toString()); } catch (Exception e) { LOG.error("Failed to verify APK signature for {}", node.openFile, e); StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); builder.append("

"); builder.escape(NLS.str("apkSignature.exception")); builder.append("

");
				builder.escape(ExceptionUtils.getStackTrace(e));
				builder.append("
"); return new SimpleCodeInfo(builder.toString()); } } @Override protected void done() { try { node.content = get(); if (tabbedPane != null) { ContentPanel panel = tabbedPane.getTabByNode(node); if (panel instanceof HtmlPanel) { ((HtmlPanel) panel).loadContent(node); } } else { LOG.warn("Could not find TabbedPane to refresh ApkSignatureNode panel."); } } catch (InterruptedException | ExecutionException e) { LOG.error("Error during APK signature verification SwingWorker", e); StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); builder.append("

"); builder.escape(NLS.str("apkSignature.exception")); builder.append("

");
				Throwable cause = (e instanceof ExecutionException) ? e.getCause() : e;
				builder.escape(ExceptionUtils.getStackTrace(cause));
				builder.append("
"); node.content = new SimpleCodeInfo(builder.toString()); if (tabbedPane != null) { ContentPanel panel = tabbedPane.getTabByNode(node); if (panel instanceof HtmlPanel) { ((HtmlPanel) panel).loadContent(node); } } } finally { node.loadingStarted = false; } } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java ================================================ package jadx.gui.treemodel; import javax.swing.Icon; import org.jetbrains.annotations.NotNull; import jadx.api.JavaNode; public class CodeNode extends JNode { private static final long serialVersionUID = 1658650786734966545L; private final transient JClass rootCls; private final transient JNode jNode; private final transient String line; private final transient int pos; public CodeNode(JClass rootCls, JNode jNode, String lineStr, int pos) { this.rootCls = rootCls; this.jNode = jNode; this.line = lineStr; this.pos = pos; } @Override public Icon getIcon() { return jNode.getIcon(); } @Override public JavaNode getJavaNode() { return jNode.getJavaNode(); } @Override public JClass getJParent() { return getRootClass(); } @Override public JClass getRootClass() { return rootCls; } @Override public String makeDescString() { return line; } @Override public boolean hasDescString() { return true; } @Override public String makeString() { return jNode.makeString(); } @Override public String makeStringHtml() { return jNode.makeStringHtml(); } @Override public String makeLongString() { return jNode.makeLongString(); } @Override public String makeLongStringHtml() { return jNode.makeLongStringHtml(); } @Override public boolean disableHtml() { return jNode.disableHtml(); } @Override public String getSyntaxName() { return jNode.getSyntaxName(); } @Override public int getPos() { return pos; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof CodeNode)) { return false; } CodeNode codeNode = (CodeNode) o; return jNode.equals(codeNode.jNode); } @Override public int hashCode() { return jNode.hashCode(); } @Override public int compareTo(@NotNull JNode other) { if (other instanceof CodeNode) { return jNode.compareTo(((CodeNode) other).jNode); } return super.compareTo(other); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java ================================================ package jadx.gui.treemodel; import java.util.List; import java.util.Set; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JPopupMenu; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeInfo; import jadx.api.JavaClass; import jadx.api.JavaField; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.api.data.ICodeRename; import jadx.api.data.impl.JadxCodeRename; import jadx.api.data.impl.JadxNodeRef; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.nodes.ICodeNode; import jadx.gui.jobs.SimpleTask; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.ClassCodeContentPanel; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.popupmenu.JClassPopupMenu; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.CacheObject; import jadx.gui.utils.Icons; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class JClass extends JLoadableNode implements JRenameNode { private static final long serialVersionUID = -1239986875244097177L; private static final ImageIcon ICON_CLASS_ABSTRACT = UiUtils.openSvgIcon("nodes/abstractClass"); private static final ImageIcon ICON_CLASS_PUBLIC = UiUtils.openSvgIcon("nodes/publicClass"); private static final ImageIcon ICON_CLASS_PRIVATE = UiUtils.openSvgIcon("nodes/privateClass"); private static final ImageIcon ICON_CLASS_PROTECTED = UiUtils.openSvgIcon("nodes/protectedClass"); private static final ImageIcon ICON_INTERFACE = UiUtils.openSvgIcon("nodes/interface"); private static final ImageIcon ICON_ENUM = UiUtils.openSvgIcon("nodes/enum"); private static final ImageIcon ICON_ANNOTATION = UiUtils.openSvgIcon("nodes/annotationtype"); private final transient JavaClass cls; private final transient JClass jParent; private final transient JNodeCache nodeCache; private transient boolean loaded; /** * Should be called only from JNodeCache! */ public JClass(JavaClass cls, JClass parent, JNodeCache nodeCache) { this.cls = cls; this.jParent = parent; this.loaded = parent != null; this.nodeCache = nodeCache; } public JavaClass getCls() { return cls; } @Override public boolean canRename() { return !cls.getClassNode().contains(AFlag.DONT_RENAME); } @Override public void loadNode() { getRootClass().load(); } @Override public synchronized @Nullable SimpleTask getLoadTask() { if (loaded) { return null; } JClass rootClass = getRootClass(); return new SimpleTask(NLS.str("progress.decompile"), () -> rootClass.getCls().getClassNode().decompile(), // run decompilation in background rootClass::load // load class internals and update UI ); } private synchronized void load() { if (loaded) { return; } cls.decompile(); loaded = true; update(); } public synchronized ICodeInfo reload(CacheObject cache) { cache.getNodeCache().removeWholeClass(cls); ICodeInfo codeInfo = cls.reload(); loaded = true; update(); return codeInfo; } public synchronized void unload(CacheObject cache) { cache.getNodeCache().removeWholeClass(cls); cls.unload(); loaded = false; } public synchronized void update() { removeAllChildren(); if (!loaded) { add(new TextNode(NLS.str("tree.loading"))); } else { for (JavaClass javaClass : cls.getInnerClasses()) { JClass innerCls = nodeCache.makeFrom(javaClass); add(innerCls); innerCls.update(); } for (JavaField f : cls.getFields()) { add(nodeCache.makeFrom(f)); } for (JavaMethod m : cls.getMethods()) { add(nodeCache.makeFrom(m)); } } } @Override public ICodeInfo getCodeInfo() { return cls.getCodeInfo(); } @Override public boolean hasContent() { return true; } @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new ClassCodeContentPanel(tabbedPane, this); } public String getSmali() { return cls.getSmali(); } @Override public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_JAVA; } @Override public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return new JClassPopupMenu(mainWindow, this); } @Override public Icon getIcon() { AccessInfo accessInfo = cls.getAccessInfo(); if (accessInfo.isEnum()) { return ICON_ENUM; } if (accessInfo.isAnnotation()) { return ICON_ANNOTATION; } if (accessInfo.isInterface()) { return ICON_INTERFACE; } if (accessInfo.isAbstract()) { return ICON_CLASS_ABSTRACT; } if (accessInfo.isProtected()) { return ICON_CLASS_PROTECTED; } if (accessInfo.isPrivate()) { return ICON_CLASS_PRIVATE; } if (accessInfo.isPublic()) { return ICON_CLASS_PUBLIC; } return Icons.CLASS; } @Override public JavaNode getJavaNode() { return cls; } @Override public ICodeNode getCodeNodeRef() { return cls.getClassNode(); } @Override public JClass getJParent() { return jParent; } @Override public JClass getRootClass() { if (jParent == null) { return this; } return jParent.getRootClass(); } @Override public String getName() { return cls.getName(); } public String getFullName() { return cls.getFullName(); } @Override public String getTitle() { return makeLongStringHtml(); } @Override public boolean isValidName(String newName) { if (NameMapper.isValidIdentifier(newName)) { return true; } if (cls.isInner()) { // disallow to change package for inner classes return false; } if (NameMapper.isValidFullIdentifier(newName)) { return true; } // moving to default pkg return newName.startsWith(".") && NameMapper.isValidIdentifier(newName.substring(1)); } @Override public ICodeRename buildCodeRename(String newName, Set renames) { return new JadxCodeRename(JadxNodeRef.forCls(cls), newName); } @Override public void removeAlias() { // reset only short name, package name should be reset explicitly using PackageNode cls.getClassNode().rename(""); } @Override public void addUpdateNodes(List toUpdate) { toUpdate.add(cls); toUpdate.addAll(cls.getUseIn()); } @Override public void reload(MainWindow mainWindow) { // TODO: rebuild packages only if class package has been changed mainWindow.reloadTreePreservingState(); } @Override public int hashCode() { return cls.hashCode(); } @Override public boolean equals(Object obj) { return this == obj || obj instanceof JClass && cls.equals(((JClass) obj).cls); } @Override public String makeString() { return cls.getName(); } @Override public String makeLongString() { return cls.getFullName(); } public int compareToCls(@NotNull JClass otherCls) { return this.getCls().getRawName().compareTo(otherCls.getCls().getRawName()); } @Override public int compareTo(@NotNull JNode other) { if (other instanceof JClass) { return compareToCls((JClass) other); } if (other instanceof JMethod) { int cmp = compareToCls(other.getJParent()); if (cmp != 0) { return cmp; } return -1; } return super.compareTo(other); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JEditableNode.java ================================================ package jadx.gui.treemodel; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; public abstract class JEditableNode extends JNode { private volatile boolean changed = false; private final List> changeListeners = new ArrayList<>(); public abstract void save(String newContent); @Override public boolean isEditable() { return true; } public boolean isChanged() { return changed; } public void setChanged(boolean changed) { if (this.changed != changed) { this.changed = changed; for (Consumer changeListener : changeListeners) { changeListener.accept(changed); } } } public void addChangeListener(Consumer listener) { changeListeners.add(listener); listener.accept(changed); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JField.java ================================================ package jadx.gui.treemodel; import java.util.Comparator; import java.util.List; import java.util.Set; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JPopupMenu; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.jetbrains.annotations.NotNull; import jadx.api.JavaField; import jadx.api.JavaNode; import jadx.api.data.ICodeRename; import jadx.api.data.impl.JadxCodeRename; import jadx.api.data.impl.JadxNodeRef; import jadx.api.metadata.ICodeNodeRef; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.AccessInfo; import jadx.gui.ui.MainWindow; import jadx.gui.ui.dialog.RenameDialog; import jadx.gui.utils.Icons; import jadx.gui.utils.UiUtils; public class JField extends JNode implements JRenameNode { private static final long serialVersionUID = 1712572192106793359L; private static final ImageIcon ICON_FLD_PRI = UiUtils.openSvgIcon("nodes/privateField"); private static final ImageIcon ICON_FLD_PRO = UiUtils.openSvgIcon("nodes/protectedField"); private static final ImageIcon ICON_FLD_PUB = UiUtils.openSvgIcon("nodes/publicField"); private final transient JavaField field; private final transient JClass jParent; /** * Should be called only from JNodeCache! */ public JField(JavaField javaField, JClass jClass) { this.field = javaField; this.jParent = jClass; } public JavaField getJavaField() { return (JavaField) getJavaNode(); } @Override public JavaNode getJavaNode() { return field; } @Override public ICodeNodeRef getCodeNodeRef() { return field.getFieldNode(); } @Override public JClass getJParent() { return jParent; } @Override public JClass getRootClass() { return jParent.getRootClass(); } @Override public boolean canRename() { return !field.getFieldNode().contains(AFlag.DONT_RENAME); } @Override public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return RenameDialog.buildRenamePopup(mainWindow, this); } @Override public String getTitle() { return makeLongStringHtml(); } @Override public ICodeRename buildCodeRename(String newName, Set renames) { return new JadxCodeRename(JadxNodeRef.forFld(field), newName); } @Override public boolean isValidName(String newName) { return NameMapper.isValidIdentifier(newName); } @Override public void removeAlias() { field.removeAlias(); } @Override public void addUpdateNodes(List toUpdate) { toUpdate.add(field); toUpdate.addAll(field.getUseIn()); } @Override public void reload(MainWindow mainWindow) { mainWindow.reloadTreePreservingState(); } @Override public Icon getIcon() { AccessInfo af = field.getAccessFlags(); return UiUtils.makeIcon(af, ICON_FLD_PUB, ICON_FLD_PRI, ICON_FLD_PRO, Icons.FIELD); } @Override public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_JAVA; } @Override public String makeString() { return UiUtils.typeFormat(field.getName(), field.getType()); } @Override public String makeStringHtml() { return UiUtils.typeFormatHtml(field.getName(), field.getType()); } @Override public String makeLongString() { return UiUtils.typeFormat(field.getFullName(), field.getType()); } @Override public String makeLongStringHtml() { return UiUtils.typeFormatHtml(field.getFullName(), field.getType()); } @Override public String getTooltip() { String fullType = UiUtils.escapeHtml(field.getType().toString()); return UiUtils.wrapHtml(fullType + ' ' + UiUtils.escapeHtml(field.getName())); } @Override public String makeDescString() { return UiUtils.typeStr(field.getType()) + " " + field.getName(); } @Override public boolean disableHtml() { return false; } @Override public boolean hasDescString() { return false; } @Override public int hashCode() { return field.hashCode(); } @Override public boolean equals(Object o) { return this == o || o instanceof JField && field.equals(((JField) o).field); } private static final Comparator COMPARATOR = Comparator .comparing(JField::getJParent) .thenComparing(JNode::getName) .thenComparingInt(JField::getPos); public int compareToFld(@NotNull JField other) { return COMPARATOR.compare(this, other); } @Override public int compareTo(@NotNull JNode other) { if (other instanceof JField) { return compareToFld(((JField) other)); } return super.compareTo(other); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JInputFile.java ================================================ package jadx.gui.treemodel; import java.nio.file.Path; import java.util.Objects; import javax.swing.Icon; import javax.swing.JPopupMenu; import jadx.gui.ui.MainWindow; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; import jadx.gui.utils.ui.SimpleMenuItem; public class JInputFile extends JNode { private final Path filePath; public JInputFile(Path filePath) { this.filePath = Objects.requireNonNull(filePath); } @Override public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return buildInputFilePopupMenu(mainWindow, filePath); } public static JPopupMenu buildInputFilePopupMenu(MainWindow mainWindow, Path filePath) { JPopupMenu menu = new JPopupMenu(); menu.add(new SimpleMenuItem(NLS.str("popup.add_files"), mainWindow::addFiles)); menu.add(new SimpleMenuItem(NLS.str("popup.remove"), () -> mainWindow.removeInput(filePath))); menu.add(new SimpleMenuItem(NLS.str("popup.rename"), () -> mainWindow.renameInput(filePath))); return menu; } @Override public JClass getJParent() { return null; } @Override public Icon getIcon() { return Icons.FILE; } @Override public String makeString() { return filePath.getFileName().toString(); } @Override public String getTooltip() { return filePath.normalize().toAbsolutePath().toString(); } @Override public int hashCode() { return filePath.hashCode(); } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } return ((JInputFile) o).filePath.equals(filePath); } @Override public String toString() { return "JInputFile{" + filePath + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JInputFiles.java ================================================ package jadx.gui.treemodel; import java.nio.file.Path; import java.util.List; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JPopupMenu; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.SimpleMenuItem; public class JInputFiles extends JNode { private static final ImageIcon INPUT_FILES_ICON = UiUtils.openSvgIcon("nodes/moduleDirectory"); public JInputFiles(List files) { for (Path file : files) { String fileName = file.getFileName().toString(); if (fileName.endsWith(".smali")) { add(new JInputSmaliFile(file)); } else { add(new JInputFile(file)); } } } @Override public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { JPopupMenu menu = new JPopupMenu(); menu.add(new SimpleMenuItem(NLS.str("popup.add_files"), mainWindow::addFiles)); return menu; } @Override public JClass getJParent() { return null; } @Override public Icon getIcon() { return INPUT_FILES_ICON; } @Override public String getID() { return "JInputFiles"; } @Override public String makeString() { return NLS.str("tree.input_files"); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JInputSmaliFile.java ================================================ package jadx.gui.treemodel; import java.nio.file.Path; import java.util.Objects; import javax.swing.Icon; import javax.swing.JPopupMenu; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.impl.SimpleCodeInfo; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.codearea.CodeContentPanel; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.Icons; public class JInputSmaliFile extends JEditableNode { private static final Logger LOG = LoggerFactory.getLogger(JInputSmaliFile.class); private final Path filePath; public JInputSmaliFile(Path filePath) { this.filePath = Objects.requireNonNull(filePath); } @Override public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return JInputFile.buildInputFilePopupMenu(mainWindow, filePath); } @Override public boolean hasContent() { return true; } @Override public @Nullable ContentPanel getContentPanel(TabbedPane tabbedPane) { return new CodeContentPanel(tabbedPane, this); } @Override public String getSyntaxName() { return AbstractCodeArea.SYNTAX_STYLE_SMALI; } @Override public ICodeInfo getCodeInfo() { try { return new SimpleCodeInfo(FileUtils.readFile(filePath)); } catch (Exception e) { throw new JadxRuntimeException("Failed to read file: " + filePath.toAbsolutePath(), e); } } @Override public void save(String newContent) { try { FileUtils.writeFile(filePath, newContent); LOG.debug("File saved: {}", filePath.toAbsolutePath()); } catch (Exception e) { throw new JadxRuntimeException("Failed to write file: " + filePath.toAbsolutePath(), e); } } @Override public JClass getJParent() { return null; } @Override public Icon getIcon() { return Icons.FILE; } @Override public String makeString() { return filePath.getFileName().toString(); } @Override public String getTooltip() { return filePath.normalize().toAbsolutePath().toString(); } @Override public int hashCode() { return filePath.hashCode(); } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } return ((JInputSmaliFile) o).filePath.equals(filePath); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JInputs.java ================================================ package jadx.gui.treemodel; import java.nio.file.Path; import java.util.List; import javax.swing.Icon; import javax.swing.ImageIcon; import jadx.core.utils.files.FileUtils; import jadx.gui.settings.JadxProject; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.plugins.TreeInputsHelper; public class JInputs extends JNode { private static final ImageIcon INPUTS_ICON = UiUtils.openSvgIcon("nodes/projectStructure"); public JInputs(MainWindow mainWindow) { JadxProject project = mainWindow.getProject(); List inputs = project.getFilePaths(); List files = FileUtils.expandDirs(inputs); TreeInputsHelper inputsHelper = new TreeInputsHelper(mainWindow); inputsHelper.processInputs(files); add(new JInputFiles(inputsHelper.getSimpleFiles())); inputsHelper.getCustomNodes().forEach(this::add); } @Override public JClass getJParent() { return null; } @Override public Icon getIcon() { return INPUTS_ICON; } @Override public String getID() { return "JInputs"; } @Override public String makeString() { return NLS.str("tree.inputs_title"); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JLoadableNode.java ================================================ package jadx.gui.treemodel; import java.util.function.Predicate; import org.jetbrains.annotations.Nullable; import jadx.gui.jobs.IBackgroundTask; public abstract class JLoadableNode extends JNode { private static final long serialVersionUID = 5543590584166374958L; public abstract void loadNode(); public abstract @Nullable IBackgroundTask getLoadTask(); @Override public @Nullable JNode searchNode(Predicate filter) { loadNode(); return super.searchNode(filter); } @Override public @Nullable JNode searchDepthNode(Predicate filter) { loadNode(); return super.searchDepthNode(filter); } @Override public @Nullable JNode removeNode(Predicate filter) { loadNode(); return super.removeNode(filter); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java ================================================ package jadx.gui.treemodel; import java.util.Comparator; import java.util.List; import java.util.Set; import javax.swing.Icon; import javax.swing.JPopupMenu; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.jetbrains.annotations.NotNull; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.api.data.ICodeRename; import jadx.api.data.impl.JadxCodeRename; import jadx.api.data.impl.JadxNodeRef; import jadx.api.metadata.ICodeNodeRef; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.args.ArgType; import jadx.gui.ui.MainWindow; import jadx.gui.ui.cellrenders.MethodRenderHelper; import jadx.gui.ui.dialog.RenameDialog; import jadx.gui.utils.UiUtils; public class JMethod extends JNode implements JRenameNode { private static final long serialVersionUID = 3834526867464663751L; private final transient JavaMethod mth; private final transient JClass jParent; /** * Should be called only from JNodeCache! */ public JMethod(JavaMethod javaMethod, JClass jClass) { this.mth = javaMethod; this.jParent = jClass; } @Override public JavaNode getJavaNode() { return mth; } public JavaMethod getJavaMethod() { return mth; } @Override public ICodeNodeRef getCodeNodeRef() { return mth.getMethodNode(); } @Override public JClass getJParent() { return jParent; } public ArgType getReturnType() { return mth.getReturnType(); } @Override public JClass getRootClass() { return jParent.getRootClass(); } @Override public Icon getIcon() { return MethodRenderHelper.getIcon(mth); } @Override public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_JAVA; } @Override public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return RenameDialog.buildRenamePopup(mainWindow, this); } String makeBaseString() { return MethodRenderHelper.makeBaseString(mth); } @Override public String getName() { return mth.getName(); } @Override public String getTitle() { return makeLongStringHtml(); } @Override public boolean canRename() { if (mth.isClassInit()) { return false; } return !mth.getMethodNode().contains(AFlag.DONT_RENAME); } @Override public JRenameNode replace() { if (mth.isConstructor()) { // rename class instead constructor return jParent; } return this; } @Override public ICodeRename buildCodeRename(String newName, Set renames) { List relatedMethods = mth.getOverrideRelatedMethods(); if (!relatedMethods.isEmpty()) { for (JavaMethod relatedMethod : relatedMethods) { renames.remove(new JadxCodeRename(JadxNodeRef.forMth(relatedMethod), "")); } } return new JadxCodeRename(JadxNodeRef.forMth(mth), newName); } @Override public boolean isValidName(String newName) { return NameMapper.isValidIdentifier(newName); } @Override public void removeAlias() { mth.removeAlias(); } @Override public void addUpdateNodes(List toUpdate) { toUpdate.add(mth); toUpdate.addAll(mth.getUseIn()); List overrideRelatedMethods = mth.getOverrideRelatedMethods(); toUpdate.addAll(overrideRelatedMethods); for (JavaMethod ovrdMth : overrideRelatedMethods) { toUpdate.addAll(ovrdMth.getUseIn()); } } @Override public void reload(MainWindow mainWindow) { mainWindow.reloadTreePreservingState(); } @Override public String makeString() { return UiUtils.typeFormat(makeBaseString(), getReturnType()); } @Override public String makeStringHtml() { return UiUtils.typeFormatHtml(makeBaseString(), getReturnType()); } @Override public String makeLongString() { String name = mth.getDeclaringClass().getFullName() + '.' + makeBaseString(); return UiUtils.typeFormat(name, getReturnType()); } @Override public String makeLongStringHtml() { String name = mth.getDeclaringClass().getFullName() + '.' + makeBaseString(); return UiUtils.typeFormatHtml(name, getReturnType()); } @Override public boolean disableHtml() { return false; } @Override public String makeDescString() { return UiUtils.typeStr(getReturnType()) + " " + makeBaseString(); } @Override public boolean hasDescString() { return false; } @Override public int getPos() { return mth.getDefPos(); } @Override public int hashCode() { return mth.hashCode(); } @Override public boolean equals(Object o) { return this == o || o instanceof JMethod && mth.equals(((JMethod) o).mth); } private static final Comparator COMPARATOR = Comparator .comparing(JMethod::getJParent) .thenComparing(jMethod -> jMethod.mth.getMethodNode().getMethodInfo().getShortId()) .thenComparingInt(JMethod::getPos); public int compareToMth(@NotNull JMethod other) { return COMPARATOR.compare(this, other); } @Override public int compareTo(@NotNull JNode other) { if (other instanceof JMethod) { return compareToMth(((JMethod) other)); } if (other instanceof JClass) { JClass cls = (JClass) other; int cmp = jParent.compareToCls(cls); if (cmp != 0) { return cmp; } return 1; } return super.compareTo(other); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java ================================================ package jadx.gui.treemodel; import java.util.Comparator; import java.util.Enumeration; import java.util.List; import java.util.function.Predicate; import javax.swing.JPopupMenu; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreeNode; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeInfo; import jadx.api.JavaNode; import jadx.api.gui.tree.ITreeNode; import jadx.api.metadata.ICodeNodeRef; import jadx.api.resources.ResourceContentType; import jadx.core.utils.ListUtils; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.TabbedPane; public abstract class JNode extends DefaultMutableTreeNode implements ITreeNode, Comparable { private static final long serialVersionUID = -5154479091781041008L; public abstract JClass getJParent(); /** * Return top level JClass or self if already at top. */ public JClass getRootClass() { return null; } public JavaNode getJavaNode() { return null; } @Override public ICodeNodeRef getCodeNodeRef() { return null; } public boolean hasContent() { return false; } public @Nullable ContentPanel getContentPanel(TabbedPane tabbedPane) { return null; } public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_NONE; } public ICodeInfo getCodeInfo() { return ICodeInfo.EMPTY; } public ResourceContentType getContentType() { return ResourceContentType.CONTENT_TEXT; } public boolean isEditable() { return false; } @Override public String getName() { JavaNode javaNode = getJavaNode(); if (javaNode == null) { return null; } return javaNode.getName(); } public boolean supportsQuickTabs() { return true; } public @Nullable JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return null; } @Override public String getID() { return makeString(); } public abstract String makeString(); public String makeStringHtml() { return makeString(); } public String makeDescString() { return null; } public boolean hasDescString() { return false; } public String makeLongString() { return makeString(); } public String makeLongStringHtml() { return makeLongString(); } public boolean disableHtml() { return true; } public int getPos() { JavaNode javaNode = getJavaNode(); if (javaNode == null) { return -1; } return javaNode.getDefPos(); } public String getTooltip() { return makeLongStringHtml(); } public @Nullable JNode searchNode(Predicate filter) { Enumeration en = this.children(); while (en.hasMoreElements()) { JNode node = (JNode) en.nextElement(); if (filter.test(node)) { return node; } } return null; } public @Nullable JNode searchDepthNode(Predicate filter) { Enumeration en = this.breadthFirstEnumeration(); while (en.hasMoreElements()) { JNode node = (JNode) en.nextElement(); if (filter.test(node)) { return node; } } return null; } /** * Remove and return first found node */ public @Nullable JNode removeNode(Predicate filter) { Enumeration en = this.children(); while (en.hasMoreElements()) { JNode node = (JNode) en.nextElement(); if (filter.test(node)) { this.remove(node); return node; } } return null; } public List childrenList() { return ListUtils.enumerationToList(this.children()); } private static final Comparator COMPARATOR = Comparator .comparing(JNode::makeLongString) .thenComparingInt(JNode::getPos); @Override public int compareTo(@NotNull JNode other) { return COMPARATOR.compare(this, other); } @Override public String toString() { return makeString(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java ================================================ package jadx.gui.treemodel; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.swing.Icon; import javax.swing.JPopupMenu; import jadx.api.JavaNode; import jadx.api.JavaPackage; import jadx.gui.ui.MainWindow; import jadx.gui.ui.popupmenu.JPackagePopupMenu; import jadx.gui.utils.Icons; import static jadx.gui.utils.UiUtils.escapeHtml; import static jadx.gui.utils.UiUtils.fadeHtml; import static jadx.gui.utils.UiUtils.wrapHtml; public class JPackage extends JNode { private static final long serialVersionUID = -4120718634156839804L; public static final String PACKAGE_DEFAULT_HTML_STR = wrapHtml(fadeHtml(escapeHtml(""))); private final JavaPackage pkg; private final boolean enabled; private final List classes; private final List subPackages; /** * Package created by full package alias, don't have a raw package reference. * `pkg` field point to the closest raw package leaf. */ private final boolean synthetic; private String name; /** * Should be called only from JNodeCache! */ public JPackage(JavaPackage pkg, boolean enabled, List classes, List subPackages, boolean synthetic) { this.pkg = pkg; this.enabled = enabled; this.classes = classes; this.subPackages = subPackages; this.synthetic = synthetic; } public static JPackage makeTmpRoot() { return new JPackage(null, true, Collections.emptyList(), new ArrayList<>(), true); } public void update() { removeAllChildren(); if (isEnabled()) { for (JPackage pkg : subPackages) { pkg.update(); add(pkg); } for (JClass cls : classes) { cls.update(); add(cls); } } } @Override public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return new JPackagePopupMenu(mainWindow, this); } public JavaPackage getPkg() { return pkg; } public JavaNode getJavaNode() { return pkg; } @Override public String getName() { return name; } public void setName(String name) { this.name = name; } public List getSubPackages() { return subPackages; } public List getClasses() { return classes; } public boolean isEnabled() { return enabled; } public boolean isSynthetic() { return synthetic; } @Override public Icon getIcon() { return Icons.PACKAGE; } @Override public JClass getJParent() { return null; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } return pkg.equals(((JPackage) o).pkg); } @Override public int hashCode() { return pkg.hashCode(); } @Override public String makeString() { return name; } @Override public String makeStringHtml() { if (name.isEmpty()) { return PACKAGE_DEFAULT_HTML_STR; } return name; } @Override public boolean disableHtml() { if (name.isEmpty()) { // show PACKAGE_DEFAULT_HTML_STR for empty package return false; } return true; } @Override public String makeLongString() { return pkg.getFullName(); } @Override public String toString() { return name; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JRenameNode.java ================================================ package jadx.gui.treemodel; import java.util.List; import java.util.Set; import javax.swing.Icon; import jadx.api.JavaNode; import jadx.api.data.ICodeRename; import jadx.gui.ui.MainWindow; public interface JRenameNode { JavaNode getJavaNode(); String getTitle(); String getName(); Icon getIcon(); boolean canRename(); default JRenameNode replace() { return this; } ICodeRename buildCodeRename(String newName, Set renames); boolean isValidName(String newName); void removeAlias(); void addUpdateNodes(List toUpdate); void reload(MainWindow mainWindow); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JResSearchNode.java ================================================ package jadx.gui.treemodel; import javax.swing.Icon; import jadx.core.utils.StringUtils; public class JResSearchNode extends JNode { private static final long serialVersionUID = -2222084945157778639L; private final transient JResource resNode; private final transient String text; private final transient int pos; public JResSearchNode(JResource resNode, String text, int pos) { this.pos = pos; this.text = text; this.resNode = resNode; } public JResource getResNode() { return resNode; } public int getPos() { return pos; } @Override public String makeDescString() { return text; } @Override public JClass getJParent() { return resNode.getJParent(); } @Override public String getName() { return resNode.getName(); } @Override public String makeString() { return resNode.makeString(); } @Override public String makeLongString() { return resNode.makeLongString(); } @Override public String makeLongStringHtml() { return resNode.makeLongStringHtml(); } @Override public String getTooltip() { return resNode.getTooltip(); } @Override public boolean disableHtml() { return resNode.disableHtml(); } @Override public Icon getIcon() { return resNode.getIcon(); } @Override public boolean hasDescString() { return !StringUtils.isEmpty(text); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java ================================================ package jadx.gui.treemodel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JPopupMenu; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeInfo; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.api.ResourcesLoader; import jadx.api.impl.SimpleCodeInfo; import jadx.api.resources.ResourceContentType; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.core.xmlgen.ResContainer; import jadx.gui.jobs.SimpleTask; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.codearea.BinaryContentPanel; import jadx.gui.ui.codearea.CodeContentPanel; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.panel.FontPanel; import jadx.gui.ui.panel.ImagePanel; import jadx.gui.ui.popupmenu.JResourcePopupMenu; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.res.ResTableHelper; public class JResource extends JLoadableNode { private static final long serialVersionUID = -201018424302612434L; private static final ImageIcon ROOT_ICON = UiUtils.openSvgIcon("nodes/resourcesRoot"); private static final ImageIcon ARSC_ICON = UiUtils.openSvgIcon("nodes/resourceBundle"); private static final ImageIcon XML_ICON = UiUtils.openSvgIcon("nodes/xml"); private static final ImageIcon IMAGE_ICON = UiUtils.openSvgIcon("nodes/ImagesFileType"); private static final ImageIcon SO_ICON = UiUtils.openSvgIcon("nodes/binaryFile"); private static final ImageIcon MANIFEST_ICON = UiUtils.openSvgIcon("nodes/manifest"); private static final ImageIcon JAVA_ICON = UiUtils.openSvgIcon("nodes/java"); private static final ImageIcon APK_ICON = UiUtils.openSvgIcon("nodes/archiveApk"); private static final ImageIcon AUDIO_ICON = UiUtils.openSvgIcon("nodes/audioFile"); private static final ImageIcon VIDEO_ICON = UiUtils.openSvgIcon("nodes/videoFile"); private static final ImageIcon FONT_ICON = UiUtils.openSvgIcon("nodes/fontFile"); private static final ImageIcon HTML_ICON = UiUtils.openSvgIcon("nodes/html"); private static final ImageIcon JSON_ICON = UiUtils.openSvgIcon("nodes/json"); private static final ImageIcon TEXT_ICON = UiUtils.openSvgIcon("nodes/text"); private static final ImageIcon ARCHIVE_ICON = UiUtils.openSvgIcon("nodes/archive"); private static final ImageIcon UNKNOWN_ICON = UiUtils.openSvgIcon("nodes/unknown"); public static final Comparator RESOURCES_COMPARATOR = Comparator.comparingInt(r -> r.type.ordinal()) .thenComparing(JResource::getName, String.CASE_INSENSITIVE_ORDER); public enum JResType { ROOT, DIR, FILE } private final transient String name; private final transient String shortName; private final transient JResType type; private final transient ResourceFile resFile; private transient volatile boolean loaded; private transient List subNodes = Collections.emptyList(); private transient ICodeInfo content = ICodeInfo.EMPTY; public JResource(@Nullable ResourceFile resFile, String name, JResType type) { this(resFile, name, name, type); } public JResource(@Nullable ResourceFile resFile, String name, String shortName, JResType type) { if (resFile == null && type == JResType.FILE) { throw new IllegalArgumentException("Null resource file"); } this.resFile = resFile; this.name = name; this.shortName = shortName; this.type = type; this.loaded = false; } public synchronized void update() { removeAllChildren(); if (Utils.isEmpty(subNodes)) { if (type == JResType.DIR || type == JResType.ROOT || resFile.getType() == ResourceType.ARSC) { // fake leaf to force show expand button // real sub nodes will load on expand in loadNode() method add(new TextNode(NLS.str("tree.loading"))); } } else { for (JResource res : subNodes) { res.update(); add(res); } if (type != JResType.FILE) { // no content, nothing to load loaded = true; } } } @Override public synchronized void loadNode() { getCodeInfo(); update(); } @Override public synchronized SimpleTask getLoadTask() { if (loaded) { return null; } return new SimpleTask(NLS.str("progress.load"), this::getCodeInfo, this::update); } @Override public String getName() { return name; } public String getShortName() { return shortName; } public JResType getType() { return type; } public List getSubNodes() { return subNodes; } public void addSubNode(JResource node) { subNodes = ListUtils.safeAdd(subNodes, node); } public void sortSubNodes() { sortResNodes(subNodes); } private static void sortResNodes(List nodes) { if (Utils.notEmpty(nodes)) { nodes.forEach(JResource::sortSubNodes); nodes.sort(RESOURCES_COMPARATOR); } } @Override public boolean hasContent() { return resFile != null; } @Override public @Nullable ContentPanel getContentPanel(TabbedPane tabbedPane) { if (resFile == null) { return null; } // TODO: allow to register custom viewers switch (resFile.getType()) { case IMG: return new ImagePanel(tabbedPane, this); case FONT: return new FontPanel(tabbedPane, this); } if (getContentType() == ResourceContentType.CONTENT_BINARY) { return new BinaryContentPanel(tabbedPane, this, false); } if (getSyntaxByExtension(resFile.getDeobfName()) != null) { return new CodeContentPanel(tabbedPane, this); } // unknown file type, show both text and binary return new BinaryContentPanel(tabbedPane, this, true); } @Override public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return new JResourcePopupMenu(mainWindow, this); } @Override public synchronized ICodeInfo getCodeInfo() { if (loaded) { return content; } ICodeInfo codeInfo = loadContent(); content = codeInfo; loaded = true; return codeInfo; } @Override public ResourceContentType getContentType() { if (type == JResType.FILE) { return resFile.getType().getContentType(); } return ResourceContentType.CONTENT_NONE; } private ICodeInfo loadContent() { if (resFile == null || type != JResType.FILE) { return ICodeInfo.EMPTY; } ResContainer rc = resFile.loadContent(); if (rc == null) { return ICodeInfo.EMPTY; } if (rc.getDataType() == ResContainer.DataType.RES_TABLE) { ICodeInfo codeInfo = loadCurrentSingleRes(rc); List nodes = ResTableHelper.buildTree(this, rc); sortResNodes(nodes); subNodes = nodes; UiUtils.uiRun(this::update); return codeInfo; } // single node return loadCurrentSingleRes(rc); } private ICodeInfo loadCurrentSingleRes(ResContainer rc) { switch (rc.getDataType()) { case TEXT: case RES_TABLE: return rc.getText(); case RES_LINK: try { ResourceFile resourceFile = rc.getResLink(); return ResourcesLoader.decodeStream(resourceFile, (size, is) -> { // TODO: check size before loading if (size > 10 * 1024 * 1024L) { return new SimpleCodeInfo("File too large for view"); } Charset charset; if (resourceFile.getType().getContentType() == ResourceContentType.CONTENT_TEXT) { charset = StandardCharsets.UTF_8; } else { // force one byte charset for binary data to have the same offsets as in a byte array charset = StandardCharsets.US_ASCII; } return ResourcesLoader.loadToCodeWriter(is, charset); }); } catch (Exception e) { return new SimpleCodeInfo("Failed to load resource file:\n" + Utils.getStackTrace(e)); } case DECODED_DATA: default: return new SimpleCodeInfo("Unexpected resource type: " + rc); } } @Override public String getSyntaxName() { if (resFile == null) { return null; } switch (resFile.getType()) { case CODE: return super.getSyntaxName(); case MANIFEST: case XML: case ARSC: return SyntaxConstants.SYNTAX_STYLE_XML; default: String syntax = getSyntaxByExtension(resFile.getDeobfName()); if (syntax != null) { return syntax; } return super.getSyntaxName(); } } private static final Map EXTENSION_TO_FILE_SYNTAX = jadx.core.utils.Utils.newConstStringMap( "java", SyntaxConstants.SYNTAX_STYLE_JAVA, "smali", AbstractCodeArea.SYNTAX_STYLE_SMALI, "js", SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT, "ts", SyntaxConstants.SYNTAX_STYLE_TYPESCRIPT, "json", SyntaxConstants.SYNTAX_STYLE_JSON, "css", SyntaxConstants.SYNTAX_STYLE_CSS, "less", SyntaxConstants.SYNTAX_STYLE_LESS, "html", SyntaxConstants.SYNTAX_STYLE_HTML, "xml", SyntaxConstants.SYNTAX_STYLE_XML, "yaml", SyntaxConstants.SYNTAX_STYLE_YAML, "properties", SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE, "ini", SyntaxConstants.SYNTAX_STYLE_INI, "sql", SyntaxConstants.SYNTAX_STYLE_SQL); private String getSyntaxByExtension(String name) { int dot = name.lastIndexOf('.'); if (dot == -1) { return null; } String ext = name.substring(dot + 1); return EXTENSION_TO_FILE_SYNTAX.get(ext); } @Override public Icon getIcon() { switch (type) { case ROOT: return ROOT_ICON; case DIR: return Icons.FOLDER; case FILE: ResourceType resType = resFile.getType(); switch (resType) { case MANIFEST: return MANIFEST_ICON; case ARSC: return ARSC_ICON; case XML: return XML_ICON; case IMG: return IMAGE_ICON; case LIB: return SO_ICON; case CODE: return JAVA_ICON; case APK: return APK_ICON; case VIDEOS: return VIDEO_ICON; case SOUNDS: return AUDIO_ICON; case FONT: return FONT_ICON; case HTML: return HTML_ICON; case JSON: return JSON_ICON; case TEXT: return TEXT_ICON; case ARCHIVE: return ARCHIVE_ICON; case UNKNOWN: return UNKNOWN_ICON; } return UNKNOWN_ICON; } return Icons.FILE; } public static boolean isSupportedForView(ResourceType type) { switch (type) { case SOUNDS: case VIDEOS: case ARCHIVE: case APK: return false; case MANIFEST: case XML: case ARSC: case IMG: case LIB: case FONT: case TEXT: case JSON: case HTML: case UNKNOWN: return true; } return true; } public static boolean isOpenInExternalTool(ResourceType type) { switch (type) { case SOUNDS: case VIDEOS: return true; default: return false; } } public ResourceFile getResFile() { return resFile; } @Override public JClass getJParent() { return null; } @Override public String getID() { if (type == JResType.ROOT) { return "JResources"; } return makeString(); } @Override public String makeString() { return shortName; } @Override public String makeLongString() { return name; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } JResource other = (JResource) o; return name.equals(other.name) && type.equals(other.type); } @Override public int hashCode() { return name.hashCode() + 31 * type.ordinal(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java ================================================ package jadx.gui.treemodel; import java.io.File; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.regex.Pattern; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.tree.TreeNode; import org.jetbrains.annotations.Nullable; import jadx.api.ResourceFile; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.JadxWrapper; import jadx.gui.settings.JadxProject; import jadx.gui.treemodel.JResource.JResType; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class JRoot extends JNode { private static final long serialVersionUID = 8888495789773527342L; private static final ImageIcon ROOT_ICON = UiUtils.openSvgIcon("nodes/rootPackageFolder"); private final transient JadxWrapper wrapper; private final transient MainWindow mainWindow; private transient boolean flatPackages = false; private final transient List customNodes = new ArrayList<>(); public JRoot(MainWindow mainWindow) { this.mainWindow = mainWindow; this.wrapper = mainWindow.getWrapper(); } public final void update() { removeAllChildren(); add(new JInputs(mainWindow)); add(new JSources(this, wrapper)); List resources = wrapper.getResources(); if (!resources.isEmpty()) { add(getHierarchyResources(resources)); } for (JNode customNode : customNodes) { add(customNode); } } private JResource getHierarchyResources(List resources) { JResource root = new JResource(null, NLS.str("tree.resources_title"), JResType.ROOT); String splitPathStr = Pattern.quote(File.separator); for (ResourceFile rf : resources) { String rfName; if (rf.getZipEntry() != null) { rfName = rf.getDeobfName(); } else { rfName = new File(rf.getDeobfName()).getName(); } String[] parts = new File(rfName).getPath().split(splitPathStr); JResource curRf = root; int count = parts.length; for (int i = 0; i < count - 1; i++) { String name = parts[i]; JResource subRF = getSubNodeByName(curRf, name); if (subRF == null) { subRF = new JResource(null, name, JResType.DIR); curRf.addSubNode(subRF); } curRf = subRF; } JResource leaf = new JResource(rf, rf.getDeobfName(), parts[count - 1], JResType.FILE); curRf.addSubNode(leaf); } root.sortSubNodes(); root.update(); return root; } public static JResource getSubNodeByName(JResource rf, String name) { for (JResource sub : rf.getSubNodes()) { if (sub.getName().equals(name)) { return sub; } } return null; } public @Nullable JResource searchResourceByName(String name) { Enumeration en = this.breadthFirstEnumeration(); while (en.hasMoreElements()) { Object obj = en.nextElement(); if (obj instanceof JResource) { JResource res = (JResource) obj; if (res.getName().equals(name)) { return res; } } } return null; } public @Nullable JNode searchNode(JNode node) { Enumeration en = this.breadthFirstEnumeration(); while (en.hasMoreElements()) { Object obj = en.nextElement(); if (node.equals(obj)) { return (JNode) obj; } } return null; } public JNode followStaticPath(String... path) { List list = Arrays.asList(path); JNode node = getNodeByClsPath(this, 0, list); if (node == null) { throw new JadxRuntimeException("Incorrect static path in tree: " + list); } return node; } private static @Nullable JNode getNodeByClsPath(JNode start, int pos, List path) { if (pos >= path.size()) { return start; } String clsName = path.get(pos); Enumeration en = start.children(); while (en.hasMoreElements()) { JNode node = (JNode) en.nextElement(); if (node.getClass().getSimpleName().equals(clsName)) { return getNodeByClsPath(node, pos + 1, path); } } return null; } public boolean isFlatPackages() { return flatPackages; } public void setFlatPackages(boolean flatPackages) { if (this.flatPackages != flatPackages) { this.flatPackages = flatPackages; update(); } } public void replaceCustomNode(@Nullable JNode node) { if (node == null) { return; } Class nodeCls = node.getClass(); customNodes.removeIf(n -> n.getClass().equals(nodeCls)); customNodes.add(node); } public List getCustomNodes() { return customNodes; } @Override public Icon getIcon() { return ROOT_ICON; } @Override public JClass getJParent() { return null; } @Override public String getID() { return "JRoot"; } @Override public String makeString() { JadxProject project = wrapper.getProject(); if (project.getProjectPath() != null) { return project.getName(); } List paths = project.getFilePaths(); int count = paths.size(); if (count == 0) { return "File not open"; } if (count == 1) { Path fileNamePath = paths.get(0).getFileName(); if (fileNamePath != null) { return fileNamePath.toString(); } return paths.get(0).toString(); } return count + " files"; } @Override public String getTooltip() { List paths = wrapper.getProject().getFilePaths(); int count = paths.size(); if (count < 2) { return null; } // Show list of loaded files (full path) StringBuilder sb = new StringBuilder(""); for (Path p : paths) { sb.append(UiUtils.escapeHtml(p.toString())); sb.append("
"); } sb.append(""); return sb.toString(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JSources.java ================================================ package jadx.gui.treemodel; import java.util.List; import javax.swing.Icon; import javax.swing.ImageIcon; import jadx.gui.JadxWrapper; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.pkgs.PackageHelper; public class JSources extends JNode { private static final long serialVersionUID = 8962924556824862801L; private static final ImageIcon ROOT_ICON = UiUtils.openSvgIcon("nodes/packageClasses"); private final transient JadxWrapper wrapper; private final transient boolean flatPackages; public JSources(JRoot jRoot, JadxWrapper wrapper) { this.flatPackages = jRoot.isFlatPackages(); this.wrapper = wrapper; update(); } public final void update() { removeAllChildren(); PackageHelper packageHelper = wrapper.getCache().getPackageHelper(); List roots = packageHelper.getRoots(flatPackages); for (JPackage rootPkg : roots) { rootPkg.update(); add(rootPkg); } } @Override public Icon getIcon() { return ROOT_ICON; } @Override public JClass getJParent() { return null; } @Override public String getID() { return "JSources"; } @Override public String makeString() { return NLS.str("tree.sources_title"); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JSubResource.java ================================================ package jadx.gui.treemodel; import java.util.Objects; import org.jetbrains.annotations.Nullable; import jadx.api.ResourceFile; /** * Resource inside resource file. * Add base file prefix to distinguish from other files. */ public class JSubResource extends JResource { public static final String SUB_RES_PREFIX = ":/"; public JResource baseRes; public JSubResource(JResource baseRes, @Nullable ResourceFile resFile, String name, String shortName, JResType type) { super(resFile, name, shortName, type); this.baseRes = Objects.requireNonNull(baseRes); } public JResource getBaseRes() { return baseRes; } @Override public String makeLongString() { return baseRes.makeLongString() + SUB_RES_PREFIX + super.makeLongString(); } @Override public final boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof JSubResource)) { return false; } JSubResource other = (JSubResource) o; return baseRes.equals(other.baseRes) && getName().equals(other.getName()) && getType().equals(other.getType()); } @Override public int hashCode() { return baseRes.hashCode() + 31 * super.hashCode(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java ================================================ package jadx.gui.treemodel; import java.util.List; import java.util.Set; import javax.swing.Icon; import jadx.api.JavaNode; import jadx.api.JavaVariable; import jadx.api.data.ICodeRename; import jadx.api.data.impl.JadxCodeRef; import jadx.api.data.impl.JadxCodeRename; import jadx.api.data.impl.JadxNodeRef; import jadx.api.metadata.ICodeNodeRef; import jadx.core.deobf.NameMapper; import jadx.gui.ui.MainWindow; import jadx.gui.utils.UiUtils; public class JVariable extends JNode implements JRenameNode { private static final long serialVersionUID = -3002100457834453783L; private final JMethod jMth; private final JavaVariable var; public JVariable(JMethod jMth, JavaVariable var) { this.jMth = jMth; this.var = var; } public JavaVariable getJavaVarNode() { return var; } @Override public JavaNode getJavaNode() { return var; } @Override public JClass getRootClass() { return jMth.getRootClass(); } @Override public ICodeNodeRef getCodeNodeRef() { return var.getVarNode(); } @Override public JClass getJParent() { return jMth.getJParent(); } @Override public int getPos() { return var.getDefPos(); } @Override public Icon getIcon() { return null; } @Override public String makeString() { return var.getName(); } @Override public String makeLongString() { return var.getFullName(); } @Override public String makeLongStringHtml() { return UiUtils.typeFormatHtml(var.getName(), var.getType()); } @Override public boolean disableHtml() { return false; } @Override public String getTooltip() { String name = var.getName() + " (r" + var.getReg() + "v" + var.getSsa() + ")"; String fullType = UiUtils.escapeHtml(var.getType().toString()); return UiUtils.wrapHtml(fullType + ' ' + UiUtils.escapeHtml(name)); } @Override public boolean canRename() { return var.getName() != null; } @Override public String getTitle() { return makeLongStringHtml(); } @Override public boolean isValidName(String newName) { return NameMapper.isValidIdentifier(newName); } @Override public ICodeRename buildCodeRename(String newName, Set renames) { return new JadxCodeRename(JadxNodeRef.forMth(var.getMth()), JadxCodeRef.forVar(var), newName); } @Override public void removeAlias() { var.removeAlias(); } @Override public void addUpdateNodes(List toUpdate) { toUpdate.add(var.getMth()); } @Override public void reload(MainWindow mainWindow) { } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/treemodel/TextNode.java ================================================ package jadx.gui.treemodel; import javax.swing.Icon; public class TextNode extends JNode { private static final long serialVersionUID = 2342749142368352232L; private final String label; public TextNode(String str) { this.label = str; } @Override public JClass getJParent() { return null; } @Override public Icon getIcon() { return null; } @Override public String makeString() { return label; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java ================================================ package jadx.gui.ui; import java.awt.Color; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.swing.FocusManager; import javax.swing.JProgressBar; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import hu.akarnokd.rxjava3.swing.SwingSchedulers; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class HeapUsageBar extends JProgressBar { private static final long serialVersionUID = -8739563124249884967L; private static final Logger LOG = LoggerFactory.getLogger(HeapUsageBar.class); private static final double GB = 1024 * 1024 * 1024d; private static final Color GREEN = new Color(0, 180, 0); private static final Color RED = new Color(200, 0, 0); private final transient Runtime runtime = Runtime.getRuntime(); private final transient FocusManager focusManager = FocusManager.getCurrentManager(); private final double maxGB; private final long limit; private long peakUsed; private final String labelTemplate; private transient Disposable timer; private transient Color currentColor; public HeapUsageBar() { setBorderPainted(false); setStringPainted(true); long maxMemory = runtime.maxMemory(); peakUsed = 0; maxGB = maxMemory / GB; limit = maxMemory - UiUtils.MIN_FREE_MEMORY; labelTemplate = NLS.str("heapUsage.text"); setMaximum((int) (maxMemory / 1024)); setColor(GREEN); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { Runtime.getRuntime().gc(); HeapUsageBar.this.update(); if (LOG.isDebugEnabled()) { LOG.debug("Memory used: {}", UiUtils.memoryInfo()); } } }); } @Override public void setVisible(boolean enabled) { super.setVisible(enabled); if (enabled) { startTimer(); } else { reset(); } } private static class UpdateData { int value; String label; Color color; } private static final UpdateData SKIP_UPDATE = new UpdateData(); private void startTimer() { if (timer != null) { return; } update(); timer = Flowable.interval(2, TimeUnit.SECONDS, Schedulers.newThread()) .map(i -> prepareUpdate()) .filter(update -> update != SKIP_UPDATE) .distinctUntilChanged((a, b) -> Objects.equals(a.label, b.label)) // pass only if label changed .subscribeOn(SwingSchedulers.edt()) .subscribe(this::applyUpdate); } public UpdateData prepareUpdate() { if (focusManager.getActiveWindow() == null) { // skip update if app window not active return SKIP_UPDATE; } UpdateData updateData = new UpdateData(); long used = runtime.totalMemory() - runtime.freeMemory(); if (used > peakUsed) { peakUsed = used; } updateData.value = (int) (used / 1024); updateData.label = String.format(labelTemplate, used / GB, maxGB, peakUsed / GB); updateData.color = used > limit ? RED : GREEN; return updateData; } public void applyUpdate(UpdateData update) { setValue(update.value); setString(update.label); setColor(update.color); } private void setColor(Color color) { if (currentColor != color) { setForeground(color); currentColor = color; } } private void update() { UpdateData update = prepareUpdate(); if (update != SKIP_UPDATE) { applyUpdate(update); } } public void reset() { if (timer != null) { timer.dispose(); timer = null; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/JadxEventQueue.java ================================================ package jadx.gui.ui; import java.awt.AWTEvent; import java.awt.EventQueue; import java.awt.Toolkit; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import jadx.commons.app.JadxSystemInfo; public class JadxEventQueue extends EventQueue { private static final boolean IS_X_TOOLKIT = JadxSystemInfo.IS_LINUX && "sun.awt.X11.XToolkit".equals(Toolkit.getDefaultToolkit().getClass().getName()); public static void register() { if (IS_X_TOOLKIT) { Toolkit.getDefaultToolkit().getSystemEventQueue().push(new JadxEventQueue()); } } private JadxEventQueue() { } @Override protected void dispatchEvent(AWTEvent event) { AWTEvent mappedEvent = mapEvent(event); super.dispatchEvent(mappedEvent); } private static AWTEvent mapEvent(AWTEvent event) { if (IS_X_TOOLKIT && event instanceof MouseEvent && ((MouseEvent) event).getButton() > 3) { return mapXWindowMouseEvent((MouseEvent) event); } return event; } @SuppressWarnings({ "deprecation", "MagicConstant" }) private static AWTEvent mapXWindowMouseEvent(MouseEvent src) { if (src.getButton() < 6) { // buttons 4-5 come from touchpad, they must be converted to horizontal scrolling events int modifiers = src.getModifiers() | InputEvent.SHIFT_DOWN_MASK; return new MouseWheelEvent(src.getComponent(), MouseEvent.MOUSE_WHEEL, src.getWhen(), modifiers, src.getX(), src.getY(), 0, false, MouseWheelEvent.WHEEL_UNIT_SCROLL, src.getClickCount(), src.getButton() == 4 ? -1 : 1); } else { // Here we "shift" events with buttons `6` and `7` to similar events with buttons 4 and 5 // See `java.awt.InputEvent#BUTTON_DOWN_MASK`, 1<<14 is the 4th physical button, 1<<15 is the 5th. int modifiers = src.getModifiers() | (1 << (8 + src.getButton())); return new MouseEvent(src.getComponent(), src.getID(), src.getWhen(), modifiers, src.getX(), src.getY(), 1, src.isPopupTrigger(), src.getButton() - 2); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java ================================================ package jadx.gui.ui; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.io.File; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.files.FileUtils; /** * Enables drop support from external applications for the {@link MainWindow} (load dropped APK * file) */ public class MainDropTarget implements DropTargetListener { private static final Logger LOG = LoggerFactory.getLogger(MainDropTarget.class); private final MainWindow mainWindow; public MainDropTarget(MainWindow mainWindow) { this.mainWindow = mainWindow; } protected void processDrag(DropTargetDragEvent dtde) { if (dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { dtde.acceptDrag(DnDConstants.ACTION_COPY); } else { dtde.rejectDrag(); } } @Override public void dragEnter(DropTargetDragEvent dtde) { processDrag(dtde); } @Override public void dragOver(DropTargetDragEvent dtde) { processDrag(dtde); } @Override public void dropActionChanged(DropTargetDragEvent dtde) { } @Override @SuppressWarnings("unchecked") public void drop(DropTargetDropEvent dtde) { if (!dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { dtde.rejectDrop(); return; } dtde.acceptDrop(dtde.getDropAction()); try { Transferable transferable = dtde.getTransferable(); List transferData = (List) transferable.getTransferData(DataFlavor.javaFileListFlavor); if (!transferData.isEmpty()) { dtde.dropComplete(true); mainWindow.open(FileUtils.toPaths(transferData)); } } catch (Exception e) { LOG.error("File drop operation failed", e); } } @Override public void dragExit(DropTargetEvent dte) { } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java ================================================ package jadx.gui.ui; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Desktop; import java.awt.Dimension; import java.awt.DisplayMode; import java.awt.Font; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTarget; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.geom.AffineTransform; import java.io.File; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Timer; import java.util.TimerTask; import java.util.function.Consumer; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Box; import javax.swing.JCheckBox; import javax.swing.JCheckBoxMenuItem; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import javax.swing.UIManager; import javax.swing.WindowConstants; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeWillExpandListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.exbin.bined.swing.section.SectCodeArea; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.extras.FlatInspector; import com.formdev.flatlaf.extras.FlatUIDefaultsInspector; import com.formdev.flatlaf.util.UIScale; import ch.qos.logback.classic.Level; import jadx.api.JadxArgs; import jadx.api.JavaClass; import jadx.api.JavaNode; import jadx.api.ResourceFile; import jadx.api.plugins.events.JadxEvents; import jadx.api.plugins.events.types.ReloadProject; import jadx.api.plugins.events.types.ReloadSettingsWindow; import jadx.api.plugins.utils.CommonFileUtils; import jadx.commons.app.JadxSystemInfo; import jadx.core.Jadx; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.ListUtils; import jadx.core.utils.StringUtils; import jadx.core.utils.android.AndroidManifestParser; import jadx.core.utils.android.AppAttribute; import jadx.core.utils.android.ApplicationParams; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.gui.JadxWrapper; import jadx.gui.cache.manager.CacheManager; import jadx.gui.device.debugger.BreakpointManager; import jadx.gui.events.services.RenameService; import jadx.gui.events.types.JadxGuiEventsImpl; import jadx.gui.jobs.BackgroundExecutor; import jadx.gui.jobs.DecompileTask; import jadx.gui.jobs.ExportTask; import jadx.gui.jobs.IBackgroundTask; import jadx.gui.jobs.TaskStatus; import jadx.gui.jobs.TaskWithExtraOnFinish; import jadx.gui.logs.LogCollector; import jadx.gui.logs.LogOptions; import jadx.gui.logs.LogPanel; import jadx.gui.plugins.context.CommonGuiPluginsContext; import jadx.gui.plugins.context.TreePopupMenuEntry; import jadx.gui.plugins.mappings.RenameMappingsGui; import jadx.gui.plugins.quark.QuarkDialog; import jadx.gui.report.ExceptionDialog; import jadx.gui.report.JadxExceptionHandler; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.data.SaveOptionEnum; import jadx.gui.settings.ui.JadxSettingsWindow; import jadx.gui.tree.TreeExpansionService; import jadx.gui.treemodel.ApkSignatureNode; import jadx.gui.treemodel.JLoadableNode; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JPackage; import jadx.gui.treemodel.JResource; import jadx.gui.treemodel.JRoot; import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.action.JadxGuiAction; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.codearea.AbstractCodeContentPanel; import jadx.gui.ui.codearea.EditorViewState; import jadx.gui.ui.codearea.theme.EditorThemeManager; import jadx.gui.ui.dialog.ADBDialog; import jadx.gui.ui.dialog.AboutDialog; import jadx.gui.ui.dialog.CharsetDialog; import jadx.gui.ui.dialog.GotoAddressDialog; import jadx.gui.ui.dialog.LogViewerDialog; import jadx.gui.ui.dialog.SearchDialog; import jadx.gui.ui.export.ExportProjectDialog; import jadx.gui.ui.filedialog.FileDialogWrapper; import jadx.gui.ui.filedialog.FileOpenMode; import jadx.gui.ui.hexviewer.HexInspectorPanel; import jadx.gui.ui.hexviewer.HexPreviewPanel; import jadx.gui.ui.menu.HiddenMenuItem; import jadx.gui.ui.menu.JadxMenu; import jadx.gui.ui.menu.JadxMenuBar; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.panel.IssuesPanel; import jadx.gui.ui.panel.JDebuggerPanel; import jadx.gui.ui.panel.ProgressPanel; import jadx.gui.ui.popupmenu.RecentProjectsMenuListener; import jadx.gui.ui.startpage.StartPageNode; import jadx.gui.ui.tab.EditorSyncManager; import jadx.gui.ui.tab.NavigationController; import jadx.gui.ui.tab.QuickTabsTree; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.ui.tab.TabsController; import jadx.gui.ui.tab.dnd.TabDndController; import jadx.gui.ui.treenodes.SummaryNode; import jadx.gui.ui.treenodes.UndisplayedStringsNode; import jadx.gui.update.JadxUpdate; import jadx.gui.utils.CacheObject; import jadx.gui.utils.DesktopEntryUtils; import jadx.gui.utils.FontUtils; import jadx.gui.utils.ILoadListener; import jadx.gui.utils.Icons; import jadx.gui.utils.LafManager; import jadx.gui.utils.Link; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.dbg.UIWatchDog; import jadx.gui.utils.fileswatcher.LiveReloadWorker; import jadx.gui.utils.shortcut.ShortcutsController; import jadx.gui.utils.ui.ActionHandler; import jadx.gui.utils.ui.FileOpenerHelper; import jadx.gui.utils.ui.NodeLabel; public class MainWindow extends JFrame { private static final Logger LOG = LoggerFactory.getLogger(MainWindow.class); private static final String DEFAULT_TITLE = "jadx-gui"; private static final double BORDER_RATIO = 0.15; private static final double WINDOW_RATIO = 1 - BORDER_RATIO * 2; public static final double SPLIT_PANE_RESIZE_WEIGHT = 0.15; private final transient JadxWrapper wrapper; private final transient JadxSettings settings; private final transient CacheObject cacheObject; private final transient CacheManager cacheManager; private final transient BackgroundExecutor backgroundExecutor; private final transient JadxGuiEventsImpl events = new JadxGuiEventsImpl(); private final transient TreeExpansionService treeExpansionService; private final TabsController tabsController; private final NavigationController navController; private final EditorSyncManager editorSyncManager; private final EditorThemeManager editorThemeManager; private transient @NotNull JadxProject project; private transient JadxGuiAction newProjectAction; private transient JadxGuiAction saveProjectAction; private transient JPanel mainPanel; private transient JSplitPane treeSplitPane; private transient JSplitPane rightSplitPane; private transient JSplitPane bottomSplitPane; private transient JSplitPane quickTabsAndCodeSplitPane; private JTree tree; private DefaultTreeModel treeModel; private JRoot treeRoot; private TabbedPane tabbedPane; private HeapUsageBar heapUsageBar; private transient boolean treeReloading; private boolean isFlattenPackage; private JToggleButton flatPkgButton; private JCheckBoxMenuItem flatPkgMenuItem; private JToggleButton deobfToggleBtn; private JCheckBoxMenuItem deobfMenuItem; private JCheckBoxMenuItem liveReloadMenuItem; private final LiveReloadWorker liveReloadWorker; private transient Link updateLink; private transient ProgressPanel progressPane; private transient IssuesPanel issuesPanel; private transient @Nullable LogPanel logPanel; private transient @Nullable JDebuggerPanel debuggerPanel; private transient @Nullable QuickTabsTree quickTabsTree; private final List loadListeners = new ArrayList<>(); private final List> treeUpdateListener = new ArrayList<>(); private boolean loaded; private boolean settingsOpen = false; private boolean showUndisplayedCharsDialog; private final ShortcutsController shortcutsController; private JadxMenuBar menuBar; private JMenu pluginsMenu; public JMenu hexViewerMenu; private final transient RenameMappingsGui renameMappings; public MainWindow(JadxSettings settings) { this.settings = settings; this.project = new JadxProject(this); this.wrapper = new JadxWrapper(this); this.cacheObject = new CacheObject(wrapper); this.liveReloadWorker = new LiveReloadWorker(this); this.renameMappings = new RenameMappingsGui(this); this.cacheManager = new CacheManager(settings); this.shortcutsController = new ShortcutsController(settings); this.tabsController = new TabsController(this); this.navController = new NavigationController(this); this.editorThemeManager = new EditorThemeManager(settings); JadxEventQueue.register(); JadxExceptionHandler.register(this); resetCache(); initUI(); this.editorSyncManager = new EditorSyncManager(this, tabbedPane); this.backgroundExecutor = new BackgroundExecutor(settings, progressPane); this.treeExpansionService = new TreeExpansionService(this, tree); initMenuAndToolbar(); UiUtils.setWindowIcons(this); this.shortcutsController.registerMouseEventListener(this); loadSettings(); initEvents(); update(); checkForUpdate(); } public void init() { pack(); setLocationAndPosition(); treeSplitPane.setDividerLocation(settings.getTreeWidth()); heapUsageBar.setVisible(settings.isShowHeapUsageBar()); setVisible(true); processCommandLineArgs(); } private void processCommandLineArgs() { if (settings.getFiles().isEmpty()) { tabsController.selectTab(new StartPageNode()); } else { open(FileUtils.fileNamesToPaths(settings.getFiles()), this::handleSelectClassOption); } } private void handleSelectClassOption() { if (settings.getCmdSelectClass() != null) { JavaNode javaNode = wrapper.searchJavaClassByFullAlias(settings.getCmdSelectClass()); if (javaNode == null) { javaNode = wrapper.searchJavaClassByOrigClassName(settings.getCmdSelectClass()); } if (javaNode == null) { JOptionPane.showMessageDialog(this, NLS.str("msg.cmd_select_class_error", settings.getCmdSelectClass()), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); return; } tabsController.codeJump(cacheObject.getNodeCache().makeFrom(javaNode)); } } private void checkForUpdate() { if (!settings.isCheckForUpdates()) { return; } new JadxUpdate().check(settings.getJadxUpdateChannel(), release -> SwingUtilities.invokeLater(() -> { switch (settings.getJadxUpdateChannel()) { case STABLE: updateLink.setUrl(JadxUpdate.JADX_RELEASES_URL); break; case UNSTABLE: updateLink.setUrl(JadxUpdate.JADX_ARTIFACTS_URL); break; } updateLink.setText(NLS.str("menu.update_label", release.getName())); updateLink.setVisible(true); })); } public void openFileDialog() { showOpenDialog(FileOpenMode.OPEN); } public void openProjectDialog() { showOpenDialog(FileOpenMode.OPEN_PROJECT); } private void showOpenDialog(FileOpenMode mode) { saveAll(); if (!ensureProjectIsSaved()) { return; } FileDialogWrapper fileDialog = new FileDialogWrapper(this, mode); List openPaths = fileDialog.show(); if (!openPaths.isEmpty()) { settings.setLastOpenFilePath(fileDialog.getCurrentDir()); open(openPaths); } } public void addFiles() { FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.ADD); List addPaths = fileDialog.show(); if (!addPaths.isEmpty()) { addFiles(addPaths); } } public void addFiles(List addPaths) { project.setFilePaths(ListUtils.distinctMergeSortedLists(addPaths, project.getFilePaths())); reopen(); } private void newProject() { saveAll(); if (!ensureProjectIsSaved()) { return; } UiUtils.bgRun(() -> { closeAll(); updateProject(new JadxProject(this)); }); } private void saveProject() { saveOpenTabs(); if (!project.isSaveFileSelected()) { saveProjectAs(); } else { project.save(); update(); } } private void saveProjectAs() { FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.SAVE_PROJECT); if (project.getFilePaths().size() == 1) { // If there is only one file loaded we suggest saving the jadx project file next to the loaded file Path projectPath = getProjectPathForFile(this.project.getFilePaths().get(0)); fileDialog.setSelectedFile(projectPath); } List saveFiles = fileDialog.show(); if (saveFiles.isEmpty()) { return; } settings.setLastSaveProjectPath(fileDialog.getCurrentDir()); Path savePath = saveFiles.get(0); if (!savePath.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(JadxProject.PROJECT_EXTENSION)) { savePath = savePath.resolveSibling(savePath.getFileName() + "." + JadxProject.PROJECT_EXTENSION); } if (Files.exists(savePath)) { int res = JOptionPane.showConfirmDialog( this, NLS.str("confirm.save_as_message", savePath.getFileName()), NLS.str("confirm.save_as_title"), JOptionPane.YES_NO_OPTION); if (res == JOptionPane.NO_OPTION) { return; } } project.saveAs(savePath); settings.addRecentProject(savePath); update(); } public void removeInput(Path file) { int dialogResult = JOptionPane.showConfirmDialog( this, NLS.str("message.confirm_remove_script"), NLS.str("msg.warning_title"), JOptionPane.YES_NO_OPTION); if (dialogResult == JOptionPane.NO_OPTION) { return; } List inputs = project.getFilePaths(); inputs.remove(file); refreshTree(inputs); } public void renameInput(Path file) { String newName = JOptionPane.showInputDialog(this, NLS.str("message.enter_new_name"), file.getFileName().toString()); if (newName == null || newName.trim().isEmpty()) { return; } Path targetPath = file.resolveSibling(newName); boolean success = FileUtils.renameFile(file, targetPath); if (success) { List inputs = project.getFilePaths(); inputs.remove(file); inputs.add(targetPath); refreshTree(inputs); } else { JOptionPane.showMessageDialog(this, NLS.str("message.could_not_rename"), NLS.str("message.errorTitle"), JOptionPane.ERROR_MESSAGE); } } private void refreshTree(List inputs) { project.setFilePaths(inputs); project.save(); reopen(); } public void open(Path path) { open(Collections.singletonList(path), UiUtils.EMPTY_RUNNABLE); } public void open(List paths) { open(paths, UiUtils.EMPTY_RUNNABLE); } private void open(List paths, Runnable onFinish) { saveAll(); UiUtils.bgRun(() -> { closeAll(); if (paths.size() == 1 && openSingleFile(paths.get(0), onFinish)) { return; } // start new project project = new JadxProject(this); project.setFilePaths(paths); showUndisplayedCharsDialog = false; loadFiles(onFinish); }); } private boolean openSingleFile(Path singleFile, Runnable onFinish) { if (singleFile.getFileName() == null) { return false; } String fileExtension = CommonFileUtils.getFileExtension(singleFile.getFileName().toString()); if (fileExtension != null && fileExtension.equalsIgnoreCase(JadxProject.PROJECT_EXTENSION)) { openProject(singleFile, onFinish); return true; } // check if project file already saved with default name Path projectPath = getProjectPathForFile(singleFile); if (Files.exists(projectPath)) { openProject(projectPath, onFinish); return true; } return false; } private static Path getProjectPathForFile(Path loadedFile) { String fileName = loadedFile.getFileName() + "." + JadxProject.PROJECT_EXTENSION; return loadedFile.resolveSibling(fileName); } public void reopen() { LOG.debug("starting reopen"); UiUtils.bgRun(() -> { getBackgroundExecutor().waitForComplete(); synchronized (ReloadProject.EVENT) { saveAll(); closeAll(); System.gc(); loadFiles(() -> { menuBar.reloadShortcuts(); events().send(ReloadSettingsWindow.INSTANCE); LOG.debug("reopen complete"); }); } }); } private void openProject(Path path, Runnable onFinish) { LOG.debug("Loading project: {}", path); project = JadxProject.load(this, path); settings.addRecentProject(path); loadFiles(onFinish); } private void loadFiles(Runnable onFinish) { if (project.getFilePaths().isEmpty()) { tabsController.selectTab(new StartPageNode()); onFinish.run(); return; } backgroundExecutor.execute(NLS.str("progress.load"), () -> { try { wrapper.open(); } catch (Exception e) { LOG.error("Project load error", e); closeAll(); } }, status -> { if (status == TaskStatus.CANCEL_BY_MEMORY) { showHeapUsageBar(); UiUtils.errorMessage(this, NLS.str("message.memoryLow")); return; } if (status != TaskStatus.COMPLETE) { LOG.warn("Loading task incomplete, status: {}", status); return; } checkLoadedStatus(); onOpen(onFinish); }); } private void saveAll() { saveOpenTabs(); project.setTreeExpansions(treeExpansionService.save()); BreakpointManager.saveAndExit(); } private void closeAll() { UiUtils.notUiThreadGuard(); cancelBackgroundJobs(); UiUtils.uiRunAndWait(() -> { tabsController.forceCloseAllTabs(); tabbedPane.reset(); navController.reset(); shortcutsController.reset(); clearTree(); UiUtils.resetClipboardOwner(); update(); }); wrapper.close(); LogCollector.getInstance().reset(); resetCache(); notifyLoadListeners(false); } private void checkLoadedStatus() { if (!wrapper.getClasses().isEmpty()) { return; } int errors = issuesPanel.getErrorsCount(); if (errors > 0) { int result = JOptionPane.showConfirmDialog(this, NLS.str("message.load_errors", errors), NLS.str("message.errorTitle"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.ERROR_MESSAGE); if (result == JOptionPane.OK_OPTION) { showLogViewer(LogOptions.allWithLevel(Level.ERROR)); } } else { showLogViewer(LogOptions.allWithLevel(Level.WARN)); UiUtils.showMessageBox(this, NLS.str("message.no_classes")); } } private void onOpen(Runnable onFinish) { initTree(); updateLiveReload(project.isEnableLiveReload()); BreakpointManager.init(project.getFilePaths().get(0).toAbsolutePath().getParent()); List openTabs = project.getOpenTabs(this); backgroundExecutor.startLoading( () -> preLoadOpenTabs(openTabs), () -> { restoreOpenTabs(openTabs); update(); notifyLoadListeners(true); onFinish.run(); checkIfCodeHasNonPrintableChars(); runInitialBackgroundJobs(); }); // queue tree state restore after loading task treeExpansionService.load(project.getTreeExpansions()); } public void passesReloaded() { UiUtils.uiThreadGuard(); tabbedPane.reloadInactiveTabs(); reloadTreePreservingState(); } private void initEvents() { events().global().addListener(JadxEvents.RELOAD_PROJECT, ev -> UiUtils.uiRun(this::reopen)); RenameService.init(this); } public void updateLiveReload(boolean state) { if (liveReloadWorker.isStarted() == state) { return; } project.setEnableLiveReload(state); liveReloadMenuItem.setEnabled(false); backgroundExecutor.execute( (state ? "Starting" : "Stopping") + " live reload", () -> liveReloadWorker.updateState(state), s -> { liveReloadMenuItem.setState(state); liveReloadMenuItem.setEnabled(true); }); } private void addTreeCustomNodes() { treeRoot.replaceCustomNode(ApkSignatureNode.getApkSignature(wrapper)); treeRoot.replaceCustomNode(new SummaryNode(this)); } private boolean ensureProjectIsSaved() { if (project.isSaved() || project.isInitial()) { return true; } if (project.getFilePaths().isEmpty()) { // ignore blank project save return true; } // Check if we saved settings that indicate what to do if (settings.getSaveOption() == SaveOptionEnum.NEVER) { return true; } if (settings.getSaveOption() == SaveOptionEnum.ALWAYS) { saveProject(); return true; } JCheckBox remember = new JCheckBox(NLS.str("confirm.remember")); JLabel message = new JLabel(NLS.str("confirm.not_saved_message")); JPanel inner = new JPanel(new BorderLayout()); inner.add(remember, BorderLayout.SOUTH); inner.add(message, BorderLayout.NORTH); int res = JOptionPane.showConfirmDialog( this, inner, NLS.str("confirm.not_saved_title"), JOptionPane.YES_NO_CANCEL_OPTION); switch (res) { case JOptionPane.YES_OPTION: if (remember.isSelected()) { settings.setSaveOption(SaveOptionEnum.ALWAYS); settings.sync(); } saveProject(); return true; case JOptionPane.NO_OPTION: if (remember.isSelected()) { settings.setSaveOption(SaveOptionEnum.NEVER); settings.sync(); } return true; case JOptionPane.CANCEL_OPTION: return false; } return true; } public void updateProject(@NotNull JadxProject jadxProject) { this.project = jadxProject; UiUtils.uiRun(this::update); } public void update() { UiUtils.uiThreadGuard(); newProjectAction.setEnabled(!project.isInitial()); saveProjectAction.setEnabled(loaded && !project.isSaved()); deobfToggleBtn.setSelected(settings.isDeobfuscationOn()); renameMappings.onUpdate(loaded); Path projectPath = project.getProjectPath(); String pathString; if (projectPath == null) { pathString = ""; } else { pathString = " [" + projectPath.toAbsolutePath().getParent() + ']'; } setTitle((project.isSaved() ? "" : '*') + project.getName() + pathString + " - " + DEFAULT_TITLE); } protected void resetCache() { cacheObject.reset(); } synchronized void runInitialBackgroundJobs() { if (settings.isAutoStartJobs()) { new Timer().schedule(new TimerTask() { @Override public void run() { requestFullDecompilation(); } }, 1000); } } public void requestFullDecompilation() { if (cacheObject.isFullDecompilationFinished()) { return; } backgroundExecutor.execute(new DecompileTask(this)); } public void resetCodeCache() { backgroundExecutor.execute( NLS.str("preferences.cache.task.delete"), () -> { try { getWrapper().getCurrentDecompiler().ifPresent(jadx -> { try { jadx.getArgs().getCodeCache().close(); } catch (Exception e) { LOG.error("Failed to close code cache", e); } }); Path cacheDir = project.getCacheDir(); project.resetCacheDir(); FileUtils.deleteDirIfExists(cacheDir); } catch (Exception e) { LOG.error("Error during code cache reset", e); } }, status -> events().send(ReloadProject.EVENT)); } public void cancelBackgroundJobs() { backgroundExecutor.cancelAll(); } public void exportProject() { ExportProjectDialog dialog = new ExportProjectDialog(this, props -> { JadxArgs args = wrapper.getArgs(); if (props.isAsGradleMode()) { args.setExportGradleType(props.getExportGradleType()); args.setSkipSources(false); args.setSkipResources(false); } else { args.setExportGradleType(null); args.setSkipSources(props.isSkipSources()); args.setSkipResources(props.isSkipResources()); } backgroundExecutor.execute(new ExportTask(this, wrapper, new File(props.getExportPath()))); }); dialog.setVisible(true); } public void initTree() { treeRoot = new JRoot(this); treeRoot.setFlatPackages(isFlattenPackage); treeModel.setRoot(treeRoot); addTreeCustomNodes(); treeRoot.update(); reloadTree(); } private void clearTree() { treeRoot = null; treeModel.setRoot(null); treeModel.reload(); } public void reloadTree() { treeReloading = true; treeUpdateListener.forEach(listener -> listener.accept(treeRoot)); treeModel.reload(); treeReloading = false; } public void rebuildPackagesTree() { treeRoot.update(); } /** * Simple save and restore tree state after renaming. * TODO: maybe need improve for find and update only changed node */ public void reloadTreePreservingState() { List treePath = treeExpansionService.save(); reloadTree(); treeExpansionService.load(treePath); } private void toggleFlattenPackage() { setFlattenPackage(!isFlattenPackage); } private void setFlattenPackage(boolean value) { isFlattenPackage = value; settings.setFlattenPackage(isFlattenPackage); flatPkgButton.setSelected(isFlattenPackage); flatPkgMenuItem.setState(isFlattenPackage); Object root = treeModel.getRoot(); if (root instanceof JRoot) { JRoot treeRoot = (JRoot) root; treeRoot.setFlatPackages(isFlattenPackage); reloadTree(); } } private void toggleDeobfuscation() { boolean deobfOn = !settings.isDeobfuscationOn(); settings.setDeobfuscationOn(deobfOn); settings.sync(); deobfToggleBtn.setSelected(deobfOn); deobfMenuItem.setState(deobfOn); reopen(); } private boolean nodeClickAction(@Nullable Object obj) { if (obj == null) { return false; } try { if (obj instanceof JResource) { JResource res = (JResource) obj; ResourceFile resFile = res.getResFile(); if (resFile != null) { if (JResource.isOpenInExternalTool(resFile.getType())) { FileOpenerHelper.openFile(this, res); return true; } if (JResource.isSupportedForView(resFile.getType())) { tabsController.selectTab(res, true); return true; } } } else if (obj instanceof JNode) { JNode treeNode = (JNode) obj; if (treeNode.hasContent() || treeNode.getJParent() != null) { tabsController.codeJump(treeNode, true); return true; } } } catch (Exception e) { LOG.error("Content loading error", e); } return false; } private void treeRightClickAction(MouseEvent e) { JNode node = getJNodeUnderMouse(e); if (node == null) { return; } JPopupMenu menu = node.onTreePopupMenu(this); CommonGuiPluginsContext pluginsContext = getWrapper().getGuiPluginsContext(); for (TreePopupMenuEntry entry : pluginsContext.getTreePopupMenuEntries()) { JMenuItem menuItem = entry.buildEntry(node); if (menuItem != null) { if (menu == null) { menu = new JPopupMenu(); } menu.add(menuItem); } } if (menu != null) { menu.show(e.getComponent(), e.getX(), e.getY()); } } @Nullable private JNode getJNodeUnderMouse(MouseEvent mouseEvent) { TreeNode treeNode = UiUtils.getTreeNodeUnderMouse(tree, mouseEvent); if (treeNode instanceof JNode) { return (JNode) treeNode; } return null; } // TODO: extract tree component into new class public void selectNodeInTree(JNode node) { if (node.getParent() == null && treeRoot != null) { // node not register in tree node = treeRoot.searchNode(node); if (node == null) { LOG.error("Class not found in tree"); return; } } TreeNode[] pathNodes = treeModel.getPathToRoot(node); if (pathNodes == null) { return; } TreePath path = new TreePath(pathNodes); tree.setSelectionPath(path); tree.makeVisible(path); tree.scrollPathToVisible(path); tree.requestFocus(); } public void textSearch() { ContentPanel panel = tabbedPane.getSelectedContentPanel(); if (panel instanceof AbstractCodeContentPanel) { AbstractCodeArea codeArea = ((AbstractCodeContentPanel) panel).getCodeArea(); if (codeArea != null) { String preferText = codeArea.getSelectedText(); if (StringUtils.isEmpty(preferText)) { preferText = codeArea.getWordUnderCaret(); } if (!StringUtils.isEmpty(preferText)) { SearchDialog.searchText(MainWindow.this, preferText); return; } } } SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.TEXT); } private void sendActionsToHexViewer(ActionModel action) { HexPreviewPanel hexPreviewPanel = getCurrentHexViewTab(); if (hexPreviewPanel != null) { HexInspectorPanel inspector = hexPreviewPanel.getInspector(); SectCodeArea hexEditor = hexPreviewPanel.getEditor(); switch (action) { case HEX_VIEWER_SHOW_INSPECTOR: hexPreviewPanel.getInspector().setVisible(!inspector.isVisible()); break; case HEX_VIEWER_CHANGE_ENCODING: String result = CharsetDialog.chooseCharset(this, hexEditor.getCharset().name()); if (!StringUtils.isEmpty(result)) { hexEditor.setCharset(Charset.forName(result)); } break; case HEX_VIEWER_GO_TO_ADDRESS: new GotoAddressDialog().showSetSelectionDialog(hexEditor); break; case HEX_VIEWER_FIND: hexPreviewPanel.showSearchBar(); break; } } } public HexPreviewPanel getCurrentHexViewTab() { ContentPanel panel = tabbedPane.getSelectedContentPanel(); if (panel instanceof AbstractCodeContentPanel) { Component childrenComponent = ((AbstractCodeContentPanel) panel).getChildrenComponent(); if (childrenComponent instanceof HexPreviewPanel) { return (HexPreviewPanel) childrenComponent; } } return null; } public void toggleHexViewMenu() { hexViewerMenu.setEnabled(getCurrentHexViewTab() != null); } public void goToMainActivity() { AndroidManifestParser parser = new AndroidManifestParser( AndroidManifestParser.getAndroidManifest(getWrapper().getResources()), EnumSet.of(AppAttribute.MAIN_ACTIVITY), getWrapper().getArgs().getSecurity()); if (!parser.isManifestFound()) { JOptionPane.showMessageDialog(MainWindow.this, NLS.str("error_dialog.not_found", "AndroidManifest.xml"), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); return; } try { ApplicationParams results = parser.parse(); if (results.getMainActivity() == null) { throw new JadxRuntimeException("Failed to get main activity name from manifest"); } JavaClass mainActivityClass = results.getMainActivityJavaClass(getWrapper().getDecompiler()); if (mainActivityClass == null) { throw new JadxRuntimeException("Failed to find main activity class: " + results.getMainActivity()); } tabsController.codeJump(getCacheObject().getNodeCache().makeFrom(mainActivityClass)); } catch (Exception e) { LOG.error("Main activity not found", e); JOptionPane.showMessageDialog(MainWindow.this, NLS.str("error_dialog.not_found", "Main Activity"), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); } } public void goToApplication() { AndroidManifestParser parser = new AndroidManifestParser( AndroidManifestParser.getAndroidManifest(getWrapper().getResources()), EnumSet.of(AppAttribute.APPLICATION), getWrapper().getArgs().getSecurity()); if (!parser.isManifestFound()) { JOptionPane.showMessageDialog(MainWindow.this, NLS.str("error_dialog.not_found", "AndroidManifest.xml"), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); return; } try { ApplicationParams results = parser.parse(); if (results.getApplication() == null) { throw new JadxRuntimeException("Failed to get application from manifest"); } JavaClass applicationClass = results.getApplicationJavaClass(getWrapper().getDecompiler()); if (applicationClass == null) { throw new JadxRuntimeException("Failed to find application class: " + results.getApplication()); } tabsController.codeJump(getCacheObject().getNodeCache().makeFrom(applicationClass)); } catch (Exception e) { LOG.error("Application not found", e); JOptionPane.showMessageDialog(MainWindow.this, NLS.str("error_dialog.not_found", "Application"), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); } } public void goToAndroidManifest() { ResourceFile androidManifest = AndroidManifestParser.getAndroidManifest(getWrapper().getResources()); if (androidManifest == null) { JOptionPane.showMessageDialog(MainWindow.this, NLS.str("error_dialog.not_found", "AndroidManifest.xml"), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); return; } JResource res = new JResource(androidManifest, androidManifest.getDeobfName(), JResource.JResType.FILE); tabsController.codeJump(res); } private void initMenuAndToolbar() { JadxGuiAction openAction = new JadxGuiAction(ActionModel.OPEN, this::openFileDialog); JadxGuiAction openProject = new JadxGuiAction(ActionModel.OPEN_PROJECT, this::openProjectDialog); JadxGuiAction addFilesAction = new JadxGuiAction(ActionModel.ADD_FILES, () -> addFiles()); newProjectAction = new JadxGuiAction(ActionModel.NEW_PROJECT, this::newProject); saveProjectAction = new JadxGuiAction(ActionModel.SAVE_PROJECT, this::saveProject); JadxGuiAction saveProjectAsAction = new JadxGuiAction(ActionModel.SAVE_PROJECT_AS, this::saveProjectAs); JadxGuiAction reloadAction = new JadxGuiAction(ActionModel.RELOAD, () -> UiUtils.uiRun(this::reopen)); JadxGuiAction liveReloadAction = new JadxGuiAction(ActionModel.LIVE_RELOAD, () -> updateLiveReload(!project.isEnableLiveReload())); liveReloadMenuItem = new JCheckBoxMenuItem(liveReloadAction); liveReloadMenuItem.setState(project.isEnableLiveReload()); JadxGuiAction exportAction = new JadxGuiAction(ActionModel.EXPORT, this::exportProject); JMenu recentProjects = new JadxMenu(NLS.str("menu.recent_projects"), shortcutsController); recentProjects.addMenuListener(new RecentProjectsMenuListener(this, recentProjects)); hexViewerMenu = new JadxMenu(NLS.str("menu.hex_viewer"), shortcutsController); initHexViewMenu(); JadxGuiAction prefsAction = new JadxGuiAction(ActionModel.PREFS, () -> openSettings()); JadxGuiAction exitAction = new JadxGuiAction(ActionModel.EXIT, this::closeWindow); isFlattenPackage = settings.isFlattenPackage(); flatPkgMenuItem = new JCheckBoxMenuItem(NLS.str("menu.flatten"), Icons.FLAT_PKG); flatPkgMenuItem.setState(isFlattenPackage); JadxGuiAction enablePreviewTabAction = new JadxGuiAction(ActionModel.PREVIEW_TAB, () -> { settings.setEnablePreviewTab(!settings.isEnablePreviewTab()); }); enablePreviewTabAction.setSelected(settings.isEnablePreviewTab()); JCheckBoxMenuItem heapUsageBarMenuItem = new JCheckBoxMenuItem(NLS.str("menu.heapUsageBar")); heapUsageBarMenuItem.setState(settings.isShowHeapUsageBar()); heapUsageBarMenuItem.addActionListener(event -> { settings.setShowHeapUsageBar(!settings.isShowHeapUsageBar()); heapUsageBar.setVisible(settings.isShowHeapUsageBar()); }); JCheckBoxMenuItem alwaysSelectOpened = new JCheckBoxMenuItem(NLS.str("menu.alwaysSelectOpened")); alwaysSelectOpened.setState(settings.isAlwaysSelectOpened()); alwaysSelectOpened.addActionListener(event -> { settings.setAlwaysSelectOpened(!settings.isAlwaysSelectOpened()); if (settings.isAlwaysSelectOpened()) { this.editorSyncManager.sync(); } }); JCheckBoxMenuItem dockLog = new JCheckBoxMenuItem(NLS.str("menu.dock_log")); dockLog.setState(settings.isDockLogViewer()); dockLog.addActionListener(event -> settings.saveDockLogViewer(!settings.isDockLogViewer())); ActionHandler quickTabsAction = new ActionHandler(ev -> { boolean visible = quickTabsTree == null; setQuickTabsVisibility(visible); settings.saveDockQuickTabs(visible); }); quickTabsAction.setNameAndDesc(NLS.str("menu.dock_quick_tabs")); quickTabsAction.setIcon(Icons.QUICK_TABS); quickTabsAction.setSelected(settings.isDockQuickTabs()); setQuickTabsVisibility(settings.isDockQuickTabs()); JadxGuiAction syncAction = new JadxGuiAction(ActionModel.SYNC, this.editorSyncManager::sync); JadxGuiAction textSearchAction = new JadxGuiAction(ActionModel.TEXT_SEARCH, this::textSearch); JadxGuiAction clsSearchAction = new JadxGuiAction(ActionModel.CLASS_SEARCH, () -> SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.CLASS)); JadxGuiAction commentSearchAction = new JadxGuiAction(ActionModel.COMMENT_SEARCH, () -> SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.COMMENT)); JadxGuiAction goToMainActivityAction = new JadxGuiAction(ActionModel.GO_TO_MAIN_ACTIVITY, this::goToMainActivity); JadxGuiAction goToApplicationAction = new JadxGuiAction(ActionModel.GO_TO_APPLICATION, this::goToApplication); JadxGuiAction goToAndroidManifestAction = new JadxGuiAction(ActionModel.GO_TO_ANDROID_MANIFEST, this::goToAndroidManifest); JadxGuiAction decompileAllAction = new JadxGuiAction(ActionModel.DECOMPILE_ALL, this::requestFullDecompilation); JadxGuiAction resetCacheAction = new JadxGuiAction(ActionModel.RESET_CACHE, this::resetCodeCache); JadxGuiAction deobfAction = new JadxGuiAction(ActionModel.DEOBF, this::toggleDeobfuscation); deobfToggleBtn = new JToggleButton(deobfAction); deobfToggleBtn.setSelected(settings.isDeobfuscationOn()); deobfToggleBtn.setText(""); deobfMenuItem = new JCheckBoxMenuItem(deobfAction); deobfMenuItem.setState(settings.isDeobfuscationOn()); JadxGuiAction showLogAction = new JadxGuiAction(ActionModel.SHOW_LOG, () -> showLogViewer(LogOptions.current())); JadxGuiAction aboutAction = new JadxGuiAction(ActionModel.ABOUT, () -> new AboutDialog().setVisible(true)); JadxGuiAction backAction = new JadxGuiAction(ActionModel.BACK, navController::navBack); JadxGuiAction backVariantAction = new JadxGuiAction(ActionModel.BACK_V, navController::navBack); JadxGuiAction forwardAction = new JadxGuiAction(ActionModel.FORWARD, navController::navForward); JadxGuiAction forwardVariantAction = new JadxGuiAction(ActionModel.FORWARD_V, navController::navForward); JadxGuiAction quarkAction = new JadxGuiAction(ActionModel.QUARK, () -> new QuarkDialog(MainWindow.this).setVisible(true)); JadxGuiAction debuggerAction = new JadxGuiAction(ActionModel.OPEN_DEVICE, () -> new ADBDialog(MainWindow.this).setVisible(true)); JMenu file = new JadxMenu(NLS.str("menu.file"), shortcutsController); file.setMnemonic(KeyEvent.VK_F); file.add(openAction); file.add(openProject); file.add(addFilesAction); file.addSeparator(); file.add(newProjectAction); file.add(saveProjectAction); file.add(saveProjectAsAction); file.addSeparator(); file.add(reloadAction); file.add(liveReloadMenuItem); renameMappings.addMenuActions(file); file.addSeparator(); file.add(exportAction); file.addSeparator(); file.add(recentProjects); file.addSeparator(); file.add(prefsAction); file.addSeparator(); file.add(exitAction); JMenu view = new JadxMenu(NLS.str("menu.view"), shortcutsController); view.setMnemonic(KeyEvent.VK_V); view.add(quickTabsAction.makeCheckBoxMenuItem()); view.add(hexViewerMenu); view.add(flatPkgMenuItem); view.addSeparator(); view.add(enablePreviewTabAction.makeCheckBoxMenuItem()); view.add(syncAction); view.add(alwaysSelectOpened); view.addSeparator(); view.add(dockLog); view.add(heapUsageBarMenuItem); JMenu nav = new JadxMenu(NLS.str("menu.navigation"), shortcutsController); nav.setMnemonic(KeyEvent.VK_N); nav.add(textSearchAction); nav.add(clsSearchAction); nav.add(commentSearchAction); nav.add(goToMainActivityAction); nav.add(goToApplicationAction); nav.add(goToAndroidManifestAction); nav.addSeparator(); nav.add(backAction); nav.add(forwardAction); pluginsMenu = new JadxMenu(NLS.str("menu.plugins"), shortcutsController); pluginsMenu.setMnemonic(KeyEvent.VK_P); resetPluginsMenu(); JMenu tools = new JadxMenu(NLS.str("menu.tools"), shortcutsController); tools.setMnemonic(KeyEvent.VK_T); tools.add(decompileAllAction); tools.add(resetCacheAction); tools.add(deobfMenuItem); tools.add(quarkAction); tools.add(debuggerAction); JMenu help = new JadxMenu(NLS.str("menu.help"), shortcutsController); help.setMnemonic(KeyEvent.VK_H); help.add(showLogAction); if (JadxSystemInfo.IS_LINUX) { help.add(new JadxGuiAction(ActionModel.CREATE_DESKTOP_ENTRY, this::createDesktopEntry)); } if (Jadx.isDevVersion()) { help.add(new AbstractAction("Show sample error report") { @Override public void actionPerformed(ActionEvent e) { ExceptionDialog.throwTestException(); } }); } if (UiUtils.JADX_GUI_DEBUG) { JCheckBoxMenuItem uiWatchDog = new JCheckBoxMenuItem(new ActionHandler("UI WatchDog", UIWatchDog::toggle)); uiWatchDog.setState(UIWatchDog.onStart()); help.add(uiWatchDog); } if (JadxSystemInfo.IS_MAC) { System.setProperty("apple.laf.useScreenMenuBar", "true"); Desktop.getDesktop().setAboutHandler(e -> aboutAction.actionPerformed(null)); } else { help.add(aboutAction); } menuBar = new JadxMenuBar(); menuBar.add(file); menuBar.add(view); menuBar.add(nav); menuBar.add(tools); menuBar.add(pluginsMenu); menuBar.add(help); setJMenuBar(menuBar); flatPkgButton = new JToggleButton(Icons.FLAT_PKG); flatPkgButton.setSelected(isFlattenPackage); ActionListener flatPkgAction = e -> toggleFlattenPackage(); flatPkgMenuItem.addActionListener(flatPkgAction); flatPkgButton.addActionListener(flatPkgAction); flatPkgButton.setToolTipText(NLS.str("menu.flatten")); updateLink = new Link(); updateLink.setVisible(false); JToolBar toolbar = new JToolBar(); toolbar.setFloatable(false); toolbar.add(openAction); toolbar.add(addFilesAction); toolbar.addSeparator(); toolbar.add(reloadAction); toolbar.addSeparator(); toolbar.add(exportAction); toolbar.addSeparator(); toolbar.add(syncAction); toolbar.add(flatPkgButton); toolbar.add(enablePreviewTabAction.makeToggleButton()); toolbar.add(quickTabsAction.makeToggleButton()); toolbar.addSeparator(); toolbar.add(textSearchAction); toolbar.add(clsSearchAction); toolbar.add(commentSearchAction); toolbar.add(goToMainActivityAction); toolbar.add(goToApplicationAction); toolbar.add(goToAndroidManifestAction); toolbar.addSeparator(); toolbar.add(backAction); toolbar.add(forwardAction); toolbar.addSeparator(); toolbar.add(deobfToggleBtn); toolbar.add(quarkAction); toolbar.add(debuggerAction); toolbar.addSeparator(); toolbar.add(showLogAction); toolbar.addSeparator(); toolbar.add(prefsAction); toolbar.addSeparator(); toolbar.add(Box.createHorizontalGlue()); toolbar.add(updateLink); mainPanel.add(toolbar, BorderLayout.NORTH); nav.add(new HiddenMenuItem(backVariantAction)); nav.add(new HiddenMenuItem(forwardVariantAction)); shortcutsController.bind(backVariantAction); shortcutsController.bind(forwardVariantAction); addLoadListener(loaded -> { textSearchAction.setEnabled(loaded); clsSearchAction.setEnabled(loaded); commentSearchAction.setEnabled(loaded); goToMainActivityAction.setEnabled(loaded); goToApplicationAction.setEnabled(loaded); goToAndroidManifestAction.setEnabled(loaded); backAction.setEnabled(loaded); backVariantAction.setEnabled(loaded); forwardAction.setEnabled(loaded); forwardVariantAction.setEnabled(loaded); syncAction.setEnabled(loaded); exportAction.setEnabled(loaded); saveProjectAsAction.setEnabled(loaded); reloadAction.setEnabled(loaded); decompileAllAction.setEnabled(loaded); deobfAction.setEnabled(loaded); quarkAction.setEnabled(loaded); debuggerAction.setEnabled(loaded); resetCacheAction.setEnabled(loaded); return false; }); } private void initUI() { setMinimumSize(new Dimension(200, 150)); mainPanel = new JPanel(new BorderLayout()); treeSplitPane = new JSplitPane(); treeSplitPane.setResizeWeight(SPLIT_PANE_RESIZE_WEIGHT); mainPanel.add(treeSplitPane); DefaultMutableTreeNode treeRootNode = new DefaultMutableTreeNode(NLS.str("msg.open_file")); treeModel = new DefaultTreeModel(treeRootNode); tree = new JTree(treeModel); ToolTipManager.sharedInstance().registerComponent(tree); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.setFocusable(false); tree.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { tree.setFocusable(false); } }); tree.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e)) { if (!nodeClickAction(getJNodeUnderMouse(e))) { // click ignored -> switch to focusable mode tree.setFocusable(true); tree.requestFocus(); } } else if (SwingUtilities.isRightMouseButton(e)) { treeRightClickAction(e); } } }); tree.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) { nodeClickAction(tree.getLastSelectedPathComponent()); } } }); tree.setCellRenderer(new DefaultTreeCellRenderer() { @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean isLeaf, int row, boolean focused) { Component c = super.getTreeCellRendererComponent(tree, value, selected, expanded, isLeaf, row, focused); if (value instanceof JNode) { JNode jNode = (JNode) value; NodeLabel.disableHtml(this, jNode.disableHtml()); setText(jNode.makeStringHtml()); setIcon(jNode.getIcon()); setToolTipText(jNode.getTooltip()); } else { setToolTipText(null); } if (value instanceof JPackage) { setEnabled(((JPackage) value).isEnabled()); } return c; } }); tree.addTreeWillExpandListener(new TreeWillExpandListener() { @Override public void treeWillExpand(TreeExpansionEvent event) { TreePath path = event.getPath(); Object node = path.getLastPathComponent(); if (node instanceof JLoadableNode) { JLoadableNode treeNode = (JLoadableNode) node; IBackgroundTask loadTask = treeNode.getLoadTask(); if (loadTask != null) { backgroundExecutor.execute(new TaskWithExtraOnFinish(loadTask, status -> { if (!treeReloading) { treeModel.nodeStructureChanged(treeNode); } })); } } } @Override public void treeWillCollapse(TreeExpansionEvent event) { if (!treeReloading) { update(); } } }); progressPane = new ProgressPanel(this, true); issuesPanel = new IssuesPanel(this); JPanel leftPane = new JPanel(new BorderLayout()); JScrollPane treeScrollPane = new JScrollPane(tree); treeScrollPane.setMinimumSize(new Dimension(100, 150)); JPanel bottomPane = new JPanel(new BorderLayout()); bottomPane.add(issuesPanel, BorderLayout.PAGE_START); bottomPane.add(progressPane, BorderLayout.PAGE_END); leftPane.add(treeScrollPane, BorderLayout.CENTER); leftPane.add(bottomPane, BorderLayout.PAGE_END); treeSplitPane.setLeftComponent(leftPane); tabbedPane = new TabbedPane(this, tabsController); tabbedPane.setMinimumSize(new Dimension(150, 150)); new TabDndController(tabbedPane, settings); quickTabsAndCodeSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); quickTabsAndCodeSplitPane.setResizeWeight(0.15); quickTabsAndCodeSplitPane.setDividerSize(0); quickTabsAndCodeSplitPane.setRightComponent(tabbedPane); rightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); rightSplitPane.setTopComponent(quickTabsAndCodeSplitPane); rightSplitPane.setResizeWeight(SPLIT_PANE_RESIZE_WEIGHT); treeSplitPane.setRightComponent(rightSplitPane); new DropTarget(this, DnDConstants.ACTION_COPY, new MainDropTarget(this)); heapUsageBar = new HeapUsageBar(); mainPanel.add(heapUsageBar, BorderLayout.SOUTH); bottomSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); bottomSplitPane.setTopComponent(treeSplitPane); bottomSplitPane.setResizeWeight(SPLIT_PANE_RESIZE_WEIGHT); mainPanel.add(bottomSplitPane, BorderLayout.CENTER); setContentPane(mainPanel); setTitle(DEFAULT_TITLE); if (UiUtils.JADX_GUI_DEBUG) { FlatInspector.install("ctrl shift alt X"); FlatUIDefaultsInspector.install("ctrl shift alt Y"); } setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { closeWindow(); } }); } public void setLocationAndPosition() { if (settings.loadWindowPos(this)) { return; } GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); DisplayMode mode = gd.getDisplayMode(); AffineTransform trans = gd.getDefaultConfiguration().getDefaultTransform(); int w = (int) (mode.getWidth() / trans.getScaleX()); int h = (int) (mode.getHeight() / trans.getScaleY()); setBounds((int) (w * BORDER_RATIO), (int) (h * BORDER_RATIO), (int) (w * WINDOW_RATIO), (int) (h * WINDOW_RATIO)); setLocationRelativeTo(null); } private void openSettings() { openSettings(null); } private void openSettings(@Nullable String navigateTo) { settingsOpen = true; JadxSettingsWindow settingsWindow = new JadxSettingsWindow(MainWindow.this, settings); settingsWindow.addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { settingsOpen = false; } }); if (navigateTo != null) { settingsWindow.activatePage(navigateTo); } settingsWindow.setVisible(true); } public boolean isSettingsOpen() { return settingsOpen; } public void loadSettings() { // queue update to not interrupt current UI tasks UiUtils.uiRun(this::updateUiSettings); } private void updateUiSettings() { boolean needUpdateUI = false; Font defaultUiFont = UIManager.getFont("defaultFont"); Font uiFont = settings.getUiFont(); if (!uiFont.equals(defaultUiFont)) { UIManager.put("defaultFont", uiFont); setFont(uiFont); needUpdateUI = true; } if (LafManager.updateLaf(settings)) { needUpdateUI = true; } editorThemeManager.setTheme(settings.getEditorTheme()); if (UIScale.setZoomFactor(settings.getUiZoom())) { needUpdateUI = true; } tree.setFont(settings.getCodeFont()); tree.setRowHeight(-1); tabbedPane.loadSettings(); if (logPanel != null) { logPanel.loadSettings(); } if (quickTabsTree != null) { quickTabsTree.loadSettings(); } shortcutsController.loadSettings(); if (needUpdateUI) { FlatLaf.updateUI(); } } @SuppressWarnings("finally") private void closeWindow() { saveAll(); if (!ensureProjectIsSaved()) { return; } UiUtils.bgRun(() -> { try { settings.setTreeWidth(treeSplitPane.getDividerLocation()); settings.saveWindowPos(this); settings.setMainWindowExtendedState(getExtendedState()); if (debuggerPanel != null) { saveSplittersInfo(); } // block UI thread to avoid settings data changes during sync UiUtils.uiRunAndWait(settings::sync); closeAll(); UiUtils.uiRunAndWait(() -> { heapUsageBar.reset(); editorThemeManager.unload(); dispose(); }); } catch (Exception e) { LOG.error("Close window error", e); } finally { System.exit(0); } }); } private void saveOpenTabs() { project.saveOpenTabs(tabsController.getEditorViewStates()); } private void restoreOpenTabs(List openTabs) { UiUtils.uiThreadGuard(); if (openTabs.isEmpty()) { return; } for (EditorViewState viewState : openTabs) { tabsController.restoreEditorViewState(viewState); } tabsController.notifyRestoreEditorViewStateDone(); } private void preLoadOpenTabs(List openTabs) { UiUtils.notUiThreadGuard(); for (EditorViewState tabState : openTabs) { if (tabState.isHidden()) { continue; } JNode node = tabState.getNode(); try { node.getCodeInfo(); } catch (Exception e) { LOG.warn("Failed to preload code for node: {}", node, e); } } } private void saveSplittersInfo() { settings.setMainWindowVerticalSplitterLoc(bottomSplitPane.getDividerLocation()); if (debuggerPanel != null) { settings.setDebuggerStackFrameSplitterLoc(debuggerPanel.getLeftSplitterLocation()); settings.setDebuggerVarTreeSplitterLoc(debuggerPanel.getRightSplitterLocation()); } } public void addLoadListener(ILoadListener loadListener) { this.loadListeners.add(loadListener); // set initial value loadListener.update(loaded); } public void notifyLoadListeners(boolean loaded) { this.loaded = loaded; loadListeners.removeIf(listener -> listener.update(loaded)); } public void addTreeUpdateListener(Consumer listener) { treeUpdateListener.add(listener); } public JadxWrapper getWrapper() { return wrapper; } public JadxProject getProject() { return project; } public TabbedPane getTabbedPane() { return tabbedPane; } public TabsController getTabsController() { return tabsController; } public NavigationController getNavController() { return navController; } public JadxSettings getSettings() { return settings; } public CacheObject getCacheObject() { return cacheObject; } public BackgroundExecutor getBackgroundExecutor() { return backgroundExecutor; } public JRoot getTreeRoot() { return treeRoot; } public JDebuggerPanel getDebuggerPanel() { initDebuggerPanel(); return debuggerPanel; } public ShortcutsController getShortcutsController() { return shortcutsController; } public void showDebuggerPanel() { initDebuggerPanel(); } public void destroyDebuggerPanel() { saveSplittersInfo(); if (debuggerPanel != null) { debuggerPanel.setVisible(false); debuggerPanel = null; } } public void showHeapUsageBar() { settings.setShowHeapUsageBar(true); heapUsageBar.setVisible(true); } private void initDebuggerPanel() { if (debuggerPanel == null) { debuggerPanel = new JDebuggerPanel(this); debuggerPanel.loadSettings(); bottomSplitPane.setBottomComponent(debuggerPanel); int loc = settings.getMainWindowVerticalSplitterLoc(); if (loc == 0) { loc = 300; } bottomSplitPane.setDividerLocation(loc); } } public void showLogViewer(LogOptions logOptions) { UiUtils.uiRun(() -> { if (settings.isDockLogViewer()) { showDockedLog(logOptions); } else { LogViewerDialog.open(this, logOptions); } }); } private void showDockedLog(LogOptions logOptions) { if (logPanel != null) { logPanel.applyLogOptions(logOptions); return; } Runnable undock = () -> { hideDockedLog(); settings.saveDockLogViewer(false); LogViewerDialog.open(this, logOptions); }; logPanel = new LogPanel(this, logOptions, undock, this::hideDockedLog); rightSplitPane.setBottomComponent(logPanel); } private void hideDockedLog() { if (logPanel == null) { return; } logPanel.dispose(); logPanel = null; rightSplitPane.setBottomComponent(null); } private void setQuickTabsVisibility(boolean visible) { if (visible) { if (quickTabsTree == null) { quickTabsTree = new QuickTabsTree(this); } quickTabsAndCodeSplitPane.setLeftComponent(quickTabsTree); quickTabsAndCodeSplitPane.setDividerSize(5); } else { quickTabsAndCodeSplitPane.setLeftComponent(null); quickTabsAndCodeSplitPane.setDividerSize(0); if (quickTabsTree != null) { quickTabsTree.dispose(); quickTabsTree = null; } } } public JMenu getPluginsMenu() { return pluginsMenu; } public void resetPluginsMenu() { pluginsMenu.removeAll(); pluginsMenu.add(new ActionHandler(() -> openSettings("PluginSettingsGroup.class")) .withNameAndDesc(NLS.str("preferences.plugins.manage"))); } public void addToPluginsMenu(Action item) { if (pluginsMenu.getMenuComponentCount() == 1) { pluginsMenu.addSeparator(); } pluginsMenu.add(item); } private void createDesktopEntry() { if (DesktopEntryUtils.createDesktopEntry()) { JOptionPane.showMessageDialog(this, NLS.str("message.desktop_entry_creation_success"), NLS.str("message.success_title"), JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(this, NLS.str("message.desktop_entry_creation_error"), NLS.str("message.errorTitle"), JOptionPane.ERROR_MESSAGE); } } private void checkIfCodeHasNonPrintableChars() { if (getSettings().isRenamePrintable() || getSettings().isDeobfuscationOn()) { return; } if (showUndisplayedCharsDialog) { return; } StringBuilder nonDisplayString = new StringBuilder(); List classes = wrapper.getRootNode().getClasses(true); Font font = getSettings().getCodeFont(); boolean hasNonDisplayable = false; for (ClassNode cls : classes) { String className = cls.getRawName(); if (!FontUtils.canStringBeDisplayed(className, font)) { hasNonDisplayable = true; nonDisplayString.append(className); nonDisplayString.append("\n"); } for (MethodNode methodNode : cls.getMethods()) { String methodName = methodNode.getName(); if (!FontUtils.canStringBeDisplayed(methodName, font)) { hasNonDisplayable = true; nonDisplayString.append(methodName); nonDisplayString.append("\n"); } } for (FieldNode fieldNode : cls.getFields()) { String fieldName = fieldNode.getName(); if (!FontUtils.canStringBeDisplayed(fieldName, font)) { hasNonDisplayable = true; nonDisplayString.append(fieldName); nonDisplayString.append("\n"); } } } if (hasNonDisplayable) { showUndisplayedCharsDialog = true; int dialogResult = JOptionPane.showConfirmDialog(this, NLS.str("msg.non_displayable_chars", font.getFontName()), NLS.str("msg.warning_title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if (dialogResult == JOptionPane.YES_OPTION) { tabsController.selectTab(new UndisplayedStringsNode(nonDisplayString.toString())); } } } public RenameMappingsGui getRenameMappings() { return renameMappings; } public CacheManager getCacheManager() { return cacheManager; } public EditorThemeManager getEditorThemeManager() { return editorThemeManager; } public JadxGuiEventsImpl events() { return events; } private void initHexViewMenu() { hexViewerMenu.setEnabled(false); JadxGuiAction showInspectorAction = new JadxGuiAction(ActionModel.HEX_VIEWER_SHOW_INSPECTOR, () -> sendActionsToHexViewer(ActionModel.HEX_VIEWER_SHOW_INSPECTOR)); JCheckBoxMenuItem showInspectorMenuItem = new JCheckBoxMenuItem(showInspectorAction); JadxGuiAction changeEncoding = new JadxGuiAction(ActionModel.HEX_VIEWER_CHANGE_ENCODING, () -> sendActionsToHexViewer(ActionModel.HEX_VIEWER_CHANGE_ENCODING)); JadxGuiAction goToAddress = new JadxGuiAction(ActionModel.HEX_VIEWER_GO_TO_ADDRESS, () -> sendActionsToHexViewer(ActionModel.HEX_VIEWER_GO_TO_ADDRESS)); JadxGuiAction findAction = new JadxGuiAction(ActionModel.HEX_VIEWER_FIND, () -> sendActionsToHexViewer(ActionModel.HEX_VIEWER_FIND)); hexViewerMenu.add(showInspectorMenuItem); hexViewerMenu.add(changeEncoding); hexViewerMenu.add(goToAddress); hexViewerMenu.addSeparator(); hexViewerMenu.add(findAction); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/ActionCategory.java ================================================ package jadx.gui.ui.action; import jadx.gui.utils.NLS; public enum ActionCategory { MENU_TOOLBAR("action_category.menu_toolbar"), CODE_AREA("action_category.code_area"), PLUGIN_SCRIPT("action_category.plugin_script"), HEX_VIEWER_MENU("action_category.hex_viewer"); private final String nameRes; ActionCategory(String nameRes) { this.nameRes = nameRes; } public String getName() { if (nameRes != null) { return NLS.str(nameRes); } return null; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java ================================================ package jadx.gui.ui.action; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import javax.swing.ImageIcon; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.shortcut.Shortcut; import static jadx.gui.ui.action.ActionCategory.*; public enum ActionModel { ABOUT(MENU_TOOLBAR, "menu.about", "menu.about", "ui/showInfos", Shortcut.keyboard(KeyEvent.VK_F1)), OPEN(MENU_TOOLBAR, "file.open_action", "file.open_action", "ui/openDisk", Shortcut.keyboard(KeyEvent.VK_O, KeyEvent.CTRL_DOWN_MASK)), OPEN_PROJECT(MENU_TOOLBAR, "file.open_project", "file.open_project", "ui/projectDirectory", Shortcut.keyboard(KeyEvent.VK_O, InputEvent.SHIFT_DOWN_MASK | UiUtils.ctrlButton())), ADD_FILES(MENU_TOOLBAR, "file.add_files_action", "file.add_files_action", "ui/addFile", Shortcut.none()), NEW_PROJECT(MENU_TOOLBAR, "file.new_project", "file.new_project", "ui/newFolder", Shortcut.none()), SAVE_PROJECT(MENU_TOOLBAR, "file.save_project", "file.save_project", null, Shortcut.none()), SAVE_PROJECT_AS(MENU_TOOLBAR, "file.save_project_as", "file.save_project_as", null, Shortcut.none()), RELOAD(MENU_TOOLBAR, "file.reload", "file.reload", "ui/refresh", Shortcut.keyboard(KeyEvent.VK_F5)), LIVE_RELOAD(MENU_TOOLBAR, "file.live_reload", "file.live_reload_desc", null, Shortcut.keyboard(KeyEvent.VK_F5, InputEvent.SHIFT_DOWN_MASK)), SAVE_ALL(MENU_TOOLBAR, "file.save_all", "file.save_all", "ui/menu-saveall", Shortcut.keyboard(KeyEvent.VK_E, UiUtils.ctrlButton())), EXPORT(MENU_TOOLBAR, "file.export", "file.export", "ui/export", Shortcut.keyboard(KeyEvent.VK_E, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), PREFS(MENU_TOOLBAR, "menu.preferences", "menu.preferences", "ui/settings", Shortcut.keyboard(KeyEvent.VK_P, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), EXIT(MENU_TOOLBAR, "file.exit", "file.exit", "ui/exit", Shortcut.none()), SYNC(MENU_TOOLBAR, "menu.sync", "menu.sync", "ui/locate", Shortcut.keyboard(KeyEvent.VK_T, UiUtils.ctrlButton())), TEXT_SEARCH(MENU_TOOLBAR, "menu.text_search", "menu.text_search", "ui/find", Shortcut.keyboard(KeyEvent.VK_F, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), CLASS_SEARCH(MENU_TOOLBAR, "menu.class_search", "menu.class_search", "ui/ejbFinderMethod", Shortcut.keyboard(KeyEvent.VK_N, UiUtils.ctrlButton())), COMMENT_SEARCH(MENU_TOOLBAR, "menu.comment_search", "menu.comment_search", "ui/usagesFinder", Shortcut.keyboard(KeyEvent.VK_SEMICOLON, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), GO_TO_MAIN_ACTIVITY(MENU_TOOLBAR, "menu.go_to_main_activity", "menu.go_to_main_activity", "ui/home", Shortcut.keyboard(KeyEvent.VK_M, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), GO_TO_APPLICATION(MENU_TOOLBAR, "menu.go_to_application", "menu.go_to_application", "ui/application", Shortcut.keyboard(KeyEvent.VK_A, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), GO_TO_ANDROID_MANIFEST(MENU_TOOLBAR, "menu.go_to_android_manifest", "menu.go_to_android_manifest", "ui/androidManifest", Shortcut.none()), PREVIEW_TAB(MENU_TOOLBAR, "menu.enable_preview_tab", "menu.enable_preview_tab", "ui/editorPreview", Shortcut.none()), DECOMPILE_ALL(MENU_TOOLBAR, "menu.decompile_all", "menu.decompile_all", "ui/runAll", Shortcut.none()), RESET_CACHE(MENU_TOOLBAR, "menu.reset_cache", "menu.reset_cache", "ui/reset", Shortcut.none()), DEOBF(MENU_TOOLBAR, "menu.deobfuscation", "preferences.deobfuscation", "ui/helmChartLock", Shortcut.keyboard(KeyEvent.VK_D, UiUtils.ctrlButton() | KeyEvent.ALT_DOWN_MASK)), SHOW_LOG(MENU_TOOLBAR, "menu.log", "menu.log", "ui/logVerbose", Shortcut.keyboard(KeyEvent.VK_L, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), CREATE_DESKTOP_ENTRY(MENU_TOOLBAR, "menu.create_desktop_entry", "menu.create_desktop_entry", null, Shortcut.none()), BACK(MENU_TOOLBAR, "nav.back", "nav.back", "ui/left", Shortcut.keyboard(KeyEvent.VK_ESCAPE)), BACK_V(MENU_TOOLBAR, "nav.back", "nav.back", "ui/left", Shortcut.none()), FORWARD(MENU_TOOLBAR, "nav.forward", "nav.forward", "ui/right", Shortcut.keyboard(KeyEvent.VK_RIGHT, KeyEvent.ALT_DOWN_MASK)), FORWARD_V(MENU_TOOLBAR, "nav.forward", "nav.forward", "ui/right", Shortcut.none()), QUARK(MENU_TOOLBAR, "menu.quark", "menu.quark", "ui/quark", Shortcut.none()), OPEN_DEVICE(MENU_TOOLBAR, "debugger.process_selector", "debugger.process_selector", "ui/startDebugger", Shortcut.none()), FIND_USAGE(CODE_AREA, "popup.find_usage", "popup.find_usage", null, Shortcut.keyboard(KeyEvent.VK_X)), FIND_USAGE_PLUS(CODE_AREA, "popup.usage_dialog_plus", "popup.usage_dialog_plus", null, Shortcut.keyboard(KeyEvent.VK_C)), GOTO_DECLARATION(CODE_AREA, "popup.go_to_declaration", "popup.go_to_declaration", null, Shortcut.keyboard(KeyEvent.VK_D)), CONVERT_NUMBER(CODE_AREA, "popup.convert_number", "popup.convert_number", null, Shortcut.none()), VIEW_CLASS_INHERITANCE_GRAPH(CODE_AREA, "popup.view_class_graph", "popup.view_class_graph_description", null, Shortcut.none()), VIEW_CLASS_METHOD_GRAPH(CODE_AREA, "popup.view_class_method_graph", "popup.view_class_method_graph_description", null, Shortcut.none()), VIEW_CALL_GRAPH(CODE_AREA, "popup.view_call_graph", "popup.view_call_graph_description", null, Shortcut.none()), VIEW_CONTROL_FLOW_GRAPH(CODE_AREA, "popup.view_cfg", "popup.view_cfg_description", null, Shortcut.none()), VIEW_RAW_CONTROL_FLOW_GRAPH(CODE_AREA, "popup.view_raw_cfg", "popup.view_raw_cfg_description", null, Shortcut.none()), VIEW_REGION_CONTROL_FLOW_GRAPH(CODE_AREA, "popup.view_region_cfg", "popup.view_region_cfg_description", null, Shortcut.none()), CODE_COMMENT(CODE_AREA, "popup.add_comment", "popup.add_comment", null, Shortcut.keyboard(KeyEvent.VK_SEMICOLON)), CODE_COMMENT_SEARCH(CODE_AREA, "popup.search_comment", "popup.search_comment", null, Shortcut.keyboard(KeyEvent.VK_SEMICOLON, UiUtils.ctrlButton())), CODE_RENAME(CODE_AREA, "popup.rename", "popup.rename", null, Shortcut.keyboard(KeyEvent.VK_N)), FRIDA_COPY(CODE_AREA, "popup.frida", "popup.frida", null, Shortcut.keyboard(KeyEvent.VK_F)), XPOSED_COPY(CODE_AREA, "popup.xposed", "popup.xposed", null, Shortcut.keyboard(KeyEvent.VK_Y)), JSON_PRETTIFY(CODE_AREA, "popup.json_prettify", "popup.json_prettify", null, Shortcut.none()), SCRIPT_RUN(PLUGIN_SCRIPT, "script.run", "script.run", "ui/run", Shortcut.keyboard(KeyEvent.VK_F8)), SCRIPT_SAVE(PLUGIN_SCRIPT, "script.save", "script.save", "ui/menu-saveall", Shortcut.keyboard(KeyEvent.VK_S, UiUtils.ctrlButton())), SCRIPT_AUTO_COMPLETE(PLUGIN_SCRIPT, "script.auto_complete", "script.auto_complete", null, Shortcut.keyboard(KeyEvent.VK_SPACE, UiUtils.ctrlButton())), HEX_VIEWER_SHOW_INSPECTOR(HEX_VIEWER_MENU, "hex_viewer.show_inspector", "hex_viewer.show_inspector", null, Shortcut.none()), HEX_VIEWER_CHANGE_ENCODING(HEX_VIEWER_MENU, "hex_viewer.change_encoding", "hex_viewer.change_encoding", null, Shortcut.none()), HEX_VIEWER_GO_TO_ADDRESS(HEX_VIEWER_MENU, "hex_viewer.goto_address", "hex_viewer.goto_address", null, Shortcut.keyboard(KeyEvent.VK_J, UiUtils.ctrlButton())), HEX_VIEWER_FIND(HEX_VIEWER_MENU, "hex_viewer.find", "hex_viewer.find", null, Shortcut.keyboard(KeyEvent.VK_F, UiUtils.ctrlButton())); private final ActionCategory category; private final String nameRes; private final String descRes; private final String iconPath; private final Shortcut defaultShortcut; ActionModel(ActionCategory category, String nameRes, String descRes, String iconPath, Shortcut defaultShortcut) { this.category = category; this.nameRes = nameRes; this.descRes = descRes; this.iconPath = iconPath; this.defaultShortcut = defaultShortcut; } public static List select(ActionCategory category) { return Arrays.stream(values()) .filter(actionModel -> actionModel.category == category) .collect(Collectors.toUnmodifiableList()); } public ActionCategory getCategory() { return category; } public String getName() { if (nameRes != null) { String name = NLS.str(nameRes); if (name().endsWith("_V")) { name = NLS.str("action.variant", name); } return name; } return null; } public String getDescription() { if (descRes != null) { return NLS.str(descRes); } return null; } public ImageIcon getIcon() { if (iconPath != null) { return UiUtils.openSvgIcon(iconPath); } return null; } public Shortcut getDefaultShortcut() { return defaultShortcut; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/CodeAreaAction.java ================================================ package jadx.gui.ui.action; import jadx.gui.ui.codearea.CodeArea; public class CodeAreaAction extends JadxGuiAction { protected transient CodeArea codeArea; public CodeAreaAction(ActionModel actionModel, CodeArea codeArea) { super(actionModel); this.codeArea = codeArea; setShortcutComponent(codeArea); } public CodeAreaAction(String id, CodeArea codeArea) { super(id); this.codeArea = codeArea; setShortcutComponent(codeArea); } public void dispose() { codeArea = null; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/CommentSearchAction.java ================================================ package jadx.gui.ui.action; import java.awt.event.ActionEvent; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.SearchDialog; public class CommentSearchAction extends CodeAreaAction { private static final long serialVersionUID = -3646341661734961590L; public CommentSearchAction(CodeArea codeArea) { super(ActionModel.CODE_COMMENT_SEARCH, codeArea); } @Override public void actionPerformed(ActionEvent e) { startSearch(); } private void startSearch() { SearchDialog.searchInActiveTab(codeArea.getMainWindow(), SearchDialog.SearchPreset.COMMENT); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/FindUsageAction.java ================================================ package jadx.gui.ui.action; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.UsageDialog; public final class FindUsageAction extends JNodeAction { private static final long serialVersionUID = 4692546569977976384L; public FindUsageAction(CodeArea codeArea) { super(ActionModel.FIND_USAGE, codeArea); } @Override public void runAction(JNode node) { UsageDialog.open(getCodeArea().getMainWindow(), node); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/FridaAction.java ================================================ package jadx.gui.ui.action; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.apache.commons.text.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JavaClass; import jadx.api.JavaField; import jadx.api.JavaMethod; import jadx.api.metadata.annotations.VarNode; import jadx.core.codegen.TypeGen; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JField; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.MethodsDialog; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public final class FridaAction extends JNodeAction { private static final Logger LOG = LoggerFactory.getLogger(FridaAction.class); private static final long serialVersionUID = -3084073927621269039L; public FridaAction(CodeArea codeArea) { super(ActionModel.FRIDA_COPY, codeArea); } @Override public void runAction(JNode node) { try { generateFridaSnippet(node); } catch (Exception e) { LOG.error("Failed to generate Frida code snippet", e); JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); } } @Override public boolean isActionEnabled(JNode node) { return node instanceof JMethod || node instanceof JClass || node instanceof JField; } private void generateFridaSnippet(JNode node) { String fridaSnippet; if (node instanceof JMethod) { fridaSnippet = generateMethodSnippet((JMethod) node); copySnipped(fridaSnippet); } else if (node instanceof JField) { fridaSnippet = generateFieldSnippet((JField) node); copySnipped(fridaSnippet); } else if (node instanceof JClass) { SwingUtilities.invokeLater(() -> showMethodSelectionDialog((JClass) node)); } else { throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); } } private void copySnipped(String fridaSnippet) { if (!StringUtils.isEmpty(fridaSnippet)) { LOG.info("Frida snippet:\n{}", fridaSnippet); UiUtils.copyToClipboard(fridaSnippet); } } private String generateMethodSnippet(JMethod jMth) { String classSnippet = generateClassSnippet(jMth.getJParent()); String methodSnippet = getMethodSnippet(jMth.getJavaMethod(), jMth.getJParent()); return String.format("%s\n%s", classSnippet, methodSnippet); } private String generateMethodSnippet(JavaMethod javaMethod, JClass jc) { return getMethodSnippet(javaMethod, jc); } private String getMethodSnippet(JavaMethod javaMethod, JClass jc) { MethodNode mth = javaMethod.getMethodNode(); MethodInfo methodInfo = mth.getMethodInfo(); String methodName; String newMethodName; if (methodInfo.isConstructor()) { methodName = "$init"; newMethodName = methodName; } else { methodName = StringEscapeUtils.escapeEcmaScript(methodInfo.getName()); newMethodName = StringEscapeUtils.escapeEcmaScript(methodInfo.getAlias()); } String overload; if (isOverloaded(mth)) { String overloadArgs = methodInfo.getArgumentsTypes().stream() .map(this::parseArgType).collect(Collectors.joining(", ")); overload = ".overload(" + overloadArgs + ")"; } else { overload = ""; } List argNames = mth.collectArgNodes().stream() .map(VarNode::getName).collect(Collectors.toList()); String args = String.join(", ", argNames); String logArgs; if (argNames.isEmpty()) { logArgs = ""; } else { logArgs = ": " + argNames.stream().map(arg -> arg + "=${" + arg + "}").collect(Collectors.joining(", ")); } String shortClassName = mth.getParentClass().getAlias(); if (methodInfo.isConstructor() || methodInfo.getReturnType() == ArgType.VOID) { // no return value return shortClassName + "[\"" + methodName + "\"]" + overload + ".implementation = function (" + args + ") {\n" + " console.log(`" + shortClassName + "." + newMethodName + " is called" + logArgs + "`);\n" + " this[\"" + methodName + "\"](" + args + ");\n" + "};"; } return shortClassName + "[\"" + methodName + "\"]" + overload + ".implementation = function (" + args + ") {\n" + " console.log(`" + shortClassName + "." + newMethodName + " is called" + logArgs + "`);\n" + " let result = this[\"" + methodName + "\"](" + args + ");\n" + " console.log(`" + shortClassName + "." + newMethodName + " result=${result}`);\n" + " return result;\n" + "};"; } private String generateClassSnippet(JClass jc) { JavaClass javaClass = jc.getCls(); String rawClassName = StringEscapeUtils.escapeEcmaScript(javaClass.getRawName()); String shortClassName = javaClass.getName(); return String.format("var %s = Java.use(\"%s\");", shortClassName, rawClassName); } private void showMethodSelectionDialog(JClass jc) { JavaClass javaClass = jc.getCls(); new MethodsDialog(getCodeArea().getMainWindow(), javaClass.getMethods(), (result) -> { String fridaSnippet = generateClassAllMethodSnippet(jc, result); copySnipped(fridaSnippet); }); } private String generateClassAllMethodSnippet(JClass jc, List methodList) { StringBuilder result = new StringBuilder(); String classSnippet = generateClassSnippet(jc); result.append(classSnippet).append("\n"); for (JavaMethod javaMethod : methodList) { result.append(generateMethodSnippet(javaMethod, jc)).append("\n"); } return result.toString(); } private String generateFieldSnippet(JField jf) { JavaField javaField = jf.getJavaField(); String rawFieldName = StringEscapeUtils.escapeEcmaScript(javaField.getRawName()); String fieldName = javaField.getName(); List methodNodes = javaField.getFieldNode().getParentClass().getMethods(); for (MethodNode methodNode : methodNodes) { if (methodNode.getName().equals(rawFieldName)) { rawFieldName = "_" + rawFieldName; break; } } JClass jc = jf.getRootClass(); String classSnippet = generateClassSnippet(jc); return String.format("%s\n%s = %s.%s.value;", classSnippet, fieldName, jc.getName(), rawFieldName); } public Boolean isOverloaded(MethodNode methodNode) { return methodNode.getParentClass().getMethods().stream() .anyMatch(m -> m.getName().equals(methodNode.getName()) && !Objects.equals(methodNode.getMethodInfo().getShortId(), m.getMethodInfo().getShortId())); } private String parseArgType(ArgType x) { String typeStr; if (x.isArray()) { typeStr = TypeGen.signature(x).replace("/", "."); } else { typeStr = x.toString(); } return "'" + typeStr + "'"; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/GoToDeclarationAction.java ================================================ package jadx.gui.ui.action; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; public final class GoToDeclarationAction extends JNodeAction { private static final long serialVersionUID = -1186470538894941301L; public GoToDeclarationAction(CodeArea codeArea) { super(ActionModel.GOTO_DECLARATION, codeArea); } @Override public void runAction(JNode node) { getCodeArea().getContentPanel().getTabsController().codeJump(node); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/IShortcutAction.java ================================================ package jadx.gui.ui.action; import javax.swing.JComponent; import jadx.gui.utils.shortcut.Shortcut; public interface IShortcutAction { ActionModel getActionModel(); JComponent getShortcutComponent(); void performAction(); void setShortcut(Shortcut shortcut); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/JNodeAction.java ================================================ package jadx.gui.ui.action; import java.awt.event.ActionEvent; import java.beans.PropertyChangeListener; import org.jetbrains.annotations.Nullable; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; /** * Add menu and key binding actions for JNode in code area */ public abstract class JNodeAction extends CodeAreaAction { private static final long serialVersionUID = -2600154727884853550L; private transient @Nullable JNode node; public JNodeAction(ActionModel actionModel, CodeArea codeArea) { super(actionModel, codeArea); } public JNodeAction(String id, CodeArea codeArea) { super(id, codeArea); } public abstract void runAction(JNode node); public boolean isActionEnabled(@Nullable JNode node) { return node != null; } @Override public void actionPerformed(ActionEvent e) { if (JadxGuiAction.isSource(e)) { node = codeArea.getNodeUnderCaret(); if (isActionEnabled(node)) { runAction(node); } } else { runAction(node); } } public void changeNode(@Nullable JNode node) { this.node = node; setEnabled(isActionEnabled(node)); } public CodeArea getCodeArea() { return codeArea; } @Override public void dispose() { super.dispose(); node = null; for (PropertyChangeListener changeListener : getPropertyChangeListeners()) { removePropertyChangeListener(changeListener); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/JadxAutoCompletion.java ================================================ package jadx.gui.ui.action; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import javax.swing.JComponent; import javax.swing.KeyStroke; import org.fife.ui.autocomplete.AutoCompletion; import org.fife.ui.autocomplete.CompletionProvider; import jadx.gui.utils.shortcut.Shortcut; public class JadxAutoCompletion extends AutoCompletion implements IShortcutAction { public static final String COMMAND = "JadxAutoCompletion.Command"; /** * Constructor. * * @param provider The completion provider. This cannot be null */ public JadxAutoCompletion(CompletionProvider provider) { super(provider); } @Override public ActionModel getActionModel() { return ActionModel.SCRIPT_AUTO_COMPLETE; } @Override public JComponent getShortcutComponent() { return getTextComponent(); } @Override public void performAction() { createAutoCompleteAction().actionPerformed( new ActionEvent(this, ActionEvent.ACTION_PERFORMED, COMMAND)); } @Override public void setShortcut(Shortcut shortcut) { if (shortcut != null && shortcut.isKeyboard()) { setTriggerKey(shortcut.toKeyStroke()); } else { setTriggerKey(KeyStroke.getKeyStroke(KeyEvent.VK_UNDEFINED, 0)); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/JadxGuiAction.java ================================================ package jadx.gui.ui.action; import java.awt.event.ActionEvent; import java.util.function.Consumer; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.KeyStroke; import org.jetbrains.annotations.Nullable; import jadx.gui.ui.menu.JadxMenu; import jadx.gui.utils.UiUtils; import jadx.gui.utils.shortcut.Shortcut; import jadx.gui.utils.ui.ActionHandler; public class JadxGuiAction extends ActionHandler implements IShortcutAction { private static final String COMMAND_PREFIX = "JadxGuiAction.Command."; private final ActionModel actionModel; private final String id; private JComponent shortcutComponent = null; private KeyStroke addedKeyStroke = null; private Shortcut shortcut; public JadxGuiAction(ActionModel actionModel) { this.actionModel = actionModel; this.id = actionModel.name(); updateProperties(); } public JadxGuiAction(ActionModel actionModel, Runnable action) { super(action); this.actionModel = actionModel; this.id = actionModel.name(); updateProperties(); } public JadxGuiAction(ActionModel actionModel, Consumer consumer) { super(consumer); this.actionModel = actionModel; this.id = actionModel.name(); updateProperties(); } public JadxGuiAction(String id) { this.actionModel = null; this.id = id; updateProperties(); } private void updateProperties() { if (actionModel == null) { return; } String name = actionModel.getName(); String description = actionModel.getDescription(); ImageIcon icon = actionModel.getIcon(); if (name != null) { setName(name); } if (description != null) { setShortDescription(description); } if (icon != null) { setIcon(icon); } } @Nullable public ActionModel getActionModel() { return actionModel; } @Override public void setShortcut(Shortcut shortcut) { this.shortcut = shortcut; if (shortcut != null) { setKeyBinding(shortcut.toKeyStroke()); } else { setKeyBinding(null); } } public void setShortcutComponent(JComponent component) { this.shortcutComponent = component; } @Override public JComponent getShortcutComponent() { return shortcutComponent; } @Override public void actionPerformed(ActionEvent e) { super.actionPerformed(e); } @Override public void performAction() { if (shortcutComponent != null) { if (shortcutComponent == JadxMenu.JADX_MENU_COMPONENT) { // always enabled } else if (!shortcutComponent.isShowing()) { return; } } String shortcutType = shortcut != null ? shortcut.getTypeString() : "null"; actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, COMMAND_PREFIX + shortcutType)); } public static boolean isSource(ActionEvent event) { String command = event.getActionCommand(); return command != null && command.startsWith(COMMAND_PREFIX); } @Override public void setKeyBinding(KeyStroke keyStroke) { if (shortcutComponent == null) { super.setKeyBinding(keyStroke); } else { // We just set the keyStroke for it to appear in the menu item // (grayed out in the right) super.setKeyBinding(keyStroke); if (addedKeyStroke != null) { UiUtils.removeKeyBinding(shortcutComponent, addedKeyStroke, id); } UiUtils.addKeyBinding(shortcutComponent, keyStroke, id, this::performAction); addedKeyStroke = keyStroke; } } @Override public String toString() { return "JadxGuiAction{" + id + ", component: " + shortcutComponent + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/JsonPrettifyAction.java ================================================ package jadx.gui.ui.action; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import jadx.core.utils.GsonUtils; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; public class JsonPrettifyAction extends JNodeAction { private static final long serialVersionUID = -2682529369671695550L; private static final Gson GSON = GsonUtils.buildGson(); public JsonPrettifyAction(CodeArea codeArea) { super(ActionModel.JSON_PRETTIFY, codeArea); } @Override public void runAction(JNode node) { String originString = getCodeArea().getCodeInfo().getCodeStr(); JsonElement je = JsonParser.parseString(originString); String prettyString = GSON.toJson(je); getCodeArea().setText(prettyString); } @Override public boolean isActionEnabled(JNode node) { return true; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/RenameAction.java ================================================ package jadx.gui.ui.action; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JRenameNode; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.RenameDialog; public final class RenameAction extends JNodeAction { private static final long serialVersionUID = -4680872086148463289L; public RenameAction(CodeArea codeArea) { super(ActionModel.CODE_RENAME, codeArea); } @Override public boolean isActionEnabled(JNode node) { if (node == null) { return false; } if (node instanceof JRenameNode) { return ((JRenameNode) node).canRename(); } return false; } @Override public void runAction(JNode node) { RenameDialog.rename(getCodeArea().getMainWindow(), (JRenameNode) node); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/ViewCallGraphAction.java ================================================ package jadx.gui.ui.action; import javax.swing.JOptionPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.CallGraphDialog; import jadx.gui.utils.NLS; public final class ViewCallGraphAction extends JNodeAction { private static final Logger LOG = LoggerFactory.getLogger(ViewCallGraphAction.class); private static final long serialVersionUID = -11122327621269039L; public ViewCallGraphAction(CodeArea codeArea) { super(ActionModel.VIEW_CALL_GRAPH, codeArea); } @Override public void runAction(JNode node) { try { JMethod methodNode; if (node instanceof JMethod) { methodNode = (JMethod) node; } else { throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); } CallGraphDialog.open(getCodeArea().getMainWindow(), methodNode); } catch (Exception e) { LOG.error("Failed to view graph", e); JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); } } @Override public boolean isActionEnabled(JNode node) { return node instanceof JMethod; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassInheritanceGraphAction.java ================================================ package jadx.gui.ui.action; import javax.swing.JOptionPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JField; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.ClassInheritanceGraphDialog; import jadx.gui.utils.NLS; public final class ViewClassInheritanceGraphAction extends JNodeAction { private static final Logger LOG = LoggerFactory.getLogger(ViewClassInheritanceGraphAction.class); private static final long serialVersionUID = -331826691076655264L; public ViewClassInheritanceGraphAction(CodeArea codeArea) { super(ActionModel.VIEW_CLASS_INHERITANCE_GRAPH, codeArea); } @Override public void runAction(JNode node) { try { JClass classNode; if (node instanceof JMethod) { classNode = node.getJParent(); } else if (node instanceof JField) { classNode = node.getJParent(); } else if (node instanceof JClass) { classNode = (JClass) node; } else { throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); } ClassInheritanceGraphDialog.open(getCodeArea().getMainWindow(), classNode); } catch (Exception e) { LOG.error("Failed to view graph", e); JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); } } @Override public boolean isActionEnabled(JNode node) { return node instanceof JMethod || node instanceof JClass || node instanceof JField; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassMethodGraphAction.java ================================================ package jadx.gui.ui.action; import javax.swing.JOptionPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JField; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.ClassMethodGraphDialog; import jadx.gui.utils.NLS; public final class ViewClassMethodGraphAction extends JNodeAction { private static final Logger LOG = LoggerFactory.getLogger(ViewClassMethodGraphAction.class); private static final long serialVersionUID = -331826691076655264L; public ViewClassMethodGraphAction(CodeArea codeArea) { super(ActionModel.VIEW_CLASS_METHOD_GRAPH, codeArea); } @Override public void runAction(JNode node) { try { JClass classNode; if (node instanceof JMethod) { classNode = node.getJParent(); } else if (node instanceof JField) { classNode = node.getJParent(); } else if (node instanceof JClass) { classNode = (JClass) node; } else { throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); } ClassMethodGraphDialog.open(getCodeArea().getMainWindow(), classNode); } catch (Exception e) { LOG.error("Failed to view graph", e); JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); } } @Override public boolean isActionEnabled(JNode node) { return node instanceof JMethod || node instanceof JClass || node instanceof JField; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/ViewControlFlowGraphAction.java ================================================ package jadx.gui.ui.action; import java.io.File; import javax.swing.JOptionPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.DotGraphUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.ControlFlowGraphDialog; import jadx.gui.utils.NLS; public final class ViewControlFlowGraphAction extends JNodeAction { private static final Logger LOG = LoggerFactory.getLogger(ViewControlFlowGraphAction.class); private static final long serialVersionUID = -490213655L; public ViewControlFlowGraphAction(CodeArea codeArea) { super(ActionModel.VIEW_CONTROL_FLOW_GRAPH, codeArea); } @Override public void runAction(JNode node) { try { JMethod methodNode; if (node instanceof JMethod) { methodNode = (JMethod) node; } else { throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); } ControlFlowGraphDialog.open(getCodeArea().getMainWindow(), methodNode, false, false); } catch (Exception e) { LOG.error("Failed to view graph", e); JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); } } @Override public boolean isActionEnabled(JNode node) { if (!(node instanceof JMethod)) { return false; } MethodNode mth = ((JMethod) node).getJavaMethod().getMethodNode(); File file = new DotGraphUtils(false, false).getFullFile(mth); return file.exists() && !file.isDirectory(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/ViewRawControlFlowGraphAction.java ================================================ package jadx.gui.ui.action; import java.io.File; import javax.swing.JOptionPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.DotGraphUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.ControlFlowGraphDialog; import jadx.gui.utils.NLS; public final class ViewRawControlFlowGraphAction extends JNodeAction { private static final Logger LOG = LoggerFactory.getLogger(ViewRawControlFlowGraphAction.class); private static final long serialVersionUID = -535703386523657L; public ViewRawControlFlowGraphAction(CodeArea codeArea) { super(ActionModel.VIEW_RAW_CONTROL_FLOW_GRAPH, codeArea); } @Override public void runAction(JNode node) { try { JMethod methodNode; if (node instanceof JMethod) { methodNode = (JMethod) node; } else { throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); } ControlFlowGraphDialog.open(getCodeArea().getMainWindow(), methodNode, false, true); } catch (Exception e) { LOG.error("Failed to view graph", e); JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); } } @Override public boolean isActionEnabled(JNode node) { if (!(node instanceof JMethod)) { return false; } MethodNode mth = ((JMethod) node).getJavaMethod().getMethodNode(); File file = new DotGraphUtils(false, true).getFullFile(mth); return file.exists() && !file.isDirectory(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/ViewRegionControlFlowGraphAction.java ================================================ package jadx.gui.ui.action; import java.io.File; import javax.swing.JOptionPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.DotGraphUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.dialog.ControlFlowGraphDialog; import jadx.gui.utils.NLS; public final class ViewRegionControlFlowGraphAction extends JNodeAction { private static final Logger LOG = LoggerFactory.getLogger(ViewRegionControlFlowGraphAction.class); private static final long serialVersionUID = -14970352087936L; public ViewRegionControlFlowGraphAction(CodeArea codeArea) { super(ActionModel.VIEW_REGION_CONTROL_FLOW_GRAPH, codeArea); } @Override public void runAction(JNode node) { try { JMethod methodNode; if (node instanceof JMethod) { methodNode = (JMethod) node; } else { throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); } ControlFlowGraphDialog.open(getCodeArea().getMainWindow(), methodNode, true, false); } catch (Exception e) { LOG.error("Failed to view graph", e); JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE); } } @Override public boolean isActionEnabled(JNode node) { if (!(node instanceof JMethod)) { return false; } MethodNode mth = ((JMethod) node).getJavaMethod().getMethodNode(); File file = new DotGraphUtils(true, false).getFullFile(mth); return file.exists() && !file.isDirectory(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/action/XposedAction.kt ================================================ package jadx.gui.ui.action import jadx.core.dex.instructions.args.ArgType import jadx.core.dex.instructions.args.PrimitiveType import jadx.core.utils.exceptions.JadxRuntimeException import jadx.gui.settings.XposedCodegenLanguage import jadx.gui.treemodel.JClass import jadx.gui.treemodel.JField import jadx.gui.treemodel.JMethod import jadx.gui.treemodel.JNode import jadx.gui.ui.codearea.CodeArea import jadx.gui.utils.NLS import jadx.gui.utils.UiUtils import org.slf4j.Logger import org.slf4j.LoggerFactory import javax.swing.JOptionPane class XposedAction(codeArea: CodeArea) : JNodeAction(ActionModel.XPOSED_COPY, codeArea) { override fun runAction(node: JNode) { try { val xposedSnippet = generateXposedSnippet(node) LOG.info("Xposed snippet:\n{}", xposedSnippet) UiUtils.copyToClipboard(xposedSnippet) } catch (e: Exception) { LOG.error("Failed to generate Xposed code snippet", e) JOptionPane.showMessageDialog( getCodeArea().mainWindow, e.localizedMessage, NLS.str("error_dialog.title"), JOptionPane.ERROR_MESSAGE, ) } } override fun isActionEnabled(node: JNode?): Boolean { return node is JMethod || node is JClass || node is JField } private fun generateXposedSnippet(node: JNode): String { return when (node) { is JMethod -> generateMethodSnippet(node) is JClass -> generateClassSnippet(node) is JField -> generateFieldSnippet(node) else -> throw JadxRuntimeException("Unsupported node type: " + node.javaClass) } } private fun generateMethodSnippet(jMethod: JMethod): String { val javaMethod = jMethod.javaMethod val methodNode = javaMethod.methodNode val methodInfo = methodNode.methodInfo val xposedMethod: String var args = methodInfo.argumentsTypes.map(::fixTypeContent) val rawClassName = javaMethod.declaringClass.rawName if (methodNode.isConstructor) { xposedMethod = "findAndHookConstructor" } else { xposedMethod = "findAndHookMethod" args = listOf("\"${methodInfo.name}\"") + args } val template = when (language) { XposedCodegenLanguage.JAVA -> """XposedHelpers.%s("%s", classLoader, %s, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { super.beforeHookedMethod(param); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); } });""" XposedCodegenLanguage.KOTLIN -> """XposedHelpers.%s("%s", classLoader, %s, object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam) { super.beforeHookedMethod(param) } override fun afterHookedMethod(param: MethodHookParam) { super.afterHookedMethod(param) } })""" } return String.format(template, xposedMethod, rawClassName, args.joinToString(", ")) } private fun fixTypeContent(type: ArgType): String { return when { type.isGeneric -> "\"${type.`object`}\"" type.isGenericType && type.isObject && type.isTypeKnown -> "java.lang.Object" type.isPrimitive -> when (language) { XposedCodegenLanguage.JAVA -> "$type.class" XposedCodegenLanguage.KOTLIN -> when (type.primitiveType) { PrimitiveType.BOOLEAN -> "Boolean::class.javaPrimitiveType" PrimitiveType.CHAR -> "Char::class.javaPrimitiveType" PrimitiveType.BYTE -> "Byte::class.javaPrimitiveType" PrimitiveType.SHORT -> "Short::class.javaPrimitiveType" PrimitiveType.INT -> "Int::class.javaPrimitiveType" PrimitiveType.FLOAT -> "Float::class.javaPrimitiveType" PrimitiveType.LONG -> "Long::class.javaPrimitiveType" PrimitiveType.DOUBLE -> "Double::class.javaPrimitiveType" PrimitiveType.OBJECT -> "Any::class.java" PrimitiveType.ARRAY -> "Array::class.java" PrimitiveType.VOID -> "Void::class.javaPrimitiveType" else -> throw JadxRuntimeException("Unknown or null primitive type: $type") } } else -> "\"$type\"" } } private fun generateClassSnippet(jClass: JClass): String { val javaClass = jClass.cls val rawClassName = javaClass.rawName val className = javaClass.name val template = when (language) { XposedCodegenLanguage.JAVA -> "Class %sClass = classLoader.loadClass(\"%s\");" XposedCodegenLanguage.KOTLIN -> "val %sClass = classLoader.loadClass(\"%s\")" } return String.format(template, className, rawClassName) } private fun generateFieldSnippet(jField: JField): String { val javaField = jField.javaField val static = if (javaField.accessFlags.isStatic) "Static" else "" val type = PRIMITIVE_TYPE_MAPPING.getOrDefault(javaField.fieldNode.type.toString(), "Object") val xposedMethod = "XposedHelpers.get${static}${type}Field" val template = when (language) { XposedCodegenLanguage.JAVA -> "%s(/*runtimeObject*/, \"%s\");" XposedCodegenLanguage.KOTLIN -> "%s(/*runtimeObject*/, \"%s\")" } return String.format(template, xposedMethod, javaField.fieldNode.fieldInfo.name) } private val language: XposedCodegenLanguage get() = getCodeArea().mainWindow.settings.xposedCodegenLanguage companion object { private val LOG: Logger = LoggerFactory.getLogger(XposedAction::class.java) private const val serialVersionUID = 2641585141624592578L private val PRIMITIVE_TYPE_MAPPING = mapOf( "int" to "Int", "byte" to "Byte", "short" to "Short", "long" to "Long", "float" to "Float", "double" to "Double", "char" to "Char", "boolean" to "Boolean", ) } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/cellrenders/MethodRenderHelper.java ================================================ package jadx.gui.ui.cellrenders; import java.util.Iterator; import javax.swing.Icon; import javax.swing.ImageIcon; import jadx.api.JavaMethod; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.gui.utils.Icons; import jadx.gui.utils.OverlayIcon; import jadx.gui.utils.UiUtils; public class MethodRenderHelper { private static final ImageIcon ICON_METHOD_ABSTRACT = UiUtils.openSvgIcon("nodes/abstractMethod"); private static final ImageIcon ICON_METHOD_PRIVATE = UiUtils.openSvgIcon("nodes/privateMethod"); private static final ImageIcon ICON_METHOD_PROTECTED = UiUtils.openSvgIcon("nodes/protectedMethod"); private static final ImageIcon ICON_METHOD_PUBLIC = UiUtils.openSvgIcon("nodes/publicMethod"); private static final ImageIcon ICON_METHOD_CONSTRUCTOR = UiUtils.openSvgIcon("nodes/constructorMethod"); private static final ImageIcon ICON_METHOD_SYNC = UiUtils.openSvgIcon("nodes/methodReference"); public static Icon getIcon(JavaMethod mth) { AccessInfo accessFlags = mth.getAccessFlags(); Icon icon = Icons.METHOD; if (accessFlags.isAbstract()) { icon = ICON_METHOD_ABSTRACT; } if (accessFlags.isConstructor()) { icon = ICON_METHOD_CONSTRUCTOR; } if (accessFlags.isPublic()) { icon = ICON_METHOD_PUBLIC; } if (accessFlags.isPrivate()) { icon = ICON_METHOD_PRIVATE; } if (accessFlags.isProtected()) { icon = ICON_METHOD_PROTECTED; } if (accessFlags.isSynchronized()) { icon = ICON_METHOD_SYNC; } OverlayIcon overIcon = new OverlayIcon(icon); if (accessFlags.isFinal()) { overIcon.add(Icons.FINAL); } if (accessFlags.isStatic()) { overIcon.add(Icons.STATIC); } return overIcon; } public static String makeBaseString(JavaMethod mth) { if (mth.isClassInit()) { return "{...}"; } StringBuilder base = new StringBuilder(); if (mth.isConstructor()) { base.append(mth.getDeclaringClass().getName()); } else { base.append(mth.getName()); } base.append('('); for (Iterator it = mth.getArguments().iterator(); it.hasNext();) { base.append(UiUtils.typeStr(it.next())); if (it.hasNext()) { base.append(", "); } } base.append(')'); return base.toString(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/cellrenders/MethodsListRenderer.java ================================================ package jadx.gui.ui.cellrenders; import java.awt.BorderLayout; import java.awt.Component; import javax.swing.BorderFactory; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.ListCellRenderer; import jadx.api.JavaMethod; import jadx.gui.utils.UiUtils; public class MethodsListRenderer extends JPanel implements ListCellRenderer { private final JCheckBox checkBox; private final JLabel label; public MethodsListRenderer() { setLayout(new BorderLayout(5, 0)); checkBox = new JCheckBox(); label = new JLabel(); setBorder(BorderFactory.createEmptyBorder(1, 5, 1, 5)); add(checkBox, BorderLayout.WEST); add(label, BorderLayout.CENTER); setOpaque(true); checkBox.setOpaque(false); label.setOpaque(false); } @Override public Component getListCellRendererComponent(JList list, JavaMethod value, int index, boolean isSelected, boolean cellHasFocus) { label.setText(UiUtils.typeFormatHtml(MethodRenderHelper.makeBaseString(value), value.getReturnType())); label.setIcon(MethodRenderHelper.getIcon(value)); checkBox.setSelected(isSelected); setBackground(isSelected ? list.getSelectionBackground() : list.getBackground()); setForeground(isSelected ? list.getSelectionForeground() : list.getForeground()); label.setForeground(isSelected ? list.getSelectionForeground() : list.getForeground()); return this; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/cellrenders/PathHighlightTreeCellRenderer.java ================================================ package jadx.gui.ui.cellrenders; import java.awt.Color; import java.awt.Component; import javax.swing.BorderFactory; import javax.swing.JTree; import javax.swing.UIManager; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreePath; import jadx.gui.treemodel.JNode; import jadx.gui.utils.UiUtils; public class PathHighlightTreeCellRenderer extends DefaultTreeCellRenderer { private final boolean isDarkTheme; public PathHighlightTreeCellRenderer() { super(); Color themeBackground = UIManager.getColor("Panel.background"); isDarkTheme = UiUtils.isDarkTheme(themeBackground); } @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { Component comp = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; Object userObject = node.getUserObject(); // Calculate the node level int level = node.getLevel(); // Set different colors according to the level float hue = (level * 0.1f) % 1.0f; // Hue cycle Color levelColor = Color.getHSBColor(hue, 0.1f, 0.95f); // Check if it is on the selected path boolean onSelectionPath = false; TreePath selectionPath = tree.getSelectionPath(); if (selectionPath != null) { // Check if the current node is on the selected path (whether it is part of the selected path) Object[] selectedPathNodes = selectionPath.getPath(); for (Object pathNode : selectedPathNodes) { if (pathNode == node) { onSelectionPath = true; break; } } } if (onSelectionPath && !selected) { // If it is on the selected path but not the selected node, use a special foreground setForeground(isDarkTheme ? Color.decode("#70AEFF") : Color.decode("#0033B3")); } else if (!selected) { // Only apply the background color when it is not selected setBackground(levelColor); // Normal border setBorder(BorderFactory.createEmptyBorder(2, level * 2 + 1, 2, 1)); } else { // The selected node also adds a border setBorder(BorderFactory.createEmptyBorder(2, level * 2 + 1, 2, 1)); } if (userObject instanceof JNode) { JNode jNode = (JNode) userObject; setText(jNode.makeLongString()); setIcon(jNode.getIcon()); setToolTipText(jNode.getTooltip()); } return comp; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java ================================================ package jadx.gui.ui.codearea; import java.awt.Component; import java.awt.Dimension; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.text.BadLocationException; import javax.swing.text.Caret; import javax.swing.text.DefaultCaret; import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities; import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; import org.fife.ui.rsyntaxtextarea.TokenTypes; import org.fife.ui.rtextarea.Gutter; import org.fife.ui.rtextarea.SearchContext; import org.fife.ui.rtextarea.SearchEngine; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.jobs.IBackgroundTask; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JEditableNode; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.action.JNodeAction; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.utils.DefaultPopupMenuListener; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.DocumentUpdateListener; import jadx.gui.utils.ui.ZoomActions; public abstract class AbstractCodeArea extends RSyntaxTextArea { private static final long serialVersionUID = -3980354865216031972L; private static final Logger LOG = LoggerFactory.getLogger(AbstractCodeArea.class); public static final String SYNTAX_STYLE_SMALI = "text/smali"; static { TokenMakerFactory tokenMakerFactory = TokenMakerFactory.getDefaultInstance(); if (tokenMakerFactory instanceof AbstractTokenMakerFactory) { AbstractTokenMakerFactory atmf = (AbstractTokenMakerFactory) tokenMakerFactory; atmf.putMapping(SYNTAX_STYLE_SMALI, "jadx.gui.ui.codearea.SmaliTokenMaker"); // use simple token maker instead default PlainTextTokenMaker to avoid parse errors atmf.putMapping(SYNTAX_STYLE_NONE, "jadx.gui.ui.codearea.SimpleTokenMaker"); } else { throw new JadxRuntimeException("Unexpected TokenMakerFactory instance: " + tokenMakerFactory.getClass()); } SmaliFoldParser.register(); } protected ContentPanel contentPanel; protected JNode node; private final AtomicBoolean loaded = new AtomicBoolean(false); public AbstractCodeArea(ContentPanel contentPanel, JNode node) { this.contentPanel = contentPanel; this.node = Objects.requireNonNull(node); setMarkOccurrences(false); setFadeCurrentLineHighlight(true); setAntiAliasingEnabled(true); applyEditableProperties(node); loadSettings(); JadxSettings settings = contentPanel.getMainWindow().getSettings(); setLineWrap(settings.isCodeAreaLineWrap()); ZoomActions.register(this, settings, this::loadSettings); if (node instanceof JEditableNode) { JEditableNode editableNode = (JEditableNode) node; addSaveActions(editableNode); addChangeUpdates(editableNode); } else { addCaretActions(); addFastCopyAction(); } } private void applyEditableProperties(JNode node) { boolean editable = node.isEditable(); setEditable(editable); if (editable) { setCloseCurlyBraces(true); setCloseMarkupTags(true); setAutoIndentEnabled(true); setClearWhitespaceLinesEnabled(true); } } @Override protected JPopupMenu createPopupMenu() { JPopupMenu menu = new JPopupMenu(); if (node.isEditable()) { menu.add(createPopupMenuItem(getAction(UNDO_ACTION))); menu.add(createPopupMenuItem(getAction(REDO_ACTION))); menu.addSeparator(); menu.add(createPopupMenuItem(cutAction)); menu.add(createPopupMenuItem(copyAction)); menu.add(createPopupMenuItem(getAction(PASTE_ACTION))); menu.add(createPopupMenuItem(getAction(DELETE_ACTION))); menu.addSeparator(); menu.add(createPopupMenuItem(getAction(SELECT_ALL_ACTION))); } else { menu.add(createPopupMenuItem(copyAction)); menu.add(createPopupMenuItem(getAction(SELECT_ALL_ACTION))); } appendFoldingMenu(menu); appendWrapLineMenu(menu); return menu; } @Override protected void appendFoldingMenu(JPopupMenu popup) { // append code folding popup menu entry only if enabled if (isCodeFoldingEnabled()) { super.appendFoldingMenu(popup); } } private void appendWrapLineMenu(JPopupMenu popupMenu) { JadxSettings settings = contentPanel.getMainWindow().getSettings(); popupMenu.addSeparator(); JCheckBoxMenuItem wrapItem = new JCheckBoxMenuItem(NLS.str("popup.line_wrap"), getLineWrap()); wrapItem.setAction(new AbstractAction(NLS.str("popup.line_wrap")) { @Override public void actionPerformed(ActionEvent e) { boolean wrap = !getLineWrap(); settings.setCodeAreaLineWrap(wrap); settings.sync(); contentPanel.getTabbedPane().getTabs().forEach(v -> { if (v instanceof AbstractCodeContentPanel) { AbstractCodeArea codeArea = ((AbstractCodeContentPanel) v).getCodeArea(); setCodeAreaLineWrap(codeArea, wrap); if (v instanceof ClassCodeContentPanel) { codeArea = ((ClassCodeContentPanel) v).getSmaliCodeArea(); setCodeAreaLineWrap(codeArea, wrap); } } }); } }); popupMenu.add(wrapItem); popupMenu.addPopupMenuListener(new DefaultPopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { wrapItem.setState(getLineWrap()); } }); } private void setCodeAreaLineWrap(AbstractCodeArea codeArea, boolean wrap) { codeArea.setLineWrap(wrap); if (codeArea.isVisible()) { codeArea.repaint(); } } private void addCaretActions() { Caret caret = getCaret(); if (caret instanceof DefaultCaret) { ((DefaultCaret) caret).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); } this.addFocusListener(new FocusListener() { // fix caret missing bug. // when lost focus set visible to false, // and when regained set back to true will force // the caret to be repainted. @Override public void focusGained(FocusEvent e) { caret.setVisible(true); } @Override public void focusLost(FocusEvent e) { caret.setVisible(false); } }); addCaretListener(new CaretListener() { int lastPos = -1; String lastText = ""; @Override public void caretUpdate(CaretEvent e) { int pos = getCaretPosition(); if (lastPos != pos) { lastPos = pos; lastText = highlightCaretWord(lastText, pos); } } }); } /** * Ctrl+C will copy highlighted word */ private void addFastCopyAction() { addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_C && UiUtils.isCtrlDown(e)) { UiUtils.copyToClipboard(getSelectedTokenOrWord()); } } }); } /** * If the user has selected an individual word, for example by clicking and dragging * the mouse, then get that. Otherwise get the token underneath the cursor. * This is useful when the token is a string or comment and we want to control or copy * the word rather than the whole thing. * * @return The word or the token text */ public @Nullable String getSelectedTokenOrWord() { final String rc = getSelectedText(); if (rc == null) { return getWordUnderCaret(); } if (StringUtils.isEmpty(rc)) { return getWordUnderCaret(); } return rc; } private void addSaveActions(JEditableNode node) { addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_S && UiUtils.isCtrlDown(e)) { node.save(AbstractCodeArea.this.getText()); node.setChanged(false); } } }); } private void addChangeUpdates(JEditableNode editableNode) { getDocument().addDocumentListener(new DocumentUpdateListener(ev -> { if (loaded.get()) { editableNode.setChanged(true); } })); } private String highlightCaretWord(String lastText, int pos) { String text = getWordByPosition(pos); if (StringUtils.isEmpty(text)) { highlightAllMatches(null); lastText = ""; } else if (!lastText.equals(text)) { highlightAllMatches(text); lastText = text; } return lastText; } @Nullable public String getWordUnderCaret() { return getWordByPosition(getCaretPosition()); } public @Nullable String getWordByPosition(int offset) { Token token = getWordTokenAtOffset(offset); if (token == null) { return null; } String str = token.getLexeme(); int len = str.length(); if (len > 2 && str.startsWith("\"") && str.endsWith("\"")) { return str.substring(1, len - 1); } return str; } /** * Return any word token (not whitespace or special symbol) at offset. * Select the previous token if the cursor at word end (current token already is whitespace) */ public @Nullable Token getWordTokenAtOffset(int offset) { try { int line = this.getLineOfOffset(offset); Token lineTokens = this.getTokenListForLine(line); Token token = null; Token prevToken = null; for (Token t = lineTokens; t != null && t.isPaintable(); t = t.getNextToken()) { if (t.containsPosition(offset)) { token = t; break; } prevToken = t; } if (token == null) { return null; } if (isWordToken(token)) { return token; } if (isWordToken(prevToken)) { return prevToken; } return null; } catch (Exception e) { LOG.error("Failed to get token at pos: {}", offset, e); return null; } } public static boolean isWordToken(@Nullable Token token) { if (token == null) { return false; } switch (token.getType()) { case TokenTypes.NULL: case TokenTypes.WHITESPACE: case TokenTypes.SEPARATOR: case TokenTypes.OPERATOR: case TokenTypes.FUNCTION: return false; case TokenTypes.IDENTIFIER: if (token.length() == 1) { char ch = token.charAt(0); return ch != ';' && ch != '.' && ch != ','; } return true; default: return true; } } public abstract ICodeInfo getCodeInfo(); public void load() { if (isLoaded()) { return; } IBackgroundTask loadTask = getLoadTask(); contentPanel.getMainWindow().getBackgroundExecutor().execute(loadTask); } /** * Implement in this method the code that loads and sets the content to be displayed * Call `setLoaded()` on load finish. */ public abstract IBackgroundTask getLoadTask(); public void setLoaded() { this.loaded.set(true); discardAllEdits(); // disable 'undo' action to empty state (before load) } public void setUnLoaded() { this.loaded.set(false); } public boolean isLoaded() { return loaded.get(); } /** * Implement in this method the code that reloads node from cache and sets the new content to be * displayed */ public abstract void refresh(); public static RSyntaxTextArea getDefaultArea(MainWindow mainWindow) { RSyntaxTextArea area = new RSyntaxTextArea(); area.setEditable(false); area.setCodeFoldingEnabled(false); area.setAntiAliasingEnabled(true); loadCommonSettings(mainWindow, area); return area; } public static void loadCommonSettings(MainWindow mainWindow, RSyntaxTextArea area) { JadxSettings settings = mainWindow.getSettings(); mainWindow.getEditorThemeManager().apply(area); area.setFont(settings.getCodeFont()); Gutter gutter = RSyntaxUtilities.getGutter(area); if (gutter != null) { gutter.setLineNumberFont(settings.getCodeFont()); } } public void loadSettings() { loadCommonSettings(contentPanel.getMainWindow(), this); } public void scrollToPos(int pos) { try { setCaretPosition(pos); centerCurrentLine(); forceCurrentLineHighlightRepaint(); } catch (Exception e) { LOG.warn("Can't scroll to position {}", pos, e); } } @SuppressWarnings("deprecation") public void centerCurrentLine() { JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, this); if (viewport == null) { return; } try { Rectangle r = modelToView(getCaretPosition()); if (r == null) { return; } int extentHeight = viewport.getExtentSize().height; Dimension viewSize = viewport.getViewSize(); if (viewSize == null) { return; } int viewHeight = viewSize.height; int y = Math.max(0, r.y - extentHeight / 2); y = Math.min(y, viewHeight - extentHeight); viewport.setViewPosition(new Point(0, y)); } catch (BadLocationException e) { LOG.debug("Can't center current line", e); } } /** * @param str - if null -> reset current highlights */ private void highlightAllMatches(@Nullable String str) { try { SearchContext context = new SearchContext(str); context.setMarkAll(true); context.setMatchCase(true); context.setWholeWord(true); SearchEngine.markAll(this, context); } catch (Throwable e) { // syntax parsing can fail for incorrect code LOG.debug("Search highlight failed", e); } } public @Nullable JumpPosition getCurrentPosition() { int pos = getCaretPosition(); if (pos == 0) { return null; } return new JumpPosition(node, pos); } public int getLineStartFor(int pos) throws BadLocationException { return getLineStartOffset(getLineOfOffset(pos)); } public String getLineAt(int pos) throws BadLocationException { return getLineText(getLineOfOffset(pos) + 1); } public String getLineText(int line) throws BadLocationException { int lineNum = line - 1; int startOffset = getLineStartOffset(lineNum); int endOffset = getLineEndOffset(lineNum); return getText(startOffset, endOffset - startOffset); } public ContentPanel getContentPanel() { return contentPanel; } public JNode getNode() { return node; } @Nullable public JClass getJClass() { if (node instanceof JClass) { return (JClass) node; } return null; } public boolean isDisposed() { return node == null; } public void dispose() { // clear internals try { setIgnoreRepaint(true); setText(""); setEnabled(false); setSyntaxEditingStyle(SYNTAX_STYLE_NONE); setLinkGenerator(null); for (MouseListener mouseListener : getMouseListeners()) { removeMouseListener(mouseListener); } for (MouseMotionListener mouseMotionListener : getMouseMotionListeners()) { removeMouseMotionListener(mouseMotionListener); } JPopupMenu popupMenu = getPopupMenu(); for (PopupMenuListener popupMenuListener : popupMenu.getPopupMenuListeners()) { popupMenu.removePopupMenuListener(popupMenuListener); } for (Component component : popupMenu.getComponents()) { if (component instanceof JMenuItem) { Action action = ((JMenuItem) component).getAction(); if (action instanceof JNodeAction) { ((JNodeAction) action).dispose(); } } } popupMenu.removeAll(); } catch (Throwable e) { LOG.debug("Error on code area dispose", e); } // code area reference can still be used somewhere in UI objects, // reset node reference to allow to GC jadx objects tree node = null; contentPanel = null; } @Override public Dimension getPreferredSize() { try { return super.getPreferredSize(); } catch (Exception e) { LOG.warn("Failed to calculate preferred size for code area", e); // copied from javax.swing.JTextArea.getPreferredSize (super call above) // as a fallback for returned null size Dimension d = new Dimension(400, 400); Insets insets = getInsets(); if (getColumns() != 0) { d.width = Math.max(d.width, getColumns() * getColumnWidth() + insets.left + insets.right); } if (getRows() != 0) { d.height = Math.max(d.height, getRows() * getRowHeight() + insets.top + insets.bottom); } return d; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeContentPanel.java ================================================ package jadx.gui.ui.codearea; import java.awt.Component; import org.jetbrains.annotations.Nullable; import jadx.gui.treemodel.JNode; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.TabbedPane; /** * The abstract base class for a content panel that show text based code or a.g. a resource */ public abstract class AbstractCodeContentPanel extends ContentPanel { private static final long serialVersionUID = 4685846894279064422L; protected AbstractCodeContentPanel(TabbedPane panel, JNode jnode) { super(panel, jnode); } public abstract @Nullable AbstractCodeArea getCodeArea(); public abstract Component getChildrenComponent(); public void scrollToPos(int pos) { AbstractCodeArea codeArea = getCodeArea(); if (codeArea != null) { codeArea.requestFocus(); codeArea.scrollToPos(pos); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java ================================================ package jadx.gui.ui.codearea; import java.awt.BorderLayout; import java.awt.Component; import java.nio.charset.StandardCharsets; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ResourcesLoader; import jadx.api.resources.ResourceContentType; import jadx.gui.jobs.BackgroundExecutor; import jadx.gui.jobs.IBackgroundTask; import jadx.gui.jobs.TaskWithExtraOnFinish; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.LineNumbersMode; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResource; import jadx.gui.ui.hexviewer.HexPreviewPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.UiUtils; public class BinaryContentPanel extends AbstractCodeContentPanel { private static final Logger LOG = LoggerFactory.getLogger(BinaryContentPanel.class); private final transient CodePanel textCodePanel; private final transient HexPreviewPanel hexPreviewPanel; private final transient JTabbedPane areaTabbedPane; public BinaryContentPanel(TabbedPane panel, JNode jnode, boolean supportsText) { super(panel, jnode); setLayout(new BorderLayout()); setBorder(new EmptyBorder(0, 0, 0, 0)); if (supportsText) { textCodePanel = new CodePanel(new CodeArea(this, jnode)); } else { textCodePanel = null; } hexPreviewPanel = new HexPreviewPanel(getSettings()); hexPreviewPanel.getInspector().setVisible(false); areaTabbedPane = buildTabbedPane(); add(areaTabbedPane); SwingUtilities.invokeLater(this::loadSelectedPanel); } private void loadHexView() { if (hexPreviewPanel.isDataLoaded()) { return; } UiUtils.notUiThreadGuard(); byte[] bytes = getNodeBytes(); UiUtils.uiRunAndWait(() -> hexPreviewPanel.setData(bytes)); } private byte[] getNodeBytes() { JNode binaryNode = getNode(); if (binaryNode instanceof JResource) { JResource jResource = (JResource) binaryNode; try { return ResourcesLoader.decodeStream(jResource.getResFile(), (size, is) -> is.readAllBytes()); } catch (Exception e) { LOG.error("Failed to directly load resource binary data {}: {}", jResource.getName(), e.getMessage()); } } return binaryNode.getCodeInfo().getCodeStr().getBytes(StandardCharsets.US_ASCII); } private JTabbedPane buildTabbedPane() { JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.BOTTOM); tabbedPane.setBorder(new EmptyBorder(0, 0, 0, 0)); tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); if (textCodePanel != null) { tabbedPane.add(textCodePanel, "Text"); } tabbedPane.add(hexPreviewPanel, "Hex"); tabbedPane.addChangeListener(e -> { getMainWindow().toggleHexViewMenu(); loadSelectedPanel(); }); return tabbedPane; } private void loadSelectedPanel() { BackgroundExecutor bgExec = getMainWindow().getBackgroundExecutor(); Component codePanel = getSelectedPanel(); if (codePanel instanceof CodeArea) { CodeArea codeArea = (CodeArea) codePanel; codeArea.load(); } else { bgExec.startLoading(this::loadHexView); } } @Override public AbstractCodeArea getCodeArea() { if (textCodePanel != null) { return textCodePanel.getCodeArea(); } else { return null; } } @Override public void scrollToPos(int pos) { UiUtils.uiThreadGuard(); BackgroundExecutor bgExec = getMainWindow().getBackgroundExecutor(); if (getNode().getContentType() == ResourceContentType.CONTENT_TEXT) { areaTabbedPane.setSelectedComponent(textCodePanel); AbstractCodeArea codeArea = textCodePanel.getCodeArea(); if (codeArea.isLoaded()) { codeArea.scrollToPos(pos); } else { IBackgroundTask loadTask = codeArea.getLoadTask(); bgExec.execute(new TaskWithExtraOnFinish(loadTask, () -> codeArea.scrollToPos(pos))); } } else { areaTabbedPane.setSelectedComponent(hexPreviewPanel); bgExec.startLoading(this::loadHexView, () -> hexPreviewPanel.scrollToOffset(pos)); } } @Override public Component getChildrenComponent() { return getSelectedPanel(); } @Override public void loadSettings() { if (textCodePanel != null) { textCodePanel.loadSettings(); } updateUI(); } @Override public JadxSettings getSettings() { JadxSettings settings = super.getSettings(); settings.setLineNumbersMode(LineNumbersMode.NORMAL); return settings; } private Component getSelectedPanel() { Component selectedComponent = areaTabbedPane.getSelectedComponent(); Component selectedPanel; if (selectedComponent instanceof CodePanel) { selectedPanel = ((CodePanel) selectedComponent).getCodeArea(); } else if (selectedComponent instanceof JSplitPane) { selectedPanel = ((JSplitPane) selectedComponent).getLeftComponent(); } else if (selectedComponent instanceof HexPreviewPanel) { selectedPanel = selectedComponent; } else { throw new RuntimeException("tabbedPane.getSelectedComponent returned a Component " + "of unexpected type " + selectedComponent); } return selectedPanel; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java ================================================ package jadx.gui.ui.codearea; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.JCheckBox; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JToolBar; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.DecompilationMode; import jadx.gui.treemodel.JClass; import jadx.gui.ui.codearea.mode.JCodeMode; import jadx.gui.ui.codearea.sync.CodePanelSyncee; import jadx.gui.ui.codearea.sync.CodePanelSyncer; import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory; import jadx.gui.ui.codearea.sync.fallback.FallbackSyncer; import jadx.gui.ui.panel.IViewStateSupport; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.NLS; import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_TRAILING_COMPONENT; /** * Displays one class with two different views: * *
    *
  • Java source code of the selected class (default)
  • *
  • Smali source code of the selected class
  • *
*/ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implements IViewStateSupport { private static final Logger LOG = LoggerFactory.getLogger(ClassCodeContentPanel.class); private static final long serialVersionUID = -7229931102504634591L; private final transient CodePanel javaCodePanel; private final transient CodePanel smaliCodePanel; private final transient JTabbedPane areaTabbedPane; private final AtomicBoolean syncInProgress = new AtomicBoolean(false); private boolean splitView = false; private final JCheckBox splitCheckboxNormal; public ClassCodeContentPanel(TabbedPane panel, JClass jCls) { super(panel, jCls); javaCodePanel = new CodePanel(new CodeArea(this, jCls)); smaliCodePanel = new CodePanel(new SmaliArea(this, jCls, false)); areaTabbedPane = buildTabbedPane(jCls); splitCheckboxNormal = addCustomControls(areaTabbedPane, false); javaCodePanel.load(); initView(false); } private void initView(boolean splitViewEnabled) { splitView = splitViewEnabled; removeAll(); setLayout(new BorderLayout()); setBorder(new EmptyBorder(0, 0, 0, 0)); if (splitViewEnabled) { setupSplitPane(); } else { javaCodePanel.load(); smaliCodePanel.load(); attachSyncListeners(javaCodePanel, smaliCodePanel); areaTabbedPane.setSelectedIndex(0); // default to Java splitCheckboxNormal.setSelected(false); add(areaTabbedPane); } revalidate(); repaint(); } private void attachSyncListeners(CodePanel javaPanel, CodePanel smaliPanel) { javaPanel.getCodeArea().addCaretListener(e -> { if (syncInProgress.get()) { return; } syncInProgress.set(true); syncToMethod(javaPanel, smaliPanel); syncInProgress.set(false); }); smaliPanel.getCodeArea().addCaretListener(e -> { if (syncInProgress.get()) { return; } syncInProgress.set(true); syncToMethod(smaliPanel, javaPanel); syncInProgress.set(false); }); } private void setupSplitPane() { JTabbedPane leftTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM); JTabbedPane rightTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM); CodePanel[] leftPanels = { new CodePanel(new CodeArea(this, (JClass) node)), // Java new CodePanel(new SmaliArea(this, (JClass) node, false)), // Smali new CodePanel(new SmaliArea(this, (JClass) node, true)), // Smali with Dalvik new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.SIMPLE))), // Simple new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.FALLBACK))) // Fallback }; CodePanel[] rightPanels = { new CodePanel(new SmaliArea(this, (JClass) node, false)), // Smali new CodePanel(new SmaliArea(this, (JClass) node, true)), // Smali with Dalvik new CodePanel(new CodeArea(this, (JClass) node)), // Java new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.SIMPLE))), // Simple new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.FALLBACK))) // Fallback }; leftTabbedPane.add(leftPanels[0], NLS.str("tabs.code")); leftTabbedPane.add(leftPanels[1], NLS.str("tabs.smali")); leftTabbedPane.add(leftPanels[2], NLS.str("tabs.smali_bytecode")); leftTabbedPane.add(leftPanels[3], "Simple"); leftTabbedPane.add(leftPanels[4], "Fallback"); rightTabbedPane.add(rightPanels[0], NLS.str("tabs.smali")); rightTabbedPane.add(rightPanels[1], NLS.str("tabs.smali_bytecode")); rightTabbedPane.add(rightPanels[2], NLS.str("tabs.code")); rightTabbedPane.add(rightPanels[3], "Simple"); rightTabbedPane.add(rightPanels[4], "Fallback"); for (CodePanel p : leftPanels) { p.load(); } for (CodePanel p : rightPanels) { p.load(); } leftTabbedPane.addChangeListener(e -> ((CodePanel) leftTabbedPane.getSelectedComponent()).load()); rightTabbedPane.addChangeListener(e -> ((CodePanel) rightTabbedPane.getSelectedComponent()).load()); // Attach caret sync between all combinations for (CodePanel leftPanel : leftPanels) { for (CodePanel rightPanel : rightPanels) { attachSyncListeners(leftPanel, rightPanel); } } // Create and configure split pane JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftTabbedPane, rightTabbedPane); splitPane.setResizeWeight(0.5); leftTabbedPane.setMinimumSize(new Dimension(200, 200)); rightTabbedPane.setMinimumSize(new Dimension(200, 200)); add(splitPane); // Set divider location after layout SwingUtilities.invokeLater(() -> splitPane.setDividerLocation(0.5)); rightTabbedPane.setSelectedIndex(0); addCustomControls(leftTabbedPane, true); } private JTabbedPane buildTabbedPane(JClass jCls) { JTabbedPane areaTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM); areaTabbedPane.setBorder(new EmptyBorder(0, 0, 0, 0)); areaTabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); areaTabbedPane.add(javaCodePanel, NLS.str("tabs.code")); areaTabbedPane.add(smaliCodePanel, NLS.str("tabs.smali")); areaTabbedPane.add(new CodePanel(new SmaliArea(this, jCls, true)), NLS.str("tabs.smali_bytecode")); areaTabbedPane.add(new CodePanel(new CodeArea(this, new JCodeMode(jCls, DecompilationMode.SIMPLE))), "Simple"); areaTabbedPane.add(new CodePanel(new CodeArea(this, new JCodeMode(jCls, DecompilationMode.FALLBACK))), "Fallback"); areaTabbedPane.addChangeListener(e -> { CodePanel selectedPanel = (CodePanel) areaTabbedPane.getSelectedComponent(); // TODO: to run background load extract ui update to other method selectedPanel.load(); // execInBackground(selectedPanel::load); }); return areaTabbedPane; } private JCheckBox addCustomControls(JTabbedPane tabbedPane, boolean splitCheckboxInitialState) { JCheckBox splitCheckBox = new JCheckBox("Split view", splitCheckboxInitialState); splitCheckBox.addItemListener(e -> { boolean newSplitView = splitCheckBox.isSelected(); if (splitView != newSplitView) { this.initView(newSplitView); } }); JToolBar trailing = new JToolBar(); trailing.setFloatable(false); trailing.setBorder(null); // trailing.add(Box.createHorizontalGlue()); trailing.addSeparator(new Dimension(50, 1)); trailing.add(splitCheckBox); tabbedPane.putClientProperty(TABBED_PANE_TRAILING_COMPONENT, trailing); return splitCheckBox; } @Override public void loadSettings() { javaCodePanel.loadSettings(); smaliCodePanel.loadSettings(); updateUI(); } @Override public AbstractCodeArea getCodeArea() { return javaCodePanel.getCodeArea(); } @Override public Component getChildrenComponent() { return getCodeArea(); } public CodePanel getJavaCodePanel() { return javaCodePanel; } public void switchPanel() { boolean toSmali = areaTabbedPane.getSelectedComponent() == javaCodePanel; areaTabbedPane.setSelectedComponent(toSmali ? smaliCodePanel : javaCodePanel); } public AbstractCodeArea getCurrentCodeArea() { return ((CodePanel) areaTabbedPane.getSelectedComponent()).getCodeArea(); } public AbstractCodeArea getSmaliCodeArea() { return smaliCodePanel.getCodeArea(); } public void showSmaliPane() { areaTabbedPane.setSelectedComponent(smaliCodePanel); } @Override public void saveEditorViewState(EditorViewState viewState) { CodePanel codePanel = (CodePanel) areaTabbedPane.getSelectedComponent(); int caretPos = codePanel.getCodeArea().getCaretPosition(); Point viewPoint = codePanel.getCodeScrollPane().getViewport().getViewPosition(); String subPath = codePanel == javaCodePanel ? "java" : "smali"; viewState.setSubPath(subPath); viewState.setCaretPos(caretPos); viewState.setViewPoint(viewPoint); } @Override public void restoreEditorViewState(EditorViewState viewState) { boolean isJava = viewState.getSubPath().equals("java"); CodePanel activePanel = isJava ? javaCodePanel : smaliCodePanel; areaTabbedPane.setSelectedComponent(activePanel); try { activePanel.getCodeScrollPane().getViewport().setViewPosition(viewState.getViewPoint()); } catch (Exception e) { LOG.debug("Failed to restore view position: {}", viewState.getViewPoint(), e); } int caretPos = viewState.getCaretPos(); try { AbstractCodeArea codeArea = activePanel.getCodeArea(); int codeLen = codeArea.getDocument().getLength(); if (caretPos >= 0 && caretPos < codeLen) { codeArea.setCaretPosition(caretPos); } } catch (Exception e) { LOG.debug("Failed to restore caret position: {}", caretPos, e); } } @Override public void dispose() { javaCodePanel.dispose(); smaliCodePanel.dispose(); for (Component component : areaTabbedPane.getComponents()) { if (component instanceof CodePanel) { ((CodePanel) component).dispose(); } } super.dispose(); } private void syncToMethod(CodePanel fromPanel, CodePanel toPanel) { if (!fromPanel.isShowing() || !toPanel.isShowing()) { return; } try { AbstractCodeArea from = fromPanel.getCodeArea(); AbstractCodeArea to = toPanel.getCodeArea(); toPanel.load(); if (from instanceof CodePanelSyncerAbstractFactory && to instanceof CodePanelSyncee) { CodePanelSyncer syncer = ((CodePanelSyncerAbstractFactory) from).createCodePanelSyncer(); if (((CodePanelSyncee) to).sync(syncer)) { return; } } if (!FallbackSyncer.sync(fromPanel, toPanel)) { LOG.warn("Code pane area sync not possible"); } } catch (Exception ex) { LOG.warn("Failed to sync method/class across views: {}", ex.getLocalizedMessage()); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java ================================================ package jadx.gui.ui.codearea; import java.awt.Point; import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; import javax.swing.JPopupMenu; import javax.swing.event.PopupMenuEvent; import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rsyntaxtextarea.TokenTypes; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.JavaClass; import jadx.api.JavaNode; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeMetadata; import jadx.gui.JadxWrapper; import jadx.gui.jobs.IBackgroundTask; import jadx.gui.jobs.LoadTask; import jadx.gui.jobs.TaskWithExtraOnFinish; import jadx.gui.settings.JadxProject; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JLoadableNode; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResource; import jadx.gui.ui.MainWindow; import jadx.gui.ui.action.CommentSearchAction; import jadx.gui.ui.action.FindUsageAction; import jadx.gui.ui.action.FridaAction; import jadx.gui.ui.action.GoToDeclarationAction; import jadx.gui.ui.action.JNodeAction; import jadx.gui.ui.action.JsonPrettifyAction; import jadx.gui.ui.action.RenameAction; import jadx.gui.ui.action.ViewCallGraphAction; import jadx.gui.ui.action.ViewClassInheritanceGraphAction; import jadx.gui.ui.action.ViewClassMethodGraphAction; import jadx.gui.ui.action.ViewControlFlowGraphAction; import jadx.gui.ui.action.ViewRawControlFlowGraphAction; import jadx.gui.ui.action.ViewRegionControlFlowGraphAction; import jadx.gui.ui.action.XposedAction; import jadx.gui.ui.codearea.mode.JCodeMode; import jadx.gui.ui.codearea.sync.CodePanelSyncee; import jadx.gui.ui.codearea.sync.CodePanelSyncer; import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory; import jadx.gui.ui.codearea.sync.JavaSyncer; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.utils.CaretPositionFix; import jadx.gui.utils.DefaultPopupMenuListener; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.shortcut.ShortcutsController; /** * The {@link AbstractCodeArea} implementation used for displaying Java code and text based * resources (e.g. AndroidManifest.xml) */ public final class CodeArea extends AbstractCodeArea implements CodePanelSyncerAbstractFactory, CodePanelSyncee { private static final Logger LOG = LoggerFactory.getLogger(CodeArea.class); private static final long serialVersionUID = 6312736869579635796L; private @Nullable ICodeInfo cachedCodeInfo; private @Nullable MouseHoverHighlighter mouseHoverHighlighter; private final ShortcutsController shortcutsController; CodeArea(ContentPanel contentPanel, JNode node) { super(contentPanel, node); this.shortcutsController = getMainWindow().getShortcutsController(); setSyntaxEditingStyle(node.getSyntaxName()); boolean isJavaCode = isCodeNode(); if (isJavaCode) { ((RSyntaxDocument) getDocument()).setSyntaxStyle(new JadxTokenMaker(this)); } if (node instanceof JResource && node.makeString().endsWith(".json")) { addMenuForJsonFile(); } setHyperlinksEnabled(true); setCodeFoldingEnabled(true); setLinkScanningMask(InputEvent.CTRL_DOWN_MASK); CodeLinkGenerator codeLinkGenerator = new CodeLinkGenerator(this); setLinkGenerator(codeLinkGenerator); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.isControlDown() || jumpOnDoubleClick(e)) { navToDecl(e.getPoint()); } } }); if (isJavaCode) { mouseHoverHighlighter = new MouseHoverHighlighter(this, codeLinkGenerator); addMouseMotionListener(mouseHoverHighlighter); } } @Override public void loadSettings() { super.loadSettings(); if (mouseHoverHighlighter != null) { mouseHoverHighlighter.loadSettings(); } } public boolean isCodeNode() { return node instanceof JClass || node instanceof JCodeMode; } private boolean jumpOnDoubleClick(MouseEvent e) { return e.getClickCount() == 2 && getMainWindow().getSettings().isJumpOnDoubleClick(); } private void navToDecl(Point point) { int offs = viewToModel2D(point); JNode node = getJNodeAtOffset(adjustOffsetForWordToken(offs)); if (node != null) { contentPanel.getTabsController().codeJump(node); } } @Override public ICodeInfo getCodeInfo() { if (cachedCodeInfo == null) { if (isDisposed()) { LOG.debug("CodeArea used after dispose!"); return ICodeInfo.EMPTY; } cachedCodeInfo = Objects.requireNonNull(node.getCodeInfo()); } return cachedCodeInfo; } @Override public IBackgroundTask getLoadTask() { if (node instanceof JLoadableNode) { IBackgroundTask loadTask = ((JLoadableNode) node).getLoadTask(); if (loadTask != null) { return new TaskWithExtraOnFinish(loadTask, () -> { setText(getCodeInfo().getCodeStr()); setCaretPosition(0); setLoaded(); }); } } return new LoadTask<>( () -> getCodeInfo().getCodeStr(), code -> { setText(code); setCaretPosition(0); setLoaded(); }); } @Override public void refresh() { cachedCodeInfo = null; setText(getCodeInfo().getCodeStr()); } @Override protected JPopupMenu createPopupMenu() { JPopupMenu popup = super.createPopupMenu(); if (node instanceof JClass) { appendCodeMenuItems(popup); } return popup; } private void appendCodeMenuItems(JPopupMenu popupMenu) { ShortcutsController shortcutsController = getMainWindow().getShortcutsController(); JNodePopupBuilder popup = new JNodePopupBuilder(this, popupMenu, shortcutsController); popup.addSeparator(); popup.add(new FindUsageAction(this)); popup.add(new UsageDialogPlusAction(this)); popup.add(new GoToDeclarationAction(this)); popup.add(new CommentAction(this)); popup.add(new CommentSearchAction(this)); popup.add(new RenameAction(this)); popup.addSeparator(); popup.add(new FridaAction(this)); popup.add(new XposedAction(this)); popup.addSeparator(); popup.add(new ViewClassInheritanceGraphAction(this)); popup.add(new ViewClassMethodGraphAction(this)); popup.add(new ViewCallGraphAction(this)); popup.addSubmenu(new JNodeAction[] { new ViewControlFlowGraphAction(this), new ViewRawControlFlowGraphAction(this), new ViewRegionControlFlowGraphAction(this), }, NLS.str("popup.cfg_submenu")); popup.addSeparator(); popup.add(new ConvertNumberAction(this)); getMainWindow().getWrapper().getGuiPluginsContext().appendPopupMenus(this, popup); // move caret on mouse right button click popupMenu.addPopupMenuListener(new DefaultPopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { CodeArea codeArea = CodeArea.this; if (codeArea.getSelectedText() == null) { int offset = UiUtils.getOffsetAtMousePosition(codeArea); if (offset >= 0) { codeArea.setCaretPosition(offset); } } } }); } private void addMenuForJsonFile() { ShortcutsController shortcutsController = getMainWindow().getShortcutsController(); JNodePopupBuilder popup = new JNodePopupBuilder(this, getPopupMenu(), shortcutsController); popup.addSeparator(); popup.add(new JsonPrettifyAction(this)); } /** * Search start of word token at specified offset * * @return -1 if no word token found */ public int adjustOffsetForWordToken(int offset) { Token token = getWordTokenAtOffset(offset); if (token == null) { return -1; } int type = token.getType(); if (isCodeNode()) { if (type == TokenTypes.IDENTIFIER || type == TokenTypes.FUNCTION) { return token.getOffset(); } if (type == TokenTypes.ANNOTATION && token.length() > 1) { return token.getOffset() + 1; } if (type == TokenTypes.RESERVED_WORD && token.length() == 6 && token.getLexeme().equals("static")) { // maybe a class init method return token.getOffset(); } } else if (type == TokenTypes.MARKUP_TAG_ATTRIBUTE_VALUE) { return token.getOffset() + 1; // skip quote at start (") } return -1; } /** * Search node by offset in {@code jCls} code and return its definition position * (useful for jumps from usage) */ @Nullable public JumpPosition getDefPosForNodeAtOffset(int offset) { if (offset == -1) { return null; } JavaNode foundNode = getJavaNodeAtOffset(offset); if (foundNode == null) { return null; } if (foundNode == node.getJavaNode()) { // current node return new JumpPosition(node); } JNode jNode = convertJavaNode(foundNode); return new JumpPosition(jNode); } private JNode convertJavaNode(JavaNode javaNode) { JNodeCache nodeCache = getMainWindow().getCacheObject().getNodeCache(); return nodeCache.makeFrom(javaNode); } @Nullable public JNode getNodeUnderCaret() { int caretPos = getCaretPosition(); return getJNodeAtOffset(adjustOffsetForWordToken(caretPos)); } @Nullable public JNode getEnclosingNodeUnderCaret() { int caretPos = getCaretPosition(); int start = adjustOffsetForWordToken(caretPos); if (start == -1) { start = caretPos; } return getEnclosingJNodeAtOffset(start); } @Nullable public JNode getNodeUnderMouse() { Point pos = UiUtils.getMousePosition(this); return getJNodeAtOffset(adjustOffsetForWordToken(viewToModel2D(pos))); } @Nullable public JNode getEnclosingNodeUnderMouse() { Point pos = UiUtils.getMousePosition(this); return getEnclosingJNodeAtOffset(adjustOffsetForWordToken(viewToModel2D(pos))); } @Nullable public JNode getEnclosingJNodeAtOffset(int offset) { JavaNode javaNode = getEnclosingJavaNode(offset); if (javaNode != null) { return convertJavaNode(javaNode); } return null; } @Nullable public JNode getJNodeAtOffset(int offset) { JavaNode javaNode = getJavaNodeAtOffset(offset); if (javaNode != null) { return convertJavaNode(javaNode); } return null; } /** * Search referenced java node by offset in {@code jCls} code */ public JavaNode getJavaNodeAtOffset(int offset) { if (offset == -1) { return null; } try { return getJadxWrapper().getDecompiler().getJavaNodeAtPosition(getCodeInfo(), offset); } catch (Exception e) { LOG.error("Can't get java node by offset: {}", offset, e); } return null; } public JavaNode getClosestJavaNode(int offset) { if (offset == -1) { return null; } try { return getJadxWrapper().getDecompiler().getClosestJavaNode(getCodeInfo(), offset); } catch (Exception e) { LOG.error("Can't get java node by offset: {}", offset, e); return null; } } public JavaNode getEnclosingJavaNode(int offset) { if (offset == -1) { return null; } try { return getJadxWrapper().getDecompiler().getEnclosingNode(getCodeInfo(), offset); } catch (Exception e) { LOG.error("Can't get java node by offset: {}", offset, e); return null; } } public @Nullable JavaClass getJavaClassIfAtPos(int pos) { try { ICodeInfo codeInfo = getCodeInfo(); if (!codeInfo.hasMetadata()) { return null; } ICodeAnnotation ann = codeInfo.getCodeMetadata().getAt(pos); if (ann == null) { return null; } switch (ann.getAnnType()) { case CLASS: return (JavaClass) getJadxWrapper().getDecompiler().getJavaNodeByCodeAnnotation(codeInfo, ann); case METHOD: // use class from constructor call JavaNode node = getJadxWrapper().getDecompiler().getJavaNodeByCodeAnnotation(codeInfo, ann); return node != null ? node.getDeclaringClass() : null; default: return null; } } catch (Exception e) { LOG.error("Can't get java node by offset: {}", pos, e); return null; } } public void refreshClass() { refreshClass(false); } public void refreshClass(boolean alreadyReloaded) { if (node instanceof JClass) { JClass cls = node.getRootClass(); try { CaretPositionFix caretFix = new CaretPositionFix(this); caretFix.save(); if (alreadyReloaded) { cachedCodeInfo = cls.getCodeInfo(); } else { // bad. blocks the UI thread for a potentially expensive decomp cachedCodeInfo = cls.reload(getMainWindow().getCacheObject()); } ClassCodeContentPanel codeContentPanel = (ClassCodeContentPanel) this.contentPanel; codeContentPanel.getTabbedPane().refresh(cls); codeContentPanel.getJavaCodePanel().refresh(caretFix); } catch (Exception e) { LOG.error("Failed to reload class: {}", cls.getFullName(), e); } } } /** * Refresh the class in the background, updating the UI once the potential decomp is complete. * Should be called from the UI thread. */ public void backgroundRefreshClass() { UiUtils.uiThreadGuard(); this.getMainWindow().getBackgroundExecutor().execute("Refreshing...", () -> { this.getNode().getRootClass().reload(this.getMainWindow().getCacheObject()); UiUtils.uiRunAndWait(() -> { this.refreshClass(true); }); }); } public MainWindow getMainWindow() { return contentPanel.getMainWindow(); } public JadxWrapper getJadxWrapper() { return getMainWindow().getWrapper(); } public JadxProject getProject() { return getMainWindow().getProject(); } @Override public void dispose() { shortcutsController.unbindActionsForComponent(this); super.dispose(); cachedCodeInfo = null; } @Override public CodePanelSyncer createCodePanelSyncer() { return new JavaSyncer(this); } @Override public boolean sync(CodePanelSyncer codePanelSyncer) { return codePanelSyncer.syncTo(this); } @Nullable public ICodeMetadata getCodeMetadata() { ICodeInfo codeInfo = getCodeInfo(); if (!codeInfo.hasMetadata()) { LOG.warn("No code info metadata for {}", codeInfo.toString()); return null; } return codeInfo.getCodeMetadata(); } /** * Returns a mapping of 'decompilation output line number' to 'dex debug line number' * These are 1-indexed line numbers not the line indices of the CodeArea * * @return the line mapping */ public Map getLineMappings() { ICodeInfo codeInfo = getCodeInfo(); if (!codeInfo.hasMetadata()) { LOG.debug("No code info metadata for {}", codeInfo.toString()); return Map.of(); } Map lineMapping = codeInfo.getCodeMetadata().getLineMapping(); if (lineMapping.isEmpty()) { LOG.debug("Line mappings are empty for {}", codeInfo.toString()); return Map.of(); } return lineMapping; } /** * Returns the same as {@link #getLineMappings()} but only if each value (dex debug line number) * appears only once. * If a value appears more than once then it suggests that methods might share dex debug line * numbers. * If this is the case then the line mapping cannot be used for code sync correlation. * * @return the line mapping */ public Map getFunctionUniqueLineMappings() { final var lineMappings = getLineMappings(); final boolean isAnyRepeated = lineMappings.values().stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting())).values().stream() .filter(v -> v > 1).findAny().isPresent(); if (isAnyRepeated) { LOG.debug("Dex debug line mappings are not unique"); return Map.of(); } return lineMappings; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeContentPanel.java ================================================ package jadx.gui.ui.codearea; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Point; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.treemodel.JNode; import jadx.gui.ui.panel.IViewStateSupport; import jadx.gui.ui.tab.TabbedPane; public final class CodeContentPanel extends AbstractCodeContentPanel implements IViewStateSupport { private static final long serialVersionUID = 5310536092010045565L; private static final Logger LOG = LoggerFactory.getLogger(CodeContentPanel.class); private final CodePanel codePanel; public CodeContentPanel(TabbedPane panel, JNode jnode) { super(panel, jnode); setLayout(new BorderLayout()); codePanel = new CodePanel(new CodeArea(this, jnode)); add(codePanel, BorderLayout.CENTER); codePanel.load(); } @Override public void loadSettings() { codePanel.loadSettings(); updateUI(); } SearchBar getSearchBar() { return codePanel.getSearchBar(); } @Override public AbstractCodeArea getCodeArea() { return codePanel.getCodeArea(); } @Override public Component getChildrenComponent() { return getCodeArea(); } @Override public void saveEditorViewState(EditorViewState viewState) { int caretPos = codePanel.getCodeArea().getCaretPosition(); Point viewPoint = codePanel.getCodeScrollPane().getViewport().getViewPosition(); viewState.setCaretPos(caretPos); viewState.setViewPoint(viewPoint); } @Override public void restoreEditorViewState(EditorViewState viewState) { try { codePanel.getCodeScrollPane().getViewport().setViewPosition(viewState.getViewPoint()); codePanel.getCodeArea().setCaretPosition(viewState.getCaretPos()); } catch (Exception e) { LOG.error("Failed to restore view state", e); } } @Override public void dispose() { codePanel.dispose(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeLinkGenerator.java ================================================ package jadx.gui.ui.codearea; import java.util.Objects; import javax.swing.event.HyperlinkEvent; import org.fife.ui.rsyntaxtextarea.LinkGenerator; import org.fife.ui.rsyntaxtextarea.LinkGeneratorResult; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JavaNode; import jadx.gui.treemodel.JNode; import jadx.gui.utils.JumpPosition; public class CodeLinkGenerator implements LinkGenerator { private static final Logger LOG = LoggerFactory.getLogger(CodeLinkGenerator.class); private final CodeArea codeArea; private final JNode jNode; public CodeLinkGenerator(CodeArea codeArea) { this.codeArea = codeArea; this.jNode = codeArea.getNode(); } public JavaNode getNodeAtOffset(int offset) { try { if (!codeArea.getCodeInfo().hasMetadata()) { return null; } int sourceOffset = codeArea.adjustOffsetForWordToken(offset); if (sourceOffset == -1) { return null; } return codeArea.getJavaNodeAtOffset(offset); } catch (Exception e) { LOG.error("getNodeAtOffset error", e); return null; } } @Override public LinkGeneratorResult isLinkAtOffset(RSyntaxTextArea textArea, int offset) { try { if (!codeArea.getCodeInfo().hasMetadata()) { return null; } int sourceOffset = codeArea.adjustOffsetForWordToken(offset); if (sourceOffset == -1) { return null; } JumpPosition defPos = getJumpBySourceOffset(sourceOffset); if (defPos == null) { return null; } return new LinkGeneratorResult() { @Override public HyperlinkEvent execute() { return new HyperlinkEvent(defPos, HyperlinkEvent.EventType.ACTIVATED, null, defPos.getNode().makeLongString()); } @Override public int getSourceOffset() { return sourceOffset; } }; } catch (Exception e) { LOG.error("isLinkAtOffset error", e); return null; } } @Nullable private JumpPosition getJumpBySourceOffset(int sourceOffset) { final JumpPosition defPos = codeArea.getDefPosForNodeAtOffset(sourceOffset); if (defPos == null) { return null; } if (Objects.equals(defPos.getNode().getRootClass(), jNode) && defPos.getPos() == sourceOffset) { // ignore self jump return null; } return defPos; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java ================================================ package jadx.gui.ui.codearea; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JPopupMenu.Separator; import javax.swing.JScrollPane; import javax.swing.JViewport; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import javax.swing.event.PopupMenuEvent; import org.fife.ui.rtextarea.LineNumberFormatter; import org.fife.ui.rtextarea.LineNumberList; import org.fife.ui.rtextarea.RTextScrollPane; import jadx.api.ICodeInfo; import jadx.core.utils.StringUtils; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.LineNumbersMode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.dialog.SearchDialog; import jadx.gui.utils.CaretPositionFix; import jadx.gui.utils.DefaultPopupMenuListener; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.MousePressedHandler; /** * A panel combining a {@link SearchBar} and a scollable {@link CodeArea} */ public class CodePanel extends JPanel { private static final long serialVersionUID = 1117721869391885865L; private final SearchBar searchBar; private final AbstractCodeArea codeArea; private final RTextScrollPane codeScrollPane; private boolean useSourceLines; public CodePanel(AbstractCodeArea codeArea) { this.codeArea = codeArea; this.searchBar = new SearchBar(codeArea); this.codeScrollPane = new RTextScrollPane(codeArea); setLayout(new BorderLayout()); setBorder(new EmptyBorder(0, 0, 0, 0)); add(searchBar, BorderLayout.NORTH); add(codeScrollPane, BorderLayout.CENTER); initLinesModeSwitch(); KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, UiUtils.ctrlButton()); UiUtils.addKeyBinding(codeArea, key, "SearchAction", new AbstractAction() { private static final long serialVersionUID = 71338030532869694L; @Override public void actionPerformed(ActionEvent e) { searchBar.showAndFocus(); } }); JMenuItem searchItem = new JMenuItem(); JMenuItem globalSearchItem = new JMenuItem(); AbstractAction searchAction = new AbstractAction(NLS.str("popup.search", "")) { @Override public void actionPerformed(ActionEvent e) { searchBar.toggle(); } }; AbstractAction globalSearchAction = new AbstractAction(NLS.str("popup.search_global", "")) { @Override public void actionPerformed(ActionEvent e) { MainWindow mainWindow = codeArea.getContentPanel().getMainWindow(); SearchDialog.searchText(mainWindow, codeArea.getSelectedText()); } }; searchItem.setAction(searchAction); globalSearchItem.setAction(globalSearchAction); Separator separator = new Separator(); JPopupMenu popupMenu = codeArea.getPopupMenu(); popupMenu.addPopupMenuListener(new DefaultPopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { String preferText = codeArea.getSelectedText(); if (!StringUtils.isEmpty(preferText)) { if (preferText.length() >= 23) { preferText = preferText.substring(0, 20) + " ..."; } searchAction.putValue(Action.NAME, NLS.str("popup.search", preferText)); globalSearchAction.putValue(Action.NAME, NLS.str("popup.search_global", preferText)); popupMenu.add(separator); popupMenu.add(globalSearchItem); popupMenu.add(searchItem); } else { popupMenu.remove(separator); popupMenu.remove(globalSearchItem); popupMenu.remove(searchItem); } } }); } public void loadSettings() { codeArea.loadSettings(); initLineNumbers(); } public void load() { codeArea.load(); initLineNumbers(); } private synchronized void initLineNumbers() { codeScrollPane.getGutter().setLineNumberFont(getSettings().getCodeFont()); LineNumbersMode mode = getLineNumbersMode(); if (mode == LineNumbersMode.DISABLE) { codeScrollPane.setLineNumbersEnabled(false); return; } useSourceLines = mode == LineNumbersMode.DEBUG; applyLineFormatter(); codeScrollPane.setLineNumbersEnabled(true); } private static final LineNumberFormatter SIMPLE_LINE_FORMATTER = new LineNumberFormatter() { @Override public String format(int lineNumber) { return Integer.toString(lineNumber); } @Override public int getMaxLength(int maxLineNumber) { return SourceLineFormatter.getNumberLength(maxLineNumber); } }; private synchronized void applyLineFormatter() { LineNumberFormatter linesFormatter = useSourceLines ? new SourceLineFormatter(codeArea.getCodeInfo()) : SIMPLE_LINE_FORMATTER; codeScrollPane.getGutter().setLineNumberFormatter(linesFormatter); } private LineNumbersMode getLineNumbersMode() { LineNumbersMode mode = getSettings().getLineNumbersMode(); boolean canShowDebugLines = canShowDebugLines(); if (mode == LineNumbersMode.AUTO) { mode = canShowDebugLines ? LineNumbersMode.DEBUG : LineNumbersMode.NORMAL; } else if (mode == LineNumbersMode.DEBUG && !canShowDebugLines) { // nothing to show => hide lines view mode = LineNumbersMode.DISABLE; } return mode; } private boolean canShowDebugLines() { if (codeArea instanceof SmaliArea) { return false; } ICodeInfo codeInfo = codeArea.getCodeInfo(); if (!codeInfo.hasMetadata()) { return false; } Map lineMapping = codeInfo.getCodeMetadata().getLineMapping(); if (lineMapping.isEmpty()) { return false; } Set uniqueDebugLines = new HashSet<>(lineMapping.values()); return uniqueDebugLines.size() > 3; } private void initLinesModeSwitch() { MousePressedHandler lineModeSwitch = new MousePressedHandler(ev -> { useSourceLines = !useSourceLines; applyLineFormatter(); }); for (Component gutterComp : codeScrollPane.getGutter().getComponents()) { if (gutterComp instanceof LineNumberList) { gutterComp.addMouseListener(lineModeSwitch); } } } public SearchBar getSearchBar() { return searchBar; } public AbstractCodeArea getCodeArea() { return codeArea; } public JScrollPane getCodeScrollPane() { return codeScrollPane; } public void refresh(CaretPositionFix caretFix) { JViewport viewport = getCodeScrollPane().getViewport(); Point viewPosition = viewport.getViewPosition(); codeArea.refresh(); initLineNumbers(); SwingUtilities.invokeLater(() -> { viewport.setViewPosition(viewPosition); caretFix.restore(); }); } private JadxSettings getSettings() { return this.codeArea.getContentPanel().getTabbedPane() .getMainWindow().getSettings(); } public void dispose() { codeArea.dispose(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java ================================================ package jadx.gui.ui.codearea; import java.awt.event.ActionEvent; import java.util.Objects; import javax.swing.event.PopupMenuEvent; import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rsyntaxtextarea.TokenTypes; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.api.data.ICodeComment; import jadx.api.data.impl.JadxCodeComment; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxCodeRef; import jadx.api.data.impl.JadxNodeRef; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeAnnotation.AnnType; import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.gui.JadxWrapper; import jadx.gui.settings.JadxProject; import jadx.gui.treemodel.JClass; import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.action.CodeAreaAction; import jadx.gui.ui.action.JadxGuiAction; import jadx.gui.ui.dialog.CommentDialog; import jadx.gui.utils.DefaultPopupMenuListener; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class CommentAction extends CodeAreaAction implements DefaultPopupMenuListener { private static final long serialVersionUID = 4753838562204629112L; private static final Logger LOG = LoggerFactory.getLogger(CommentAction.class); protected final boolean enabled; private @Nullable ICodeComment actionComment; private boolean updateComment; public CommentAction(CodeArea codeArea) { super(ActionModel.CODE_COMMENT, codeArea); this.enabled = codeArea.getNode() instanceof JClass; } public CommentAction(ActionModel actionModel, CodeArea codeArea) { super(actionModel, codeArea); this.enabled = codeArea.getNode() instanceof JClass; } @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { if (enabled && updateCommentAction(UiUtils.getOffsetAtMousePosition(codeArea))) { setNameAndDesc(updateComment ? NLS.str("popup.update_comment") : NLS.str("popup.add_comment")); setEnabled(true); } else { setEnabled(false); } } @Override public void popupMenuCanceled(PopupMenuEvent e) { actionComment = null; setEnabled(false); } private boolean updateCommentAction(int pos) { ICodeComment codeComment = getCommentRef(pos); if (codeComment == null) { actionComment = null; return false; } ICodeComment exitsComment = searchForExistComment(codeComment); if (exitsComment != null) { actionComment = exitsComment; updateComment = true; } else { actionComment = codeComment; updateComment = false; } return true; } @Override public void actionPerformed(ActionEvent e) { if (!enabled) { return; } if (JadxGuiAction.isSource(e)) { updateCommentAction(codeArea.getCaretPosition()); } if (actionComment == null) { UiUtils.showMessageBox(codeArea.getMainWindow(), NLS.str("msg.cant_add_comment")); return; } CommentDialog.show(codeArea, actionComment, updateComment); } protected @Nullable ICodeComment searchForExistComment(ICodeComment blankComment) { try { JadxProject project = codeArea.getProject(); JadxCodeData codeData = project.getCodeData(); if (codeData == null || codeData.getComments().isEmpty()) { return null; } for (ICodeComment comment : codeData.getComments()) { if (Objects.equals(comment.getNodeRef(), blankComment.getNodeRef()) && Objects.equals(comment.getCodeRef(), blankComment.getCodeRef())) { return comment; } } } catch (Exception e) { LOG.error("Error searching for exists comment", e); } return null; } /** * Check if possible insert comment at current line. * * @return blank code comment object (comment string empty) */ @Nullable protected ICodeComment getCommentRef(int pos) { if (pos == -1) { return null; } try { JadxWrapper wrapper = codeArea.getJadxWrapper(); ICodeInfo codeInfo = codeArea.getCodeInfo(); ICodeMetadata metadata = codeInfo.getCodeMetadata(); int lineStartPos = codeArea.getLineStartFor(pos); // add method line comment by instruction offset ICodeAnnotation offsetAnn = metadata.searchUp(pos, lineStartPos, AnnType.OFFSET); if (offsetAnn instanceof InsnCodeOffset) { JavaNode node = wrapper.getJavaNodeByRef(metadata.getNodeAt(pos)); if (node instanceof JavaMethod) { int rawOffset = ((InsnCodeOffset) offsetAnn).getOffset(); JadxNodeRef nodeRef = JadxNodeRef.forMth((JavaMethod) node); return new JadxCodeComment(nodeRef, JadxCodeRef.forInsn(rawOffset), ""); } } // check for definition at this line ICodeNodeRef nodeDef = metadata.searchUp(pos, (off, ann) -> { if (lineStartPos <= off && ann.getAnnType() == AnnType.DECLARATION) { ICodeNodeRef defRef = ((NodeDeclareRef) ann).getNode(); if (defRef.getAnnType() != AnnType.VAR) { return defRef; } } return null; }); if (nodeDef != null) { JadxNodeRef nodeRef = JadxNodeRef.forJavaNode(wrapper.getJavaNodeByRef(nodeDef)); return new JadxCodeComment(nodeRef, ""); } // check if at comment above node definition if (isCommentLine(pos)) { ICodeNodeRef nodeRef = metadata.searchDown(pos, (off, ann) -> { if (off > pos && ann.getAnnType() == AnnType.DECLARATION) { return ((NodeDeclareRef) ann).getNode(); } return null; }); if (nodeRef != null) { JavaNode defNode = wrapper.getJavaNodeByRef(nodeRef); return new JadxCodeComment(JadxNodeRef.forJavaNode(defNode), ""); } } } catch (Exception e) { LOG.error("Failed to add comment at: {}", pos, e); } return null; } /** * Check if all tokens are 'comment' in line at 'pos' */ protected boolean isCommentLine(int pos) { try { int line = codeArea.getLineOfOffset(pos); Token lineTokens = codeArea.getTokenListForLine(line); boolean commentFound = false; for (Token t = lineTokens; t != null; t = t.getNextToken()) { if (t.isComment()) { commentFound = true; } else { switch (t.getType()) { case TokenTypes.WHITESPACE: case TokenTypes.NULL: // allowed tokens break; default: return false; } } } return commentFound; } catch (Exception e) { LOG.warn("Failed to check for comment line", e); return false; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/ConvertNumberAction.java ================================================ package jadx.gui.ui.codearea; import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Consumer; import javax.swing.event.PopupMenuEvent; import javax.swing.text.BadLocationException; import org.fife.ui.rsyntaxtextarea.Token; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.data.CommentStyle; import jadx.api.data.ICodeComment; import jadx.api.data.impl.JadxCodeComment; import jadx.api.data.impl.JadxCodeData; import jadx.gui.settings.JadxProject; import jadx.gui.treemodel.JClass; import jadx.gui.ui.action.ActionModel; import jadx.gui.utils.NLS; public class ConvertNumberAction extends CommentAction { private static final Logger LOG = LoggerFactory.getLogger(ConvertNumberAction.class); private static final String DEFAULT_TEXT = ""; private final String tooltipText; public ConvertNumberAction(CodeArea codeArea) { super(ActionModel.CONVERT_NUMBER, codeArea); tooltipText = NLS.str("popup.convert_number"); // default menu item to disabled setEnabled(false); setNameAndDesc(DEFAULT_TEXT); } @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { if (codeArea.getNode() instanceof JClass) { // try parse number from word under caret // and set text of popup menu dynamically String word = getWordByPosition(codeArea.getCaretPosition()); List conversions = getConversionsFromWord(word); if (conversions != null && !conversions.isEmpty()) { String joined = String.join(" | ", conversions); setName(joined); setShortDescription(tooltipText); setEnabled(true); } } } @Override public void popupMenuCanceled(PopupMenuEvent e) { // reset menu to disabled on cancel setEnabled(false); setNameAndDesc(DEFAULT_TEXT); } @Override public void actionPerformed(ActionEvent e) { if (!super.enabled) { return; } String newText = e.getActionCommand(); if (newText == null) { return; } ICodeComment comment = getCommentRef(codeArea.getCaretPosition()); if (comment == null) { return; } ICodeComment newComment = new JadxCodeComment(comment.getNodeRef(), comment.getCodeRef(), newText, CommentStyle.LINE); updateCommentsData(codeArea, list -> list.add(newComment)); } /** * Adds comments to project file and code area */ private static void updateCommentsData(CodeArea codeArea, Consumer> updater) { try { JadxProject project = codeArea.getProject(); JadxCodeData codeData = project.getCodeData(); if (codeData == null) { codeData = new JadxCodeData(); } List list = new ArrayList<>(codeData.getComments()); updater.accept(list); Collections.sort(list); codeData.setComments(list); project.setCodeData(codeData); codeArea.getMainWindow().getWrapper().reloadCodeData(); } catch (Exception e) { LOG.error("Comment action failed", e); } try { // refresh code codeArea.backgroundRefreshClass(); } catch (Exception e) { LOG.error("Failed to reload code", e); } } /** * similar to AbstractCodeArea::getWordByPosition * but includes "-" for negative numbers */ public @Nullable String getWordByPosition(int offset) { Token token = codeArea.getWordTokenAtOffset(offset); if (token == null) { return null; } String str = token.getLexeme(); try { String prev = codeArea.getText(token.getOffset() - 1, 1); if (prev.equals("-")) { str = "-" + str; } } catch (BadLocationException e) { // ignore } int len = str.length(); if (len > 2 && str.startsWith("\"") && str.endsWith("\"")) { return str.substring(1, len - 1); } return str; } /** * Tries to parse a number from input string, * returns list of strings of the number converted to different formats. * e.g. if input number is in hex, converts to decimal and binary. */ static @Nullable List getConversionsFromWord(String word) { List conversions = new ArrayList<>(); if (word == null || word.isEmpty()) { return null; } int i32 = 0; long i64 = 0; int radix = 10; boolean parsedLong = false; // handle hex if (word.startsWith("0x")) { word = word.substring(2); radix = 16; } // handle long int syntax like "12345L" if (word.endsWith("L")) { word = word.substring(0, word.length() - 1); parsedLong = true; } // try parse int try { i32 = Integer.parseInt(word, radix); i64 = i32; } catch (NumberFormatException e) { // try parse long try { i64 = Long.parseLong(word, radix); parsedLong = true; } catch (NumberFormatException ignore) { return null; } } // if we parsed decimal, output hex and vice versa if (radix == 10) { if (parsedLong) { conversions.add(String.format("0x%x", i64)); } else { conversions.add(String.format("0x%x", i32)); } } else if (radix == 16) { conversions.add(String.format("%d", i32)); } // pad binary in 8-bit groups // int leadingZeros = parsed_long ? : Integer.numberOfLeadingZeros(i32); int padBits = (int) Math.ceil((64 - Long.numberOfLeadingZeros(i64)) / 8.0) * 8; if (padBits < 8) { padBits = 8; } if (!parsedLong && padBits > 32) { padBits = 32; } // format padded binary String binaryString = parsedLong ? Long.toBinaryString(i64) : Integer.toBinaryString(i32); String fmt = String.format("0b%%%ds", padBits); conversions.add(String.format(fmt, binaryString).replace(' ', '0')); // format printable ascii chars if (i32 >= ' ' && i32 <= '~') { conversions.add(String.format("'%c'", (int) i32)); } return conversions; // no match } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/EditorViewState.java ================================================ package jadx.gui.ui.codearea; import java.awt.Point; import jadx.gui.treemodel.JNode; public class EditorViewState { public static final Point ZERO = new Point(0, 0); private final JNode node; private int caretPos; private Point viewPoint; private String subPath; private boolean active; private boolean pinned; private boolean bookmarked; private boolean hidden; private boolean previewTab; public EditorViewState(JNode node) { this(node, "", 0, EditorViewState.ZERO); } public EditorViewState(JNode node, String subPath, int caretPos, Point viewPoint) { this.node = node; this.subPath = subPath; this.caretPos = caretPos; this.viewPoint = viewPoint; } public JNode getNode() { return node; } public int getCaretPos() { return caretPos; } public void setCaretPos(int caretPos) { this.caretPos = caretPos; } public Point getViewPoint() { return viewPoint; } public void setViewPoint(Point viewPoint) { this.viewPoint = viewPoint; } public String getSubPath() { return subPath; } public void setSubPath(String subPath) { this.subPath = subPath; } public boolean isActive() { return active; } public void setActive(boolean active) { this.active = active; } public boolean isPinned() { return pinned; } public void setPinned(boolean pinned) { this.pinned = pinned; } public boolean isBookmarked() { return bookmarked; } public void setBookmarked(boolean bookmarked) { this.bookmarked = bookmarked; } public boolean isHidden() { return hidden; } public boolean isPreviewTab() { return previewTab; } public void setPreviewTab(boolean previewTab) { this.previewTab = previewTab; } public void setHidden(boolean hidden) { this.hidden = hidden; } @Override public String toString() { return "EditorViewState{node=" + node + ", caretPos=" + caretPos + ", viewPoint=" + viewPoint + ", subPath='" + subPath + '\'' + ", active=" + active + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java ================================================ package jadx.gui.ui.codearea; import javax.swing.JMenu; import javax.swing.JPopupMenu; import javax.swing.event.PopupMenuListener; import jadx.gui.ui.action.JNodeAction; import jadx.gui.ui.action.JadxGuiAction; import jadx.gui.utils.shortcut.ShortcutsController; public class JNodePopupBuilder { private final JPopupMenu menu; private final JNodePopupListener popupListener; private final ShortcutsController shortcutsController; public JNodePopupBuilder(CodeArea codeArea, JPopupMenu popupMenu, ShortcutsController shortcutsController) { this.shortcutsController = shortcutsController; menu = popupMenu; popupListener = new JNodePopupListener(codeArea); popupMenu.addPopupMenuListener(popupListener); } public void addSeparator() { menu.addSeparator(); } public void add(JNodeAction nodeAction) { // We set the shortcut immediately for two reasons // - there might be multiple instances of this action with // same ActionModel across different codeAreas, while // ShortcutController only supports one instance // - This action will be recreated when shortcuts are changed, // so no need to bind it if (nodeAction.getActionModel() != null) { shortcutsController.bindImmediate(nodeAction); } menu.add(nodeAction); popupListener.addActions(nodeAction); } public void addSubmenu(JNodeAction[] nodeActions, String name) { JMenu submenu = new JMenu(name); for (JNodeAction nodeAction : nodeActions) { if (nodeAction.getActionModel() != null) { shortcutsController.bindImmediate(nodeAction); } submenu.add(nodeAction); popupListener.addActions(nodeAction); } menu.add(submenu); } public void add(JadxGuiAction action) { if (action.getActionModel() != null) { shortcutsController.bindImmediate(action); } menu.add(action); if (action instanceof PopupMenuListener) { menu.addPopupMenuListener((PopupMenuListener) action); } } public JPopupMenu getMenu() { return menu; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupListener.java ================================================ package jadx.gui.ui.codearea; import java.util.ArrayList; import java.util.List; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import jadx.gui.treemodel.JNode; import jadx.gui.ui.action.JNodeAction; public final class JNodePopupListener implements PopupMenuListener { private final CodeArea codeArea; private final List actions = new ArrayList<>(); public JNodePopupListener(CodeArea codeArea) { this.codeArea = codeArea; } public void addActions(JNodeAction action) { actions.add(action); } private void updateNode(JNode node) { for (JNodeAction action : actions) { action.changeNode(node); } } @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { updateNode(codeArea.getNodeUnderMouse()); } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { // this event can be called just before running action, so can't reset node here } @Override public void popupMenuCanceled(PopupMenuEvent e) { updateNode(null); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/JadxTokenMaker.java ================================================ package jadx.gui.ui.codearea; import java.util.Set; import javax.swing.text.Segment; import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rsyntaxtextarea.TokenImpl; import org.fife.ui.rsyntaxtextarea.TokenTypes; import org.fife.ui.rsyntaxtextarea.modes.JavaTokenMaker; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JavaClass; import jadx.api.JavaNode; import static jadx.api.plugins.utils.Utils.constSet; public final class JadxTokenMaker extends JavaTokenMaker { private static final Logger LOG = LoggerFactory.getLogger(JadxTokenMaker.class); private final CodeArea codeArea; public JadxTokenMaker(CodeArea codeArea) { this.codeArea = codeArea; } @Override public Token getTokenList(Segment text, int initialTokenType, int startOffset) { if (codeArea.isDisposed()) { return new TokenImpl(); } try { Token tokens = super.getTokenList(text, initialTokenType, startOffset); if (tokens != null && tokens.getType() != TokenTypes.NULL) { processTokens(tokens); } return tokens; } catch (Throwable e) { // JavaTokenMaker throws 'java.lang.Error' if failed to parse input string LOG.error("Process tokens failed for text: {}", text, e); return new TokenImpl(); } } private void processTokens(Token tokens) { Token prev = null; Token current = tokens; while (current != null && current.getType() != TokenTypes.NULL) { if (prev != null) { switch (current.getType()) { case TokenTypes.RESERVED_WORD: fixContextualKeyword(current); break; case TokenTypes.IDENTIFIER: current = mergeLongClassNames(prev, current, false); break; case TokenTypes.ANNOTATION: current = mergeLongClassNames(prev, current, true); break; } } prev = current; current = current.getNextToken(); } } private static final Set CONTEXTUAL_KEYWORDS = constSet( "exports", "module", "non-sealed", "open", "opens", "permits", "provides", "record", "requires", "sealed", "to", "transitive", "uses", "var", "with", "yield"); private static void fixContextualKeyword(Token token) { String lexeme = token.getLexeme(); // TODO: create new string every call, better to avoid if (lexeme != null && CONTEXTUAL_KEYWORDS.contains(lexeme)) { token.setType(TokenTypes.IDENTIFIER); } } @NotNull private Token mergeLongClassNames(Token prev, Token current, boolean annotation) { int offset = current.getTextOffset(); if (annotation) { offset++; } JavaClass javaCls = codeArea.getJavaClassIfAtPos(offset); if (javaCls != null) { String name = javaCls.getName(); String lexeme = current.getLexeme(); if (annotation && lexeme.length() > 1) { lexeme = lexeme.substring(1); } if (!lexeme.equals(name) && isClassNameStart(javaCls, lexeme)) { // try to replace long class name with one token Token replace = concatTokensUntil(current, name); if (replace != null && prev instanceof TokenImpl) { TokenImpl impl = ((TokenImpl) prev); impl.setNextToken(replace); current = replace; } } } return current; } private boolean isClassNameStart(JavaNode javaNode, String lexeme) { if (javaNode.getFullName().startsWith(lexeme)) { // full class name return true; } if (javaNode.getTopParentClass().getName().startsWith(lexeme)) { // inner class references from parent class return true; } return false; } @Nullable private Token concatTokensUntil(Token start, String endText) { StringBuilder sb = new StringBuilder(); Token current = start; while (current != null && current.getType() != TokenTypes.NULL) { String text = current.getLexeme(); if (text != null) { sb.append(text); if (text.equals(endText)) { char[] line = sb.toString().toCharArray(); TokenImpl token = new TokenImpl(line, 0, line.length - 1, start.getOffset(), start.getType(), start.getLanguageIndex()); token.setNextToken(current.getNextToken()); return token; } } current = current.getNextToken(); } return null; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/MouseHoverHighlighter.java ================================================ package jadx.gui.ui.codearea; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import javax.swing.text.Caret; import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rtextarea.SmartHighlightPainter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JavaNode; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.utils.JNodeCache; class MouseHoverHighlighter extends MouseMotionAdapter { private static final Logger LOG = LoggerFactory.getLogger(MouseHoverHighlighter.class); private final CodeArea codeArea; private final CodeLinkGenerator codeLinkGenerator; private final SmartHighlightPainter highlighter; private Object tag; private int highlightedTokenOffset = -1; public MouseHoverHighlighter(CodeArea codeArea, CodeLinkGenerator codeLinkGenerator) { this.codeArea = codeArea; this.codeLinkGenerator = codeLinkGenerator; this.highlighter = new SmartHighlightPainter(); loadSettings(); } public void loadSettings() { highlighter.setPaint(codeArea.getMarkOccurrencesColor()); } @Override public void mouseMoved(MouseEvent e) { if (!addHighlight(e)) { removeHighlight(); } } private boolean addHighlight(MouseEvent e) { if (e.getModifiersEx() != 0) { return false; } Caret caret = codeArea.getCaret(); if (caret.getDot() != caret.getMark()) { // selection in action, highlight will interfere with selection return false; } try { Token token = codeArea.viewToToken(e.getPoint()); if (token == null) { return false; } int tokenOffset = token.getOffset(); if (tokenOffset == highlightedTokenOffset) { // don't repaint highlight return true; } JavaNode nodeAtOffset = codeLinkGenerator.getNodeAtOffset(tokenOffset); if (nodeAtOffset == null) { return false; } removeHighlight(); tag = codeArea.getHighlighter().addHighlight(tokenOffset, token.getEndOffset(), this.highlighter); highlightedTokenOffset = tokenOffset; updateToolTip(nodeAtOffset); return true; } catch (Exception exc) { LOG.error("Mouse hover highlight error", exc); return false; } } private void removeHighlight() { if (tag != null) { codeArea.getHighlighter().removeHighlight(tag); tag = null; highlightedTokenOffset = -1; updateToolTip(null); } } private void updateToolTip(JavaNode node) { MainWindow mainWindow = codeArea.getMainWindow(); if (node == null || mainWindow.getSettings().isDisableTooltipOnHover()) { codeArea.setToolTipText(null); return; } JNodeCache nodeCache = mainWindow.getCacheObject().getNodeCache(); JNode jNode = nodeCache.makeFrom(node); codeArea.setToolTipText(jNode.getTooltip()); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/SearchBar.java ================================================ package jadx.gui.ui.codearea; import java.awt.Color; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JTextField; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.border.EmptyBorder; import javax.swing.text.BadLocationException; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rtextarea.SearchContext; import org.fife.ui.rtextarea.SearchEngine; import org.fife.ui.rtextarea.SearchResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.formdev.flatlaf.FlatClientProperties; import jadx.core.utils.StringUtils; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; public class SearchBar extends JToolBar { private static final long serialVersionUID = 1836871286618633003L; private static final Logger LOG = LoggerFactory.getLogger(SearchBar.class); private static final int MAX_RESULT_COUNT = 999; private final RSyntaxTextArea rTextArea; private final JTextField searchField; private final JLabel resultCountLabel; private final JToggleButton markAllCB; private final JToggleButton regexCB; private final JToggleButton wholeWordCB; private final JToggleButton matchCaseCB; private boolean notFound; public SearchBar(RSyntaxTextArea textArea) { rTextArea = textArea; JLabel findLabel = new JLabel(NLS.str("search.find") + ':'); add(findLabel); searchField = new JTextField(30); searchField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true); searchField.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_ENTER: // skip break; case KeyEvent.VK_ESCAPE: toggle(); break; default: search(0); break; } } }); searchField.addActionListener(e -> search(1)); TextStandardActions.attach(searchField); add(searchField); ActionListener forwardListener = e -> search(1); resultCountLabel = new JLabel(); resultCountLabel.setBorder(new EmptyBorder(0, 10, 0, 10)); resultCountLabel.setForeground(Color.GRAY); add(resultCountLabel); setResultCount(0); matchCaseCB = new JToggleButton(); matchCaseCB.setIcon(Icons.ICON_MATCH); matchCaseCB.setSelectedIcon(Icons.ICON_MATCH_SELECTED); matchCaseCB.setToolTipText(NLS.str("search.match_case")); matchCaseCB.addActionListener(forwardListener); add(matchCaseCB); wholeWordCB = new JToggleButton(); wholeWordCB.setIcon(Icons.ICON_WORDS); wholeWordCB.setSelectedIcon(Icons.ICON_WORDS_SELECTED); wholeWordCB.setToolTipText(NLS.str("search.whole_word")); wholeWordCB.addActionListener(forwardListener); add(wholeWordCB); regexCB = new JToggleButton(); regexCB.setIcon(Icons.ICON_REGEX); regexCB.setSelectedIcon(Icons.ICON_REGEX_SELECTED); regexCB.setToolTipText(NLS.str("search.regex")); regexCB.addActionListener(forwardListener); add(regexCB); JButton prevButton = new JButton(); prevButton.setIcon(Icons.ICON_UP); prevButton.setToolTipText(NLS.str("search.previous")); prevButton.addActionListener(e -> search(-1)); prevButton.setBorderPainted(false); add(prevButton); JButton nextButton = new JButton(); nextButton.setIcon(Icons.ICON_DOWN); nextButton.setToolTipText(NLS.str("search.next")); nextButton.addActionListener(e -> search(1)); nextButton.setBorderPainted(false); add(nextButton); markAllCB = new JToggleButton(); markAllCB.setIcon(Icons.ICON_MARK); markAllCB.setSelectedIcon(Icons.ICON_MARK_SELECTED); markAllCB.setToolTipText(NLS.str("search.mark_all")); markAllCB.addActionListener(forwardListener); add(markAllCB); JButton closeButton = new JButton(); closeButton.setIcon(Icons.ICON_CLOSE); closeButton.addActionListener(e -> toggle()); closeButton.setBorderPainted(false); add(closeButton); setFloatable(false); setVisible(false); } /* * Replicates IntelliJ's search bar behavior * 1.1. If the user has selected text, use that as the search text * 1.2. Otherwise, use the previous search text (or empty if none) * 2. Select all text in the search bar and give it focus */ public void showAndFocus() { setVisible(true); String selectedText = rTextArea.getSelectedText(); if (!StringUtils.isEmpty(selectedText)) { searchField.setText(selectedText); } searchField.selectAll(); searchField.requestFocus(); } public void toggle() { boolean visible = !isVisible(); setVisible(visible); if (visible) { String preferText = rTextArea.getSelectedText(); if (!StringUtils.isEmpty(preferText)) { searchField.setText(preferText); } searchField.selectAll(); searchField.requestFocus(); } else { rTextArea.requestFocus(); } } private void search(int direction) { String searchText = searchField.getText(); if (searchText == null || searchText.isEmpty() || rTextArea.getText() == null) { setResultCount(0); return; } boolean forward = direction >= 0; boolean matchCase = matchCaseCB.isSelected(); boolean regex = regexCB.isSelected(); boolean wholeWord = wholeWordCB.isSelected(); SearchContext context = new SearchContext(); context.setSearchFor(searchText); context.setMatchCase(matchCase); context.setRegularExpression(regex); context.setSearchForward(forward); context.setWholeWord(wholeWord); context.setSearchWrap(true); // We enable Mark All even if the corresponding toggle button is off, // this is a bit hackish, but it's the only way to count matches through SearchEngine context.setMarkAll(true); // TODO hack: move cursor before previous search for not jump to next occurrence if (direction == 0 && !notFound) { try { int caretPos = rTextArea.getCaretPosition(); int lineNum = rTextArea.getLineOfOffset(caretPos) - 1; if (lineNum > 1) { rTextArea.setCaretPosition(rTextArea.getLineStartOffset(lineNum)); } } catch (BadLocationException e) { LOG.error("Caret move error", e); } } SearchResult result = SearchEngine.find(rTextArea, context); setResultCount(result.getMarkedCount()); // Clear the highlighted results if Mark All is disabled if (!markAllCB.isSelected()) { context.setMarkAll(false); SearchEngine.markAll(rTextArea, context); } notFound = !result.wasFound(); if (notFound) { searchField.putClientProperty("JComponent.outline", "error"); } else { searchField.putClientProperty("JComponent.outline", ""); } searchField.repaint(); } private void setResultCount(int count) { boolean exceedsLimit = count > MAX_RESULT_COUNT; String plusSign = exceedsLimit ? "+" : ""; count = exceedsLimit ? MAX_RESULT_COUNT : count; resultCountLabel.setText(NLS.str("search.results", plusSign, count)); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/SimpleTokenMaker.java ================================================ package jadx.gui.ui.codearea; import javax.swing.text.Segment; import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rsyntaxtextarea.TokenImpl; import org.fife.ui.rsyntaxtextarea.TokenMakerBase; import org.fife.ui.rsyntaxtextarea.TokenTypes; /** * Very simple token maker to use only one token per line without any parsing */ @SuppressWarnings("unused") // class registered by name in {@link AbstractCodeArea} public class SimpleTokenMaker extends TokenMakerBase { private final TokenImpl token; public SimpleTokenMaker() { token = new TokenImpl(); token.setType(TokenTypes.IDENTIFIER); } @Override public Token getTokenList(Segment segment, int initialTokenType, int startOffset) { token.text = segment.array; token.textOffset = startOffset; token.textCount = segment.count; token.setOffset(startOffset); return token; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java ================================================ package jadx.gui.ui.codearea; import java.awt.Color; import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.beans.PropertyChangeListener; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.AbstractAction; import javax.swing.Icon; import javax.swing.JCheckBoxMenuItem; import javax.swing.KeyStroke; import javax.swing.text.BadLocationException; import javax.swing.text.EditorKit; import javax.swing.text.JTextComponent; import org.fife.ui.rsyntaxtextarea.FoldingAwareIconRowHeader; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit; import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaUI; import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities; import org.fife.ui.rsyntaxtextarea.Style; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.fife.ui.rsyntaxtextarea.SyntaxScheme; import org.fife.ui.rtextarea.Gutter; import org.fife.ui.rtextarea.GutterIconInfo; import org.fife.ui.rtextarea.IconRowHeader; import org.fife.ui.rtextarea.RTextArea; import org.fife.ui.rtextarea.RTextAreaUI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.gui.device.debugger.BreakpointManager; import jadx.gui.device.debugger.DbgUtils; import jadx.gui.jobs.IBackgroundTask; import jadx.gui.jobs.LoadTask; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.TextNode; import jadx.gui.ui.codearea.sync.CodePanelSyncee; import jadx.gui.ui.codearea.sync.CodePanelSyncer; import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory; import jadx.gui.ui.codearea.sync.SmaliSyncer; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public final class SmaliArea extends AbstractCodeArea implements CodePanelSyncerAbstractFactory, CodePanelSyncee { private static final Logger LOG = LoggerFactory.getLogger(SmaliArea.class); private static final long serialVersionUID = 1334485631870306494L; private static final Icon ICON_BREAKPOINT = UiUtils.openSvgIcon("debugger/db_set_breakpoint"); private static final Icon ICON_BREAKPOINT_DISABLED = UiUtils.openSvgIcon("debugger/db_disabled_breakpoint"); private static final Color BREAKPOINT_LINE_COLOR = Color.decode("#ad103c"); private static final Color DEBUG_LINE_COLOR = Color.decode("#9c1138"); private final JNode textNode; private final JCheckBoxMenuItem cbUseSmaliV2; private final boolean allowToggleV2 = false; // add to constructor args to change back private final boolean initialDisplayV2; private boolean curVersion = false; private SmaliModel model; SmaliArea(ContentPanel contentPanel, JClass node, boolean initialDisplayV2) { super(contentPanel, node); this.textNode = new TextNode(node.getName()); this.initialDisplayV2 = initialDisplayV2; setCodeFoldingEnabled(true); cbUseSmaliV2 = new JCheckBoxMenuItem(NLS.str("popup.bytecode_col"), shouldUseSmaliPrinterV2()); cbUseSmaliV2.setAction(new AbstractAction(NLS.str("popup.bytecode_col")) { private static final long serialVersionUID = -1111111202103170737L; @Override public void actionPerformed(ActionEvent e) { JadxSettings settings = getContentPanel().getMainWindow().getSettings(); settings.setSmaliAreaShowBytecode(!settings.isSmaliAreaShowBytecode()); contentPanel.getTabbedPane().getTabs().forEach(v -> { if (v instanceof ClassCodeContentPanel) { switchModel(); ((ClassCodeContentPanel) v).getSmaliCodeArea().refresh(); } }); settings.sync(); } }); if (allowToggleV2) { getPopupMenu().add(cbUseSmaliV2); } switchModel(); } @Override public IBackgroundTask getLoadTask() { return new LoadTask<>( () -> model.loadCode(), code -> { curVersion = shouldUseSmaliPrinterV2(); model.loadUI(code); setCaretPosition(0); setLoaded(); }); } @Override public ICodeInfo getCodeInfo() { return ICodeInfo.EMPTY; } @Override public void refresh() { load(); } @Override public JNode getNode() { // this area contains only smali without other node attributes return textNode; } public boolean isShowingDalvikBytecode() { return model instanceof DebugModel; } public JClass getJClass() { return (JClass) node; } private void switchModel() { if (model != null) { model.unload(); } curVersion = shouldUseSmaliPrinterV2(); model = curVersion ? new DebugModel() : new NormalModel(this); setUnLoaded(); load(); } public void scrollToDebugPos(int pos) { // don't sync when it's set programmatically. getContentPanel().getMainWindow().getSettings().setSmaliAreaShowBytecode(true); cbUseSmaliV2.setState(shouldUseSmaliPrinterV2()); if (!(model instanceof DebugModel)) { switchModel(); refresh(); } model.togglePosHighlight(pos); } @Override public Font getFont() { if (model == null || isDisposed()) { return super.getFont(); } return model.getFont(); } @Override public Font getFontForTokenType(int type) { return getFont(); } private boolean shouldUseSmaliPrinterV2() { return getContentPanel().getMainWindow().getSettings().isSmaliAreaShowBytecode(); } private abstract class SmaliModel { abstract String loadCode(); abstract void loadUI(String code); abstract void unload(); Font getFont() { return SmaliArea.super.getFont(); } Font getFontForTokenType(int type) { return SmaliArea.super.getFontForTokenType(type); } void setBreakpoint(int off) { } void togglePosHighlight(int pos) { } } private class NormalModel extends SmaliModel { public NormalModel(SmaliArea smaliArea) { getContentPanel().getMainWindow().getEditorThemeManager().apply(smaliArea); setSyntaxEditingStyle(SYNTAX_STYLE_SMALI); } @Override public String loadCode() { return getJClass().getSmali(); } @Override public void loadUI(String code) { setText(code); } @Override public void unload() { } } private class DebugModel extends SmaliModel { private KeyStroke bpShortcut; private Gutter gutter; private Object runningHighlightTag = null; // running line private final SmaliV2Style smaliV2Style = new SmaliV2Style(SmaliArea.this); private final Map bpMap = new HashMap<>(); private final PropertyChangeListener schemeListener = evt -> { if (smaliV2Style.refreshTheme()) { setSyntaxScheme(smaliV2Style); } }; public DebugModel() { loadV2Style(); setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_ASSEMBLER_6502); addPropertyChangeListener(SYNTAX_SCHEME_PROPERTY, schemeListener); regBreakpointEvents(); } @Override String loadCode() { return DbgUtils.getSmaliCode(((JClass) node).getCls().getClassNode()); } @Override void loadUI(String code) { if (gutter == null) { gutter = RSyntaxUtilities.getGutter(SmaliArea.this); gutter.setBookmarkingEnabled(true); gutter.setIconRowHeaderInheritsGutterBackground(true); Font baseFont = SmaliArea.super.getFont(); gutter.setLineNumberFont(baseFont.deriveFont(baseFont.getSize2D() - 1.0f)); } setText(code); loadV2Style(); loadBreakpoints(); } @Override public void unload() { removePropertyChangeListener(schemeListener); removeLineHighlight(runningHighlightTag); UiUtils.removeKeyBinding(SmaliArea.this, bpShortcut, "set a break point"); BreakpointManager.removeListener((JClass) node); bpMap.forEach((k, v) -> v.remove()); } @Override public Font getFont() { return smaliV2Style.getFont(); } @Override public Font getFontForTokenType(int type) { return smaliV2Style.getFont(); } private void loadV2Style() { setSyntaxScheme(smaliV2Style); } private void regBreakpointEvents() { bpShortcut = KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0); UiUtils.addKeyBinding(SmaliArea.this, bpShortcut, "set break point", new AbstractAction() { private static final long serialVersionUID = -1111111202103170738L; @Override public void actionPerformed(ActionEvent e) { setBreakpoint(getCaretPosition()); } }); BreakpointManager.addListener((JClass) node, this::setBreakpointDisabled); } private void loadBreakpoints() { List posList = BreakpointManager.getPositions((JClass) node); for (Integer integer : posList) { setBreakpoint(integer); } } @Override public void setBreakpoint(int pos) { int line; try { line = getLineOfOffset(pos); } catch (BadLocationException e) { LOG.error("Failed to get line by offset: {}", pos, e); return; } BreakpointLine bpLine = bpMap.remove(line); if (bpLine == null) { bpLine = new BreakpointLine(line); bpLine.setDisabled(false); bpMap.put(line, bpLine); if (!BreakpointManager.set((JClass) node, line)) { bpLine.setDisabled(true); } } else { BreakpointManager.remove((JClass) node, line); bpLine.remove(); } } @Override public void togglePosHighlight(int pos) { if (runningHighlightTag != null) { removeLineHighlight(runningHighlightTag); } try { int line = getLineOfOffset(pos); runningHighlightTag = addLineHighlight(line, DEBUG_LINE_COLOR); } catch (BadLocationException e) { LOG.error("Failed to get line by offset: {}", pos, e); } } private void setBreakpointDisabled(int pos) { try { int line = getLineOfOffset(pos); bpMap.computeIfAbsent(line, k -> new BreakpointLine(line)).setDisabled(true); } catch (BadLocationException e) { LOG.error("Failed to get line by offset: {}", pos, e); } } private class SmaliV2Style extends SyntaxScheme { public SmaliV2Style(SmaliArea smaliArea) { super(true); smaliArea.getContentPanel().getMainWindow().getEditorThemeManager().apply(smaliArea); updateTheme(); } public Font getFont() { return getContentPanel().getMainWindow().getSettings().getSmaliFont(); } public boolean refreshTheme() { boolean refresh = getSyntaxScheme() != this; if (refresh) { updateTheme(); } return refresh; } private void updateTheme() { Style[] mainStyles = getSyntaxScheme().getStyles(); Style[] styles = new Style[mainStyles.length]; for (int i = 0; i < mainStyles.length; i++) { Style mainStyle = mainStyles[i]; if (mainStyle == null) { styles[i] = new Style(); } else { // font will be hijacked by getFont & getFontForTokenType, // so it doesn't need to be set here. styles[i] = new Style(mainStyle.foreground, mainStyle.background, null); } } setStyles(styles); } @Override public void restoreDefaults(Font baseFont) { restoreDefaults(baseFont, true); } @Override public void restoreDefaults(Font baseFont, boolean fontStyles) { // Note: it's a hook for continuing using the editor theme, better don't remove it. } } private class BreakpointLine { Object highlightTag; GutterIconInfo iconInfo; boolean disabled; final int line; BreakpointLine(int line) { this.line = line; this.disabled = true; } void remove() { safeRemoveTrackingIcon(iconInfo); if (!this.disabled) { removeLineHighlight(highlightTag); } } void setDisabled(boolean disabled) { if (disabled) { if (!this.disabled) { safeRemoveTrackingIcon(iconInfo); removeLineHighlight(highlightTag); try { iconInfo = gutter.addLineTrackingIcon(line, ICON_BREAKPOINT_DISABLED); } catch (BadLocationException e) { LOG.error("Failed to add line tracking icon", e); } } } else { if (this.disabled) { safeRemoveTrackingIcon(this.iconInfo); try { iconInfo = gutter.addLineTrackingIcon(line, ICON_BREAKPOINT); highlightTag = addLineHighlight(line, BREAKPOINT_LINE_COLOR); } catch (BadLocationException e) { LOG.error("Failed to remove line tracking icon", e); } } } this.disabled = disabled; } } private void safeRemoveTrackingIcon(GutterIconInfo iconInfo) { if (gutter != null && iconInfo != null) { gutter.removeTrackingIcon(iconInfo); } } } @Override protected RTextAreaUI createRTextAreaUI() { // IconRowHeader won't fire an event when people click on it for adding/removing icons, // so our poor breakpoints won't be set if we don't hijack IconRowHeader. return new RSyntaxTextAreaUI(this) { @Override public EditorKit getEditorKit(JTextComponent tc) { return new RSyntaxTextAreaEditorKit() { private static final long serialVersionUID = -1111111202103170740L; @Override public IconRowHeader createIconRowHeader(RTextArea textArea) { return new FoldingAwareIconRowHeader((RSyntaxTextArea) textArea) { private static final long serialVersionUID = -1111111202103170739L; @Override public void mousePressed(MouseEvent e) { int offs = textArea.viewToModel2D(e.getPoint()); if (offs > -1) { model.setBreakpoint(offs); } } }; } }; } }; } @Override public CodePanelSyncer createCodePanelSyncer() { return new SmaliSyncer(this); } @Override public boolean sync(CodePanelSyncer codePanelSyncer) { return codePanelSyncer.syncTo(this); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliFoldParser.java ================================================ package jadx.gui.ui.codearea; import java.util.ArrayList; import java.util.List; import java.util.NavigableSet; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.folding.Fold; import org.fife.ui.rsyntaxtextarea.folding.FoldParser; import org.fife.ui.rsyntaxtextarea.folding.FoldParserManager; import org.fife.ui.rsyntaxtextarea.folding.FoldType; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SmaliFoldParser implements FoldParser { private static final Logger LOG = LoggerFactory.getLogger(SmaliFoldParser.class); private static final Pattern CLASS_LINE_PATTERN = Pattern.compile("^\\.class\\b", Pattern.MULTILINE); private static final Pattern ENDMETHOD_LINE_PATTERN = Pattern.compile("^\\.end method\\b", Pattern.MULTILINE); private static final Pattern STARTMETHOD_LINE_PATTERN = Pattern.compile("^\\.method\\b", Pattern.MULTILINE); public static void register() { FoldParserManager.get().addFoldParserMapping(AbstractCodeArea.SYNTAX_STYLE_SMALI, new SmaliFoldParser()); } private SmaliFoldParser() { } @Override public List getFolds(RSyntaxTextArea textArea) { List classFolds = new ArrayList<>(); String text = textArea.getText(); List classStartOffsets = getClassStartOffsets(text); NavigableSet startMethodStartOffsets = getStartMethodStartOffsets(text); NavigableSet endMethodEndOffsets = getEndMethodEndOffsets(text); for (int i = 0; i < classStartOffsets.size(); i++) { // Start offset of .class int startOffset = classStartOffsets.get(i); int classLimit; if (i < classStartOffsets.size() - 1) { classLimit = classStartOffsets.get(i + 1); } else { classLimit = text.length(); } // Get the last ".end method" before next .class or end of file Integer endOffset = endMethodEndOffsets.floor(classLimit); if (endOffset != null) { Fold classFold = createFold(textArea, startOffset, endOffset); if (classFold != null) { classFolds.add(classFold); // Start looking for .method after .class definition Integer startMethodStartOffset = startMethodStartOffsets.ceiling(startOffset); while (startMethodStartOffset != null && startMethodStartOffset < endOffset) { Integer endMethodEndOffset = endMethodEndOffsets.ceiling(startMethodStartOffset); if (endMethodEndOffset != null) { addFold(classFold, startMethodStartOffset, endMethodEndOffset); } // Look for next .method starting from last .end method startMethodStartOffset = startMethodStartOffsets.ceiling(endMethodEndOffset); } } } } return classFolds; } private static @Nullable Fold createFold(RSyntaxTextArea textArea, int startOffset, int endOffset) { try { Fold fold = new Fold(FoldType.CODE, textArea, startOffset); fold.setEndOffset(endOffset); return fold; } catch (Exception e) { LOG.error("Failed to create code fold", e); return null; } } private static void addFold(Fold parent, int startOffset, int endOffset) { try { Fold fold = parent.createChild(FoldType.CODE, startOffset); fold.setEndOffset(endOffset); } catch (Exception e) { LOG.error("Failed to add code fold", e); } } private List getClassStartOffsets(String text) { List startOffsets = new ArrayList<>(); Matcher matcher = CLASS_LINE_PATTERN.matcher(text); while (matcher.find()) { int startOffset = matcher.start(); startOffsets.add(startOffset); } return startOffsets; } private NavigableSet getStartMethodStartOffsets(String text) { NavigableSet startOffsets = new TreeSet<>(); Matcher matcher = STARTMETHOD_LINE_PATTERN.matcher(text); while (matcher.find()) { int startOffset = matcher.start(); startOffsets.add(startOffset); } return startOffsets; } private NavigableSet getEndMethodEndOffsets(String text) { NavigableSet endOffsets = new TreeSet<>(); Matcher matcher = ENDMETHOD_LINE_PATTERN.matcher(text); while (matcher.find()) { int endOffset = matcher.end(); endOffsets.add(endOffset); } return endOffsets; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliTokenMaker.java ================================================ /* The following code was generated by JFlex */ package jadx.gui.ui.codearea; import java.io.*; import javax.swing.text.Segment; import org.fife.ui.rsyntaxtextarea.*; /** * SmaliTokenMaker * MartinKay@qq.com */ @SuppressWarnings("checkstyle:all") public class SmaliTokenMaker extends AbstractJFlexCTokenMaker { /** This character denotes the end of file */ public static final int YYEOF = -1; /** initial size of the lookahead buffer */ private static final int ZZ_BUFFERSIZE = 16384; /** lexical states */ public static final int EOL_COMMENT = 1; public static final int YYINITIAL = 0; /** * Translates characters to character classes */ private static final String ZZ_CMAP_PACKED = "\11\0\1\21\1\10\1\0\1\21\1\17\22\0\1\77\1\54\1\15" + "\1\20\1\1\1\35\1\35\1\7\1\37\1\37\1\35\1\40\1\35" + "\1\25\1\23\1\41\1\4\1\66\1\16\1\72\1\71\1\6\1\67" + "\1\6\1\76\1\3\1\45\1\50\1\55\1\54\1\56\1\35\1\36" + "\1\5\1\53\1\53\1\53\1\5\1\53\2\1\1\52\1\52\1\1" + "\1\47\6\1\1\52\2\1\1\51\3\1\1\52\1\37\1\11\1\37" + "\1\17\1\2\1\0\1\31\1\14\1\60\1\61\1\24\1\30\1\62" + "\1\42\1\44\1\70\1\73\1\32\1\65\1\13\1\63\1\43\1\74" + "\1\27\1\33\1\26\1\12\1\57\1\46\1\22\1\64\1\75\1\34" + "\1\17\1\34\1\35\uff81\0"; /** * Translates characters to character classes */ private static final char[] ZZ_CMAP = zzUnpackCMap(ZZ_CMAP_PACKED); /** * Translates DFA states to action switch labels. */ private static final int[] ZZ_ACTION = zzUnpackAction(); private static final String ZZ_ACTION_PACKED_0 = "\2\0\2\1\2\2\1\3\1\4\3\1\1\5\1\6" + "\1\7\12\1\1\10\1\11\1\12\4\1\2\13\1\12" + "\5\1\1\14\1\15\3\14\1\0\1\16\1\0\2\16" + "\1\3\1\17\1\0\1\3\5\1\2\5\1\20\1\21" + "\12\0\1\1\1\10\23\1\1\12\5\1\6\0\1\13" + "\1\0\15\1\5\0\1\21\1\0\1\22\1\3\1\23" + "\2\3\1\17\1\3\5\1\1\24\1\1\1\5\1\25" + "\1\5\20\0\35\1\7\0\10\1\1\0\2\1\5\0" + "\1\3\2\0\1\1\1\0\1\1\1\5\22\0\1\26" + "\1\27\3\1\1\0\7\1\1\24\1\1\1\0\2\1" + "\1\0\6\1\1\0\2\1\11\0\4\1\1\0\3\1" + "\1\24\2\0\1\1\1\24\2\0\1\30\1\0\1\3" + "\6\0\1\1\1\5\6\0\1\31\13\0\2\1\3\0" + "\2\1\1\0\3\1\3\0\2\1\1\0\6\1\1\0" + "\2\1\1\24\5\0\3\1\1\24\1\0\2\1\3\0" + "\1\1\5\0\1\3\6\0\1\5\7\0\1\31\4\0" + "\1\31\1\1\1\24\4\0\1\1\1\0\2\1\10\0" + "\1\1\1\0\3\1\1\0\2\1\1\24\3\0\1\32" + "\2\1\2\0\1\1\1\0\2\1\3\0\1\24\1\1" + "\23\0\1\1\7\0\2\1\10\0\1\24\1\1\1\24" + "\1\0\2\1\1\0\1\1\11\0\1\1\1\0\1\1" + "\2\0\1\1\22\0\1\24\3\0\1\1\12\0\1\1" + "\1\0\1\1\15\0\1\1\1\0\1\1\25\0\1\1" + "\22\0\1\1\10\0\1\24\26\0\1\24\2\0\1\1" + "\35\0\1\24\3\0\1\24\6\0\1\24\50\0\1\26"; private static int[] zzUnpackAction() { int[] result = new int[701]; int offset = 0; offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result); return result; } private static int zzUnpackAction(String packed, int offset, int[] result) { int i = 0; /* index in packed string */ int j = offset; /* index in unpacked array */ int l = packed.length(); while (i < l) { int count = packed.charAt(i++); int value = packed.charAt(i++); do result[j++] = value; while (--count > 0); } return j; } /** * Translates a state to a row index in the transition table */ private static final int[] ZZ_ROWMAP = zzUnpackRowMap(); private static final String ZZ_ROWMAP_PACKED_0 = "\0\0\0\100\0\200\0\300\0\u0100\0\u0140\0\u0180\0\200" + "\0\u01c0\0\u0200\0\u0240\0\u0280\0\200\0\u02c0\0\u0300\0\u0340" + "\0\u0380\0\u03c0\0\u0400\0\u0440\0\u0480\0\u04c0\0\u0500\0\u0540" + "\0\200\0\200\0\u0580\0\u05c0\0\u0600\0\u0640\0\u0680\0\300" + "\0\u06c0\0\u0700\0\u0740\0\u0780\0\u07c0\0\u0800\0\u0840\0\u0880" + "\0\200\0\u08c0\0\u0900\0\u0940\0\u0980\0\u09c0\0\u0a00\0\u0a40" + "\0\u0a80\0\u0ac0\0\200\0\u0b00\0\u0b40\0\u0b80\0\u0bc0\0\u0c00" + "\0\u0c40\0\u0c80\0\u0cc0\0\u0d00\0\200\0\u0d40\0\u0d80\0\u0dc0" + "\0\u0e00\0\u0e40\0\u0e80\0\u0ec0\0\u0f00\0\u0f40\0\u0f80\0\u0fc0" + "\0\u1000\0\u1040\0\u1080\0\u10c0\0\u1100\0\u1140\0\u1180\0\u11c0" + "\0\u1200\0\u1240\0\u1280\0\u12c0\0\u1300\0\u1340\0\u1380\0\u13c0" + "\0\u1400\0\u1440\0\u1480\0\u14c0\0\u1500\0\u1540\0\u1580\0\u15c0" + "\0\u1600\0\u1640\0\u1680\0\u16c0\0\u1700\0\u1740\0\u1780\0\u17c0" + "\0\u1800\0\200\0\u1840\0\u06c0\0\u1880\0\u18c0\0\u1900\0\u1940" + "\0\u1980\0\u19c0\0\u1a00\0\u1a40\0\u1a80\0\u1ac0\0\u1b00\0\u1b40" + "\0\u1b80\0\u1bc0\0\u1c00\0\u1c40\0\u1c80\0\u1cc0\0\u1d00\0\u0a80" + "\0\u1d40\0\200\0\u1d80\0\u1dc0\0\u0b00\0\u1e00\0\u1e40\0\u1e80" + "\0\u1ec0\0\u1f00\0\u1f40\0\300\0\u1f80\0\u1fc0\0\200\0\u2000" + "\0\u2040\0\u2080\0\u20c0\0\u2100\0\u2140\0\u2180\0\u21c0\0\u2200" + "\0\u2240\0\u2280\0\u22c0\0\u2300\0\u2340\0\u2380\0\u23c0\0\u2400" + "\0\u2440\0\u2480\0\u24c0\0\u2500\0\u2540\0\u2580\0\u25c0\0\u2600" + "\0\u2640\0\u2680\0\u26c0\0\u2700\0\u2740\0\u2780\0\u27c0\0\u2800" + "\0\u2840\0\u2880\0\u28c0\0\u2900\0\u2940\0\u2980\0\u29c0\0\u2a00" + "\0\u2a40\0\u2a80\0\u2ac0\0\u2b00\0\u2b40\0\u2b80\0\u2bc0\0\u2c00" + "\0\u2c40\0\u2c80\0\u2cc0\0\u2d00\0\u2d40\0\u2d80\0\u2dc0\0\u2e00" + "\0\u2e40\0\u2e80\0\u2ec0\0\u2f00\0\u2f40\0\u2f80\0\u2fc0\0\u3000" + "\0\u3040\0\u3080\0\u30c0\0\u3100\0\u3140\0\u3180\0\u31c0\0\u3200" + "\0\u3240\0\u3280\0\u32c0\0\u3300\0\u3340\0\u3380\0\u33c0\0\u3400" + "\0\u3440\0\u3480\0\u34c0\0\u3500\0\u3540\0\u3580\0\u35c0\0\u3600" + "\0\u3640\0\u3680\0\u36c0\0\u3700\0\u3740\0\300\0\300\0\u3780" + "\0\u37c0\0\u3800\0\u3840\0\u3880\0\u38c0\0\u3900\0\u3940\0\u3980" + "\0\u39c0\0\u3a00\0\u3a40\0\u3a80\0\u3ac0\0\u3b00\0\u3b40\0\u3b80" + "\0\u3bc0\0\u3c00\0\u3c40\0\u3c80\0\u3cc0\0\u3d00\0\u3d40\0\u3d80" + "\0\u3dc0\0\u3e00\0\u3e40\0\u3e80\0\u3ec0\0\u3f00\0\u3f40\0\u3f80" + "\0\u3fc0\0\u4000\0\u4040\0\u4080\0\u40c0\0\u4100\0\u4140\0\u4180" + "\0\u41c0\0\u4200\0\u4240\0\u4280\0\u42c0\0\u4300\0\u4340\0\u4380" + "\0\u43c0\0\u4400\0\u4440\0\u4480\0\u44c0\0\u4500\0\u4540\0\u4580" + "\0\u45c0\0\u4600\0\u4640\0\u4680\0\u46c0\0\u4700\0\u4740\0\u4780" + "\0\u47c0\0\u4800\0\200\0\u4840\0\u4880\0\u48c0\0\u4900\0\u4940" + "\0\u4980\0\u49c0\0\u4a00\0\u4a40\0\u4a80\0\u4ac0\0\u4b00\0\u4b40" + "\0\u4b80\0\u4bc0\0\u4c00\0\u4c40\0\u4c80\0\u4cc0\0\u4d00\0\u4d40" + "\0\u4d80\0\u4dc0\0\u4e00\0\u4e40\0\u4e80\0\u4ec0\0\u4f00\0\u4f40" + "\0\u4f80\0\u4fc0\0\u5000\0\u5040\0\u5080\0\u50c0\0\u5100\0\u5140" + "\0\u5180\0\u51c0\0\u5200\0\u5240\0\u5280\0\u52c0\0\u5300\0\u5340" + "\0\u5380\0\u53c0\0\u5400\0\u5440\0\u5480\0\u54c0\0\u5500\0\u5540" + "\0\u5580\0\u55c0\0\u5600\0\u5640\0\u4400\0\u5680\0\u56c0\0\u5700" + "\0\u5740\0\u5780\0\u57c0\0\u5800\0\u5840\0\u5880\0\u58c0\0\u5900" + "\0\u5940\0\u5980\0\u59c0\0\u5a00\0\u5a40\0\u4a80\0\u5a80\0\u5ac0" + "\0\u5b00\0\u5b40\0\u5b80\0\u5bc0\0\u5c00\0\u5c40\0\u5c80\0\u5cc0" + "\0\u5d00\0\u5d40\0\u5d80\0\u5dc0\0\u5e00\0\u5e40\0\u5e80\0\u5ec0" + "\0\u5f00\0\u5f40\0\u5f80\0\u5fc0\0\u6000\0\u6040\0\u6080\0\u60c0" + "\0\u6100\0\u6140\0\u6180\0\u61c0\0\u6200\0\200\0\u6240\0\u6280" + "\0\u62c0\0\u6300\0\u6340\0\u6380\0\u63c0\0\u6400\0\u6440\0\u6480" + "\0\u64c0\0\u6500\0\u6540\0\u6580\0\u65c0\0\u6600\0\u6640\0\u6680" + "\0\u66c0\0\u6700\0\u6740\0\u6780\0\u67c0\0\u6800\0\u6840\0\u6880" + "\0\u68c0\0\u6900\0\u6940\0\u6980\0\u69c0\0\u6a00\0\u6a40\0\u6a80" + "\0\u6ac0\0\u6b00\0\u6b40\0\u6b80\0\u6bc0\0\u6c00\0\u6c40\0\u6c80" + "\0\u6cc0\0\u6d00\0\u6d40\0\u6d80\0\u6dc0\0\u6e00\0\u6e40\0\u6e80" + "\0\u6ec0\0\u6f00\0\u6f40\0\u6f80\0\u6fc0\0\u7000\0\u7040\0\u7080" + "\0\u70c0\0\u7100\0\u7140\0\u7180\0\u71c0\0\u7200\0\u7240\0\u7280" + "\0\u72c0\0\u7300\0\u7340\0\u7380\0\u73c0\0\u7400\0\u7440\0\u7480" + "\0\u74c0\0\u7500\0\u7540\0\u7580\0\u75c0\0\u7600\0\u7640\0\u7680" + "\0\u76c0\0\u7700\0\u7740\0\u7780\0\u77c0\0\u7800\0\u7840\0\u7880" + "\0\u78c0\0\u7900\0\u7940\0\u7980\0\u79c0\0\u7a00\0\u7a40\0\u7a80" + "\0\u7ac0\0\u7b00\0\u7b40\0\u7b80\0\u7bc0\0\u7c00\0\u7c40\0\u7c80" + "\0\u7cc0\0\u7d00\0\u7d40\0\u7d80\0\u7dc0\0\u7e00\0\u7e40\0\u7e80" + "\0\u7ec0\0\u7f00\0\u7f40\0\u7f80\0\u7fc0\0\u8000\0\u8040\0\u8080" + "\0\u80c0\0\u8100\0\u8140\0\u8180\0\u81c0\0\u8200\0\u8240\0\u8280" + "\0\u82c0\0\u8300\0\u8340\0\u8380\0\u83c0\0\u8400\0\u8440\0\u8480" + "\0\u84c0\0\u8500\0\u8540\0\u8580\0\u85c0\0\u8600\0\u8640\0\u8680" + "\0\u86c0\0\u8700\0\u8740\0\u8780\0\u87c0\0\u8800\0\u8840\0\u8880" + "\0\u88c0\0\u8900\0\u8940\0\u8980\0\u89c0\0\u8a00\0\u8a40\0\u8a80" + "\0\u8ac0\0\u8b00\0\u8b40\0\u8b80\0\u8bc0\0\u8c00\0\u8c40\0\u8c80" + "\0\u8cc0\0\u8d00\0\u8d40\0\u8d80\0\u8dc0\0\u8e00\0\u8e40\0\u8e80" + "\0\u8ec0\0\u8f00\0\u8f40\0\u8f80\0\u8fc0\0\u9000\0\u9040\0\u9080" + "\0\u90c0\0\u9100\0\u9140\0\u9180\0\u91c0\0\u9200\0\u9240\0\u9280" + "\0\u92c0\0\u9300\0\u9340\0\u9380\0\u93c0\0\u9400\0\u9440\0\u9480" + "\0\u94c0\0\u9500\0\u9540\0\u9580\0\u95c0\0\u9600\0\u9640\0\u9680" + "\0\u96c0\0\u9700\0\u9740\0\u9780\0\u97c0\0\u9800\0\u9840\0\u9880" + "\0\u98c0\0\u9900\0\u9940\0\u9980\0\u99c0\0\u9a00\0\u9a40\0\u9a80" + "\0\u9ac0\0\u9b00\0\u9b40\0\u9b80\0\u9bc0\0\u9c00\0\u9c40\0\u9c80" + "\0\u9cc0\0\u9d00\0\u9d40\0\u9d80\0\u9dc0\0\u9e00\0\u9e40\0\u9e80" + "\0\u9ec0\0\u9f00\0\u9f40\0\u9f80\0\u9fc0\0\ua000\0\ua040\0\ua080" + "\0\ua0c0\0\ua100\0\ua140\0\ua180\0\ua1c0\0\ua200\0\ua240\0\ua280" + "\0\ua2c0\0\ua300\0\ua340\0\ua380\0\ua3c0\0\ua400\0\ua440\0\ua480" + "\0\ua4c0\0\ua500\0\ua540\0\ua580\0\ua5c0\0\ua600\0\ua640\0\ua680" + "\0\ua6c0\0\ua700\0\ua740\0\ua780\0\ua7c0\0\ua800\0\ua840\0\ua880" + "\0\ua8c0\0\ua900\0\ua940\0\ua980\0\200"; private static int[] zzUnpackRowMap() { int[] result = new int[701]; int offset = 0; offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result); return result; } private static int zzUnpackRowMap(String packed, int offset, int[] result) { int i = 0; /* index in packed string */ int j = offset; /* index in unpacked array */ int l = packed.length(); while (i < l) { int high = packed.charAt(i++) << 16; result[j++] = high | packed.charAt(i++); } return j; } /** * The transition table of the DFA */ private static final int[] ZZ_TRANS = zzUnpackTrans(); private static final String ZZ_TRANS_PACKED_0 = "\1\3\2\4\1\5\1\6\1\4\1\5\1\7\1\10" + "\1\3\1\11\1\12\1\13\1\14\1\5\1\3\1\15" + "\1\16\1\17\1\20\1\21\1\22\1\23\1\24\1\25" + "\1\26\1\27\1\30\1\31\1\3\1\32\1\31\1\3" + "\1\32\1\4\1\33\1\34\1\35\1\4\1\36\1\37" + "\1\40\2\41\3\32\1\42\1\43\1\44\1\45\1\46" + "\1\4\1\47\2\5\1\4\2\5\3\4\1\5\1\16" + "\10\50\1\51\17\50\1\52\11\50\1\53\3\50\1\54" + "\31\50\101\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\1\0\3\56" + "\2\5\1\56\1\5\2\0\4\56\1\0\1\5\1\0" + "\1\56\1\0\1\56\1\57\1\60\1\0\6\56\2\0" + "\1\56\3\0\3\56\1\0\2\56\1\0\3\56\3\0" + "\7\56\2\5\1\56\2\5\3\56\1\5\1\0\3\56" + "\2\5\1\56\1\5\2\0\4\56\1\0\1\5\1\0" + "\1\56\1\0\1\61\1\57\1\60\1\0\6\56\2\0" + "\1\56\3\0\3\56\1\0\2\56\1\0\3\56\3\0" + "\7\56\2\5\1\56\2\5\3\56\1\5\1\0\7\62" + "\1\63\1\64\1\65\66\62\1\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\5\4\1\66\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\67\1\0\3\4\1\70" + "\2\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\4\4\1\71\13\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\0\1\4" + "\1\72\4\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\1\0\10\14\1\73\1\74\3\14\1\75" + "\62\14\21\0\1\16\55\0\1\16\1\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\4\4\1\46\13\4\4\0\2\76\1\0\1\76" + "\7\0\1\76\5\0\1\77\2\0\1\100\1\101\1\102" + "\1\103\1\104\7\0\1\105\1\106\13\0\1\107\4\0" + "\1\110\2\76\1\0\2\76\3\0\1\76\2\0\6\4" + "\2\0\1\55\1\4\1\111\1\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\57\0\1\112\22\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\1\4\1\113\4\4\6\0\1\114" + "\2\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\115\1\0\5\4\1\116\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\3\4\1\117\1\120\1\4\6\0\2\4\1\121" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\1\4\1\122\1\123\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\1\4\1\124\4\4\6\0" + "\1\4\1\125\1\4\1\0\2\4\1\0\3\4\3\0" + "\2\4\1\126\1\127\14\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\4\4\1\130\13\4\2\0\6\4\2\0\1\55\1\131" + "\2\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\1\132\5\4\6\0\1\133\1\134\1\4\1\0\2\4" + "\1\0\3\4\3\0\3\4\1\127\1\4\1\135\12\4" + "\2\0\2\4\2\136\1\4\1\136\2\0\1\55\1\137" + "\2\4\1\0\1\136\3\0\1\4\1\0\1\4\1\0" + "\1\4\1\140\1\4\1\141\2\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\7\4\2\136\1\4\2\136" + "\3\4\1\136\2\0\6\4\2\0\1\55\1\4\1\142" + "\1\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\2\4\1\143\3\4\6\0\1\4\1\125\1\4\1\0" + "\2\4\1\0\3\4\3\0\3\4\1\127\14\4\27\0" + "\1\144\2\0\1\145\11\0\1\146\14\0\1\147\1\0" + "\1\150\16\0\6\36\2\0\1\55\3\36\1\0\1\36" + "\3\0\1\36\1\0\1\36\1\0\6\36\5\0\1\151" + "\3\36\1\0\2\36\1\152\3\36\3\0\20\36\26\0" + "\1\153\53\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\1\4\1\36\1\152\1\4\2\154\3\0\20\4" + "\2\0\2\4\2\136\1\4\1\136\2\0\1\55\3\4" + "\1\0\1\136\3\0\1\4\1\0\1\4\1\0\3\4" + "\1\155\2\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\4\4\1\156\2\4\2\136\1\4\2\136\3\4" + "\1\136\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\1\157" + "\2\4\1\0\2\4\1\0\3\4\3\0\4\4\1\160" + "\1\4\1\161\11\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\162\1\0\6\4" + "\6\0\2\4\1\163\1\0\2\4\1\0\3\4\3\0" + "\4\4\1\164\13\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\0\6\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\4\4" + "\1\165\13\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\1\4\1\166" + "\4\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\1\167\2\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\4\4\1\170" + "\13\4\1\0\10\50\1\0\17\50\1\0\11\50\1\0" + "\3\50\1\0\31\50\26\0\1\171\15\0\1\172\61\0" + "\1\173\117\0\1\174\43\0\1\175\65\0\7\56\2\0" + "\4\56\1\0\1\56\1\0\1\56\1\0\1\56\1\0" + "\1\56\1\0\6\56\2\0\1\56\3\0\3\56\1\0" + "\2\56\1\0\3\56\3\0\20\56\4\0\2\76\1\0" + "\1\76\7\0\1\76\47\0\2\76\1\0\2\76\3\0" + "\1\76\1\0\3\56\2\176\1\56\1\176\2\0\4\56" + "\1\0\1\176\1\0\1\56\1\0\1\56\1\0\1\56" + "\1\177\6\56\2\0\1\56\1\0\1\177\1\0\3\56" + "\1\0\2\56\1\0\3\56\3\0\7\56\2\176\1\56" + "\2\176\3\56\1\176\1\0\3\56\4\200\2\0\3\56" + "\1\200\1\0\1\200\1\0\1\56\1\0\1\56\1\0" + "\1\200\1\0\2\56\2\200\2\56\2\0\1\56\3\0" + "\3\56\1\0\2\56\1\0\2\56\1\200\3\0\1\56" + "\2\200\4\56\2\200\1\56\2\200\3\56\1\200\1\0" + "\7\201\1\202\1\0\67\201\7\0\1\202\70\0\4\201" + "\1\203\1\201\1\204\1\205\1\0\1\62\1\206\3\62" + "\1\203\7\201\3\62\35\201\1\203\1\204\1\201\1\204" + "\1\203\5\201\1\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\6\4\6\0" + "\1\207\2\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\6\4\6\0\3\4\1\0" + "\1\210\1\4\1\0\3\4\3\0\3\4\1\211\14\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\1\212\5\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\1\213\5\4\6\0\1\4\1\214\1\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\6\4\6\0\2\4\1\215\1\0\2\4" + "\1\0\3\4\3\0\20\4\1\0\11\73\1\216\3\73" + "\1\217\66\73\1\14\1\73\2\14\1\0\1\14\1\220" + "\4\14\7\73\3\14\35\73\2\14\1\73\2\14\5\73" + "\3\56\2\76\1\56\1\76\2\0\4\56\1\0\1\76" + "\1\0\1\56\1\0\1\56\1\0\1\60\1\0\6\56" + "\2\0\1\56\3\0\3\56\1\0\2\56\1\0\3\56" + "\3\0\7\56\2\76\1\56\2\76\3\56\1\76\14\0" + "\1\221\110\0\1\222\117\0\1\223\46\0\1\224\13\0" + "\1\225\114\0\1\226\16\0\1\227\26\0\1\230\30\0" + "\1\231\17\0\1\232\43\0\1\233\1\0\1\234\133\0" + "\1\235\43\0\1\236\1\237\71\0\1\240\54\0\6\4" + "\2\0\1\55\1\241\2\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\2\112\2\0\1\112" + "\4\0\3\112\5\0\1\112\1\0\1\112\1\0\6\112" + "\6\0\3\112\1\0\2\112\1\0\3\112\1\0\11\112" + "\2\0\1\112\2\0\3\112\3\0\6\4\2\0\1\55" + "\1\242\2\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\3\4\1\243\2\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\1\4\1\244\4\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\20\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\0\1\245" + "\5\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\6\4\1\246\11\4\2\0\6\4\2\0\1\55\1\247" + "\2\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\4\4\1\250\1\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\4\4\1\251\13\4\2\0" + "\6\4\2\0\1\55\1\4\1\252\1\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\4\4\1\253\1\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\2\0\6\4\2\0\1\55\1\4\1\254\1\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\2\4\1\166" + "\15\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\5\4\1\255\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\1\4\1\256\4\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\1\257\2\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\2\4\1\246\15\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\257\1\0\6\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\2\0\6\4\2\0\1\55\1\4\1\260\1\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\2\4\1\261\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\1\4\1\262\1\4\1\263\2\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\1\4\1\264\2\4\1\264\1\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\1\257\2\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\3\4\1\265\2\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\1\4\1\266\1\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\2\4" + "\2\136\1\4\1\136\2\0\1\55\3\4\1\0\1\136" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\7\4\2\136\1\4" + "\2\136\3\4\1\136\2\0\6\4\2\0\1\55\2\4" + "\1\267\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\2\4" + "\1\270\1\0\2\4\1\0\3\4\3\0\4\4\1\271" + "\13\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\1\4\1\272\16\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\1\273\4\4\1\274\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\1\275\17\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\276\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\30\0\1\277\77\0" + "\1\300\103\0\1\301\75\0\1\302\31\0\1\303\77\0" + "\1\304\15\0\6\151\3\0\3\151\1\0\1\151\3\0" + "\1\151\1\0\1\151\1\0\6\151\5\0\4\151\1\0" + "\2\151\1\152\3\151\3\0\20\151\57\0\1\32\22\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\1\4\1\305\4\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\4\4\1\306\1\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\307" + "\1\0\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\1\4\1\310" + "\1\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\1\4" + "\1\311\1\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\1\4\1\312\16\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\1\246\17\4\2\0\6\4\2\0" + "\1\55\1\313\2\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\6\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\20\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\0\1\314" + "\5\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\315\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\4\4\1\246\1\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\1\4\1\316\1\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\1\317\17\4\44\0\1\320\66\0" + "\1\321\73\0\1\322\117\0\1\323\34\0\4\324\5\0" + "\1\324\1\0\1\324\5\0\1\324\3\0\2\324\21\0" + "\1\324\4\0\2\324\4\0\2\324\1\0\2\324\3\0" + "\1\324\1\0\3\56\2\176\1\56\1\176\2\0\4\56" + "\1\0\1\176\1\0\1\56\1\0\1\56\1\0\1\56" + "\1\0\6\56\2\0\1\56\3\0\3\56\1\0\2\56" + "\1\0\3\56\3\0\7\56\2\176\1\56\2\176\3\56" + "\1\176\4\0\2\176\1\0\1\176\7\0\1\176\47\0" + "\2\176\1\0\2\176\3\0\1\176\1\0\7\201\1\63" + "\1\0\73\201\1\204\1\201\1\204\1\202\1\0\5\201" + "\1\204\47\201\2\204\1\201\2\204\11\201\1\62\1\201" + "\1\62\1\202\1\0\5\201\1\62\47\201\2\62\1\201" + "\2\62\10\201\4\325\1\63\1\0\3\201\1\325\1\201" + "\1\325\5\201\1\325\3\201\2\325\21\201\1\325\4\201" + "\2\325\4\201\2\325\1\201\2\325\3\201\1\325\1\201" + "\1\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\1\4\1\264\4\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\326\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\327" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\2\4" + "\1\330\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\331\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\2\4\1\332\15\4\1\0\10\73\1\0\72\73\4\333" + "\2\73\1\216\2\73\1\333\1\217\1\333\5\73\1\333" + "\3\73\2\333\21\73\1\333\4\73\2\333\4\73\2\333" + "\1\73\2\333\3\73\1\333\1\73\61\0\1\334\51\0" + "\1\335\26\0\1\336\41\0\1\337\66\0\1\340\113\0" + "\1\341\63\0\1\342\144\0\1\343\62\0\1\344\65\0" + "\1\345\60\0\1\346\150\0\1\347\43\0\1\350\30\0" + "\1\351\62\0\1\352\62\0\1\353\102\0\1\354\74\0" + "\1\355\52\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\6\4\1\356\11\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\357\1\0\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\1\4\1\360\1\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\4\4\1\361\13\4\2\0\6\4\2\0\1\55\1\362" + "\2\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\363\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\2\4\1\364\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\5\4\1\242\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\3\4\1\365" + "\2\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\3\4\1\366\2\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\4\4\1\367\1\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\4\4\1\370\13\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\1\371\5\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\3\4\1\372\2\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\20\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\0\1\373" + "\5\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\3\4\1\374\14\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\375\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\6\4\6\0\2\4\1\376\1\0\2\4\1\0" + "\3\4\3\0\20\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\0\1\377" + "\5\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\u0100\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\1\4\1\u0101\4\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\1\u0102\5\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\20\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\0\4\4" + "\1\377\1\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\1\u0103\17\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\1\u0104\5\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\6\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\14\4\1\u0105\3\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\u0106" + "\1\u0107\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\1\u0108\5\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\4\4\1\u0109\13\4\14\0" + "\1\u010a\10\0\1\u010b\5\0\1\u010c\27\0\1\u010c\101\0" + "\1\u010d\42\0\1\u010e\116\0\1\u010f\57\0\1\u0110\64\0" + "\1\u0111\112\0\1\u0112\52\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\0\3\4" + "\1\u0113\2\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\3\4\1\u0114" + "\2\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\1\4\1\u0115\16\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\5\4\1\u0116\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\u0117\4\4\1\u0118\1\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\3\4\1\u0118\14\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\4\4\1\u0119\1\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\2\4\1\u011a\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\4\4\1\u011b\13\4\33\0\1\u011c\11\0\1\u011d\34\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\6\4\6\0\2\4\1\u011e\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\u011f" + "\1\0\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\46\0\1\u0120\56\0\1\320\116\0\1\u0121" + "\57\0\1\u0122\57\0\4\u0123\5\0\1\u0123\1\0\1\u0123" + "\5\0\1\u0123\3\0\2\u0123\21\0\1\u0123\4\0\2\u0123" + "\4\0\2\u0123\1\0\2\u0123\3\0\1\u0123\1\0\3\201" + "\4\u0124\1\63\1\0\3\201\1\u0124\1\201\1\u0124\5\201" + "\1\u0124\3\201\2\u0124\21\201\1\u0124\4\201\2\u0124\4\201" + "\2\u0124\1\201\2\u0124\3\201\1\u0124\1\201\31\0\1\u0125" + "\12\0\1\u0126\63\0\1\u0127\1\0\1\u0128\11\0\1\u0129" + "\14\0\1\u012a\17\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\1\u012b\17\4" + "\33\0\1\u0128\11\0\1\u0129\34\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\3\4\1\u012b\14\4\1\0\3\73\4\u012c\2\73\1\216" + "\2\73\1\u012c\1\217\1\u012c\5\73\1\u012c\3\73\2\u012c" + "\21\73\1\u012c\4\73\2\u012c\4\73\2\u012c\1\73\2\u012c" + "\3\73\1\u012c\1\73\77\0\1\u012d\26\0\1\u012e\115\0" + "\1\u012f\65\0\1\u0130\130\0\1\u0131\45\0\1\u0132\72\0" + "\1\u0133\104\0\1\u0134\72\0\1\u0135\102\0\1\u0136\77\0" + "\1\u0137\102\0\1\u0138\76\0\1\u0139\141\0\1\u013a\36\0" + "\1\u013b\125\0\1\u013c\52\0\1\u013d\106\0\1\u013e\36\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\5\4\1\u013f\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\6\4\6\0\3\4\1\0\1\214\1\4\1\0" + "\3\4\3\0\20\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\0\1\4" + "\1\u0140\4\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\31\0\1\u0141\1\0\1\u011c\11\0\1\u011d" + "\14\0\1\u0142\17\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\u0143\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\1\u0144\5\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\4\4\1\356\1\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\u0145\1\u0146" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\1\u0147\5\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\1\4\1\u0148\4\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\6\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\5\4\1\u0149\12\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\u014a\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\u014b\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\31\0" + "\1\u0141\1\0\1\u011c\11\0\1\u014c\14\0\1\u0142\17\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\1\4\1\u014d\16\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\6\4\6\0\2\4\1\u014e\1\0\2\4" + "\1\0\3\4\3\0\20\4\33\0\1\u011c\11\0\1\u014f" + "\34\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\5\4\1\u0150\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\2\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\6\4\6\0\1\u0151\2\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\3\4\1\u0152\2\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\20\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\u0153\1\0\6\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\u0154\1\0\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\1\4\1\u0155\4\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\27\0\1\u0156\52\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\3\4\1\u0157\2\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\14\4\1\u0158\3\4\25\0\1\u0159\147\0\1\u0159" + "\27\0\1\u0159\1\0\1\u0159\53\0\1\u015a\126\0\1\u015b" + "\112\0\1\u015c\113\0\1\u015d\100\0\1\u015e\101\0\1\u015e" + "\15\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\1\4\1\u015f\4\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\1\u0160\5\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\14\4\1\u0161\3\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\1\u0162\5\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\33\0\1\u0128\46\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\u0163" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\3\4\1\u0164\2\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\4\4\1\u0165\1\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\6\4\5\0\1\u0166\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\64\0\1\u0167\27\0" + "\1\u0168\65\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\1\u0169\5\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\u016a\6\4\5\0\1\u016b\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\42\0\1\u016c\71\0" + "\1\320\11\0\1\u0120\33\0\1\u0122\1\u016d\4\u0122\1\u016d" + "\2\0\3\u0122\1\0\1\u0122\1\0\1\u016d\1\0\1\u0122" + "\1\u016d\1\u0122\1\u016d\6\u0122\1\0\4\u016d\4\u0122\1\u016d" + "\2\u0122\1\u016d\3\u0122\1\u016d\2\0\20\u0122\4\0\4\u016e" + "\5\0\1\u016e\1\0\1\u016e\5\0\1\u016e\3\0\2\u016e" + "\21\0\1\u016e\4\0\2\u016e\4\0\2\u016e\1\0\2\u016e" + "\3\0\1\u016e\1\0\3\201\4\u016f\1\63\1\0\3\201" + "\1\u016f\1\201\1\u016f\5\201\1\u016f\3\201\2\u016f\21\201" + "\1\u016f\4\201\2\u016f\4\201\2\u016f\1\201\2\u016f\3\201" + "\1\u016f\1\201\27\0\1\u0170\63\0\1\u0171\116\0\1\u0172" + "\130\0\1\u0173\27\0\1\u0174\147\0\1\u0175\15\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\356\1\0\6\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\20\4\1\0\3\73\4\u0176\2\73\1\216" + "\2\73\1\u0176\1\217\1\u0176\5\73\1\u0176\3\73\2\u0176" + "\21\73\1\u0176\4\73\2\u0176\4\73\2\u0176\1\73\2\u0176" + "\3\73\1\u0176\1\73\30\0\1\101\1\102\1\u0177\1\u0178" + "\7\0\1\u0179\21\0\1\110\43\0\1\u017a\101\0\1\u017b" + "\125\0\1\u0133\44\0\1\u017c\135\0\1\u017d\45\0\1\u017e" + "\74\0\1\u0133\103\0\1\u017f\124\0\1\342\102\0\1\u0180" + "\101\0\1\u0133\36\0\1\u0181\77\0\1\u0182\115\0\1\u0183" + "\70\0\1\u0133\127\0\1\u0130\15\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\2\4\1\u0184\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\1\4\1\u0185" + "\1\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\33\0\1\u0186\130\0\1\u0187\60\0\1\u0188\34\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\u0189\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\2\4\1\u018a\15\4\32\0\1\u018b\47\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\3\4\1\u018c\2\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\3\4\1\u018d\2\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\20\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\u018e\6\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\15\0\1\u018f\16\0\1\u0190\12\0\1\u0191\11\0\1\u0192" + "\2\0\1\u0193\42\0\1\u0194\64\0\1\u0195\65\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\1\u0196\5\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\1\4\1\356\16\4\14\0\1\u0197\65\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\u0198" + "\1\0\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\263\1\0\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\1\u012b\5\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\1\4\1\u0199\16\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\2\4\1\u0198\15\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\0\2\4" + "\1\u019a\3\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\64\0\1\u019b\15\0\6\4\2\0\1\55" + "\1\4\1\u019c\1\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\6\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\20\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\u019d\1\0\6\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\76\0\1\u019e\26\0\1\303\6\0\1\u019f\130\0\1\u015e" + "\41\0\1\u01a0\113\0\1\u01a1\37\0\1\u01a2\76\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\6\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\3\4\1\u01a3\14\4\2\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\6\4\6\0\2\4\1\u01a4\1\0\2\4\1\0" + "\3\4\3\0\20\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\u01a5\6\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\u01a6\1\4\1\u01a7\4\4\5\0" + "\1\u01a8\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\31\0\1\u0127\30\0\1\u012a\17\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\1\4\1\u01a9\4\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\20\4\2\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\u01aa\1\0\6\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\67\0\1\u01ab\3\0\1\u01ac\20\0\1\u01ad\112\0\1\u01ae" + "\52\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\4\4\1\u01af\13\4\25\0" + "\1\u01b0\2\0\1\u01b1\16\0\1\u01b2\14\0\1\u01b3\44\0" + "\1\u01b4\35\0\1\u01ab\52\0\1\u0122\41\0\4\4\5\0" + "\1\4\1\0\1\4\5\0\1\4\3\0\2\4\21\0" + "\1\4\4\0\2\4\4\0\2\4\1\0\2\4\3\0" + "\1\4\1\0\3\201\4\62\1\63\1\0\3\201\1\62" + "\1\201\1\62\5\201\1\62\3\201\2\62\21\201\1\62" + "\4\201\2\62\4\201\2\62\1\201\2\62\3\201\1\62" + "\1\201\27\0\1\u01b5\103\0\1\u01b6\127\0\1\u01b7\27\0" + "\1\u01b8\112\0\1\u019e\63\0\1\u01b9\65\0\3\73\4\14" + "\2\73\1\216\2\73\1\14\1\217\1\14\5\73\1\14" + "\3\73\2\14\21\73\1\14\4\73\2\14\4\73\2\14" + "\1\73\2\14\3\73\1\14\1\73\63\0\1\u01ba\57\0" + "\1\231\65\0\1\234\75\0\1\u01bb\76\0\1\u01bc\102\0" + "\1\u01bd\73\0\1\u01be\76\0\1\u01bf\135\0\1\u01c0\76\0" + "\1\u01bf\103\0\1\u01c1\43\0\1\u01c2\47\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\u01c3" + "\1\0\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\u01c4\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\64\0" + "\1\u01c5\26\0\1\u01c6\100\0\1\u01c7\112\0\1\u01c8\52\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\u01c9\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\30\0\1\u01ca\51\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\1\u01cb\5\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\2\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\1\4\1\u01cc\16\4\33\0\1\u01cd\130\0\1\u01ce\1\u01cf" + "\55\0\1\u01d0\101\0\1\u01d1\75\0\1\u01d2\51\0\1\u01d3" + "\146\0\1\u01d4\42\0\1\u01d5\52\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\2\4\1\u01d6\3\4\6\0\3\4\1\0\2\4\1\0" + "\3\4\3\0\20\4\27\0\1\u01d7\52\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\u01d8\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\1\u01d9\5\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\3\4\1\u01da\2\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\26\0" + "\1\u01db\53\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\1\4\1\u01dc\16\4" + "\2\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\u01dd\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\27\0\1\u01de\131\0" + "\1\u01df\21\0\1\u01a2\26\0\1\u01e0\47\0\6\u01a2\3\0" + "\3\u01a2\1\0\1\u01a2\3\0\1\u01a2\1\0\1\u01a2\1\0" + "\6\u01a2\6\0\3\u01a2\1\0\2\u01a2\1\0\3\u01a2\3\0" + "\20\u01a2\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\5\4\1\356\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\2\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\0\4\4\1\u012b\1\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\61\0\1\u01e1" + "\52\0\1\u01e2\12\0\1\u01e3\11\0\1\u01e4\4\0\1\u01e5" + "\13\0\6\4\2\0\1\55\1\u01e6\2\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\43\0\1\u01e7" + "\23\0\1\u01ab\2\0\1\u019e\7\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\u01e8\1\0" + "\6\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\2\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\u01e9\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\20\4\70\0\1\u019e" + "\26\0\1\u019e\143\0\1\u01d5\56\0\1\u01ea\37\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\1\4\1\u01eb\4\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\23\0\1\u01ec\101\0" + "\1\u01ed\117\0\1\u01ee\47\0\1\u01ef\112\0\1\u01f0\101\0" + "\1\u01f1\74\0\1\u01f2\102\0\1\u0174\130\0\1\u019e\31\0" + "\1\u01f3\143\0\1\u01f4\45\0\1\u01f5\75\0\1\u01f6\101\0" + "\1\u01f7\132\0\1\u01f8\43\0\1\u01f9\64\0\1\342\111\0" + "\1\u01fa\105\0\1\u01fb\46\0\6\4\2\0\1\55\1\4" + "\1\u01cc\1\4\1\0\1\4\3\0\1\4\1\0\1\4" + "\1\0\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\47\0\1\u0191\10\0\1\u01fc\3\0\1\u0193" + "\45\0\1\u0195\62\0\1\u01fd\111\0\1\u01fe\134\0\1\u01ff" + "\27\0\1\u0200\113\0\1\u0201\51\0\6\4\2\0\1\55" + "\3\4\1\0\1\4\3\0\1\4\1\0\1\4\1\0" + "\6\4\6\0\2\4\1\u0202\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\1\356\5\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\25\0\1\u0203\136\0\1\u0204\42\0\1\u0205\134\0\1\u0206" + "\75\0\1\u0205\47\0\1\u0207\136\0\1\u0208\34\0\1\u0209" + "\113\0\1\u020a\37\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\6\4\6\0" + "\1\4\1\356\1\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\42\0\1\u020b\71\0\1\u020c\45\0\6\4\2\0" + "\1\55\3\4\1\0\1\4\3\0\1\4\1\0\1\u020d" + "\1\0\6\4\6\0\3\4\1\0\2\4\1\0\3\4" + "\3\0\20\4\2\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\1\4\1\u012b" + "\16\4\15\0\1\u020e\13\0\1\u0127\1\0\1\u0128\1\u0190" + "\24\0\1\u0192\1\u012a\17\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\u020f\1\0\6\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\34\0\1\u0210\7\0\1\u0211\1\u0212\12\0\1\u0213\1\u0214" + "\1\u0215\47\0\1\u0216\110\0\1\u015e\67\0\1\u0217\76\0" + "\1\u0218\74\0\1\u0219\115\0\1\u021a\65\0\1\u021b\71\0" + "\1\u021c\54\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\1\4\1\u021d\16\4" + "\45\0\1\u021e\34\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\0\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\2\4\1\u021f" + "\15\4\27\0\1\u0220\67\0\1\u0221\13\0\1\u0222\46\0" + "\6\4\2\0\1\55\3\4\1\0\1\4\3\0\1\4" + "\1\0\1\4\1\u0223\6\4\6\0\3\4\1\0\2\4" + "\1\0\3\4\3\0\20\4\61\0\1\u0224\52\0\1\u0225" + "\125\0\1\u0226\106\0\1\u0227\72\0\1\u0228\100\0\1\u019e" + "\44\0\1\u0229\100\0\1\u0205\76\0\1\u01fb\145\0\1\u022a" + "\27\0\1\u013d\114\0\1\u022b\64\0\1\u022c\101\0\1\u022d" + "\57\0\1\u022e\116\0\1\u0133\130\0\1\u022f\46\0\1\u0230" + "\106\0\1\u0231\63\0\1\u0232\76\0\1\u0233\104\0\1\u0234" + "\47\0\6\4\2\0\1\55\3\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\4\4\1\u0235\13\4\14\0" + "\1\u0236\116\0\1\u0237\71\0\1\u019e\102\0\1\u0174\77\0" + "\1\u019e\74\0\1\u0238\103\0\1\u0127\13\0\1\u0129\14\0" + "\1\u012a\34\0\1\u0221\77\0\1\u0221\13\0\1\u0239\113\0" + "\1\u023a\32\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\2\4\1\356\15\4" + "\65\0\1\u01cf\14\0\6\4\2\0\1\55\3\4\1\0" + "\1\4\3\0\1\4\1\0\1\4\1\u023b\6\4\6\0" + "\3\4\1\0\2\4\1\0\3\4\3\0\20\4\13\0" + "\1\u023c\13\0\1\u023d\134\0\1\u023e\27\0\1\u023f\130\0" + "\1\u0240\45\0\1\u0241\131\0\1\u0242\62\0\1\u0243\102\0" + "\1\u015e\100\0\1\u0174\73\0\1\u0244\131\0\1\u0245\47\0" + "\1\u0246\74\0\1\u0247\52\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\0\1\u0248" + "\5\4\6\0\3\4\1\0\2\4\1\0\3\4\3\0" + "\20\4\63\0\1\u0249\16\0\6\4\2\0\1\55\3\4" + "\1\0\1\4\3\0\1\4\1\0\1\4\1\u024a\6\4" + "\6\0\3\4\1\0\2\4\1\0\3\4\3\0\20\4" + "\64\0\1\u024b\45\0\1\u024c\112\0\1\u024d\57\0\1\u024e" + "\77\0\1\u024f\65\0\1\u0250\111\0\1\u0251\77\0\1\u0252" + "\140\0\1\u0253\25\0\1\u0254\116\0\1\u0177\130\0\1\u0255" + "\42\0\1\u0256\117\0\1\u0257\57\0\1\u013d\115\0\1\u0258" + "\57\0\1\u01d5\105\0\1\u0239\77\0\1\u0128\11\0\1\u0129" + "\14\0\1\u012a\64\0\1\u0259\115\0\1\u025a\14\0\6\4" + "\2\0\1\55\1\4\1\356\1\4\1\0\1\4\3\0" + "\1\4\1\0\1\4\1\0\6\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\63\0\1\u025b\41\0" + "\1\u025c\133\0\1\u0174\63\0\1\u025d\77\0\1\u025e\116\0" + "\1\u025f\57\0\1\u0260\65\0\1\u0261\100\0\1\u0262\73\0" + "\1\u0263\100\0\1\u0264\103\0\1\u0265\73\0\1\u0266\76\0" + "\1\u015e\115\0\1\u0267\57\0\1\u0268\106\0\1\u0269\106\0" + "\1\u026a\36\0\6\4\2\0\1\55\3\4\1\0\1\4" + "\3\0\1\4\1\0\1\4\1\0\6\4\6\0\3\4" + "\1\0\2\4\1\0\3\4\3\0\4\4\1\u026b\13\4" + "\43\0\1\u0253\70\0\1\u026c\71\0\1\u026d\133\0\1\u026e" + "\44\0\1\u026f\64\0\1\u0270\6\0\1\u0271\120\0\1\u0272" + "\66\0\1\u0273\106\0\1\u016b\116\0\1\u0274\105\0\1\u01ab" + "\71\0\1\u0205\32\0\1\u0133\115\0\1\u0133\112\0\1\u0275" + "\114\0\1\u019e\43\0\1\u0276\77\0\1\u0277\100\0\1\u0278" + "\102\0\1\u0279\74\0\1\u027a\77\0\1\u027b\101\0\1\u019e" + "\73\0\1\u027c\101\0\1\u027d\135\0\1\u027e\37\0\1\u027f" + "\101\0\1\u0280\77\0\1\u0281\75\0\1\u0282\66\0\1\u0283" + "\125\0\1\u0284\71\0\1\u019e\127\0\1\u0285\15\0\6\4" + "\2\0\1\55\3\4\1\0\1\4\3\0\1\4\1\0" + "\1\4\1\0\1\4\1\356\4\4\6\0\3\4\1\0" + "\2\4\1\0\3\4\3\0\20\4\65\0\1\u0286\43\0" + "\1\u0127\1\0\1\u0128\11\0\1\u0129\114\0\1\u0207\104\0" + "\1\u01ab\7\0\1\u019e\27\0\1\u0287\115\0\1\u0174\61\0" + "\1\u0288\77\0\1\u0289\77\0\1\u0251\77\0\1\u028a\102\0" + "\1\u028b\127\0\1\u028c\60\0\1\u019e\50\0\1\u019e\162\0" + "\1\u019e\61\0\1\u0278\46\0\1\u028d\114\0\1\u028e\120\0" + "\1\u028f\41\0\1\u0290\62\0\1\u0291\150\0\1\u0292\74\0" + "\1\u0293\101\0\1\u0294\57\0\1\u01e7\23\0\1\u01ab\3\0" + "\1\u01ac\66\0\1\u0295\31\0\1\u0296\110\0\1\u0207\117\0" + "\1\u0297\60\0\1\u0298\132\0\1\u0299\46\0\1\u029a\101\0" + "\1\u029b\107\0\1\u029c\116\0\1\u028d\102\0\1\u029d\44\0" + "\1\u029e\100\0\1\u029f\133\0\1\u028d\40\0\1\u028d\112\0" + "\1\u02a0\63\0\1\u02a1\132\0\1\u02a2\102\0\1\u0279\62\0" + "\1\u0191\14\0\1\u0193\56\0\1\u0133\64\0\1\u02a3\76\0" + "\1\u02a4\100\0\1\u02a5\77\0\1\u02a6\101\0\1\u02a7\100\0" + "\1\u028d\135\0\1\u02a8\35\0\1\u02a9\13\0\1\u02aa\77\0" + "\1\u02ab\66\0\1\u02ac\77\0\1\u019e\77\0\1\u02ad\111\0" + "\1\u02ae\114\0\1\u02af\31\0\1\u02b0\151\0\1\u02b1\44\0" + "\1\u02b2\75\0\1\u02b3\134\0\1\u028d\26\0\1\u02b4\126\0" + "\1\u027d\61\0\1\u028d\140\0\1\u02b5\55\0\1\u0205\47\0" + "\1\u02b6\147\0\1\u02b7\76\0\1\u0205\31\0\1\u02b8\144\0" + "\1\u01f3\31\0\1\u02b9\147\0\1\u019e\60\0\1\u02ba\130\0" + "\1\u02bb\26\0\1\u02bc\134\0\1\u02bd\16\0"; private static int[] zzUnpackTrans() { int[] result = new int[43456]; int offset = 0; offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result); return result; } private static int zzUnpackTrans(String packed, int offset, int[] result) { int i = 0; /* index in packed string */ int j = offset; /* index in unpacked array */ int l = packed.length(); while (i < l) { int count = packed.charAt(i++); int value = packed.charAt(i++); value--; do result[j++] = value; while (--count > 0); } return j; } /* error codes */ private static final int ZZ_UNKNOWN_ERROR = 0; private static final int ZZ_NO_MATCH = 1; private static final int ZZ_PUSHBACK_2BIG = 2; /* error messages for the codes above */ private static final String ZZ_ERROR_MSG[] = { "Unknown internal scanner error", "Error: could not match input", "Error: pushback value was too large" }; /** * ZZ_ATTRIBUTE[aState] contains the attributes of state aState */ private static final int[] ZZ_ATTRIBUTE = zzUnpackAttribute(); private static final String ZZ_ATTRIBUTE_PACKED_0 = "\2\0\1\11\4\1\1\11\4\1\1\11\13\1\2\11" + "\16\1\1\11\3\1\1\0\1\1\1\0\3\1\1\11" + "\1\0\10\1\1\11\1\1\12\0\33\1\6\0\1\11" + "\1\0\15\1\5\0\1\1\1\0\2\1\1\11\14\1" + "\1\11\1\1\20\0\35\1\7\0\10\1\1\0\2\1" + "\5\0\1\1\2\0\1\1\1\0\2\1\22\0\5\1" + "\1\0\11\1\1\0\2\1\1\0\6\1\1\0\2\1" + "\11\0\4\1\1\0\4\1\2\0\2\1\2\0\1\1" + "\1\0\1\1\6\0\2\1\6\0\1\11\13\0\2\1" + "\3\0\2\1\1\0\3\1\3\0\2\1\1\0\6\1" + "\1\0\3\1\5\0\4\1\1\0\2\1\3\0\1\1" + "\5\0\1\1\6\0\1\1\7\0\1\1\4\0\3\1" + "\4\0\1\1\1\0\2\1\10\0\1\1\1\0\3\1" + "\1\0\2\1\1\11\3\0\3\1\2\0\1\1\1\0" + "\2\1\3\0\2\1\23\0\1\1\7\0\2\1\10\0" + "\3\1\1\0\2\1\1\0\1\1\11\0\1\1\1\0" + "\1\1\2\0\1\1\22\0\1\1\3\0\1\1\12\0" + "\1\1\1\0\1\1\15\0\1\1\1\0\1\1\25\0" + "\1\1\22\0\1\1\10\0\1\1\26\0\1\1\2\0" + "\1\1\35\0\1\1\3\0\1\1\6\0\1\1\50\0" + "\1\11"; private static int[] zzUnpackAttribute() { int[] result = new int[701]; int offset = 0; offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result); return result; } private static int zzUnpackAttribute(String packed, int offset, int[] result) { int i = 0; /* index in packed string */ int j = offset; /* index in unpacked array */ int l = packed.length(); while (i < l) { int count = packed.charAt(i++); int value = packed.charAt(i++); do result[j++] = value; while (--count > 0); } return j; } /** the input device */ private Reader zzReader; /** the current state of the DFA */ private int zzState; /** the current lexical state */ private int zzLexicalState = YYINITIAL; /** * this buffer contains the current text to be matched and is * the source of the yytext() string */ private char zzBuffer[]; /** the textposition at the last accepting state */ private int zzMarkedPos; /** the textposition at the last state to be included in yytext */ private int zzPushbackPos; /** the current text position in the buffer */ private int zzCurrentPos; /** startRead marks the beginning of the yytext() string in the buffer */ private int zzStartRead; /** * endRead marks the last character in the buffer, that has been read * from input */ private int zzEndRead; /** number of newlines encountered up to the start of the matched text */ private int yyline; /** the number of characters up to the start of the matched text */ private int yychar; /** * the number of characters from the last newline up to the start of the * matched text */ private int yycolumn; /** * zzAtBOL == true <=> the scanner is currently at the beginning of a line */ private boolean zzAtBOL = true; /** zzAtEOF == true <=> the scanner is at the EOF */ private boolean zzAtEOF; /* user code: */ /** * Constructor. This must be here because JFlex does not generate a * no-parameter constructor. */ public SmaliTokenMaker() { } /** * Adds the token specified to the current linked list of tokens. * * @param tokenType The token's type. * @see #addToken(int, int, int) */ private void addHyperlinkToken(int start, int end, int tokenType) { int so = start + offsetShift; addToken(zzBuffer, start, end, tokenType, so, true); } /** * Adds the token specified to the current linked list of tokens. * * @param tokenType The token's type. */ private void addToken(int tokenType) { addToken(zzStartRead, zzMarkedPos - 1, tokenType); } /** * Adds the token specified to the current linked list of tokens. * * @param tokenType The token's type. * @see #addHyperlinkToken(int, int, int) */ private void addToken(int start, int end, int tokenType) { int so = start + offsetShift; addToken(zzBuffer, start, end, tokenType, so, false); } /** * Adds the token specified to the current linked list of tokens. * * @param array The character array. * @param start The starting offset in the array. * @param end The ending offset in the array. * @param tokenType The token's type. * @param startOffset The offset in the document at which this token * occurs. * @param hyperlink Whether this token is a hyperlink. */ public void addToken(char[] array, int start, int end, int tokenType, int startOffset, boolean hyperlink) { super.addToken(array, start, end, tokenType, startOffset, hyperlink); zzStartRead = zzMarkedPos; } /** * {@inheritDoc} */ public String[] getLineCommentStartAndEnd(int languageIndex) { return new String[] { "#", null }; } /** * Returns the first token in the linked list of tokens generated * from text. This method must be implemented by * subclasses so they can correctly implement syntax highlighting. * * @param text The text from which to get tokens. * @param initialTokenType The token type we should start with. * @param startOffset The offset into the document at which * text starts. * @return The first Token in a linked list representing * the syntax highlighted text. */ public Token getTokenList(Segment text, int initialTokenType, int startOffset) { resetTokenList(); this.offsetShift = -text.offset + startOffset; // Start off in the proper state. int state = Token.NULL; switch (initialTokenType) { /* No multi-line comments */ /* No documentation comments */ default: state = Token.NULL; } s = text; try { yyreset(zzReader); yybegin(state); return yylex(); } catch (IOException ioe) { ioe.printStackTrace(); return new TokenImpl(); } } /** * Refills the input buffer. * * @return true if EOF was reached, otherwise * false. */ private boolean zzRefill() { return zzCurrentPos >= s.offset + s.count; } /** * Resets the scanner to read from a new input stream. * Does not close the old reader. * * All internal variables are reset, the old input stream * cannot be reused (internal buffer is discarded and lost). * Lexical state is set to YY_INITIAL * * @param reader the new input stream */ public final void yyreset(Reader reader) { // 's' has been updated. zzBuffer = s.array; /* * We replaced the line below with the two below it because zzRefill * no longer "refills" the buffer (since the way we do it, it's always * "full" the first time through, since it points to the segment's * array). So, we assign zzEndRead here. */ // zzStartRead = zzEndRead = s.offset; zzStartRead = s.offset; zzEndRead = zzStartRead + s.count - 1; zzCurrentPos = zzMarkedPos = zzPushbackPos = s.offset; zzLexicalState = YYINITIAL; zzReader = reader; zzAtBOL = true; zzAtEOF = false; } /** * Creates a new scanner * There is also a java.io.InputStream version of this constructor. * * @param in the java.io.Reader to read input from. */ public SmaliTokenMaker(Reader in) { this.zzReader = in; } /** * Creates a new scanner. * There is also java.io.Reader version of this constructor. * * @param in the java.io.Inputstream to read input from. */ public SmaliTokenMaker(InputStream in) { this(new InputStreamReader(in)); } /** * Unpacks the compressed character translation table. * * @param packed the packed character translation table * @return the unpacked character translation table */ private static char[] zzUnpackCMap(String packed) { char[] map = new char[0x10000]; int i = 0; /* index in packed string */ int j = 0; /* index in unpacked array */ while (i < 188) { int count = packed.charAt(i++); char value = packed.charAt(i++); do map[j++] = value; while (--count > 0); } return map; } /** * Closes the input stream. */ public final void yyclose() throws IOException { zzAtEOF = true; /* indicate end of file */ zzEndRead = zzStartRead; /* invalidate buffer */ if (zzReader != null) zzReader.close(); } /** * Enters a new lexical state * * @param newState the new lexical state */ public final void yybegin(int newState) { zzLexicalState = newState; } public final int yystate() { return zzLexicalState; } /** * Returns the text matched by the current regular expression. */ public final String yytext() { return new String(zzBuffer, zzStartRead, zzMarkedPos - zzStartRead); } /** * Returns the character at positionpos from the * matched text. * * It is equivalent to yytext().charAt(pos), but faster * * @param pos the position of the character to fetch. * A value from 0 to yylength()-1. * * @return the character at position pos */ public final char yycharat(int pos) { return zzBuffer[zzStartRead + pos]; } /** * Returns the length of the matched text region. */ public final int yylength() { return zzMarkedPos - zzStartRead; } /** * Reports an error that occurred while scanning. * * In a wellformed scanner (no or only correct usage of * yypushback(int) and a match-all fallback rule) this method * will only be called with things that "Can't Possibly Happen". * If this method is called, something is seriously wrong * (e.g. a JFlex bug producing a faulty scanner etc.). * * Usual syntax/scanner level error handling should be done * in error fallback rules. * * @param errorCode the code of the errormessage to display */ private void zzScanError(int errorCode) { String message; try { message = ZZ_ERROR_MSG[errorCode]; } catch (ArrayIndexOutOfBoundsException e) { message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR]; } throw new Error(message); } /** * Pushes the specified amount of characters back into the input stream. * * They will be read again by then next call of the scanning method * * @param number the number of characters to be read again. * This number must not be greater than yylength()! */ public void yypushback(int number) { if (number > yylength()) zzScanError(ZZ_PUSHBACK_2BIG); zzMarkedPos -= number; } /** * Resumes scanning until the next regular expression is matched, * the end of input is encountered or an I/O-Error occurs. * * @return the next token * @exception IOException if any I/O-Error occurs */ public Token yylex() throws IOException { int zzInput; int zzAction; // cached fields: int zzCurrentPosL; int zzMarkedPosL; int zzEndReadL = zzEndRead; char[] zzBufferL = zzBuffer; char[] zzCMapL = ZZ_CMAP; int[] zzTransL = ZZ_TRANS; int[] zzRowMapL = ZZ_ROWMAP; int[] zzAttrL = ZZ_ATTRIBUTE; while (true) { zzMarkedPosL = zzMarkedPos; zzAction = -1; zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL; zzState = zzLexicalState; zzForAction: { while (true) { if (zzCurrentPosL < zzEndReadL) zzInput = zzBufferL[zzCurrentPosL++]; else if (zzAtEOF) { zzInput = YYEOF; break zzForAction; } else { // store back cached positions zzCurrentPos = zzCurrentPosL; zzMarkedPos = zzMarkedPosL; boolean eof = zzRefill(); // get translated positions and possibly new buffer zzCurrentPosL = zzCurrentPos; zzMarkedPosL = zzMarkedPos; zzBufferL = zzBuffer; zzEndReadL = zzEndRead; if (eof) { zzInput = YYEOF; break zzForAction; } else { zzInput = zzBufferL[zzCurrentPosL++]; } } int zzNext = zzTransL[zzRowMapL[zzState] + zzCMapL[zzInput]]; if (zzNext == -1) break zzForAction; zzState = zzNext; int zzAttributes = zzAttrL[zzState]; if ((zzAttributes & 1) == 1) { zzAction = zzState; zzMarkedPosL = zzCurrentPosL; if ((zzAttributes & 8) == 8) break zzForAction; } } } // store back cached position zzMarkedPos = zzMarkedPosL; switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) { case 4: { addNullToken(); return firstToken; } case 27: break; case 19: { addToken(Token.LITERAL_CHAR); } case 28: break; case 26: { addToken(Token.MARKUP_TAG_NAME); } case 29: break; case 7: { addToken(Token.WHITESPACE); } case 30: break; case 18: { addToken(Token.LITERAL_NUMBER_HEXADECIMAL); } case 31: break; case 21: { addToken(Token.ERROR_STRING_DOUBLE); } case 32: break; case 17: { addToken(Token.LITERAL_NUMBER_FLOAT); } case 33: break; case 22: { addToken(Token.RESERVED_WORD); } case 34: break; case 8: { addToken(Token.SEPARATOR); } case 35: break; case 10: { addToken(Token.VARIABLE); } case 36: break; case 1: { addToken(Token.IDENTIFIER); } case 37: break; case 13: { addToken(start, zzStartRead - 1, Token.COMMENT_EOL); addNullToken(); return firstToken; } case 38: break; case 20: { addToken(Token.FUNCTION); } case 39: break; case 3: { addToken(Token.ERROR_CHAR); addNullToken(); return firstToken; } case 40: break; case 5: { addToken(Token.ERROR_STRING_DOUBLE); addNullToken(); return firstToken; } case 41: break; case 11: { addToken(Token.DATA_TYPE); } case 42: break; case 15: { addToken(Token.ERROR_CHAR); } case 43: break; case 23: { addToken(Token.LITERAL_BOOLEAN); } case 44: break; case 16: { addToken(Token.LITERAL_STRING_DOUBLE_QUOTE); } case 45: break; case 24: { int temp = zzStartRead; addToken(start, zzStartRead - 1, Token.COMMENT_EOL); addHyperlinkToken(temp, zzMarkedPos - 1, Token.COMMENT_EOL); start = zzMarkedPos; } case 46: break; case 25: { addToken(Token.RESERVED_WORD_2); } case 47: break; case 14: { addToken(Token.ERROR_NUMBER_FORMAT); } case 48: break; case 6: { start = zzMarkedPos - 1; yybegin(EOL_COMMENT); } case 49: break; case 2: { addToken(Token.LITERAL_NUMBER_DECIMAL_INT); } case 50: break; case 9: { addToken(Token.OPERATOR); } case 51: break; case 12: { } case 52: break; default: if (zzInput == YYEOF && zzStartRead == zzCurrentPos) { zzAtEOF = true; switch (zzLexicalState) { case EOL_COMMENT: { addToken(start, zzStartRead - 1, Token.COMMENT_EOL); addNullToken(); return firstToken; } case 702: break; case YYINITIAL: { addNullToken(); return firstToken; } case 703: break; default: return null; } } else { zzScanError(ZZ_NO_MATCH); } } } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/SourceLineFormatter.java ================================================ package jadx.gui.ui.codearea; import org.fife.ui.rtextarea.LineNumberFormatter; import jadx.api.ICodeInfo; public class SourceLineFormatter implements LineNumberFormatter { private final ICodeInfo codeInfo; private final int maxLength; public SourceLineFormatter(ICodeInfo codeInfo) { this.codeInfo = codeInfo; this.maxLength = calcMaxLength(codeInfo); } @Override public String format(int lineNumber) { Integer sourceLine = codeInfo.getCodeMetadata().getLineMapping().get(lineNumber); if (sourceLine == null) { return ""; } return String.valueOf(sourceLine); } @Override public int getMaxLength(int maxLineNumber) { return maxLength; } private static int calcMaxLength(ICodeInfo codeInfo) { int maxLine = codeInfo.getCodeMetadata().getLineMapping() .values().stream() .mapToInt(Integer::intValue) .max().orElse(1); return getNumberLength(maxLine); } public static int getNumberLength(int num) { return num < 10 ? 1 : 1 + (int) Math.log10(num); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/UsageDialogPlusAction.java ================================================ package jadx.gui.ui.codearea; import jadx.gui.treemodel.JNode; import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.action.JNodeAction; import jadx.gui.ui.dialog.UsageDialogPlus; public final class UsageDialogPlusAction extends JNodeAction { private static final long serialVersionUID = 4692546569977976384L; public UsageDialogPlusAction(CodeArea codeArea) { super(ActionModel.FIND_USAGE_PLUS, codeArea); } @Override public void runAction(JNode node) { UsageDialogPlus.open(getCodeArea().getMainWindow(), node); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/mode/JCodeMode.java ================================================ package jadx.gui.ui.codearea.mode; import javax.swing.Icon; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.jetbrains.annotations.Nullable; import jadx.api.DecompilationMode; import jadx.api.ICodeInfo; import jadx.core.dex.nodes.ClassNode; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; public class JCodeMode extends JNode { private final JClass jCls; private final DecompilationMode mode; private @Nullable ICodeInfo codeInfo; public JCodeMode(JClass jClass, DecompilationMode mode) { this.jCls = jClass; this.mode = mode; } @Override public JClass getJParent() { return jCls.getJParent(); } @Override public Icon getIcon() { return jCls.getIcon(); } @Override public String makeString() { return jCls.makeString(); } @Override public ICodeInfo getCodeInfo() { if (codeInfo != null) { return codeInfo; } ClassNode cls = jCls.getCls().getClassNode(); codeInfo = cls.decompileWithMode(mode); return codeInfo; } @Override public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_JAVA; } @Override public String getName() { return jCls.getName(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodeMetadataRange.java ================================================ package jadx.gui.ui.codearea.sync; import java.util.Map; import jadx.api.metadata.ICodeAnnotation; /** * Marks the start and end of annotation within a CodeMetadataStorage */ public class CodeMetadataRange { // Use Map.Entry here because Java has no built in tuple/pair utility private final Map.Entry start; private final Map.Entry end; CodeMetadataRange( Map.Entry start, Map.Entry end) { this.start = start; this.end = end; } Map.Entry getStart() { return start; } Map.Entry getEnd() { return end; } @Override public String toString() { return "CodeMetadataRange{start=" + start.getKey() + "->" + start.getValue() + ",end=" + end.getKey() + "->" + end.getValue() + "}"; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncee.java ================================================ package jadx.gui.ui.codearea.sync; /** * Accepts a code panel syncer for syncing code areas */ public interface CodePanelSyncee { boolean sync(CodePanelSyncer syncer); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncer.java ================================================ package jadx.gui.ui.codearea.sync; public interface CodePanelSyncer extends IToJavaSyncStrategy, IToSmaliSyncStrategy { } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncerAbstractFactory.java ================================================ package jadx.gui.ui.codearea.sync; public interface CodePanelSyncerAbstractFactory { CodePanelSyncer createCodePanelSyncer(); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodeSyncHighlighter.java ================================================ package jadx.gui.ui.codearea.sync; import java.awt.Color; import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultHighlighter; import javax.swing.text.Highlighter; import javax.swing.text.Highlighter.HighlightPainter; import jadx.gui.ui.codearea.AbstractCodeArea; /** * Highlighting and scrolling utility into a CodeArea for a given color */ public class CodeSyncHighlighter { private final Color color; public CodeSyncHighlighter(Color color) { this.color = color; } public void highlightAndScrollToLine(AbstractCodeArea area, int lineIndex) throws BadLocationException { highlightLine(area, lineIndex); area.scrollToPos(area.getLineStartOffset(lineIndex)); } public void highlightLine(AbstractCodeArea area, int lineIndex) throws BadLocationException { int startOffset = area.getLineStartOffset(lineIndex); int endOffset = area.getLineEndOffset(lineIndex); highlightRange(area, startOffset, endOffset); } // Highlight range in code area with a temporary yellow highlight public void highlightRange(AbstractCodeArea area, int startOffset, int endOffset) throws BadLocationException { Highlighter hl = area.getHighlighter(); HighlightPainter painter = new DefaultHighlighter.DefaultHighlightPainter(this.color); Object tag = hl.addHighlight(startOffset, endOffset, painter); new Timer(1000, e -> hl.removeHighlight(tag)).start(); } public static CodeSyncHighlighter defaultHighlighter() { return new CodeSyncHighlighter(UIManager.getColor("TabbedPane.hoverColor")); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/DebugLineJavaSyncer.java ================================================ package jadx.gui.ui.codearea.sync; import java.util.Map; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.codearea.SmaliArea; /** * Use debug line info from dex to correlate from java to java/smali */ public class DebugLineJavaSyncer implements IToSmaliSyncStrategy, IToJavaSyncStrategy { private static final Logger LOG = LoggerFactory.getLogger(DebugLineJavaSyncer.class); private final CodeArea from; public DebugLineJavaSyncer(CodeArea area) { this.from = area; } @Override public boolean syncTo(CodeArea to) { // This might be any combination between java/simple/fallback // We cannot just rely on the current line. // Instead try to correlate with line mappings. try { int lineIndex = from.getCaretLineNumber(); Map toLineMapping = to.getFunctionUniqueLineMappings(); // lineIndex is 0-indexed whereas the line mappings are based off a 1-index. Integer sourceLine = getClosestSourceLine(lineIndex + 1); if (sourceLine == null) { return false; } // find the equivalent linenumber in the 'to' by a reverse lookup from the source line for (Map.Entry entry : toLineMapping.entrySet()) { int toLine = entry.getKey(); int candidateSourceLine = entry.getValue(); if (sourceLine == candidateSourceLine) { // we have the mapped line we target the lineIndex which is a 0-index CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, toLine - 1); LOG.info("{} - successful sync of code to code", LOG.getName()); return true; } } } catch (Exception e) { LOG.error("{} - Failed to sync from CodeArea to CodeArea: {}", LOG.getName(), e.getLocalizedMessage()); } return false; } @Override public boolean syncTo(SmaliArea to) { try { int lineIndex = from.getCaretLineNumber(); // lineIndex is 0-indexed but the line mappings are based of 1-indexed line numbers. int lineNum = lineIndex + 1; Integer sourceLine = getClosestSourceLine(lineNum); if (sourceLine == null) { to.removeAllLineHighlights(); LOG.debug("decompiled line {} not mapped to source line", lineNum); return false; } // find the smali line where ".line " is LOG.debug("Finding \".line {}\" in smali", sourceLine); int smaliLine = findSmaliLineIndex(to, sourceLine); if (smaliLine < 0) { LOG.warn("{} - Source line {} not annotated in Smali", LOG.getName(), sourceLine); return false; } CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, smaliLine); LOG.info("{} - successful sync of code to smali", LOG.getName()); return true; } catch (Exception ex) { LOG.error("{} - Failed to sync CodeArea to SmaliArea: {}", LOG.getName(), ex.getLocalizedMessage()); } return false; } private @Nullable Integer getClosestSourceLine(int lineNum) { // get the line mappings of the Java/Simple/Fallback code Map lineMapping = from.getFunctionUniqueLineMappings(); if (lineMapping == null || lineMapping.isEmpty()) { return null; } // get the source line from the decomp line Integer sourceLine = null; // Some of the intermediate lines are not mapped so keep going back until we find one // e.g. multiple instruction lines in the 'Simple' view belong to a single source line while (lineNum >= 0 && (sourceLine = lineMapping.get(lineNum)) == null) { --lineNum; } return sourceLine; } /** * find the ".line \d+" line in the smali */ private static int findSmaliLineIndex(SmaliArea smaliArea, int sourceLine) { String line = ".line " + Integer.toString(sourceLine); String[] smaliLines = smaliArea.getText().split("\\R"); for (int i = 0; i < smaliLines.length; ++i) { String l = smaliLines[i]; if (l.trim().equals(line)) { return i; } } return -1; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/DebugLineSmaliSyncer.java ================================================ package jadx.gui.ui.codearea.sync; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.codearea.SmaliArea; /** * Use Debug lines in smali from dex debug info to correlate with code */ public class DebugLineSmaliSyncer implements IToJavaSyncStrategy { private static final Logger LOG = LoggerFactory.getLogger(DebugLineSmaliSyncer.class); private final SmaliArea from; public DebugLineSmaliSyncer(SmaliArea area) { this.from = area; } @Override public boolean syncTo(CodeArea to) { try { // Get the from lines and currentline index int lineIndex = from.getCaretLineNumber(); String[] fromLines = from.getText().split("\\R"); if (lineIndex >= fromLines.length) { return false; } // find an Anchor to guide what to look for and highlight in the CodeArea Anchor anchor = findNearestAnchor(lineIndex, fromLines); if (anchor == null) { LOG.error("{} - No Smali Anchor found", LOG.getName()); return false; } if (anchor.getType() == Anchor.Type.SOURCE_LINE) { LOG.debug(anchor.toString()); Map toDecompToSourceMapping = to.getFunctionUniqueLineMappings(); for (Map.Entry entry : toDecompToSourceMapping.entrySet()) { int decompLine = entry.getKey(); int sourceLine = entry.getValue(); if (anchor.getCodeMappedLineNumber() == sourceLine) { int decompLineIndex = decompLine - 1; LOG.debug("Highlighting {} on {}", decompLine, to); CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, decompLineIndex); LOG.info("{} - successful sync of smali to code", LOG.getName()); return true; } } } to.removeAllLineHighlights(); } catch (Exception ex) { LOG.error("{} - Failed to sync from Smali to Code", LOG.getName(), ex); } return false; } @Nullable private Anchor findNearestAnchor(int smaliLineNumber, String[] lines) { for (int i = smaliLineNumber; i >= 0; i--) { String trimmedLine = lines[i].trim(); if (trimmedLine.startsWith(".line")) { return new Anchor(Anchor.Type.SOURCE_LINE, trimmedLine, i); } if (trimmedLine.startsWith(".method")) { return new Anchor(Anchor.Type.METHOD_START, trimmedLine, i); } if (trimmedLine.startsWith(".end")) { return new Anchor(Anchor.Type.METHOD_END, trimmedLine, i); } if (trimmedLine.startsWith(".field")) { return new Anchor(Anchor.Type.FIELD, trimmedLine, i); } if (trimmedLine.startsWith(".class")) { return new Anchor(Anchor.Type.CLASS, trimmedLine, smaliLineNumber); } } return null; } /** * Line in the smali that can be used to find a section to highlight in the code area */ private static class Anchor { public enum Type { SOURCE_LINE, METHOD_START, METHOD_END, FIELD, CLASS } private final Type type; private final String line; private final int smaliLineNumber; private int codeMappedLineNumber = -1; public Anchor(Type type, String line, int smaliLineNumber) { this.type = type; this.line = line; this.smaliLineNumber = smaliLineNumber; this.map(); } public Type getType() { return type; } public int getCodeMappedLineNumber() { return codeMappedLineNumber; } private void map() { switch (type) { case SOURCE_LINE: Pattern p = Pattern.compile("(\\.line\\s)(\\d+)"); Matcher m = p.matcher(line); if (m.find()) { codeMappedLineNumber = Integer.parseInt(m.group(2)); } break; default: codeMappedLineNumber = -1; break; } } @Override public String toString() { return String.format("Anchor %s, %d, %d", type.name(), smaliLineNumber, codeMappedLineNumber); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/IToJavaSyncStrategy.java ================================================ package jadx.gui.ui.codearea.sync; import jadx.gui.ui.codearea.CodeArea; public interface IToJavaSyncStrategy { boolean syncTo(CodeArea area); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/IToSmaliSyncStrategy.java ================================================ package jadx.gui.ui.codearea.sync; import jadx.gui.ui.codearea.SmaliArea; public interface IToSmaliSyncStrategy { boolean syncTo(SmaliArea area); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/InsnOffsetJavaSyncer.java ================================================ package jadx.gui.ui.codearea.sync; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.core.dex.nodes.MethodNode; import jadx.gui.device.debugger.DbgUtils; import jadx.gui.device.debugger.smali.SmaliMethodNode; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.codearea.SmaliArea; /** * Use insn code offsets to sync code panel area to code/smali * This only works for Smali when SmaliArea is showing the dalvik bytecode. */ public class InsnOffsetJavaSyncer implements IToJavaSyncStrategy, IToSmaliSyncStrategy { private static final Logger LOG = LoggerFactory.getLogger(InsnOffsetJavaSyncer.class); private final CodeArea from; public InsnOffsetJavaSyncer(CodeArea area) { this.from = area; } @Override public boolean syncTo(SmaliArea to) { if (!to.isShowingDalvikBytecode()) { return false; } // 1. Find the Method start and end boundaries enclosing the caret position in the code metadata // 2. Find the closest InsnCodeOffset range within the method boundary corresponding to the caret // position // 3. Get all of the smali lines which fall within the InsnCodeOffset range. // 4. Highlight those found in 3. and scroll to the first one. int caretPos = from.getCaretPosition(); CodeMetadataRange mthRange = findEnclosingMethodRange(caretPos); if (mthRange == null) { return false; } Integer mthDefPos = mthRange.getStart().getKey(); Integer mthEndPos = mthRange.getEnd().getKey(); LOG.debug("InsnOffsetJavaSyncer caretPos = {}", caretPos); LOG.debug("InsnOffsetJavaSyncer mthDefPos = {}", mthDefPos); LOG.debug("InsnOffsetJavaSyncer mthEndPos = {}", mthEndPos); CodeMetadataRange insnOffsetRange = findOffsetRange(caretPos, mthDefPos, mthEndPos); if (insnOffsetRange == null) { return false; } String mthID = getMthRawFullID(mthDefPos); SmaliMethodNode smaliMthNode = DbgUtils.getSmaliMethodNode(to.getJClass(), mthID); if (smaliMthNode == null) { LOG.error("{} - mth ID {} not mapped to a SmaliMethodNode", LOG.getName(), mthID); return false; } List smaliLines = getMappedSmaliLines(smaliMthNode, insnOffsetRange); if (smaliLines.size() < 2) { return false; } try { CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, smaliLines.get(0)); for (int i = 1; i < smaliLines.size(); ++i) { CodeSyncHighlighter.defaultHighlighter().highlightLine(to, smaliLines.get(i)); } LOG.info("{} - successful sync of code to smali", LOG.getName()); return true; } catch (Exception ex) { LOG.error("{} - Failed to sync code to smali with instruction offsets ", LOG.getName(), ex); } return false; } @Override public boolean syncTo(CodeArea to) { int caretPos = from.getCaretPosition(); CodeMetadataRange fromMthRange = findEnclosingMethodRange(caretPos); if (fromMthRange == null) { return false; } Integer mthDefPos = fromMthRange.getStart().getKey(); Integer mthEndPos = fromMthRange.getEnd().getKey(); LOG.debug("InsnOffsetJavaSyncer caretPos = {}", caretPos); LOG.debug("InsnOffsetJavaSyncer mthDefPos = {}", mthDefPos); LOG.debug("InsnOffsetJavaSyncer mthEndPos = {}", mthEndPos); CodeMetadataRange fromInsnOffsetRange = findOffsetRange(caretPos, mthDefPos, mthEndPos); if (fromInsnOffsetRange == null) { return false; } String mthID = getMthRawFullID(mthDefPos); // now search for this range within the target area CodeMetadataRange toMthRange = findMethodRange(mthID, to); if (toMthRange == null) { return false; } // search for the first insn offset int firstInsnOffset = ((InsnCodeOffset) fromInsnOffsetRange.getStart().getValue()).getOffset(); Integer highlightPosStart = to.getCodeMetadata().searchDown(toMthRange.getStart().getKey(), (offset, ann) -> { if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) { return null; } int pos = ((InsnCodeOffset) ann).getOffset(); if (pos != firstInsnOffset) { return null; } return offset; }); if (highlightPosStart == null) { return false; } // search for the second insn offset int secondInsnOffset = ((InsnCodeOffset) fromInsnOffsetRange.getEnd().getValue()).getOffset(); Integer highlightPosEnd = to.getCodeMetadata().searchDown(highlightPosStart, (offset, ann) -> { if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) { return null; } int pos = ((InsnCodeOffset) ann).getOffset(); if (pos != secondInsnOffset) { return null; } return offset; }); if (highlightPosEnd == null) { return false; } to.scrollToPos(highlightPosStart); try { CodeSyncHighlighter.defaultHighlighter().highlightRange(to, highlightPosStart, highlightPosEnd); LOG.info("{} - successful sync of code to code", LOG.getName()); return true; } catch (Exception ex) { LOG.error("{} - Unable to highlight code area from insn offset mappings {} -> {}", LOG.getName(), highlightPosStart, highlightPosEnd); } return false; } @Nullable private static CodeMetadataRange findMethodRange(String mthFullRawID, CodeArea area) { Map.Entry toMthDecl = area.getCodeMetadata().searchDown(0, (offset, ann) -> { if (ann.getAnnType() != ICodeAnnotation.AnnType.DECLARATION) { return null; } NodeDeclareRef decl = (NodeDeclareRef) ann; ICodeNodeRef node = decl.getNode(); if (node.getAnnType() != ICodeAnnotation.AnnType.METHOD) { return null; } MethodNode mth = (MethodNode) node; if (!mth.getMethodInfo().getRawFullId().equals(mthFullRawID)) { return null; } return new SimpleEntry<>(offset, ann); }); if (toMthDecl == null) { return null; } Map.Entry toMthEnd = area.getCodeMetadata().searchDown(toMthDecl.getKey(), (offset, ann) -> { if (ann.getAnnType() != ICodeAnnotation.AnnType.END) { return null; } return new SimpleEntry<>(offset, ann); }); if (toMthEnd == null) { return null; } return new CodeMetadataRange(toMthDecl, toMthEnd); } @Nullable private CodeMetadataRange findEnclosingMethodRange(Integer startPos) { Map.Entry mthDef = from.getCodeMetadata().searchUp(startPos, (offset, ann) -> { if (ann.getAnnType() != ICodeAnnotation.AnnType.DECLARATION) { return null; } NodeDeclareRef decl = (NodeDeclareRef) ann; ICodeNodeRef node = decl.getNode(); if (node.getAnnType() != ICodeAnnotation.AnnType.METHOD) { return null; } return new SimpleEntry<>(offset, ann); }); if (mthDef == null) { return null; } Map.Entry mthEnd = from.getCodeMetadata().searchDown(startPos, (offset, ann) -> { if (ann.getAnnType() != ICodeAnnotation.AnnType.END) { return null; } return new SimpleEntry<>(offset, ann); }); if (mthEnd == null) { return null; } return new CodeMetadataRange(mthDef, mthEnd); } /** * Gets a CodeMetadataRange for the from CodeArea where start and end * are InsnCodeOffsets whose offsets are monotonically increasing. * * @param - startPos the starting position to start searching from * @param - mthDefPos the method node decl position enclosing the range * @param - mthEndPos the method end position enclosing the range */ @Nullable private CodeMetadataRange findOffsetRange(Integer startPos, Integer mthDefPos, Integer mthEndPos) { Map.Entry first = findInsnOffsetBeforePos(startPos, mthDefPos); Map.Entry second = findInsnOffsetAfterPos(startPos, mthEndPos); if (first == null || second == null) { LOG.warn("{} - Unable to find InsnCodeOffsets between {} -> {}", LOG.getName(), mthDefPos, mthEndPos); return null; } int startOffset = ((InsnCodeOffset) first.getValue()).getOffset(); int endOffset = ((InsnCodeOffset) second.getValue()).getOffset(); if (startOffset > endOffset) { LOG.warn("{} - insn startOffset={} is greater than insn endOffset={} - cannot construct range", LOG.getName(), startOffset, endOffset); return null; } return new CodeMetadataRange(first, second); } @Nullable private Map.Entry findInsnOffsetBeforePos(Integer startPos, Integer limit) { return from.getCodeMetadata().searchUp(startPos, (offset, ann) -> { if (offset <= limit) { return null; } if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) { return null; } return new SimpleEntry(offset, ann); }); } @Nullable private Map.Entry findInsnOffsetAfterPos(Integer startPos, Integer limit) { return from.getCodeMetadata().searchDown(startPos, (offset, ann) -> { if (offset >= limit) { return null; } if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) { return null; } return new SimpleEntry(offset, ann); }); } /** * Assumes that there is a NodeDeclareRef{MethodNode{}} annotation at mthDefPos in the `from` * CodeInfoMetadata */ private String getMthRawFullID(Integer mthDefPos) { ICodeAnnotation ann = from.getCodeMetadata().getAt(mthDefPos); NodeDeclareRef ref = (NodeDeclareRef) ann; MethodNode mth = (MethodNode) ref.getNode(); return mth.getMethodInfo().getRawFullId(); } /** * Gets the mapped smali line indices for the code offsets of interest * * @param smaliMethodNode - method of interest * @param insnCodeOffsetRange - code offset range from the caret pos * @return */ private static List getMappedSmaliLines( SmaliMethodNode smaliMethodNode, CodeMetadataRange insnCodeOffsetRange) { List lines = new ArrayList<>(); int startInsnCodeOffset = ((InsnCodeOffset) insnCodeOffsetRange.getStart().getValue()).getOffset(); int endInsnCodeOffset = ((InsnCodeOffset) insnCodeOffsetRange.getEnd().getValue()).getOffset(); // Line mappings are Line index -> Code offset Map smaliLineMapping = smaliMethodNode.getLineMapping(); LOG.debug("startInsnPos={}, endInsnPos={}", startInsnCodeOffset, endInsnCodeOffset); for (Map.Entry lineToCodeOffset : smaliLineMapping.entrySet()) { LOG.debug("line={} -> codeOffset={}", lineToCodeOffset.getKey(), lineToCodeOffset.getValue()); // Asume code offsets from smali debug utils are the same as those in the code metadata if (lineToCodeOffset.getValue() == startInsnCodeOffset || lineToCodeOffset.getValue() == endInsnCodeOffset) { lines.add(lineToCodeOffset.getKey()); } } Collections.sort(lines); // only two elements return lines; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/InsnOffsetSmaliSyncer.java ================================================ package jadx.gui.ui.codearea.sync; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NavigableMap; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.core.dex.nodes.MethodNode; import jadx.gui.device.debugger.DbgUtils; import jadx.gui.treemodel.JClass; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.codearea.SmaliArea; /* * Use insn code offsets to sync smali to code panel area * This only works for Smali when the SmaliArea is showing the dalvik bytecode */ public class InsnOffsetSmaliSyncer implements IToJavaSyncStrategy { private static final Logger LOG = LoggerFactory.getLogger(InsnOffsetSmaliSyncer.class); private final SmaliArea from; public InsnOffsetSmaliSyncer(SmaliArea area) { this.from = area; } @Override public boolean syncTo(CodeArea to) { if (!from.isShowingDalvikBytecode()) { // This strategy can only be used when the debug model has been used to generate the smali. // This populates the code offsets by line as opposed to just text. return false; } // 1. Get the code offset from the Smali caret line number // 2. Find the appropriate NodeDeclareRef for the method enclosed in the CodeArea annotations // 3. Find all code offset range intervals in the map which contain the code offset // 4. Get the CodeArea positions of these intervals and hightlight them in the code area // 5. Scroll to the first one. JClass jclass = from.getJClass(); Map.Entry lineInfo = DbgUtils.getCodeOffsetInfoByLine(jclass, from.getCaretLineNumber()); if (lineInfo == null) { return false; } Integer lineInfoPos = lineInfo.getValue(); LOG.debug("lineInfo key {}, lineInfo value {}, caretLineNumber {}", lineInfo.getKey(), lineInfo.getValue(), from.getCaretLineNumber()); ICodeMetadata toMetadata = to.getCodeMetadata(); NavigableMap codeAreaAnnotationMap = (NavigableMap) toMetadata.getAsMap(); Iterator> methodDecl = findMethodDeclAnnotation(codeAreaAnnotationMap, lineInfo.getKey()); if (methodDecl == null) { LOG.warn("{} - No NodeDeclareRef exists for {}", LOG.getName(), lineInfo.getKey()); return false; } // Looking through the annotations in order from the Method declaration to its end // compare every adjacent pair of instruction offsets where the second is greater than the first. // Highlight if the smali offset falls between the second and the first. Iterator> it = methodDecl; NavigableMap.Entry prev = null; List offsetBoundariesToHighlight = new ArrayList<>(); while (it.hasNext()) { NavigableMap.Entry entry = it.next(); if (entry.getValue().getAnnType() == ICodeAnnotation.AnnType.END) { break; } if (entry.getValue().getAnnType() != ICodeAnnotation.AnnType.OFFSET) { continue; } if (prev != null) { InsnCodeOffset currentInsnOffset = (InsnCodeOffset) entry.getValue(); InsnCodeOffset prevInsnOffset = (InsnCodeOffset) prev.getValue(); if (prevInsnOffset.getOffset() <= lineInfoPos && lineInfoPos <= currentInsnOffset.getOffset()) { offsetBoundariesToHighlight.add(new CodeMetadataRange(prev, entry)); } } prev = entry; } if (offsetBoundariesToHighlight.isEmpty()) { return false; } to.scrollToPos(offsetBoundariesToHighlight.get(0).getStart().getKey()); try { for (CodeMetadataRange cmr : offsetBoundariesToHighlight) { LOG.debug("Highlighting {}", cmr); CodeSyncHighlighter.defaultHighlighter().highlightRange(to, cmr.getStart().getKey(), cmr.getEnd().getKey()); } LOG.info("{} - successful sync of smali to code", LOG.getName()); return true; } catch (Exception ex) { LOG.error("{} - Unable to highlight smali -> code insn offset range: {}", LOG.getName(), ex.getLocalizedMessage()); } return false; } /** * Find the NodeDeclareRef annotation of the method identified by smaliLineMthFullID * * @param map the annotation map from the CodeArea * @param smaliLineMthFullID the raw full method ID to look for * @return iterator to the entry in the annotation map */ @Nullable private static Iterator> findMethodDeclAnnotation( NavigableMap map, String smaliLineMthFullID) { // Ensure we use NavigableMap here to get ordering guarantee from iterator call Iterator> it = map.descendingMap().entrySet().iterator(); while (it.hasNext()) { NavigableMap.Entry entry = it.next(); if (entry.getValue() instanceof NodeDeclareRef) { NodeDeclareRef nodeDeclareRef = (NodeDeclareRef) entry.getValue(); if (nodeDeclareRef.getNode() instanceof MethodNode) { MethodNode mth = (MethodNode) nodeDeclareRef.getNode(); if (mth.getMethodInfo().getRawFullId().equals(smaliLineMthFullID)) { return it; } } } } return null; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/JavaSyncer.java ================================================ package jadx.gui.ui.codearea.sync; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.codearea.SmaliArea; /** * Syncs a Java code panel area (Java/Simple/Fallback) to another area */ public class JavaSyncer implements CodePanelSyncer { private static final Logger LOG = LoggerFactory.getLogger(JavaSyncer.class); private final DebugLineJavaSyncer debugLineSyncer; private final InsnOffsetJavaSyncer insnOffsetSyncer; public JavaSyncer(CodeArea area) { this.debugLineSyncer = new DebugLineJavaSyncer(area); this.insnOffsetSyncer = new InsnOffsetJavaSyncer(area); } @Override public boolean syncTo(CodeArea to) { return debugLineSyncer.syncTo(to) || insnOffsetSyncer.syncTo(to); } @Override public boolean syncTo(SmaliArea to) { return debugLineSyncer.syncTo(to) || insnOffsetSyncer.syncTo(to); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/SmaliSyncer.java ================================================ package jadx.gui.ui.codearea.sync; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.codearea.SmaliArea; /** * Syncs a Smali code panel area to another area */ public class SmaliSyncer implements CodePanelSyncer { private static final Logger LOG = LoggerFactory.getLogger(SmaliSyncer.class); private final SmaliArea from; private final InsnOffsetSmaliSyncer insnOffsetSyncer; private final DebugLineSmaliSyncer debugLineSyncer; public SmaliSyncer(SmaliArea area) { this.from = area; this.insnOffsetSyncer = new InsnOffsetSmaliSyncer(area); this.debugLineSyncer = new DebugLineSmaliSyncer(area); } @Override public boolean syncTo(CodeArea to) { // first try debug lines then insn offsets return debugLineSyncer.syncTo(to) || insnOffsetSyncer.syncTo(to); } @Override public boolean syncTo(SmaliArea to) { if (from.isShowingDalvikBytecode() == to.isShowingDalvikBytecode()) { // smali -> smali just highlight the current line but only if content is the same to.scrollToPos(from.getLineStartOffsetOfCurrentLine()); } return true; // Prevent fallback syncing } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/AbstractCodeAreaLine.java ================================================ package jadx.gui.ui.codearea.sync.fallback; import javax.swing.text.BadLocationException; import org.jetbrains.annotations.Nullable; import jadx.gui.ui.codearea.AbstractCodeArea; abstract class AbstractCodeAreaLine { private final AbstractCodeArea area; private final int lineIndex; private final String line; protected AbstractCodeAreaLine(AbstractCodeArea area, int lineIndex) throws BadLocationException { this.area = area; this.lineIndex = lineIndex; this.line = this.area.getText().split("\\R")[lineIndex]; } public AbstractCodeArea getArea() { return area; } public int getLineIndex() { return lineIndex; } public String getStr() { return line; } public String getTrimmedStr() { return line.trim(); } public abstract AbstractCodeAreaLine getLineAt(int lineIndex) throws BadLocationException; public abstract boolean isClassDeclaration(); public abstract boolean isMethodOrConstructorDeclaration(); public abstract boolean isFieldDeclaration(); @Nullable public abstract String extractDeclaredMethodName(); @Nullable public abstract String extractDeclaredClassName(); protected abstract MethodDeclaration createMethodDeclaration() throws FallbackSyncException; /** * This could be itself or: * - the enclosing method delcaration if line is in a method * - the enclosing class declaration if line is a field declaration */ public IDeclaration getEnclosingScopeDeclaration() throws BadLocationException, FallbackSyncException { IDeclaration decl = this.getDeclaration(); if (decl != null) { return decl; } for (int i = lineIndex - 1; i >= 0; i--) { AbstractCodeAreaLine line = getLineAt(i); boolean enclosingDecl = line.isScopeDeclarationLine(); if (enclosingDecl) { return line.getDeclaration(); } } throw new FallbackSyncException("No enclosing declaration found for " + this); } public boolean isScopeDeclarationLine() { return isClassDeclaration() || isMethodOrConstructorDeclaration(); } public boolean isDeclarationLine() { return isScopeDeclarationLine() || isFieldDeclaration(); } @Nullable public IDeclaration getDeclaration() throws FallbackSyncException { if (isClassDeclaration()) { return new ClassDeclaration(this); } if (isMethodOrConstructorDeclaration()) { return createMethodDeclaration(); } return null; } @Override public String toString() { return line; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/AbstractCodeAreaToken.java ================================================ package jadx.gui.ui.codearea.sync.fallback; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.text.BadLocationException; import jadx.gui.ui.codearea.AbstractCodeArea; public abstract class AbstractCodeAreaToken { protected final AbstractCodeArea area; private final int atPos; protected int startPos; protected int length; protected AbstractCodeAreaToken(AbstractCodeArea area, int at) throws BadLocationException, FallbackSyncException { this.area = area; this.atPos = at; this.extractTokenAt(); } public int getAtPos() { return atPos; } public String getStr() throws BadLocationException { return area.getText(this.startPos, this.length); } public boolean isMethodConstructorDeclarationOrCall() throws BadLocationException { return area.getText(this.startPos + this.length, 1).equals("("); } // Class field reference within a method public abstract boolean isFieldReference() throws BadLocationException; // Class field token in class field declaration public abstract boolean isClassField() throws BadLocationException; public abstract AbstractCodeAreaLine getLine() throws BadLocationException; // Helper to extract token under caret (at pos) private void extractTokenAt() throws FallbackSyncException, BadLocationException { String text = area.getText(); if (text == null || text.isEmpty()) { throw new FallbackSyncException("text area is null or empty"); } // Find word boundaries around caretPos int start = atPos; int end = atPos; while (start > 0 && Character.isJavaIdentifierPart(text.charAt(start - 1))) { start--; } while (end < text.length() && Character.isJavaIdentifierPart(text.charAt(end))) { end++; } if (start == end) { // No identifier found, try string literal at caret line int line = area.getLineOfOffset(atPos); String lineText = area.getText(area.getLineStartOffset(line), area.getLineEndOffset(line) - area.getLineStartOffset(line)); Pattern p = Pattern.compile("\"([^\"]*)\""); Matcher m = p.matcher(lineText); while (m.find()) { int litStart = area.getLineStartOffset(line) + m.start(1); int litEnd = area.getLineStartOffset(line) + m.end(1); if (atPos >= litStart && atPos <= litEnd) { this.startPos = m.start(1); this.length = m.end(1) - m.start(1); return; } } throw new FallbackSyncException("Unable to extract token at position " + atPos); } this.startPos = start; this.length = end - start; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/ClassDeclaration.java ================================================ package jadx.gui.ui.codearea.sync.fallback; import java.util.Objects; public class ClassDeclaration implements IDeclaration { private final AbstractCodeAreaLine line; private final String name; public ClassDeclaration(AbstractCodeAreaLine line) throws FallbackSyncException { this.name = line.extractDeclaredClassName(); if (this.name == null) { throw new FallbackSyncException("line does not declare a class: " + toString()); } this.line = line; } @Override public String getIdentifyingName() { return name; } @Override public AbstractCodeAreaLine getLine() { return line; } @Override public boolean equals(Object o) { if (o instanceof ClassDeclaration) { ClassDeclaration cd = (ClassDeclaration) o; return this.getIdentifyingName().equals(cd.getIdentifyingName()); } return false; } // Not necessary but removes checkstyle warning @Override public int hashCode() { return Objects.hash(line, name); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/FallbackSyncException.java ================================================ package jadx.gui.ui.codearea.sync.fallback; public class FallbackSyncException extends Exception { public FallbackSyncException(String msg) { super(msg); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/FallbackSyncer.java ================================================ package jadx.gui.ui.codearea.sync.fallback; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.text.BadLocationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.ui.codearea.CodePanel; import jadx.gui.ui.codearea.SmaliArea; import jadx.gui.ui.codearea.sync.CodeSyncHighlighter; /** * Regex/String based sync strategy of toPanel when clicking in fromPanel * Summary of syncing strategy: * 1) Look for an identifying class member token under the caret position. * 2) If found look for the enclosing method or class declaration. * 3) If the line is a declaration line, find the equivalent line in the other code panel. * 4) Otherwise find the nth occurence of the token in the enclosing method/class in the other code * panel. * The following are not yet supported: * - generic classes/methods * - anonymous classes * - lambda functions * - constructors */ public class FallbackSyncer { private static final Logger LOG = LoggerFactory.getLogger(FallbackSyncer.class); public static boolean sync(CodePanel fromPanel, CodePanel toPanel) throws BadLocationException, Exception { LOG.debug("FALLBACK SYNC START"); try { AbstractCodeArea from = fromPanel.getCodeArea(); AbstractCodeArea to = toPanel.getCodeArea(); int caretPos = from.getCaretPosition(); int lineIndex = from.getLineOfOffset(caretPos); String[] fromLines = from.getText().split("\\R"); if (lineIndex >= fromLines.length) { return false; } String caretLine = fromLines[lineIndex]; LOG.debug("Caret line [{}]: {}", caretPos, caretLine); // Extract token under caret (string literal or identifier) AbstractCodeAreaToken areaToken = FallbackSyncer.getToken(from, caretPos); String token = areaToken.getStr(); LOG.debug("Token at caret: '{}'", token); if (token == null || token.isEmpty()) { return false; } if (!allowSync(areaToken)) { LOG.debug("Fallback matching only applicable for variable, classname, field or method tokens"); return false; } return syncToIdentifyingNthOccurence(areaToken, to); } finally { LOG.debug("FALLBACK SYNC END"); } } // This function just serves as a way to create the correct Token type // FallbackSyncer should be refactored to use CodePanelSyncer private static AbstractCodeAreaToken getToken(AbstractCodeArea from, int caretPos) throws BadLocationException, FallbackSyncException { if (from instanceof SmaliArea) { return new SmaliAreaToken((SmaliArea) from, caretPos); } if (from instanceof CodeArea) { return new JavaCodeAreaToken((CodeArea) from, caretPos); } throw new FallbackSyncException("Unknown AbstractCodeArea type for " + from); } /** * Looks for the nth occurence of the token in the enclosing class/method scope in the `to` area. * If found, sync to it in the `to` area. */ private static boolean syncToIdentifyingNthOccurence(AbstractCodeAreaToken sourceToken, AbstractCodeArea to) throws BadLocationException, FallbackSyncException { AbstractCodeAreaLine tokenLine = sourceToken.getLine(); // Locate the method/class declaration line for context IDeclaration fromDeclaration = tokenLine.getEnclosingScopeDeclaration(); if (fromDeclaration == null) { LOG.warn("Unable to find declaration line above {}", tokenLine); return false; } AbstractCodeAreaLine fromDeclaringLine = fromDeclaration.getLine(); AbstractCodeArea from = fromDeclaringLine.getArea(); String declarationLineStr = fromDeclaringLine.getStr(); LOG.debug("Found declaration line: {}", declarationLineStr); String nameToFind = fromDeclaration.getIdentifyingName(); if (nameToFind == null || nameToFind.isEmpty()) { return false; } // Determine whether we're matching a class or method boolean isClass = fromDeclaringLine.isClassDeclaration(); String regex = isClass ? generateClassRegex(nameToFind) : generateMethodRegex(nameToFind); // Find the declaration in target text Matcher matcher = Pattern.compile(regex).matcher(to.getText()); LOG.debug("Searching for {} in targetText, isClass {}", nameToFind, isClass); AbstractCodeAreaLine targetDeclLine = findTargetDeclaringLine(to, matcher, fromDeclaration); if (targetDeclLine == null) { LOG.debug("Cannot find target declaration line"); return false; } int targetDeclarationLineIndex = targetDeclLine.getLineIndex(); LOG.debug("Target declaration line {}", targetDeclLine.getStr()); if (tokenLine.isScopeDeclarationLine()) { CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, targetDeclarationLineIndex); LOG.info("{} - Highlighted target declaration line", LOG.getName(), targetDeclLine.getStr()); return true; } // Extract the method/class body from target String methodBody = extractMethodBody(to, matcher.start()); // Find nth occurrence of token in source method // Extract method body from source (to count occurrences) Matcher fromMatcher = Pattern.compile(regex).matcher(from.getText()); if (!fromMatcher.find()) { LOG.debug("No method/class match found in source for regex: {}", regex); return false; } String sourceMethodBody = extractMethodBody(from, fromMatcher.start()); // Count which occurrence of token the caret corresponds to in the source method body String tokenStr = sourceToken.getStr(); int caretPos = sourceToken.getAtPos(); int caretOffsetInMethod = caretPos - fromMatcher.start(); int nthOccurrence = 0; Pattern tokenPattern = Pattern.compile("\"" + Pattern.quote(tokenStr) + "\"|\\b" + Pattern.quote(tokenStr) + "\\b"); Matcher tokenMatcher = tokenPattern.matcher(sourceMethodBody); while (tokenMatcher.find()) { if (tokenMatcher.start() > caretOffsetInMethod) { break; } nthOccurrence++; } LOG.debug("Caret is at occurrence number: {}", nthOccurrence); // Now find nth occurrence of token in target method body tokenMatcher = tokenPattern.matcher(methodBody); int occurrenceCount = 0; while (tokenMatcher.find()) { occurrenceCount++; if (occurrenceCount == nthOccurrence) { // Find absolute offset of this line in targetText int tokenPosInMethod = tokenMatcher.start(); int absoluteOffset = matcher.start() + tokenPosInMethod; // Find line start and end offset in target int tokenLineIndex = to.getLineOfOffset(absoluteOffset); CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, tokenLineIndex); LOG.info("{} - Highlighted token '{}' at nth occurrence: {}", LOG.getName(), tokenStr, nthOccurrence); return true; } } LOG.debug("No matching token or instruction found in method: {}", nameToFind); return false; } private static AbstractCodeAreaLine findTargetDeclaringLine( AbstractCodeArea to, // target area Matcher matcher, // matcher to search for method/ctor name IDeclaration sourceDecl // source decl to match against ) throws BadLocationException, FallbackSyncException { // Find the declaration in target text while (matcher.find()) { LOG.debug("Match found at offset: {}", matcher.start()); int targetDeclarationLineIndex = to.getLineOfOffset(matcher.start()); AbstractCodeAreaLine toDeclCandidate = getLine(to, targetDeclarationLineIndex); if (!toDeclCandidate.isScopeDeclarationLine()) { continue; } IDeclaration targetDecl = toDeclCandidate.getDeclaration(); if (sourceDecl.equals(targetDecl)) { return toDeclCandidate; } } return null; } // Similar with the function above if refactored to use the CodePanelSyncer Abstraction we can // remove this. private static AbstractCodeAreaLine getLine(AbstractCodeArea area, int lineIndex) throws BadLocationException, FallbackSyncException { if (area instanceof SmaliArea) { return new SmaliAreaLine((SmaliArea) area, lineIndex); } if (area instanceof CodeArea) { return new JavaCodeAreaLine((CodeArea) area, lineIndex); } throw new FallbackSyncException("Unknown AbstractCodeArea type for " + area); } private static boolean allowSync(AbstractCodeAreaToken areaToken) throws BadLocationException { boolean isOnDeclarationLine = areaToken.getLine().isDeclarationLine(); return isOnDeclarationLine || areaToken.isClassField() || areaToken.isFieldReference() || areaToken.isMethodConstructorDeclarationOrCall(); } private static String generateClassRegex(String name) { return "\\b(class|interface|enum)\\s+" + Pattern.quote(name) + "\\b" // java + "|" + "\\.class.*L.*" + Pattern.quote(name) + ";" // smali text + "|" + "Class:\\sL.*" + Pattern.quote(name) + ";"; // smali + dalvik } private static String generateMethodRegex(String name) { return "\\b" + Pattern.quote(name) + "\\s*\\(" // java like + "|" + "\\.method.*" + Pattern.quote(name) + "\\s*\\("; // smali } private static String extractMethodBody(AbstractCodeArea area, int startIndex) { String text = area.getText(); if (area instanceof SmaliArea) { int end = text.indexOf(".end method", startIndex); return end != -1 ? text.substring(startIndex, end + ".end method".length()) : text.substring(startIndex); } else { int brace = 0; boolean inMethod = false; for (int i = startIndex; i < text.length(); i++) { char c = text.charAt(i); if (c == '{') { brace++; inMethod = true; } else if (c == '}') { brace--; if (brace == 0 && inMethod) { return text.substring(startIndex, i + 1); } } } return text.substring(startIndex); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/IDeclaration.java ================================================ package jadx.gui.ui.codearea.sync.fallback; interface IDeclaration { String getIdentifyingName(); AbstractCodeAreaLine getLine(); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/JavaCodeAreaLine.java ================================================ package jadx.gui.ui.codearea.sync.fallback; import javax.swing.text.BadLocationException; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.ui.codearea.CodeArea; public class JavaCodeAreaLine extends AbstractCodeAreaLine { private static final Logger LOG = LoggerFactory.getLogger(JavaCodeAreaLine.class); public JavaCodeAreaLine(CodeArea area, int lineIndex) throws BadLocationException { super(area, lineIndex); } @Override public AbstractCodeAreaLine getLineAt(int lineIndex) throws BadLocationException { return new JavaCodeAreaLine((CodeArea) getArea(), lineIndex); } @Override public boolean isClassDeclaration() { return getTrimmedStr().matches(".*\\b(class|interface|enum)\\b.*\\{"); } @Override public boolean isMethodOrConstructorDeclaration() { String l = getTrimmedStr(); // Skip control-flow constructs (to avoid matching 'if', 'for', etc.) // WARNING - we are relying on the code gen format output of jadx here and that it is trimmed. // it also assumes that jadx will never output two statements on the same line separated by ';' if (l.startsWith("if ") || l.startsWith("for ") || l.startsWith("while ") || l.startsWith("switch ") || l.startsWith("case ") || l.startsWith("break ") || l.startsWith("default ") || l.startsWith("} else if ") || l.startsWith("} else ") || l.startsWith("try ") || l.startsWith("} catch ") || l.startsWith("} finally ") || l.startsWith("throw ") || l.startsWith("do ") || l.startsWith("synchronized ")) { return false; } boolean hasParens = l.contains("(") && l.contains(")"); boolean isDefined = l.endsWith("{"); boolean isAbstract = l.contains("abstract") && l.endsWith(";"); return hasParens && (isDefined || isAbstract); } @Override public boolean isFieldDeclaration() { try { IDeclaration enclosingDeclaration = getEnclosingScopeDeclaration(); if (!(enclosingDeclaration instanceof ClassDeclaration)) { return false; } String line = getTrimmedStr(); // This may also include fields which are anonymous classes or lambdas return line.endsWith(";") || line.contains(" = "); } catch (Exception ex) { LOG.error("{} - Unable to determine if line is a field declaration", LOG.getName(), ex); } return false; } @Override public final @Nullable String extractDeclaredClassName() { if (!isClassDeclaration()) { return null; } String[] tokens = getTrimmedStr().split("\\s+"); for (int i = 0; i < tokens.length; i++) { if (tokens[i].equals("class") || tokens[i].equals("interface") || tokens[i].equals("enum")) { if (i + 1 < tokens.length) { return tokens[i + 1]; } } } return null; } @Override public @Nullable String extractDeclaredMethodName() { if (!isMethodOrConstructorDeclaration()) { return null; } int paren = getTrimmedStr().indexOf('('); String before = getTrimmedStr().substring(0, paren).trim(); String[] parts = before.split("\\s+"); return parts[parts.length - 1]; // last token } @Override protected MethodDeclaration createMethodDeclaration() throws FallbackSyncException { return MethodDeclaration.create(this); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/JavaCodeAreaToken.java ================================================ package jadx.gui.ui.codearea.sync.fallback; import javax.swing.text.BadLocationException; import jadx.gui.ui.codearea.CodeArea; public class JavaCodeAreaToken extends AbstractCodeAreaToken { public JavaCodeAreaToken(CodeArea area, int at) throws BadLocationException, FallbackSyncException { super(area, at); } @Override public boolean isClassField() throws BadLocationException { AbstractCodeAreaLine line = getLine(); if (!line.isFieldDeclaration()) { return false; } // assignment immediately follows the token if (line.getStr().contains("=")) { return area.getText(this.startPos + this.length, 2).equals(" ="); } // ends with ';' return area.getText(this.startPos + this.length, 1).equals(";"); } @Override public boolean isFieldReference() throws BadLocationException { return area.getText(this.startPos - 5, 5).equals("this."); } @Override public AbstractCodeAreaLine getLine() throws BadLocationException { return new JavaCodeAreaLine((CodeArea) area, area.getLineOfOffset(getAtPos())); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/MethodDeclaration.java ================================================ package jadx.gui.ui.codearea.sync.fallback; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import jadx.core.utils.Utils; class MethodDeclaration implements IDeclaration { private final AbstractCodeAreaLine line; private final Type returnType; private final List argTypes; private final String name; boolean isStatic; public static MethodDeclaration create(JavaCodeAreaLine line) throws FallbackSyncException { String methodName = line.extractDeclaredMethodName(); if (methodName == null) { throw new FallbackSyncException("no method name found in java declaration"); } // Get the return string String trimmed = line.getTrimmedStr(); int methodNameStartPos = trimmed.indexOf(methodName); // -2 to jump to last char of return type // +1 to get to first char of return type int returnTypeStartPos = trimmed.lastIndexOf(' ', methodNameStartPos - 2) + 1; returnTypeStartPos = returnTypeStartPos > -1 ? returnTypeStartPos : 0; String returnStr = trimmed.substring(returnTypeStartPos, methodNameStartPos - 1); // Get the arg types String argString = trimmed.substring(trimmed.indexOf('(') + 1, trimmed.indexOf(')')); String[] argStringParts = argString.split(", "); List argTypeStrings = new ArrayList<>(); for (int i = 0; i < argStringParts.length; i++) { String part = argStringParts[i]; if (part.isEmpty()) { break; } argTypeStrings.add(part.substring(0, part.indexOf(" "))); } boolean isStatic = trimmed.contains("static "); List argTypes = argTypeStrings.stream().map(s -> Type.fromJavaName(s)).collect(Collectors.toList()); return new MethodDeclaration(line, Type.fromJavaName(returnStr), argTypes, isStatic, methodName); } public static MethodDeclaration create(SmaliAreaLine line) throws FallbackSyncException { String methodName = line.extractDeclaredMethodName(); if (methodName == null) { throw new FallbackSyncException("no method name found in smali declaration"); } // Get the return string String trimmed = line.getTrimmedStr(); String returnStr = trimmed.substring(trimmed.indexOf(')') + 1); returnStr = returnStr.endsWith(";") ? returnStr.substring(0, returnStr.length() - 1) : returnStr; boolean isStatic = trimmed.contains("static "); return new MethodDeclaration(line, Type.fromSmaliName(returnStr), parseSmaliArgs(trimmed), isStatic, methodName); } private MethodDeclaration(AbstractCodeAreaLine line, Type returnType, List argTypes, boolean isStatic, String name) { this.line = line; this.returnType = returnType; this.argTypes = argTypes; this.isStatic = isStatic; this.name = name; } @Override public String getIdentifyingName() { return name; } @Override public AbstractCodeAreaLine getLine() { return line; } private static List parseSmaliArgs(String lineStr) { List argTypeStrings = new ArrayList<>(); String argString = lineStr.substring(lineStr.indexOf('(') + 1, lineStr.indexOf(')')); for (int i = 0; i < argString.length();) { char c = argString.charAt(i); if (c == 'L') { int j = i; for (; j < argString.length(); ++j) { if (argString.charAt(j) == ';') { argTypeStrings.add(argString.substring(i, j + 1)); break; } } i = j + 1; } else if (c == '[') { argTypeStrings.add(argString.substring(i, i + 2)); i += 2; } else if (c != ' ') { argTypeStrings.add(argString.substring(i, i + 1)); ++i; } else { ++i; } } return argTypeStrings.stream().map(s -> Type.fromSmaliName(s)).collect(Collectors.toList()); } @Override public boolean equals(Object o) { if (o instanceof MethodDeclaration) { MethodDeclaration decl = (MethodDeclaration) o; if (!decl.name.equals(this.name)) { return false; } if (decl.isStatic != this.isStatic) { return false; } if (!decl.returnType.equals(this.returnType)) { return false; } if (decl.argTypes.size() != this.argTypes.size()) { return false; } for (int i = 0; i < decl.argTypes.size(); ++i) { if (!decl.argTypes.get(i).equals(this.argTypes.get(i))) { return false; } } return true; } return false; } // Not necessary but removes checkstyle warning @Override public int hashCode() { return Objects.hash(name, isStatic, returnType, argTypes); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("NAME=").append(name).append("+++") .append("RETURN=").append(returnType).append("+++") .append("ARGS="); for (final var a : argTypes) { sb.append(a).append(","); } return sb.toString(); } private static class Type { private String smaliName; private String javaName; public static Type fromJavaName(String name) { return new Type(Utils.javaNameToSmaliName(name), name); } public static Type fromSmaliName(String name) { return new Type(name, Utils.smaliNameToJavaName(name)); } private Type(String smaliName, String javaName) { this.smaliName = smaliName; this.javaName = javaName; } private boolean isNonPrimitive() { return smaliName.startsWith("L"); } @Override public boolean equals(Object o) { if (o instanceof Type) { Type t = (Type) o; if (t.isNonPrimitive() || this.isNonPrimitive()) { // One of them might be missing the package prefix return t.javaName.endsWith(this.javaName) || this.javaName.endsWith(t.javaName); } return t.javaName.equals(this.javaName) || t.smaliName.equals(this.smaliName); // Slightly less strict - should think about this more // && t.smaliName.equals(this.smaliName); } return false; } // Not necessary but removes checkstyle warning @Override public int hashCode() { return Objects.hash(this, javaName, smaliName); } @Override public String toString() { return "@" + smaliName + "-OR-" + javaName + "@"; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/SmaliAreaLine.java ================================================ package jadx.gui.ui.codearea.sync.fallback; import javax.swing.text.BadLocationException; import org.jetbrains.annotations.Nullable; import jadx.gui.ui.codearea.SmaliArea; public class SmaliAreaLine extends AbstractCodeAreaLine { public SmaliAreaLine(SmaliArea area, int lineIndex) throws BadLocationException { super(area, lineIndex); } @Override public AbstractCodeAreaLine getLineAt(int lineIndex) throws BadLocationException { return new SmaliAreaLine((SmaliArea) getArea(), lineIndex); } @Override public boolean isClassDeclaration() { return getTrimmedStr().startsWith("Class: ") || getTrimmedStr().startsWith(".class "); } @Override public boolean isMethodOrConstructorDeclaration() { return getTrimmedStr().startsWith(".method"); } @Override public boolean isFieldDeclaration() { return getTrimmedStr().startsWith(".field"); } @Override public final @Nullable String extractDeclaredClassName() { if (!isClassDeclaration()) { return null; } String[] parts = getTrimmedStr().split("\\s+"); for (String part : parts) { if (part.startsWith("L") && part.endsWith(";")) { String fileClassName; if (part.contains("/")) { fileClassName = part.substring(part.lastIndexOf('/') + 1, part.length() - 1); } else { fileClassName = part.substring(1, part.length() - 1); // remove leading 'L' and trailing ';' } if (fileClassName.contains("$")) { // inner class return fileClassName.substring(fileClassName.lastIndexOf('$') + 1); } return fileClassName; } } return null; } @Override public final @Nullable String extractDeclaredMethodName() { if (!isMethodOrConstructorDeclaration()) { return null; } int parenIndex = getTrimmedStr().indexOf('('); if (parenIndex > 0) { String beforeParen = getTrimmedStr().substring(0, parenIndex).trim(); String[] tokens = beforeParen.split("\\s+"); return tokens[tokens.length - 1]; } return null; } @Override protected MethodDeclaration createMethodDeclaration() throws FallbackSyncException { return MethodDeclaration.create(this); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/SmaliAreaToken.java ================================================ package jadx.gui.ui.codearea.sync.fallback; import javax.swing.text.BadLocationException; import jadx.gui.ui.codearea.SmaliArea; public class SmaliAreaToken extends AbstractCodeAreaToken { public SmaliAreaToken(SmaliArea area, int at) throws BadLocationException, FallbackSyncException { super(area, at); } @Override public boolean isFieldReference() throws BadLocationException { return area.getText(this.startPos - 2, 2).equals("->"); } @Override public boolean isClassField() throws BadLocationException { AbstractCodeAreaLine line = this.getLine(); boolean startsWithField = line.isFieldDeclaration(); if (startsWithField) { String tokenStr = getStr(); String trimmedLine = line.getTrimmedStr(); int lineTokenStartPos = trimmedLine.indexOf(tokenStr); int lineTokenAfterPos = lineTokenStartPos + this.length; for (int i = lineTokenAfterPos; i < trimmedLine.length(); ++i) { char c = trimmedLine.charAt(i); switch (c) { case ' ': break; case ':': return true; default: return false; } } } return false; } @Override public AbstractCodeAreaLine getLine() throws BadLocationException { return new SmaliAreaLine((SmaliArea) area, area.getLineOfOffset(getAtPos())); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/theme/DynamicCodeAreaTheme.java ================================================ package jadx.gui.ui.codearea.theme; import java.awt.Color; import javax.swing.UIManager; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities; import org.fife.ui.rsyntaxtextarea.SyntaxScheme; import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rtextarea.Gutter; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; /** * Mix current UI theme colors and apply to code area theme. */ public class DynamicCodeAreaTheme implements IEditorTheme { @Override public String getId() { return "DynamicCodeAreaTheme"; } @Override public String getName() { return NLS.str("preferences.dynamic_editor_theme"); } public void apply(RSyntaxTextArea textArea) { // Get the current colors from UIManager Color themeBackground = UIManager.getColor("Panel.background"); Color themeForeground = UIManager.getColor("Panel.foreground"); Color separatorForeground = UIManager.getColor("Separator.foreground"); Color editorSelectionBackground = UIManager.getColor("EditorPane.selectionBackground"); Color caretForeground = UIManager.getColor("EditorPane.caretForeground"); SyntaxScheme scheme = textArea.getSyntaxScheme(); boolean isDarkTheme = UiUtils.isDarkTheme(themeBackground); // Background colors based on the theme Color editorBackground = isDarkTheme ? themeBackground : Color.WHITE; // Use white for light theme Color lineHighlight = isDarkTheme ? UiUtils.adjustBrightness(themeBackground, 1.2f) : Color.decode("#EBECF0"); // Light gray for light theme Color lineNumberForeground = UIManager.getColor("Label.foreground"); // Add these lines after setting the background colors Color selectionColor = isDarkTheme ? new Color(51, 153, 255, 90) // Semi-transparent blue for dark theme : new Color(51, 153, 255, 50); // Lighter blue for light theme Color markAllHighlightColor = isDarkTheme ? Color.decode("#32593D") : Color.decode("#ffc800"); Color matchedBracketBackground = isDarkTheme ? UiUtils.adjustBrightness(Color.decode("#3B514D"), 1.2f) : Color.decode("#93D9D9"); Color markOccurrencesColor = UiUtils.adjustBrightness(editorSelectionBackground, isDarkTheme ? 0.6f : 1.4f); // Set the syntax colors for the theme if (isDarkTheme) { Color dataTypeColor = Color.decode("#4EC9B0"); scheme.getStyle(Token.COMMENT_EOL).foreground = Color.decode("#57A64A"); scheme.getStyle(Token.COMMENT_MULTILINE).foreground = Color.decode("#57A64A"); scheme.getStyle(Token.COMMENT_DOCUMENTATION).foreground = Color.decode("#57A64A"); scheme.getStyle(Token.COMMENT_KEYWORD).foreground = Color.decode("#57A64A"); scheme.getStyle(Token.COMMENT_MARKUP).foreground = Color.decode("#57A64A"); scheme.getStyle(Token.RESERVED_WORD).foreground = Color.decode("#569CD6"); scheme.getStyle(Token.RESERVED_WORD_2).foreground = dataTypeColor; scheme.getStyle(Token.FUNCTION).foreground = Color.decode("#DCDCAA"); scheme.getStyle(Token.ANNOTATION).foreground = Color.decode("#B3AE60"); scheme.getStyle(Token.LITERAL_NUMBER_DECIMAL_INT).foreground = Color.decode("#D7BA7D"); scheme.getStyle(Token.LITERAL_NUMBER_FLOAT).foreground = Color.decode("#D7BA7D"); scheme.getStyle(Token.LITERAL_NUMBER_HEXADECIMAL).foreground = Color.decode("#D7BA7D"); scheme.getStyle(Token.LITERAL_BOOLEAN).foreground = Color.decode("#569CD6"); scheme.getStyle(Token.LITERAL_CHAR).foreground = Color.decode("#CE9178"); scheme.getStyle(Token.LITERAL_STRING_DOUBLE_QUOTE).foreground = Color.decode("#CE9178"); scheme.getStyle(Token.DATA_TYPE).foreground = dataTypeColor; scheme.getStyle(Token.OPERATOR).foreground = Color.WHITE; scheme.getStyle(Token.SEPARATOR).foreground = Color.WHITE; scheme.getStyle(Token.IDENTIFIER).foreground = themeForeground; // XML-specific colors for dark theme scheme.getStyle(Token.MARKUP_TAG_DELIMITER).foreground = Color.decode("#808080"); // Gray for < > / scheme.getStyle(Token.MARKUP_TAG_NAME).foreground = Color.decode("#569CD6"); // Blue for tag names scheme.getStyle(Token.MARKUP_TAG_ATTRIBUTE).foreground = Color.decode("#9CDCFE"); // Light blue for attributes scheme.getStyle(Token.MARKUP_TAG_ATTRIBUTE_VALUE).foreground = Color.decode("#CE9178"); // Orange for values } else { Color dataTypeColor = Color.decode("#267F99"); scheme.getStyle(Token.COMMENT_EOL).foreground = Color.decode("#008000"); scheme.getStyle(Token.COMMENT_MULTILINE).foreground = Color.decode("#008000"); scheme.getStyle(Token.COMMENT_DOCUMENTATION).foreground = Color.decode("#008000"); scheme.getStyle(Token.COMMENT_KEYWORD).foreground = Color.decode("#008000"); scheme.getStyle(Token.COMMENT_MARKUP).foreground = Color.decode("#008000"); scheme.getStyle(Token.RESERVED_WORD).foreground = Color.decode("#0000FF"); scheme.getStyle(Token.RESERVED_WORD_2).foreground = dataTypeColor; scheme.getStyle(Token.FUNCTION).foreground = Color.decode("#795E26"); scheme.getStyle(Token.ANNOTATION).foreground = Color.decode("#9E8809"); scheme.getStyle(Token.LITERAL_NUMBER_DECIMAL_INT).foreground = Color.decode("#098658"); scheme.getStyle(Token.LITERAL_NUMBER_FLOAT).foreground = Color.decode("#098658"); scheme.getStyle(Token.LITERAL_NUMBER_HEXADECIMAL).foreground = Color.decode("#098658"); scheme.getStyle(Token.LITERAL_BOOLEAN).foreground = Color.decode("#0451A5"); scheme.getStyle(Token.LITERAL_CHAR).foreground = Color.decode("#067d17"); scheme.getStyle(Token.LITERAL_STRING_DOUBLE_QUOTE).foreground = Color.decode("#067d17"); // Soft blue for values scheme.getStyle(Token.DATA_TYPE).foreground = dataTypeColor; scheme.getStyle(Token.OPERATOR).foreground = Color.decode("#333333"); scheme.getStyle(Token.SEPARATOR).foreground = Color.decode("#333333"); scheme.getStyle(Token.IDENTIFIER).foreground = themeForeground; // XML-specific colors for light theme scheme.getStyle(Token.MARKUP_TAG_DELIMITER).foreground = Color.decode("#800000"); // Dark red for < > / scheme.getStyle(Token.MARKUP_TAG_NAME).foreground = Color.decode("#4A7A4F"); // Soft green for tag names (keys) scheme.getStyle(Token.MARKUP_TAG_ATTRIBUTE).foreground = Color.decode("#FF0000"); // Red for attributes scheme.getStyle(Token.MARKUP_TAG_ATTRIBUTE_VALUE).foreground = Color.decode("#0000FF"); // Blue for values } textArea.setBackground(editorBackground); textArea.setCaretColor(caretForeground); textArea.setSelectionColor(selectionColor); textArea.setCurrentLineHighlightColor(lineHighlight); textArea.setMarkAllHighlightColor(markAllHighlightColor); textArea.setMarkOccurrencesColor(markOccurrencesColor); textArea.setHyperlinkForeground(editorSelectionBackground); textArea.setMatchedBracketBGColor(matchedBracketBackground); textArea.setMatchedBracketBorderColor(lineNumberForeground); textArea.setPaintMatchedBracketPair(true); textArea.setAnimateBracketMatching(false); textArea.setFadeCurrentLineHighlight(true); // Reset gutter colors directly to ensure the change applies Gutter gutter = RSyntaxUtilities.getGutter(textArea); if (gutter != null) { gutter.setBackground(editorBackground); gutter.setBorderColor(separatorForeground); gutter.setLineNumberColor(lineNumberForeground); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/theme/EditorThemeManager.java ================================================ package jadx.gui.ui.codearea.theme; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.StringUtils; import jadx.gui.settings.JadxSettings; public class EditorThemeManager { private static final Logger LOG = LoggerFactory.getLogger(EditorThemeManager.class); private final List themes = new ArrayList<>(); private final Map themesMap = new HashMap<>(); private IEditorTheme currentTheme = new FallbackEditorTheme(); public EditorThemeManager(JadxSettings settings) { registerThemes(); if (StringUtils.isEmpty(settings.getEditorTheme())) { // set default theme IEditorTheme defaultTheme = themes.get(0); settings.setEditorTheme(defaultTheme.getId()); } } private void registerThemes() { registerTheme(new DynamicCodeAreaTheme()); registerTheme(new RSTABundledTheme("default")); registerTheme(new RSTABundledTheme("eclipse")); registerTheme(new RSTABundledTheme("idea")); registerTheme(new RSTABundledTheme("vs")); registerTheme(new RSTABundledTheme("dark")); registerTheme(new RSTABundledTheme("monokai")); registerTheme(new RSTABundledTheme("druid")); } public void registerTheme(IEditorTheme editorTheme) { IEditorTheme prev = themesMap.put(editorTheme.getId(), editorTheme); if (prev != null) { themes.remove(prev); } themes.add(editorTheme); } public synchronized void setTheme(String id) { if (currentTheme.getId().equals(id)) { // already set return; } // resolve new IEditorTheme newTheme = themesMap.get(id); if (newTheme == null) { LOG.warn("Failed to resolve editor theme: {}", id); return; } // unload current unload(); // load new try { newTheme.load(); } catch (Throwable t) { LOG.warn("Failed to load editor theme: {}", id, t); } currentTheme = newTheme; } public void apply(RSyntaxTextArea textArea) { this.currentTheme.apply(textArea); } public ThemeIdAndName[] getThemeIdNameArray() { return themes.stream() .map(EditorThemeManager::toThemeIdAndName) .toArray(ThemeIdAndName[]::new); } public ThemeIdAndName getCurrentThemeIdName() { return toThemeIdAndName(currentTheme); } private static ThemeIdAndName toThemeIdAndName(IEditorTheme t) { return new ThemeIdAndName(t.getId(), t.getName()); } public void unload() { try { currentTheme.unload(); } catch (Throwable t) { LOG.warn("Failed to unload editor theme: {}", currentTheme.getId(), t); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/theme/FallbackEditorTheme.java ================================================ package jadx.gui.ui.codearea.theme; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.Theme; public class FallbackEditorTheme implements IEditorTheme { private Theme baseTheme; @Override public String getId() { return "fallback"; } @Override public String getName() { return "Fallback"; } @Override public void load() { baseTheme = new Theme(new RSyntaxTextArea()); } @Override public void apply(RSyntaxTextArea textArea) { baseTheme.apply(textArea); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/theme/IEditorTheme.java ================================================ package jadx.gui.ui.codearea.theme; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; public interface IEditorTheme { String getId(); String getName(); default void load() { // optional method } void apply(RSyntaxTextArea textArea); default void unload() { // optional method } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/theme/RSTABundledTheme.java ================================================ package jadx.gui.ui.codearea.theme; import java.io.InputStream; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.Theme; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class RSTABundledTheme implements IEditorTheme { private static final Logger LOG = LoggerFactory.getLogger(RSTABundledTheme.class); private static final String RSTA_THEME_PATH = "/org/fife/ui/rsyntaxtextarea/themes/"; private final String name; private Theme loadedTheme; public RSTABundledTheme(String name) { this.name = name; } @Override public String getId() { return "RSTA:" + name; } @Override public String getName() { return name; } @Override public void load() { String path = RSTA_THEME_PATH + name + ".xml"; try { try (InputStream is = RSTABundledTheme.class.getResourceAsStream(path)) { loadedTheme = Theme.load(is); } } catch (Throwable t) { LOG.error("Failed to load editor theme: {}", path, t); loadedTheme = new Theme(new RSyntaxTextArea()); } } @Override public void apply(RSyntaxTextArea textArea) { loadedTheme.apply(textArea); } @Override public void unload() { loadedTheme = null; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/theme/RSTAThemeXML.java ================================================ package jadx.gui.ui.codearea.theme; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.Theme; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class RSTAThemeXML implements IEditorTheme { private static final Logger LOG = LoggerFactory.getLogger(RSTAThemeXML.class); private final Path themePath; private final String name; private Theme loadedTheme; public RSTAThemeXML(Path themeXmlPath, String name) { this.themePath = themeXmlPath; this.name = name; } @Override public String getId() { return "file:" + themePath; } @Override public String getName() { return name; } @Override public void load() { try { try (InputStream is = Files.newInputStream(themePath)) { loadedTheme = Theme.load(is); } } catch (Exception e) { LOG.warn("Failed to load editor theme: {}", themePath, e); loadedTheme = new Theme(new RSyntaxTextArea()); } } @Override public void apply(RSyntaxTextArea textArea) { loadedTheme.apply(textArea); } @Override public void unload() { loadedTheme = null; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/codearea/theme/ThemeIdAndName.java ================================================ package jadx.gui.ui.codearea.theme; public class ThemeIdAndName { private final String id; private final String name; public ThemeIdAndName(String id, String name) { this.id = id; this.name = name; } public String getId() { return id; } public String getName() { return name; } @Override public final boolean equals(Object other) { if (!(other instanceof ThemeIdAndName)) { return false; } return id.equals(((ThemeIdAndName) other).id); } @Override public int hashCode() { return id.hashCode(); } @Override public String toString() { return name; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/ADBDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.GridLayout; import java.awt.Label; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import java.net.Socket; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.commons.app.JadxSystemInfo; import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.device.debugger.DbgUtils; import jadx.gui.device.debugger.DebugSettings; import jadx.gui.device.protocol.ADB; import jadx.gui.device.protocol.ADBDevice; import jadx.gui.device.protocol.ADBDeviceInfo; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.IDebugController; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.JDWPProcessListener { private static final Logger LOG = LoggerFactory.getLogger(ADBDialog.class); private static final long serialVersionUID = -1111111202102181630L; private static final ImageIcon ICON_DEVICE = UiUtils.openSvgIcon("adb/androidDevice"); private static final ImageIcon ICON_PROCESS = UiUtils.openSvgIcon("adb/addToWatch"); private final transient MainWindow mainWindow; private transient Label tipLabel; private transient JTextField pathTextField; private transient JTextField hostTextField; private transient JTextField portTextField; private transient DefaultTreeModel procTreeModel; private transient DefaultMutableTreeNode procTreeRoot; private transient JTree procTree; private Socket deviceSocket; private transient List deviceNodes = new ArrayList<>(); private transient DeviceNode lastSelectedDeviceNode; public ADBDialog(MainWindow mainWindow) { super(mainWindow); this.mainWindow = mainWindow; initUI(); pathTextField.setText(mainWindow.getSettings().getAdbDialogPath()); hostTextField.setText(mainWindow.getSettings().getAdbDialogHost()); portTextField.setText(mainWindow.getSettings().getAdbDialogPort()); if (pathTextField.getText().isEmpty()) { detectADBPath(); } else { pathTextField.setText(""); } SwingUtilities.invokeLater(this::connectToADB); UiUtils.addEscapeShortCutToDispose(this); } private void initUI() { pathTextField = new JTextField(); portTextField = new JTextField(); hostTextField = new JTextField(); JPanel adbPanel = new JPanel(new BorderLayout(5, 5)); adbPanel.add(new JLabel(NLS.str("adb_dialog.path")), BorderLayout.WEST); adbPanel.add(pathTextField, BorderLayout.CENTER); JPanel portPanel = new JPanel(new BorderLayout(5, 0)); portPanel.add(new JLabel(NLS.str("adb_dialog.port")), BorderLayout.WEST); portPanel.add(portTextField, BorderLayout.CENTER); JPanel hostPanel = new JPanel(new BorderLayout(5, 0)); hostPanel.add(new JLabel(NLS.str("adb_dialog.addr")), BorderLayout.WEST); hostPanel.add(hostTextField, BorderLayout.CENTER); JPanel wrapperPanel = new JPanel(new GridLayout(1, 2, 5, 0)); wrapperPanel.add(hostPanel); wrapperPanel.add(portPanel); adbPanel.add(wrapperPanel, BorderLayout.SOUTH); procTree = new JTree(); JScrollPane scrollPane = new JScrollPane(procTree); scrollPane.setMinimumSize(new Dimension(100, 150)); scrollPane.setBorder(BorderFactory.createLineBorder(Color.black)); procTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); procTreeRoot = new DefaultMutableTreeNode(NLS.str("adb_dialog.device_node")); procTreeModel = new DefaultTreeModel(procTreeRoot); procTree.setModel(procTreeModel); procTree.setRowHeight(-1); procTree.setFont(mainWindow.getSettings().getCodeFont()); procTree.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { processSelected(e); } } }); procTree.setCellRenderer(new DefaultTreeCellRenderer() { private static final long serialVersionUID = -1111111202103170735L; @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { Component c = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); if (value instanceof DeviceTreeNode || value == procTreeRoot) { setIcon(ICON_DEVICE); } else { setIcon(ICON_PROCESS); } return c; } }); procTree.addTreeSelectionListener(event -> { Object selectedNode = procTree.getLastSelectedPathComponent(); if (selectedNode instanceof DeviceTreeNode) { lastSelectedDeviceNode = deviceNodes.stream() .filter(item -> item.tNode == selectedNode) .findFirst().orElse(null); } }); JPanel btnPane = new JPanel(); BoxLayout boxLayout = new BoxLayout(btnPane, BoxLayout.LINE_AXIS); btnPane.setLayout(boxLayout); tipLabel = new Label(NLS.str("adb_dialog.waiting")); btnPane.add(tipLabel); JButton refreshBtn = new JButton(NLS.str("adb_dialog.refresh")); JButton startServerBtn = new JButton(NLS.str("adb_dialog.start_server")); JButton launchAppBtn = new JButton(NLS.str("adb_dialog.launch_app")); btnPane.add(launchAppBtn); btnPane.add(startServerBtn); btnPane.add(refreshBtn); refreshBtn.addActionListener(e -> { clear(); procTreeRoot.removeAllChildren(); procTreeModel.reload(procTreeRoot); SwingUtilities.invokeLater(this::connectToADB); }); startServerBtn.addActionListener(e -> startADBServer()); launchAppBtn.addActionListener(e -> launchApp()); JPanel mainPane = new JPanel(new BorderLayout(5, 5)); mainPane.add(adbPanel, BorderLayout.NORTH); mainPane.add(scrollPane, BorderLayout.CENTER); mainPane.add(btnPane, BorderLayout.SOUTH); mainPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); getContentPane().add(mainPane); pack(); setSize(800, 500); setLocationRelativeTo(null); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setModalityType(ModalityType.MODELESS); } private void clear() { if (deviceSocket != null) { try { deviceSocket.close(); } catch (Exception e) { LOG.error("Failed to close device socket", e); } deviceSocket = null; } for (DeviceNode deviceNode : deviceNodes) { deviceNode.device.stopListenForJDWP(); } deviceNodes.clear(); } private void detectADBPath() { boolean isWinOS = JadxSystemInfo.IS_WINDOWS; String slash = isWinOS ? "\\" : "/"; String adbName = isWinOS ? "adb.exe" : "adb"; String sdkPath = System.getenv("ANDROID_HOME"); if (!StringUtils.isEmpty(sdkPath)) { if (!sdkPath.endsWith(slash)) { sdkPath += slash; } sdkPath += "platform-tools" + slash + adbName; if ((new File(sdkPath)).exists()) { pathTextField.setText(sdkPath); return; } } String envPath = System.getenv("PATH"); String[] paths = envPath.split(isWinOS ? ";" : ":"); for (String path : paths) { if (!path.endsWith(slash)) { path += slash; } path = path + adbName; if (new File(path).exists()) { pathTextField.setText(path); return; } } } private void startADBServer() { String path = pathTextField.getText(); if (path.isEmpty()) { UiUtils.showMessageBox(mainWindow, NLS.str("adb_dialog.missing_path")); return; } String tip; try { if (ADB.startServer(path, Integer.parseInt(portTextField.getText()))) { tip = NLS.str("adb_dialog.start_okay", portTextField.getText()); } else { tip = NLS.str("adb_dialog.start_fail", portTextField.getText()); } } catch (Exception e) { LOG.error("Failed to start adb server", e); tip = e.getMessage(); } UiUtils.showMessageBox(mainWindow, tip); tipLabel.setText(tip); } private void connectToADB() { String tip; try { String host = hostTextField.getText().trim(); String port = portTextField.getText().trim(); tipLabel.setText(NLS.str("adb_dialog.connecting", host, port)); deviceSocket = ADB.listenForDeviceState(this, host, Integer.parseInt(port)); if (deviceSocket != null) { tip = NLS.str("adb_dialog.connect_okay", host, port); this.setTitle(tip); } else { tip = NLS.str("adb_dialog.connect_fail"); } } catch (Exception e) { LOG.error("Failed to connect to adb", e); tip = e.getMessage(); UiUtils.showMessageBox(mainWindow, tip); } tipLabel.setText(tip); } @Override public void onDeviceStatusChange(List deviceInfoList) { LOG.debug("onDeviceStatusChange {}", deviceInfoList); List nodes = new ArrayList<>(deviceInfoList.size()); info_loop: for (ADBDeviceInfo info : deviceInfoList) { for (DeviceNode deviceNode : deviceNodes) { if (deviceNode.device.updateDeviceInfo(info)) { deviceNode.refresh(); nodes.add(deviceNode); continue info_loop; } } ADBDevice device = new ADBDevice(info); device.getAndroidReleaseVersion(); nodes.add(new DeviceNode(device)); listenJDWP(device); } deviceNodes = nodes; SwingUtilities.invokeLater(() -> { tipLabel.setText(NLS.str("adb_dialog.tip_devices", deviceNodes.size())); procTreeRoot.removeAllChildren(); deviceNodes.forEach(n -> procTreeRoot.add(n.tNode)); procTreeModel.reload(procTreeRoot); for (DeviceNode deviceNode : deviceNodes) { procTree.expandPath(new TreePath(deviceNode.tNode.getPath())); } }); } private void processSelected(MouseEvent e) { TreePath path = procTree.getPathForLocation(e.getX(), e.getY()); if (path != null) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); String pid = getPid((String) node.getUserObject()); if (StringUtils.isEmpty(pid)) { return; } if (mainWindow.getDebuggerPanel() != null && mainWindow.getDebuggerPanel().getDbgController().isDebugging()) { if (JOptionPane.showConfirmDialog(mainWindow, NLS.str("adb_dialog.restart_while_debugging_msg"), NLS.str("adb_dialog.restart_while_debugging_title"), JOptionPane.OK_CANCEL_OPTION) != JOptionPane.CANCEL_OPTION) { IDebugController ctrl = mainWindow.getDebuggerPanel().getDbgController(); if (launchForDebugging(mainWindow, ctrl.getProcessName(), true)) { dispose(); } } return; } DeviceNode deviceNode = getDeviceNode((DefaultMutableTreeNode) node.getParent()); if (deviceNode == null) { return; } if (!setupArgs(deviceNode.device, pid, (String) node.getUserObject())) { return; } if (DebugSettings.INSTANCE.isBeingDebugged()) { if (JOptionPane.showConfirmDialog(mainWindow, NLS.str("adb_dialog.being_debugged_msg"), NLS.str("adb_dialog.being_debugged_title"), JOptionPane.OK_CANCEL_OPTION) == JOptionPane.CANCEL_OPTION) { return; } } tipLabel.setText(NLS.str("adb_dialog.starting_debugger")); if (!attachProcess(mainWindow)) { tipLabel.setText(NLS.str("adb_dialog.init_dbg_fail")); } else { dispose(); } } } private static boolean attachProcess(MainWindow mainWindow) { DebugSettings debugSettings = DebugSettings.INSTANCE; debugSettings.clearForward(); String rst = debugSettings.forwardJDWP(); if (!rst.isEmpty()) { UiUtils.showMessageBox(mainWindow, rst); return false; } try { return mainWindow.getDebuggerPanel().showDebugger( debugSettings.getName(), debugSettings.getDevice().getDeviceInfo().getAdbHost(), debugSettings.getForwardTcpPort(), debugSettings.getVer(), debugSettings.getDevice(), debugSettings.getPid()); } catch (Exception e) { LOG.error("Failed to attach to process", e); return false; } } public static boolean launchForDebugging(MainWindow mainWindow, String fullAppPath, boolean autoAttach) { DebugSettings debugSettings = DebugSettings.INSTANCE; debugSettings.setAutoAttachPkg(autoAttach); try { int pid = debugSettings.getDevice().launchApp(fullAppPath); if (pid != -1) { debugSettings.setPid(String.valueOf(pid)) .setName(fullAppPath); return attachProcess(mainWindow); } } catch (Exception e) { LOG.error("Failed to launch app", e); } return false; } private String getPid(String nodeText) { if (nodeText.startsWith("[pid:")) { int pos = nodeText.indexOf(']', "[pid:".length()); if (pos != -1) { return nodeText.substring("[pid:".length(), pos).trim(); } } return null; } private DeviceNode getDeviceNode(DefaultMutableTreeNode node) { for (DeviceNode deviceNode : deviceNodes) { if (deviceNode.tNode == node) { return deviceNode; } } return null; } private DeviceNode getDeviceNode(ADBDevice device) { for (DeviceNode deviceNode : deviceNodes) { if (deviceNode.device.equals(device)) { return deviceNode; } } throw new JadxRuntimeException("Unexpected device: " + device); } private void listenJDWP(ADBDevice device) { try { device.listenForJDWP(this); } catch (Exception e) { LOG.error("Failed listen for JDWP", e); } } @Override public void dispose() { clear(); JadxSettings settings = mainWindow.getSettings(); boolean changed = !settings.getAdbDialogPath().equals(pathTextField.getText()); changed |= !settings.getAdbDialogHost().equals(hostTextField.getText()); changed |= !settings.getAdbDialogPort().equals(portTextField.getText()); if (changed) { settings.setAdbDialogPath(pathTextField.getText()); settings.setAdbDialogHost(hostTextField.getText()); settings.setAdbDialogPort(portTextField.getText()); settings.sync(); } super.dispose(); } @Override public void adbDisconnected() { deviceSocket = null; SwingUtilities.invokeLater(() -> { tipLabel.setText(NLS.str("adb_dialog.disconnected")); this.setTitle(""); }); } @Override public void jdwpProcessOccurred(ADBDevice device, Set id) { List procs; try { Thread.sleep(40); /* * wait for a moment, let the new processes on remote be fully initialized, * otherwise we may not get its real name but the state text. */ procs = device.getProcessList(); } catch (Exception e) { LOG.error("Failed to get device process list", e); procs = Collections.emptyList(); } List procList = new ArrayList<>(id.size()); if (procs.isEmpty()) { procList.addAll(id); } else { for (ADB.Process proc : procs) { if (id.contains(proc.pid)) { procList.add(String.format("[pid: %-6s] %s", proc.pid, proc.name)); } } } Collections.reverse(procList); DeviceNode node; try { node = getDeviceNode(device); } catch (Exception e) { LOG.error("Failed to find device", e); return; } SwingUtilities.invokeLater(() -> { node.tNode.removeAllChildren(); DefaultMutableTreeNode foundNode = null; DebugSettings debugSettings = DebugSettings.INSTANCE; for (String procStr : procList) { DefaultMutableTreeNode pnode = new DefaultMutableTreeNode(procStr); node.tNode.add(pnode); if (!debugSettings.getExpectPkg().isEmpty() && procStr.endsWith(debugSettings.getExpectPkg())) { if (debugSettings.isAutoAttachPkg() && debugSettings.getDevice().equals(node.device)) { debugSettings.set(node.device, debugSettings.getVer(), getPid(procStr), procStr); if (attachProcess(mainWindow)) { dispose(); return; } } foundNode = pnode; } } procTreeModel.reload(node.tNode); procTree.expandPath(new TreePath(node.tNode.getPath())); if (foundNode != null) { TreePath thePath = new TreePath(foundNode.getPath()); procTree.scrollPathToVisible(thePath); procTree.setSelectionPath(thePath); } }); } private void launchApp() { if (deviceNodes.isEmpty()) { UiUtils.showMessageBox(mainWindow, NLS.str("adb_dialog.no_devices")); return; } DbgUtils.AppData appData = DbgUtils.parseAppData(mainWindow); if (appData == null) { // error already reported return; } if (scrollToProcNode(appData.getAppPackage())) { return; } String processName = appData.getProcessName(); ADBDevice device = lastSelectedDeviceNode == null ? deviceNodes.get(0).device : lastSelectedDeviceNode.device; if (device != null) { try { device.launchApp(processName); } catch (Exception e) { LOG.error("Failed to launch app: {}", processName, e); UiUtils.showMessageBox(mainWindow, e.getMessage()); } } } private boolean scrollToProcNode(String pkg) { if (pkg.isEmpty()) { return false; } DebugSettings.INSTANCE.setExpectPkg(" " + pkg); for (int i = 0; i < procTreeRoot.getChildCount(); i++) { DefaultMutableTreeNode rn = (DefaultMutableTreeNode) procTreeRoot.getChildAt(i); for (int j = 0; j < rn.getChildCount(); j++) { DefaultMutableTreeNode n = (DefaultMutableTreeNode) rn.getChildAt(j); String pName = (String) n.getUserObject(); if (pName.endsWith(DebugSettings.INSTANCE.getExpectPkg())) { TreePath path = new TreePath(n.getPath()); procTree.scrollPathToVisible(path); procTree.setSelectionPath(path); return true; } } } return false; } @Override public void jdwpListenerClosed(ADBDevice device) { } private static class DeviceTreeNode extends DefaultMutableTreeNode { private static final long serialVersionUID = -1111111202103131112L; } private static class DeviceNode { ADBDevice device; DeviceTreeNode tNode; DeviceNode(ADBDevice adbDevice) { this.device = adbDevice; tNode = new DeviceTreeNode(); refresh(); } void refresh() { ADBDeviceInfo info = device.getDeviceInfo(); String text = info.getModel(); if (text != null) { if (!text.equals(info.getSerial())) { text += String.format(" [serial: %s]", info.getSerial()); } text += String.format(" [state: %s]", info.isOnline() ? "online" : "offline"); tNode.setUserObject(text); } } } private boolean setupArgs(ADBDevice device, String pid, String name) { String ver = device.getAndroidReleaseVersion(); if (StringUtils.isEmpty(ver)) { if (JOptionPane.showConfirmDialog(mainWindow, NLS.str("adb_dialog.unknown_android_ver"), "", JOptionPane.OK_CANCEL_OPTION) == JOptionPane.CANCEL_OPTION) { return false; } ver = "8"; } ver = getMajorVer(ver); DebugSettings.INSTANCE.set(device, Integer.parseInt(ver), pid, name); return true; } private String getMajorVer(String ver) { int pos = ver.indexOf('.'); if (pos != -1) { ver = ver.substring(0, pos); } return ver; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/AboutDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.net.URL; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SwingConstants; import jadx.api.JadxDecompiler; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class AboutDialog extends JDialog { private static final long serialVersionUID = 5763493590584039096L; public AboutDialog() { initUI(); } public final void initUI() { URL logoURL = getClass().getResource("/logos/jadx-logo-48px.png"); Icon logo = new ImageIcon(logoURL, "JADX logo"); JLabel name = new JLabel("JADX", logo, SwingConstants.CENTER); name.setAlignmentX(0.5f); JLabel desc = new JLabel("Dex to Java decompiler"); desc.setAlignmentX(0.5f); JLabel version = new JLabel("JADX version: " + JadxDecompiler.getVersion()); version.setAlignmentX(0.5f); String javaVm = System.getProperty("java.vm.name"); String javaVer = System.getProperty("java.version"); javaVm = javaVm == null ? "" : javaVm; JLabel javaVmLabel = new JLabel("Java VM: " + javaVm); javaVmLabel.setAlignmentX(0.5f); javaVer = javaVer == null ? "" : javaVer; JLabel javaVerLabel = new JLabel("Java version: " + javaVer); javaVerLabel.setAlignmentX(0.5f); JPanel textPane = new JPanel(); textPane.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); textPane.setLayout(new BoxLayout(textPane, BoxLayout.PAGE_AXIS)); textPane.add(Box.createRigidArea(new Dimension(0, 10))); textPane.add(name); textPane.add(Box.createRigidArea(new Dimension(0, 10))); textPane.add(desc); textPane.add(Box.createRigidArea(new Dimension(0, 10))); textPane.add(version); textPane.add(Box.createRigidArea(new Dimension(0, 20))); textPane.add(javaVmLabel); textPane.add(javaVerLabel); textPane.add(Box.createRigidArea(new Dimension(0, 20))); JButton close = new JButton(NLS.str("tabs.close")); close.addActionListener(event -> dispose()); close.setAlignmentX(0.5f); Container contentPane = getContentPane(); contentPane.add(textPane, BorderLayout.CENTER); contentPane.add(close, BorderLayout.PAGE_END); UiUtils.setWindowIcons(this); setModalityType(ModalityType.APPLICATION_MODAL); setTitle(NLS.str("about_dialog.title")); pack(); setDefaultCloseOperation(DISPOSE_ON_CLOSE); setLocationRelativeTo(null); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/CallGraphDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.FlowLayout; import java.util.Formatter; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JMenuBar; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.UIManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.api.plugins.input.data.IMethodRef; import jadx.core.utils.DotGraphUtils; import jadx.gui.treemodel.JMethod; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.layout.WrapLayout; public class CallGraphDialog extends GraphDialog { private static final long serialVersionUID = -850803763322590708L; private static final Logger LOG = LoggerFactory.getLogger(CallGraphDialog.class); private static final String FONT = "fontname=\"Courier\" fontsize=12"; private int callerDepthLimit = 3; private int calleeDepthLimit = 3; private int nextNodeID; private Map methodToNodeID; private Map unresolvedMethodToNodeID; private Set edges; private JavaMethod javaMethod; private boolean longNames = false; public CallGraphDialog(MainWindow mainWindow, JavaMethod javaMethod) { super(mainWindow, String.format("%s: %s", NLS.str("graph_viewer.call_graph.title"), DotGraphUtils.methodFormatName(javaMethod, false))); this.javaMethod = javaMethod; } public JMenuBar addMenuBar() { JMenuBar menuBar = super.addMenuBar(); // Long names checkbox JCheckBox showLongNames = new JCheckBox(NLS.str("graph_viewer.long_names")); showLongNames.setSelected(false); showLongNames.addItemListener(e -> { longNames = showLongNames.isSelected(); reload(); }); // Calee spinner SpinnerNumberModel calleeDepthSpinnerModel = new SpinnerNumberModel(3, 0, 100, 1); JSpinner calleeDepthSpinner = new JSpinner(calleeDepthSpinnerModel); calleeDepthSpinner.addChangeListener(e -> { calleeDepthLimit = (int) calleeDepthSpinner.getValue(); reload(); }); // Callee label JLabel calleeLbl = new JLabel(NLS.str("graph_viewer.callee_depth")); calleeLbl.setLabelFor(calleeDepthSpinner); calleeLbl.setHorizontalAlignment(SwingConstants.LEFT); // Assemble callee panel JPanel calleePanel = new JPanel(); calleePanel.setOpaque(false); calleePanel.setLayout(new BoxLayout(calleePanel, BoxLayout.LINE_AXIS)); calleePanel.add(calleeLbl); calleePanel.add(Box.createRigidArea(new Dimension(3, 0))); calleePanel.add(calleeDepthSpinner); // Caller spinner SpinnerNumberModel callerDepthSpinnerModel = new SpinnerNumberModel(3, 0, 100, 1); JSpinner callerDepthSpinner = new JSpinner(callerDepthSpinnerModel); callerDepthSpinner.addChangeListener(e -> { callerDepthLimit = (int) callerDepthSpinner.getValue(); reload(); }); // Caller label JLabel callerLbl = new JLabel(NLS.str("graph_viewer.caller_depth")); callerLbl.setLabelFor(callerDepthSpinner); callerLbl.setHorizontalAlignment(SwingConstants.LEFT); // Assemble caller panel JPanel callerPanel = new JPanel(); callerPanel.setOpaque(false); callerPanel.setLayout(new BoxLayout(callerPanel, BoxLayout.LINE_AXIS)); callerPanel.add(callerLbl); callerPanel.add(Box.createRigidArea(new Dimension(3, 0))); callerPanel.add(callerDepthSpinner); // Assemble menubar panel JPanel menuBarPanel = new JPanel(); menuBarPanel.setOpaque(false); menuBarPanel.setLayout(new WrapLayout(FlowLayout.LEFT)); menuBarPanel.add(showLongNames, BorderLayout.PAGE_START); menuBarPanel.add(Box.createRigidArea(new Dimension(10, 0))); menuBarPanel.add(calleePanel); menuBarPanel.add(Box.createRigidArea(new Dimension(10, 0))); menuBarPanel.add(callerPanel); // Add menubar panel to menuBar menuBar.add(menuBarPanel); return menuBar; } public static void open(MainWindow window, JMethod method) { JavaMethod javaMethod = method.getJavaMethod(); CallGraphDialog graphDialog = new CallGraphDialog(window, javaMethod); graphDialog.addMenuBar(); graphDialog.setVisible(true); graphDialog.reload(); } public void reload() { SwingUtilities.invokeLater(() -> { String graph = generateGraph(javaMethod); getPanel().setGraph(graph); }); } private String generateGraph(JavaMethod javaMethod) { StringBuilder sb = new StringBuilder(); Color themeBackground = UIManager.getColor("Panel.background"); Color themeForeground = UIManager.getColor("Label.foreground"); Color themeHighlight = UIManager.getColor("Component.focusedBorderColor"); Color themeShade = UIManager.getColor("TextArea.background"); String bgColor = String.format("bgcolor=\"#%02x%02x%02x\"", themeBackground.getRed(), themeBackground.getGreen(), themeBackground.getBlue()); String lineColor = String.format("color=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), themeForeground.getBlue()); String fontColor = String.format("fontcolor=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), themeForeground.getBlue()); String highlightColor = String.format("color=\"#%02x%02x%02x\"", themeHighlight.getRed(), themeHighlight.getGreen(), themeHighlight.getBlue()); String shadeColor = String.format("fillcolor=\"#%02x%02x%02x\"", themeShade.getRed(), themeShade.getGreen(), themeShade.getBlue()); try (Formatter f = new Formatter(sb)) { // graph header f.format("digraph G {\n"); f.format("%s\n", bgColor); f.format("node[shape=\"record\" style=\"filled\" %s %s %s %s]\n", FONT, fontColor, lineColor, shadeColor); f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor); nextNodeID = 0; methodToNodeID = new HashMap<>(); unresolvedMethodToNodeID = new HashMap<>(); edges = new HashSet<>(); addNode(f, javaMethod, highlightColor); // add caller relationships addCallers(0, f, javaMethod); // add calee relationships addCallees(0, f, javaMethod); // close graph f.format("}"); return f.toString(); } } private void addCallers(int depth, Formatter f, JavaMethod javaMethod) { if (depth >= callerDepthLimit) { return; } List uses = javaMethod.getUseIn(); // add "calls" relationships for (JavaNode node : uses) { if (!(node instanceof JavaMethod)) { continue; } JavaMethod caller = (JavaMethod) node; int nodeID = addNode(f, caller); addEdge(f, nodeID, methodToNodeID.get(javaMethod)); addCallers(depth + 1, f, caller); } } private void addCallees(int depth, Formatter f, JavaMethod javaMethod) { if (depth >= calleeDepthLimit) { return; } List used = javaMethod.getUsed(); // add "calls" relationships for (JavaNode node : used) { if (!(node instanceof JavaMethod)) { continue; } JavaMethod callee = (JavaMethod) node; int nodeID = addNode(f, callee); addEdge(f, methodToNodeID.get(javaMethod), nodeID); addCallees(depth + 1, f, callee); } addUnresolvedCallees(depth, f, javaMethod); } private void addUnresolvedCallees(int depth, Formatter f, JavaMethod javaMethod) { if (depth >= calleeDepthLimit) { return; } List used = javaMethod.getUnresolvedUsed(); // add "calls" relationships for (IMethodRef callee : used) { String name = callee.getName(); if (name == null) { continue; } int nodeID = addNode(f, callee); addEdge(f, methodToNodeID.get(javaMethod), nodeID); } } private int addNode(Formatter f, JavaMethod method) { return addNode(f, method, ""); } // Add a node representing method to the graph in f. Returns the ID of the new node private int addNode(Formatter f, JavaMethod method, String extra) { int nodeID; if (methodToNodeID.containsKey(method)) { nodeID = methodToNodeID.get(method); } else { nodeID = nextNodeID; nextNodeID++; methodToNodeID.put(method, nodeID); } String name = DotGraphUtils.methodFormatName(method, longNames); f.format("Node_%d [ label=\"{%s}\" %s]\n", nodeID, UiUtils.toDotNodeName(name), extra); if (javaMethod.callsSelf()) { addEdge(f, nodeID, nodeID); } return nodeID; } private int addNode(Formatter f, IMethodRef method) { return addNode(f, method, ""); } // Add a node representing an unresolved method to the graph in f. Returns the ID of the new node private int addNode(Formatter f, IMethodRef method, String extra) { int nodeID; if (unresolvedMethodToNodeID.containsKey(method)) { nodeID = unresolvedMethodToNodeID.get(method); } else { nodeID = nextNodeID; nextNodeID++; unresolvedMethodToNodeID.put(method, nodeID); } String name = DotGraphUtils.unresolvedMethodFormatName(method, longNames); Color themeOutOfFocus = UIManager.getColor("Component.disabledBorderColor"); String outOfFocus = String.format("color=\"#%02x%02x%02x\"", themeOutOfFocus.getRed(), themeOutOfFocus.getGreen(), themeOutOfFocus.getBlue()); f.format("Node_%d [ label=\"{%s}\" style=dashed %s %s]\n", nodeID, UiUtils.toDotNodeName(name), outOfFocus, extra); return nodeID; } // Add an edge between sourceID and destID to the graph in f private void addEdge(Formatter f, int sourceID, int destID) { Edge edge = new Edge(sourceID, destID); if (!edges.contains(edge)) { f.format("Node_%d -> Node_%d\n", sourceID, destID); edges.add(edge); } } private static class Edge { public int source; public int dest; public Edge(int source, int dest) { this.source = source; this.dest = dest; } @Override public boolean equals(Object otherObject) { if (!(otherObject instanceof Edge)) { return false; } Edge other = (Edge) otherObject; return (this.source == other.source) && (this.dest == other.dest); } @Override public int hashCode() { return Objects.hash(source, dest); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/CharsetDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.Component; import java.nio.charset.Charset; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import javax.swing.JOptionPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.utils.NLS; public class CharsetDialog { private static final Logger LOG = LoggerFactory.getLogger(CharsetDialog.class); private static final Comparator CHARSET_COMPARATOR = Comparator.comparing( Charset::displayName, String::compareToIgnoreCase); public static String chooseCharset(Component parent, String currentCharsetName) { Collection availableCharsets = Charset.availableCharsets().values(); List sortedCharsets = availableCharsets.stream() .sorted(CHARSET_COMPARATOR) .collect(Collectors.toList()); Charset initialSelection = null; try { if (currentCharsetName != null && Charset.isSupported(currentCharsetName)) { initialSelection = Charset.forName(currentCharsetName); if (!sortedCharsets.contains(initialSelection)) { initialSelection = null; } } } catch (Exception e) { LOG.warn("Failed to find initial charset '{}'", currentCharsetName, e); } if (initialSelection == null && !sortedCharsets.isEmpty()) { initialSelection = sortedCharsets.get(0); } Charset[] charsetArray = sortedCharsets.toArray(new Charset[0]); Object selectedValue = JOptionPane.showInputDialog( parent, NLS.str("encoding_dialog.message"), NLS.str("encoding_dialog.title"), JOptionPane.INFORMATION_MESSAGE, null, charsetArray, initialSelection); if (selectedValue instanceof Charset) { return ((Charset) selectedValue).name(); } else { return null; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassInheritanceGraphDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Color; import java.awt.FlowLayout; import java.util.ArrayList; import java.util.Formatter; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.JCheckBox; import javax.swing.JMenuBar; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.UIManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.android.apksig.internal.util.Pair; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.IMethodDetails; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.DotGraphUtils; import jadx.gui.treemodel.JClass; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.layout.WrapLayout; public class ClassInheritanceGraphDialog extends GraphDialog { private static final long serialVersionUID = 938883901412562913L; private static final Logger LOG = LoggerFactory.getLogger(ClassInheritanceGraphDialog.class); private static final String FONT = "fontname=\"Courier\" fontsize=12"; private ClassNode cls; private boolean longNames = false; private boolean overrides = false; private Map objectToNodeID = new HashMap<>(); private int nextNodeID = 0; public ClassInheritanceGraphDialog(MainWindow mainWindow, ClassNode cls) { super(mainWindow, String.format("%s: %s", NLS.str("graph_viewer.inheritance_graph.title"), DotGraphUtils.classFormatName(cls, false))); this.cls = cls; } public JMenuBar addMenuBar() { JMenuBar menuBar = super.addMenuBar(); // Long names checkbox JCheckBox showLongNames = new JCheckBox(NLS.str("graph_viewer.long_names")); showLongNames.setSelected(false); showLongNames.addItemListener(e -> { longNames = showLongNames.isSelected(); reload(); }); // Overrides checkbox JCheckBox showOverrides = new JCheckBox(NLS.str("graph_viewer.overrides")); showOverrides.setSelected(false); showOverrides.addItemListener(e -> { overrides = showOverrides.isSelected(); reload(); }); // Assemble menubar panel JPanel menuBarPanel = new JPanel(); menuBarPanel.setOpaque(false); menuBarPanel.setLayout(new WrapLayout(FlowLayout.LEFT)); menuBarPanel.add(showLongNames, BorderLayout.PAGE_START); menuBarPanel.add(showOverrides, BorderLayout.PAGE_START); // Add menubar panel to menuBar menuBar.add(menuBarPanel); return menuBar; } public static void open(MainWindow window, JClass node) { ClassNode cls = node.getCls().getClassNode(); ClassInheritanceGraphDialog graphDialog = new ClassInheritanceGraphDialog(window, cls); graphDialog.addMenuBar(); graphDialog.setVisible(true); graphDialog.reload(); } public void reload() { SwingUtilities.invokeLater(() -> { String graph = generateGraph(cls); getPanel().setGraph(graph); }); } private String generateGraph(ClassNode rootClass) { StringBuilder sb = new StringBuilder(); ClassNode cls = rootClass; objectToNodeID = new HashMap<>(); Color themeBackground = UIManager.getColor("Panel.background"); Color themeForeground = UIManager.getColor("Label.foreground"); Color themeHighlight = UIManager.getColor("Component.focusedBorderColor"); Color themeShade = UIManager.getColor("TextArea.background"); String bgColor = String.format("bgcolor=\"#%02x%02x%02x\"", themeBackground.getRed(), themeBackground.getGreen(), themeBackground.getBlue()); String lineColor = String.format("color=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), themeForeground.getBlue()); String fontColor = String.format("fontcolor=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), themeForeground.getBlue()); String highlightColor = String.format("color=\"#%02x%02x%02x\"", themeHighlight.getRed(), themeHighlight.getGreen(), themeHighlight.getBlue()); String shadeColor = String.format("fillcolor=\"#%02x%02x%02x\"", themeShade.getRed(), themeShade.getGreen(), themeShade.getBlue()); try (Formatter f = new Formatter(sb)) { // graph header f.format("digraph G {\n"); f.format("%s\n", bgColor); f.format("node[shape=\"record\" style=\"filled\" %s %s %s %s]\n", FONT, fontColor, lineColor, shadeColor); f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor); // add nodes processClass(f, cls, highlightColor); // close graph f.format("}"); return f.toString(); } } private int processClass(Formatter f, ClassNode cls) { return processClass(f, cls, ""); } private int processClass(Formatter f, ClassNode cls, String extra) { if (objectToNodeID.containsKey(cls)) { // Don't process a class that has been processed before return objectToNodeID.get(cls); } int classID = addNode(f, cls, extra); // add interface relationships List ifaces = cls.getInterfaces(); for (int i = 0; i < ifaces.size(); i++) { ArgType iface = ifaces.get(i); int ifaceID; ClassNode ifaceNode = cls.root().resolveClass(iface); if (ifaceNode != null) { ifaceID = processClass(f, ifaceNode); objectToNodeID.put(iface, ifaceID); } else { ifaceID = addNode(f, iface); } // Classes implement interfaces, interfaces extend interfaces String edgeLabel = cls.getAccessFlags().isInterface() ? "extends" : "implements"; f.format("Node_%d -> Node_%d [label=\"%s\" style=\"dashed\" ]\n", classID, ifaceID, edgeLabel); } // add superclass relationship ArgType superClass = cls.getSuperClass(); if (superClass != ArgType.OBJECT) { int superClsID; cls = cls.root().resolveClass(superClass); if (cls != null) { superClsID = processClass(f, cls); objectToNodeID.put(superClass, superClsID); } else { superClsID = addNode(f, superClass); } f.format("Node_%d -> Node_%d [label=\"extends\" ]\n", classID, superClsID); } return classID; } // Add a node for a class private int addNode(Formatter f, ClassNode cls) { return addNode(f, cls, ""); } private int addNode(Formatter f, ClassNode cls, String extra) { int nodeID; if (objectToNodeID.containsKey(cls)) { nodeID = objectToNodeID.get(cls); } else { nodeID = nextNodeID; nextNodeID++; objectToNodeID.put(cls, nodeID); } if (cls.getAccessFlags().isInterface()) { extra += " style=\"dashed, filled\""; } String name = DotGraphUtils.classFormatName(cls, longNames); f.format("Node_%d [ label=\"{%s\\ ", nodeID, UiUtils.toDotNodeName(name)); if (overrides) { f.format("|"); List> table = new ArrayList<>(); for (MethodNode method : cls.getMethods()) { MethodOverrideAttr ovrdAttr = method.get(AType.METHOD_OVERRIDE); if (ovrdAttr != null) { if (!ovrdAttr.getOverrideList().isEmpty()) { String methodName = DotGraphUtils.methodFormatName(method, longNames); Formatter details = new Formatter(); details.format(" overrides "); for (IMethodDetails baseMthDetails : ovrdAttr.getOverrideList()) { String baseClassName = DotGraphUtils.classFormatName(baseMthDetails.getMethodInfo().getDeclClass(), longNames); details.format("%s, ", baseClassName); } String detailsString = details.toString(); // Remove trailing ', ' detailsString = detailsString.substring(0, detailsString.length() - 2); table.add(Pair.of(methodName, detailsString)); details.close(); } } } if (!table.isEmpty()) { int longestLength = table.stream().map(Pair::getFirst).map(String::length).max((a, b) -> a - b).get(); for (Pair entry : table) { f.format("%-" + longestLength + "s %s\\l", entry.getFirst(), entry.getSecond()); } } else { f.format("No overrides."); } } f.format("}\" %s]\n", extra); return nodeID; } // Add a node for an unresolved argtype private int addNode(Formatter f, ArgType argType) { return addNode(f, argType, ""); } private int addNode(Formatter f, ArgType argType, String extra) { int nodeID; if (objectToNodeID.containsKey(argType)) { nodeID = objectToNodeID.get(argType); } else { nodeID = nextNodeID; nextNodeID++; objectToNodeID.put(argType, nodeID); } Color themeOutOfFocus = UIManager.getColor("Component.disabledBorderColor"); String outOfFocus = String.format("color=\"#%02x%02x%02x\"", themeOutOfFocus.getRed(), themeOutOfFocus.getGreen(), themeOutOfFocus.getBlue()); String name = DotGraphUtils.interfaceFormatName(argType, cls, longNames); f.format("Node_%d [ label=\"{%s}\" %s %s]\n", nodeID, UiUtils.toDotNodeName(name), outOfFocus, extra); return nodeID; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassMethodGraphDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Color; import java.awt.FlowLayout; import java.util.Collections; import java.util.Formatter; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.swing.JCheckBox; import javax.swing.JMenuBar; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.UIManager; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.DotGraphUtils; import jadx.gui.treemodel.JClass; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.layout.WrapLayout; public class ClassMethodGraphDialog extends GraphDialog { private static final long serialVersionUID = -850803763322590708L; private static final String FONT = "fontname=\"Courier\" fontsize=12"; private int callerDepthLimit = 10; private int nextNodeID = 0; private Map methodToNodeID; private Set edges; private List javaMethods = Collections.emptyList(); private ClassNode cls; private boolean longNames = false; public ClassMethodGraphDialog(MainWindow mainWindow, ClassNode cls) { super(mainWindow, String.format("%s: %s", NLS.str("graph_viewer.method_graph.title"), DotGraphUtils.classFormatName(cls, false))); this.cls = cls; } public JMenuBar addMenuBar() { JMenuBar menuBar = super.addMenuBar(); // Long names checkbox JCheckBox showLongNames = new JCheckBox(NLS.str("graph_viewer.long_names")); showLongNames.setSelected(false); showLongNames.addItemListener(e -> { longNames = showLongNames.isSelected(); reload(); }); // Assemble menubar panel JPanel menuBarPanel = new JPanel(); menuBarPanel.setOpaque(false); menuBarPanel.setLayout(new WrapLayout(FlowLayout.LEFT)); menuBarPanel.add(showLongNames, BorderLayout.PAGE_START); // Add menubar panel to menuBar menuBar.add(menuBarPanel); return menuBar; } public static void open(MainWindow window, JClass node) { ClassNode cls = node.getCls().getClassNode(); ClassMethodGraphDialog graphDialog = new ClassMethodGraphDialog(window, cls); graphDialog.addMenuBar(); graphDialog.setVisible(true); graphDialog.reload(); } public void reload() { SwingUtilities.invokeLater(() -> { String graph = generateGraph(cls); getPanel().setGraph(graph); }); } private String generateGraph(ClassNode classNode) { StringBuilder sb = new StringBuilder(); Color themeBackground = UIManager.getColor("Panel.background"); Color themeForeground = UIManager.getColor("Label.foreground"); Color themeShade = UIManager.getColor("TextArea.background"); String bgColor = String.format("bgcolor=\"#%02x%02x%02x\"", themeBackground.getRed(), themeBackground.getGreen(), themeBackground.getBlue()); String lineColor = String.format("color=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), themeForeground.getBlue()); String fontColor = String.format("fontcolor=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), themeForeground.getBlue()); String shadeColor = String.format("fillcolor=\"#%02x%02x%02x\"", themeShade.getRed(), themeShade.getGreen(), themeShade.getBlue()); try (Formatter f = new Formatter(sb)) { // graph header f.format("digraph G {\n"); f.format("%s\n", bgColor); f.format("node[shape=\"record\" style=\"filled\" %s %s %s %s]\n", FONT, fontColor, lineColor, shadeColor); f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor); nextNodeID = 0; methodToNodeID = new HashMap<>(); edges = new HashSet<>(); List methods = classNode.getMethods(); javaMethods = methods.stream().map(method -> method.getJavaNode()).collect(Collectors.toList()); for (JavaMethod javaMethod : javaMethods) { addNode(f, javaMethod); // add caller relationships addCallers(0, f, javaMethod); } // close graph f.format("}"); return f.toString(); } } private void addCallers(int depth, Formatter f, JavaMethod javaMethod) { if (depth >= callerDepthLimit) { return; } List uses = javaMethod.getUseIn(); // add "calls" relationships for (JavaNode node : uses) { if (!(node instanceof JavaMethod)) { continue; } JavaMethod caller = (JavaMethod) node; // Do not process callers that are not methods from the class if (!javaMethods.contains(node)) { continue; } int nodeID = addNode(f, caller); addEdge(f, nodeID, methodToNodeID.get(javaMethod)); addCallers(depth + 1, f, caller); } } // Add a node representing method to the graph in f. Returns the ID of the new node private int addNode(Formatter f, JavaMethod method) { int nodeID; if (methodToNodeID.containsKey(method)) { nodeID = methodToNodeID.get(method); } else { nodeID = nextNodeID; nextNodeID++; methodToNodeID.put(method, nodeID); } String name = DotGraphUtils.methodFormatName(method, longNames); f.format("Node_%d [ label=\"{%s}\"]\n", nodeID, UiUtils.toDotNodeName(name)); if (method.callsSelf()) { addEdge(f, nodeID, nodeID); } return nodeID; } // Add an edge between sourceID and destID to the graph in f private void addEdge(Formatter f, int sourceID, int destID) { Edge edge = new Edge(sourceID, destID); if (!edges.contains(edge)) { f.format("Node_%d -> Node_%d\n", sourceID, destID); edges.add(edge); } } private static class Edge { public int source; public int dest; public Edge(int source, int dest) { this.source = source; this.dest = dest; } @Override public boolean equals(Object otherObject) { if (!(otherObject instanceof Edge)) { return false; } Edge other = (Edge) otherObject; return (this.source == other.source) && (this.dest == other.dest); } @Override public int hashCode() { return Objects.hash(source, dest); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Consumer; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.SwingConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.data.CommentStyle; import jadx.api.data.ICodeComment; import jadx.api.data.impl.JadxCodeComment; import jadx.api.data.impl.JadxCodeData; import jadx.gui.settings.JadxProject; import jadx.gui.ui.codearea.CodeArea; import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; public class CommentDialog extends CommonDialog { private static final long serialVersionUID = -1865682124935757528L; private static final Logger LOG = LoggerFactory.getLogger(CommentDialog.class); public static void show(CodeArea codeArea, ICodeComment comment, boolean updateComment) { CommentDialog dialog = new CommentDialog(codeArea, comment, updateComment); dialog.setVisible(true); } private static void updateCommentsData(CodeArea codeArea, Consumer> updater) { try { JadxProject project = codeArea.getProject(); JadxCodeData codeData = project.getCodeData(); if (codeData == null) { codeData = new JadxCodeData(); } List list = new ArrayList<>(codeData.getComments()); updater.accept(list); Collections.sort(list); codeData.setComments(list); project.setCodeData(codeData); codeArea.getMainWindow().getWrapper().reloadCodeData(); } catch (Exception e) { LOG.error("Comment action failed", e); } try { // refresh code in a background thread to avoid blocking the ui codeArea.backgroundRefreshClass(); } catch (Exception e) { LOG.error("Failed to reload code", e); } } private final transient CodeArea codeArea; private final transient ICodeComment comment; private final transient boolean updateComment; private transient JTextArea commentArea; private transient JComboBox styleCombo; public CommentDialog(CodeArea codeArea, ICodeComment comment, boolean updateComment) { super(codeArea.getMainWindow()); this.codeArea = codeArea; this.comment = comment; this.updateComment = updateComment; initUI(); } private void apply() { String newCommentStr = commentArea.getText().trim(); if (newCommentStr.isEmpty()) { if (updateComment) { remove(); } else { cancel(); } return; } CommentStyle style = (CommentStyle) styleCombo.getSelectedItem(); ICodeComment newComment = new JadxCodeComment(comment.getNodeRef(), comment.getCodeRef(), newCommentStr, style); if (updateComment) { updateCommentsData(codeArea, list -> { list.remove(comment); list.add(newComment); }); } else { updateCommentsData(codeArea, list -> list.add(newComment)); } dispose(); } private void remove() { updateCommentsData(codeArea, list -> list.removeIf(c -> c == comment)); dispose(); } private void cancel() { dispose(); } private void initUI() { commentArea = new JTextArea(); TextStandardActions.attach(commentArea); commentArea.setEditable(true); commentArea.setFont(mainWindow.getSettings().getCodeFont()); commentArea.setAlignmentX(Component.LEFT_ALIGNMENT); commentArea.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_ENTER: if (e.isShiftDown() || e.isControlDown()) { commentArea.insert("\n", commentArea.getCaretPosition()); } else { apply(); } break; case KeyEvent.VK_ESCAPE: cancel(); break; } } }); if (updateComment) { commentArea.setText(comment.getComment()); } JScrollPane textAreaScrollPane = new JScrollPane(commentArea); textAreaScrollPane.setAlignmentX(LEFT_ALIGNMENT); styleCombo = new JComboBox<>(CommentStyle.values()); styleCombo.setSelectedItem(comment.getStyle()); JLabel commentLabel = new JLabel(NLS.str("comment_dialog.label"), SwingConstants.LEFT); JLabel styleLabel = new JLabel(NLS.str("comment_dialog.style"), SwingConstants.LEFT); JLabel usageLabel = new JLabel(NLS.str("comment_dialog.usage"), SwingConstants.LEFT); JPanel inputPanel = new JPanel(); inputPanel.setLayout(new BoxLayout(inputPanel, BoxLayout.PAGE_AXIS)); inputPanel.add(commentLabel); inputPanel.add(Box.createRigidArea(new Dimension(0, 5))); inputPanel.add(textAreaScrollPane); inputPanel.add(Box.createRigidArea(new Dimension(0, 5))); inputPanel.add(usageLabel); JPanel stylePanel = new JPanel(); stylePanel.setLayout(new BoxLayout(stylePanel, BoxLayout.LINE_AXIS)); stylePanel.setAlignmentX(LEFT_ALIGNMENT); stylePanel.add(styleLabel); stylePanel.add(Box.createRigidArea(new Dimension(5, 0))); stylePanel.add(styleCombo); JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); mainPanel.add(inputPanel, BorderLayout.CENTER); mainPanel.add(stylePanel, BorderLayout.PAGE_END); mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); JPanel buttonPane = initButtonsPanel(); Container contentPane = getContentPane(); contentPane.add(mainPanel, BorderLayout.CENTER); contentPane.add(buttonPane, BorderLayout.PAGE_END); if (updateComment) { setTitle(NLS.str("comment_dialog.title.update")); } else { setTitle(NLS.str("comment_dialog.title.add")); } commonWindowInit(); } protected JPanel initButtonsPanel() { JButton cancelButton = new JButton(NLS.str("common_dialog.cancel")); cancelButton.addActionListener(event -> cancel()); String applyStr = updateComment ? NLS.str("common_dialog.update") : NLS.str("common_dialog.add"); JButton renameBtn = new JButton(applyStr); renameBtn.addActionListener(event -> apply()); getRootPane().setDefaultButton(renameBtn); JButton removeBtn; if (updateComment) { removeBtn = new JButton(NLS.str("common_dialog.remove")); removeBtn.addActionListener(event -> remove()); } else { removeBtn = null; } JPanel buttonPane = new JPanel(); buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); buttonPane.add(Box.createRigidArea(new Dimension(5, 0))); buttonPane.add(Box.createHorizontalGlue()); buttonPane.add(renameBtn); if (removeBtn != null) { buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(removeBtn); } buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(cancelButton); return buttonPane; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.Dimension; import javax.swing.JDialog; import javax.swing.WindowConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.ui.MainWindow; import jadx.gui.utils.UiUtils; public abstract class CommonDialog extends JDialog { private static final Logger LOG = LoggerFactory.getLogger(CommonDialog.class); protected final MainWindow mainWindow; public CommonDialog(MainWindow mainWindow) { super(mainWindow); this.mainWindow = mainWindow; } protected void commonWindowInit() { setModalityType(ModalityType.APPLICATION_MODAL); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); UiUtils.addEscapeShortCutToDispose(this); setLocationRelativeTo(null); UiUtils.uiRunAndWait(this::pack); Dimension minSize = getSize(); setMinimumSize(minSize); if (!mainWindow.getSettings().loadWindowPos(this)) { setSize(incByPercent(minSize.getWidth(), 30), incByPercent(minSize.getHeight(), 30)); } } @Override public void dispose() { try { mainWindow.getSettings().saveWindowPos(this); } catch (Exception e) { LOG.warn("Failed to save window size and position", e); } super.dispose(); } private static int incByPercent(double value, int percent) { return (int) (value * (1 + percent * 0.01)); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rtextarea.SearchContext; import org.fife.ui.rtextarea.SearchEngine; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ch.qos.logback.classic.Level; import jadx.api.JavaNode; import jadx.gui.logs.LogOptions; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResSearchNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.panel.ProgressPanel; import jadx.gui.ui.tab.TabsController; import jadx.gui.utils.CacheObject; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.NodeLabel; import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED; import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED; public abstract class CommonSearchDialog extends JFrame { private static final Logger LOG = LoggerFactory.getLogger(CommonSearchDialog.class); private static final long serialVersionUID = 8939332306115370276L; protected final transient TabsController tabsController; protected final transient CacheObject cache; protected final transient MainWindow mainWindow; protected final transient Font codeFont; protected final transient String windowTitle; protected ResultsModel resultsModel; protected ResultsTable resultsTable; protected JLabel resultsInfoLabel; protected JLabel progressInfoLabel; protected JLabel warnLabel; protected ProgressPanel progressPane; private SearchContext highlightContext; public CommonSearchDialog(MainWindow mainWindow, String title) { this.mainWindow = mainWindow; this.tabsController = mainWindow.getTabsController(); this.cache = mainWindow.getCacheObject(); this.codeFont = mainWindow.getSettings().getCodeFont(); this.windowTitle = title; UiUtils.setWindowIcons(this); updateTitle(""); } protected abstract void openInit(); protected abstract void loadFinished(); protected abstract void loadStart(); public void loadWindowPos() { if (!mainWindow.getSettings().loadWindowPos(this)) { setSize(800, 500); } } private void updateTitle(String searchText) { if (searchText == null || searchText.isEmpty() || searchText.trim().isEmpty()) { setTitle(windowTitle); } else { setTitle(windowTitle + ": " + searchText); } } public void updateHighlightContext(String text, boolean caseSensitive, boolean regexp, boolean wholeWord) { updateTitle(text); highlightContext = new SearchContext(text); highlightContext.setMatchCase(caseSensitive); highlightContext.setWholeWord(wholeWord); highlightContext.setRegularExpression(regexp); highlightContext.setMarkAll(true); } public void disableHighlight() { highlightContext = null; } protected void registerInitOnOpen() { addWindowListener(new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { SwingUtilities.invokeLater(CommonSearchDialog.this::openInit); } }); } protected void openSelectedItem() { JNode node = getSelectedNode(); if (node == null) { return; } openItem(node); } protected void openItem(JNode node) { if (node instanceof JResSearchNode) { JumpPosition jmpPos = new JumpPosition(((JResSearchNode) node).getResNode(), node.getPos()); tabsController.codeJump(jmpPos); } else { tabsController.codeJump(node); } if (!mainWindow.getSettings().isKeepCommonDialogOpen()) { dispose(); } } @Nullable private JNode getSelectedNode() { try { int selectedId = resultsTable.getSelectedRow(); if (selectedId == -1 || selectedId >= resultsTable.getRowCount()) { return null; } return (JNode) resultsModel.getValueAt(selectedId, 0); } catch (Exception e) { LOG.error("Failed to get results table selected object", e); return null; } } @Override public void dispose() { mainWindow.getSettings().saveWindowPos(this); super.dispose(); } protected void initCommon() { UiUtils.addEscapeShortCutToDispose(this); } protected void copyAllSearchResults() { StringBuilder sb = new StringBuilder(); Set uniqueRefs = new HashSet<>(); for (JNode node : resultsModel.rows) { JavaNode javaNode = node.getJavaNode(); if (javaNode != null) { String codeNodeRef = javaNode.getCodeNodeRef().toString(); if (uniqueRefs.add(codeNodeRef)) { sb.append(codeNodeRef).append("\n"); } } } UiUtils.copyToClipboard(sb.toString()); } @NotNull protected JPanel initButtonsPanel() { progressPane = new ProgressPanel(mainWindow, false); JButton cancelButton = new JButton(NLS.str("search_dialog.cancel")); cancelButton.addActionListener(event -> dispose()); JButton openBtn = new JButton(NLS.str("search_dialog.open")); openBtn.addActionListener(event -> openSelectedItem()); getRootPane().setDefaultButton(openBtn); JButton copyBtn = new JButton(NLS.str("search_dialog.copy")); copyBtn.addActionListener(event -> copyAllSearchResults()); JCheckBox cbKeepOpen = new JCheckBox(NLS.str("search_dialog.keep_open")); cbKeepOpen.setSelected(mainWindow.getSettings().isKeepCommonDialogOpen()); cbKeepOpen.addActionListener(e -> mainWindow.getSettings().saveKeepCommonDialogOpen(cbKeepOpen.isSelected())); cbKeepOpen.setAlignmentY(Component.CENTER_ALIGNMENT); JPanel buttonPane = new JPanel(); buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); buttonPane.add(cbKeepOpen); buttonPane.add(Box.createRigidArea(new Dimension(15, 0))); buttonPane.add(progressPane); buttonPane.add(Box.createRigidArea(new Dimension(5, 0))); buttonPane.add(Box.createHorizontalGlue()); buttonPane.add(copyBtn); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(openBtn); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(cancelButton); return buttonPane; } protected JPanel initResultsTable() { ResultsTableCellRenderer renderer = new ResultsTableCellRenderer(); resultsModel = new ResultsModel(); resultsModel.addTableModelListener(e -> updateProgressLabel(false)); resultsTable = new ResultsTable(resultsModel, renderer); resultsTable.setShowHorizontalLines(false); resultsTable.setDragEnabled(false); resultsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); resultsTable.setColumnSelectionAllowed(false); resultsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); resultsTable.setAutoscrolls(false); resultsTable.setDefaultRenderer(Object.class, renderer); Enumeration columns = resultsTable.getColumnModel().getColumns(); while (columns.hasMoreElements()) { TableColumn column = columns.nextElement(); column.setCellRenderer(renderer); } resultsTable.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent evt) { if (evt.getClickCount() == 2) { openSelectedItem(); } } }); resultsTable.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) { openSelectedItem(); } } }); // override copy action to copy long string of node column resultsTable.getActionMap().put("copy", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { JNode selectedNode = getSelectedNode(); if (selectedNode != null) { UiUtils.copyToClipboard(selectedNode.makeLongString()); } } }); warnLabel = new JLabel(); warnLabel.setForeground(Color.RED); warnLabel.setVisible(false); JScrollPane scroll = new JScrollPane(resultsTable, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_AS_NEEDED); resultsInfoLabel = new JLabel(""); resultsInfoLabel.setFont(mainWindow.getSettings().getUiFont()); progressInfoLabel = new JLabel(""); progressInfoLabel.setFont(mainWindow.getSettings().getUiFont()); progressInfoLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { mainWindow.showLogViewer(LogOptions.allWithLevel(Level.INFO)); } }); JPanel resultsActionsPanel = new JPanel(); resultsActionsPanel.setLayout(new BoxLayout(resultsActionsPanel, BoxLayout.LINE_AXIS)); resultsActionsPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0)); addResultsActions(resultsActionsPanel); JPanel resultsPanel = new JPanel(); resultsPanel.setLayout(new BoxLayout(resultsPanel, BoxLayout.PAGE_AXIS)); resultsPanel.add(warnLabel, BorderLayout.PAGE_START); resultsPanel.add(scroll, BorderLayout.CENTER); resultsPanel.add(resultsActionsPanel, BorderLayout.PAGE_END); return resultsPanel; } protected void addResultsActions(JPanel resultsActionsPanel) { resultsActionsPanel.add(Box.createRigidArea(new Dimension(20, 0))); resultsActionsPanel.add(resultsInfoLabel); resultsActionsPanel.add(Box.createRigidArea(new Dimension(20, 0))); resultsActionsPanel.add(progressInfoLabel); resultsActionsPanel.add(Box.createHorizontalGlue()); } protected void updateProgressLabel(boolean complete) { int count = resultsModel.getRowCount(); String statusText; if (complete) { statusText = NLS.str("search_dialog.results_complete", count); } else { statusText = NLS.str("search_dialog.results_incomplete", count); } resultsInfoLabel.setText(statusText); } protected void showSearchState() { resultsInfoLabel.setText(NLS.str("search_dialog.tip_searching") + "..."); } protected static final class ResultsTable extends JTable { private static final long serialVersionUID = 3901184054736618969L; private final transient ResultsModel model; public ResultsTable(ResultsModel resultsModel, ResultsTableCellRenderer renderer) { super(resultsModel); this.model = resultsModel; setRowHeight(renderer.getMaxRowHeight()); } public void initColumnWidth() { int columnCount = getColumnCount(); int width = getParent().getWidth(); int colWidth = model.isAddDescColumn() ? width / 2 : width; columnModel.getColumn(0).setPreferredWidth(colWidth); for (int col = 1; col < columnCount; col++) { columnModel.getColumn(col).setPreferredWidth(width); } } public void updateTable() { UiUtils.uiThreadGuard(); int rowCount = getRowCount(); if (rowCount == 0) { updateUI(); return; } long start = System.currentTimeMillis(); int width = getParent().getWidth(); TableColumn firstColumn = columnModel.getColumn(0); if (model.isAddDescColumn()) { if (firstColumn.getWidth() > width * 0.8) { // first column too big and hide second column, resize it firstColumn.setPreferredWidth(width / 2); } TableColumn secondColumn = columnModel.getColumn(1); int columnMaxWidth = width * 2; // set big enough size to skip per row check if (secondColumn.getWidth() < columnMaxWidth) { secondColumn.setPreferredWidth(columnMaxWidth); } } else { firstColumn.setPreferredWidth(width); } updateUI(); if (LOG.isDebugEnabled()) { LOG.debug("Update results table in {}ms, count: {}", System.currentTimeMillis() - start, rowCount); } } @Override public Object getValueAt(int row, int column) { return model.getValueAt(row, column); } @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { // ResultsTable only has two wide columns, the default increment is way too fast if (orientation == SwingConstants.HORIZONTAL) { return 30; } return super.getScrollableUnitIncrement(visibleRect, orientation, direction); } } protected static final class ResultsModel extends AbstractTableModel { private static final long serialVersionUID = -7821286846923903208L; private static final String[] COLUMN_NAMES = { NLS.str("search_dialog.col_node"), NLS.str("search_dialog.col_code") }; private final transient List rows = new ArrayList<>(); private transient boolean addDescColumn; public void addAll(Collection nodes) { rows.addAll(nodes); if (!addDescColumn) { for (JNode row : rows) { if (row.hasDescString()) { addDescColumn = true; break; } } } } public void clear() { addDescColumn = false; rows.clear(); } public void sort() { Collections.sort(rows); } public boolean isAddDescColumn() { return addDescColumn; } @Override public int getRowCount() { return rows.size(); } @Override public int getColumnCount() { return 2; } @Override public String getColumnName(int index) { return COLUMN_NAMES[index]; } @Override public Object getValueAt(int rowIndex, int columnIndex) { return rows.get(rowIndex); } } protected final class ResultsTableCellRenderer implements TableCellRenderer { private final NodeLabel label; private final RSyntaxTextArea codeArea; private final NodeLabel emptyLabel; private final Color codeSelectedColor; private final Color codeBackground; public ResultsTableCellRenderer() { codeArea = AbstractCodeArea.getDefaultArea(mainWindow); codeArea.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10)); codeArea.setRows(1); codeBackground = codeArea.getBackground(); codeSelectedColor = codeArea.getSelectionColor(); label = new NodeLabel(); label.setOpaque(true); label.setFont(codeArea.getFont()); label.setHorizontalAlignment(SwingConstants.LEFT); emptyLabel = new NodeLabel(); emptyLabel.setOpaque(true); } @Override public Component getTableCellRendererComponent(JTable table, Object obj, boolean isSelected, boolean hasFocus, int row, int column) { if (obj == null || table == null) { return emptyLabel; } Component comp = makeCell((JNode) obj, column); updateSelection(table, comp, column, isSelected); return comp; } private void updateSelection(JTable table, Component comp, int column, boolean isSelected) { if (column == 1) { if (isSelected) { comp.setBackground(codeSelectedColor); } else { comp.setBackground(codeBackground); } } else { if (isSelected) { comp.setBackground(table.getSelectionBackground()); comp.setForeground(table.getSelectionForeground()); } else { comp.setBackground(table.getBackground()); comp.setForeground(table.getForeground()); } } } private Component makeCell(JNode node, int column) { if (column == 0) { label.disableHtml(node.disableHtml()); label.setText(node.makeLongStringHtml()); label.setToolTipText(node.getTooltip()); label.setIcon(node.getIcon()); return label; } if (!node.hasDescString()) { return emptyLabel; } codeArea.setSyntaxEditingStyle(node.getSyntaxName()); String descStr = node.makeDescString(); codeArea.setText(descStr); codeArea.setColumns(descStr.length() + 1); if (highlightContext != null) { SearchEngine.markAll(codeArea, highlightContext); } return codeArea; } public int getMaxRowHeight() { label.setText("Text"); codeArea.setText("Text"); return Math.max(getCompHeight(label), getCompHeight(codeArea)); } private int getCompHeight(Component comp) { return Math.max(comp.getHeight(), comp.getPreferredSize().height); } } void progressStartCommon() { progressPane.setIndeterminate(true); progressPane.setVisible(true); warnLabel.setVisible(false); } void progressFinishedCommon() { progressPane.setVisible(false); } protected JNodeCache getNodeCache() { return mainWindow.getCacheObject().getNodeCache(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/ControlFlowGraphDialog.java ================================================ package jadx.gui.ui.dialog; import java.io.File; import java.util.Scanner; import javax.swing.SwingUtilities; import jadx.api.JavaMethod; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.DotGraphUtils; import jadx.gui.treemodel.JMethod; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; public class ControlFlowGraphDialog extends GraphDialog { private static final long serialVersionUID = -68749445239697710L; public ControlFlowGraphDialog(MainWindow mainWindow, String method) { super(mainWindow, String.format("%s: %s", NLS.str("graph_viewer.cfg.title"), method)); } public static void open(MainWindow window, JMethod method, boolean useRegions, boolean rawInsn) { JavaMethod javaMethod = method.getJavaMethod(); GraphDialog graphDialog = new ControlFlowGraphDialog(window, DotGraphUtils.methodFormatName(javaMethod, false)); graphDialog.addMenuBar(); graphDialog.setVisible(true); SwingUtilities.invokeLater(() -> { String graph = generateGraph(javaMethod, useRegions, rawInsn); if (graph != null) { graphDialog.getPanel().setGraph(graph); } else { graphDialog.getPanel().invalidateImage(graphError(NLS.str("graph_viewer.file_not_found_error"))); } }); } private static String generateGraph(JavaMethod javaMethod, boolean useRegions, boolean rawInsn) { MethodNode mth = javaMethod.getMethodNode(); File file = new DotGraphUtils(useRegions, rawInsn).getFullFile(mth); try (Scanner reader = new Scanner(file)) { String contents = reader.useDelimiter("\\Z").next(); return contents; } catch (Exception e) { return null; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/ExcludePkgDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JDialog; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.WindowConstants; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; import jadx.api.JavaPackage; import jadx.gui.ui.MainWindow; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class ExcludePkgDialog extends JDialog { private static final long serialVersionUID = -1111111202104151030L; private final transient MainWindow mainWindow; private transient JTree tree; private transient DefaultMutableTreeNode treeRoot; private final transient List roots = new ArrayList<>(); public ExcludePkgDialog(MainWindow mainWindow) { super(mainWindow); this.mainWindow = mainWindow; initUI(); UiUtils.addEscapeShortCutToDispose(this); initPackageList(); } private void initUI() { setTitle(NLS.str("exclude_dialog.title")); tree = new JTree(); tree.setRowHeight(-1); treeRoot = new DefaultMutableTreeNode("Packages"); DefaultTreeModel treeModel = new DefaultTreeModel(treeRoot); tree.setModel(treeModel); tree.setCellRenderer(new PkgListCellRenderer()); JScrollPane listPanel = new JScrollPane(tree); listPanel.setBorder(BorderFactory.createEmptyBorder()); tree.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { TreePath path = tree.getPathForLocation(e.getX(), e.getY()); if (path != null && path.getLastPathComponent() instanceof PkgNode) { PkgNode node = (PkgNode) path.getLastPathComponent(); node.toggle(); repaint(); } } }); JPanel actionPanel = new JPanel(); actionPanel.setLayout(new BoxLayout(actionPanel, BoxLayout.LINE_AXIS)); JButton btnOk = new JButton(NLS.str("common_dialog.ok")); JButton btnAll = new JButton(NLS.str("exclude_dialog.select_all")); JButton btnInvert = new JButton(NLS.str("exclude_dialog.invert")); JButton btnDeselect = new JButton(NLS.str("exclude_dialog.deselect")); actionPanel.add(btnDeselect); actionPanel.add(Box.createRigidArea(new Dimension(10, 0))); actionPanel.add(btnInvert); actionPanel.add(Box.createRigidArea(new Dimension(10, 0))); actionPanel.add(btnAll); actionPanel.add(Box.createHorizontalGlue()); actionPanel.add(btnOk, BorderLayout.PAGE_END); JPanel mainPane = new JPanel(new BorderLayout(5, 5)); mainPane.add(listPanel, BorderLayout.CENTER); mainPane.add(actionPanel, BorderLayout.SOUTH); mainPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); getContentPane().add(mainPane); pack(); setSize(600, 700); setLocationRelativeTo(null); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setModalityType(ModalityType.MODELESS); btnOk.addActionListener(e -> { mainWindow.getWrapper().setExcludedPackages(getExcludes()); mainWindow.reopen(); dispose(); }); btnAll.addActionListener(e -> { roots.forEach(p -> p.setSelected(true)); tree.updateUI(); }); btnDeselect.addActionListener(e -> { roots.forEach(p -> p.setSelected(false)); tree.updateUI(); }); btnInvert.addActionListener(e -> { roots.forEach(PkgNode::toggle); tree.updateUI(); }); } private void initPackageList() { List pkgs = mainWindow.getWrapper().getPackages() .stream() .map(JavaPackage::getFullName) .collect(Collectors.toList()); getPackageTree(pkgs).forEach(treeRoot::add); initCheckbox(); tree.expandPath(new TreePath(treeRoot.getPath())); } private List getPackageTree(List names) { List roots = new ArrayList<>(); Set nameSet = new HashSet<>(); Map> childMap = new HashMap<>(); for (String name : names) { String parent = ""; int last = 0; do { int pos = name.indexOf('.', last); if (pos == -1) { pos = name.length(); } String fullName = name.substring(0, pos); if (!nameSet.contains(fullName)) { nameSet.add(fullName); PkgNode node = new PkgNode(fullName, name.substring(last, pos)); if (!parent.isEmpty()) { childMap.computeIfAbsent(parent, k -> new ArrayList<>()) .add(node); } else { roots.add(node); } } parent = fullName; last = pos + 1; } while (last < name.length()); } addToParent(null, roots, childMap); return this.roots; } private PkgNode addToParent(PkgNode parent, List roots, Map> childMap) { for (PkgNode root : roots) { String tempFullName = root.getFullName(); do { List children = childMap.get(tempFullName); if (children != null) { if (children.size() == 1) { PkgNode next = children.get(0); next.name = root.name + "." + next.name; tempFullName = next.fullName; next.fullName = root.fullName; root = next; continue; } else { addToParent(root, children, childMap); } } if (parent == null) { this.roots.add(root); } else { parent.add(root); } break; } while (true); } return parent; } private List getExcludes() { List excludes = new ArrayList<>(); walkTree(true, p -> excludes.add(p.getFullName())); return excludes; } private void initCheckbox() { Font tmp = mainWindow.getSettings().getCodeFont(); Font font = tmp.deriveFont(tmp.getSize() + 1.f); Set excluded = new HashSet<>(mainWindow.getWrapper().getExcludedPackages()); walkTree(false, p -> p.initCheckbox(excluded.contains(p.getFullName()), font)); } private void walkTree(boolean findSelected, Consumer consumer) { List queue = new ArrayList<>(roots); for (int i = 0; i < queue.size(); i++) { PkgNode node = queue.get(i); if (findSelected && node.isSelected()) { consumer.accept(node); } else { if (!findSelected) { consumer.accept(node); } for (int j = 0; j < node.getChildCount(); j++) { queue.add((PkgNode) node.getChildAt(j)); } } } } private static class PkgNode extends DefaultMutableTreeNode { private static final long serialVersionUID = -1111111202104151430L; String name; String fullName; JCheckBox checkbox; PkgNode(String fullName, String name) { this.name = name; this.fullName = fullName; } void initCheckbox(boolean select, Font font) { if (!select) { if (getParent() instanceof PkgNode) { select = ((PkgNode) getParent()).isSelected(); } } checkbox = new JCheckBox(name, select); checkbox.setFont(font); } boolean toggle() { boolean selected = !checkbox.isSelected(); setSelected(selected); toggleParents(selected); return selected; } void toggleParents(boolean select) { if (getParent() instanceof PkgNode) { PkgNode p = ((PkgNode) getParent()); if (select) { select = p.isChildrenAllSelected(); if (select) { p.checkbox.setSelected(true); p.toggleParents(true); } } else { p.checkbox.setSelected(false); p.toggleParents(false); } } } void setSelected(boolean select) { checkbox.setSelected(select); for (int i = 0; i < getChildCount(); i++) { ((PkgNode) getChildAt(i)).setSelected(select); } } boolean isSelected() { return checkbox.isSelected(); } String getFullName() { return fullName; } String getDisplayName() { return name; } boolean isChildrenAllSelected() { for (int i = 0; i < getChildCount(); i++) { if (!((PkgNode) getChildAt(i)).isSelected()) { return false; } } return true; } @Override public String toString() { return name; } } private static class PkgListCellRenderer extends DefaultTreeCellRenderer { private static final long serialVersionUID = -1111111202104151235L; @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { if (value instanceof PkgNode) { PkgNode node = (PkgNode) value; node.checkbox.setBackground(tree.getBackground()); node.checkbox.setForeground(tree.getForeground()); return node.checkbox; } Component c = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); setIcon(Icons.PACKAGE); return c; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/GotoAddressDialog.java ================================================ package jadx.gui.ui.dialog; import javax.swing.JOptionPane; import org.exbin.bined.swing.section.SectCodeArea; import jadx.gui.utils.HexUtils; import jadx.gui.utils.NLS; public class GotoAddressDialog { public void showSetSelectionDialog(SectCodeArea codeArea) { Object o = JOptionPane.showInputDialog( codeArea, NLS.str("hex_viewer.enter_address"), NLS.str("hex_viewer.goto_address"), JOptionPane.QUESTION_MESSAGE, null, null, Long.toHexString(codeArea.getDataPosition())); if (o != null) { boolean isValidAddress = HexUtils.isValidHexString(toString()); if (!isValidAddress) { return; } codeArea.setActiveCaretPosition(Long.parseLong(o.toString(), 16)); codeArea.validateCaret(); codeArea.revealCursor(); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/GraphDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseWheelEvent; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JMenuBar; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextArea; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import guru.nidi.graphviz.engine.Format; import guru.nidi.graphviz.engine.Graphviz; import guru.nidi.graphviz.model.MutableGraph; import guru.nidi.graphviz.parse.Parser; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.layout.WrapLayout; public abstract class GraphDialog extends JFrame { private static final long serialVersionUID = 5840390965763493590L; private static final Logger LOG = LoggerFactory.getLogger(GraphDialog.class); private final MainWindow mainWindow; private GraphPanel panel; private static final Dimension MIN_WINDOW_SIZE = new Dimension(800, 500); private JMenuBar menuBar = null; public static JTextArea graphError() { return graphError(NLS.str("graph_viewer.default_error")); } public static JTextArea graphError(String errorMessage) { JTextArea errorText = new JTextArea(); errorText.setText(errorMessage); errorText.setVisible(true); errorText.setEditable(false); errorText.setLineWrap(false); return errorText; } public static JTextArea graphError(Exception error) { JTextArea errorText = new JTextArea(); StringWriter stringWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(stringWriter); stringWriter.write(NLS.str("graph_viewer.default_error")); stringWriter.write(": "); error.printStackTrace(printWriter); errorText.setText(stringWriter.toString()); errorText.setVisible(true); errorText.setEditable(false); errorText.setLineWrap(false); return errorText; } public GraphDialog(MainWindow mainWindow) { this(mainWindow, NLS.str("graph_viewer.default_title")); } public JMenuBar addMenuBar() { JMenuBar menuBar = new JMenuBar(); menuBar.setLayout(new WrapLayout(FlowLayout.LEFT)); add(menuBar, BorderLayout.PAGE_START); this.menuBar = menuBar; JFileChooser fileChooser = new JFileChooser(); JButton saveButton = new JButton(NLS.str("graph_viewer.save_graph")); saveButton.setEnabled(false); saveButton.addActionListener(e -> { try { int option = fileChooser.showSaveDialog(this); if (option == JFileChooser.APPROVE_OPTION) { File file = fileChooser.getSelectedFile(); getPanel().renderer.render(Format.SVG).toFile(file); } } catch (Exception ex) { LOG.error("Failed to save file: ", ex); JOptionPane.showMessageDialog(this, NLS.str("graph_viewer.file_failure"), NLS.str("graph_viewer.file_failure"), JOptionPane.INFORMATION_MESSAGE); } }); // Assemble menubar panel JPanel menuBarPanel = new JPanel(); menuBarPanel.setOpaque(false); menuBarPanel.add(saveButton); // Add menubar panel to menuBar menuBar.add(menuBarPanel); return menuBar; } private void enableMenu() { JMenuBar menu = this.menuBar; setAllEnabled(true, menu); } private void disableMenu() { JMenuBar menu = this.menuBar; setAllEnabled(false, menu); } private void setAllEnabled(boolean isEnabled, JComponent component) { component.setEnabled(isEnabled); Component[] components = component.getComponents(); for (Component subComponent : components) { if (subComponent instanceof JComponent) { setAllEnabled(isEnabled, (JComponent) subComponent); } else { subComponent.setEnabled(isEnabled); } } } public GraphDialog(MainWindow mainWindow, String title) { super(title); this.mainWindow = mainWindow; setMinimumSize(MIN_WINDOW_SIZE); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); UiUtils.addEscapeShortCutToDispose(this); setLocationRelativeTo(null); loadWindowPos(); LOG.debug("Dialog w: {} h: {}", getWidth(), getHeight()); LOG.debug("cwd: {}", System.getProperty("user.dir")); panel = new GraphPanel(this); panel.setFocusable(true); panel.addMouseListener(new MouseListener() { public void mouseClicked(MouseEvent e) { requestFocusInWindow(); } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } }); setLayout(new BorderLayout()); add(panel, BorderLayout.CENTER); } public void loadWindowPos() { if (!mainWindow.getSettings().loadWindowPos(this)) { setPreferredSize(MIN_WINDOW_SIZE); } } @Override public void dispose() { try { mainWindow.getSettings().saveWindowPos(this); } catch (Exception e) { LOG.warn("Failed to save window size and position", e); } super.dispose(); } class GraphPanel extends JPanel { private Dimension fullImageSize = new Dimension(); private double scale = 1.0; private double minimumScale = 0.01; private double maximumScale = 7.0; private double translateX = 0; private double translateY = 0; private Point lastDragPoint = null; private BufferedImage image; private Graphviz renderer; private final GraphDialog parentDialog; public GraphPanel(GraphDialog parentDialog) { this.parentDialog = parentDialog; MouseAdapter ma = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { lastDragPoint = e.getPoint(); } @Override public void mouseDragged(MouseEvent e) { if (image != null) { Point p = e.getPoint(); translateX += (p.x - lastDragPoint.x) / scale; translateY += (p.y - lastDragPoint.y) / scale; lastDragPoint = p; repaint(); } } @Override public void mouseWheelMoved(MouseWheelEvent e) { if (image != null) { double prevScale = scale; scale *= Math.pow(1.1, -e.getWheelRotation()); if (scale > maximumScale) { scale = maximumScale; } if (scale < minimumScale) { scale = minimumScale; } if (scale != prevScale) { Point p = e.getPoint(); double px = (p.x - translateX * prevScale) / prevScale; double py = (p.y - translateY * prevScale) / prevScale; translateX = (p.x / scale) - px; translateY = (p.y / scale) - py; LOG.debug("Rescaling {}%", scale * 100); renderGraphScaled(); if (image == null) { return; } repaint(); } } } }; addMouseListener(ma); addMouseMotionListener(ma); addMouseWheelListener(ma); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (image != null) { Graphics2D g2d = (Graphics2D) g; AffineTransform transform = new AffineTransform(); transform.translate(translateX * scale, translateY * scale); g2d.drawImage(image, transform, null); } } public void setGraph(File dotString) { try { LOG.debug("Parsing DOT file: {} ", dotString.getAbsolutePath()); setGraph(new Parser().read(dotString)); } catch (Exception e) { LOG.error("Error parsing DOT file", e); invalidateImage(graphError(e)); } } public void setGraph(String dotString) { try { setGraph(new Parser().read(dotString)); } catch (Exception e) { LOG.error("Error parsing DOT string", e); invalidateImage(graphError(e)); } } public void setGraph(MutableGraph g) { renderer = Graphviz.fromGraph(g); parentDialog.enableMenu(); scale = 1.0; // set initial image scale and posiition Runnable doCenter = new Runnable() { public void run() { renderGraphFullSize(); if (image == null) { return; } LOG.debug("full image w {} h {}", fullImageSize.width, fullImageSize.height); // scale required to fit image to window width or height double heightScale = (double) getHeight() / (double) fullImageSize.height; double widthScale = (double) getWidth() / (double) fullImageSize.width; if (widthScale < heightScale) { scale = widthScale; LOG.debug("scaling to fit width {}/{} {}", getWidth(), fullImageSize.width, scale); } else { scale = heightScale; LOG.debug("scaling to fit height {}/{} {}", getHeight(), fullImageSize.height, scale); } scale = scale * 0.95; maximumScale = Math.sqrt(Integer.MAX_VALUE / (fullImageSize.width * fullImageSize.height)) / 8; minimumScale = Math.min(scale, maximumScale); renderGraphScaled(); if (image == null) { return; } // center image in window translateY = (getHeight() / 2 - (fullImageSize.height * scale) / 2) / scale; translateX = (getWidth() / 2 - (fullImageSize.width * scale) / 2) / scale; repaint(); } }; SwingUtilities.invokeLater(doCenter); } private void renderGraphFullSize() { try { image = null; image = renderer.render(Format.SVG).toImage(); if (image.getWidth() == 0 || image.getHeight() == 0) { // If rendered image is too small, calculating the scale would later cause a // division by zero LOG.error("Graph render failed, image too small"); invalidateImage(graphError(NLS.str("graph_viewer.image_too_small"))); return; } fullImageSize.setSize(image.getWidth(), image.getHeight()); } catch (IllegalArgumentException illegalArgumentException) { // If rendered image is too large, a Dimension object is passed invalid arguments LOG.error("Graph render failed, illegal arguments: ", illegalArgumentException); invalidateImage(graphError(NLS.str("graph_viewer.image_too_large"))); } catch (Exception e) { // A large image may cause a number of other other exception types caught here along with other // failure cases LOG.error("Graph render failed: ", e); invalidateImage(graphError(e)); } } private void renderGraphScaled() { try { if (fullImageSize.width * scale * fullImageSize.height * scale >= Integer.MAX_VALUE) { scale = maximumScale; } image = renderer.width((int) (fullImageSize.width * scale)).render(Format.SVG).toImage(); } catch (Exception e) { LOG.error("Graph render failed: ", e); invalidateImage(graphError(e)); } } public void invalidateImage(JTextArea errorMsg) { this.add(errorMsg); image = null; this.parentDialog.disableMenu(); this.revalidate(); repaint(); } } protected GraphPanel getPanel() { return this.panel; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/LogViewerDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Container; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import javax.swing.JFrame; import jadx.gui.logs.LogOptions; import jadx.gui.logs.LogPanel; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class LogViewerDialog extends JFrame { private static final long serialVersionUID = -2188700277429054641L; private static LogViewerDialog openLogDialog; private final transient JadxSettings settings; private final transient LogPanel logPanel; public static void open(MainWindow mainWindow, LogOptions logOptions) { LogViewerDialog logDialog; if (openLogDialog != null) { logDialog = openLogDialog; } else { logDialog = new LogViewerDialog(mainWindow, logOptions); openLogDialog = logDialog; } logDialog.setVisible(true); logDialog.toFront(); } private LogViewerDialog(MainWindow mainWindow, LogOptions logOptions) { settings = mainWindow.getSettings(); UiUtils.setWindowIcons(this); Runnable dock = () -> { mainWindow.getSettings().saveDockLogViewer(true); dispose(); mainWindow.showLogViewer(LogOptions.current()); }; logPanel = new LogPanel(mainWindow, logOptions, dock, this::dispose); Container contentPane = getContentPane(); contentPane.add(logPanel, BorderLayout.CENTER); setTitle(NLS.str("log_viewer.title")); pack(); setSize(800, 600); setDefaultCloseOperation(DISPOSE_ON_CLOSE); setLocationRelativeTo(null); settings.loadWindowPos(this); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { openLogDialog = null; } }); } @Override public void dispose() { logPanel.dispose(); settings.saveWindowPos(this); super.dispose(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/MethodsDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Dimension; import java.util.List; import java.util.function.Consumer; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.DefaultListModel; import javax.swing.DefaultListSelectionModel; import javax.swing.JButton; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ListSelectionModel; import javax.swing.WindowConstants; import jadx.api.JavaMethod; import jadx.gui.ui.MainWindow; import jadx.gui.ui.cellrenders.MethodsListRenderer; import jadx.gui.utils.NLS; public class MethodsDialog extends CommonDialog { private JList methodList; private final Consumer> listConsumer; public MethodsDialog(MainWindow mainWindow, List methods, Consumer> listConsumer) { super(mainWindow); this.listConsumer = listConsumer; initUI(methods); setVisible(true); } private void initUI(List methods) { setTitle(NLS.str("methods_dialog.title")); DefaultListModel defaultListModel = new DefaultListModel<>(); defaultListModel.addAll(methods); methodList = new JList<>(); methodList.setModel(defaultListModel); methodList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); methodList.setCellRenderer(new MethodsListRenderer()); methodList.setSelectionModel(new DefaultListSelectionModel() { @Override public void setSelectionInterval(int index0, int index1) { if (super.isSelectedIndex(index0)) { super.removeSelectionInterval(index0, index1); } else { super.addSelectionInterval(index0, index1); } } }); JScrollPane scrollPane = new JScrollPane(methodList); JPanel buttonPane = initButtonsPanel(); JPanel contentPanel = new JPanel(); contentPanel.setLayout(new BorderLayout(5, 5)); contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); contentPanel.add(scrollPane, BorderLayout.CENTER); contentPanel.add(buttonPane, BorderLayout.PAGE_END); getContentPane().add(contentPanel); pack(); setSize(500, 300); setLocationRelativeTo(null); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); } protected JPanel initButtonsPanel() { JButton cancelButton = new JButton(NLS.str("common_dialog.cancel")); cancelButton.addActionListener(event -> dispose()); JButton okBtn = new JButton(NLS.str("common_dialog.ok")); okBtn.addActionListener(event -> generateForSelected()); getRootPane().setDefaultButton(okBtn); JPanel buttonPane = new JPanel(); buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); buttonPane.add(Box.createHorizontalGlue()); buttonPane.add(okBtn); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(cancelButton); return buttonPane; } private void generateForSelected() { List selectedMethods = methodList.getSelectedValuesList(); if (!selectedMethods.isEmpty()) { this.listConsumer.accept(selectedMethods); } dispose(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/RenameDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JTextField; import javax.swing.SwingUtilities; import org.jetbrains.annotations.NotNull; import jadx.api.metadata.ICodeNodeRef; import jadx.api.plugins.events.types.NodeRenamedByUser; import jadx.core.utils.Utils; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JPackage; import jadx.gui.treemodel.JRenameNode; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; import jadx.gui.utils.pkgs.JRenamePackage; import jadx.gui.utils.ui.DocumentUpdateListener; import jadx.gui.utils.ui.NodeLabel; public class RenameDialog extends CommonDialog { private static final long serialVersionUID = -3269715644416902410L; private final transient JRenameNode node; private transient JTextField renameField; private transient JButton renameBtn; public static boolean rename(MainWindow mainWindow, JRenameNode node) { SwingUtilities.invokeLater(() -> { RenameDialog renameDialog = new RenameDialog(mainWindow, node); renameDialog.initRenameField(); renameDialog.setVisible(true); }); return true; } public static JPopupMenu buildRenamePopup(MainWindow mainWindow, JRenameNode node) { JPopupMenu menu = new JPopupMenu(); menu.add(buildRenamePopupMenuItem(mainWindow, node)); return menu; } public static JMenuItem buildRenamePopupMenuItem(MainWindow mainWindow, JRenameNode node) { JMenuItem jmi = new JMenuItem(NLS.str("popup.rename")); jmi.addActionListener(action -> RenameDialog.rename(mainWindow, node)); jmi.setEnabled(node.canRename()); return jmi; } private RenameDialog(MainWindow mainWindow, JRenameNode node) { super(mainWindow); this.node = node.replace(); initUI(); } private void initRenameField() { renameField.setText(node.getName()); renameField.selectAll(); } private boolean checkNewName(String newName) { if (newName.isEmpty()) { // use empty name to reset rename (revert to original) return true; } boolean valid = node.isValidName(newName); if (renameBtn.isEnabled() != valid) { renameBtn.setEnabled(valid); renameField.putClientProperty("JComponent.outline", valid ? "" : "error"); } return valid; } private void rename() { rename(renameField.getText().trim()); } private void resetName() { rename(""); } private void rename(String newName) { if (!checkNewName(newName)) { return; } String oldName = node.getName(); String newNodeName; boolean reset = newName.isEmpty(); if (reset) { node.removeAlias(); newNodeName = Utils.getOrElse(node.getJavaNode().getName(), ""); } else { newNodeName = newName; } sendRenameEvent(oldName, newNodeName, reset); dispose(); } private void sendRenameEvent(String oldName, String newName, boolean reset) { ICodeNodeRef nodeRef = node.getJavaNode().getCodeNodeRef(); NodeRenamedByUser event = new NodeRenamedByUser(nodeRef, oldName, newName); event.setRenameNode(node); event.setResetName(reset); mainWindow.events().send(event); } @NotNull protected JPanel initButtonsPanel() { JButton resetButton = new JButton(NLS.str("common_dialog.reset")); resetButton.addActionListener(event -> resetName()); JButton cancelButton = new JButton(NLS.str("common_dialog.cancel")); cancelButton.addActionListener(event -> dispose()); renameBtn = new JButton(NLS.str("common_dialog.ok")); renameBtn.addActionListener(event -> rename()); getRootPane().setDefaultButton(renameBtn); JPanel buttonPane = new JPanel(); buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); buttonPane.add(resetButton); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(Box.createHorizontalGlue()); buttonPane.add(renameBtn); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(cancelButton); return buttonPane; } private void initUI() { JLabel lbl = new JLabel(NLS.str("popup.rename")); NodeLabel nodeLabel = new NodeLabel(node.getTitle()); nodeLabel.setIcon(node.getIcon()); if (node instanceof JNode) { nodeLabel.disableHtml(((JNode) node).disableHtml()); } else if (node instanceof JRenamePackage) { // TODO: get from JRenameNode directly nodeLabel.disableHtml(!node.getTitle().equals(JPackage.PACKAGE_DEFAULT_HTML_STR)); } lbl.setLabelFor(nodeLabel); renameField = new JTextField(40); renameField.setFont(mainWindow.getSettings().getCodeFont()); renameField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> checkNewName(renameField.getText()))); renameField.addActionListener(e -> rename()); new TextStandardActions(renameField); JPanel renamePane = new JPanel(); renamePane.setLayout(new FlowLayout(FlowLayout.LEFT)); renamePane.add(lbl); renamePane.add(nodeLabel); renamePane.setBorder(BorderFactory.createEmptyBorder(10, 10, 0, 10)); JPanel textPane = new JPanel(); textPane.setLayout(new BoxLayout(textPane, BoxLayout.PAGE_AXIS)); textPane.add(renameField); if (node instanceof JClass) { textPane.add(new JLabel(NLS.str("rename_dialog.class_help"))); } textPane.setBorder(BorderFactory.createEmptyBorder(5, 10, 10, 10)); JPanel buttonPane = initButtonsPanel(); Container contentPane = getContentPane(); contentPane.add(renamePane, BorderLayout.PAGE_START); contentPane.add(textPane, BorderLayout.CENTER); contentPane.add(buttonPane, BorderLayout.PAGE_END); setTitle(NLS.str("popup.rename")); commonWindowInit(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Insets; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ItemListener; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JSpinner; import javax.swing.JTextField; import javax.swing.JToggleButton; import javax.swing.SpinnerNumberModel; import javax.swing.WindowConstants; import javax.swing.border.TitledBorder; import javax.swing.event.ChangeListener; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.icons.FlatSearchWithHistoryIcon; import io.reactivex.rxjava3.core.BackpressureStrategy; import io.reactivex.rxjava3.core.Emitter; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; import jadx.api.JavaClass; import jadx.api.JavaPackage; import jadx.api.resources.ResourceContentType; import jadx.core.dex.nodes.PackageNode; import jadx.core.utils.ListUtils; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.gui.jobs.ITaskInfo; import jadx.gui.jobs.ITaskProgress; import jadx.gui.search.SearchSettings; import jadx.gui.search.SearchTask; import jadx.gui.search.providers.ClassSearchProvider; import jadx.gui.search.providers.CodeSearchProvider; import jadx.gui.search.providers.CommentSearchProvider; import jadx.gui.search.providers.FieldSearchProvider; import jadx.gui.search.providers.MergedSearchProvider; import jadx.gui.search.providers.MethodSearchProvider; import jadx.gui.search.providers.ResourceFilter; import jadx.gui.search.providers.ResourceSearchProvider; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResource; import jadx.gui.ui.MainWindow; import jadx.gui.utils.CacheObject; import jadx.gui.utils.Icons; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import jadx.gui.utils.SimpleListener; import jadx.gui.utils.TextStandardActions; import jadx.gui.utils.UiUtils; import jadx.gui.utils.cache.ValueCache; import jadx.gui.utils.layout.WrapLayout; import jadx.gui.utils.rx.RxUtils; import jadx.gui.utils.ui.DocumentUpdateListener; import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.ACTIVE_TAB; import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.CLASS; import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.CODE; import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.COMMENT; import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.FIELD; import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.IGNORE_CASE; import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.METHOD; import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.RESOURCE; import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.USE_REGEX; public class SearchDialog extends CommonSearchDialog { private static final Logger LOG = LoggerFactory.getLogger(SearchDialog.class); private static final long serialVersionUID = -5105405456969134105L; public static void search(MainWindow window, SearchPreset preset) { SearchDialog searchDialog = new SearchDialog(window, preset, Collections.emptySet()); show(searchDialog, window); } public static void searchInActiveTab(MainWindow window, SearchPreset preset) { SearchDialog searchDialog = new SearchDialog(window, preset, EnumSet.of(ACTIVE_TAB)); show(searchDialog, window); } public static void searchText(MainWindow window, String text) { SearchDialog searchDialog = new SearchDialog(window, SearchPreset.TEXT, Collections.emptySet()); searchDialog.initSearchText = text; show(searchDialog, window); } public static void searchPackage(MainWindow window, String packageName) { SearchDialog searchDialog = new SearchDialog(window, SearchPreset.TEXT, Collections.emptySet()); searchDialog.initSearchPackage = packageName; show(searchDialog, window); } private static void show(SearchDialog searchDialog, MainWindow mw) { mw.addLoadListener(loaded -> { if (!loaded) { searchDialog.dispose(); return true; } return false; }); searchDialog.setVisible(true); } public enum SearchPreset { TEXT, CLASS, COMMENT } public enum SearchOptions { CLASS, METHOD, FIELD, CODE, RESOURCE, COMMENT, IGNORE_CASE, USE_REGEX, ACTIVE_TAB } private final transient SearchPreset searchPreset; private final transient Set options; private final transient SimpleListener> optionsListener = new SimpleListener<>(); private transient JTextField searchField; private transient JTextField packageField; private transient JTextField resExtField; private transient JSpinner resSizeLimit; private transient @Nullable SearchTask searchTask; private transient JButton loadAllButton; private transient JButton loadMoreButton; private transient JButton stopBtn; private transient JButton sortBtn; private transient Disposable searchDisposable; private transient SearchEventEmitter searchEmitter; private transient ChangeListener activeTabListener; private transient String initSearchText = null; private transient String initSearchPackage = null; // temporal list for pending results private final List pendingResults = new ArrayList<>(); /** * Use single thread to do all background work, so additional synchronization not needed */ private final Executor searchBackgroundExecutor = Executors.newSingleThreadExecutor(); // save values between searches private final ValueCache> includedClsCache = new ValueCache<>(); private final ValueCache, List>> batchesCache = new ValueCache<>(); private SearchDialog(MainWindow mainWindow, SearchPreset preset, Set additionalOptions) { super(mainWindow, NLS.str("menu.text_search")); this.searchPreset = preset; this.options = buildOptions(preset); this.options.addAll(additionalOptions); loadWindowPos(); initUI(); initSearchEvents(); registerInitOnOpen(); registerActiveTabListener(); } @Override public void dispose() { if (searchDisposable != null && !searchDisposable.isDisposed()) { searchDisposable.dispose(); } resultsModel.clear(); removeActiveTabListener(); searchBackgroundExecutor.execute(() -> { stopSearchTask(); unloadTempData(); }); super.dispose(); } private Set buildOptions(SearchPreset preset) { Set searchOptions = cache.getLastSearchOptions().get(preset); if (searchOptions == null) { searchOptions = EnumSet.noneOf(SearchOptions.class); } switch (preset) { case TEXT: if (searchOptions.isEmpty()) { searchOptions.add(SearchOptions.CODE); searchOptions.add(IGNORE_CASE); } break; case CLASS: searchOptions.add(CLASS); break; case COMMENT: searchOptions.add(COMMENT); searchOptions.remove(ACTIVE_TAB); break; } return searchOptions; } @Override protected void openInit() { String searchText = initSearchText != null ? initSearchText : cache.getLastSearch(); if (searchText != null) { searchField.setText(searchText); searchField.selectAll(); } String searchPackage = initSearchPackage != null ? initSearchPackage : cache.getLastSearchPackage(); if (searchPackage != null) { packageField.setText(searchPackage); } searchField.requestFocus(); resultsTable.initColumnWidth(); if (options.contains(COMMENT)) { // show all comments on empty input searchEmitter.emitSearch(); } } private void initUI() { searchField = new JTextField(); TextStandardActions.attach(searchField); addSearchHistoryButton(); searchField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true); boolean autoSearch = mainWindow.getSettings().isUseAutoSearch(); JButton searchBtn = new JButton(NLS.str("search_dialog.search_button")); searchBtn.setVisible(!autoSearch); searchBtn.addActionListener(ev -> searchEmitter.emitSearch()); JCheckBox autoSearchCB = new JCheckBox(NLS.str("search_dialog.auto_search")); autoSearchCB.setSelected(autoSearch); autoSearchCB.addActionListener(ev -> { boolean newValue = autoSearchCB.isSelected(); mainWindow.getSettings().saveUseAutoSearch(newValue); searchBtn.setVisible(!newValue); initSearchEvents(); if (newValue) { searchEmitter.emitSearch(); } }); JPanel searchButtons = new JPanel(); searchButtons.setLayout(new BoxLayout(searchButtons, BoxLayout.LINE_AXIS)); searchButtons.add(searchBtn); searchButtons.add(Box.createRigidArea(new Dimension(5, 0))); searchButtons.add(makeOptionsToggleButton(NLS.str("search_dialog.ignorecase"), Icons.ICON_MATCH, Icons.ICON_MATCH_SELECTED, SearchOptions.IGNORE_CASE)); searchButtons.add(Box.createRigidArea(new Dimension(5, 0))); searchButtons.add(makeOptionsToggleButton(NLS.str("search_dialog.regex"), Icons.ICON_REGEX, Icons.ICON_REGEX_SELECTED, SearchOptions.USE_REGEX)); searchButtons.add(Box.createRigidArea(new Dimension(5, 0))); searchButtons.add(makeOptionsToggleButton(NLS.str("search_dialog.active_tab"), Icons.ICON_ACTIVE_TAB, Icons.ICON_ACTIVE_TAB_SELECTED, SearchOptions.ACTIVE_TAB)); searchButtons.add(Box.createRigidArea(new Dimension(5, 0))); searchButtons.add(autoSearchCB); JPanel searchFieldPanel = new JPanel(); searchFieldPanel.setLayout(new BorderLayout(5, 5)); searchFieldPanel.add(new JLabel(NLS.str("search_dialog.open_by_name")), BorderLayout.LINE_START); searchFieldPanel.add(searchField, BorderLayout.CENTER); searchFieldPanel.add(searchButtons, BorderLayout.LINE_END); JPanel searchInPanel = new JPanel(); searchInPanel.setLayout(new BoxLayout(searchInPanel, BoxLayout.LINE_AXIS)); searchInPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.search_in"))); searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.class"), SearchOptions.CLASS)); searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.method"), SearchOptions.METHOD)); searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.field"), SearchOptions.FIELD)); searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.code"), SearchOptions.CODE)); searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.resource"), SearchOptions.RESOURCE)); searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.comments"), SearchOptions.COMMENT)); packageField = new JTextField(Math.min(100, getMaxPkgLen())); TextStandardActions.attach(packageField); packageField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true); packageField.setToolTipText(NLS.str("search_dialog.limit_package")); JPanel searchPackagePanel = new JPanel(new BorderLayout()); searchPackagePanel.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.limit_package"))); searchPackagePanel.add(packageField, BorderLayout.CENTER); Dimension minPanelSize = calcMinSizeForTitledBorder(searchPackagePanel); searchPackagePanel .setPreferredSize(new Dimension(Math.max(packageField.getPreferredSize().width, minPanelSize.width), minPanelSize.height)); resExtField = new JTextField(30); TextStandardActions.attach(resExtField); resExtField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true); resExtField.setToolTipText(NLS.str("preferences.res_file_ext")); String resFilterStr = mainWindow.getProject().getSearchResourcesFilter(); resExtField.setText(resFilterStr); ResourceFilter resFilter = ResourceFilter.parse(resFilterStr); JCheckBox textResBox = new JCheckBox(NLS.str("search_dialog.res_text")); textResBox.setSelected(resFilter.getContentTypes().contains(ResourceContentType.CONTENT_TEXT)); JCheckBox binResBox = new JCheckBox(NLS.str("search_dialog.res_binary")); binResBox.setSelected(resFilter.getContentTypes().contains(ResourceContentType.CONTENT_BINARY)); ItemListener resContentTypeListener = ev -> { try { Set contentTypes = EnumSet.noneOf(ResourceContentType.class); if (textResBox.isSelected()) { contentTypes.add(ResourceContentType.CONTENT_TEXT); } if (binResBox.isSelected()) { contentTypes.add(ResourceContentType.CONTENT_BINARY); } String newStr = ResourceFilter.withContentType(resExtField.getText(), contentTypes); if (!newStr.equals(resExtField.getText())) { resExtField.setText(newStr); } } catch (Exception e) { // ignore } }; textResBox.addItemListener(resContentTypeListener); binResBox.addItemListener(resContentTypeListener); resExtField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> UiUtils.uiRun(() -> { try { ResourceFilter filter = ResourceFilter.parse(resExtField.getText()); textResBox.setSelected(filter.getContentTypes().contains(ResourceContentType.CONTENT_TEXT)); binResBox.setSelected(filter.getContentTypes().contains(ResourceContentType.CONTENT_BINARY)); } catch (Exception e) { // ignore } }))); JPanel resExtFilePanel = new JPanel(); resExtFilePanel.setLayout(new BoxLayout(resExtFilePanel, BoxLayout.LINE_AXIS)); resExtFilePanel.setBorder(BorderFactory.createTitledBorder(NLS.str("preferences.res_file_ext"))); resExtFilePanel.add(resExtField); resExtFilePanel.add(textResBox); resExtFilePanel.add(binResBox); resExtFilePanel.setPreferredSize(calcMinSizeForTitledBorder(resExtFilePanel)); resSizeLimit = new JSpinner(new SpinnerNumberModel(mainWindow.getProject().getSearchResourcesSizeLimit(), 0, Integer.MAX_VALUE, 1)); resSizeLimit.setToolTipText(NLS.str("preferences.res_skip_file")); JPanel sizeLimitPanel = new JPanel(new BorderLayout()); sizeLimitPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("preferences.res_skip_file"))); sizeLimitPanel.add(resSizeLimit, BorderLayout.CENTER); sizeLimitPanel.setPreferredSize(calcMinSizeForTitledBorder(sizeLimitPanel)); JPanel optionsPanel = new JPanel(new WrapLayout(FlowLayout.LEFT)); optionsPanel.add(searchInPanel); optionsPanel.add(searchPackagePanel); optionsPanel.add(resExtFilePanel); optionsPanel.add(sizeLimitPanel); optionsListener.addListener(searchOptions -> { boolean codeSearch = Utils.isSetContainsAny(searchOptions, EnumSet.of(CODE, CLASS, METHOD, FIELD, COMMENT)); searchPackagePanel.setVisible(codeSearch); boolean resSearch = searchOptions.contains(RESOURCE); resExtFilePanel.setVisible(resSearch); sizeLimitPanel.setVisible(resSearch); optionsPanel.revalidate(); optionsPanel.repaint(); }); JPanel searchPane = new JPanel(); searchPane.setLayout(new BoxLayout(searchPane, BoxLayout.PAGE_AXIS)); searchPane.add(searchFieldPanel); searchPane.add(Box.createRigidArea(new Dimension(0, 5))); searchPane.add(optionsPanel); initCommon(); JPanel resultsPanel = initResultsTable(); JPanel buttonPane = initButtonsPanel(); JPanel contentPanel = new JPanel(); contentPanel.setLayout(new BorderLayout(5, 5)); contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); contentPanel.add(searchPane, BorderLayout.PAGE_START); contentPanel.add(resultsPanel, BorderLayout.CENTER); contentPanel.add(buttonPane, BorderLayout.PAGE_END); getContentPane().add(contentPanel); addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { optionsPanel.revalidate(); optionsPanel.repaint(); } }); setLocationRelativeTo(null); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); } private int getMaxPkgLen() { CacheObject cacheObject = mainWindow.getCacheObject(); int maxPkgLength = cacheObject.getMaxPkgLength(); if (maxPkgLength != 0) { return maxPkgLength; } int max = 1; for (PackageNode pkg : mainWindow.getWrapper().getRootNode().getPackages()) { int len = pkg.getPkgInfo().getFullName().length(); if (len > max) { max = len; } } cacheObject.setMaxPkgLength(max); return max; } /** * Workaround to calculate minimum size for a panel with titled border */ private Dimension calcMinSizeForTitledBorder(JPanel panel) { TitledBorder border = (TitledBorder) panel.getBorder(); Insets borderInsets = border.getBorderInsets(panel); int insets = 2 * (borderInsets.left + borderInsets.right); double titleWidth = panel.getFontMetrics(border.getTitleFont()).stringWidth(border.getTitle()); return new Dimension((int) titleWidth + insets, panel.getPreferredSize().height); } private void addSearchHistoryButton() { JButton searchHistoryButton = new JButton(new FlatSearchWithHistoryIcon(true)); searchHistoryButton.setToolTipText(NLS.str("search_dialog.search_history")); searchHistoryButton.addActionListener(e -> { JPopupMenu popupMenu = new JPopupMenu(); List searchHistory = mainWindow.getProject().getSearchHistory(); if (searchHistory.isEmpty()) { popupMenu.add("(empty)"); } else { for (String str : searchHistory) { JMenuItem item = popupMenu.add(str); item.addActionListener(ev -> searchField.setText(str)); } } popupMenu.show(searchHistoryButton, 0, searchHistoryButton.getHeight()); }); searchField.putClientProperty(FlatClientProperties.TEXT_FIELD_LEADING_COMPONENT, searchHistoryButton); } protected void addResultsActions(JPanel resultsActionsPanel) { loadAllButton = new JButton(NLS.str("search_dialog.load_all")); loadAllButton.addActionListener(e -> loadMoreResults(true)); loadAllButton.setEnabled(false); loadMoreButton = new JButton(NLS.str("search_dialog.load_more")); loadMoreButton.addActionListener(e -> loadMoreResults(false)); loadMoreButton.setEnabled(false); stopBtn = new JButton(NLS.str("search_dialog.stop")); stopBtn.addActionListener(e -> pauseSearch()); stopBtn.setEnabled(false); sortBtn = new JButton(NLS.str("search_dialog.sort_results")); sortBtn.addActionListener(e -> { synchronized (pendingResults) { resultsModel.sort(); resultsTable.updateTable(); } }); sortBtn.setEnabled(false); resultsActionsPanel.add(loadAllButton); resultsActionsPanel.add(Box.createRigidArea(new Dimension(10, 0))); resultsActionsPanel.add(loadMoreButton); resultsActionsPanel.add(Box.createRigidArea(new Dimension(10, 0))); resultsActionsPanel.add(stopBtn); resultsActionsPanel.add(Box.createRigidArea(new Dimension(10, 0))); resultsActionsPanel.add(stopBtn); super.addResultsActions(resultsActionsPanel); resultsActionsPanel.add(Box.createRigidArea(new Dimension(10, 0))); resultsActionsPanel.add(sortBtn); } private class SearchEventEmitter { private final Flowable flowable; private Emitter emitter; public SearchEventEmitter() { flowable = Flowable.create(this::saveEmitter, BackpressureStrategy.LATEST); } public Flowable getFlowable() { return flowable; } private void saveEmitter(Emitter emitter) { this.emitter = emitter; } public synchronized void emitSearch() { this.emitter.onNext(searchField.getText()); } } private void initSearchEvents() { if (searchDisposable != null) { searchDisposable.dispose(); searchDisposable = null; } searchEmitter = new SearchEventEmitter(); Flowable searchEvents; if (mainWindow.getSettings().isUseAutoSearch()) { searchEvents = Flowable.merge(List.of( RxUtils.textFieldChanges(searchField), RxUtils.textFieldEnterPress(searchField), RxUtils.textFieldChanges(packageField), RxUtils.textFieldEnterPress(packageField), RxUtils.textFieldChanges(resExtField), RxUtils.textFieldEnterPress(resExtField), RxUtils.spinnerChanges(resSizeLimit), RxUtils.spinnerEnterPress(resSizeLimit), searchEmitter.getFlowable())); } else { searchEvents = Flowable.merge(List.of( RxUtils.textFieldEnterPress(searchField), RxUtils.textFieldEnterPress(packageField), RxUtils.textFieldEnterPress(resExtField), RxUtils.spinnerEnterPress(resSizeLimit), searchEmitter.getFlowable())); } searchDisposable = searchEvents .debounce(100, TimeUnit.MILLISECONDS) .observeOn(Schedulers.from(searchBackgroundExecutor)) .subscribe(t -> this.search(searchField.getText())); // set initial values optionsListener.sendUpdate(options); } private void search(String text) { UiUtils.notUiThreadGuard(); stopSearchTask(); UiUtils.uiRun(this::resetSearch); searchTask = prepareSearch(text); if (searchTask == null) { return; } UiUtils.uiRunAndWait(() -> { updateTableHighlight(); prepareForSearch(); }); this.searchTask.setResultsLimit(mainWindow.getSettings().getSearchResultsPerPage()); this.searchTask.setProgressListener(this::updateProgress); this.searchTask.fetchResults(); LOG.debug("Total search items count estimation: {}", this.searchTask.getTaskProgress().total()); } private SearchTask prepareSearch(String text) { if (text == null || options.isEmpty()) { return null; } // allow empty text for search in comments if (text.isEmpty() && !options.contains(COMMENT)) { return null; } LOG.debug("Building search for '{}', options: {}", text, options); SearchSettings searchSettings = new SearchSettings(text); searchSettings.setIgnoreCase(options.contains(IGNORE_CASE)); searchSettings.setUseRegex(options.contains(USE_REGEX)); searchSettings.setSearchPkgStr(packageField.getText().trim()); searchSettings.setResFilterStr(resExtField.getText().trim()); searchSettings.setResSizeLimit((Integer) resSizeLimit.getValue()); String error = searchSettings.prepare(mainWindow); UiUtils.highlightAsErrorField(searchField, !StringUtils.isEmpty(error)); if (!StringUtils.isEmpty(error)) { resultsInfoLabel.setText(error); return null; } SearchTask newSearchTask = new SearchTask(mainWindow, this::addSearchResult, this::searchFinished); if (!buildSearch(newSearchTask, text, searchSettings)) { UiUtils.highlightAsErrorField(searchField, true); return null; } // save search settings mainWindow.getProject().setSearchResourcesFilter(resExtField.getText().trim()); mainWindow.getProject().setSearchResourcesSizeLimit(searchSettings.getResSizeLimit()); return newSearchTask; } private boolean buildSearch(SearchTask newSearchTask, String text, SearchSettings searchSettings) { List searchClasses; if (options.contains(ACTIVE_TAB)) { JumpPosition currentPos = mainWindow.getTabbedPane().getCurrentPosition(); if (currentPos == null) { resultsInfoLabel.setText("Can't search in current tab"); return false; } JNode currentNode = currentPos.getNode(); if (currentNode instanceof JClass) { JClass activeCls = currentNode.getRootClass(); searchSettings.setActiveCls(activeCls); searchClasses = Collections.singletonList(activeCls.getCls()); } else if (currentNode instanceof JResource) { searchSettings.setActiveResource((JResource) currentNode); searchClasses = Collections.emptyList(); } else { resultsInfoLabel.setText("Can't search in current tab"); return false; } } else { searchClasses = includedClsCache.get(mainWindow.getSettings().getExcludedPackages(), exc -> mainWindow.getWrapper().getIncludedClassesWithInners()); } JavaPackage searchPkg = searchSettings.getSearchPackage(); if (searchPkg != null) { searchClasses = searchClasses.stream() .filter(searchSettings::isInSearchPkg) .collect(Collectors.toList()); } if (text.isEmpty() && options.contains(COMMENT)) { // allow empty text for comment search newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings, searchClasses)); return true; } if (!searchClasses.isEmpty()) { // using ordered execution for fast tasks MergedSearchProvider merged = new MergedSearchProvider(); if (options.contains(CLASS)) { merged.add(new ClassSearchProvider(mainWindow, searchSettings, searchClasses)); } if (options.contains(METHOD)) { merged.add(new MethodSearchProvider(mainWindow, searchSettings, searchClasses)); } if (options.contains(FIELD)) { merged.add(new FieldSearchProvider(mainWindow, searchSettings, searchClasses)); } if (!merged.isEmpty()) { merged.prepare(); newSearchTask.addProviderJob(merged); } if (options.contains(CODE)) { int clsCount = searchClasses.size(); if (clsCount == 1) { newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, searchClasses, null)); } else if (clsCount > 1) { List topClasses = ListUtils.filter(searchClasses, c -> !c.isInner()); List> batches = batchesCache.get(topClasses, clsList -> mainWindow.getWrapper().buildDecompileBatches(clsList)); Set includedClasses = new HashSet<>(topClasses); for (List batch : batches) { newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, batch, includedClasses)); } } } if (options.contains(COMMENT)) { newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings, searchClasses)); } } if (options.contains(RESOURCE)) { newSearchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings, this)); } return true; } @Override protected void openItem(JNode node) { if (mainWindow.getSettings().isUseAutoSearch()) { // for auto search save only searches which leads to node opening mainWindow.getProject().addToSearchHistory(searchField.getText()); } super.openItem(node); } private void pauseSearch() { stopBtn.setEnabled(false); searchBackgroundExecutor.execute(() -> { if (searchTask != null) { searchTask.cancel(); } }); } private void stopSearchTask() { UiUtils.notUiThreadGuard(); if (searchTask != null) { searchTask.cancel(); searchTask.waitTask(); searchTask = null; } } private void loadMoreResults(boolean all) { searchBackgroundExecutor.execute(() -> { if (searchTask == null) { return; } searchTask.cancel(); searchTask.waitTask(); UiUtils.uiRunAndWait(this::prepareForSearch); if (all) { searchTask.setResultsLimit(0); } searchTask.fetchResults(); }); } private void resetSearch() { UiUtils.uiThreadGuard(); resultsModel.clear(); resultsTable.updateTable(); synchronized (pendingResults) { pendingResults.clear(); } updateProgressLabel(""); progressPane.setVisible(false); warnLabel.setVisible(false); loadAllButton.setEnabled(false); loadMoreButton.setEnabled(false); } private void prepareForSearch() { UiUtils.uiThreadGuard(); stopBtn.setEnabled(true); sortBtn.setEnabled(false); showSearchState(); progressStartCommon(); } private void addSearchResult(JNode node) { Objects.requireNonNull(node); synchronized (pendingResults) { UiUtils.notUiThreadGuard(); pendingResults.add(node); } } private void updateTable() { synchronized (pendingResults) { UiUtils.uiThreadGuard(); Collections.sort(pendingResults); resultsModel.addAll(pendingResults); pendingResults.clear(); resultsTable.updateTable(); } } private void updateTableHighlight() { String text = searchField.getText(); updateHighlightContext(text, !options.contains(IGNORE_CASE), options.contains(USE_REGEX), false); cache.setLastSearch(text); cache.setLastSearchPackage(packageField.getText()); cache.getLastSearchOptions().put(searchPreset, options); if (!mainWindow.getSettings().isUseAutoSearch()) { mainWindow.getProject().addToSearchHistory(text); } } private void updateProgress(ITaskProgress progress) { UiUtils.uiRun(() -> { progressPane.setProgress(progress); updateTable(); }); } public void updateProgressLabel(String text) { UiUtils.uiRun(() -> progressInfoLabel.setText(text)); } private void searchFinished(ITaskInfo status, Boolean complete) { UiUtils.uiThreadGuard(); LOG.debug("Search complete: {}, complete: {}", status, complete); loadAllButton.setEnabled(!complete); loadMoreButton.setEnabled(!complete); stopBtn.setEnabled(false); progressFinishedCommon(); updateTable(); updateProgressLabel(complete); sortBtn.setEnabled(resultsModel.getRowCount() != 0); } private void unloadTempData() { mainWindow.getWrapper().unloadClasses(); System.gc(); } private JCheckBox makeOptionsCheckBox(String name, final SearchOptions opt) { final JCheckBox chBox = new JCheckBox(name); chBox.setSelected(options.contains(opt)); chBox.addItemListener(e -> { if (chBox.isSelected()) { options.add(opt); } else { options.remove(opt); } optionsListener.sendUpdate(options); searchEmitter.emitSearch(); }); return chBox; } private JToggleButton makeOptionsToggleButton(String name, ImageIcon icon, ImageIcon selectedIcon, final SearchOptions opt) { final JToggleButton toggleButton = new JToggleButton(); toggleButton.setToolTipText(name); toggleButton.setIcon(icon); toggleButton.setSelectedIcon(selectedIcon); toggleButton.setSelected(options.contains(opt)); toggleButton.addItemListener(e -> { if (toggleButton.isSelected()) { options.add(opt); } else { options.remove(opt); } optionsListener.sendUpdate(options); searchEmitter.emitSearch(); }); return toggleButton; } @Override protected void loadFinished() { resultsTable.setEnabled(true); searchField.setEnabled(true); searchEmitter.emitSearch(); } @Override protected void loadStart() { resultsTable.setEnabled(false); searchField.setEnabled(false); } private void registerActiveTabListener() { removeActiveTabListener(); activeTabListener = e -> { if (options.contains(ACTIVE_TAB)) { LOG.debug("active tab change event received"); searchEmitter.emitSearch(); } }; mainWindow.getTabbedPane().addChangeListener(activeTabListener); } private void removeActiveTabListener() { if (activeTabListener != null) { mainWindow.getTabbedPane().removeChangeListener(activeTabListener); activeTabListener = null; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/SetValueDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Label; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Map.Entry; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.WindowConstants; import jadx.core.dex.instructions.args.ArgType; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.JDebuggerPanel.ValueTreeNode; import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; import jadx.gui.utils.UiUtils; public class SetValueDialog extends JDialog { private static final long serialVersionUID = -1111111202103121002L; private final transient MainWindow mainWindow; private final transient ValueTreeNode valNode; public SetValueDialog(MainWindow mainWindow, ValueTreeNode valNode) { super(mainWindow); this.mainWindow = mainWindow; this.valNode = valNode; initUI(); UiUtils.addEscapeShortCutToDispose(this); setTitle(valNode.toString()); } private void initUI() { JTextField valField = new JTextField(); TextStandardActions.attach(valField); JPanel valPane = new JPanel(new BorderLayout(5, 5)); valPane.add(new JLabel(NLS.str("set_value_dialog.label_value")), BorderLayout.WEST); valPane.add(valField, BorderLayout.CENTER); JPanel btnPane = new JPanel(); btnPane.setLayout(new BoxLayout(btnPane, BoxLayout.LINE_AXIS)); JButton setValueBtn = new JButton(NLS.str("set_value_dialog.btn_set")); btnPane.add(new Label()); btnPane.add(setValueBtn); UiUtils.addKeyBinding(valField, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "set value", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { setValueBtn.doClick(); } }); JPanel typePane = new JPanel(); typePane.setLayout(new BoxLayout(typePane, BoxLayout.LINE_AXIS)); java.util.List rbs = new ArrayList<>(6); rbs.add(new JRadioButton("int")); rbs.add(new JRadioButton("String")); rbs.add(new JRadioButton("long")); rbs.add(new JRadioButton("float")); rbs.add(new JRadioButton("double")); rbs.add(new JRadioButton("Object id")); rbs.get(0).setSelected(true); // select int radio ButtonGroup rbGroup = new ButtonGroup(); rbs.forEach(rbGroup::add); rbs.forEach(typePane::add); JPanel mainPane = new JPanel(new BorderLayout(5, 5)); mainPane.add(typePane, BorderLayout.NORTH); mainPane.add(valPane, BorderLayout.CENTER); mainPane.add(btnPane, BorderLayout.SOUTH); mainPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); getContentPane().add(mainPane); this.setTitle(NLS.str("set_value_dialog.title")); pack(); setSize(480, 160); setLocationRelativeTo(null); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setModalityType(ModalityType.MODELESS); UiUtils.addEscapeShortCutToDispose(this); setValueBtn.addActionListener(new AbstractAction() { private static final long serialVersionUID = -1111111202103260220L; @Override public void actionPerformed(ActionEvent e) { boolean ok; try { Entry type = getType(); if (type != null) { ok = mainWindow .getDebuggerPanel() .getDbgController() .modifyRegValue(valNode, type.getKey(), type.getValue()); } else { UiUtils.showMessageBox(mainWindow, NLS.str("set_value_dialog.sel_type")); return; } } catch (JadxRuntimeException except) { UiUtils.showMessageBox(mainWindow, except.getMessage()); return; } if (ok) { dispose(); } else { UiUtils.showMessageBox(mainWindow, NLS.str("set_value_dialog.neg_msg")); } } private Entry getType() { String val = valField.getText(); for (JRadioButton rb : rbs) { if (rb.isSelected()) { switch (rb.getText()) { case "int": return new SimpleEntry<>(ArgType.INT, Integer.valueOf(val)); case "String": return new SimpleEntry<>(ArgType.STRING, val); case "long": return new SimpleEntry<>(ArgType.LONG, Long.valueOf(val)); case "float": return new SimpleEntry<>(ArgType.FLOAT, Float.valueOf(val)); case "double": return new SimpleEntry<>(ArgType.DOUBLE, Double.valueOf(val)); case "Object id": return new SimpleEntry<>(ArgType.OBJECT, Long.valueOf(val)); default: throw new JadxRuntimeException("Unexpected type: " + rb.getText()); } } } return null; } }); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.Font; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.WindowConstants; import jadx.api.ICodeInfo; import jadx.api.JavaClass; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.api.utils.CodeUtils; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.visitors.prepare.CollectConstValues; import jadx.gui.JadxWrapper; import jadx.gui.jobs.TaskStatus; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.CodeNode; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JField; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.NodeLabel; public class UsageDialog extends CommonSearchDialog { private static final long serialVersionUID = -5105405789969134105L; private final transient JNode node; private transient List usageList; public static void open(MainWindow mainWindow, JNode node) { UsageDialog usageDialog = new UsageDialog(mainWindow, node); mainWindow.addLoadListener(loaded -> { if (!loaded) { usageDialog.dispose(); return true; } return false; }); usageDialog.setVisible(true); } private UsageDialog(MainWindow mainWindow, JNode node) { super(mainWindow, NLS.str("usage_dialog.title")); this.node = node; initUI(); registerInitOnOpen(); loadWindowPos(); } @Override protected void openInit() { progressStartCommon(); prepareUsageData(); mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"), this::collectUsageData, (status) -> { if (status == TaskStatus.CANCEL_BY_MEMORY) { mainWindow.showHeapUsageBar(); UiUtils.errorMessage(this, NLS.str("message.memoryLow")); } progressFinishedCommon(); loadFinished(); }); } private void prepareUsageData() { if (mainWindow.getSettings().isReplaceConsts() && node instanceof JField) { FieldNode fld = ((JField) node).getJavaField().getFieldNode(); boolean constField = CollectConstValues.getFieldConstValue(fld) != null; if (constField && !fld.getAccessFlags().isPrivate()) { // run full decompilation to prepare for full code scan mainWindow.requestFullDecompilation(); } } } private void collectUsageData() { usageList = new ArrayList<>(); buildUsageQuery().forEach( (searchNode, useNodes) -> useNodes.stream() .map(JavaNode::getTopParentClass) .distinct() .forEach(u -> processUsage(searchNode, u))); } /** * Return mapping of 'node to search' to 'use places' */ private Map> buildUsageQuery() { Map> map = new HashMap<>(); if (node instanceof JMethod) { JavaMethod javaMethod = ((JMethod) node).getJavaMethod(); for (JavaMethod mth : getMethodWithOverrides(javaMethod)) { map.put(mth, mth.getUseIn()); } return map; } if (node instanceof JClass) { JavaClass javaCls = ((JClass) node).getCls(); map.put(javaCls, javaCls.getUseIn()); // add constructors usage into class usage for (JavaMethod javaMth : javaCls.getMethods()) { if (javaMth.isConstructor()) { map.put(javaMth, javaMth.getUseIn()); } } return map; } if (node instanceof JField && mainWindow.getSettings().isReplaceConsts()) { FieldNode fld = ((JField) node).getJavaField().getFieldNode(); boolean constField = CollectConstValues.getFieldConstValue(fld) != null; if (constField && !fld.getAccessFlags().isPrivate()) { // search all classes to collect usage of replaced constants map.put(fld.getJavaNode(), mainWindow.getWrapper().getIncludedClasses()); return map; } } JavaNode javaNode = node.getJavaNode(); map.put(javaNode, javaNode.getUseIn()); return map; } private List getMethodWithOverrides(JavaMethod javaMethod) { List relatedMethods = javaMethod.getOverrideRelatedMethods(); if (!relatedMethods.isEmpty()) { return relatedMethods; } return Collections.singletonList(javaMethod); } private void processUsage(JavaNode searchNode, JavaClass topUseClass) { ICodeInfo codeInfo = topUseClass.getCodeInfo(); List usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode); if (usePositions.isEmpty()) { return; } String code = codeInfo.getCodeStr(); JadxWrapper wrapper = mainWindow.getWrapper(); for (int pos : usePositions) { String line = CodeUtils.getLineForPos(code, pos); if (line.startsWith("import ")) { continue; } JNodeCache nodeCache = getNodeCache(); JavaNode enclosingNode = wrapper.getEnclosingNode(codeInfo, pos); JClass rootJCls = nodeCache.makeFrom(topUseClass); JNode usageJNode = enclosingNode == null ? rootJCls : nodeCache.makeFrom(enclosingNode); usageList.add(new CodeNode(rootJCls, usageJNode, line.trim(), pos)); } } @Override protected void loadFinished() { resultsTable.setEnabled(true); resultsModel.clear(); Collections.sort(usageList); resultsModel.addAll(usageList); updateHighlightContext(node.getName(), true, false, true); resultsTable.initColumnWidth(); resultsTable.updateTable(); updateProgressLabel(true); } @Override protected void loadStart() { resultsTable.setEnabled(false); } private void initUI() { JadxSettings settings = mainWindow.getSettings(); Font codeFont = settings.getCodeFont(); JLabel lbl = new JLabel(NLS.str("usage_dialog.label")); lbl.setFont(codeFont); JLabel nodeLabel = NodeLabel.longName(node); nodeLabel.setFont(codeFont); lbl.setLabelFor(nodeLabel); JPanel searchPane = new JPanel(); searchPane.setLayout(new FlowLayout(FlowLayout.LEFT)); searchPane.add(lbl); searchPane.add(nodeLabel); searchPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); initCommon(); JPanel resultsPanel = initResultsTable(); JPanel buttonPane = initButtonsPanel(); JPanel contentPanel = new JPanel(); contentPanel.setLayout(new BorderLayout(5, 5)); contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); contentPanel.add(searchPane, BorderLayout.PAGE_START); contentPanel.add(resultsPanel, BorderLayout.CENTER); contentPanel.add(buttonPane, BorderLayout.PAGE_END); getContentPane().add(contentPanel); pack(); setSize(800, 500); setLocationRelativeTo(null); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialogPlus.java ================================================ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeExpansionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.JavaClass; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.api.utils.CodeUtils; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.visitors.prepare.CollectConstValues; import jadx.gui.JadxWrapper; import jadx.gui.jobs.TaskStatus; import jadx.gui.treemodel.CodeNode; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JField; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.cellrenders.PathHighlightTreeCellRenderer; import jadx.gui.ui.panel.ProgressPanel; import jadx.gui.ui.panel.SimpleCodePanel; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class UsageDialogPlus extends CommonSearchDialog { private static final long serialVersionUID = -5105405789969134107L; private final JPanel mainPanel; private final JSplitPane splitPane; private final SimpleCodePanel simpleCodePanel; private final JTree usageTree; private final DefaultTreeModel treeModel; private final DefaultMutableTreeNode rootNode; private final ProgressPanel localProgressPanel; private final JLabel resultsInfoLabel; private final JLabel progressInfoLabel; private final JNode initialNode; private static final Logger LOG = LoggerFactory.getLogger(UsageDialogPlus.class); public static void open(MainWindow mainWindow, JNode node) { UsageDialogPlus usageDialog = new UsageDialogPlus(mainWindow, node); mainWindow.addLoadListener(loaded -> { if (!loaded) { usageDialog.dispose(); return true; } return false; }); usageDialog.setVisible(true); // Set the position of the split panel after the window is displayed. SwingUtilities.invokeLater(() -> { int width = usageDialog.splitPane.getWidth(); if (width > 0) { usageDialog.splitPane.setDividerLocation((int) (width * 0.3)); } }); } private UsageDialogPlus(MainWindow mainWindow, JNode node) { super(mainWindow, NLS.str("usage_dialog_plus.title")); this.initialNode = node; // Initialize the progress panel and warning label progressPane = new ProgressPanel(mainWindow, false); warnLabel = new JLabel(); warnLabel.setForeground(Color.RED); warnLabel.setVisible(false); // Initialize result and progress info labels resultsInfoLabel = new JLabel(); progressInfoLabel = new JLabel(); localProgressPanel = new ProgressPanel(mainWindow, false); mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout()); // Create the code panel simpleCodePanel = new SimpleCodePanel(mainWindow); // Create the tree rootNode = new DefaultMutableTreeNode(node); treeModel = new DefaultTreeModel(rootNode); usageTree = new JTree(treeModel); usageTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); usageTree.setRootVisible(true); usageTree.setShowsRootHandles(true); usageTree.putClientProperty("JTree.lineStyle", "Horizontal"); usageTree.setRowHeight(22); usageTree.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); usageTree.setFont(mainWindow.getSettings().getCodeFont()); // Use a custom renderer instead of a custom UI usageTree.setCellRenderer(new PathHighlightTreeCellRenderer()); // Add tree listeners usageTree.addTreeSelectionListener(e -> { DefaultMutableTreeNode node1 = (DefaultMutableTreeNode) usageTree.getLastSelectedPathComponent(); if (node1 == null) { return; } Object nodeInfo = node1.getUserObject(); if (nodeInfo instanceof CodeNode) { CodeNode codeNode = (CodeNode) nodeInfo; simpleCodePanel.showCode(codeNode, codeNode.makeDescString()); } else if (nodeInfo instanceof JNode) { JNode jNode = (JNode) nodeInfo; simpleCodePanel.showCode(jNode, jNode.makeDescString()); } // Update the result information to display the number of child nodes of the currently selected node updateResultsInfo(node1); }); usageTree.addTreeExpansionListener(new TreeExpansionListener() { @Override public void treeExpanded(TreeExpansionEvent event) { TreePath path = event.getPath(); DefaultMutableTreeNode expandedNode = (DefaultMutableTreeNode) path.getLastPathComponent(); // Load only when the node has no child nodes. if (expandedNode.getChildCount() == 0) { Object userObject = expandedNode.getUserObject(); if (userObject instanceof JNode) { JNode nodeToUse = (JNode) userObject; // If it is a CodeNode, first convert it to an actual JNode and then search for its usage. if (nodeToUse.getClass() == CodeNode.class) { nodeToUse = getNodeFromCodeNode((CodeNode) nodeToUse); } if (nodeToUse != null) { loadNodeUsages(nodeToUse, expandedNode); } } } } @Override public void treeCollapsed(TreeExpansionEvent event) { // No need to process } }); usageTree.addMouseListener(new MouseAdapter() { private long lastClickTime = 0; private TreePath lastClickPath = null; @Override public void mouseClicked(MouseEvent e) { TreePath path = usageTree.getPathForLocation(e.getX(), e.getY()); if (path == null) { return; } // Set the selected path usageTree.setSelectionPath(path); // Handle the right-click menu if (SwingUtilities.isRightMouseButton(e)) { DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) path.getLastPathComponent(); Object userObject = selectedNode.getUserObject(); if (userObject instanceof JNode || userObject instanceof CodeNode) { JNode nodeForMenu = (userObject instanceof JNode) ? (JNode) userObject : getNodeFromCodeNode((CodeNode) userObject); showPopupMenu(e, nodeForMenu, path); } return; } // Handle left-click single/double click if (SwingUtilities.isLeftMouseButton(e)) { long clickTime = System.currentTimeMillis(); // Double-click interval long doubleClickInterval = 300; DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) path.getLastPathComponent(); // If the time interval between two clicks is less than the threshold and the same node is clicked, // it is considered a double-click if ((clickTime - lastClickTime) < doubleClickInterval && path.equals(lastClickPath)) { // Double-click processing - switch between expanded/collapsed states if (usageTree.isExpanded(path)) { usageTree.collapsePath(path); } else { usageTree.expandPath(path); // If the node has no child nodes and is a JNode, load its usage if (selectedNode.getChildCount() == 0 && selectedNode.getUserObject() instanceof JNode) { JNode nodeToUse = (JNode) selectedNode.getUserObject(); // If it is a CodeNode, first convert it to an actual JNode and then search for its usage if (nodeToUse.getClass() == CodeNode.class) { nodeToUse = getNodeFromCodeNode((CodeNode) nodeToUse); } if (nodeToUse != null) { loadNodeUsages(nodeToUse, selectedNode); } } } // Update the result information to display the number of child nodes of the currently selected node updateResultsInfo(selectedNode); } else { // Single-click processing - if the node is not expanded, expand it if (!usageTree.isExpanded(path)) { usageTree.expandPath(path); // If the node has no child nodes and is a JNode, then load its usage. if (selectedNode.getChildCount() == 0 && selectedNode.getUserObject() instanceof JNode) { JNode nodeToUse = (JNode) selectedNode.getUserObject(); // If it is a CodeNode, first convert it to an actual JNode and then search for its usage if (nodeToUse.getClass() == CodeNode.class) { nodeToUse = getNodeFromCodeNode((CodeNode) nodeToUse); } if (nodeToUse != null) { loadNodeUsages(nodeToUse, selectedNode); } } } // Update the result information to display the number of child nodes of the currently selected node updateResultsInfo(selectedNode); } lastClickTime = clickTime; lastClickPath = path; } } }); // Create the split panel splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); splitPane.setOneTouchExpandable(true); splitPane.setContinuousLayout(true); splitPane.setResizeWeight(0.3); // Left 30% splitPane.setDividerSize(10); // Increase the width of the divider for easier dragging // Add tree to the scroll pane JScrollPane treeScrollPane = new JScrollPane(usageTree); treeScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); treeScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); // Create status panel JPanel statusPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); statusPanel.add(resultsInfoLabel); statusPanel.add(Box.createRigidArea(new Dimension(10, 0))); statusPanel.add(progressInfoLabel); statusPanel.add(Box.createRigidArea(new Dimension(10, 0))); statusPanel.add(localProgressPanel); // Add components to the main panel JPanel leftPanel = new JPanel(new BorderLayout()); leftPanel.add(treeScrollPane, BorderLayout.CENTER); leftPanel.add(statusPanel, BorderLayout.SOUTH); splitPane.setLeftComponent(leftPanel); splitPane.setRightComponent(simpleCodePanel); mainPanel.add(splitPane, BorderLayout.CENTER); initUI(); registerInitOnOpen(); loadWindowPos(); } private void initUI() { initCommon(); JPanel buttonPane = initButtonsPanel(); JPanel contentPanel = new JPanel(); contentPanel.setLayout(new BorderLayout(5, 5)); contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); contentPanel.add(mainPanel, BorderLayout.CENTER); contentPanel.add(buttonPane, BorderLayout.PAGE_END); getContentPane().add(contentPanel); pack(); setSize(1300, 700); setLocationRelativeTo(null); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); // Add a window size change listener to ensure the divider position adapts to window size changes addComponentListener(new java.awt.event.ComponentAdapter() { @Override public void componentResized(java.awt.event.ComponentEvent e) { // Keep the left-right ratio int width = splitPane.getWidth(); if (width > 0) { int currentDividerLocation = splitPane.getDividerLocation(); double ratio = (double) currentDividerLocation / width; // If the ratio is too small or too large, reset to a reasonable value if (ratio < 0.2 || ratio > 0.5) { splitPane.setDividerLocation((int) (width * 0.3)); // Use pixel values } } } }); } @Override protected void openInit() { prepareUsageData(initialNode); localProgressPanel.setIndeterminate(true); localProgressPanel.setVisible(true); progressInfoLabel.setText(NLS.str("search_dialog.tip_searching")); // Load the usage of the root node loadNodeUsages(initialNode, rootNode); } private void loadNodeUsages(JNode node, DefaultMutableTreeNode treeNode) { // When loading starts, display the searching state localProgressPanel.setIndeterminate(true); localProgressPanel.setVisible(true); progressInfoLabel.setText(NLS.str("search_dialog.tip_searching")); mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"), () -> collectUsageData(node, treeNode), (status) -> { if (status == TaskStatus.CANCEL_BY_MEMORY) { mainWindow.showHeapUsageBar(); UiUtils.errorMessage(UsageDialogPlus.this, NLS.str("message.memoryLow")); } localProgressPanel.setVisible(false); progressInfoLabel.setText(NLS.str("usage_dialog_plus.search_complete")); // Update the result information - always display the number of child nodes of the currently // selected node updateResultsInfo(treeNode); // Expand the root node if (treeNode == rootNode) { usageTree.expandPath(new TreePath(rootNode.getPath())); } }); } private void updateResultsInfo(DefaultMutableTreeNode node) { if (node != null) { int childCount = node.getChildCount(); resultsInfoLabel.setText(NLS.str("search_dialog.results_complete", childCount)); } } private int getTotalChildCount(DefaultMutableTreeNode node) { int count = node.getChildCount(); for (Enumeration e = node.children(); e.hasMoreElements();) { DefaultMutableTreeNode child = (DefaultMutableTreeNode) e.nextElement(); count += getTotalChildCount(child); } return count; } private void prepareUsageData(JNode node) { if (mainWindow.getSettings().isReplaceConsts() && node instanceof JField) { FieldNode fld = ((JField) node).getJavaField().getFieldNode(); boolean constField = CollectConstValues.getFieldConstValue(fld) != null; if (constField && !fld.getAccessFlags().isPrivate()) { // run full decompilation to prepare for full code scan mainWindow.requestFullDecompilation(); } } } private void collectUsageData(JNode node, DefaultMutableTreeNode treeNode) { List usageList = new ArrayList<>(); buildUsageQuery(node).forEach( (searchNode, useNodes) -> useNodes.stream() .map(JavaNode::getTopParentClass) .distinct() .forEach(u -> processUsage(searchNode, u, usageList))); // Sort and add to the tree node Collections.sort(usageList); SwingUtilities.invokeLater(() -> { for (CodeNode codeNode : usageList) { DefaultMutableTreeNode usageTreeNode = new DefaultMutableTreeNode(codeNode); treeModel.insertNodeInto(usageTreeNode, treeNode, treeNode.getChildCount()); } treeModel.nodeStructureChanged(treeNode); }); } private Map> buildUsageQuery(JNode node) { Map> map = new HashMap<>(); if (node instanceof JMethod) { JavaMethod javaMethod = ((JMethod) node).getJavaMethod(); for (JavaMethod mth : getMethodWithOverrides(javaMethod)) { map.put(mth, mth.getUseIn()); } return map; } if (node instanceof JClass) { JavaClass javaCls = ((JClass) node).getCls(); map.put(javaCls, javaCls.getUseIn()); // add constructors usage into class usage for (JavaMethod javaMth : javaCls.getMethods()) { if (javaMth.isConstructor()) { map.put(javaMth, javaMth.getUseIn()); } } return map; } if (node instanceof JField && mainWindow.getSettings().isReplaceConsts()) { FieldNode fld = ((JField) node).getJavaField().getFieldNode(); boolean constField = CollectConstValues.getFieldConstValue(fld) != null; if (constField && !fld.getAccessFlags().isPrivate()) { // search all classes to collect usage of replaced constants map.put(fld.getJavaNode(), mainWindow.getWrapper().getIncludedClasses()); return map; } } JavaNode javaNode = node.getJavaNode(); map.put(javaNode, javaNode.getUseIn()); return map; } private List getMethodWithOverrides(JavaMethod javaMethod) { List relatedMethods = javaMethod.getOverrideRelatedMethods(); if (!relatedMethods.isEmpty()) { return relatedMethods; } return Collections.singletonList(javaMethod); } private void processUsage(JavaNode searchNode, JavaClass topUseClass, List usageList) { ICodeInfo codeInfo = topUseClass.getCodeInfo(); List usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode); if (usePositions.isEmpty()) { return; } String code = codeInfo.getCodeStr(); JadxWrapper wrapper = mainWindow.getWrapper(); for (int pos : usePositions) { String line = CodeUtils.getLineForPos(code, pos); if (line.startsWith("import ")) { continue; } JNodeCache nodeCache = getNodeCache(); JavaNode enclosingNode = wrapper.getEnclosingNode(codeInfo, pos); JClass rootJCls = nodeCache.makeFrom(topUseClass); JNode usageJNode = enclosingNode == null ? rootJCls : nodeCache.makeFrom(enclosingNode); // Create CodeNode and add to list CodeNode codeNode = new CodeNode(rootJCls, usageJNode, line.trim(), pos); usageList.add(codeNode); } } private JNode getNodeFromCodeNode(CodeNode codeNode) { if (codeNode != null) { try { // Try to get the actual node referenced by the CodeNode JavaNode javaNode = codeNode.getJavaNode(); JNodeCache nodeCache = getNodeCache(); JNode node = nodeCache.makeFrom(javaNode); // If it cannot be obtained directly, try to get it from jParent if (node == null) { node = codeNode.getJParent(); } // Record the log for debugging if (node != null) { LOG.debug("Converted CodeNode to {} of type {}", node.getName(), node.getClass().getSimpleName()); } else { LOG.debug("Failed to convert CodeNode: {}", codeNode.getName()); } return node; } catch (Exception e) { LOG.error("Error converting CodeNode to JNode", e); } } return null; } private void showPopupMenu(MouseEvent e, JNode node, TreePath path) { if (node == null) { return; } JPopupMenu popup = new JPopupMenu(); // Add the expand/load usage menu item DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) path.getLastPathComponent(); JMenuItem expandItem = new JMenuItem(NLS.str("usage_dialog_plus.expand_usages")); expandItem.addActionListener(evt -> { // Expand the node and load the usage if (treeNode.getChildCount() == 0) { JNode nodeToUse = node; // If it is a CodeNode, first convert it to an actual JNode and then search for its usage if (node.getClass() == CodeNode.class) { nodeToUse = getNodeFromCodeNode((CodeNode) node); } if (nodeToUse != null) { loadNodeUsages(nodeToUse, treeNode); } } usageTree.expandPath(path); }); JMenuItem jumpToItem = new JMenuItem(NLS.str("usage_dialog_plus.jump_to")); jumpToItem.addActionListener(evt -> openItem(node)); JMenuItem copyPathItem = new JMenuItem(NLS.str("usage_dialog_plus.copy_path")); copyPathItem.addActionListener(evt -> copyUsagePath(path)); popup.add(expandItem); popup.addSeparator(); popup.add(jumpToItem); popup.add(copyPathItem); popup.show(e.getComponent(), e.getX(), e.getY()); } private void copyUsagePath(TreePath path) { if (path != null) { StringBuilder pathBuilder = new StringBuilder(); Object[] nodes = path.getPath(); // Actually reverse the node order, from the leaf node (currently selected node) to the root node for (int i = nodes.length - 1; i >= 0; i--) { DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) nodes[i]; Object userObject = treeNode.getUserObject(); if (i < nodes.length - 1) { pathBuilder.append("\n"); // Add indentation - reverse calculate the indentation level int indentLevel = nodes.length - 1 - i; for (int j = 0; j < indentLevel; j++) { pathBuilder.append(" "); } pathBuilder.append("-> "); } // Add node information if (userObject instanceof JNode) { pathBuilder.append(((JNode) userObject).getJavaNode().getCodeNodeRef().toString()); } else if (userObject instanceof CodeNode) { CodeNode codeNode = (CodeNode) userObject; pathBuilder.append(codeNode.getJavaNode().getCodeNodeRef().toString()); } } // Copy to clipboard UiUtils.copyToClipboard(pathBuilder.toString()); } } @NotNull @Override protected JPanel initButtonsPanel() { progressPane = new ProgressPanel(mainWindow, false); JButton cancelButton = new JButton(NLS.str("search_dialog.cancel")); cancelButton.addActionListener(event -> dispose()); JButton openBtn = new JButton(NLS.str("search_dialog.open")); openBtn.addActionListener(event -> openSelectedItem()); getRootPane().setDefaultButton(openBtn); JCheckBox cbKeepOpen = new JCheckBox(NLS.str("search_dialog.keep_open")); cbKeepOpen.setSelected(mainWindow.getSettings().isKeepCommonDialogOpen()); cbKeepOpen.addActionListener(e -> mainWindow.getSettings().saveKeepCommonDialogOpen(cbKeepOpen.isSelected())); cbKeepOpen.setAlignmentY(Component.CENTER_ALIGNMENT); JPanel buttonPane = new JPanel(); buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); buttonPane.add(cbKeepOpen); buttonPane.add(Box.createRigidArea(new Dimension(15, 0))); buttonPane.add(progressPane); buttonPane.add(Box.createRigidArea(new Dimension(5, 0))); buttonPane.add(Box.createHorizontalGlue()); buttonPane.add(openBtn); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(cancelButton); return buttonPane; } @Override protected void openSelectedItem() { // Get the currently selected node JNode node = getSelectedNode(); if (node == null) { return; } openItem(node); } @Override protected void loadFinished() { // The tree loading is already handled } @Override protected void loadStart() { // The tree loading is already handled } @Nullable private JNode getSelectedNode() { try { DefaultMutableTreeNode node = (DefaultMutableTreeNode) usageTree.getLastSelectedPathComponent(); if (node == null) { return null; } Object userObject = node.getUserObject(); if (userObject instanceof JNode) { return (JNode) userObject; } else if (userObject instanceof CodeNode) { CodeNode codeNode = (CodeNode) userObject; return getNodeFromCodeNode(codeNode); } return null; } catch (Exception e) { LOG.error("Failed to get selected node", e); return null; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/export/ExportProjectDialog.java ================================================ package jadx.gui.ui.export; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.event.ItemEvent; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.function.Consumer; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.export.ExportGradle; import jadx.core.export.ExportGradleType; import jadx.core.utils.files.FileUtils; import jadx.gui.JadxWrapper; import jadx.gui.ui.MainWindow; import jadx.gui.ui.dialog.CommonDialog; import jadx.gui.ui.filedialog.FileDialogWrapper; import jadx.gui.ui.filedialog.FileOpenMode; import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; import jadx.gui.utils.ui.DocumentUpdateListener; public class ExportProjectDialog extends CommonDialog { private static final Logger LOG = LoggerFactory.getLogger(ExportProjectDialog.class); private final ExportProjectProperties exportProjectProperties = new ExportProjectProperties(); private final Consumer exportListener; public ExportProjectDialog(MainWindow mainWindow, Consumer exportListener) { super(mainWindow); this.exportListener = exportListener; initUI(); } private void initUI() { JPanel contentPanel = new JPanel(); contentPanel.setLayout(new BorderLayout(5, 5)); contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); contentPanel.add(makeContentPane(), BorderLayout.PAGE_START); contentPanel.add(initButtonsPanel(), BorderLayout.PAGE_END); getContentPane().add(contentPanel); setTitle(NLS.str("export_dialog.title")); commonWindowInit(); } private JPanel makeContentPane() { JLabel pathLbl = new JLabel(NLS.str("export_dialog.save_path")); JTextField pathField = new JTextField(); pathField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> setExportProjectPath(pathField))); pathField.setText(mainWindow.getSettings().getLastSaveFilePath().toString()); TextStandardActions.attach(pathField); JButton browseButton = makeEditorBrowseButton(pathField); JCheckBox resourceDecode = new JCheckBox(NLS.str("preferences.skipResourcesDecode")); resourceDecode.setSelected(mainWindow.getSettings().isSkipResources()); resourceDecode.addItemListener(e -> { exportProjectProperties.setSkipResources(e.getStateChange() == ItemEvent.SELECTED); }); JCheckBox skipSources = new JCheckBox(NLS.str("preferences.skipSourcesDecode")); skipSources.setSelected(mainWindow.getSettings().isSkipSources()); skipSources.addItemListener(e -> { exportProjectProperties.setSkipSources(e.getStateChange() == ItemEvent.SELECTED); }); JLabel exportTypeLbl = new JLabel(NLS.str("export_dialog.export_gradle_type")); JComboBox exportTypeComboBox = new JComboBox<>(ExportGradleType.values()); exportTypeLbl.setLabelFor(exportTypeComboBox); ExportGradleType initialExportType = getExportGradleType(); exportProjectProperties.setExportGradleType(initialExportType); exportTypeComboBox.setSelectedItem(initialExportType); exportTypeComboBox.addItemListener(e -> { exportProjectProperties.setExportGradleType((ExportGradleType) e.getItem()); }); exportTypeComboBox.setEnabled(false); JCheckBox exportAsGradleProject = new JCheckBox(NLS.str("export_dialog.export_gradle")); exportAsGradleProject.addItemListener(e -> { boolean enableGradle = e.getStateChange() == ItemEvent.SELECTED; exportProjectProperties.setAsGradleMode(enableGradle); exportTypeComboBox.setEnabled(enableGradle); resourceDecode.setEnabled(!enableGradle); skipSources.setEnabled(!enableGradle); }); JPanel pathPanel = new JPanel(); pathPanel.setLayout(new BoxLayout(pathPanel, BoxLayout.LINE_AXIS)); pathPanel.setAlignmentX(LEFT_ALIGNMENT); pathPanel.add(pathLbl); pathPanel.add(Box.createRigidArea(new Dimension(5, 0))); pathPanel.add(pathField); pathPanel.add(Box.createRigidArea(new Dimension(5, 0))); pathPanel.add(browseButton); JPanel typePanel = new JPanel(); typePanel.setLayout(new BoxLayout(typePanel, BoxLayout.LINE_AXIS)); typePanel.setAlignmentX(LEFT_ALIGNMENT); typePanel.add(Box.createRigidArea(new Dimension(20, 0))); typePanel.add(exportTypeLbl); typePanel.add(Box.createRigidArea(new Dimension(5, 0))); typePanel.add(exportTypeComboBox); typePanel.add(Box.createHorizontalGlue()); JPanel exportOptionsPanel = new JPanel(); exportOptionsPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("export_dialog.export_options"))); exportOptionsPanel.setLayout(new BoxLayout(exportOptionsPanel, BoxLayout.PAGE_AXIS)); exportOptionsPanel.add(exportAsGradleProject); exportOptionsPanel.add(typePanel); exportOptionsPanel.add(resourceDecode); exportOptionsPanel.add(skipSources); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.PAGE_AXIS)); mainPanel.add(pathPanel); mainPanel.add(Box.createRigidArea(new Dimension(0, 10))); mainPanel.add(exportOptionsPanel); return mainPanel; } private ExportGradleType getExportGradleType() { try { JadxWrapper wrapper = mainWindow.getWrapper(); return ExportGradle.detectExportType(wrapper.getRootNode(), wrapper.getResources()); } catch (Exception e) { LOG.warn("Failed to detect export type", e); return ExportGradleType.AUTO; } } private void setExportProjectPath(JTextField field) { String path = field.getText(); if (!path.isEmpty()) { exportProjectProperties.setExportPath(field.getText()); } } protected JPanel initButtonsPanel() { JButton cancelButton = new JButton(NLS.str("common_dialog.cancel")); cancelButton.addActionListener(event -> dispose()); JButton exportProjectButton = new JButton(NLS.str("common_dialog.ok")); exportProjectButton.addActionListener(event -> exportProject()); getRootPane().setDefaultButton(exportProjectButton); JPanel buttonPane = new JPanel(); buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); buttonPane.add(Box.createHorizontalGlue()); buttonPane.add(exportProjectButton); buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(cancelButton); return buttonPane; } private JButton makeEditorBrowseButton(final JTextField textField) { JButton button = new JButton(NLS.str("export_dialog.browse")); button.addActionListener(e -> { FileDialogWrapper fileDialog = new FileDialogWrapper(mainWindow, FileOpenMode.EXPORT); mainWindow.getSettings().setLastSaveFilePath(fileDialog.getCurrentDir()); List saveDirs = fileDialog.show(); if (saveDirs.isEmpty()) { return; } String path = saveDirs.get(0).toString(); textField.setText(path); }); return button; } private void exportProject() { String exportPathStr = exportProjectProperties.getExportPath(); if (!validateAndMakeDir(exportPathStr)) { JOptionPane.showMessageDialog(this, NLS.str("message.enter_valid_path"), NLS.str("message.errorTitle"), JOptionPane.WARNING_MESSAGE); return; } mainWindow.getSettings().setLastSaveFilePath(Path.of(exportPathStr)); LOG.debug("Export properties: {}", exportProjectProperties); exportListener.accept(exportProjectProperties); dispose(); } private static boolean validateAndMakeDir(String exportPath) { if (exportPath == null || exportPath.isBlank()) { return false; } try { Path path = Path.of(exportPath); if (Files.isRegularFile(path)) { // dir exists as a file return false; } FileUtils.makeDirs(path); return true; } catch (Exception e) { LOG.warn("Export path validate error, path string:{}", exportPath, e); return false; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/export/ExportProjectProperties.java ================================================ package jadx.gui.ui.export; import org.jetbrains.annotations.Nullable; import jadx.core.export.ExportGradleType; public class ExportProjectProperties { private boolean skipSources; private boolean skipResources; private boolean asGradleMode; private @Nullable ExportGradleType exportGradleType; private String exportPath; public boolean isSkipSources() { return skipSources; } public void setSkipSources(boolean skipSources) { this.skipSources = skipSources; } public boolean isSkipResources() { return skipResources; } public void setSkipResources(boolean skipResources) { this.skipResources = skipResources; } public boolean isAsGradleMode() { return asGradleMode; } public void setAsGradleMode(boolean asGradleMode) { this.asGradleMode = asGradleMode; } public @Nullable ExportGradleType getExportGradleType() { return exportGradleType; } public void setExportGradleType(@Nullable ExportGradleType exportGradleType) { this.exportGradleType = exportGradleType; } public String getExportPath() { return exportPath; } public void setExportPath(String exportPath) { this.exportPath = exportPath; } @Override public String toString() { return "ExportProjectProperties{exportPath='" + exportPath + '\'' + ", asGradleMode=" + asGradleMode + ", exportGradleType=" + exportGradleType + ", skipSources=" + skipSources + ", skipResources=" + skipResources + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/filedialog/CustomFileChooser.java ================================================ package jadx.gui.ui.filedialog; import java.awt.Component; import java.awt.HeadlessException; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.UIManager; import jadx.api.plugins.utils.CommonFileUtils; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.core.utils.files.FileUtils; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; class CustomFileChooser extends JFileChooser { static { // disable left shortcut panel, can crush in "Win32ShellFolderManager2.getNetwork()" or similar call UIManager.put("FileChooser.noPlacesBar", Boolean.TRUE); } private final FileDialogWrapper data; public CustomFileChooser(FileDialogWrapper data) { super(data.getCurrentDir() == null ? CommonFileUtils.CWD : data.getCurrentDir().toFile()); putClientProperty("FileChooser.useShellFolder", Boolean.FALSE); this.data = data; } public List showDialog() { setToolTipText(data.getTitle()); setFileSelectionMode(data.getSelectionMode()); setMultiSelectionEnabled(data.isOpen()); setAcceptAllFileFilterUsed(true); List fileExtList = data.getFileExtList(); if (Utils.notEmpty(fileExtList)) { List validFileExtList = fileExtList.stream() .filter(StringUtils::notBlank) .collect(Collectors.toList()); if (Utils.notEmpty(validFileExtList)) { String description = NLS.str("file_dialog.supported_files") + ": (" + Utils.listToString(validFileExtList) + ')'; setFileFilter(new FileNameMultiExtensionFilter(description, validFileExtList.toArray(new String[0]))); } } if (data.getSelectedFile() != null) { setSelectedFile(data.getSelectedFile().toFile()); } MainWindow mainWindow = data.getMainWindow(); int ret = data.isOpen() ? showOpenDialog(mainWindow) : showSaveDialog(mainWindow); if (ret != JFileChooser.APPROVE_OPTION) { return Collections.emptyList(); } data.setCurrentDir(getCurrentDirectory().toPath()); File[] selectedFiles = getSelectedFiles(); if (selectedFiles.length != 0) { return FileUtils.toPathsWithTrim(selectedFiles); } File chosenFile = getSelectedFile(); if (chosenFile != null) { return Collections.singletonList(FileUtils.toPathWithTrim(chosenFile)); } return Collections.emptyList(); } @Override protected JDialog createDialog(Component parent) throws HeadlessException { JDialog dialog = super.createDialog(parent); dialog.setTitle(data.getTitle()); dialog.setLocationRelativeTo(null); data.getMainWindow().getSettings().loadWindowPos(dialog); dialog.addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { data.getMainWindow().getSettings().saveWindowPos(dialog); super.windowClosed(e); } }); return dialog; } @Override public void approveSelection() { if (data.getSelectionMode() == FILES_AND_DIRECTORIES) { File currentFile = getSelectedFile(); if (currentFile.isDirectory()) { int option = JOptionPane.showConfirmDialog( data.getMainWindow(), NLS.str("file_dialog.load_dir_confirm") + "\n " + currentFile, NLS.str("file_dialog.load_dir_title"), JOptionPane.YES_NO_OPTION); if (option != JOptionPane.YES_OPTION) { this.setCurrentDirectory(currentFile); this.updateUI(); return; } } } super.approveSelection(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/filedialog/CustomFileDialog.java ================================================ package jadx.gui.ui.filedialog; import java.awt.FileDialog; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.List; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.core.utils.files.FileUtils; class CustomFileDialog { private final FileDialogWrapper data; public CustomFileDialog(FileDialogWrapper data) { this.data = data; } public List showDialog() { FileDialog fileDialog = new FileDialog(data.getMainWindow(), data.getTitle()); fileDialog.setMode(data.isOpen() ? FileDialog.LOAD : FileDialog.SAVE); fileDialog.setMultipleMode(true); List fileExtList = data.getFileExtList(); if (Utils.notEmpty(fileExtList)) { fileDialog.setFilenameFilter((dir, name) -> ListUtils.anyMatch(fileExtList, name::endsWith)); } if (data.getSelectedFile() != null) { fileDialog.setFile(data.getSelectedFile().toAbsolutePath().toString()); } if (data.getCurrentDir() != null) { fileDialog.setDirectory(data.getCurrentDir().toAbsolutePath().toString()); } fileDialog.setVisible(true); File[] selectedFiles = fileDialog.getFiles(); if (!Utils.isEmpty(selectedFiles)) { data.setCurrentDir(Paths.get(fileDialog.getDirectory())); return FileUtils.toPathsWithTrim(selectedFiles); } if (fileDialog.getFile() != null) { return Collections.singletonList(FileUtils.toPathWithTrim(fileDialog.getFile())); } return Collections.emptyList(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/filedialog/FileDialogWrapper.java ================================================ package jadx.gui.ui.filedialog; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.swing.JFileChooser; import org.jetbrains.annotations.Nullable; import jadx.gui.settings.JadxProject; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; public class FileDialogWrapper { private static final List OPEN_FILES_EXTS = Arrays.asList( "apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc", "jadx.kts", "xapk", "apkm", "apks"); private final MainWindow mainWindow; private boolean isOpen; private String title; private List fileExtList = new ArrayList<>(); private int selectionMode = JFileChooser.FILES_AND_DIRECTORIES; private @Nullable Path currentDir; private @Nullable Path selectedFile; public FileDialogWrapper(MainWindow mainWindow, FileOpenMode mode) { this.mainWindow = mainWindow; initForMode(mode); } public void setTitle(String title) { this.title = title; } public void setFileExtList(List fileExtList) { this.fileExtList = fileExtList; } public void setSelectionMode(int selectionMode) { this.selectionMode = selectionMode; } public void setSelectedFile(Path path) { this.selectedFile = path; } public void setCurrentDir(Path currentDir) { this.currentDir = currentDir; } public List show() { if (mainWindow.getSettings().isUseAlternativeFileDialog()) { return new CustomFileDialog(this).showDialog(); } else { return new CustomFileChooser(this).showDialog(); } } private void initForMode(FileOpenMode mode) { switch (mode) { case OPEN_PROJECT: title = NLS.str("file.open_title"); fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION); selectionMode = JFileChooser.FILES_AND_DIRECTORIES; currentDir = mainWindow.getSettings().getLastOpenFilePath(); isOpen = true; break; case OPEN: title = NLS.str("file.open_title"); fileExtList = new ArrayList<>(OPEN_FILES_EXTS); fileExtList.add(JadxProject.PROJECT_EXTENSION); fileExtList.add("aab"); selectionMode = JFileChooser.FILES_AND_DIRECTORIES; currentDir = mainWindow.getSettings().getLastOpenFilePath(); isOpen = true; break; case ADD: title = NLS.str("file.add_files_action"); fileExtList = new ArrayList<>(OPEN_FILES_EXTS); fileExtList.add("aab"); selectionMode = JFileChooser.FILES_AND_DIRECTORIES; currentDir = mainWindow.getSettings().getLastOpenFilePath(); isOpen = true; break; case SAVE_PROJECT: title = NLS.str("file.save_project"); fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION); selectionMode = JFileChooser.FILES_ONLY; currentDir = mainWindow.getSettings().getLastSaveFilePath(); isOpen = false; break; case EXPORT: title = NLS.str("file.save_all_msg"); fileExtList = Collections.emptyList(); selectionMode = JFileChooser.DIRECTORIES_ONLY; currentDir = mainWindow.getSettings().getLastSaveFilePath(); isOpen = false; break; case CUSTOM_SAVE: isOpen = false; currentDir = mainWindow.getSettings().getLastSaveFilePath(); break; case CUSTOM_OPEN: isOpen = true; currentDir = mainWindow.getSettings().getLastOpenFilePath(); break; case EXPORT_NODE: isOpen = false; title = NLS.str("file.export_node"); currentDir = mainWindow.getSettings().getLastSaveFilePath(); selectionMode = JFileChooser.FILES_ONLY; break; case EXPORT_NODE_FOLDER: isOpen = true; title = NLS.str("file.save_all_msg"); currentDir = mainWindow.getSettings().getLastSaveFilePath(); selectionMode = JFileChooser.DIRECTORIES_ONLY; break; } } public Path getCurrentDir() { return currentDir; } public MainWindow getMainWindow() { return mainWindow; } public boolean isOpen() { return isOpen; } public String getTitle() { return title; } public List getFileExtList() { return fileExtList; } public int getSelectionMode() { return selectionMode; } public Path getSelectedFile() { return selectedFile; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/filedialog/FileNameMultiExtensionFilter.java ================================================ package jadx.gui.ui.filedialog; import java.io.File; import javax.swing.filechooser.FileFilter; import javax.swing.filechooser.FileNameExtensionFilter; /** * Custom file filter for filtering files with multiple extensions. * It overcomes the limitation of {@link FileNameExtensionFilter}, * which treats only the last file extension split by dots as the * file extension, and does not support multiple extensions such as * {@code .jadx.kts}. */ class FileNameMultiExtensionFilter extends FileFilter { private final FileNameExtensionFilter delegate; private final String[] extensions; public FileNameMultiExtensionFilter(String description, String... extensions) { this.delegate = new FileNameExtensionFilter(description, extensions[0]); this.extensions = extensions; } @Override public boolean accept(File file) { if (file.isDirectory()) { return true; } String fileName = file.getName(); for (String extension : extensions) { if (fileName.endsWith(extension)) { return true; } } return false; } @Override public String getDescription() { return delegate.getDescription(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/filedialog/FileOpenMode.java ================================================ package jadx.gui.ui.filedialog; public enum FileOpenMode { OPEN, OPEN_PROJECT, ADD, SAVE_PROJECT, EXPORT, CUSTOM_SAVE, CUSTOM_OPEN, EXPORT_NODE, EXPORT_NODE_FOLDER, } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/hexviewer/BinEdCodeAreaAssessor.java ================================================ package jadx.gui.ui.hexviewer; /* * Copyright (C) ExBin Project * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * https://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.awt.Color; import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.exbin.bined.CodeAreaSection; import org.exbin.bined.highlight.swing.NonAsciiCodeAreaColorAssessor; import org.exbin.bined.highlight.swing.NonprintablesCodeAreaAssessor; import org.exbin.bined.highlight.swing.SearchCodeAreaColorAssessor; import org.exbin.bined.swing.CodeAreaCharAssessor; import org.exbin.bined.swing.CodeAreaColorAssessor; import org.exbin.bined.swing.CodeAreaPaintState; /** * Color assessor for binary editor with registrable modifiers. * * @author ExBin Project (https://exbin.org) */ public class BinEdCodeAreaAssessor implements CodeAreaColorAssessor, CodeAreaCharAssessor { private final List priorityColorModifiers = new ArrayList<>(); private final List colorModifiers = new ArrayList<>(); private final CodeAreaColorAssessor parentColorAssessor; private final CodeAreaCharAssessor parentCharAssessor; public BinEdCodeAreaAssessor(CodeAreaColorAssessor parentColorAssessor, CodeAreaCharAssessor parentCharAssessor) { NonAsciiCodeAreaColorAssessor nonAsciiCodeAreaColorAssessor = new NonAsciiCodeAreaColorAssessor(parentColorAssessor); NonprintablesCodeAreaAssessor nonprintablesCodeAreaAssessor = new NonprintablesCodeAreaAssessor(nonAsciiCodeAreaColorAssessor, parentCharAssessor); SearchCodeAreaColorAssessor searchCodeAreaColorAssessor = new SearchCodeAreaColorAssessor(nonprintablesCodeAreaAssessor); this.parentColorAssessor = searchCodeAreaColorAssessor; this.parentCharAssessor = nonprintablesCodeAreaAssessor; } public void addColorModifier(PositionColorModifier colorModifier) { colorModifiers.add(colorModifier); } public void removeColorModifier(PositionColorModifier colorModifier) { colorModifiers.remove(colorModifier); } public void addPriorityColorModifier(PositionColorModifier colorModifier) { priorityColorModifiers.add(colorModifier); } public void removePriorityColorModifier(PositionColorModifier colorModifier) { priorityColorModifiers.remove(colorModifier); } @Override public void startPaint(CodeAreaPaintState codeAreaPaintState) { for (PositionColorModifier colorModifier : priorityColorModifiers) { colorModifier.resetColors(); } for (PositionColorModifier colorModifier : colorModifiers) { colorModifier.resetColors(); } if (parentColorAssessor != null) { parentColorAssessor.startPaint(codeAreaPaintState); } } @Override public Color getPositionBackgroundColor(long rowDataPosition, int byteOnRow, int charOnRow, CodeAreaSection section, boolean inSelection) { for (PositionColorModifier colorModifier : priorityColorModifiers) { Color positionBackgroundColor = colorModifier.getPositionBackgroundColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); if (positionBackgroundColor != null) { return positionBackgroundColor; } } if (!inSelection) { for (PositionColorModifier colorModifier : colorModifiers) { Color positionBackgroundColor = colorModifier.getPositionBackgroundColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); if (positionBackgroundColor != null) { return positionBackgroundColor; } } } if (parentColorAssessor != null) { return parentColorAssessor.getPositionBackgroundColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); } return null; } @Override public Color getPositionTextColor(long rowDataPosition, int byteOnRow, int charOnRow, CodeAreaSection section, boolean inSelection) { for (PositionColorModifier colorModifier : priorityColorModifiers) { Color positionTextColor = colorModifier.getPositionTextColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); if (positionTextColor != null) { return positionTextColor; } } if (!inSelection) { for (PositionColorModifier colorModifier : colorModifiers) { Color positionTextColor = colorModifier.getPositionTextColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); if (positionTextColor != null) { return positionTextColor; } } } if (parentColorAssessor != null) { return parentColorAssessor.getPositionTextColor(rowDataPosition, byteOnRow, charOnRow, section, inSelection); } return null; } @Override public char getPreviewCharacter(long rowDataPosition, int byteOnRow, int charOnRow, CodeAreaSection section) { return parentCharAssessor != null ? parentCharAssessor.getPreviewCharacter(rowDataPosition, byteOnRow, charOnRow, section) : ' '; } @Override public char getPreviewCursorCharacter(long rowDataPosition, int byteOnRow, int charOnRow, byte[] cursorData, int cursorDataLength, CodeAreaSection section) { return parentCharAssessor != null ? parentCharAssessor.getPreviewCursorCharacter(rowDataPosition, byteOnRow, charOnRow, cursorData, cursorDataLength, section) : ' '; } @Override public Optional getParentCharAssessor() { return Optional.ofNullable(parentCharAssessor); } @Override public Optional getParentColorAssessor() { return Optional.ofNullable(parentColorAssessor); } public interface PositionColorModifier { Color getPositionBackgroundColor(long rowDataPosition, int byteOnRow, int charOnRow, CodeAreaSection section, boolean inSelection); Color getPositionTextColor(long rowDataPosition, int byteOnRow, int charOnRow, CodeAreaSection section, boolean inSelection); void resetColors(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexEditorHeader.java ================================================ package jadx.gui.ui.hexviewer; import java.awt.Color; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.RenderingHints; import javax.swing.JComponent; import javax.swing.UIManager; import org.exbin.bined.CodeAreaCaretListener; import org.exbin.bined.CodeAreaSection; import org.exbin.bined.DataChangedListener; import org.exbin.bined.SelectionChangedListener; import org.exbin.bined.SelectionRange; import org.exbin.bined.basic.BasicCodeAreaSection; import org.exbin.bined.swing.section.SectCodeArea; public class HexEditorHeader extends JComponent { private static final long serialVersionUID = 1L; private static final String HEX_ALPHABET = "0123456789ABCDEF"; private final SectCodeArea parent; private final DataChangedListener dataChangedListener = this::repaint; private final CodeAreaCaretListener caretMovedListener = caretPosition -> repaint(); private final SelectionChangedListener selectionChangedListener = this::repaint; private Dimension minimumSize = null; private Dimension preferredSize = null; public HexEditorHeader(SectCodeArea parent) { this.parent = parent; if (this.parent != null) { this.parent.addCaretMovedListener(caretMovedListener); this.parent.addSelectionChangedListener(selectionChangedListener); this.parent.addDataChangedListener(dataChangedListener); } } @Override public void addNotify() { super.addNotify(); if (this.parent != null) { this.parent.addCaretMovedListener(caretMovedListener); this.parent.addSelectionChangedListener(selectionChangedListener); this.parent.addDataChangedListener(dataChangedListener); } } @Override public void removeNotify() { if (this.parent != null) { this.parent.removeCaretMovedListener(caretMovedListener); this.parent.removeSelectionChangedListener(selectionChangedListener); this.parent.removeDataChangedListener(dataChangedListener); } super.removeNotify(); } @Override public Dimension getMinimumSize() { if (minimumSize != null) { return minimumSize; } Insets i = getInsets(); FontMetrics fm = getFontMetrics(parent.getFont()); if (fm == null) { return new Dimension(100, 20); // Fallback } int ch = fm.getHeight() + 2; // Row height int cw = fm.stringWidth(HEX_ALPHABET) / 16; // Char width estimate String sampleText = "Sel: 00000000:00000000 Len: 00000000/00000000 TXT UTF-8"; int minTextWidth = fm.stringWidth(sampleText); int minimumWidth = minTextWidth + cw * 5 + i.left + i.right; int minimumHeight = ch + 5 + i.top + i.bottom; return new Dimension(minimumWidth, minimumHeight); } @Override public void setMinimumSize(Dimension minimumSize) { this.minimumSize = minimumSize; revalidate(); } @Override public Dimension getPreferredSize() { if (preferredSize != null) { return preferredSize; } Insets i = getInsets(); FontMetrics fm = getFontMetrics(parent.getFont()); if (fm == null) { return getMinimumSize(); // Fallback } int ch = fm.getHeight() + 2; int cw = fm.stringWidth(HEX_ALPHABET) / 16; String sampleText = "Sel: 00000000:00000000 Len: 00000000/00000000 TXT UTF-8"; int preferredTextWidth = fm.stringWidth(sampleText); int preferredWidth = preferredTextWidth + cw * 10 + i.left + i.right; int preferredHeight = ch + 5 + i.top + i.bottom; return new Dimension(preferredWidth, preferredHeight); } @Override public void setPreferredSize(Dimension preferredSize) { this.preferredSize = preferredSize; revalidate(); } @Override protected void paintComponent(Graphics g) { // Standard Graphics2D setup if (g instanceof Graphics2D) { Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } // Get insets, width, height Insets i = getInsets(); int fw = getWidth(); int fh = getHeight(); int w = fw - i.left - i.right; int h = fh - i.top - i.bottom; // Get font metrics g.setFont(parent.getFont()); FontMetrics fm = g.getFontMetrics(); if (fm == null) { return; // Cannot paint without font metrics } int ca = fm.getAscent() + 1; // Character ascent for baseline int ch = fm.getHeight() + 2; // Row height including padding int cw = fm.stringWidth(HEX_ALPHABET) / 16; // Character width estimate if (cw <= 0) { cw = 1; // Avoid division by zero or incorrect calculations } // Get colors and status from parent editor Color separatorForeground = UIManager.getColor("Separator.foreground"); Color themeBackground = UIManager.getColor("Panel.background"); Color themeForeground = UIManager.getColor("Panel.foreground"); SelectionRange selectionRange = parent.getSelection(); long ss = selectionRange.getStart(); long se = selectionRange.getEnd(); long sl = selectionRange.getLength(); long length = parent.getDataSize(); // Vertical position for the text baseline (centered vertically) int ty = i.top + ((h - ch) / 2) + ca; // Calculate middle Y for the single line of text // Draw Background g.setColor(themeBackground); g.fillRect(i.left, i.top, w, h); // Draw Text Elements (Sel, Len, Status) g.setColor(themeForeground); // Start drawing position (adjust for left inset) int currentX = i.left + cw / 2; // Start with a small left padding // Selection Range (Sel: start:end String selLabel = "Sel:"; g.drawString(selLabel, currentX, ty); currentX += fm.stringWidth(selLabel) + cw; // "Sel:" + space String sss = addressString(ss); g.drawString(sss, currentX, ty); currentX += fm.stringWidth(sss); // start String separator1 = ":"; g.drawString(separator1, currentX, ty); currentX += fm.stringWidth(separator1); // : String ses = addressString(se); g.drawString(ses, currentX, ty); currentX += fm.stringWidth(ses) + cw; // end + larger space // Draw Divider After Sel Range int dividerTopY = i.top; int dividerHeight = h; g.setColor(separatorForeground); g.fillRect(currentX, dividerTopY, 1, dividerHeight); currentX += cw; // Add larger space after the divider // Length Information (Len: selected/total- g.setColor(themeForeground); String lenLabel = "Len:"; g.drawString(lenLabel, currentX, ty); currentX += fm.stringWidth(lenLabel) + cw; // "Len:" + space String sls = addressString(sl); g.drawString(sls, currentX, ty); currentX += fm.stringWidth(sls); // selected length String separator2 = "/"; g.drawString(separator2, currentX, ty); currentX += fm.stringWidth(separator2); String ls = addressString(length); g.drawString(ls, currentX, ty); currentX += fm.stringWidth(ls) + cw; // total length + larger space // Draw Divider After Len Info g.setColor(separatorForeground); g.fillRect(currentX, dividerTopY, 1, dividerHeight); currentX += cw; // Add larger space after the divider // Status Indicators (TXT/HEX, RO/RW, OVR/INS, LE/BE, Charset) g.setColor(themeForeground); // Ensure color is set for text // Draw TXT/HEX status String statusTxtHex = "HEX"; CodeAreaSection section = parent.getActiveSection(); if (section == BasicCodeAreaSection.TEXT_PREVIEW) { statusTxtHex = "TXT"; } g.drawString(statusTxtHex, currentX, ty); currentX += fm.stringWidth(statusTxtHex) + cw; // TXT/HEX // Draw Divider After TXT/HEX g.setColor(separatorForeground); g.fillRect(currentX, dividerTopY, 1, dividerHeight); currentX += cw; // Add larger space after the divider g.setColor(themeForeground); // Restore text color // Draw Charset String statusCharset = parent.getCharset().name(); g.drawString(statusCharset, currentX, ty); // No divider or space needed after the last element // Draw Bottom Border g.setColor(separatorForeground); // Use headerDivider color for the border g.fillRect(i.left, i.top + h - 1, w, 1); } public String addressString(long address) { return String.format("%08X", address); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexInspectorPanel.java ================================================ package jadx.gui.ui.hexviewer; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ItemEvent; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.function.Function; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import org.apache.commons.lang3.ArrayUtils; public class HexInspectorPanel extends JPanel { private final List formatters = new ArrayList<>(); private byte[] bytes = null; private Integer offset = null; private boolean isLittleEndian; private int row = 0; public HexInspectorPanel() { setLayout(new GridBagLayout()); addValueFormat("Signed 8 bit", 1, b -> Integer.toString(b.get())); addValueFormat("Unsigned 8 bit", 1, b -> Integer.toString(b.get() & 0xFF)); addValueFormat("Signed 16 bit", 2, b -> Short.toString(b.getShort())); addValueFormat("Unsigned 16 bit", 2, b -> Integer.toString(b.getShort() & 0xFFFF)); addValueFormat("Float 32 bit", 4, b -> Float.toString(b.getFloat())); addValueFormat("Signed 32 bit", 4, b -> Integer.toString(b.getInt())); addValueFormat("Unsigned 32 bit", 4, b -> Integer.toUnsignedString(b.getInt())); addValueFormat("Signed 64 bit", 8, b -> Long.toString(b.getLong())); addValueFormat("Float 64 bit", 8, b -> Double.toString(b.getDouble())); addValueFormat("Unsigned 64 bit", 8, b -> Long.toUnsignedString(b.getLong())); addValueFormat("Hexadecimal", 1, b -> Integer.toString(b.get(), 16)); addValueFormat("Octal", 1, b -> Integer.toString(b.get(), 8)); addValueFormat("Binary", 1, b -> Integer.toString(b.get(), 2)); GridBagConstraints constraints; constraints = getConstraints(); constraints.gridwidth = 2; JCheckBox littleEndianCheckBox = new JCheckBox("Little endian", false); littleEndianCheckBox.addItemListener(ev -> { isLittleEndian = ev.getStateChange() == ItemEvent.SELECTED; reloadOffset(); }); add(littleEndianCheckBox, constraints); // Workaround to force widgets to start from the top (otherwise centered) constraints = getConstraints(); constraints.weighty = 1; add(new JLabel(" "), constraints); } public void setOffset(int offset) { this.offset = offset; reloadOffset(); } public void setBytes(byte[] bytes) { this.bytes = bytes; } private void reloadOffset() { if (bytes == null || offset == null) { return; } for (int i = 0; i < formatters.size(); i++) { ValueFormatter formatter = formatters.get(i); if (canDisplay(offset, formatter.dataSize)) { ByteBuffer buffer = decodeByteArray(offset, formatter.dataSize); String value = formatter.function.apply(buffer); ((JTextField) getComponent(i * 2 + 1)).setText(value); } } } private GridBagConstraints getConstraints() { GridBagConstraints constraints = new GridBagConstraints(); constraints.insets = new Insets(5, 5, 5, 5); constraints.gridy = row; row++; return constraints; } private void addValueFormat(String name, int dataSize, Function formatter) { formatters.add(new ValueFormatter(dataSize, formatter)); GridBagConstraints constraints = getConstraints(); constraints.gridx = 0; constraints.anchor = GridBagConstraints.WEST; add(new JLabel(name), constraints); constraints.fill = GridBagConstraints.HORIZONTAL; constraints.gridx = 1; JTextField textField = new JTextField(); textField.setEditable(false); add(textField, constraints); } private boolean canDisplay(int offset, int size) { return offset + size <= bytes.length; } private ByteBuffer decodeByteArray(int offset, int size) { byte[] chunk = sliceBytes(offset, size); if (isLittleEndian) { ArrayUtils.reverse(chunk); } return ByteBuffer.wrap(chunk); } private byte[] sliceBytes(int offset, int size) { byte[] slice = new byte[size]; System.arraycopy(bytes, offset, slice, 0, size); return slice; } private static class ValueFormatter { public final int dataSize; public final Function function; public ValueFormatter(int dataSize, Function function) { this.dataSize = dataSize; this.function = function; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexPreviewPanel.java ================================================ package jadx.gui.ui.hexviewer; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Objects; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import org.exbin.auxiliary.binary_data.BinaryData; import org.exbin.auxiliary.binary_data.array.ByteArrayEditableData; import org.exbin.bined.CodeAreaCaretListener; import org.exbin.bined.CodeAreaCaretPosition; import org.exbin.bined.CodeAreaUtils; import org.exbin.bined.CodeCharactersCase; import org.exbin.bined.CodeType; import org.exbin.bined.EditMode; import org.exbin.bined.SelectionRange; import org.exbin.bined.basic.BasicCodeAreaZone; import org.exbin.bined.color.CodeAreaBasicColors; import org.exbin.bined.highlight.swing.color.CodeAreaMatchColorType; import org.exbin.bined.swing.CodeAreaPainter; import org.exbin.bined.swing.basic.DefaultCodeAreaCommandHandler; import org.exbin.bined.swing.capability.CharAssessorPainterCapable; import org.exbin.bined.swing.capability.ColorAssessorPainterCapable; import org.exbin.bined.swing.section.SectCodeArea; import org.exbin.bined.swing.section.color.SectionCodeAreaColorProfile; import jadx.gui.settings.JadxSettings; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class HexPreviewPanel extends JPanel { private static final long serialVersionUID = 3261685857479120073L; private static final int CACHE_SIZE = 250; private final byte[] valuesCache = new byte[CACHE_SIZE]; private final SectCodeArea hexCodeArea; private final SectionCodeAreaColorProfile defaultColors; private final HexEditorHeader header; private final HexSearchBar searchBar; private final HexInspectorPanel inspector; private JPopupMenu popupMenu; private JMenuItem cutAction; private JMenuItem copyAction; private JMenuItem copyHexAction; private JMenuItem copyStringAction; private JMenuItem pasteAction; private JMenuItem deleteAction; private JMenuItem selectAllAction; private JMenuItem copyOffsetItem; private BasicCodeAreaZone popupMenuPositionZone = BasicCodeAreaZone.UNKNOWN; public HexPreviewPanel(JadxSettings settings) { hexCodeArea = new SectCodeArea(); hexCodeArea.setCodeFont(settings.getSmaliFont()); hexCodeArea.setEditMode(EditMode.READ_ONLY); hexCodeArea.setCharset(StandardCharsets.UTF_8); hexCodeArea.setComponentPopupMenu(new JPopupMenu() { @Override public void show(Component invoker, int x, int y) { popupMenuPositionZone = hexCodeArea.getPainter().getPositionZone(x, y); createPopupMenu(); if (popupMenu != null && popupMenuPositionZone != BasicCodeAreaZone.HEADER && popupMenuPositionZone != BasicCodeAreaZone.ROW_POSITIONS) { updatePopupActionStates(); popupMenu.show(invoker, x, y); } } }); inspector = new HexInspectorPanel(); searchBar = new HexSearchBar(hexCodeArea); header = new HexEditorHeader(hexCodeArea); header.setFont(settings.getUiFont()); CodeAreaPainter painter = hexCodeArea.getPainter(); defaultColors = (SectionCodeAreaColorProfile) hexCodeArea.getColorsProfile(); hexCodeArea.setColorsProfile(getColorsProfile()); BinEdCodeAreaAssessor codeAreaAssessor = new BinEdCodeAreaAssessor(((ColorAssessorPainterCapable) painter).getColorAssessor(), ((CharAssessorPainterCapable) painter).getCharAssessor()); ((ColorAssessorPainterCapable) painter).setColorAssessor(codeAreaAssessor); ((CharAssessorPainterCapable) painter).setCharAssessor(codeAreaAssessor); setLayout(new BorderLayout()); add(searchBar, BorderLayout.PAGE_START); add(hexCodeArea, BorderLayout.CENTER); add(header, BorderLayout.PAGE_END); add(inspector, BorderLayout.EAST); setFocusable(true); addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { hexCodeArea.requestFocusInWindow(); } @Override public void focusLost(FocusEvent e) { } }); createActions(); enableUpdate(); } public SectionCodeAreaColorProfile getColorsProfile() { boolean isDarkTheme = UiUtils.isDarkTheme(Objects.requireNonNull(defaultColors.getColor(CodeAreaBasicColors.TEXT_BACKGROUND))); Color markAllHighlightColor = isDarkTheme ? Color.decode("#32593D") : Color.decode("#ffc800"); Color editorSelectionBackground = Objects.requireNonNull(defaultColors.getColor(CodeAreaBasicColors.SELECTION_BACKGROUND)); Color currentMatchColor = UiUtils.adjustBrightness(editorSelectionBackground, isDarkTheme ? 0.6f : 1.4f); defaultColors.setColor(CodeAreaMatchColorType.MATCH_BACKGROUND, markAllHighlightColor); defaultColors.setColor(CodeAreaMatchColorType.CURRENT_MATCH_BACKGROUND, currentMatchColor); return defaultColors; } public boolean isDataLoaded() { return !hexCodeArea.getContentData().isEmpty(); } public void setData(byte[] data) { if (data != null) { hexCodeArea.setContentData(new ByteArrayEditableData(data)); inspector.setBytes(data); } } public void scrollToOffset(int pos) { hexCodeArea.setSelection(pos, pos + 1); hexCodeArea.setActiveCaretPosition(pos); hexCodeArea.centerOnPosition(hexCodeArea.getActiveCaretPosition()); } public void enableUpdate() { CodeAreaCaretListener caretMovedListener = (CodeAreaCaretPosition caretPosition) -> updateValues(); hexCodeArea.addCaretMovedListener(caretMovedListener); } private void updateValues() { CodeAreaCaretPosition caretPosition = hexCodeArea.getActiveCaretPosition(); long dataPosition = caretPosition.getDataPosition(); long dataSize = hexCodeArea.getDataSize(); if (dataPosition < dataSize) { int availableData = dataSize - dataPosition >= CACHE_SIZE ? CACHE_SIZE : (int) (dataSize - dataPosition); BinaryData contentData = hexCodeArea.getContentData(); contentData.copyToArray(dataPosition, valuesCache, 0, availableData); if (availableData < CACHE_SIZE) { Arrays.fill(valuesCache, availableData, CACHE_SIZE, (byte) 0); } } inspector.setOffset((int) dataPosition); } private void createActions() { cutAction = new JMenuItem(NLS.str("popup.cut")); cutAction.addActionListener(e -> performCut()); copyAction = new JMenuItem(NLS.str("popup.copy")); copyAction.addActionListener(e -> performCopy()); copyHexAction = new JMenuItem(NLS.str("popup.copy_as_hex")); copyHexAction.addActionListener(e -> performCopyAsCode()); copyStringAction = new JMenuItem(NLS.str("popup.copy_as_string")); copyStringAction.addActionListener(e -> performCopy()); pasteAction = new JMenuItem(NLS.str("popup.paste")); pasteAction.addActionListener(e -> performPaste()); deleteAction = new JMenuItem(NLS.str("popup.delete")); deleteAction.addActionListener(e -> { if (!isEditable()) { performDelete(); } }); selectAllAction = new JMenuItem(NLS.str("popup.select_all")); selectAllAction.addActionListener(e -> performSelectAll()); copyOffsetItem = new JMenuItem(NLS.str("popup.copy_offset")); copyOffsetItem.addActionListener(e -> copyOffset()); } private void createPopupMenu() { boolean isEditable = isEditable(); popupMenu = new JPopupMenu(); popupMenu.add(copyAction); if (isEditable) { popupMenu.add(cutAction); popupMenu.add(pasteAction); popupMenu.add(deleteAction); popupMenu.addSeparator(); } JMenu copyMenu = new JMenu(NLS.str("popup.copy_as")); copyMenu.add(copyHexAction); copyMenu.add(copyStringAction); popupMenu.add(copyMenu); popupMenu.add(copyOffsetItem); popupMenu.add(selectAllAction); } private void updatePopupActionStates() { boolean selectionExists = isSelection(); boolean isEditable = !isEditable(); cutAction.setEnabled(isEditable && selectionExists); copyAction.setEnabled(selectionExists); copyHexAction.setEnabled(selectionExists); copyStringAction.setEnabled(selectionExists); deleteAction.setEnabled(isEditable && selectionExists); selectAllAction.setEnabled(hexCodeArea.getDataSize() > 0); } public SectCodeArea getEditor() { return this.hexCodeArea; } public HexEditorHeader getHeader() { return this.header; } public HexInspectorPanel getInspector() { return this.inspector; } public HexSearchBar getSearchBar() { return this.searchBar; } public void showSearchBar() { searchBar.showAndFocus(); } public void performCut() { hexCodeArea.cut(); } public void performCopy() { hexCodeArea.copy(); } public void performCopyAsCode() { ((DefaultCodeAreaCommandHandler) hexCodeArea.getCommandHandler()).copyAsCode(); } public void performPaste() { hexCodeArea.paste(); } public void performDelete() { hexCodeArea.delete(); } public void performSelectAll() { hexCodeArea.selectAll(); } public boolean isSelection() { return hexCodeArea.hasSelection(); } public boolean isEditable() { return hexCodeArea.isEditable(); } public boolean canPaste() { return hexCodeArea.canPaste(); } public static String getSelectionData(SectCodeArea core) { SelectionRange selection = core.getSelection(); if (!selection.isEmpty()) { long first = selection.getFirst(); long last = selection.getLast(); BinaryData copy = core.getContentData().copy(first, last - first + 1); CodeType codeType = core.getCodeType(); CodeCharactersCase charactersCase = core.getCodeCharactersCase(); int charsPerByte = codeType.getMaxDigitsForByte() + 1; int textLength = (int) (copy.getDataSize() * charsPerByte); if (textLength > 0) { textLength--; } char[] targetData = new char[textLength]; Arrays.fill(targetData, ' '); for (int i = 0; i < (int) copy.getDataSize(); i++) { CodeAreaUtils.byteToCharsCode(copy.getByte(i), codeType, targetData, i * charsPerByte, charactersCase); } return new String(targetData); } return null; } public void copyOffset() { String str = header.addressString(hexCodeArea.getSelection().getStart()); UiUtils.copyToClipboard(str); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexSearchBar.java ================================================ package jadx.gui.ui.hexviewer; import java.awt.Color; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JTextField; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.border.EmptyBorder; import org.exbin.auxiliary.binary_data.array.ByteArrayEditableData; import org.exbin.bined.CodeAreaUtils; import org.exbin.bined.swing.section.SectCodeArea; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.formdev.flatlaf.FlatClientProperties; import jadx.core.utils.StringUtils; import jadx.gui.ui.hexviewer.search.BinarySearch; import jadx.gui.ui.hexviewer.search.SearchCondition; import jadx.gui.ui.hexviewer.search.SearchParameters; import jadx.gui.ui.hexviewer.search.service.BinarySearchServiceImpl; import jadx.gui.utils.HexUtils; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; import jadx.gui.utils.UiUtils; public class HexSearchBar extends JToolBar { private static final long serialVersionUID = 1836871286618633003L; private static final Logger LOG = LoggerFactory.getLogger(HexSearchBar.class); private final SectCodeArea hexCodeArea; private final JTextField searchField; private final JLabel resultCountLabel; private final JToggleButton markAllCB; private final JToggleButton findTypeCB; private final JToggleButton matchCaseCB; private final JButton nextMatchButton; private final JButton prevMatchButton; private Control control = null; public HexSearchBar(SectCodeArea textArea) { hexCodeArea = textArea; JLabel findLabel = new JLabel(NLS.str("search.find") + ':'); add(findLabel); searchField = new JTextField(30); searchField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true); searchField.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_ENTER: // skip break; case KeyEvent.VK_ESCAPE: toggle(); break; default: control.performFind(); break; } } }); searchField.addActionListener(e -> control.notifySearchChanging()); TextStandardActions.attach(searchField); add(searchField); ActionListener searchSettingListener = e -> control.notifySearchChanged(); resultCountLabel = new JLabel(); resultCountLabel.setBorder(new EmptyBorder(0, 10, 0, 10)); resultCountLabel.setForeground(Color.GRAY); add(resultCountLabel); matchCaseCB = new JToggleButton(); matchCaseCB.setIcon(Icons.ICON_MATCH); matchCaseCB.setSelectedIcon(Icons.ICON_MATCH_SELECTED); matchCaseCB.setToolTipText(NLS.str("search.match_case")); matchCaseCB.addActionListener(searchSettingListener); add(matchCaseCB); findTypeCB = new JToggleButton(); findTypeCB.setIcon(Icons.ICON_FIND_TYPE_TXT); findTypeCB.setSelectedIcon(Icons.ICON_FIND_TYPE_HEX); if (findTypeCB.isSelected()) { findTypeCB.setToolTipText(NLS.str("search.find_type_hex")); } else { findTypeCB.setToolTipText(NLS.str("search.find_type_text")); } findTypeCB.addActionListener(e -> { searchField.setText(""); updateFindStatus(); control.notifySearchChanged(); }); add(findTypeCB); prevMatchButton = new JButton(); prevMatchButton.setIcon(Icons.ICON_UP); prevMatchButton.setToolTipText(NLS.str("search.previous")); prevMatchButton.addActionListener(e -> control.prevMatch()); prevMatchButton.setBorderPainted(false); add(prevMatchButton); nextMatchButton = new JButton(); nextMatchButton.setIcon(Icons.ICON_DOWN); nextMatchButton.setToolTipText(NLS.str("search.next")); nextMatchButton.addActionListener(e -> control.nextMatch()); nextMatchButton.setBorderPainted(false); add(nextMatchButton); markAllCB = new JToggleButton(); markAllCB.setIcon(Icons.ICON_MARK); markAllCB.setSelectedIcon(Icons.ICON_MARK_SELECTED); markAllCB.setToolTipText(NLS.str("search.mark_all")); markAllCB.setSelected(true); markAllCB.addActionListener(searchSettingListener); add(markAllCB); JButton closeButton = new JButton(); closeButton.setIcon(Icons.ICON_CLOSE); closeButton.addActionListener(e -> toggle()); closeButton.setBorderPainted(false); add(closeButton); BinarySearch binarySearch = new BinarySearch(this); binarySearch.setBinarySearchService(new BinarySearchServiceImpl(hexCodeArea)); setFloatable(false); setVisible(false); } /* * Replicates IntelliJ's search bar behavior * 1.1. If the user has selected text, use that as the search text * 1.2. Otherwise, use the previous search text (or empty if none) * 2. Select all text in the search bar and give it focus */ public void showAndFocus() { setVisible(true); if (hexCodeArea.hasSelection()) { searchField.setText(hexCodeArea.getActiveSection().toString()); } String selectedText = HexPreviewPanel.getSelectionData(hexCodeArea); if (!StringUtils.isEmpty(selectedText)) { searchField.setText(selectedText); makeFindByHexButton(); } searchField.selectAll(); searchField.requestFocus(); } public void toggle() { boolean visible = !isVisible(); setVisible(visible); if (visible) { String preferText = HexPreviewPanel.getSelectionData(hexCodeArea); if (!StringUtils.isEmpty(preferText)) { searchField.setText(preferText); makeFindByHexButton(); } searchField.selectAll(); searchField.requestFocus(); } else { control.performEscape(); hexCodeArea.requestFocus(); } } public void setInfoLabel(String text) { resultCountLabel.setText(text); } public void updateMatchCount(boolean hasMatches, boolean prevMatchAvailable, boolean nextMatchAvailable) { prevMatchButton.setEnabled(prevMatchAvailable); nextMatchButton.setEnabled(nextMatchAvailable); } public void setControl(Control control) { this.control = control; } public void clearSearch() { setInfoLabel(""); searchField.setText(""); } public SearchParameters getSearchParameters() { SearchParameters searchParameters = new SearchParameters(); searchParameters.setMatchCase(matchCaseCB.isSelected()); searchParameters.setMatchMode(SearchParameters.MatchMode.fromBoolean(markAllCB.isSelected())); SearchParameters.SearchDirection searchDirection = control.getSearchDirection(); searchParameters.setSearchDirection(searchDirection); long startPosition; if (searchParameters.isSearchFromCursor()) { startPosition = hexCodeArea.getActiveCaretPosition().getDataPosition(); } else { switch (searchDirection) { case FORWARD: { startPosition = 0; break; } case BACKWARD: { startPosition = hexCodeArea.getDataSize() - 1; break; } default: throw CodeAreaUtils.getInvalidTypeException(searchDirection); } } searchParameters.setStartPosition(startPosition); searchParameters.setCondition(new SearchCondition(makeSearchCondition())); return searchParameters; } private SearchCondition makeSearchCondition() { SearchCondition condition = new SearchCondition(); if (findTypeCB.isSelected()) { condition.setSearchMode(SearchCondition.SearchMode.BINARY); } else { condition.setSearchMode(SearchCondition.SearchMode.TEXT); } if (!StringUtils.isEmpty(searchField.getText())) { if (condition.getSearchMode() == SearchCondition.SearchMode.TEXT) { condition.setSearchText(searchField.getText()); } else { String hexBytes = searchField.getText(); boolean isValidHexInput = HexUtils.isValidHexString(hexBytes); UiUtils.highlightAsErrorField(searchField, !isValidHexInput); if (isValidHexInput) { condition.setBinaryData(new ByteArrayEditableData(HexUtils.hexStringToByteArray(hexBytes))); } } } return condition; } public void updateFindStatus() { UiUtils.highlightAsErrorField(searchField, false); SearchCondition condition = makeSearchCondition(); if (condition.getSearchMode() == SearchCondition.SearchMode.TEXT) { findTypeCB.setSelected(false); findTypeCB.setToolTipText(NLS.str("search.find_type_text")); matchCaseCB.setEnabled(true); } else { makeFindByHexButton(); matchCaseCB.setEnabled(false); } } private void makeFindByHexButton() { findTypeCB.setSelected(true); findTypeCB.setToolTipText(NLS.str("search.find_type_hex")); } public interface Control { void prevMatch(); void nextMatch(); void performEscape(); void performFind(); /** * Parameters of search have changed. */ void notifySearchChanged(); /** * Parameters of search are changing which might not lead to immediate * search change. *

* Typically, text typing. */ void notifySearchChanging(); SearchParameters.SearchDirection getSearchDirection(); void close(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/BinarySearch.java ================================================ package jadx.gui.ui.hexviewer.search; /* * Copyright (C) ExBin Project * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * https://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import org.exbin.auxiliary.binary_data.EditableBinaryData; import org.exbin.auxiliary.binary_data.array.ByteArrayEditableData; import jadx.gui.ui.hexviewer.HexSearchBar; import jadx.gui.ui.hexviewer.search.service.BinarySearchService; import jadx.gui.utils.NLS; /** * Binary search. * * @author ExBin Project (https://exbin.org) */ public class BinarySearch { private static final int DEFAULT_DELAY = 500; private InvokeSearchThread invokeSearchThread; private SearchThread searchThread; private SearchOperation currentSearchOperation = SearchOperation.FIND; private SearchParameters.SearchDirection currentSearchDirection = SearchParameters.SearchDirection.FORWARD; private final SearchParameters currentSearchParameters = new SearchParameters(); private BinarySearchService.FoundMatches foundMatches = new BinarySearchService.FoundMatches(); private BinarySearchService binarySearchService; private final BinarySearchService.SearchStatusListener searchStatusListener; private HexSearchBar binarySearchPanel; public BinarySearch(HexSearchBar binarySearchPanel) { this.binarySearchPanel = binarySearchPanel; searchStatusListener = new BinarySearchService.SearchStatusListener() { @Override public void setStatus(BinarySearchService.FoundMatches foundMatches, SearchParameters.MatchMode matchMode) { BinarySearch.this.foundMatches = foundMatches; switch (foundMatches.getMatchesCount()) { case 0: binarySearchPanel.setInfoLabel(NLS.str("search.match_not_found")); break; case 1: binarySearchPanel.setInfoLabel( matchMode == SearchParameters.MatchMode.MULTIPLE ? NLS.str("search.single_match") : NLS.str("search.match_found")); break; default: binarySearchPanel.setInfoLabel(String.format(NLS.str("search.match_of"), foundMatches.getMatchPosition() + 1, foundMatches.getMatchesCount())); break; } updateMatchStatus(); } @Override public void clearStatus() { binarySearchPanel.setInfoLabel(""); BinarySearch.this.foundMatches = new BinarySearchService.FoundMatches(); updateMatchStatus(); } private void updateMatchStatus() { int matchesCount = foundMatches.getMatchesCount(); int matchPosition = foundMatches.getMatchPosition(); binarySearchPanel.updateMatchCount(matchesCount > 0, matchesCount > 1 && matchPosition > 0, matchPosition < matchesCount - 1); } }; binarySearchPanel.setControl(new HexSearchBar.Control() { @Override public void prevMatch() { foundMatches.prev(); binarySearchService.setMatchPosition(foundMatches.getMatchPosition()); searchStatusListener.setStatus(foundMatches, binarySearchService.getLastSearchParameters().getMatchMode()); } @Override public void nextMatch() { foundMatches.next(); binarySearchService.setMatchPosition(foundMatches.getMatchPosition()); searchStatusListener.setStatus(foundMatches, binarySearchService.getLastSearchParameters().getMatchMode()); } @Override public void performEscape() { cancelSearch(); close(); clearSearch(); } @Override public void performFind() { invokeSearch(SearchOperation.FIND); } @Override public void notifySearchChanged() { if (currentSearchOperation == SearchOperation.FIND) { invokeSearch(SearchOperation.FIND); } } @Override public void notifySearchChanging() { if (currentSearchOperation != SearchOperation.FIND) { return; } SearchCondition condition = currentSearchParameters.getCondition(); SearchCondition updatedSearchCondition = binarySearchPanel.getSearchParameters().getCondition(); switch (updatedSearchCondition.getSearchMode()) { case TEXT: { String searchText = updatedSearchCondition.getSearchText(); if (searchText.isEmpty()) { condition.setSearchText(searchText); clearSearch(); return; } if (searchText.equals(condition.getSearchText())) { return; } condition.setSearchText(searchText); break; } case BINARY: { EditableBinaryData searchData = (EditableBinaryData) updatedSearchCondition.getBinaryData(); if (searchData == null || searchData.isEmpty()) { condition.setBinaryData(null); clearSearch(); return; } if (searchData.equals(condition.getBinaryData())) { return; } ByteArrayEditableData data = new ByteArrayEditableData(); data.insert(0, searchData); condition.setBinaryData(data); break; } } BinarySearch.this.invokeSearch(SearchOperation.FIND, DEFAULT_DELAY); } @Override public SearchParameters.SearchDirection getSearchDirection() { return currentSearchDirection; } @Override public void close() { cancelSearch(); clearSearch(); } }); } public void setBinarySearchService(BinarySearchService binarySearchService) { this.binarySearchService = binarySearchService; } public void setTargetComponent(HexSearchBar targetComponent) { binarySearchPanel = targetComponent; } public BinarySearchService.SearchStatusListener getSearchStatusListener() { return searchStatusListener; } private void invokeSearch(SearchOperation searchOperation) { invokeSearch(searchOperation, binarySearchPanel.getSearchParameters(), 0); } private void invokeSearch(SearchOperation searchOperation, final int delay) { invokeSearch(searchOperation, binarySearchPanel.getSearchParameters(), delay); } private void invokeSearch(SearchOperation searchOperation, SearchParameters searchParameters) { invokeSearch(searchOperation, searchParameters, 0); } private void invokeSearch(SearchOperation searchOperation, SearchParameters searchParameters, final int delay) { if (invokeSearchThread != null) { invokeSearchThread.interrupt(); } invokeSearchThread = new InvokeSearchThread(); invokeSearchThread.delay = delay; currentSearchOperation = searchOperation; currentSearchParameters.setFromParameters(searchParameters); invokeSearchThread.start(); } public void cancelSearch() { if (invokeSearchThread != null) { invokeSearchThread.interrupt(); } if (searchThread != null) { searchThread.interrupt(); } } public void clearSearch() { SearchCondition condition = currentSearchParameters.getCondition(); condition.clear(); binarySearchPanel.clearSearch(); binarySearchService.clearMatches(); searchStatusListener.clearStatus(); } public HexSearchBar getPanel() { return binarySearchPanel; } public void dataChanged() { binarySearchService.clearMatches(); invokeSearch(currentSearchOperation, DEFAULT_DELAY); } private class InvokeSearchThread extends Thread { private int delay = DEFAULT_DELAY; public InvokeSearchThread() { super("InvokeSearchThread"); } @Override public void run() { try { Thread.sleep(delay); if (searchThread != null) { searchThread.interrupt(); } searchThread = new SearchThread(); searchThread.start(); } catch (InterruptedException ex) { // don't search } } } private class SearchThread extends Thread { public SearchThread() { super("SearchThread"); } @Override public void run() { switch (currentSearchOperation) { case FIND: binarySearchService.performFind(currentSearchParameters, searchStatusListener); break; case FIND_AGAIN: binarySearchService.performFindAgain(searchStatusListener); break; default: throw new UnsupportedOperationException("Not supported yet."); } } } private enum SearchOperation { FIND, FIND_AGAIN } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/SearchCondition.java ================================================ package jadx.gui.ui.hexviewer.search; import java.util.Objects; import org.exbin.auxiliary.binary_data.BinaryData; import org.exbin.auxiliary.binary_data.EditableBinaryData; import org.exbin.auxiliary.binary_data.array.ByteArrayEditableData; import org.exbin.bined.CodeAreaUtils; /** * Parameters for action to search for occurrences of text or data. * * @author ExBin Project (https://exbin.org) */ public class SearchCondition { private SearchMode searchMode = SearchMode.TEXT; private String searchText = ""; private EditableBinaryData binaryData; public SearchCondition() { } /** * This is copy constructor. * * @param source source condition */ public SearchCondition(SearchCondition source) { searchMode = source.getSearchMode(); searchText = source.getSearchText(); binaryData = new ByteArrayEditableData(); if (source.getBinaryData() != null) { binaryData.insert(0, source.getBinaryData()); } } public SearchMode getSearchMode() { return searchMode; } public void setSearchMode(SearchMode searchMode) { this.searchMode = searchMode; } public String getSearchText() { return searchText; } public void setSearchText(String searchText) { this.searchText = searchText; } public BinaryData getBinaryData() { return binaryData; } public void setBinaryData(EditableBinaryData binaryData) { this.binaryData = binaryData; } public boolean isEmpty() { switch (searchMode) { case TEXT: { return searchText == null || searchText.isEmpty(); } case BINARY: { return binaryData == null || binaryData.isEmpty(); } default: throw CodeAreaUtils.getInvalidTypeException(searchMode); } } @Override public int hashCode() { int hash = 3; return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final SearchCondition other = (SearchCondition) obj; if (this.searchMode != other.searchMode) { return false; } if (searchMode == SearchMode.TEXT) { return Objects.equals(this.searchText, other.searchText); } else { return Objects.equals(this.binaryData, other.binaryData); } } public void clear() { searchText = ""; if (binaryData != null) { binaryData.clear(); } } public enum SearchMode { TEXT, BINARY } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/SearchParameters.java ================================================ package jadx.gui.ui.hexviewer.search; /* * Copyright (C) ExBin Project * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * https://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Parameters for action to search for occurrences of text or data. * * @author ExBin Project (https://exbin.org) */ public class SearchParameters { private SearchCondition condition = new SearchCondition(); private long startPosition; private boolean searchFromCursor; private boolean matchCase = true; private MatchMode matchMode = MatchMode.MULTIPLE; private SearchDirection searchDirection = SearchDirection.FORWARD; public SearchParameters() { } public SearchCondition getCondition() { return condition; } public void setCondition(SearchCondition condition) { this.condition = condition; } public long getStartPosition() { return startPosition; } public void setStartPosition(long startPosition) { this.startPosition = startPosition; } public boolean isSearchFromCursor() { return searchFromCursor; } public void setSearchFromCursor(boolean searchFromCursor) { this.searchFromCursor = searchFromCursor; } public boolean isMatchCase() { return matchCase; } public void setMatchCase(boolean matchCase) { this.matchCase = matchCase; } public MatchMode getMatchMode() { return matchMode; } public void setMatchMode(MatchMode matchMode) { this.matchMode = matchMode; } public SearchDirection getSearchDirection() { return searchDirection; } public void setSearchDirection(SearchDirection searchDirection) { this.searchDirection = searchDirection; } public void setFromParameters(SearchParameters searchParameters) { condition = searchParameters.getCondition(); startPosition = searchParameters.getStartPosition(); searchFromCursor = searchParameters.isSearchFromCursor(); matchCase = searchParameters.isMatchCase(); matchMode = searchParameters.getMatchMode(); searchDirection = searchParameters.getSearchDirection(); } public enum SearchDirection { FORWARD, BACKWARD } public enum MatchMode { SINGLE, MULTIPLE; public static MatchMode fromBoolean(boolean multipleMatches) { return multipleMatches ? MULTIPLE : SINGLE; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/service/BinarySearchService.java ================================================ package jadx.gui.ui.hexviewer.search.service; import jadx.gui.ui.hexviewer.search.SearchParameters; public interface BinarySearchService { void performFind(SearchParameters dialogSearchParameters, SearchStatusListener searchStatusListener); void setMatchPosition(int matchPosition); void performFindAgain(SearchStatusListener searchStatusListener); SearchParameters getLastSearchParameters(); void clearMatches(); public interface SearchStatusListener { void setStatus(FoundMatches foundMatches, SearchParameters.MatchMode matchMode); void clearStatus(); } public static class FoundMatches { private int matchesCount; private int matchPosition; public FoundMatches() { matchesCount = 0; matchPosition = -1; } public FoundMatches(int matchesCount, int matchPosition) { if (matchPosition >= matchesCount) { throw new IllegalStateException("Match position is out of range"); } this.matchesCount = matchesCount; this.matchPosition = matchPosition; } public int getMatchesCount() { return matchesCount; } public int getMatchPosition() { return matchPosition; } public void setMatchesCount(int matchesCount) { this.matchesCount = matchesCount; } public void setMatchPosition(int matchPosition) { this.matchPosition = matchPosition; } public void next() { if (matchPosition == matchesCount - 1) { throw new IllegalStateException("Cannot find next on last match"); } matchPosition++; } public void prev() { if (matchPosition == 0) { throw new IllegalStateException("Cannot find previous on first match"); } matchPosition--; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/hexviewer/search/service/BinarySearchServiceImpl.java ================================================ package jadx.gui.ui.hexviewer.search.service; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import org.exbin.auxiliary.binary_data.BinaryData; import org.exbin.bined.CharsetStreamTranslator; import org.exbin.bined.CodeAreaUtils; import org.exbin.bined.highlight.swing.SearchCodeAreaColorAssessor; import org.exbin.bined.highlight.swing.SearchMatch; import org.exbin.bined.swing.CodeAreaSwingUtils; import org.exbin.bined.swing.capability.ColorAssessorPainterCapable; import org.exbin.bined.swing.section.SectCodeArea; import jadx.gui.ui.hexviewer.search.SearchCondition; import jadx.gui.ui.hexviewer.search.SearchParameters; /** * Binary search service. * * @author ExBin Project (https://exbin.org) */ public class BinarySearchServiceImpl implements BinarySearchService { private static final int MAX_MATCHES_COUNT = 100; private final SectCodeArea codeArea; private final SearchParameters lastSearchParameters = new SearchParameters(); public BinarySearchServiceImpl(SectCodeArea codeArea) { this.codeArea = codeArea; } @Override public void performFind(SearchParameters searchParameters, SearchStatusListener searchStatusListener) { SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); SearchCondition condition = searchParameters.getCondition(); searchStatusListener.clearStatus(); if (condition.isEmpty()) { searchAssessor.clearMatches(); codeArea.repaint(); return; } long position; switch (searchParameters.getSearchDirection()) { case FORWARD: { if (searchParameters.isSearchFromCursor()) { position = codeArea.getActiveCaretPosition().getDataPosition(); } else { position = 0; } break; } case BACKWARD: { if (searchParameters.isSearchFromCursor()) { position = codeArea.getActiveCaretPosition().getDataPosition() - 1; } else { long searchDataSize; switch (condition.getSearchMode()) { case TEXT: { searchDataSize = condition.getSearchText().length(); break; } case BINARY: { searchDataSize = condition.getBinaryData().getDataSize(); break; } default: throw CodeAreaUtils.getInvalidTypeException(condition.getSearchMode()); } position = codeArea.getDataSize() - searchDataSize; } break; } default: throw CodeAreaUtils.getInvalidTypeException(searchParameters.getSearchDirection()); } searchParameters.setStartPosition(position); switch (condition.getSearchMode()) { case TEXT: { searchForText(searchParameters, searchStatusListener); break; } case BINARY: { searchForBinaryData(searchParameters, searchStatusListener); break; } default: throw CodeAreaUtils.getInvalidTypeException(condition.getSearchMode()); } } /** * Performs search by binary data. */ private void searchForBinaryData(SearchParameters searchParameters, SearchStatusListener searchStatusListener) { SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); SearchCondition condition = searchParameters.getCondition(); long position = searchParameters.getStartPosition(); BinaryData searchData = condition.getBinaryData(); long searchDataSize = searchData.getDataSize(); BinaryData data = codeArea.getContentData(); List foundMatches = new ArrayList<>(); long dataSize = data.getDataSize(); while (position >= 0 && position <= dataSize - searchDataSize) { long matchLength = 0; while (matchLength < searchDataSize) { if (data.getByte(position + matchLength) != searchData.getByte(matchLength)) { break; } matchLength++; } if (matchLength == searchDataSize) { SearchMatch match = new SearchMatch(); match.setPosition(position); match.setLength(searchDataSize); if (searchParameters.getSearchDirection() == SearchParameters.SearchDirection.BACKWARD) { foundMatches.add(0, match); } else { foundMatches.add(match); } if (foundMatches.size() == MAX_MATCHES_COUNT || searchParameters.getMatchMode() == SearchParameters.MatchMode.SINGLE) { break; } } position++; } searchAssessor.setMatches(foundMatches); if (!foundMatches.isEmpty()) { if (searchParameters.getSearchDirection() == SearchParameters.SearchDirection.BACKWARD) { searchAssessor.setCurrentMatchIndex(foundMatches.size() - 1); } else { searchAssessor.setCurrentMatchIndex(0); } SearchMatch firstMatch = Objects.requireNonNull(searchAssessor.getCurrentMatch()); codeArea.revealPosition(firstMatch.getPosition(), 0, codeArea.getActiveSection()); } lastSearchParameters.setFromParameters(searchParameters); searchStatusListener.setStatus( new FoundMatches(foundMatches.size(), foundMatches.isEmpty() ? -1 : searchAssessor.getCurrentMatchIndex()), searchParameters.getMatchMode()); codeArea.repaint(); } /** * Performs search by text/characters. */ private void searchForText(SearchParameters searchParameters, SearchStatusListener searchStatusListener) { SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); SearchCondition condition = searchParameters.getCondition(); long position = searchParameters.getStartPosition(); String findText; if (searchParameters.isMatchCase()) { findText = condition.getSearchText(); } else { findText = condition.getSearchText().toLowerCase(); } long searchDataSize = findText.length(); BinaryData data = codeArea.getContentData(); List foundMatches = new ArrayList<>(); Charset charset = codeArea.getCharset(); int maxBytesPerChar; try { CharsetEncoder encoder = charset.newEncoder(); maxBytesPerChar = (int) encoder.maxBytesPerChar(); } catch (UnsupportedOperationException ex) { maxBytesPerChar = CharsetStreamTranslator.DEFAULT_MAX_BYTES_PER_CHAR; } byte[] charData = new byte[maxBytesPerChar]; long dataSize = data.getDataSize(); long lastPosition = position; while (position >= 0 && position <= dataSize - searchDataSize) { int matchCharLength = 0; int matchLength = 0; while (matchCharLength < (int) searchDataSize) { if (Thread.interrupted()) { return; } long searchPosition = position + (long) matchLength; int bytesToUse = maxBytesPerChar; if (searchPosition + bytesToUse > dataSize) { bytesToUse = (int) (dataSize - searchPosition); } if (searchPosition == lastPosition + 1) { System.arraycopy(charData, 1, charData, 0, maxBytesPerChar - 1); charData[bytesToUse - 1] = data.getByte(searchPosition + bytesToUse - 1); } else if (searchPosition == lastPosition - 1) { System.arraycopy(charData, 0, charData, 1, maxBytesPerChar - 1); charData[0] = data.getByte(searchPosition); } else { data.copyToArray(searchPosition, charData, 0, bytesToUse); } if (bytesToUse < maxBytesPerChar) { Arrays.fill(charData, bytesToUse, maxBytesPerChar, (byte) 0); } lastPosition = searchPosition; char singleChar = new String(charData, charset).charAt(0); if (searchParameters.isMatchCase()) { if (singleChar != findText.charAt(matchCharLength)) { break; } } else if (Character.toLowerCase(singleChar) != findText.charAt(matchCharLength)) { break; } int characterLength = String.valueOf(singleChar).getBytes(charset).length; matchCharLength++; matchLength += characterLength; } if (matchCharLength == findText.length()) { SearchMatch match = new SearchMatch(); match.setPosition(position); match.setLength(matchLength); if (searchParameters.getSearchDirection() == SearchParameters.SearchDirection.BACKWARD) { foundMatches.add(0, match); } else { foundMatches.add(match); } if (foundMatches.size() == MAX_MATCHES_COUNT || searchParameters.getMatchMode() == SearchParameters.MatchMode.SINGLE) { break; } } switch (searchParameters.getSearchDirection()) { case FORWARD: { position++; break; } case BACKWARD: { position--; break; } default: throw CodeAreaUtils.getInvalidTypeException(searchParameters.getSearchDirection()); } } if (Thread.interrupted()) { return; } searchAssessor.setMatches(foundMatches); if (!foundMatches.isEmpty()) { if (searchParameters.getSearchDirection() == SearchParameters.SearchDirection.BACKWARD) { searchAssessor.setCurrentMatchIndex(foundMatches.size() - 1); } else { searchAssessor.setCurrentMatchIndex(0); } SearchMatch firstMatch = searchAssessor.getCurrentMatch(); codeArea.revealPosition(firstMatch.getPosition(), 0, codeArea.getActiveSection()); } lastSearchParameters.setFromParameters(searchParameters); searchStatusListener.setStatus( new FoundMatches(foundMatches.size(), foundMatches.isEmpty() ? -1 : searchAssessor.getCurrentMatchIndex()), searchParameters.getMatchMode()); codeArea.repaint(); } @Override public void setMatchPosition(int matchPosition) { SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); searchAssessor.setCurrentMatchIndex(matchPosition); SearchMatch currentMatch = searchAssessor.getCurrentMatch(); codeArea.revealPosition(currentMatch.getPosition(), 0, codeArea.getActiveSection()); codeArea.repaint(); } @Override public void performFindAgain(SearchStatusListener searchStatusListener) { SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); List foundMatches = searchAssessor.getMatches(); int matchesCount = foundMatches.size(); if (matchesCount > 0) { switch (lastSearchParameters.getMatchMode()) { case MULTIPLE: if (matchesCount > 1) { int currentMatchIndex = searchAssessor.getCurrentMatchIndex(); setMatchPosition(currentMatchIndex < matchesCount - 1 ? currentMatchIndex + 1 : 0); searchStatusListener.setStatus(new FoundMatches(foundMatches.size(), searchAssessor.getCurrentMatchIndex()), lastSearchParameters.getMatchMode()); } break; case SINGLE: switch (lastSearchParameters.getSearchDirection()) { case FORWARD: lastSearchParameters.setStartPosition(foundMatches.get(0).getPosition() + 1); break; case BACKWARD: SearchMatch match = foundMatches.get(0); lastSearchParameters.setStartPosition(match.getPosition() - 1); break; } SearchCondition condition = lastSearchParameters.getCondition(); switch (condition.getSearchMode()) { case TEXT: { searchForText(lastSearchParameters, searchStatusListener); break; } case BINARY: { searchForBinaryData(lastSearchParameters, searchStatusListener); break; } default: throw CodeAreaUtils.getInvalidTypeException(condition.getSearchMode()); } break; } } } @Override public SearchParameters getLastSearchParameters() { return lastSearchParameters; } @Override public void clearMatches() { SearchCodeAreaColorAssessor searchAssessor = CodeAreaSwingUtils .findColorAssessor((ColorAssessorPainterCapable) codeArea.getPainter(), SearchCodeAreaColorAssessor.class); searchAssessor.clearMatches(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/menu/HiddenMenuItem.java ================================================ package jadx.gui.ui.menu; import java.awt.Dimension; import java.awt.Graphics; import javax.swing.Action; import javax.swing.JMenuItem; public class HiddenMenuItem extends JMenuItem { public HiddenMenuItem(Action a) { super(a); } @Override protected void paintComponent(Graphics g) { } @Override public Dimension getPreferredSize() { return new Dimension(0, 0); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/menu/JadxMenu.java ================================================ package jadx.gui.ui.menu; import javax.swing.Action; import javax.swing.JComponent; import javax.swing.JMenu; import javax.swing.JMenuItem; import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.action.JadxGuiAction; import jadx.gui.utils.shortcut.Shortcut; import jadx.gui.utils.shortcut.ShortcutsController; public class JadxMenu extends JMenu { // fake component to fill action shortcut component property public static final JComponent JADX_MENU_COMPONENT = new JComponent() { @Override public String toString() { return "JADX_MENU_COMPONENT"; } }; private final ShortcutsController shortcutsController; public JadxMenu(String name, ShortcutsController shortcutsController) { super(name); this.shortcutsController = shortcutsController; } @Override public JMenuItem add(JMenuItem menuItem) { Action action = menuItem.getAction(); bindAction(action); return super.add(menuItem); } @Override public JMenuItem add(Action action) { bindAction(action); return super.add(action); } public void bindAction(Action action) { if (action instanceof JadxGuiAction) { JadxGuiAction guiAction = (JadxGuiAction) action; JComponent shortcutComponent = guiAction.getShortcutComponent(); if (shortcutComponent == null) { guiAction.setShortcutComponent(JADX_MENU_COMPONENT); } shortcutsController.bind(guiAction); } } public void reloadShortcuts() { for (int i = 0; i < getItemCount(); i++) { // TODO only repaint the items whose shortcut changed JMenuItem item = getItem(i); if (item == null) { continue; } Action action = item.getAction(); if (!(action instanceof JadxGuiAction) || ((JadxGuiAction) action).getActionModel() == null) { continue; } ActionModel actionModel = ((JadxGuiAction) action).getActionModel(); Shortcut shortcut = shortcutsController.get(actionModel); if (shortcut != null) { item.setAccelerator(shortcut.toKeyStroke()); } else { item.setAccelerator(null); } item.repaint(); item.revalidate(); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/menu/JadxMenuBar.java ================================================ package jadx.gui.ui.menu; import javax.swing.JMenu; import javax.swing.JMenuBar; public class JadxMenuBar extends JMenuBar { public void reloadShortcuts() { for (int i = 0; i < getMenuCount(); i++) { JMenu menu = getMenu(i); if (menu instanceof JadxMenu) { ((JadxMenu) menu).reloadShortcuts(); } } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java ================================================ package jadx.gui.ui.panel; import javax.swing.JPanel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.ui.tab.TabsController; public abstract class ContentPanel extends JPanel { private static final Logger LOG = LoggerFactory.getLogger(ContentPanel.class); private static final long serialVersionUID = 3237031760631677822L; protected TabbedPane tabbedPane; protected JNode node; protected ContentPanel(TabbedPane panel, JNode node) { tabbedPane = panel; this.node = node; } public abstract void loadSettings(); public TabbedPane getTabbedPane() { return tabbedPane; } public TabsController getTabsController() { return tabbedPane.getTabsController(); } public MainWindow getMainWindow() { return tabbedPane.getMainWindow(); } public JNode getNode() { return node; } public void scrollToPos(int pos) { LOG.warn("ContentPanel.scrollToPos method not implemented, class: {}", getClass().getSimpleName()); } public JadxSettings getSettings() { return tabbedPane.getMainWindow().getSettings(); } public boolean supportsQuickTabs() { return getNode().supportsQuickTabs(); } public void dispose() { tabbedPane = null; node = null; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/FontPanel.java ================================================ package jadx.gui.ui.panel; import java.awt.BorderLayout; import java.awt.Font; import java.awt.FontFormatException; import java.io.ByteArrayInputStream; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import jadx.api.ResourceFile; import jadx.api.ResourcesLoader; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.ResContainer; import jadx.gui.treemodel.JResource; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.NLS; public class FontPanel extends ContentPanel { private static final long serialVersionUID = 695370628262996993L; private static final String DEFAULT_PREVIEW_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n" + "abcdefghijklmnopqrstuvwxyz\n" + "1234567890!@#$%^&*()_-=+[]{}<,.>"; public FontPanel(TabbedPane panel, JResource res) { super(panel, res); setLayout(new BorderLayout()); RSyntaxTextArea textArea = AbstractCodeArea.getDefaultArea(panel.getMainWindow()); add(textArea, BorderLayout.CENTER); try { Font selectedFont = loadFont(res); if (selectedFont.canDisplay(DEFAULT_PREVIEW_STRING.codePointAt(0))) { textArea.setFont(selectedFont); textArea.setText(DEFAULT_PREVIEW_STRING); } else { textArea.setText(NLS.str("message.unable_preview_font")); } } catch (Exception e) { textArea.setText("Font load error:\n" + Utils.getStackTrace(e)); } } private Font loadFont(JResource res) { ResourceFile resFile = res.getResFile(); ResContainer resContainer = resFile.loadContent(); ResContainer.DataType dataType = resContainer.getDataType(); if (dataType == ResContainer.DataType.DECODED_DATA) { try { return Font.createFont(Font.TRUETYPE_FONT, new ByteArrayInputStream(resContainer.getDecodedData())).deriveFont(12f); } catch (Exception e) { throw new JadxRuntimeException("Failed to load font", e); } } else if (dataType == ResContainer.DataType.RES_LINK) { try { return ResourcesLoader.decodeStream(resFile, (size, is) -> { try { return Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(12f); } catch (FontFormatException e) { throw new JadxRuntimeException("Failed to load font", e); } }); } catch (Exception e) { throw new JadxRuntimeException("Failed to load font", e); } } else { throw new JadxRuntimeException("Unsupported resource font data type: " + resFile); } } @Override public void loadSettings() { // no op } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/HtmlPanel.java ================================================ package jadx.gui.ui.panel; import java.awt.BorderLayout; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import javax.swing.JEditorPane; import javax.swing.JScrollPane; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JNode; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.ui.ZoomActions; public final class HtmlPanel extends ContentPanel { private static final long serialVersionUID = -6251262855835426245L; private final JHtmlPane textArea; public HtmlPanel(TabbedPane panel, JNode jnode) { super(panel, jnode); setLayout(new BorderLayout()); textArea = new JHtmlPane(); loadSettings(); loadContent(jnode); textArea.setEditable(false); JScrollPane sp = new JScrollPane(textArea); add(sp); ZoomActions.register(textArea, panel.getMainWindow().getSettings(), this::loadSettings); } @Override public void loadSettings() { JadxSettings settings = getMainWindow().getSettings(); textArea.setFont(settings.getUiFont()); } public void loadContent(JNode jnode) { textArea.setText(jnode.getCodeInfo().getCodeStr()); textArea.setCaretPosition(0); // otherwise the start view will be the last line } public JEditorPane getHtmlArea() { return textArea; } private static final class JHtmlPane extends JEditorPane { private static final long serialVersionUID = 6886040384052136157L; public JHtmlPane() { setContentType("text/html"); } @Override public void paint(Graphics g) { Graphics2D g2d = (Graphics2D) g.create(); try { g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); super.paint(g2d); } finally { g2d.dispose(); } } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/IDebugController.java ================================================ package jadx.gui.ui.panel; import jadx.core.dex.instructions.args.ArgType; import jadx.gui.ui.panel.JDebuggerPanel.ValueTreeNode; public interface IDebugController { boolean startDebugger(JDebuggerPanel debuggerPanel, String adbHost, int adbPort, int androidVer); boolean run(); boolean stepOver(); boolean stepInto(); boolean stepOut(); boolean pause(); boolean stop(); boolean exit(); boolean isSuspended(); boolean isDebugging(); boolean modifyRegValue(ValueTreeNode node, ArgType type, Object val); String getProcessName(); void setStateListener(StateListener l); interface StateListener { void onStateChanged(boolean suspended, boolean stopped); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/IViewStateSupport.java ================================================ package jadx.gui.ui.panel; import jadx.gui.ui.codearea.EditorViewState; public interface IViewStateSupport { void saveEditorViewState(EditorViewState viewState); void restoreEditorViewState(EditorViewState viewState); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/ImagePanel.java ================================================ package jadx.gui.ui.panel; import java.awt.BorderLayout; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import javax.imageio.ImageIO; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import hu.kazocsaba.imageviewer.ImageViewer; import jadx.api.ResourceFile; import jadx.api.ResourcesLoader; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.ResContainer; import jadx.gui.treemodel.JResource; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.tab.TabbedPane; public class ImagePanel extends ContentPanel { private static final long serialVersionUID = 4071356367073142688L; public ImagePanel(TabbedPane panel, JResource res) { super(panel, res); setLayout(new BorderLayout()); try { BufferedImage img = loadImage(res); ImageViewer imageViewer = new ImageViewer(img); add(imageViewer.getComponent()); } catch (Exception e) { RSyntaxTextArea textArea = AbstractCodeArea.getDefaultArea(panel.getMainWindow()); textArea.setText("Image load error:\n" + Utils.getStackTrace(e)); add(textArea); } } private BufferedImage loadImage(JResource res) { ResourceFile resFile = res.getResFile(); ResContainer resContainer = resFile.loadContent(); ResContainer.DataType dataType = resContainer.getDataType(); if (dataType == ResContainer.DataType.DECODED_DATA) { try { return ImageIO.read(new ByteArrayInputStream(resContainer.getDecodedData())); } catch (Exception e) { throw new JadxRuntimeException("Failed to load image", e); } } else if (dataType == ResContainer.DataType.RES_LINK) { try { return ResourcesLoader.decodeStream(resFile, (size, is) -> ImageIO.read(is)); } catch (Exception e) { throw new JadxRuntimeException("Failed to load image", e); } } else { throw new JadxRuntimeException("Unsupported resource image data type: " + resFile); } } @Override public void loadSettings() { // no op } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/IssuesPanel.java ================================================ package jadx.gui.ui.panel; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JPanel; import ch.qos.logback.classic.Level; import jadx.gui.logs.IssuesListener; import jadx.gui.logs.LogCollector; import jadx.gui.logs.LogOptions; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class IssuesPanel extends JPanel { private static final long serialVersionUID = -7720576036668459218L; private static final ImageIcon ERROR_ICON = UiUtils.openSvgIcon("ui/error"); private static final ImageIcon WARN_ICON = UiUtils.openSvgIcon("ui/warning"); private final MainWindow mainWindow; private final IssuesListener issuesListener; private JLabel errorLabel; private JLabel warnLabel; public IssuesPanel(MainWindow mainWindow) { this.mainWindow = mainWindow; initUI(); this.issuesListener = new IssuesListener(this); LogCollector.getInstance().registerListener(issuesListener); } public int getErrorsCount() { return issuesListener.getErrors(); } private void initUI() { JLabel label = new JLabel(NLS.str("issues_panel.label")); errorLabel = new JLabel(ERROR_ICON); warnLabel = new JLabel(WARN_ICON); String toolTipText = NLS.str("issues_panel.tooltip"); errorLabel.setToolTipText(toolTipText); warnLabel.setToolTipText(toolTipText); errorLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { mainWindow.showLogViewer(LogOptions.allWithLevel(Level.ERROR)); } }); warnLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { mainWindow.showLogViewer(LogOptions.allWithLevel(Level.WARN)); } }); setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setVisible(false); add(label); add(Box.createHorizontalGlue()); add(errorLabel); add(Box.createHorizontalGlue()); add(warnLabel); } public void onUpdate(int error, int warnings) { if (error == 0 && warnings == 0) { setVisible(false); return; } setVisible(true); errorLabel.setText(NLS.str("issues_panel.errors", error)); errorLabel.setVisible(error != 0); warnLabel.setText(NLS.str("issues_panel.warnings", warnings)); warnLabel.setVisible(warnings != 0); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/JDebuggerPanel.java ================================================ package jadx.gui.ui.panel; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.KeyEventDispatcher; import java.awt.KeyboardFocusManager; import java.awt.Label; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Box; import javax.swing.DefaultComboBoxModel; import javax.swing.DefaultListModel; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JToolBar; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.StringUtils; import jadx.gui.device.debugger.DebugController; import jadx.gui.device.protocol.ADBDevice; import jadx.gui.treemodel.JClass; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.SmaliArea; import jadx.gui.ui.dialog.ADBDialog; import jadx.gui.ui.popupmenu.VarTreePopupMenu; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class JDebuggerPanel extends JPanel { private static final long serialVersionUID = -1111111202102181631L; private static final Logger LOG = LoggerFactory.getLogger(LogcatPanel.class); private static final ImageIcon ICON_RUN = UiUtils.openSvgIcon("debugger/execute"); private static final ImageIcon ICON_RERUN = UiUtils.openSvgIcon("debugger/rerun"); private static final ImageIcon ICON_PAUSE = UiUtils.openSvgIcon("debugger/threadFrozen"); private static final ImageIcon ICON_STOP = UiUtils.openSvgIcon("debugger/suspend"); private static final ImageIcon ICON_STOP_GRAY = UiUtils.openSvgIcon("debugger/suspendGray"); private static final ImageIcon ICON_STEP_INTO = UiUtils.openSvgIcon("debugger/traceInto"); private static final ImageIcon ICON_STEP_OVER = UiUtils.openSvgIcon("debugger/traceOver"); private static final ImageIcon ICON_STEP_OUT = UiUtils.openSvgIcon("debugger/stepOut"); private final transient MainWindow mainWindow; private final transient JList stackFrameList; private final transient JComboBox threadBox; private final transient JTextArea logger; private final transient JTree variableTree; private final transient DefaultTreeModel variableTreeModel; private final transient DefaultMutableTreeNode rootTreeNode; private final transient DefaultMutableTreeNode thisTreeNode; private final transient DefaultMutableTreeNode regTreeNode; private final transient JSplitPane rightSplitter; private final transient JSplitPane leftSplitter; private final transient IDebugController controller; private final LogcatPanel logcatPanel; private final transient VarTreePopupMenu varTreeMenu; private transient KeyEventDispatcher controllerShortCutDispatcher; public JDebuggerPanel(MainWindow mainWindow) { UiUtils.uiThreadGuard(); this.mainWindow = mainWindow; controller = new DebugController(); this.setLayout(new BorderLayout()); this.setMinimumSize(new Dimension(100, 150)); leftSplitter = new JSplitPane(); rightSplitter = new JSplitPane(); leftSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerStackFrameSplitterLoc()); rightSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerVarTreeSplitterLoc()); JPanel stackFramePanel = new JPanel(new BorderLayout()); threadBox = new JComboBox<>(); stackFrameList = new JList<>(); threadBox.setModel(new DefaultComboBoxModel<>()); stackFrameList.setModel(new DefaultListModel<>()); stackFramePanel.add(threadBox, BorderLayout.NORTH); stackFramePanel.add(new JScrollPane(stackFrameList), BorderLayout.CENTER); JPanel variablePanel = new JPanel(new CardLayout()); variableTree = new JTree(); variablePanel.add(new JScrollPane(variableTree)); rootTreeNode = new DefaultMutableTreeNode(); thisTreeNode = new DefaultMutableTreeNode("this"); regTreeNode = new DefaultMutableTreeNode("var"); rootTreeNode.add(thisTreeNode); rootTreeNode.add(regTreeNode); variableTreeModel = new DefaultTreeModel(rootTreeNode); variableTree.setModel(variableTreeModel); variableTree.expandPath(new TreePath(rootTreeNode.getPath())); variableTree.setCellRenderer(new DefaultTreeCellRenderer() { private static final long serialVersionUID = -1111111202103170725L; @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { Component c = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); if (value instanceof ValueTreeNode) { if (((ValueTreeNode) value).isUpdated()) { setForeground(Color.RED); } } return c; } }); varTreeMenu = new VarTreePopupMenu(mainWindow); JTabbedPane loggerPanel = new JTabbedPane(); logger = new JTextArea(); logger.setEditable(false); logger.setLineWrap(true); JScrollPane loggerScroll = new JScrollPane(logger); loggerPanel.addTab("Debugger Log", null, loggerScroll, null); this.logcatPanel = new LogcatPanel(this); loggerPanel.addTab(NLS.str("logcat.logcat"), null, logcatPanel, null); leftSplitter.setLeftComponent(stackFramePanel); leftSplitter.setRightComponent(rightSplitter); leftSplitter.setResizeWeight(MainWindow.SPLIT_PANE_RESIZE_WEIGHT); rightSplitter.setLeftComponent(variablePanel); rightSplitter.setRightComponent(loggerPanel); rightSplitter.setResizeWeight(MainWindow.SPLIT_PANE_RESIZE_WEIGHT); JPanel headerPanel = new JPanel(new BorderLayout()); headerPanel.add(new Label(), BorderLayout.WEST); headerPanel.add(initToolBar(), BorderLayout.CENTER); JButton closeBtn = new JButton(UiUtils.openSvgIcon("ui/close")); closeBtn.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (controller.isDebugging()) { int what = JOptionPane.showConfirmDialog(mainWindow, NLS.str("debugger.cfm_dialog_msg"), NLS.str("debugger.cfm_dialog_title"), JOptionPane.OK_CANCEL_OPTION); if (what == JOptionPane.OK_OPTION) { controller.exit(); logcatPanel.exit(); } else { return; } } else { mainWindow.destroyDebuggerPanel(); } unregShortcuts(); } }); headerPanel.add(closeBtn, BorderLayout.EAST); this.add(headerPanel, BorderLayout.NORTH); this.add(leftSplitter, BorderLayout.CENTER); listenUIEvents(); } public MainWindow getMainWindow() { return mainWindow; } private void listenUIEvents() { stackFrameList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() % 2 == 0) { stackFrameSelected(e.getPoint()); } } }); variableTree.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (SwingUtilities.isRightMouseButton(e)) { treeNodeRightClicked(e); } } }); } private JToolBar initToolBar() { AbstractAction stepOver = new AbstractAction(NLS.str("debugger.step_over"), ICON_STEP_OVER) { private static final long serialVersionUID = -1111111202103170726L; @Override public void actionPerformed(ActionEvent e) { controller.stepOver(); } }; stepOver.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.step_over")); AbstractAction stepInto = new AbstractAction(NLS.str("debugger.step_into"), ICON_STEP_INTO) { private static final long serialVersionUID = -1111111202103170727L; @Override public void actionPerformed(ActionEvent e) { controller.stepInto(); } }; stepInto.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.step_into")); AbstractAction stepOut = new AbstractAction(NLS.str("debugger.step_out"), ICON_STEP_OUT) { private static final long serialVersionUID = -1111111202103170728L; @Override public void actionPerformed(ActionEvent e) { controller.stepOut(); } }; stepOut.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.step_out")); AbstractAction stop = new AbstractAction(NLS.str("debugger.stop"), ICON_STOP_GRAY) { private static final long serialVersionUID = -1111111202103170728L; @Override public void actionPerformed(ActionEvent e) { controller.stop(); } }; stop.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.stop")); AbstractAction run = new AbstractAction(NLS.str("debugger.run"), ICON_RUN) { private static final long serialVersionUID = -1111111202103170728L; @Override public void actionPerformed(ActionEvent e) { if (controller.isDebugging()) { if (controller.isSuspended()) { controller.run(); } else { controller.pause(); } } } }; run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run")); AbstractAction rerun = new AbstractAction(NLS.str("debugger.rerun"), ICON_RERUN) { private static final long serialVersionUID = -1111111202103210433L; @Override public void actionPerformed(ActionEvent e) { if (controller.isDebugging()) { controller.stop(); } String pkgName = controller.getProcessName(); if (pkgName.isEmpty() || !ADBDialog.launchForDebugging(mainWindow, pkgName, true)) { (new ADBDialog(mainWindow)).setVisible(true); } } }; rerun.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.rerun")); controller.setStateListener(new DebugController.StateListener() { boolean isGray = true; @Override public void onStateChanged(boolean suspended, boolean stopped) { UiUtils.uiRun(() -> { if (!stopped) { if (isGray) { stop.putValue(Action.SMALL_ICON, ICON_STOP); } } else { stop.putValue(Action.SMALL_ICON, ICON_STOP_GRAY); run.putValue(Action.SMALL_ICON, ICON_RUN); run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run")); isGray = true; return; } if (suspended) { run.putValue(Action.SMALL_ICON, ICON_RUN); run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run")); } else { run.putValue(Action.SMALL_ICON, ICON_PAUSE); run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.pause")); } }); } }); JToolBar toolBar = new JToolBar(); toolBar.add(new Label()); toolBar.add(Box.createHorizontalGlue()); toolBar.add(rerun); toolBar.add(Box.createRigidArea(new Dimension(5, 0))); toolBar.add(stop); toolBar.add(Box.createRigidArea(new Dimension(5, 0))); toolBar.add(run); toolBar.add(Box.createRigidArea(new Dimension(5, 0))); toolBar.add(stepOver); toolBar.add(Box.createRigidArea(new Dimension(5, 0))); toolBar.add(stepInto); toolBar.add(Box.createRigidArea(new Dimension(5, 0))); toolBar.add(stepOut); toolBar.add(Box.createHorizontalGlue()); toolBar.add(new Label()); regShortcuts(); return toolBar; } private void unregShortcuts() { KeyboardFocusManager .getCurrentKeyboardFocusManager() .removeKeyEventDispatcher(controllerShortCutDispatcher); } private void regShortcuts() { controllerShortCutDispatcher = new KeyEventDispatcher() { @Override public boolean dispatchKeyEvent(KeyEvent e) { if (e.getID() == KeyEvent.KEY_PRESSED && mainWindow.getTabbedPane().getFocusedComp() instanceof SmaliArea) { if (e.getModifiersEx() == KeyEvent.SHIFT_DOWN_MASK && e.getKeyCode() == KeyEvent.VK_F8) { controller.stepOut(); return true; } switch (e.getKeyCode()) { case KeyEvent.VK_F7: controller.stepInto(); return true; case KeyEvent.VK_F8: controller.stepOver(); return true; case KeyEvent.VK_F9: controller.run(); return true; } } return false; } }; KeyboardFocusManager.getCurrentKeyboardFocusManager() .addKeyEventDispatcher(controllerShortCutDispatcher); } private void treeNodeRightClicked(MouseEvent e) { TreePath path = variableTree.getPathForLocation(e.getX(), e.getY()); if (path != null) { Object node = path.getLastPathComponent(); if (node instanceof ValueTreeNode) { varTreeMenu.show((ValueTreeNode) node, e.getComponent(), e.getX(), e.getY()); } } } private void stackFrameSelected(Point p) { int loc = stackFrameList.locationToIndex(p); if (loc > -1) { IListElement ele = stackFrameList.getModel().getElementAt(loc); if (ele != null) { ele.onSelected(); } } } public boolean showDebugger(String procName, String host, int port, int androidVer, ADBDevice device, String pid) { boolean ok = controller.startDebugger(this, host, port, androidVer); if (ok) { UiUtils.uiRun(() -> { log(String.format("Attached %s %s:%d", procName, host, port)); try { logcatPanel.init(device, pid); } catch (Exception e) { log(NLS.str("logcat.error_fail_start")); LOG.error("Logcat failed to start", e); } leftSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerStackFrameSplitterLoc()); rightSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerVarTreeSplitterLoc()); mainWindow.showDebuggerPanel(); }); } return ok; } public IDebugController getDbgController() { return controller; } public int getLeftSplitterLocation() { return leftSplitter.getDividerLocation(); } public int getRightSplitterLocation() { return rightSplitter.getDividerLocation(); } public void loadSettings() { UiUtils.uiThreadGuard(); Font font = mainWindow.getSettings().getCodeFont(); variableTree.setFont(font.deriveFont(font.getSize() + 1.f)); variableTree.setRowHeight(-1); stackFrameList.setFont(font); threadBox.setFont(font); logger.setFont(font); } public void resetUI() { UiUtils.uiThreadGuard(); thisTreeNode.removeAllChildren(); regTreeNode.removeAllChildren(); clearFrameAndThreadList(); threadBox.updateUI(); stackFrameList.updateUI(); variableTreeModel.reload(rootTreeNode); variableTree.expandPath(new TreePath(rootTreeNode.getPath())); logger.setText(""); } public void scrollToSmaliLine(JClass cls, int pos, boolean debugMode) { SwingUtilities.invokeLater(() -> getMainWindow().getTabsController().smaliJump(cls, pos, debugMode)); } public void resetAllDebuggingInfo() { UiUtils.uiThreadGuard(); clearFrameAndThreadList(); resetRegTreeNodes(); resetThisTreeNodes(); } public void resetThisTreeNodes() { thisTreeNode.removeAllChildren(); SwingUtilities.invokeLater(() -> variableTreeModel.reload(thisTreeNode)); } public void resetRegTreeNodes() { regTreeNode.removeAllChildren(); SwingUtilities.invokeLater(() -> variableTreeModel.reload(regTreeNode)); } public void updateRegTreeNodes(List nodes) { nodes.forEach(regTreeNode::add); } public void updateThisFieldNodes(List nodes) { nodes.forEach(thisTreeNode::add); } public void refreshThreadBox(List elements) { UiUtils.uiRun(() -> { if (!elements.isEmpty()) { DefaultComboBoxModel model = (DefaultComboBoxModel) threadBox.getModel(); elements.forEach(model::addElement); } threadBox.updateUI(); stackFrameList.setFont(mainWindow.getSettings().getCodeFont()); }); } public void refreshStackFrameList(List elements) { UiUtils.uiRun(() -> { if (!elements.isEmpty()) { DefaultListModel model = (DefaultListModel) stackFrameList.getModel(); model.addAll(elements); stackFrameList.setFont(mainWindow.getSettings().getCodeFont()); } stackFrameList.repaint(); }); } public void refreshRegisterTree() { SwingUtilities.invokeLater(() -> { variableTreeModel.reload(regTreeNode); variableTree.expandPath(new TreePath(regTreeNode.getPath())); }); } public void refreshThisFieldTree() { SwingUtilities.invokeLater(() -> { boolean expanded = variableTree.isExpanded(new TreePath(thisTreeNode.getPath())); variableTreeModel.reload(thisTreeNode); if (expanded) { variableTree.expandPath(new TreePath(regTreeNode.getPath())); } }); } public void clearFrameAndThreadList() { ((DefaultListModel) stackFrameList.getModel()).removeAllElements(); ((DefaultComboBoxModel) threadBox.getModel()).removeAllElements(); } public void log(String msg) { StringBuilder sb = new StringBuilder(); sb.append(" > ") .append(StringUtils.getDateText()) .append(" ") .append(msg) .append("\n"); SwingUtilities.invokeLater(() -> { logger.append(sb.toString()); }); } public void updateRegTree(ValueTreeNode node) { SwingUtilities.invokeLater(() -> { variableTreeModel.reload(regTreeNode); scrollToUpdatedNode(node); }); } public void updateThisTree(ValueTreeNode node) { SwingUtilities.invokeLater(() -> { variableTreeModel.reload(thisTreeNode); scrollToUpdatedNode(node); }); } public void scrollToUpdatedNode(ValueTreeNode node) { SwingUtilities.invokeLater(() -> { TreeNode[] path = node.getPath(); variableTree.scrollPathToVisible(new TreePath(path)); }); } public abstract static class ValueTreeNode extends DefaultMutableTreeNode { private static final long serialVersionUID = -1111111202103122236L; private boolean updated; public void setUpdated(boolean updated) { this.updated = updated; } public boolean isUpdated() { return updated; } public abstract String getName(); @Nullable public abstract String getValue(); @Nullable public abstract String getType(); public abstract long getTypeID(); public abstract ValueTreeNode updateValue(String val); public abstract ValueTreeNode updateType(String val); public abstract ValueTreeNode updateTypeID(long id); @Override public String toString() { StringBuilder sb = new StringBuilder(64); sb.append(getName()); String val = getValue(); if (val != null) { sb.append(" val: ").append(val).append(","); } String type = getType(); if (type != null) { sb.append(" type: ").append(getType()); long id = getTypeID(); if (id > 0) { sb.append("@").append(id); } } if (val == null && type == null) { sb.append(" undefined"); } return sb.toString(); } } public interface IListElement { void onSelected(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/LogcatPanel.java ================================================ package jadx.gui.ui.panel; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BoundedRangeModel; import javax.swing.Box; import javax.swing.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JTextPane; import javax.swing.JToolBar; import javax.swing.ListCellRenderer; import javax.swing.text.AttributeSet; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.StyleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.device.debugger.LogcatController; import jadx.gui.device.protocol.ADB; import jadx.gui.device.protocol.ADBDevice; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.NodeLabel; public class LogcatPanel extends JPanel { private static final Logger LOG = LoggerFactory.getLogger(LogcatPanel.class); StyleContext sc = StyleContext.getDefaultStyleContext(); private final AttributeSet defaultAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#6c71c4")); private final AttributeSet verboseAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#2aa198")); private final AttributeSet debugAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#859900")); private final AttributeSet infoAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#586e75")); private final AttributeSet warningAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#b58900")); private final AttributeSet errorAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#dc322f")); private final AttributeSet fatalAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#d33682")); private final AttributeSet silentAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#002b36")); private final Map asetMap = Map.of((byte) 1, defaultAset, (byte) 2, verboseAset, (byte) 3, debugAset, (byte) 4, infoAset, (byte) 5, warningAset, (byte) 6, errorAset, (byte) 7, fatalAset, (byte) 8, silentAset); private static final ImageIcon ICON_PAUSE = UiUtils.openSvgIcon("debugger/threadFrozen"); private static final ImageIcon ICON_RUN = UiUtils.openSvgIcon("debugger/execute"); private static final ImageIcon CLEAR_LOGCAT = UiUtils.openSvgIcon("debugger/trash"); private transient JTextPane logcatPane; private final transient JDebuggerPanel debugPanel; private LogcatController logcatController; private boolean ready = false; private List procs; public LogcatPanel(JDebuggerPanel debugPanel) { this.debugPanel = debugPanel; } private List pids; private JScrollPane logcatScroll; private int pid; private final AbstractAction pauseButton = new AbstractAction(NLS.str("logcat.pause"), ICON_PAUSE) { @Override public void actionPerformed(ActionEvent e) { toggleLogcat(); } }; private final AbstractAction clearButton = new AbstractAction(NLS.str("logcat.clear"), CLEAR_LOGCAT) { @Override public void actionPerformed(ActionEvent e) { clearLogcat(); } }; public boolean showLogcat() { this.removeAll(); List pkgs = new ArrayList<>(); pids = new ArrayList<>(); JPanel procBox; for (ADB.Process proc : procs.subList(1, procs.size())) { // skipping first element because it contains the column label pkgs.add(String.format("[pid: %-6s] %s", proc.pid, proc.name)); pids.add(Integer.valueOf(proc.pid)); } String[] msgTypes = { NLS.str("logcat.default"), NLS.str("logcat.verbose"), NLS.str("logcat.debug"), NLS.str("logcat.info"), NLS.str("logcat.warn"), NLS.str("logcat.error"), NLS.str("logcat.fatal"), NLS.str("logcat.silent") }; Integer[] msgIndex = { 1, 2, 3, 4, 5, 6, 7, 8 }; this.setLayout(new BorderLayout()); logcatPane = new JTextPane(); logcatPane.setEditable(false); logcatScroll = new JScrollPane(logcatPane); JToolBar menuPanel = new JToolBar(); CheckCombo procObj = new CheckCombo(NLS.str("logcat.process"), 1, pids.toArray(new Integer[0]), pkgs.toArray(new String[0])); procBox = procObj.getContent(); procObj.selectAllBut(this.pids.indexOf(this.pid)); JPanel msgTypeBox = new CheckCombo(NLS.str("logcat.level"), 2, msgIndex, msgTypes).getContent(); menuPanel.add(procBox); menuPanel.add(Box.createRigidArea(new Dimension(5, 0))); menuPanel.add(msgTypeBox); menuPanel.add(Box.createRigidArea(new Dimension(5, 0))); menuPanel.add(pauseButton); menuPanel.add(Box.createRigidArea(new Dimension(5, 0))); menuPanel.add(clearButton); this.add(menuPanel, BorderLayout.NORTH); this.add(logcatScroll, BorderLayout.CENTER); return true; } public boolean clearLogcatArea() { logcatPane.setText(""); return true; } public boolean init(ADBDevice device, String pid) { this.pid = Integer.parseInt(pid); try { this.logcatController = new LogcatController(this, device); this.procs = device.getProcessList(); if (!this.showLogcat()) { debugPanel.log(NLS.str("logcat.error_fail_start")); } } catch (Exception e) { this.ready = false; LOG.error("Failed to start logcat", e); return false; } this.ready = true; return true; } private void toggleLogcat() { if (Objects.equals(this.logcatController.getStatus(), "running")) { this.logcatController.stopLogcat(); this.pauseButton.putValue(Action.SMALL_ICON, ICON_RUN); this.pauseButton.putValue(Action.NAME, NLS.str("logcat.start")); } else if (Objects.equals(this.logcatController.getStatus(), "stopped")) { this.logcatController.startLogcat(); this.pauseButton.putValue(Action.SMALL_ICON, ICON_PAUSE); this.pauseButton.putValue(Action.NAME, NLS.str("logcat.pause")); } } private void clearLogcat() { boolean running = false; if (Objects.equals(this.logcatController.getStatus(), "running")) { this.logcatController.stopLogcat(); running = true; } this.logcatController.clearLogcat(); clearLogcatArea(); this.debugPanel.log(this.logcatController.getStatus()); if (running) { this.logcatController.startLogcat(); } } public boolean isReady() { return this.ready; } private boolean isAtBottom(JScrollBar scrollbar) { BoundedRangeModel model = scrollbar.getModel(); return (model.getExtent() + model.getValue()) == model.getMaximum(); } public void log(LogcatController.LogcatInfo logcatInfo) { int len = logcatPane.getDocument().getLength(); JScrollBar scrollbar = logcatScroll.getVerticalScrollBar(); boolean atBottom = isAtBottom(scrollbar); String logString = " > " + logcatInfo.getTimestamp() + " [pid: " + logcatInfo.getPid() + "] " + logcatInfo.getMsgTypeString() + ": " + logcatInfo.getMsg() + "\n"; if (logcatInfo.getMsgType() == 0) { return; // ignore unknown } AttributeSet attrSet = asetMap.get(logcatInfo.getMsgType()); UiUtils.uiRun(() -> { try { logcatPane.getDocument().insertString(len, logString, attrSet); } catch (Exception e) { LOG.error("Failed to add logcat message", e); } if (atBottom) { EventQueue.invokeLater(() -> scrollbar.setValue(scrollbar.getMaximum())); } }); } public void exit() { logcatController.exit(); clearLogcatArea(); this.logcatController.clearEvents(); } class CheckCombo implements ActionListener { private final String[] ids; private final int type; private final String label; private final Integer[] index; private JComboBox combo; public CheckCombo(String label, int type, Integer[] index, String[] ids) { this.ids = ids; this.type = type; this.label = label; this.index = index; } public void actionPerformed(ActionEvent e) { JComboBox cb = (JComboBox) e.getSource(); CheckComboStore store = (CheckComboStore) cb.getSelectedItem(); CheckComboRenderer ccr = (CheckComboRenderer) cb.getRenderer(); store.state = !store.state; ccr.checkBox.setSelected(store.state); switch (this.type) { case 1: // process logcatController.getFilter().togglePid(store.index, store.state); logcatController.reload(); break; case 2: // label logcatController.getFilter().toggleMsgType((byte) store.index, store.state); logcatController.reload(); break; default: LOG.error("Invalid Logcat Filter Type"); break; } } public JPanel getContent() { JLabel label = NodeLabel.noHtml(this.label + ": "); CheckComboStore[] stores = new CheckComboStore[ids.length]; for (int j = 0; j < ids.length; j++) { stores[j] = new CheckComboStore(index[j], ids[j], Boolean.TRUE); } combo = new JComboBox<>(stores); combo.setRenderer(new CheckComboRenderer()); JPanel panel = new JPanel(); panel.setLayout(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); c.weightx = 0; c.gridwidth = 1; c.insets = new Insets(0, 1, 0, 1); panel.add(label, c); c.weightx = 1; c.gridwidth = GridBagConstraints.REMAINDER; c.anchor = GridBagConstraints.WEST; c.insets = new Insets(0, 1, 0, 1); panel.add(combo, c); combo.addActionListener(this); combo.addMouseListener(new FilterClickListener(this)); return panel; } public void toggleAll(boolean checked) { for (int i = 0; i < combo.getItemCount(); i++) { CheckComboStore ccs = combo.getItemAt(i); ccs.state = checked; switch (type) { case 1: // process logcatController.getFilter().togglePid(ccs.index, checked); break; case 2: // level logcatController.getFilter().toggleMsgType((byte) ccs.index, checked); break; default: LOG.error("Invalid Logcat Toggle Filter Encountered"); break; } } logcatController.reload(); } public void selectAllBut(int ind) { for (int i = 0; i < combo.getItemCount(); i++) { CheckComboStore ccs = combo.getItemAt(i); ccs.state = (i == ind); switch (type) { case 1: // process logcatController.getFilter().togglePid(ccs.index, ccs.state); break; case 2: // level logcatController.getFilter().toggleMsgType((byte) ccs.index, ccs.state); break; default: LOG.error("Invalid Logcat selectAllBut filter encountered"); break; } } logcatController.reload(); } } private static class CheckComboRenderer implements ListCellRenderer { private final JCheckBox checkBox = new JCheckBox(); public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { CheckComboStore store = (CheckComboStore) value; checkBox.setText(store.id); checkBox.setSelected(store.state); return checkBox; } } static class CheckComboStore { String id; Boolean state; int index; public CheckComboStore(int index, String id, Boolean state) { this.id = id; this.state = state; this.index = index; } } class FilterClickListener extends MouseAdapter { CheckCombo combo; public FilterClickListener(CheckCombo combo) { this.combo = combo; } public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { doPop(e); } } public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { doPop(e); } } private void doPop(MouseEvent e) { FilterPopup menu = new FilterPopup(combo); menu.show(e.getComponent(), e.getX(), e.getY()); } } class FilterPopup extends JPopupMenu { CheckCombo combo; JMenuItem selectAll; JMenuItem unselectAll; JMenuItem selectAttached; public FilterPopup(CheckCombo combo) { this.combo = combo; selectAll = new JMenuItem(NLS.str("logcat.select_all")); selectAll.addActionListener(actionEvent -> combo.toggleAll(true)); unselectAll = new JMenuItem(NLS.str("logcat.unselect_all")); unselectAll.addActionListener(actionEvent -> combo.toggleAll(false)); if (combo.type == 1) { selectAttached = new JMenuItem(NLS.str("logcat.select_attached")); selectAttached.addActionListener(actionEvent -> combo.selectAllBut(pids.indexOf(pid))); add(selectAttached); } add(selectAll); add(unselectAll); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/ProgressPanel.java ================================================ package jadx.gui.ui.panel; import java.awt.Dimension; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JProgressBar; import jadx.gui.jobs.ITaskProgress; import jadx.gui.ui.MainWindow; import jadx.gui.utils.Icons; import jadx.gui.utils.UiUtils; public class ProgressPanel extends JPanel { private static final long serialVersionUID = -3238438119672015733L; private final JProgressBar progressBar; private final JLabel progressLabel; private final JButton cancelButton; private final boolean showCancelButton; public ProgressPanel(final MainWindow mainWindow, boolean showCancelButton) { this.showCancelButton = showCancelButton; progressLabel = new JLabel(); progressBar = new JProgressBar(0, 100); progressBar.setIndeterminate(true); progressBar.setStringPainted(false); progressLabel.setLabelFor(progressBar); setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setVisible(false); add(progressLabel); add(progressBar); Icon cancelIcon = Icons.ICON_CLOSE; cancelButton = new JButton(cancelIcon); cancelButton.setPreferredSize(new Dimension(cancelIcon.getIconWidth(), cancelIcon.getIconHeight())); cancelButton.setToolTipText("Cancel background jobs"); cancelButton.setBorderPainted(false); cancelButton.setFocusPainted(false); cancelButton.setContentAreaFilled(false); cancelButton.addActionListener(e -> mainWindow.cancelBackgroundJobs()); cancelButton.setVisible(showCancelButton); add(cancelButton); } public void reset() { cancelButton.setVisible(showCancelButton); progressBar.setIndeterminate(true); progressBar.setValue(0); progressBar.setString(""); progressBar.setStringPainted(true); } public void setProgress(ITaskProgress taskProgress) { int progress = taskProgress.progress(); int total = taskProgress.total(); if (progress == 0 || total == 0) { progressBar.setIndeterminate(true); } else { if (progressBar.isIndeterminate()) { progressBar.setIndeterminate(false); } setProgress(UiUtils.calcProgress(progress, total)); } } private void setProgress(int progress) { progressBar.setIndeterminate(false); progressBar.setValue(progress); progressBar.setString(progress + "%"); progressBar.setStringPainted(true); } public void setLabel(String label) { progressLabel.setText(label); } public void setIndeterminate(boolean newValue) { progressBar.setIndeterminate(newValue); } public void setCancelButtonVisible(boolean visible) { cancelButton.setVisible(visible); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/SimpleCodePanel.java ================================================ package jadx.gui.ui.panel; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import javax.swing.BorderFactory; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rtextarea.RTextScrollPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.LineNumbersMode; import jadx.gui.treemodel.CodeNode; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.utils.NLS; // The code panel class is used to display the code of the selected node. public class SimpleCodePanel extends JPanel { private static final long serialVersionUID = -4073178549744330905L; private static final Logger LOG = LoggerFactory.getLogger(SimpleCodePanel.class); private final RSyntaxTextArea codeArea; private final RTextScrollPane codeScrollPane; private final JLabel titleLabel; public SimpleCodePanel(MainWindow mainWindow) { JadxSettings settings = mainWindow.getSettings(); setLayout(new BorderLayout(5, 5)); setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); // Set the minimum size to ensure the panel is not completely minimized setMinimumSize(new Dimension(300, 400)); setPreferredSize(new Dimension(800, 600)); // The title label titleLabel = new JLabel(NLS.str("usage_dialog_plus.code_view")); titleLabel.setFont(settings.getCodeFont()); titleLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 10, 5)); // The code area codeArea = AbstractCodeArea.getDefaultArea(mainWindow); codeArea.setText("// " + NLS.str("usage_dialog_plus.select_node")); codeScrollPane = new RTextScrollPane(codeArea); codeScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); codeScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); add(titleLabel, BorderLayout.NORTH); add(codeScrollPane, BorderLayout.CENTER); applySettings(settings); } private void applySettings(JadxSettings settings) { codeScrollPane.setLineNumbersEnabled(settings.getLineNumbersMode() != LineNumbersMode.DISABLE); codeScrollPane.getGutter().setLineNumberFont(settings.getCodeFont()); codeArea.setFont(settings.getCodeFont()); } public void showCode(JNode node, String codeLine) { if (node != null) { titleLabel.setText(NLS.str("usage_dialog_plus.code_for", node.makeLongString())); codeArea.setSyntaxEditingStyle(node.getSyntaxName()); // Get the complete code String contextCode = getContextCode(node, codeLine); codeArea.setText(contextCode); // Highlight the key line and scroll to that position scrollToCodeLine(codeArea, codeLine); // If it is a CodeNode, we can get a more precise position if (node instanceof CodeNode) { CodeNode codeNode = (CodeNode) node; int pos = codeNode.getPos(); if (pos > 0) { // Try to use the position information to more accurately locate try { String text = codeArea.getText(); int lineNum = 0; int curPos = 0; // Calculate the line number corresponding to the position for (int i = 0; i < text.length() && curPos <= pos; i++) { if (text.charAt(i) == '\n') { lineNum++; } curPos++; } if (lineNum > 0) { // Scroll to the calculated line number int finalLineNum = lineNum; SwingUtilities.invokeLater(() -> { try { Rectangle2D lineRect = codeArea .modelToView2D(codeArea.getLineStartOffset(finalLineNum)); if (lineRect != null) { JScrollPane scrollPane = (JScrollPane) codeArea.getParent().getParent(); Rectangle viewRect = scrollPane.getViewport().getViewRect(); int y = (int) (lineRect.getY() - (viewRect.height - lineRect.getHeight()) / 2); if (y < 0) { y = 0; } scrollPane.getViewport().setViewPosition(new Point(0, y)); } } catch (Exception e) { // Fall back to using string matching scrollToCodeLine(codeArea, codeLine); } }); } } catch (Exception e) { // Fall back to using string matching scrollToCodeLine(codeArea, codeLine); } } else { // If there is no position information, use string matching scrollToCodeLine(codeArea, codeLine); } } else { // Not a CodeNode, use string matching scrollToCodeLine(codeArea, codeLine); } } else { titleLabel.setText(NLS.str("usage_dialog_plus.code_view")); codeArea.setText("// " + NLS.str("usage_dialog_plus.select_node")); } } private String getContextCode(JNode node, String codeLine) { // Always try to get the complete code if (node instanceof CodeNode) { CodeNode codeNode = (CodeNode) node; JNode usageJNode = codeNode.getJParent(); if (usageJNode != null) { // Try to get the complete code of the method or class String fullCode = getFullNodeCode(usageJNode); if (fullCode != null && !fullCode.isEmpty()) { return fullCode; } } } // If you cannot get more context, at least add some empty lines and comments return "// Unable to get complete context, only display related lines\n\n" + codeLine; } private String getFullNodeCode(JNode node) { if (node != null) { // Get the code information of the node ICodeInfo codeInfo = node.getCodeInfo(); if (codeInfo != null && !codeInfo.equals(ICodeInfo.EMPTY)) { return codeInfo.getCodeStr(); } // If it is a class node, try to get the class code if (node instanceof JClass) { JClass jClass = (JClass) node; return jClass.getCodeInfo().getCodeStr(); } } return null; } private void scrollToCodeLine(RSyntaxTextArea textArea, String lineToHighlight) { // Try to find and highlight a specific line in the code and scroll to that position try { String fullText = textArea.getText(); int lineIndex = fullText.indexOf(lineToHighlight); if (lineIndex >= 0) { // Ensure the text area has updated the layout textArea.revalidate(); // Highlight the code line textArea.setCaretPosition(lineIndex); int endIndex = lineIndex + lineToHighlight.length(); textArea.select(lineIndex, endIndex); textArea.getCaret().setSelectionVisible(true); // Use SwingUtilities.invokeLater to ensure the scroll is executed after the UI is updated SwingUtilities.invokeLater(() -> { try { // Get the line number int lineNum = textArea.getLineOfOffset(lineIndex); // Ensure the line is centered in the view Rectangle2D lineRect = textArea.modelToView2D(textArea.getLineStartOffset(lineNum)); if (lineRect != null) { // Calculate the center point of the view JScrollPane scrollPane = (JScrollPane) textArea.getParent().getParent(); Rectangle viewRect = scrollPane.getViewport().getViewRect(); int y = (int) (lineRect.getY() - (viewRect.height - lineRect.getHeight()) / 2); if (y < 0) { y = 0; } // Scroll to the calculated position scrollPane.getViewport().setViewPosition(new Point(0, y)); } } catch (Exception e) { LOG.debug("Error scrolling to line: {}", e.getMessage()); } }); } else { LOG.debug("Could not find line to highlight: {}", lineToHighlight); } } catch (Exception e) { LOG.debug("Error highlighting line: {}", e.getMessage()); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/panel/UndisplayedStringsPanel.java ================================================ package jadx.gui.ui.panel; import java.awt.BorderLayout; import java.awt.Font; import javax.swing.BorderFactory; import org.drjekyll.fontchooser.FontChooser; import org.drjekyll.fontchooser.model.FontSelectionModel; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rtextarea.RTextScrollPane; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.LineNumbersMode; import jadx.gui.settings.ui.font.FontChooserHack; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.ui.treenodes.UndisplayedStringsNode; public class UndisplayedStringsPanel extends ContentPanel { private static final long serialVersionUID = 695370628262996993L; private final RSyntaxTextArea textPane; private final RTextScrollPane codeScrollPane; public UndisplayedStringsPanel(TabbedPane panel, UndisplayedStringsNode node) { super(panel, node); setLayout(new BorderLayout()); textPane = AbstractCodeArea.getDefaultArea(panel.getMainWindow()); JadxSettings settings = getSettings(); Font selectedFont = settings.getCodeFont(); FontChooser fontChooser = new FontChooser(); fontChooser.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); fontChooser.setSelectedFont(selectedFont); FontChooserHack.hidePreview(fontChooser); fontChooser.addChangeListener(event -> { FontSelectionModel model = (FontSelectionModel) event.getSource(); settings.setCodeFont(model.getSelectedFont()); getMainWindow().loadSettings(); }); codeScrollPane = new RTextScrollPane(textPane); add(codeScrollPane, BorderLayout.CENTER); add(fontChooser, BorderLayout.EAST); applySettings(); showData(node.makeDescString()); } private void applySettings() { codeScrollPane.setLineNumbersEnabled(getSettings().getLineNumbersMode() != LineNumbersMode.DISABLE); codeScrollPane.getGutter().setLineNumberFont(getSettings().getCodeFont()); textPane.setFont(getSettings().getCodeFont()); } private void showData(String data) { textPane.setText(data); textPane.setCaretPosition(0); } @Override public void loadSettings() { applySettings(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/popupmenu/JClassExportType.java ================================================ package jadx.gui.ui.popupmenu; public enum JClassExportType { Code("java"), Smali("smali"), Simple("java"), Fallback("java"); final String extension; JClassExportType(String extension) { this.extension = extension; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/popupmenu/JClassPopupMenu.java ================================================ package jadx.gui.ui.popupmenu; import java.io.Writer; 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.Locale; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.DecompilationMode; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.mode.JCodeMode; import jadx.gui.ui.dialog.RenameDialog; import jadx.gui.ui.filedialog.FileDialogWrapper; import jadx.gui.ui.filedialog.FileOpenMode; import jadx.gui.utils.NLS; public class JClassPopupMenu extends JPopupMenu { private static final long serialVersionUID = -7781009781149260806L; private static final Logger LOG = LoggerFactory.getLogger(JClassPopupMenu.class); private final transient MainWindow mainWindow; public JClassPopupMenu(MainWindow mainWindow, JClass jClass) { this.mainWindow = mainWindow; add(RenameDialog.buildRenamePopupMenuItem(mainWindow, jClass)); add(makeExportSubMenu(jClass)); } private JMenuItem makeExportSubMenu(JClass jClass) { JMenu exportSubMenu = new JMenu(NLS.str("popup.export")); exportSubMenu.add(makeExportMenuItem(jClass, NLS.str("tabs.code"), JClassExportType.Code)); exportSubMenu.add(makeExportMenuItem(jClass, NLS.str("tabs.smali"), JClassExportType.Smali)); exportSubMenu.add(makeExportMenuItem(jClass, "Simple", JClassExportType.Simple)); exportSubMenu.add(makeExportMenuItem(jClass, "Fallback", JClassExportType.Fallback)); return exportSubMenu; } public JMenuItem makeExportMenuItem(JClass jClass, String label, JClassExportType exportType) { JMenuItem exportMenuItem = new JMenuItem(label); exportMenuItem.addActionListener(event -> { String fileName = jClass.getName() + "." + exportType.extension; FileDialogWrapper fileDialog = new FileDialogWrapper(mainWindow, FileOpenMode.EXPORT_NODE); fileDialog.setFileExtList(Collections.singletonList(exportType.extension)); Path currentDir = fileDialog.getCurrentDir(); if (currentDir != null) { fileDialog.setSelectedFile(currentDir.resolve(fileName)); } List selectedPaths = fileDialog.show(); if (selectedPaths.size() != 1) { return; } Path selectedPath = selectedPaths.get(0); Path savePath; // Append file extension if missing if (!selectedPath.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(exportType.extension)) { savePath = selectedPath.resolveSibling(selectedPath.getFileName() + "." + exportType.extension); } else { savePath = selectedPath; } saveJClass(jClass, savePath, exportType); LOG.info("Done saving {}", savePath); }); return exportMenuItem; } public static void saveJClass(JClass jClass, Path savePath, JClassExportType exportType) { try (Writer writer = Files.newBufferedWriter(savePath, StandardCharsets.UTF_8)) { writer.write(getCode(jClass, exportType)); } catch (Exception e) { throw new RuntimeException("Error saving project", e); } } private static String getCode(JClass jClass, JClassExportType exportType) { switch (exportType) { case Code: return jClass.getCodeInfo().getCodeStr(); case Smali: return jClass.getSmali(); case Simple: JNode jClassSimple = new JCodeMode(jClass, DecompilationMode.SIMPLE); return jClassSimple.getCodeInfo().getCodeStr(); case Fallback: JNode jClassFallback = new JCodeMode(jClass, DecompilationMode.FALLBACK); return jClassFallback.getCodeInfo().getCodeStr(); default: throw new RuntimeException("Unsupported JClassExportType " + exportType); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/popupmenu/JPackagePopupMenu.java ================================================ package jadx.gui.ui.popupmenu; import java.awt.event.ActionEvent; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import javax.swing.AbstractAction; import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.JadxWrapper; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JPackage; import jadx.gui.ui.MainWindow; import jadx.gui.ui.dialog.ExcludePkgDialog; import jadx.gui.ui.dialog.RenameDialog; import jadx.gui.ui.dialog.SearchDialog; import jadx.gui.ui.filedialog.FileDialogWrapper; import jadx.gui.ui.filedialog.FileOpenMode; import jadx.gui.utils.NLS; import jadx.gui.utils.pkgs.JRenamePackage; import jadx.gui.utils.pkgs.PackageHelper; public class JPackagePopupMenu extends JPopupMenu { private static final long serialVersionUID = -7781009781149224131L; private static final Logger LOG = LoggerFactory.getLogger(JPackagePopupMenu.class); private final transient MainWindow mainWindow; public JPackagePopupMenu(MainWindow mainWindow, JPackage pkg) { this.mainWindow = mainWindow; add(makeExcludeItem(pkg)); add(makeExcludeItem()); add(makeRenameMenuItem(pkg)); add(makeExportSubMenu(pkg)); add(makeSearchItem(pkg)); } private JMenuItem makeRenameMenuItem(JPackage pkg) { JMenuItem renameSubMenu = new JMenu(NLS.str("popup.rename")); PackageHelper packageHelper = mainWindow.getCacheObject().getPackageHelper(); List nodes = packageHelper.getRenameNodes(pkg); for (JRenamePackage node : nodes) { JMenuItem pkgPartItem = new JMenuItem(node.getTitle(), node.getIcon()); pkgPartItem.addActionListener(e -> rename(node)); renameSubMenu.add(pkgPartItem); } return renameSubMenu; } private void rename(JRenamePackage pkg) { LOG.debug("Renaming package: {}", pkg); RenameDialog.rename(mainWindow, pkg); } private JMenuItem makeExcludeItem(JPackage pkg) { JMenuItem excludeItem = new JCheckBoxMenuItem(NLS.str("popup.exclude")); excludeItem.setSelected(!pkg.isEnabled()); excludeItem.addItemListener(e -> { JadxWrapper wrapper = mainWindow.getWrapper(); String fullName = pkg.getPkg().getFullName(); if (excludeItem.isSelected()) { wrapper.addExcludedPackage(fullName); } else { wrapper.removeExcludedPackage(fullName); } mainWindow.reopen(); }); return excludeItem; } private JMenuItem makeExportSubMenu(JPackage pkg) { JMenu exportSubMenu = new JMenu(NLS.str("popup.export")); exportSubMenu.add(makeExportMenuItem(pkg, NLS.str("tabs.code"), JClassExportType.Code)); exportSubMenu.add(makeExportMenuItem(pkg, NLS.str("tabs.smali"), JClassExportType.Smali)); exportSubMenu.add(makeExportMenuItem(pkg, "Simple", JClassExportType.Simple)); exportSubMenu.add(makeExportMenuItem(pkg, "Fallback", JClassExportType.Fallback)); return exportSubMenu; } public JMenuItem makeExportMenuItem(JPackage pkg, String label, JClassExportType exportType) { JMenuItem exportMenuItem = new JMenuItem(label); exportMenuItem.addActionListener(event -> { FileDialogWrapper fileDialog = new FileDialogWrapper(mainWindow, FileOpenMode.EXPORT_NODE_FOLDER); List selectedPaths = fileDialog.show(); if (selectedPaths.size() != 1) { return; } Path savePath = selectedPaths.get(0); saveJPackage(pkg, savePath, exportType); }); return exportMenuItem; } private static void saveJPackage(JPackage pkg, Path savePath, JClassExportType exportType) { Path subSavePath = savePath.resolve(pkg.getName()); try { if (!Files.isDirectory(subSavePath)) { Files.createDirectory(subSavePath); } } catch (IOException e) { throw new RuntimeException(e); } for (JClass jClass : pkg.getClasses()) { String fileName = jClass.getName() + "." + exportType.extension; JClassPopupMenu.saveJClass(jClass, subSavePath.resolve(fileName), exportType); } for (JPackage subPkg : pkg.getSubPackages()) { saveJPackage(subPkg, subSavePath, exportType); } } private JMenuItem makeExcludeItem() { return new JMenuItem(new AbstractAction(NLS.str("popup.exclude_packages")) { private static final long serialVersionUID = -1111111202104151028L; @Override public void actionPerformed(ActionEvent e) { new ExcludePkgDialog(mainWindow).setVisible(true); } }); } private JMenuItem makeSearchItem(JPackage pkg) { JMenuItem searchItem = new JMenuItem(NLS.str("menu.text_search")); searchItem.addActionListener(e -> { String fullName = pkg.getPkg().getFullName(); LOG.debug("Searching package: {}", fullName); SearchDialog.searchPackage(mainWindow, fullName); }); return searchItem; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/popupmenu/JResourcePopupMenu.java ================================================ package jadx.gui.ui.popupmenu; 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.Collections; import java.util.List; import java.util.Locale; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.utils.CommonFileUtils; import jadx.gui.treemodel.JResource; import jadx.gui.ui.MainWindow; import jadx.gui.ui.filedialog.FileDialogWrapper; import jadx.gui.ui.filedialog.FileOpenMode; import jadx.gui.utils.NLS; import jadx.gui.utils.ui.FileOpenerHelper; public class JResourcePopupMenu extends JPopupMenu { private static final long serialVersionUID = -7781009781149260806L; private static final Logger LOG = LoggerFactory.getLogger(JResourcePopupMenu.class); private final transient MainWindow mainWindow; public JResourcePopupMenu(MainWindow mainWindow, JResource resource) { this.mainWindow = mainWindow; if (resource.getType() != JResource.JResType.ROOT) { add(makeExportMenuItem(resource)); } } private JMenuItem makeExportMenuItem(JResource resource) { JMenuItem exportMenu = new JMenuItem(NLS.str("popup.export")); exportMenu.addActionListener(event -> { Path savePath = null; switch (resource.getType()) { case ROOT: case DIR: savePath = getSaveDirPath(resource); break; case FILE: savePath = getSaveFilePath(resource); break; } if (savePath == null) { return; } saveJResource(resource, savePath, true); LOG.info("Done saving {}", savePath); }); return exportMenu; } private Path getSaveFilePath(JResource resource) { String extension = CommonFileUtils.getFileExtension(resource.getName()); FileDialogWrapper fileDialog = new FileDialogWrapper(mainWindow, FileOpenMode.EXPORT_NODE); if (extension != null) { fileDialog.setFileExtList(Collections.singletonList(extension)); } Path currentDir = fileDialog.getCurrentDir(); if (currentDir != null) { fileDialog.setSelectedFile(currentDir.resolve(resource.getName())); } List selectedPaths = fileDialog.show(); if (selectedPaths.size() != 1) { return null; } Path selectedPath = selectedPaths.get(0); Path savePath; // Append file extension if missing if (extension != null && !selectedPath.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(extension)) { savePath = selectedPath.resolveSibling(selectedPath.getFileName() + "." + extension); } else { savePath = selectedPath; } return savePath; } private Path getSaveDirPath(JResource resource) { FileDialogWrapper fileDialog = new FileDialogWrapper(mainWindow, FileOpenMode.EXPORT_NODE_FOLDER); List selectedPaths = fileDialog.show(); if (selectedPaths.size() != 1) { return null; } return selectedPaths.get(0); } private static void saveJResource(JResource resource, Path savePath, boolean comingFromDialog) { switch (resource.getType()) { case ROOT: case DIR: saveJResourceDir(resource, savePath, comingFromDialog); break; case FILE: saveJResourceFile(resource, savePath, comingFromDialog); break; } } private static void saveJResourceDir(JResource resource, Path savePath, boolean comingFromDialog) { Path subSavePath = savePath.resolve(resource.getShortName()); try { if (!Files.isDirectory(subSavePath)) { Files.createDirectories(subSavePath); } } catch (IOException e) { throw new RuntimeException(e); } for (JResource subResource : resource.getSubNodes()) { saveJResource(subResource, subSavePath, false); } } private static void saveJResourceFile(JResource resource, Path savePath, boolean comingFromDialog) { if (!comingFromDialog) { Path fileName = Path.of(resource.getName()).getFileName(); savePath = savePath.resolve(fileName); } switch (resource.getResFile().getType()) { case MANIFEST: case XML: exportString(resource, savePath); break; default: FileOpenerHelper.exportBinary(resource, savePath); break; } } private static void exportString(JResource resource, Path savePath) { try (Writer writer = Files.newBufferedWriter(savePath, StandardCharsets.UTF_8)) { writer.write(resource.getCodeInfo().getCodeStr()); } catch (Exception e) { throw new RuntimeException("Error saving file " + resource.getName(), e); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/popupmenu/RecentProjectsMenuListener.java ================================================ package jadx.gui.ui.popupmenu; import java.nio.file.Path; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; public class RecentProjectsMenuListener implements MenuListener { private final MainWindow mainWindow; private final JMenu menu; public RecentProjectsMenuListener(MainWindow mainWindow, JMenu menu) { this.mainWindow = mainWindow; this.menu = menu; } @Override public void menuSelected(MenuEvent menuEvent) { Set current = new HashSet<>(mainWindow.getProject().getFilePaths()); List items = mainWindow.getSettings().getRecentProjects() .stream() .filter(path -> !current.contains(path)) .map(path -> { JMenuItem menuItem = new JMenuItem(path.toAbsolutePath().toString()); menuItem.addActionListener(e -> mainWindow.open(Collections.singletonList(path))); return menuItem; }).collect(Collectors.toList()); menu.removeAll(); if (items.isEmpty()) { menu.add(new JMenuItem(NLS.str("menu.no_recent_projects"))); } else { items.forEach(menu::add); } } @Override public void menuDeselected(MenuEvent e) { } @Override public void menuCanceled(MenuEvent e) { } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/popupmenu/VarTreePopupMenu.java ================================================ package jadx.gui.ui.popupmenu; import java.awt.Component; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.event.ActionEvent; import javax.swing.AbstractAction; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.instructions.args.ArgType; import jadx.gui.ui.MainWindow; import jadx.gui.ui.dialog.SetValueDialog; import jadx.gui.ui.panel.JDebuggerPanel.ValueTreeNode; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class VarTreePopupMenu extends JPopupMenu { private static final Logger LOG = LoggerFactory.getLogger(VarTreePopupMenu.class); private static final long serialVersionUID = -1111111202103170724L; private final MainWindow mainWindow; private ValueTreeNode valNode; public VarTreePopupMenu(MainWindow mainWindow) { this.mainWindow = mainWindow; addItems(); } public void show(ValueTreeNode treeNode, Component invoker, int x, int y) { valNode = treeNode; super.show(invoker, x, y); } private void addItems() { JMenuItem copyValItem = new JMenuItem(new AbstractAction(NLS.str("debugger.popup_copy_value")) { private static final long serialVersionUID = -1111111202103171118L; @Override public void actionPerformed(ActionEvent e) { String val = valNode.getValue(); if (val != null) { if (val.startsWith("\"") && val.endsWith("\"")) { val = val.substring(1, val.length() - 1); } StringSelection stringSelection = new StringSelection(val); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); } } }); JMenuItem setValItem = new JMenuItem(new AbstractAction(NLS.str("debugger.popup_set_value")) { private static final long serialVersionUID = -1111111202103171119L; @Override public void actionPerformed(ActionEvent e) { (new SetValueDialog(mainWindow, valNode)).setVisible(true); } }); JMenuItem zeroItem = new JMenuItem(new AbstractAction(NLS.str("debugger.popup_change_to_zero")) { private static final long serialVersionUID = -1111111202103171120L; @Override public void actionPerformed(ActionEvent event) { try { mainWindow.getDebuggerPanel() .getDbgController() .modifyRegValue(valNode, ArgType.INT, 0); } catch (Exception e) { LOG.error("Change to zero failed", e); UiUtils.showMessageBox(mainWindow, e.getMessage()); } } }); JMenuItem oneItem = new JMenuItem(new AbstractAction(NLS.str("debugger.popup_change_to_one")) { private static final long serialVersionUID = -1111111202103171121L; @Override public void actionPerformed(ActionEvent event) { try { mainWindow.getDebuggerPanel() .getDbgController() .modifyRegValue(valNode, ArgType.INT, 1); } catch (Exception e) { LOG.error("Change to one failed", e); UiUtils.showMessageBox(mainWindow, e.getMessage()); } } }); this.add(copyValItem); this.add(new Separator()); this.add(setValItem); this.add(zeroItem); this.add(oneItem); this.add(zeroItem); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/startpage/RecentProjectItem.java ================================================ package jadx.gui.ui.startpage; import java.nio.file.Path; import java.util.Objects; import jadx.api.plugins.utils.CommonFileUtils; /** * Represents an item in the recent projects list. */ public class RecentProjectItem { private final Path path; public RecentProjectItem(Path path) { this.path = Objects.requireNonNull(path); } public Path getPath() { return path; } public String getProjectName() { return CommonFileUtils.removeFileExtension(path.getFileName().toString()); } public String getAbsolutePath() { return path.toAbsolutePath().toString(); } @Override public String toString() { return getProjectName(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } RecentProjectItem that = (RecentProjectItem) o; return Objects.equals(path, that.path); } @Override public int hashCode() { return Objects.hash(path); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/startpage/RecentProjectListCellRenderer.java ================================================ package jadx.gui.ui.startpage; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Font; import java.awt.Graphics; import java.awt.Rectangle; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.ListCellRenderer; import javax.swing.UIManager; import javax.swing.plaf.basic.BasicButtonUI; import jadx.gui.utils.Icons; public class RecentProjectListCellRenderer extends JPanel implements ListCellRenderer { private static final long serialVersionUID = 5550591869239586857L; private final JLabel fileNameLabel; private final JLabel pathLabel; private final JButton removeProjectBtn; private final Color defaultBackground; private final Color defaultForeground; private final Color selectedBackground; private final Color selectedForeground; private Rectangle removeIconBounds; public RecentProjectListCellRenderer(Font baseFont) { super(new BorderLayout(5, 0)); setOpaque(true); setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 5)); this.fileNameLabel = new JLabel(); fileNameLabel.setFont(baseFont.deriveFont(Font.BOLD, baseFont.getSize())); this.pathLabel = new JLabel(); pathLabel.setFont(baseFont.deriveFont(baseFont.getSize() - 2f)); pathLabel.setForeground(UIManager.getColor("Label.disabledForeground")); JPanel textPanel = new JPanel(new BorderLayout()); textPanel.setOpaque(false); textPanel.add(fileNameLabel, BorderLayout.NORTH); textPanel.add(pathLabel, BorderLayout.SOUTH); removeProjectBtn = new JButton(); removeProjectBtn.setIcon(Icons.CLOSE_INACTIVE); removeProjectBtn.setOpaque(false); removeProjectBtn.setUI(new BasicButtonUI()); removeProjectBtn.setContentAreaFilled(false); removeProjectBtn.setFocusable(false); removeProjectBtn.setBorder(null); removeProjectBtn.setBorderPainted(false); add(textPanel, BorderLayout.CENTER); add(removeProjectBtn, BorderLayout.EAST); defaultBackground = UIManager.getColor("List.background"); defaultForeground = UIManager.getColor("List.foreground"); selectedBackground = UIManager.getColor("List.selectionBackground"); selectedForeground = UIManager.getColor("List.selectionForeground"); } @Override public Component getListCellRendererComponent(JList list, RecentProjectItem value, int index, boolean isSelected, boolean cellHasFocus) { fileNameLabel.setText(value.getProjectName()); pathLabel.setText(value.getAbsolutePath()); boolean isThisRemoveButtonHovered = (index == StartPagePanel.hoveredRemoveBtnIndex); removeProjectBtn.setIcon(isThisRemoveButtonHovered ? Icons.CLOSE : Icons.CLOSE_INACTIVE); removeProjectBtn.setRolloverEnabled(isThisRemoveButtonHovered); if (isSelected) { setBackground(selectedBackground); fileNameLabel.setForeground(selectedForeground); pathLabel.setForeground(selectedForeground.darker()); removeProjectBtn.setForeground(selectedForeground); } else { setBackground(defaultBackground); fileNameLabel.setForeground(defaultForeground); pathLabel.setForeground(UIManager.getColor("Label.disabledForeground")); removeProjectBtn.setForeground(defaultForeground); } setToolTipText(value.getAbsolutePath()); return this; } /** * Overriding paint to calculate the bounds of the remove button. * This is crucial for the MouseListener on the JList to determine if a click/hover was on the * button. */ @Override public void paint(Graphics g) { super.paint(g); // Ensure the button's layout is valid before getting bounds removeProjectBtn.doLayout(); // Calculate bounds of the remove button relative to this renderer panel int x = getWidth() - removeProjectBtn.getWidth() - getBorder().getBorderInsets(this).right; int y = (getHeight() - removeProjectBtn.getHeight()) / 2; removeIconBounds = new Rectangle(x, y, removeProjectBtn.getWidth(), removeProjectBtn.getHeight()); } /** * Returns the bounds of the remove button within the renderer component's coordinate system. * This is crucial for the MouseListener on the JList to determine if a click was on the icon. * * @return Rectangle representing the bounds of the remove icon. */ public Rectangle getRemoveIconBounds() { return removeIconBounds; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/startpage/StartPageNode.java ================================================ package jadx.gui.ui.startpage; import javax.swing.Icon; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; public class StartPageNode extends JNode { private static final long serialVersionUID = 8983134608645736174L; @Override public boolean hasContent() { return true; } @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new StartPagePanel(tabbedPane, this); } @Override public String makeString() { return NLS.str("start_page.title"); } @Override public Icon getIcon() { return Icons.START_PAGE; } @Override public JClass getJParent() { return null; } @Override public boolean supportsQuickTabs() { return false; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/startpage/StartPagePanel.java ================================================ package jadx.gui.ui.startpage; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Font; import java.awt.Rectangle; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.nio.file.Path; import java.util.List; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.ListSelectionModel; import javax.swing.ScrollPaneConstants; import javax.swing.SwingUtilities; import javax.swing.border.Border; import javax.swing.border.TitledBorder; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; public class StartPagePanel extends ContentPanel { private static final long serialVersionUID = 2457805175218770732L; private final RecentProjectsJList recentList; private final DefaultListModel recentListModel; private final MainWindow mainWindow; private final JadxSettings settings; public static int hoveredRemoveBtnIndex = -1; public StartPagePanel(TabbedPane tabbedPane, StartPageNode node) { super(tabbedPane, node); this.mainWindow = tabbedPane.getMainWindow(); this.settings = mainWindow.getSettings(); Font baseFont = settings.getUiFont(); JButton openFile = new JButton(NLS.str("file.open_title"), Icons.OPEN); openFile.addActionListener(ev -> mainWindow.openFileDialog()); JButton openProject = new JButton(NLS.str("file.open_project"), Icons.OPEN_PROJECT); openProject.addActionListener(ev -> mainWindow.openProjectDialog()); JPanel start = new JPanel(); start.setBorder(sectionFrame(NLS.str("start_page.start"), baseFont)); start.setLayout(new BoxLayout(start, BoxLayout.LINE_AXIS)); start.add(openFile); start.add(Box.createRigidArea(new Dimension(10, 0))); start.add(openProject); start.add(Box.createHorizontalGlue()); this.recentListModel = new DefaultListModel<>(); this.recentList = new RecentProjectsJList(recentListModel); this.recentList.setCellRenderer(new RecentProjectListCellRenderer(baseFont)); this.recentList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); JScrollPane scrollPane = new JScrollPane(recentList); scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); scrollPane.setPreferredSize(new Dimension(400, 250)); scrollPane.setBorder(BorderFactory.createEmptyBorder()); recentList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { int index = recentList.locationToIndex(e.getPoint()); if (index == -1) { return; } RecentProjectItem item = recentListModel.getElementAt(index); if (item == null) { return; } RecentProjectListCellRenderer renderer = (RecentProjectListCellRenderer) recentList.getCellRenderer() .getListCellRendererComponent(recentList, item, index, false, false); Rectangle cellBounds = recentList.getCellBounds(index, index); if (cellBounds != null) { int xInCell = e.getX() - cellBounds.x; int yInCell = e.getY() - cellBounds.y; Rectangle removeIconBounds = renderer.getRemoveIconBounds(); if (removeIconBounds != null && removeIconBounds.contains(xInCell, yInCell)) { removeRecentProject(item.getPath()); return; } } if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e)) { openRecentProject(item.getPath()); } else if (SwingUtilities.isRightMouseButton(e)) { recentList.setSelectedIndex(index); showRecentProjectContextMenu(e); } } }); recentList.addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { int oldHoveredRemoveBtnIndex = hoveredRemoveBtnIndex; hoveredRemoveBtnIndex = -1; int currentCellIndex = recentList.locationToIndex(e.getPoint()); if (currentCellIndex != -1) { RecentProjectItem item = recentListModel.getElementAt(currentCellIndex); RecentProjectListCellRenderer renderer = (RecentProjectListCellRenderer) recentList.getCellRenderer() .getListCellRendererComponent(recentList, item, currentCellIndex, recentList.isSelectedIndex(currentCellIndex), false); Rectangle cellBounds = recentList.getCellBounds(currentCellIndex, currentCellIndex); if (cellBounds != null) { int xInCell = e.getX() - cellBounds.x; int yInCell = e.getY() - cellBounds.y; Rectangle removeIconBounds = renderer.getRemoveIconBounds(); if (removeIconBounds != null && removeIconBounds.contains(xInCell, yInCell)) { hoveredRemoveBtnIndex = currentCellIndex; } } } if (oldHoveredRemoveBtnIndex != hoveredRemoveBtnIndex) { if (oldHoveredRemoveBtnIndex != -1) { Rectangle bounds = recentList.getCellBounds(oldHoveredRemoveBtnIndex, oldHoveredRemoveBtnIndex); if (bounds != null) { recentList.repaint(bounds); } } if (hoveredRemoveBtnIndex != -1) { Rectangle bounds = recentList.getCellBounds(hoveredRemoveBtnIndex, hoveredRemoveBtnIndex); if (bounds != null) { recentList.repaint(bounds); } } } } }); JPanel recent = new JPanel(); recent.setBorder(sectionFrame(NLS.str("start_page.recent"), baseFont)); recent.setLayout(new BoxLayout(recent, BoxLayout.PAGE_AXIS)); recent.add(scrollPane); JPanel center = new JPanel(); center.setLayout(new BorderLayout(10, 10)); center.add(start, BorderLayout.PAGE_START); center.add(recent, BorderLayout.CENTER); center.setMaximumSize(new Dimension(700, 600)); center.setAlignmentX(CENTER_ALIGNMENT); setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); setBorder(BorderFactory.createEmptyBorder(50, 50, 50, 50)); add(Box.createVerticalGlue()); add(center); add(Box.createVerticalGlue()); fillRecentProjectsList(); } private void fillRecentProjectsList() { recentListModel.clear(); List recentPaths = settings.getRecentProjects(); for (Path path : recentPaths) { recentListModel.addElement(new RecentProjectItem(path)); } recentList.revalidate(); recentList.repaint(); } private void openRecentProject(Path path) { mainWindow.open(path); } private void removeRecentProject(Path path) { settings.removeRecentProject(path); fillRecentProjectsList(); if (hoveredRemoveBtnIndex != -1 && hoveredRemoveBtnIndex >= recentListModel.size()) { hoveredRemoveBtnIndex = -1; } } private void showRecentProjectContextMenu(MouseEvent e) { JPopupMenu popupMenu = new JPopupMenu(); RecentProjectItem selectedItem = recentList.getSelectedValue(); if (selectedItem != null) { JMenuItem openItem = new JMenuItem(NLS.str("file.open_project")); openItem.addActionListener(actionEvent -> openRecentProject(selectedItem.getPath())); popupMenu.add(openItem); JMenuItem removeItem = new JMenuItem(NLS.str("start_page.list.delete_recent_project")); removeItem.addActionListener(actionEvent -> removeRecentProject(selectedItem.getPath())); popupMenu.add(removeItem); } popupMenu.show(e.getComponent(), e.getX(), e.getY()); } private static Border sectionFrame(String title, Font font) { TitledBorder titledBorder = BorderFactory.createTitledBorder(title); titledBorder.setTitleFont(font.deriveFont(Font.BOLD, font.getSize() + 1)); Border spacing = BorderFactory.createEmptyBorder(10, 10, 10, 10); return BorderFactory.createCompoundBorder(titledBorder, spacing); } @Override public void loadSettings() { } /** * Inner class: Custom JList to override getToolTipText method. * This allows displaying specific tooltips based on mouse position within a cell. */ private static class RecentProjectsJList extends JList { private static final long serialVersionUID = 1L; public RecentProjectsJList(DefaultListModel model) { super(model); } @Override public String getToolTipText(MouseEvent event) { int index = locationToIndex(event.getPoint()); if (index == -1) { return null; } RecentProjectItem item = getModel().getElementAt(index); if (item == null) { return null; } RecentProjectListCellRenderer renderer = (RecentProjectListCellRenderer) getCellRenderer() .getListCellRendererComponent(this, item, index, isSelectedIndex(index), false); Rectangle cellBounds = getCellBounds(index, index); if (cellBounds != null) { int xInCell = event.getX() - cellBounds.x; int yInCell = event.getY() - cellBounds.y; Rectangle removeIconBounds = renderer.getRemoveIconBounds(); if (removeIconBounds != null && removeIconBounds.contains(xInCell, yInCell)) { return NLS.str("start_page.list.delete_recent_project.tooltip"); } } return item.getAbsolutePath(); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/EditorSyncManager.java ================================================ package jadx.gui.ui.tab; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.ContentPanel; public class EditorSyncManager implements ITabStatesListener { private final MainWindow mainWindow; private final TabbedPane tabbedPane; public EditorSyncManager(MainWindow mainWindow, TabbedPane tabbedPane) { this.mainWindow = mainWindow; this.tabbedPane = tabbedPane; mainWindow.getTabsController().addListener(this); } public void sync() { ContentPanel selectedContentPanel = tabbedPane.getSelectedContentPanel(); if (selectedContentPanel != null) { mainWindow.selectNodeInTree(selectedContentPanel.getNode()); } } @Override public void onTabSelect(TabBlueprint blueprint) { mainWindow.toggleHexViewMenu(); if (mainWindow.getSettings().isAlwaysSelectOpened()) { // verify that tab opened for this blueprint (some nodes don't open tab with content) ContentPanel selectedContentPanel = tabbedPane.getSelectedContentPanel(); if (selectedContentPanel != null && selectedContentPanel.getNode().equals(blueprint.getNode())) { sync(); } } } @Override public void onTabClose(TabBlueprint blueprint) { ITabStatesListener.super.onTabClose(blueprint); mainWindow.toggleHexViewMenu(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/ITabStatesListener.java ================================================ package jadx.gui.ui.tab; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.gui.ui.codearea.EditorViewState; import jadx.gui.utils.JumpPosition; /** * Tabbed pane events listener */ public interface ITabStatesListener { /** * Tab added to tabbed pane without become active (selected) */ default void onTabOpen(TabBlueprint blueprint) { } /** * Tab become active (selected) */ default void onTabSelect(TabBlueprint blueprint) { } /** * Caret position changes. * * @param prevPos previous caret position; can be null if unknown; can be from another tab * @param newPos new caret position, node refer to jump target node */ default void onTabCodeJump(TabBlueprint blueprint, @Nullable JumpPosition prevPos, JumpPosition newPos) { } default void onTabSmaliJump(TabBlueprint blueprint, int pos, boolean debugMode) { } default void onTabClose(TabBlueprint blueprint) { } default void onTabPositionFirst(TabBlueprint blueprint) { } default void onTabPinChange(TabBlueprint blueprint) { } default void onTabBookmarkChange(TabBlueprint blueprint) { } default void onTabVisibilityChange(TabBlueprint blueprint) { } default void onTabRestore(TabBlueprint blueprint, EditorViewState viewState) { } default void onTabsRestoreDone() { } default void onTabsReorder(List blueprints) { } default void onTabSave(TabBlueprint blueprint, EditorViewState viewState) { } default void onTabPreviewChange(TabBlueprint blueprint) { } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/LogTabStates.java ================================================ package jadx.gui.ui.tab; import java.util.List; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.ui.codearea.EditorViewState; import jadx.gui.utils.JumpPosition; /** * Utility class to log events from TabsController by implementing ITabStatesListener. */ public class LogTabStates implements ITabStatesListener { private static final Logger LOG = LoggerFactory.getLogger(LogTabStates.class); @Override public void onTabBookmarkChange(TabBlueprint blueprint) { LOG.debug("onTabBookmarkChange: blueprint={}", blueprint); } @Override public void onTabClose(TabBlueprint blueprint) { LOG.debug("onTabClose: blueprint={}", blueprint); } @Override public void onTabCodeJump(TabBlueprint blueprint, @Nullable JumpPosition prevPos, JumpPosition newPos) { LOG.debug("onTabCodeJump: blueprint={}, prevPos={}, newPos={}", blueprint, prevPos, newPos); } @Override public void onTabOpen(TabBlueprint blueprint) { LOG.debug("onTabOpen: blueprint={}", blueprint); } @Override public void onTabPinChange(TabBlueprint blueprint) { LOG.debug("onTabPinChange: blueprint={}", blueprint); } @Override public void onTabPositionFirst(TabBlueprint blueprint) { LOG.debug("onTabPositionFirst: blueprint={}", blueprint); } @Override public void onTabRestore(TabBlueprint blueprint, EditorViewState viewState) { LOG.debug("onTabRestore: blueprint={}, viewState={}", blueprint, viewState); } @Override public void onTabSave(TabBlueprint blueprint, EditorViewState viewState) { LOG.debug("onTabSave: blueprint={}, viewState={}", blueprint, viewState); } @Override public void onTabSelect(TabBlueprint blueprint) { LOG.debug("onTabSelect: blueprint={}", blueprint); } @Override public void onTabSmaliJump(TabBlueprint blueprint, int pos, boolean debugMode) { LOG.debug("onTabSmaliJump: blueprint={}, pos={}, debugMode={}", blueprint, pos, debugMode); } @Override public void onTabsReorder(List blueprints) { LOG.debug("onTabsReorder: blueprints={}", blueprints); } @Override public void onTabsRestoreDone() { LOG.debug("onTabsRestoreDone"); } @Override public void onTabVisibilityChange(TabBlueprint blueprint) { LOG.debug("onTabVisibilityChange: blueprint={}", blueprint); } @Override public void onTabPreviewChange(TabBlueprint blueprint) { LOG.debug("onTabPreviewChange: blueprint={}", blueprint); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/NavigationController.java ================================================ package jadx.gui.ui.tab; import org.jetbrains.annotations.Nullable; import jadx.gui.ui.MainWindow; import jadx.gui.utils.JumpManager; import jadx.gui.utils.JumpPosition; /** * TODO: Save jumps history into project file to restore after reload or reopen */ public class NavigationController implements ITabStatesListener { private final transient MainWindow mainWindow; private final transient JumpManager jumps = new JumpManager(); public NavigationController(MainWindow mainWindow) { this.mainWindow = mainWindow; mainWindow.getTabsController().addListener(this); } public void navBack() { jump(jumps.getPrev()); } public void navForward() { jump(jumps.getNext()); } private void jump(@Nullable JumpPosition pos) { if (pos != null) { mainWindow.getTabsController().codeJump(pos); } } @Override public void onTabCodeJump(TabBlueprint blueprint, @Nullable JumpPosition prevPos, JumpPosition newPos) { if (newPos.equals(jumps.getCurrent())) { // ignore self-initiated jumps return; } jumps.addPosition(prevPos); jumps.addPosition(newPos); } @Override public void onTabSmaliJump(TabBlueprint blueprint, int pos, boolean debugMode) { // TODO: save smali jump } public void reset() { jumps.reset(); } public void dispose() { reset(); mainWindow.getTabsController().removeListener(this); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsBaseNode.java ================================================ package jadx.gui.ui.tab; import javax.swing.Icon; import javax.swing.JPopupMenu; import javax.swing.tree.DefaultMutableTreeNode; import jadx.gui.ui.MainWindow; abstract class QuickTabsBaseNode extends DefaultMutableTreeNode { JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return null; } Icon getIcon() { return null; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsBookmarkParentNode.java ================================================ package jadx.gui.ui.tab; import javax.swing.Icon; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import jadx.gui.ui.MainWindow; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; public class QuickTabsBookmarkParentNode extends QuickTabsParentNode { protected QuickTabsBookmarkParentNode(TabsController tabsController) { super(tabsController); } @Override public String getTitle() { return NLS.str("tree.bookmarked_tabs"); } @Override Icon getIcon() { return Icons.BOOKMARK_DARK; } @Override JPopupMenu onTreePopupMenu(MainWindow mainWindow) { if (getChildCount() == 0) { return null; } JPopupMenu menu = new JPopupMenu(); JMenuItem unbookmarkAll = new JMenuItem(NLS.str("tabs.unbookmark_all")); unbookmarkAll.addActionListener(e -> getTabsController().unbookmarkAllTabs()); menu.add(unbookmarkAll); return menu; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsChildNode.java ================================================ package jadx.gui.ui.tab; import javax.swing.Icon; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; public class QuickTabsChildNode extends QuickTabsBaseNode { private final JNode node; public QuickTabsChildNode(JNode node) { this.node = node; } @Override public String toString() { return node.toString(); } public JNode getJNode() { return node; } @Override public JPopupMenu onTreePopupMenu(MainWindow mainWindow) { JPopupMenu menu = node.onTreePopupMenu(mainWindow); if (node.supportsQuickTabs()) { if (getParent() instanceof QuickTabsPinParentNode) { if (menu == null) { menu = new JPopupMenu(); } JMenuItem closeAction = new JMenuItem(NLS.str("tabs.close")); closeAction.addActionListener(e -> mainWindow.getTabsController().closeTab(node, true)); menu.add(closeAction, 0); menu.add(new JPopupMenu.Separator(), 1); } if (getParent() instanceof QuickTabsPinParentNode) { if (menu == null) { menu = new JPopupMenu(); } JMenuItem unpinAction = new JMenuItem(NLS.str("tabs.unpin")); unpinAction.addActionListener(e -> mainWindow.getTabsController().setTabPinned(node, false)); menu.add(unpinAction, 0); menu.add(new JPopupMenu.Separator(), 1); } if (getParent() instanceof QuickTabsBookmarkParentNode) { if (menu == null) { menu = new JPopupMenu(); } JMenuItem unbookmarkAction = new JMenuItem(NLS.str("tabs.unbookmark")); unbookmarkAction.addActionListener(e -> mainWindow.getTabsController().setTabBookmarked(node, false)); menu.add(unbookmarkAction, 0); menu.add(new JPopupMenu.Separator(), 1); } } return menu; } @Override Icon getIcon() { return node.getIcon(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsOpenParentNode.java ================================================ package jadx.gui.ui.tab; import javax.swing.Icon; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import jadx.gui.ui.MainWindow; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; public class QuickTabsOpenParentNode extends QuickTabsParentNode { protected QuickTabsOpenParentNode(TabsController tabsController) { super(tabsController); } @Override public String getTitle() { return NLS.str("tree.open_tabs"); } @Override Icon getIcon() { return Icons.FOLDER; } @Override JPopupMenu onTreePopupMenu(MainWindow mainWindow) { if (getChildCount() == 0) { return null; } JPopupMenu menu = new JPopupMenu(); JMenuItem closeAll = new JMenuItem(NLS.str("tabs.closeAll")); closeAll.addActionListener(e -> getTabsController().closeAllTabs(true)); menu.add(closeAll); return menu; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsParentNode.java ================================================ package jadx.gui.ui.tab; import java.util.HashMap; import java.util.Map; import javax.swing.JPopupMenu; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; abstract class QuickTabsParentNode extends QuickTabsBaseNode { private final TabsController tabsController; private final Map childrenMap = new HashMap<>(); protected QuickTabsParentNode(TabsController tabsController) { super(); this.tabsController = tabsController; } public boolean addJNode(JNode node) { if (childrenMap.containsKey(node)) { return false; } QuickTabsChildNode childNode = new QuickTabsChildNode(node); childrenMap.put(node, childNode); add(childNode); return true; } public boolean removeJNode(JNode node) { QuickTabsChildNode childNode = childrenMap.remove(node); if (childNode == null) { return false; } remove(childNode); return true; } public void removeAllNodes() { removeAllChildren(); } public QuickTabsChildNode getQuickTabsNode(JNode node) { return childrenMap.get(node); } public TabsController getTabsController() { return tabsController; } abstract String getTitle(); @Override public String toString() { return getTitle(); } @Override JPopupMenu onTreePopupMenu(MainWindow mainWindow) { return super.onTreePopupMenu(mainWindow); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsPinParentNode.java ================================================ package jadx.gui.ui.tab; import javax.swing.Icon; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import jadx.gui.ui.MainWindow; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; public class QuickTabsPinParentNode extends QuickTabsParentNode { protected QuickTabsPinParentNode(TabsController tabsController) { super(tabsController); } @Override public String getTitle() { return NLS.str("tree.pinned_tabs"); } @Override Icon getIcon() { return Icons.PIN; } @Override JPopupMenu onTreePopupMenu(MainWindow mainWindow) { if (getChildCount() == 0) { return null; } JPopupMenu menu = new JPopupMenu(); JMenuItem unpinAll = new JMenuItem(NLS.str("tabs.unpin_all")); unpinAll.addActionListener(e -> getTabsController().unpinAllTabs()); menu.add(unpinAll); return menu; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/QuickTabsTree.java ================================================ package jadx.gui.ui.tab; import java.awt.Component; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.JPopupMenu; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.utils.UiUtils; public class QuickTabsTree extends JTree implements ITabStatesListener, TreeSelectionListener { private final MainWindow mainWindow; private final DefaultTreeModel treeModel; private final QuickTabsParentNode openParentNode; private final QuickTabsParentNode pinParentNode; private final QuickTabsParentNode bookmarkParentNode; public QuickTabsTree(MainWindow mainWindow) { this.mainWindow = mainWindow; mainWindow.getTabsController().addListener(this); Root root = new Root(); pinParentNode = new QuickTabsPinParentNode(mainWindow.getTabsController()); openParentNode = new QuickTabsOpenParentNode(mainWindow.getTabsController()); bookmarkParentNode = new QuickTabsBookmarkParentNode(mainWindow.getTabsController()); root.add(openParentNode); root.add(pinParentNode); root.add(bookmarkParentNode); treeModel = new DefaultTreeModel(root); setModel(treeModel); setCellRenderer(new CellRenderer()); setRootVisible(false); setShowsRootHandles(true); addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { TreeNode pressedNode = UiUtils.getTreeNodeUnderMouse(QuickTabsTree.this, e); if (SwingUtilities.isLeftMouseButton(e)) { if (nodeClickAction(pressedNode)) { setFocusable(true); requestFocus(); } } if (SwingUtilities.isRightMouseButton(e)) { triggerRightClickAction(e); } } }); addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) { nodeClickAction(getLastSelectedPathComponent()); } } }); loadSettings(); fillOpenParentNode(); fillPinParentNode(); fillBookmarkParentNode(); } private void triggerRightClickAction(MouseEvent e) { TreeNode treeNode = UiUtils.getTreeNodeUnderMouse(this, e); if (!(treeNode instanceof QuickTabsBaseNode)) { return; } QuickTabsBaseNode quickTabsNode = (QuickTabsBaseNode) treeNode; JPopupMenu menu = quickTabsNode.onTreePopupMenu(mainWindow); if (menu != null) { menu.show(e.getComponent(), e.getX(), e.getY()); } } private boolean nodeClickAction(Object pressedNode) { if (pressedNode == null) { return false; } if (pressedNode instanceof QuickTabsChildNode) { QuickTabsChildNode childNode = (QuickTabsChildNode) pressedNode; mainWindow.getTabsController().selectTab(childNode.getJNode()); return true; } return false; } private void fillOpenParentNode() { mainWindow.getTabsController().getOpenTabs().forEach(this::onTabOpen); } private void fillPinParentNode() { mainWindow.getTabsController().getPinnedTabs().forEach(this::onTabPinChange); } private void fillBookmarkParentNode() { mainWindow.getTabsController().getBookmarkedTabs().forEach(this::onTabBookmarkChange); } private void clearParentNode(QuickTabsParentNode parentNode) { int[] childIndices = new int[parentNode.getChildCount()]; Object[] objects = new Object[parentNode.getChildCount()]; for (int i = 0; i < childIndices.length; i++) { childIndices[i] = i; objects[i] = parentNode.getChildAt(i); } parentNode.removeAllNodes(); treeModel.nodesWereRemoved(parentNode, childIndices, objects); } private void addJNode(QuickTabsParentNode parentNode, JNode node) { if (parentNode.addJNode(node)) { treeModel.nodesWereInserted(parentNode, new int[] { parentNode.getChildCount() - 1 }); } } private void removeJNode(QuickTabsParentNode parentNode, JNode node) { QuickTabsChildNode child = parentNode.getQuickTabsNode(node); if (child != null) { int removedIndex = parentNode.getIndex(child); if (parentNode.removeJNode(node)) { treeModel.nodesWereRemoved(parentNode, new int[] { removedIndex }, new Object[] { child }); } } } @Override public void valueChanged(TreeSelectionEvent event) { DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) getLastSelectedPathComponent(); if (selectedNode != null) { if (selectedNode instanceof QuickTabsChildNode) { QuickTabsChildNode childNode = (QuickTabsChildNode) selectedNode; JNode jNode = childNode.getJNode(); TabsController tabsController = mainWindow.getTabsController(); tabsController.selectTab(jNode); } } } public void loadSettings() { setFont(mainWindow.getSettings().getCodeFont()); } public void dispose() { mainWindow.getTabsController().removeListener(this); } @Override public void onTabOpen(TabBlueprint blueprint) { if (!blueprint.isHidden() && blueprint.getNode().supportsQuickTabs()) { addJNode(openParentNode, blueprint.getNode()); } } @Override public void onTabClose(TabBlueprint blueprint) { removeJNode(openParentNode, blueprint.getNode()); removeJNode(pinParentNode, blueprint.getNode()); removeJNode(bookmarkParentNode, blueprint.getNode()); } @Override public void onTabPinChange(TabBlueprint blueprint) { JNode node = blueprint.getNode(); if (blueprint.isPinned()) { addJNode(pinParentNode, node); } else { removeJNode(pinParentNode, node); } } @Override public void onTabBookmarkChange(TabBlueprint blueprint) { JNode node = blueprint.getNode(); if (blueprint.isBookmarked()) { addJNode(bookmarkParentNode, node); } else { removeJNode(bookmarkParentNode, node); } } @Override public void onTabVisibilityChange(TabBlueprint blueprint) { JNode node = blueprint.getNode(); if (!blueprint.isHidden()) { addJNode(openParentNode, node); } else { removeJNode(openParentNode, node); } } private class Root extends DefaultMutableTreeNode { } private class CellRenderer extends DefaultTreeCellRenderer { @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { Component c = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); if (value instanceof QuickTabsBaseNode) { QuickTabsBaseNode quickTabsNode = (QuickTabsBaseNode) value; setIcon(quickTabsNode.getIcon()); } return c; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/TabBlueprint.java ================================================ package jadx.gui.ui.tab; import java.util.Objects; import jadx.gui.treemodel.JNode; public class TabBlueprint { private final JNode node; private boolean created; private boolean pinned; private boolean bookmarked; private boolean hidden; private boolean previewTab; public TabBlueprint(JNode node) { this.node = Objects.requireNonNull(node); } public JNode getNode() { return node; } public boolean isCreated() { return created; } public void setCreated(boolean created) { this.created = created; } public boolean isPinned() { return pinned; } public void setPinned(boolean pinned) { this.pinned = pinned; } public boolean isBookmarked() { return bookmarked; } public void setBookmarked(boolean bookmarked) { this.bookmarked = bookmarked; } public boolean supportsQuickTabs() { return node.supportsQuickTabs(); } public boolean isReferenced() { return isBookmarked(); } public boolean isHidden() { return hidden; } public void setHidden(boolean hidden) { this.hidden = hidden; } public boolean isPreviewTab() { return previewTab; } public void setPreviewTab(boolean previewTab) { this.previewTab = previewTab; } @Override public final boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof TabBlueprint)) { return false; } return node.equals(((TabBlueprint) o).node); } @Override public int hashCode() { return node.hashCode(); } @Override public String toString() { return "TabBlueprint{" + "node=" + node + ", pinned=" + pinned + ", bookmarked=" + bookmarked + ", hidden=" + hidden + ", previewTab=" + previewTab + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/TabComponent.java ================================================ package jadx.gui.ui.tab; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Point; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.List; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.plaf.basic.BasicButtonUI; import jadx.core.utils.ListUtils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JEditableNode; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.action.JadxGuiAction; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.tab.dnd.TabDndGestureListener; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; import jadx.gui.utils.OverlayIcon; import jadx.gui.utils.UiUtils; import jadx.gui.utils.ui.NodeLabel; public class TabComponent extends JPanel { private static final long serialVersionUID = -8147035487543610321L; private static final int TAB_TITLE_MAX_LENGTH = 30; private final TabbedPane tabbedPane; private final TabsController tabsController; private final ContentPanel contentPanel; private OverlayIcon icon; private JLabel label; private JButton pinBtn; private JButton closeBtn; public TabComponent(TabbedPane tabbedPane, ContentPanel contentPanel) { this.tabbedPane = tabbedPane; this.tabsController = tabbedPane.getMainWindow().getTabsController(); this.contentPanel = contentPanel; init(); } public void loadSettings() { label.setFont(getLabelFont()); if (tabbedPane.getDnd() != null) { tabbedPane.getDnd().loadSettings(); } } private Font getLabelFont() { Font font = tabsController.getMainWindow().getSettings().getCodeFont(); int style = font.getStyle(); style |= Font.BOLD; if (getBlueprint().isPreviewTab()) { style ^= Font.ITALIC; // flip italic bit to distinguish preview } return font.deriveFont(style); } private void init() { setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); setOpaque(false); JNode node = getNode(); icon = new OverlayIcon(node.getIcon()); label = new NodeLabel(buildTabTitle(node), node.disableHtml()); String toolTip = contentPanel.getNode().getTooltip(); if (toolTip != null) { setToolTipText(toolTip); } label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10)); label.setIcon(icon); if (node instanceof JEditableNode) { ((JEditableNode) node).addChangeListener(c -> label.setText(buildTabTitle(node))); } pinBtn = new JButton(); pinBtn.setIcon(Icons.PIN); pinBtn.setRolloverIcon(Icons.PIN_HOVERED); pinBtn.setRolloverEnabled(true); pinBtn.setOpaque(false); pinBtn.setUI(new BasicButtonUI()); pinBtn.setContentAreaFilled(false); pinBtn.setBorder(null); pinBtn.setBorderPainted(false); pinBtn.addActionListener(e -> togglePin()); closeBtn = new JButton(); closeBtn.setIcon(Icons.CLOSE_INACTIVE); closeBtn.setRolloverIcon(Icons.CLOSE); closeBtn.setRolloverEnabled(true); closeBtn.setOpaque(false); closeBtn.setUI(new BasicButtonUI()); closeBtn.setContentAreaFilled(false); closeBtn.setFocusable(false); closeBtn.setBorder(null); closeBtn.setBorderPainted(false); closeBtn.addActionListener(e -> { tabsController.closeTab(node, true); }); MouseAdapter clickAdapter = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (SwingUtilities.isMiddleMouseButton(e)) { tabsController.closeTab(node, true); } else if (SwingUtilities.isRightMouseButton(e)) { JPopupMenu menu = createTabPopupMenu(); menu.show(e.getComponent(), e.getX(), e.getY()); } else if (SwingUtilities.isLeftMouseButton(e)) { tabsController.selectTab(node); if (e.getClickCount() == 2) { tabsController.setTabPreview(node, false); } } } }; addMouseListener(clickAdapter); addListenerForDnd(); add(label); setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); update(); } public void update() { updateCloseOrPinButton(); updateBookmarkIcon(); updateFont(); } private void updateCloseOrPinButton() { if (getBlueprint().isPinned()) { if (closeBtn.isShowing()) { remove(closeBtn); } if (!pinBtn.isShowing()) { add(pinBtn); } } else { if (pinBtn.isShowing()) { remove(pinBtn); } if (!closeBtn.isShowing()) { add(closeBtn); } } } private void updateBookmarkIcon() { icon.clear(); if (getBlueprint().isBookmarked()) { icon.add(Icons.BOOKMARK_OVERLAY_DARK); } label.repaint(); } private void togglePin() { boolean pinned = !getBlueprint().isPinned(); tabsController.setTabPinned(getNode(), pinned); if (pinned) { tabsController.setTabPositionFirst(getNode()); } } private void toggleBookmark() { boolean bookmarked = !getBlueprint().isBookmarked(); tabsController.setTabBookmarked(getNode(), bookmarked); } private void updateFont() { label.setFont(getLabelFont()); } private void addListenerForDnd() { if (tabbedPane.getDnd() == null) { return; } TabComponent comp = this; DragGestureListener dgl = new TabDndGestureListener(tabbedPane.getDnd()) { @Override protected Point getDragOrigin(DragGestureEvent e) { return SwingUtilities.convertPoint(comp, e.getDragOrigin(), tabbedPane); } }; DragSource.getDefaultDragSource() .createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_COPY_OR_MOVE, dgl); } private String buildTabTitle(JNode node) { String tabTitle = node.makeStringHtml(); if (tabbedPane.tabWithTitleExists(tabTitle)) { tabTitle = node.makeLongString(); } String newTabTitle = UiUtils.limitStringLength(tabTitle, TAB_TITLE_MAX_LENGTH); if (!newTabTitle.equals(tabTitle)) { if (tabbedPane.tabWithTitleExists(newTabTitle)) { // shorter version also exist => make longer version (last try) tabTitle = UiUtils.limitStringLength(tabTitle, (int) (TAB_TITLE_MAX_LENGTH * 1.2)); } else { tabTitle = newTabTitle; } } if (node instanceof JEditableNode) { if (((JEditableNode) node).isChanged()) { return "*" + tabTitle; } } return tabTitle; } private JPopupMenu createTabPopupMenu() { JPopupMenu menu = new JPopupMenu(); String nodeFullName = getNodeFullName(contentPanel); if (nodeFullName != null) { JMenuItem copyRootClassName = new JMenuItem(NLS.str("tabs.copy_class_name")); copyRootClassName.addActionListener(actionEvent -> UiUtils.setClipboardString(nodeFullName)); menu.add(copyRootClassName); menu.addSeparator(); } if (getBlueprint().supportsQuickTabs()) { String pinTitle = getBlueprint().isPinned() ? NLS.str("tabs.unpin") : NLS.str("tabs.pin"); JMenuItem pinTab = new JMenuItem(pinTitle); pinTab.addActionListener(e -> togglePin()); menu.add(pinTab); JMenuItem unpinAll = new JMenuItem(NLS.str("tabs.unpin_all")); unpinAll.addActionListener(e -> tabsController.unpinAllTabs()); menu.add(unpinAll); String bookmarkTitle = getBlueprint().isBookmarked() ? NLS.str("tabs.unbookmark") : NLS.str("tabs.bookmark"); JMenuItem bookmarkTab = new JMenuItem(bookmarkTitle); bookmarkTab.addActionListener(e -> toggleBookmark()); menu.add(bookmarkTab); JMenuItem unbookmarkAll = new JMenuItem(NLS.str("tabs.unbookmark_all")); unbookmarkAll.addActionListener(e -> tabsController.unbookmarkAllTabs()); menu.add(unbookmarkAll); menu.addSeparator(); } if (nodeFullName != null) { MainWindow mainWindow = tabsController.getMainWindow(); JadxGuiAction selectInTree = new JadxGuiAction(ActionModel.SYNC, () -> mainWindow.selectNodeInTree(getNode())); // attach shortcut without bind only to show current keybinding selectInTree.setShortcut(mainWindow.getShortcutsController().get(ActionModel.SYNC)); menu.add(selectInTree); menu.addSeparator(); } JMenuItem closeTab = new JMenuItem(NLS.str("tabs.close")); closeTab.addActionListener(e -> tabsController.closeTab(getNode(), true)); if (getBlueprint().isPinned()) { closeTab.setEnabled(false); } menu.add(closeTab); List tabs = tabsController.getOpenTabs(); if (tabs.size() > 1) { JMenuItem closeOther = new JMenuItem(NLS.str("tabs.closeOthers")); closeOther.addActionListener(e -> { JNode currentNode = getNode(); for (TabBlueprint tab : tabs) { if (tab.getNode() != currentNode) { tabsController.closeTab(tab, true); } } }); menu.add(closeOther); JMenuItem closeAll = new JMenuItem(NLS.str("tabs.closeAll")); closeAll.addActionListener(e -> tabsController.closeAllTabs(true)); menu.add(closeAll); // We don't use TabsController here because tabs position is // specific to TabbedPane List contentPanels = tabbedPane.getTabs(); int currentIndex = contentPanels.indexOf(contentPanel); if (currentIndex > 0) { // Add item only if there are tabs on the left (index > 0) JMenuItem closeAllLeft = new JMenuItem(NLS.str("tabs.closeAllLeft")); closeAllLeft.addActionListener(e -> { // Iterate in reverse order from the index before the current tab to the beginning for (int i = currentIndex - 1; i >= 0; i--) { ContentPanel panelToClose = contentPanels.get(i); tabsController.closeTab(panelToClose.getNode(), true); } }); menu.add(closeAllLeft); } if (contentPanel != ListUtils.last(contentPanels)) { JMenuItem closeAllRight = new JMenuItem(NLS.str("tabs.closeAllRight")); closeAllRight.addActionListener(e -> { boolean pastCurrentPanel = false; for (ContentPanel panel : contentPanels) { if (!pastCurrentPanel) { if (panel == contentPanel) { pastCurrentPanel = true; } } else { tabsController.closeTab(panel.getNode(), true); } } }); menu.add(closeAllRight); } menu.addSeparator(); TabBlueprint selectedTab = tabsController.getSelectedTab(); for (TabBlueprint tab : tabs) { if (tab == selectedTab) { continue; } JNode node = tab.getNode(); final String clsName = node.makeLongString(); JMenuItem item = new JMenuItem(clsName); item.addActionListener(e -> tabsController.codeJump(node)); item.setIcon(node.getIcon()); menu.add(item); } } return menu; } private String getNodeFullName(ContentPanel contentPanel) { JNode node = contentPanel.getNode(); JClass jClass = node.getRootClass(); if (jClass != null) { return jClass.getFullName(); } return node.getName(); } public ContentPanel getContentPanel() { return contentPanel; } public TabBlueprint getBlueprint() { JNode node = contentPanel.getNode(); TabBlueprint blueprint = tabsController.getTabByNode(node); if (blueprint == null) { throw new JadxRuntimeException("TabComponent does not have a corresponding TabBlueprint, node: " + node); } return blueprint; } public JNode getNode() { return contentPanel.getNode(); } public String getTabTitle() { return label.getText(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java ================================================ package jadx.gui.ui.tab; import java.awt.Component; import java.awt.KeyEventDispatcher; import java.awt.KeyboardFocusManager; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.jobs.SilentTask; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.AbstractCodeArea; import jadx.gui.ui.codearea.AbstractCodeContentPanel; import jadx.gui.ui.codearea.ClassCodeContentPanel; import jadx.gui.ui.codearea.EditorViewState; import jadx.gui.ui.codearea.SmaliArea; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.panel.FontPanel; import jadx.gui.ui.panel.HtmlPanel; import jadx.gui.ui.panel.IViewStateSupport; import jadx.gui.ui.panel.ImagePanel; import jadx.gui.ui.tab.dnd.TabDndController; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.UiUtils; public class TabbedPane extends JTabbedPane implements ITabStatesListener { private static final long serialVersionUID = -8833600618794570904L; private static final Logger LOG = LoggerFactory.getLogger(TabbedPane.class); private final transient MainWindow mainWindow; private final transient TabsController controller; private final transient Map tabsMap = new HashMap<>(); private transient ContentPanel curTab; private transient ContentPanel lastTab; private transient TabDndController dnd; public TabbedPane(MainWindow window, TabsController controller) { this.mainWindow = window; this.controller = controller; controller.addListener(this); setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); MouseAdapter clickAdapter = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { int tabIndex = indexAtLocation(e.getX(), e.getY()); if (tabIndex == -1 || tabIndex > getTabCount()) { return; } TabComponent tab = (TabComponent) getTabComponentAt(tabIndex); tab.dispatchEvent(e); } }; addMouseListener(clickAdapter); addMouseWheelListener(event -> { if (dnd != null && dnd.isDragging()) { return; } int direction = event.getWheelRotation(); if (getTabCount() == 0 || direction == 0) { return; } direction = (direction < 0) ? -1 : 1; // normalize direction int index = getSelectedIndex(); int maxIndex = getTabCount() - 1; index += direction; index = Math.max(0, Math.min(maxIndex, index)); try { setSelectedIndex(index); } catch (IndexOutOfBoundsException e) { // ignore error } }); interceptTabKey(); interceptCloseKey(); enableSwitchingTabs(); } private void interceptTabKey() { KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() { private static final int ctrlDown = KeyEvent.CTRL_DOWN_MASK; private long ctrlInterval = 0; @Override public boolean dispatchKeyEvent(KeyEvent e) { long cur = System.currentTimeMillis(); if (!FocusManager.isActive()) { return false; // don't do nothing when tab is not on focus. } int code = e.getKeyCode(); boolean consume = code == KeyEvent.VK_TAB; // consume Tab key event anyway boolean isReleased = e.getID() == KeyEvent.KEY_RELEASED; if (isReleased) { if (code == KeyEvent.VK_CONTROL) { ctrlInterval = cur; } else if (code == KeyEvent.VK_TAB) { boolean doSwitch = false; if ((e.getModifiersEx() & ctrlDown) != 0) { doSwitch = lastTab != null && getTabCount() > 1; } else { // the gap of the release of ctrl and tab is very close, nearly the same time, // but ctrl released first. ctrlInterval = cur - ctrlInterval; if (ctrlInterval <= 90) { doSwitch = lastTab != null && getTabCount() > 1; } } if (doSwitch) { selectTab(lastTab); } } } else if (consume && (e.getModifiersEx() & ctrlDown) == 0) { // switch between source and smali if (curTab instanceof ClassCodeContentPanel) { ((ClassCodeContentPanel) curTab).switchPanel(); } } return consume; } }); } private void interceptCloseKey() { KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() { private static final int closeKey = KeyEvent.VK_W; private boolean canClose = true; @Override public boolean dispatchKeyEvent(KeyEvent e) { if (!FocusManager.isActive()) { return false; // do nothing when tab is not on focus. } if (e.getKeyCode() != closeKey) { return false; // only intercept the events of the close key } if (e.getID() == KeyEvent.KEY_RELEASED) { canClose = true; // after the close key is lifted we allow to use it again return false; } if (e.isControlDown() && canClose) { // close the current tab closeCodePanel(curTab); canClose = false; // make sure we dont close more tabs until the close key is lifted return true; } return false; } }); } private void enableSwitchingTabs() { addChangeListener(e -> { ContentPanel tab = getSelectedContentPanel(); if (tab == null) { // all closed curTab = null; lastTab = null; return; } FocusManager.focusOnCodePanel(tab); if (tab == curTab) { // a tab was closed by not the current one. if (lastTab != null && indexOfComponent(lastTab) == -1) { // lastTab was closed setLastTabAdjacentToCurTab(); } return; } if (tab == lastTab) { if (indexOfComponent(curTab) == -1) { // curTab was closed and lastTab is the current one. curTab = lastTab; setLastTabAdjacentToCurTab(); return; } // it's switching between lastTab and curTab. } lastTab = curTab; curTab = tab; }); } private void setLastTabAdjacentToCurTab() { if (getTabCount() < 2) { lastTab = null; return; } int idx = indexOfComponent(curTab); if (idx == 0) { lastTab = (ContentPanel) getComponentAt(idx + 1); } else { lastTab = (ContentPanel) getComponentAt(idx - 1); } } public MainWindow getMainWindow() { return mainWindow; } public TabsController getTabsController() { return controller; } private @Nullable ContentPanel showCode(JumpPosition jumpPos) { UiUtils.uiThreadGuard(); JNode jumpNode = jumpPos.getNode(); ContentPanel contentPanel = getTabByNode(jumpNode); if (contentPanel == null) { return null; } selectTab(contentPanel); int pos = jumpPos.getPos(); if (pos <= 0) { LOG.warn("Invalid jump: {}", jumpPos, new JadxRuntimeException()); pos = Math.max(0, jumpNode.getPos()); } contentPanel.scrollToPos(pos); return contentPanel; } public void selectTab(ContentPanel contentPanel) { controller.selectTab(contentPanel.getNode()); } private void smaliJump(JClass cls, int pos, boolean debugMode) { ContentPanel panel = getTabByNode(cls); if (panel == null) { panel = showCode(new JumpPosition(cls, 1)); if (panel == null) { throw new JadxRuntimeException("Failed to open panel for JClass: " + cls); } } else { selectTab(panel); } ClassCodeContentPanel codePane = (ClassCodeContentPanel) panel; codePane.showSmaliPane(); SmaliArea smaliArea = (SmaliArea) codePane.getSmaliCodeArea(); if (debugMode) { smaliArea.scrollToDebugPos(pos); } smaliArea.scrollToPos(pos); smaliArea.requestFocus(); } public @Nullable JumpPosition getCurrentPosition() { ContentPanel selectedCodePanel = getSelectedContentPanel(); if (selectedCodePanel instanceof AbstractCodeContentPanel) { AbstractCodeArea codeArea = ((AbstractCodeContentPanel) selectedCodePanel).getCodeArea(); if (codeArea != null) { return codeArea.getCurrentPosition(); } } return null; } private void addContentPanel(ContentPanel contentPanel) { tabsMap.put(contentPanel.getNode(), contentPanel); int tabCount = getTabCount(); add(contentPanel, tabCount); setTabComponentAt(tabCount, makeTabComponent(contentPanel)); } public void closeCodePanel(ContentPanel contentPanel) { closeCodePanel(contentPanel, false); } public void closeCodePanel(ContentPanel contentPanel, boolean considerPins) { controller.closeTab(contentPanel.getNode(), considerPins); } public List getTabs() { List list = new ArrayList<>(getTabCount()); for (int i = 0; i < getTabCount(); i++) { list.add((ContentPanel) getComponentAt(i)); } return list; } public @Nullable ContentPanel getTabByNode(JNode node) { return tabsMap.get(node); } public @Nullable TabComponent getTabComponentByNode(JNode node) { ContentPanel contentPanel = getTabByNode(node); if (contentPanel == null) { return null; } int index = indexOfComponent(contentPanel); if (index == -1) { return null; } Component component = getTabComponentAt(index); if (!(component instanceof TabComponent)) { return null; } return (TabComponent) component; } public boolean tabWithTitleExists(String tabTitle) { try { for (int i = 0; i < getTabCount(); i++) { Component component = getTabComponentAt(i); if (component instanceof TabComponent) { if (((TabComponent) component).getTabTitle().equals(tabTitle)) { return true; } } } } catch (Exception e) { LOG.warn("Failed to check tabs titles", e); } return false; } public void refresh(JNode node) { ContentPanel panel = getTabByNode(node); if (panel != null) { setTabComponentAt(indexOfComponent(panel), makeTabComponent(panel)); fireStateChanged(); } } public void reloadInactiveTabs() { UiUtils.uiThreadGuard(); int tabCount = getTabCount(); if (tabCount == 1) { return; } int current = getSelectedIndex(); for (int i = 0; i < tabCount; i++) { if (i == current) { continue; } ContentPanel oldPanel = (ContentPanel) getComponentAt(i); TabBlueprint tab = controller.getTabByNode(oldPanel.getNode()); if (tab == null) { continue; } EditorViewState viewState = controller.getEditorViewState(tab); JNode node = oldPanel.getNode(); ContentPanel panel = node.getContentPanel(this); FocusManager.listen(panel); tabsMap.put(node, panel); setComponentAt(i, panel); setTabComponentAt(i, makeTabComponent(panel)); controller.restoreEditorViewState(viewState); } fireStateChanged(); } @Nullable public ContentPanel getSelectedContentPanel() { return (ContentPanel) getSelectedComponent(); } private Component makeTabComponent(final ContentPanel contentPanel) { return new TabComponent(this, contentPanel); } public void closeAllTabs() { closeAllTabs(false); } public void closeAllTabs(boolean considerPins) { for (ContentPanel panel : getTabs()) { closeCodePanel(panel, considerPins); } } public void loadSettings() { for (int i = 0; i < getTabCount(); i++) { ((ContentPanel) getComponentAt(i)).loadSettings(); ((TabComponent) getTabComponentAt(i)).loadSettings(); } } public void reset() { closeAllTabs(); tabsMap.clear(); curTab = null; lastTab = null; FocusManager.reset(); } @Nullable public Component getFocusedComp() { return FocusManager.getFocusedComp(); } public TabDndController getDnd() { return dnd; } public void setDnd(TabDndController dnd) { this.dnd = dnd; } @Override public void onTabOpen(TabBlueprint blueprint) { if (blueprint.isHidden()) { return; } JNode node = blueprint.getNode(); ContentPanel newPanel = node.getContentPanel(this); if (newPanel != null) { if (node != newPanel.getNode()) { throw new JadxRuntimeException("Incorrect node found in content panel"); } FocusManager.listen(newPanel); addContentPanel(newPanel); blueprint.setCreated(true); } } @Override public void onTabSelect(TabBlueprint blueprint) { ContentPanel contentPanel = getTabByNode(blueprint.getNode()); if (contentPanel != null) { setSelectedComponent(contentPanel); } } @Override public void onTabCodeJump(TabBlueprint blueprint, @Nullable JumpPosition prevPos, JumpPosition position) { // queue task to wait completion of loading tasks mainWindow.getBackgroundExecutor().execute(new SilentTask(() -> { UiUtils.uiRun(() -> showCode(position)); })); } @Override public void onTabSmaliJump(TabBlueprint blueprint, int pos, boolean debugMode) { JNode node = blueprint.getNode(); if (node instanceof JClass) { smaliJump((JClass) node, pos, debugMode); } } @Override public void onTabClose(TabBlueprint blueprint) { ContentPanel contentPanelToClose = getTabByNode(blueprint.getNode()); if (contentPanelToClose == null) { return; } ContentPanel currentContentPanel = getSelectedContentPanel(); if (currentContentPanel == contentPanelToClose) { if (lastTab != null && lastTab.getNode() != null) { selectTab(lastTab); } else if (getTabCount() > 1) { int removalIdx = indexOfComponent(contentPanelToClose); if (removalIdx > 0) { // select left tab setSelectedIndex(removalIdx - 1); } else if (removalIdx == 0) { // select right tab setSelectedIndex(removalIdx + 1); } } else { // no other tabs => inform controller to reset selection controller.deselectTab(); } } tabsMap.remove(contentPanelToClose.getNode()); remove(contentPanelToClose); contentPanelToClose.dispose(); } @Override public void onTabPositionFirst(TabBlueprint blueprint) { ContentPanel contentPanel = getTabByNode(blueprint.getNode()); if (contentPanel == null) { return; } setTabPosition(contentPanel, 0); } private void setTabPosition(ContentPanel contentPanel, int position) { TabComponent tabComponent = getTabComponentByNode(contentPanel.getNode()); if (tabComponent == null) { return; } boolean restoreSelection = contentPanel == getSelectedContentPanel(); remove(contentPanel); add(contentPanel, position); setTabComponentAt(position, tabComponent); if (restoreSelection) { setSelectedIndex(position); } } @Override public void onTabPinChange(TabBlueprint blueprint) { TabComponent tabComponent = getTabComponentByNode(blueprint.getNode()); if (tabComponent == null) { return; } tabComponent.update(); } @Override public void onTabBookmarkChange(TabBlueprint blueprint) { TabComponent tabComponent = getTabComponentByNode(blueprint.getNode()); if (tabComponent == null) { return; } tabComponent.update(); } @Override public void onTabVisibilityChange(TabBlueprint blueprint) { if (!blueprint.isHidden() && !tabsMap.containsKey(blueprint.getNode())) { onTabOpen(blueprint); } if (blueprint.isHidden() && tabsMap.containsKey(blueprint.getNode())) { onTabClose(blueprint); } } @Override public void onTabPreviewChange(TabBlueprint blueprint) { TabComponent tabComponent = getTabComponentByNode(blueprint.getNode()); if (tabComponent == null) { return; } tabComponent.update(); } @Override public void onTabRestore(TabBlueprint blueprint, EditorViewState viewState) { ContentPanel contentPanel = getTabByNode(blueprint.getNode()); if (contentPanel instanceof IViewStateSupport) { ((IViewStateSupport) contentPanel).restoreEditorViewState(viewState); } } @Override public void onTabsReorder(List blueprints) { List newBlueprints = new ArrayList<>(blueprints.size()); for (ContentPanel contentPanel : getTabs()) { TabBlueprint blueprint = controller.getTabByNode(contentPanel.getNode()); if (blueprint != null) { newBlueprints.add(blueprint); } } // Add back hidden tabs Set set = new LinkedHashSet<>(blueprints); newBlueprints.forEach(set::remove); newBlueprints.addAll(set); blueprints.clear(); blueprints.addAll(newBlueprints); } @Override public void onTabSave(TabBlueprint blueprint, EditorViewState viewState) { ContentPanel contentPanel = getTabByNode(blueprint.getNode()); if (contentPanel instanceof IViewStateSupport) { ((IViewStateSupport) contentPanel).saveEditorViewState(viewState); } } private static class FocusManager implements FocusListener { private static final FocusManager INSTANCE = new FocusManager(); private static @Nullable Component focusedComp; static boolean isActive() { return focusedComp != null; } static void reset() { focusedComp = null; } static Component getFocusedComp() { return focusedComp; } @Override public void focusGained(FocusEvent e) { focusedComp = (Component) e.getSource(); } @Override public void focusLost(FocusEvent e) { focusedComp = null; } static void listen(ContentPanel pane) { if (pane instanceof ClassCodeContentPanel) { ((ClassCodeContentPanel) pane).getCodeArea().addFocusListener(INSTANCE); ((ClassCodeContentPanel) pane).getSmaliCodeArea().addFocusListener(INSTANCE); return; } if (pane instanceof AbstractCodeContentPanel) { ((AbstractCodeContentPanel) pane).getChildrenComponent().addFocusListener(INSTANCE); return; } if (pane instanceof HtmlPanel) { ((HtmlPanel) pane).getHtmlArea().addFocusListener(INSTANCE); return; } if (pane instanceof ImagePanel) { pane.addFocusListener(INSTANCE); return; } if (pane instanceof FontPanel) { pane.addFocusListener(INSTANCE); return; } // throw new JadxRuntimeException("Add the new ContentPanel to TabbedPane.FocusManager: " + pane); } static void focusOnCodePanel(ContentPanel pane) { if (pane instanceof ClassCodeContentPanel) { SwingUtilities.invokeLater(() -> ((ClassCodeContentPanel) pane).getCurrentCodeArea().requestFocus()); return; } if (pane instanceof AbstractCodeContentPanel) { SwingUtilities.invokeLater(() -> ((AbstractCodeContentPanel) pane).getChildrenComponent().requestFocus()); return; } if (pane instanceof HtmlPanel) { SwingUtilities.invokeLater(() -> ((HtmlPanel) pane).getHtmlArea().requestFocusInWindow()); return; } if (pane instanceof ImagePanel) { SwingUtilities.invokeLater(((ImagePanel) pane)::requestFocusInWindow); return; } if (pane instanceof FontPanel) { SwingUtilities.invokeLater(((FontPanel) pane)::requestFocusInWindow); return; } // throw new JadxRuntimeException("Add the new ContentPanel to TabbedPane.FocusManager: " + pane); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java ================================================ package jadx.gui.ui.tab; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JavaClass; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.gui.jobs.SimpleTask; import jadx.gui.jobs.TaskWithExtraOnFinish; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.EditorViewState; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.UiUtils; public class TabsController { private static final Logger LOG = LoggerFactory.getLogger(TabsController.class); private final MainWindow mainWindow; private final Map tabsMap = new HashMap<>(); private final List listeners = new ArrayList<>(); private boolean forceClose; private @Nullable TabBlueprint selectedTab; public TabsController(MainWindow mainWindow) { this.mainWindow = mainWindow; if (UiUtils.JADX_GUI_DEBUG) { addListener(new LogTabStates()); } } public MainWindow getMainWindow() { return mainWindow; } public void addListener(ITabStatesListener listener) { listeners.add(listener); } public void removeListener(ITabStatesListener listener) { listeners.remove(listener); } public @Nullable TabBlueprint getTabByNode(JNode node) { return tabsMap.get(node); } public TabBlueprint openTab(JNode node) { return openTab(node, false, false); } public TabBlueprint openTab(JNode node, boolean hidden, boolean preview) { if (!node.hasContent()) { LOG.warn("Can't open tab for node without content, node: {}", node); } TabBlueprint blueprint = getTabByNode(node); if (blueprint == null) { TabBlueprint newBlueprint = new TabBlueprint(node); newBlueprint.setHidden(hidden); newBlueprint.setPreviewTab(preview); tabsMap.put(node, newBlueprint); listeners.forEach(l -> l.onTabOpen(newBlueprint)); if (hidden) { listeners.forEach(l -> l.onTabVisibilityChange(newBlueprint)); } blueprint = newBlueprint; } setTabHiddenInternal(blueprint, hidden); if (!blueprint.isCreated()) { LOG.warn("No content panel for node: {}", node); closeTabForce(blueprint); } return blueprint; } public TabBlueprint previewTab(JNode node) { TabBlueprint blueprint = getPreviewTab(); if (blueprint != null) { closeTab(blueprint.getNode()); } return openTab(node, false, true); } public void selectTab(JNode node) { selectTab(node, false); } public void selectTab(JNode node, boolean fromTree) { if (selectedTab != null && selectedTab.getNode() == node) { // already selected return; } if (mainWindow.getSettings().isEnablePreviewTab() && fromTree) { selectedTab = previewTab(node); } else { selectedTab = openTab(node); } listeners.forEach(l -> l.onTabSelect(selectedTab)); } public void deselectTab() { selectedTab = null; } /** * Jump to node definition */ public void codeJump(JNode node) { codeJump(node, false); } /** * Jump to node definition */ public void codeJump(JNode node, boolean fromTree) { JClass parentCls = node.getJParent(); if (parentCls != null) { // handle jump to inner class, method or field: // - load parent // - search position and jump to it JavaClass cls = node.getJParent().getCls(); JavaClass origTopCls = cls.getOriginalTopParentClass(); JavaClass codeParent = cls.getTopParentClass(); if (!Objects.equals(codeParent, origTopCls)) { JClass jumpCls = mainWindow.getCacheObject().getNodeCache().makeFrom(codeParent); loadCodeWithUIAction(jumpCls, () -> jumpToInnerClass(node, codeParent, jumpCls, fromTree)); return; } } JClass clsRootClass = node.getRootClass(); if (clsRootClass == null) { // not a class, select tab (without position scroll) selectTab(node, fromTree); return; } loadCodeWithUIAction(clsRootClass, () -> codeJump(new JumpPosition(node), fromTree)); } private void loadCodeWithUIAction(JClass cls, Runnable action) { SimpleTask loadTask = cls.getLoadTask(); if (loadTask == null) { // already loaded UiUtils.uiRun(action); return; } mainWindow.getBackgroundExecutor().execute(new TaskWithExtraOnFinish(loadTask, action)); } /** * Search and jump to original node in jumpCls */ private void jumpToInnerClass(JNode node, JavaClass codeParent, JClass jumpCls, boolean fromTree) { codeParent.getCodeInfo().getCodeMetadata().searchDown(0, (pos, ann) -> { if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) { ICodeNodeRef declNode = ((NodeDeclareRef) ann).getNode(); if (declNode.equals(node.getJavaNode().getCodeNodeRef())) { codeJump(new JumpPosition(jumpCls, pos), fromTree); return true; } } return null; }); } public void codeJump(JumpPosition pos) { codeJump(pos, false); } /** * Prefer {@link TabsController#codeJump(JNode)} method */ public void codeJump(JumpPosition pos, boolean fromTree) { JumpPosition currentPosition = mainWindow.getTabbedPane().getCurrentPosition(); if (selectedTab == null || selectedTab.getNode() != pos.getNode()) { selectTab(pos.getNode(), fromTree); } listeners.forEach(l -> l.onTabCodeJump(selectedTab, currentPosition, pos)); } public void smaliJump(JClass cls, int pos, boolean debugMode) { selectTab(cls); TabBlueprint blueprint = getTabByNode(cls); listeners.forEach(l -> l.onTabSmaliJump(blueprint, pos, debugMode)); } public void closeTab(JNode node) { closeTab(node, false); } public void closeTab(JNode node, boolean considerPins) { TabBlueprint blueprint = getTabByNode(node); if (blueprint != null) { closeTab(blueprint, considerPins); } } public void closeTab(TabBlueprint blueprint, boolean considerPins) { if (forceClose) { closeTabForce(blueprint); return; } if (!considerPins || !blueprint.isPinned()) { if (!blueprint.isReferenced()) { closeTabForce(blueprint); } else { closeTabSoft(blueprint); } } } /** * Removes Tab from everywhere */ private void closeTabForce(TabBlueprint blueprint) { listeners.forEach(l -> l.onTabClose(blueprint)); tabsMap.remove(blueprint.getNode()); } /** * Hides Tab from TabbedPane */ private void closeTabSoft(TabBlueprint blueprint) { setTabHidden(blueprint.getNode(), true); } public void setTabPositionFirst(JNode node) { TabBlueprint blueprint = openTab(node); listeners.forEach(l -> l.onTabPositionFirst(blueprint)); } public void setTabPinned(JNode node, boolean pinned) { TabBlueprint blueprint = openTab(node); setTabPinnedInternal(blueprint, pinned); } public void setTabPinnedInternal(TabBlueprint blueprint, boolean pinned) { if (blueprint.isPinned() != pinned) { blueprint.setPreviewTab(false); blueprint.setPinned(pinned); listeners.forEach(l -> l.onTabPinChange(blueprint)); } } public void setTabBookmarked(JNode node, boolean bookmarked) { TabBlueprint blueprint = openTab(node); setTabBookmarkedInternal(blueprint, bookmarked); } private void setTabBookmarkedInternal(TabBlueprint blueprint, boolean bookmarked) { if (blueprint.isBookmarked() != bookmarked) { blueprint.setPreviewTab(false); blueprint.setBookmarked(bookmarked); listeners.forEach(l -> l.onTabBookmarkChange(blueprint)); removeTabIfNotReferenced(blueprint); } } public void setTabHidden(JNode node, boolean hidden) { TabBlueprint blueprint = getTabByNode(node); setTabHiddenInternal(blueprint, hidden); } private void setTabHiddenInternal(TabBlueprint blueprint, boolean hidden) { if (blueprint != null && blueprint.isHidden() != hidden) { blueprint.setPreviewTab(false); blueprint.setHidden(hidden); listeners.forEach(l -> l.onTabVisibilityChange(blueprint)); } } public void setTabPreview(JNode node, boolean isPreview) { TabBlueprint blueprint = getTabByNode(node); setTabPreviewInternal(blueprint, isPreview); } private void setTabPreviewInternal(TabBlueprint blueprint, boolean isPreview) { if (blueprint != null && blueprint.isPreviewTab() != isPreview) { blueprint.setPreviewTab(isPreview); listeners.forEach(l -> l.onTabPreviewChange(blueprint)); } } private void removeTabIfNotReferenced(TabBlueprint blueprint) { if (blueprint.isHidden() && !blueprint.isReferenced()) { tabsMap.remove(blueprint.getNode()); } } public void closeAllTabs() { closeAllTabs(false); } public void forceCloseAllTabs() { forceClose = true; closeAllTabs(); forceClose = false; selectedTab = null; } public boolean isForceClose() { return forceClose; } public void closeAllTabs(boolean considerPins) { List.copyOf(tabsMap.values()).forEach(t -> closeTab(t.getNode(), considerPins)); } public void unpinAllTabs() { tabsMap.values().forEach(t -> setTabPinned(t.getNode(), false)); } public void unbookmarkAllTabs() { tabsMap.values().forEach(t -> setTabBookmarked(t.getNode(), false)); } public TabBlueprint getSelectedTab() { return selectedTab; } public List getTabs() { return List.copyOf(tabsMap.values()); } public List getOpenTabs() { return List.copyOf(tabsMap.values()); } public List getPinnedTabs() { return tabsMap.values().stream() .filter(TabBlueprint::isPinned) .collect(Collectors.toUnmodifiableList()); } public List getBookmarkedTabs() { return tabsMap.values().stream() .filter(TabBlueprint::isBookmarked) .collect(Collectors.toUnmodifiableList()); } public TabBlueprint getPreviewTab() { return tabsMap.values().stream() .filter(TabBlueprint::isPreviewTab).findFirst().orElse(null); } public void restoreEditorViewState(EditorViewState viewState) { JNode node = viewState.getNode(); TabBlueprint blueprint = openTab(node, viewState.isHidden(), viewState.isPreviewTab()); setTabPinnedInternal(blueprint, viewState.isPinned()); setTabBookmarkedInternal(blueprint, viewState.isBookmarked()); listeners.forEach(l -> l.onTabRestore(blueprint, viewState)); if (viewState.isActive()) { selectTab(node); } } public void notifyRestoreEditorViewStateDone() { if (selectedTab == null && !tabsMap.isEmpty()) { JNode node = tabsMap.values().iterator().next().getNode(); LOG.warn("No active tab found, select {}", node); // TODO: find the reason of this issue selectTab(node); } listeners.forEach(ITabStatesListener::onTabsRestoreDone); } public List getEditorViewStates() { List reorderedTabs = new ArrayList<>(tabsMap.values()); listeners.forEach(l -> l.onTabsReorder(reorderedTabs)); List states = new ArrayList<>(); for (TabBlueprint blueprint : reorderedTabs) { states.add(getEditorViewState(blueprint)); } return states; } public EditorViewState getEditorViewState(TabBlueprint blueprint) { EditorViewState viewState = new EditorViewState(blueprint.getNode()); listeners.forEach(l -> l.onTabSave(blueprint, viewState)); viewState.setActive(blueprint == selectedTab); viewState.setPinned(blueprint.isPinned()); viewState.setBookmarked(blueprint.isBookmarked()); viewState.setHidden(blueprint.isHidden()); viewState.setPreviewTab(blueprint.isPreviewTab()); return viewState; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/dnd/TabDndController.java ================================================ /* * The MIT License (MIT) * Copyright (c) 2015 TERAI Atsuhiro * Copyright (c) 2024 Skylot * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package jadx.gui.ui.tab.dnd; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Point; import java.awt.Rectangle; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragSource; import java.awt.dnd.DropTarget; import java.awt.image.BufferedImage; import java.util.Objects; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; import javax.swing.plaf.metal.MetalTabbedPaneUI; import jadx.gui.settings.JadxSettings; import jadx.gui.ui.tab.TabbedPane; public class TabDndController { private final transient JTabbedPane pane; private static final int DROP_TARGET_MARK_SIZE = 4; private static final int SCROLL_AREA_SIZE = 30; private static final int SCROLL_AREA_EXTRA = 30; // Making area with scroll buttons a bit bigger. private static final String ACTION_SCROLL_FORWARD = "scrollTabsForwardAction"; private static final String ACTION_SCROLL_BACKWARD = "scrollTabsBackwardAction"; private final transient TabDndGhostPane tabDndGhostPane; protected int dragTabIndex = -1; protected boolean drawGhost = true; // Semi-transparent tab copy moving along with cursor. protected boolean paintScrollTriggerAreas = false; // For debug purposes. protected Rectangle rectBackward = new Rectangle(); protected Rectangle rectForward = new Rectangle(); private boolean isDragging = false; public TabDndController(TabbedPane pane, JadxSettings settings) { pane.setDnd(this); this.pane = pane; tabDndGhostPane = new TabDndGhostPane(this, settings); new DropTarget(tabDndGhostPane, DnDConstants.ACTION_COPY_OR_MOVE, new TabDndTargetListener(this), true); DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(pane, DnDConstants.ACTION_COPY_OR_MOVE, new TabDndGestureListener(this)); } public static boolean isHorizontalTabPlacement(int tabPlacement) { return tabPlacement == JTabbedPane.TOP || tabPlacement == JTabbedPane.BOTTOM; } /** * Check if dragging near edges and scroll to according * direction through programmatically clicking system's scroll buttons. * * @param glassPt Cursor position in TabbedPane coordinates. */ public void scrollIfNeeded(Point glassPt) { Rectangle r = getTabAreaBounds(); boolean isHorizontal = isHorizontalTabPlacement(pane.getTabPlacement()); // Trying to avoid calculating two directions simultaneously. Forward first. if (isHorizontal) { rectForward.setBounds(r.x + r.width - SCROLL_AREA_SIZE - SCROLL_AREA_EXTRA, r.y, SCROLL_AREA_SIZE + SCROLL_AREA_EXTRA, r.height); } else { rectForward.setBounds(r.x, r.y + r.height - SCROLL_AREA_SIZE - SCROLL_AREA_EXTRA, r.width, SCROLL_AREA_SIZE + SCROLL_AREA_EXTRA); } rectForward = SwingUtilities.convertRectangle(pane.getParent(), rectForward, tabDndGhostPane); if (rectForward.contains(glassPt)) { clickScrollButton(ACTION_SCROLL_FORWARD); } // Backward. if (isHorizontal) { rectBackward.setBounds(r.x, r.y, SCROLL_AREA_SIZE, r.height); } else { rectBackward.setBounds(r.x, r.y, r.width, SCROLL_AREA_SIZE); } rectBackward = SwingUtilities.convertRectangle(pane.getParent(), rectBackward, tabDndGhostPane); if (rectBackward.contains(glassPt)) { clickScrollButton(ACTION_SCROLL_BACKWARD); } } private void clickScrollButton(String actionKey) { JButton forwardButton = null; JButton backwardButton = null; for (Component c : pane.getComponents()) { if (c instanceof JButton) { if (Objects.isNull(forwardButton)) { forwardButton = (JButton) c; } else { backwardButton = (JButton) c; break; } } } JButton scrollButton = ACTION_SCROLL_FORWARD.equals(actionKey) ? forwardButton : backwardButton; if (scrollButton != null && scrollButton.isEnabled()) { scrollButton.doClick(); } } /** * Finds the tab index by cursor position. If tabs first half contains the point, * then its index is returned. Second half means inserting at next index. * * @param glassPt Cursor position in TabbedPane coordinates. * @return Tab index. */ protected int getTargetTabIndex(Point glassPt) { Point tabPt = SwingUtilities.convertPoint(tabDndGhostPane, glassPt, pane); boolean isHorizontal = isHorizontalTabPlacement(pane.getTabPlacement()); for (int i = 0; i < pane.getTabCount(); ++i) { Rectangle r = pane.getBoundsAt(i); // First half. if (isHorizontal) { r.width = r.width / 2 + 1; } else { r.height = r.height / 2 + 1; } if (r.contains(tabPt)) { return i; } // Second half. if (isHorizontal) { r.x = r.x + r.width; } else { r.y = r.y + r.height; } if (r.contains(tabPt)) { return i + 1; } } int count = pane.getTabCount(); if (count == 0) { return -1; } Rectangle lastRect = pane.getBoundsAt(count - 1); Point d = isHorizontal ? new Point(1, 0) : new Point(0, 1); lastRect.translate(lastRect.width * d.x, lastRect.height * d.y); return lastRect.contains(tabPt) ? count : -1; } protected void swapTabs(int oldIdx, int newIdx) { if (newIdx < 0 || oldIdx == newIdx) { return; } final Component cmp = pane.getComponentAt(oldIdx); final Component tab = pane.getTabComponentAt(oldIdx); final String title = pane.getTitleAt(oldIdx); final Icon icon = pane.getIconAt(oldIdx); final String tip = pane.getToolTipTextAt(oldIdx); final boolean isEnabled = pane.isEnabledAt(oldIdx); newIdx = oldIdx > newIdx ? newIdx : (newIdx - 1); pane.remove(oldIdx); pane.insertTab(title, icon, cmp, tip, newIdx); pane.setEnabledAt(newIdx, isEnabled); if (isEnabled) { pane.setSelectedIndex(newIdx); } pane.setTabComponentAt(newIdx, tab); } protected void updateTargetMark(int tabIdx) { boolean isSideNeighbor = tabIdx < 0 || dragTabIndex == tabIdx || tabIdx == dragTabIndex + 1; if (isSideNeighbor) { tabDndGhostPane.setTargetRect(0, 0, 0, 0); return; } Rectangle boundsRect = pane.getBoundsAt(Math.max(0, tabIdx - 1)); final Rectangle r = SwingUtilities.convertRectangle(pane, boundsRect, tabDndGhostPane); int a = Math.min(tabIdx, 1); if (isHorizontalTabPlacement(pane.getTabPlacement())) { tabDndGhostPane.setTargetRect(r.x + r.width * a - DROP_TARGET_MARK_SIZE / 2, r.y, DROP_TARGET_MARK_SIZE, r.height); } else { tabDndGhostPane.setTargetRect(r.x, r.y + r.height * a - DROP_TARGET_MARK_SIZE / 2, r.width, DROP_TARGET_MARK_SIZE); } } protected void initGlassPane(Point tabPt) { pane.getRootPane().setGlassPane(tabDndGhostPane); if (drawGhost) { Component c = pane.getTabComponentAt(dragTabIndex); if (c == null) { return; } Dimension d = c.getPreferredSize(); switch (tabDndGhostPane.getGhostType()) { case IMAGE: { GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice device = env.getDefaultScreenDevice(); GraphicsConfiguration config = device.getDefaultConfiguration(); BufferedImage image = config.createCompatibleImage(d.width, d.height, BufferedImage.TRANSLUCENT); Graphics2D g2 = image.createGraphics(); SwingUtilities.paintComponent(g2, c, tabDndGhostPane, 0, 0, d.width, d.height); g2.dispose(); tabDndGhostPane.setGhostImage(image); pane.setTabComponentAt(dragTabIndex, c); break; } case OUTLINE: { tabDndGhostPane.setGhostSize(d); break; } case TARGET_MARK: break; } } Point glassPt = SwingUtilities.convertPoint(pane, tabPt, tabDndGhostPane); tabDndGhostPane.setPoint(glassPt); tabDndGhostPane.setVisible(true); } protected Rectangle getTabAreaBounds() { Rectangle tabbedRect = pane.getBounds(); Rectangle compRect; if (pane.getSelectedComponent() != null) { compRect = pane.getSelectedComponent().getBounds(); } else { compRect = new Rectangle(); } int tabPlacement = pane.getTabPlacement(); if (isHorizontalTabPlacement(tabPlacement)) { tabbedRect.height = tabbedRect.height - compRect.height; if (tabPlacement == JTabbedPane.BOTTOM) { tabbedRect.y += compRect.y + compRect.height; } } else { tabbedRect.width = tabbedRect.width - compRect.width; if (tabPlacement == JTabbedPane.RIGHT) { tabbedRect.x += compRect.x + compRect.width; } } tabbedRect.grow(2, 2); return tabbedRect; } public void onPaintGlassPane(Graphics2D g) { boolean isScrollLayout = pane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT; if (isScrollLayout && paintScrollTriggerAreas) { g.setPaint(tabDndGhostPane.getColor()); g.fill(rectBackward); g.fill(rectForward); } } public boolean onStartDrag(Point pt) { setDragging(true); int idx = pane.indexAtLocation(pt.x, pt.y); int selIdx = pane.getSelectedIndex(); boolean isTabRunsRotated = !(pane.getUI() instanceof MetalTabbedPaneUI) && pane.getTabLayoutPolicy() == JTabbedPane.WRAP_TAB_LAYOUT && idx != selIdx; dragTabIndex = isTabRunsRotated ? selIdx : idx; if (dragTabIndex >= 0 && pane.isEnabledAt(dragTabIndex)) { initGlassPane(pt); return true; } return false; } public void loadSettings() { tabDndGhostPane.loadSettings(); } public boolean isDragging() { return isDragging; } public void setDragging(boolean dragging) { isDragging = dragging; } public TabDndGhostPane getDndGhostPane() { return tabDndGhostPane; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/dnd/TabDndGestureListener.java ================================================ /* * The MIT License (MIT) * Copyright (c) 2015 TERAI Atsuhiro * Copyright (c) 2024 Skylot * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package jadx.gui.ui.tab.dnd; import java.awt.Point; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.InvalidDnDOperationException; public class TabDndGestureListener implements DragGestureListener { private final transient TabDndController dnd; public TabDndGestureListener(TabDndController dnd) { this.dnd = dnd; } @Override public void dragGestureRecognized(DragGestureEvent e) { Point tabPt = getDragOrigin(e); if (!dnd.onStartDrag(tabPt)) { return; } try { e.startDrag(DragSource.DefaultMoveDrop, new TabDndTransferable(), new TabDndSourceListener(dnd)); } catch (InvalidDnDOperationException ex) { throw new IllegalStateException(ex); } } protected Point getDragOrigin(DragGestureEvent e) { return e.getDragOrigin(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/dnd/TabDndGhostPane.java ================================================ /* * The MIT License (MIT) * Copyright (c) 2015 TERAI Atsuhiro * Copyright (c) 2024 Skylot * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package jadx.gui.ui.tab.dnd; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.image.BufferedImage; import javax.swing.JComponent; import javax.swing.UIManager; import jadx.gui.settings.JadxSettings; public class TabDndGhostPane extends JComponent { private final TabDndController dnd; private final Rectangle lineRect = new Rectangle(); private final Point location = new Point(); private transient BufferedImage ghostImage; private JadxSettings settings; private TabDndGhostType tabDndGhostType = TabDndGhostType.OUTLINE; private Dimension ghostSize; private Color ghostColor; private Insets insets; protected TabDndGhostPane(TabDndController dnd, JadxSettings settings) { super(); this.dnd = dnd; this.settings = settings; loadSettings(); } public void loadSettings() { Color systemColor = UIManager.getColor("Component.focusColor"); Color fallbackColor = new Color(0, 100, 255); ghostColor = systemColor != null ? systemColor : fallbackColor; Insets ins = UIManager.getInsets("TabbedPane.tabInsets"); insets = ins != null ? ins : new Insets(0, 0, 0, 0); tabDndGhostType = settings.getTabDndGhostType(); } public void setTargetRect(int x, int y, int width, int height) { lineRect.setBounds(x, y, width, height); } public void setGhostImage(BufferedImage ghostImage) { this.ghostImage = ghostImage; } public void setGhostSize(Dimension ghostSize) { ghostSize.setSize(ghostSize.width + insets.left + insets.right, ghostSize.height + insets.top + insets.bottom); this.ghostSize = ghostSize; } public void setGhostType(TabDndGhostType tabDndGhostType) { this.tabDndGhostType = tabDndGhostType; } public TabDndGhostType getGhostType() { return this.tabDndGhostType; } public void setColor(Color color) { this.ghostColor = color; } public Color getColor() { return this.ghostColor; } public void setPoint(Point pt) { this.location.setLocation(pt); } @Override public boolean isOpaque() { return false; } @Override public void setVisible(boolean v) { super.setVisible(v); if (!v) { setTargetRect(0, 0, 0, 0); setGhostImage(null); setGhostSize(new Dimension()); } } @Override protected void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g.create(); dnd.onPaintGlassPane(g2); renderMark(g2); renderGhost(g2); g2.dispose(); } private void renderGhost(Graphics2D g) { switch (tabDndGhostType) { case IMAGE: { g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f)); if (ghostImage == null) { return; } double x = location.getX() - ghostImage.getWidth(this) / 2d; double y = location.getY() - ghostImage.getHeight(this) / 2d; g.drawImage(ghostImage, (int) x, (int) y, this); break; } case OUTLINE: { g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .2f)); if (ghostSize == null) { return; } double x = location.getX() - ghostSize.getWidth() / 2d; double y = location.getY() - ghostSize.getHeight() / 2d; g.setPaint(ghostColor); g.fillRect((int) x, (int) y, ghostSize.width, ghostSize.height); break; } case TARGET_MARK: break; } } private void renderMark(Graphics2D g) { g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .7f)); g.setPaint(ghostColor); g.fill(lineRect); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/dnd/TabDndGhostType.java ================================================ package jadx.gui.ui.tab.dnd; public enum TabDndGhostType { /** * Bitmap is rendered from tabs component and dragged along with cursor. * May be impactful on performance. */ IMAGE, /** * Colored rect of tabs size is dragged along with cursor. */ OUTLINE, /** * Only insert mark is rendered. */ TARGET_MARK, } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/dnd/TabDndSourceListener.java ================================================ /* * The MIT License (MIT) * Copyright (c) 2015 TERAI Atsuhiro * Copyright (c) 2024 Skylot * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package jadx.gui.ui.tab.dnd; import java.awt.Component; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DragSourceEvent; import java.awt.dnd.DragSourceListener; import javax.swing.JComponent; import javax.swing.JRootPane; class TabDndSourceListener implements DragSourceListener { private final transient TabDndController dnd; public TabDndSourceListener(TabDndController dnd) { this.dnd = dnd; } @Override public void dragEnter(DragSourceDragEvent e) { e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop); } @Override public void dragExit(DragSourceEvent e) { e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop); } @Override public void dragOver(DragSourceDragEvent e) { } @Override public void dragDropEnd(DragSourceDropEvent e) { dnd.setDragging(false); Component c = e.getDragSourceContext().getComponent(); if (c instanceof JComponent) { JRootPane rp = ((JComponent) c).getRootPane(); if (rp.getGlassPane() != null) { rp.getGlassPane().setVisible(false); } } } @Override public void dropActionChanged(DragSourceDragEvent e) { } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/dnd/TabDndTargetListener.java ================================================ /* * The MIT License (MIT) * Copyright (c) 2015 TERAI Atsuhiro * Copyright (c) 2024 Skylot * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package jadx.gui.ui.tab.dnd; import java.awt.Point; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; class TabDndTargetListener implements DropTargetListener { private static final Point HIDDEN_POINT = new Point(0, -1000); private final transient TabDndController dnd; public TabDndTargetListener(TabDndController dnd) { this.dnd = dnd; } @Override public void dragEnter(DropTargetDragEvent e) { TabDndGhostPane pane = dnd.getDndGhostPane(); if (pane == null || e.getDropTargetContext().getComponent() != pane) { return; } Transferable t = e.getTransferable(); DataFlavor[] f = e.getCurrentDataFlavors(); if (t.isDataFlavorSupported(f[0])) { e.acceptDrag(e.getDropAction()); } else { e.rejectDrag(); } } @Override public void dragExit(DropTargetEvent e) { TabDndGhostPane pane = dnd.getDndGhostPane(); if (pane == null || e.getDropTargetContext().getComponent() != pane) { return; } pane.setPoint(HIDDEN_POINT); pane.setTargetRect(0, 0, 0, 0); pane.repaint(); } @Override public void dropActionChanged(DropTargetDragEvent e) { } @Override public void dragOver(DropTargetDragEvent e) { TabDndGhostPane pane = dnd.getDndGhostPane(); if (pane == null || e.getDropTargetContext().getComponent() != pane) { return; } Point glassPt = e.getLocation(); dnd.updateTargetMark(dnd.getTargetTabIndex(glassPt)); dnd.scrollIfNeeded(glassPt); // backward and forward scrolling pane.setPoint(glassPt); pane.repaint(); } @Override public void drop(DropTargetDropEvent e) { TabDndGhostPane pane = dnd.getDndGhostPane(); if (pane == null || e.getDropTargetContext().getComponent() != pane) { return; } Transferable t = e.getTransferable(); DataFlavor[] f = t.getTransferDataFlavors(); int oldIdx = dnd.dragTabIndex; int newIdx = dnd.getTargetTabIndex(e.getLocation()); if (t.isDataFlavorSupported(f[0]) && oldIdx != newIdx) { dnd.swapTabs(oldIdx, newIdx); e.dropComplete(true); } else { e.dropComplete(false); } pane.setVisible(false); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/tab/dnd/TabDndTransferable.java ================================================ /* * The MIT License (MIT) * Copyright (c) 2015 TERAI Atsuhiro * Copyright (c) 2024 Skylot * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package jadx.gui.ui.tab.dnd; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; class TabDndTransferable implements Transferable { private static final String NAME = "Transferable Tab"; @Override public Object getTransferData(DataFlavor flavor) { return this; } @Override public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[] { new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType, NAME) }; } @Override public boolean isDataFlavorSupported(DataFlavor flavor) { return NAME.equals(flavor.getHumanPresentableName()); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/treenodes/SummaryNode.java ================================================ package jadx.gui.ui.treenodes; import java.io.File; import java.io.IOException; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import javax.swing.Icon; import javax.swing.ImageIcon; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; import jadx.api.ICodeInfo; import jadx.api.ResourceFile; import jadx.api.impl.SimpleCodeInfo; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.ProcessState; import jadx.core.utils.ErrorsCounter; import jadx.core.utils.Utils; import jadx.gui.JadxWrapper; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.panel.HtmlPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.UiUtils; public class SummaryNode extends JNode { private static final long serialVersionUID = 4295299814582784805L; private static final ImageIcon ICON = UiUtils.openSvgIcon("nodes/detailView"); private final MainWindow mainWindow; private final JadxWrapper wrapper; public SummaryNode(MainWindow mainWindow) { this.mainWindow = mainWindow; this.wrapper = mainWindow.getWrapper(); } @Override public ICodeInfo getCodeInfo() { StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); try { builder.append(""); builder.append(""); writeInputSummary(builder); writeDecompilationSummary(builder); builder.append(""); } catch (Exception e) { builder.append("Error build summary: "); builder.append("

");
			builder.append(Utils.getStackTrace(e));
			builder.append("
"); } return new SimpleCodeInfo(builder.toString()); } private void writeInputSummary(StringEscapeUtils.Builder builder) throws IOException { builder.append("

Input

"); builder.append("

Files

"); builder.append("
    "); for (File inputFile : wrapper.getArgs().getInputFiles()) { builder.append("
  • "); builder.escape(inputFile.getCanonicalFile().getAbsolutePath()); builder.append("
  • "); } builder.append("
"); List classes = wrapper.getRootNode().getClasses(true); List codeSources = classes.stream() .map(ClassNode::getInputFileName) .distinct() .sorted(Comparator.naturalOrder()) .collect(Collectors.toList()); codeSources.remove("synthetic"); int codeSourcesCount = codeSources.size(); builder.append("

Code sources

"); builder.append("
    "); if (codeSourcesCount != 1) { builder.append("
  • Count: " + codeSourcesCount + "
  • "); } for (String input : codeSources) { builder.append("
  • "); builder.escape(input); builder.append("
  • "); } builder.append("
"); addNativeLibsInfo(builder); int methodsCount = classes.stream().mapToInt(cls -> cls.getMethods().size()).sum(); int fieldsCount = classes.stream().mapToInt(cls -> cls.getFields().size()).sum(); int insnCount = classes.stream().flatMap(cls -> cls.getMethods().stream()).mapToInt(MethodNode::getInsnsCount).sum(); builder.append("

Counts

"); builder.append("
    "); builder.append("
  • Classes: " + classes.size() + "
  • "); builder.append("
  • Methods: " + methodsCount + "
  • "); builder.append("
  • Fields: " + fieldsCount + "
  • "); builder.append("
  • Instructions: " + insnCount + " (units)
  • "); builder.append("
"); } private void addNativeLibsInfo(StringEscapeUtils.Builder builder) { List nativeLibs = wrapper.getResources().stream() .map(ResourceFile::getOriginalName) .filter(f -> f.endsWith(".so")) .sorted(Comparator.naturalOrder()) .collect(Collectors.toList()); builder.append("

Native libs

"); builder.append("
    "); if (nativeLibs.isEmpty()) { builder.append("
  • Total count: 0
  • "); } else { Map> libsByArch = new HashMap<>(); for (String libFile : nativeLibs) { String[] parts = StringUtils.split(libFile, '/'); int count = parts.length; if (count >= 2) { String arch = parts[count - 2]; String name = parts[count - 1]; libsByArch.computeIfAbsent(arch, (a) -> new HashSet<>()) .add(name); } } String arches = libsByArch.keySet().stream() .sorted(Comparator.naturalOrder()) .collect(Collectors.joining(", ")); builder.append("
  • Arch list: " + arches + "
  • "); String perArchCount = libsByArch.entrySet().stream() .map(entry -> entry.getKey() + ":" + entry.getValue().size()) .sorted(Comparator.naturalOrder()) .collect(Collectors.joining(", ")); builder.append("
  • Per arch count: " + perArchCount + "
  • "); builder.append("
    "); builder.append("
  • Total count: " + nativeLibs.size() + "
  • "); for (String lib : nativeLibs) { builder.append("
  • "); builder.escape(lib); builder.append("
  • "); } } builder.append("
"); } private void writeDecompilationSummary(StringEscapeUtils.Builder builder) { builder.append("

Decompilation

"); List classes = wrapper.getRootNode().getClassesWithoutInner(); int classesCount = classes.size(); long notLoadedClasses = classes.stream().filter(c -> c.getState() == ProcessState.NOT_LOADED).count(); long loadedClasses = classes.stream().filter(c -> c.getState() == ProcessState.LOADED).count(); long processedClasses = classes.stream().filter(c -> c.getState() == ProcessState.PROCESS_COMPLETE).count(); long generatedClasses = classes.stream().filter(c -> c.getState() == ProcessState.GENERATED_AND_UNLOADED).count(); builder.append("
    "); builder.append("
  • Top level classes: " + classesCount + "
  • "); builder.append("
  • Not loaded: " + valueAndPercent(notLoadedClasses, classesCount) + "
  • "); builder.append("
  • Loaded: " + valueAndPercent(loadedClasses, classesCount) + "
  • "); builder.append("
  • Processed: " + valueAndPercent(processedClasses, classesCount) + "
  • "); builder.append("
  • Code generated: " + valueAndPercent(generatedClasses, classesCount) + "
  • "); builder.append("
"); ErrorsCounter counter = wrapper.getRootNode().getErrorsCounter(); Set problemNodes = new HashSet<>(); problemNodes.addAll(counter.getErrorNodes()); problemNodes.addAll(counter.getWarnNodes()); long problemMethods = problemNodes.stream().filter(MethodNode.class::isInstance).count(); int methodsCount = classes.stream().mapToInt(cls -> cls.getMethods().size()).sum(); double methodSuccessRate = (methodsCount - problemMethods) * 100.0 / (double) methodsCount; builder.append("

Issues

"); builder.append("
    "); builder.append("
  • Errors: " + counter.getErrorCount() + "
  • "); builder.append("
  • Warnings: " + counter.getWarnsCount() + "
  • "); builder.append("
  • Nodes with errors: " + counter.getErrorNodes().size() + "
  • "); builder.append("
  • Nodes with warnings: " + counter.getWarnNodes().size() + "
  • "); builder.append("
  • Total nodes with issues: " + problemNodes.size() + "
  • "); builder.append("
  • Methods with issues: " + problemMethods + "
  • "); builder.append("
  • Methods success rate: " + String.format("%.2f", methodSuccessRate) + "%
  • "); builder.append("
"); } private String valueAndPercent(long value, int total) { return String.format("%d (%.2f%%)", value, value * 100 / ((double) total)); } @Override public boolean hasContent() { return true; } @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new HtmlPanel(tabbedPane, this); } @Override public String makeString() { return "Summary"; } @Override public Icon getIcon() { return ICON; } @Override public JClass getJParent() { return null; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/ui/treenodes/UndisplayedStringsNode.java ================================================ package jadx.gui.ui.treenodes; import javax.swing.Icon; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.ui.panel.UndisplayedStringsPanel; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.Icons; import jadx.gui.utils.NLS; public class UndisplayedStringsNode extends JNode { private static final long serialVersionUID = 2005158949697898302L; private final String undisplayedStings; public UndisplayedStringsNode(String undisplayedStings) { this.undisplayedStings = undisplayedStings; } @Override public boolean hasContent() { return true; } @Override public ContentPanel getContentPanel(TabbedPane tabbedPane) { return new UndisplayedStringsPanel(tabbedPane, this); } @Override public String makeString() { return NLS.str("msg.non_displayable_chars.title"); } @Override public Icon getIcon() { return Icons.FONT; } @Override public JClass getJParent() { return null; } @Override public String makeDescString() { return undisplayedStings; } @Override public boolean supportsQuickTabs() { return false; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.kt ================================================ package jadx.gui.update import com.google.gson.annotations.SerializedName import io.github.oshai.kotlinlogging.KotlinLogging import jadx.core.Jadx import jadx.core.plugins.versions.VersionComparator import jadx.core.utils.GsonUtils.buildGson import jadx.gui.settings.JadxUpdateChannel import java.io.InputStreamReader import java.net.HttpURLConnection import java.net.URI import kotlin.reflect.KClass data class Release(var name: String = "") data class ArtifactList(var artifacts: List? = null) data class Artifact( var name: String = "", @SerializedName("workflow_run") var workflowRun: WorkflowRun? = null, ) data class WorkflowRun( @SerializedName("head_branch") var branch: String = "", ) interface IUpdateCallback { fun onUpdate(r: Release) } class JadxUpdate(private val jadxVersion: String = Jadx.getVersion()) { companion object { private val LOG = KotlinLogging.logger {} const val JADX_ARTIFACTS_URL = "https://nightly.link/skylot/jadx/workflows/build-artifacts/master" const val JADX_RELEASES_URL = "https://github.com/skylot/jadx/releases" private const val GITHUB_API_URL = "https://api.github.com/repos/skylot/jadx" private const val GITHUB_LATEST_ARTIFACTS_URL = "$GITHUB_API_URL/actions/artifacts?per_page=5&page=1" private const val GITHUB_LATEST_RELEASE_URL = "$GITHUB_API_URL/releases/latest" } fun check(updateChannel: JadxUpdateChannel, callback: IUpdateCallback) { if (jadxVersion == Jadx.VERSION_DEV) { LOG.debug { "Ignore update check: development version" } return } Thread { try { val release = checkForNewRelease(updateChannel) if (release != null) { callback.onUpdate(release) } else { LOG.info { "No updates found" } } } catch (e: Exception) { LOG.warn(e) { "Jadx update error" } } }.apply { name = "Jadx update thread" priority = Thread.MIN_PRIORITY start() } } fun checkForNewRelease(updateChannel: JadxUpdateChannel): Release? { LOG.info { "Checking for updates..." } LOG.info { "Update channel: $updateChannel, current version: $jadxVersion" } return when (updateChannel) { JadxUpdateChannel.STABLE -> checkForNewStableRelease() JadxUpdateChannel.UNSTABLE -> checkForNewUnstableRelease() } } private fun checkForNewStableRelease(): Release? { if (jadxVersion.startsWith("r")) { // current version is 'unstable', but update channel set to 'stable' LOG.info { "Skip update check: can't compare unstable and stable versions" } return null } val latestRelease = getAndParse(GITHUB_LATEST_RELEASE_URL, Release::class) ?: return null if (VersionComparator.checkAndCompare(jadxVersion, latestRelease.name) >= 0) return null LOG.info { "Found new jadx version: ${latestRelease.name}" } return latestRelease } private fun checkForNewUnstableRelease(): Release? { val artifacts = getAndParse(GITHUB_LATEST_ARTIFACTS_URL, ArtifactList::class) ?.artifacts ?.filter { it.workflowRun?.branch == "master" } ?: return null if (artifacts.isEmpty()) return null val latestVersion = artifacts[0].name.removePrefix("jadx-gui-").removePrefix("jadx-").substringBefore('-') if (VersionComparator.checkAndCompare(jadxVersion, latestVersion) >= 0) return null LOG.info { "Found new unstable version: $latestVersion" } return Release(latestVersion) } private fun getAndParse(url: String, klass: KClass): T? { val con = URI(url).toURL().openConnection() as? HttpURLConnection if (con == null || con.responseCode != 200) { return null } return con.inputStream.use { stream -> InputStreamReader(stream).use { reader -> buildGson().fromJson(reader, klass.java) } } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java ================================================ package jadx.gui.utils; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.gui.JadxWrapper; import jadx.gui.ui.dialog.SearchDialog; import jadx.gui.utils.pkgs.PackageHelper; public class CacheObject { private final JadxWrapper wrapper; private final JNodeCache jNodeCache; private final PackageHelper packageHelper; private String lastSearch; private Map> lastSearchOptions; private String lastSearchPackage; private int maxPkgLength; private volatile boolean fullDecompilationFinished; public CacheObject(JadxWrapper wrapper) { this.wrapper = wrapper; this.jNodeCache = new JNodeCache(wrapper); this.packageHelper = new PackageHelper(wrapper, jNodeCache); reset(); } public void reset() { lastSearch = null; jNodeCache.reset(); lastSearchOptions = new HashMap<>(); lastSearchPackage = null; fullDecompilationFinished = false; } @Nullable public String getLastSearch() { return lastSearch; } @Nullable public String getLastSearchPackage() { return lastSearchPackage; } public void setLastSearch(String lastSearch) { this.lastSearch = lastSearch; } public void setLastSearchPackage(String lastSearchPackage) { this.lastSearchPackage = lastSearchPackage; } public int getMaxPkgLength() { return maxPkgLength; } public void setMaxPkgLength(int maxPkgLength) { this.maxPkgLength = maxPkgLength; } public JNodeCache getNodeCache() { return jNodeCache; } public Map> getLastSearchOptions() { return lastSearchOptions; } public PackageHelper getPackageHelper() { return packageHelper; } public boolean isFullDecompilationFinished() { return fullDecompilationFinished; } public void setFullDecompilationFinished(boolean fullDecompilationFinished) { this.fullDecompilationFinished = fullDecompilationFinished; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/CaretPositionFix.java ================================================ package jadx.gui.utils; import java.util.Map; import javax.swing.text.BadLocationException; import org.fife.ui.rsyntaxtextarea.Token; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.gui.treemodel.JClass; import jadx.gui.ui.codearea.AbstractCodeArea; /** * After class refresh (rename, comment, etc) the change of document is undetectable. * So use Token index or offset in line to calculate the new caret position. */ public class CaretPositionFix { private static final Logger LOG = LoggerFactory.getLogger(CaretPositionFix.class); private final AbstractCodeArea codeArea; private int linesCount; private int line; private int pos; private int lineOffset; private TokenInfo tokenInfo; private int javaNodePos = -1; private int codeRawOffset = -1; public CaretPositionFix(AbstractCodeArea codeArea) { this.codeArea = codeArea; } /** * Save caret position by anchor to token under caret */ public void save() { try { linesCount = codeArea.getLineCount(); pos = codeArea.getCaretPosition(); line = codeArea.getLineOfOffset(pos); lineOffset = pos - codeArea.getLineStartOffset(line); tokenInfo = getTokenInfoByOffset(codeArea.getTokenListForLine(line), pos); ICodeInfo codeInfo = codeArea.getCodeInfo(); if (codeInfo.hasMetadata()) { ICodeMetadata metadata = codeInfo.getCodeMetadata(); ICodeAnnotation ann = metadata.getAt(pos); if (ann instanceof InsnCodeOffset) { codeRawOffset = ((InsnCodeOffset) ann).getOffset(); ICodeNodeRef javaNode = metadata.getNodeAt(pos); if (javaNode != null) { javaNodePos = javaNode.getDefPosition(); } } } LOG.debug("Saved position data: line={}, lineOffset={}, token={}, codeRawOffset={}, javaNodeLine={}", line, lineOffset, tokenInfo, codeRawOffset, javaNodePos); } catch (Exception e) { LOG.error("Failed to save caret position before refresh", e); line = -1; } } /** * Restore caret position in refreshed code. * Expected to be called in UI thread. */ public void restore() { if (line == -1) { return; } try { int newPos = getNewPos(); int newLine = codeArea.getLineOfOffset(newPos); Token token = codeArea.getTokenListForLine(newLine); int tokenPos = getOffsetFromTokenInfo(tokenInfo, token); if (tokenPos == -1) { int lineStartOffset = codeArea.getLineStartOffset(newLine); int lineEndOffset = codeArea.getLineEndOffset(newLine) - 1; int lineLength = lineEndOffset - lineStartOffset; // can't restore using token -> just restore by line offset if (lineOffset < lineLength) { tokenPos = lineStartOffset + lineOffset; } else { // line truncated -> set caret at line end tokenPos = lineEndOffset; } } codeArea.setCaretPosition(tokenPos); LOG.debug("Restored caret position: {}", tokenPos); } catch (Exception e) { LOG.warn("Failed to restore caret position", e); } } private int getNewPos() throws BadLocationException { int newLinesCount = codeArea.getLineCount(); if (linesCount == newLinesCount) { return pos; } // lines count changes, try find line by raw offset ICodeInfo codeInfo = codeArea.getCodeInfo(); if (javaNodePos != -1 && codeInfo.hasMetadata()) { JClass cls = codeArea.getJClass(); if (cls != null) { ICodeMetadata codeMetadata = codeInfo.getCodeMetadata(); for (Map.Entry entry : codeMetadata.getAsMap().entrySet()) { int annPos = entry.getKey(); if (annPos >= javaNodePos) { ICodeAnnotation ann = entry.getValue(); if (ann instanceof InsnCodeOffset && ((InsnCodeOffset) ann).getOffset() == codeRawOffset) { return annPos; } } } } } // fallback: assume lines added/removed before caret int newLine = line - (linesCount - newLinesCount); return codeArea.getLineStartOffset(newLine); } private TokenInfo getTokenInfoByOffset(Token token, int offset) { if (token == null) { return null; } int index = 1; while (token.getEndOffset() < offset) { token = token.getNextToken(); if (token == null) { return null; } index++; } return new TokenInfo(index, token.getType()); } private int getOffsetFromTokenInfo(TokenInfo tokenInfo, Token token) { if (tokenInfo == null || token == null) { return -1; } int index = tokenInfo.getIndex(); if (index == -1) { return -1; } for (int i = 0; i < index; i++) { token = token.getNextToken(); if (token == null) { return -1; } } if (token.getType() != tokenInfo.getType()) { return -1; } return token.getOffset(); } private static final class TokenInfo { private final int index; private final int type; public TokenInfo(int index, int type) { this.index = index; this.type = type; } public int getIndex() { return index; } public int getType() { return type; } @Override public String toString() { return "Token{index=" + index + ", type=" + type + '}'; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java ================================================ package jadx.gui.utils; import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.PublicKey; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.interfaces.DSAPublicKey; import java.security.interfaces.RSAPublicKey; import java.util.Collection; import java.util.Collections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CertificateManager { private static final Logger LOG = LoggerFactory.getLogger(CertificateManager.class); private static final String CERTIFICATE_TYPE_NAME = "X.509"; private final Certificate cert; private X509Certificate x509cert; public static String decode(InputStream in) { StringBuilder strBuild = new StringBuilder(); Collection certificates = readCertificates(in); if (certificates != null) { for (Certificate cert : certificates) { CertificateManager certificateManager = new CertificateManager(cert); strBuild.append(certificateManager.generateText()); } } return strBuild.toString(); } static Collection readCertificates(InputStream in) { try { CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE_NAME); return cf.generateCertificates(in); } catch (Exception e) { LOG.error("Certificate read error", e); } return Collections.emptyList(); } public CertificateManager(Certificate cert) { this.cert = cert; String type = cert.getType(); if (type.equals(CERTIFICATE_TYPE_NAME) && cert instanceof X509Certificate) { x509cert = (X509Certificate) cert; } } public String generateHeader() { StringBuilder builder = new StringBuilder(); append(builder, NLS.str("certificate.cert_type"), x509cert.getType()); append(builder, NLS.str("certificate.serialSigVer"), ((Integer) x509cert.getVersion()).toString()); // serial number append(builder, NLS.str("certificate.serialNumber"), "0x" + x509cert.getSerialNumber().toString(16)); // Get subject Principal subjectDN = x509cert.getSubjectDN(); append(builder, NLS.str("certificate.cert_subject"), subjectDN.getName()); append(builder, NLS.str("certificate.serialValidFrom"), x509cert.getNotBefore().toString()); append(builder, NLS.str("certificate.serialValidUntil"), x509cert.getNotAfter().toString()); return builder.toString(); } public String generateSignature() { StringBuilder builder = new StringBuilder(); append(builder, NLS.str("certificate.serialSigType"), x509cert.getSigAlgName()); append(builder, NLS.str("certificate.serialSigOID"), x509cert.getSigAlgOID()); return builder.toString(); } public String generateFingerprint() { StringBuilder builder = new StringBuilder(); try { append(builder, NLS.str("certificate.serialMD5"), getThumbPrint(x509cert, "MD5")); append(builder, NLS.str("certificate.serialSHA1"), getThumbPrint(x509cert, "SHA-1")); append(builder, NLS.str("certificate.serialSHA256"), getThumbPrint(x509cert, "SHA-256")); } catch (Exception e) { LOG.error("Failed to parse fingerprint", e); } return builder.toString(); } public String generatePublicKey() { PublicKey publicKey = x509cert.getPublicKey(); if (publicKey instanceof RSAPublicKey) { return generateRSAPublicKey(); } if (publicKey instanceof DSAPublicKey) { return generateDSAPublicKey(); } return ""; } String generateRSAPublicKey() { RSAPublicKey pub = (RSAPublicKey) cert.getPublicKey(); StringBuilder builder = new StringBuilder(); append(builder, NLS.str("certificate.serialPubKeyType"), pub.getAlgorithm()); append(builder, NLS.str("certificate.serialPubKeyExponent"), pub.getPublicExponent().toString(10)); append(builder, NLS.str("certificate.serialPubKeyModulusSize"), Integer.toString( pub.getModulus().toString(2).length())); append(builder, NLS.str("certificate.serialPubKeyModulus"), pub.getModulus().toString(10)); return builder.toString(); } String generateDSAPublicKey() { DSAPublicKey pub = (DSAPublicKey) cert.getPublicKey(); StringBuilder builder = new StringBuilder(); append(builder, NLS.str("certificate.serialPubKeyType"), pub.getAlgorithm()); append(builder, NLS.str("certificate.serialPubKeyY"), pub.getY().toString(10)); return builder.toString(); } public String generateTextForX509() { StringBuilder builder = new StringBuilder(); if (x509cert != null) { builder.append(generateHeader()); builder.append('\n'); builder.append(generatePublicKey()); builder.append('\n'); builder.append(generateSignature()); builder.append('\n'); builder.append(generateFingerprint()); } return builder.toString(); } public String generateText() { StringBuilder str = new StringBuilder(); String type = cert.getType(); if (type.equals(CERTIFICATE_TYPE_NAME)) { str.append(generateTextForX509()); } else { str.append(cert.toString()); } return str.toString(); } static void append(StringBuilder str, String name, String value) { str.append(name).append(": ").append(value).append('\n'); } public static String getThumbPrint(X509Certificate cert, String type) throws NoSuchAlgorithmException, CertificateEncodingException { MessageDigest md = MessageDigest.getInstance(type); // lgtm [java/weak-cryptographic-algorithm] byte[] der = cert.getEncoded(); md.update(der); byte[] digest = md.digest(); return hexify(digest); } public static String hexify(byte[] bytes) { char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; StringBuilder buf = new StringBuilder(bytes.length * 3); for (byte aByte : bytes) { buf.append(hexDigits[(aByte & 0xf0) >> 4]); buf.append(hexDigits[aByte & 0x0f]); buf.append(' '); } return buf.toString(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/DefaultPopupMenuListener.java ================================================ package jadx.gui.utils; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; public interface DefaultPopupMenuListener extends PopupMenuListener { @Override default void popupMenuWillBecomeVisible(PopupMenuEvent e) { } @Override default void popupMenuWillBecomeInvisible(PopupMenuEvent e) { } @Override default void popupMenuCanceled(PopupMenuEvent e) { } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/DesktopEntryUtils.java ================================================ package jadx.gui.utils; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; import java.util.Objects; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.export.TemplateFile; import jadx.core.utils.files.FileUtils; public class DesktopEntryUtils { private static final Logger LOG = LoggerFactory.getLogger(DesktopEntryUtils.class); private static final Map SIZE_TO_LOGO_MAP = Map.of( 16, "jadx-logo-16px.png", 32, "jadx-logo-32px.png", 48, "jadx-logo-48px.png", 252, "jadx-logo.png", 256, "jadx-logo.png"); private static final Path XDG_DESKTOP_MENU_COMMAND_PATH = findExecutablePath("xdg-desktop-menu"); private static final Path XDG_ICON_RESOURCE_COMMAND_PATH = findExecutablePath("xdg-icon-resource"); public static boolean createDesktopEntry() { if (XDG_DESKTOP_MENU_COMMAND_PATH == null) { LOG.error("xdg-desktop-menu was not found in $PATH"); return false; } if (XDG_ICON_RESOURCE_COMMAND_PATH == null) { LOG.error("xdg-icon-resource was not found in $PATH"); return false; } Path desktopTempFile = FileUtils.createTempFileNonPrefixed("jadx-gui.desktop"); Path iconTempFolder = FileUtils.createTempDir("logos"); LOG.debug("Creating desktop with temp files: {}, {}", desktopTempFile, iconTempFolder); try { return createDesktopEntry(desktopTempFile, iconTempFolder); } finally { try { FileUtils.deleteFileIfExists(desktopTempFile); FileUtils.deleteDirIfExists(iconTempFolder); } catch (IOException e) { LOG.error("Failed to clean up temp files", e); } } } private static boolean createDesktopEntry(Path desktopTempFile, Path iconTempFolder) { String launchScriptPath = getLaunchScriptPath(); if (launchScriptPath == null) { return false; } for (Map.Entry entry : SIZE_TO_LOGO_MAP.entrySet()) { Path path = iconTempFolder.resolve(entry.getKey() + ".png"); if (!writeLogoFile(entry.getValue(), path)) { return false; } if (!installIcon(entry.getKey(), path)) { return false; } } if (!writeDesktopFile(launchScriptPath, desktopTempFile)) { return false; } return installDesktopEntry(desktopTempFile); } private static boolean installDesktopEntry(Path desktopTempFile) { try { ProcessBuilder desktopFileInstallCommand = new ProcessBuilder(Objects.requireNonNull(XDG_DESKTOP_MENU_COMMAND_PATH).toString(), "install", desktopTempFile.toString()); Process process = desktopFileInstallCommand.start(); int statusCode = process.waitFor(); if (statusCode != 0) { LOG.error("Got error code {} while installing desktop file", statusCode); return false; } } catch (Exception e) { LOG.error("Failed to install desktop file", e); return false; } LOG.info("Successfully installed desktop file"); return true; } private static boolean installIcon(int size, Path iconPath) { try { ProcessBuilder iconInstallCommand = new ProcessBuilder(Objects.requireNonNull(XDG_ICON_RESOURCE_COMMAND_PATH).toString(), "install", "--novendor", "--size", String.valueOf(size), iconPath.toString(), "jadx"); Process process = iconInstallCommand.start(); int statusCode = process.waitFor(); if (statusCode != 0) { LOG.error("Got error code {} while installing icon of size {}", statusCode, size); return false; } } catch (Exception e) { LOG.error("Failed to install icon of size {}", size, e); return false; } LOG.info("Successfully installed icon of size {}", size); return true; } private static Path findExecutablePath(String executableName) { for (String pathDirectory : System.getenv("PATH").split(File.pathSeparator)) { Path path = Paths.get(pathDirectory, executableName); if (path.toFile().isFile() && path.toFile().canExecute()) { return path; } } return null; } private static boolean writeDesktopFile(String launchScriptPath, Path desktopFilePath) { try { TemplateFile tmpl = TemplateFile.fromResources("/files/jadx-gui.desktop.tmpl"); tmpl.add("launchScriptPath", launchScriptPath); FileUtils.writeFile(desktopFilePath, tmpl.build()); } catch (Exception e) { LOG.error("Failed to save .desktop file at: {}", desktopFilePath, e); return false; } LOG.debug("Wrote .desktop file to {}", desktopFilePath); return true; } private static boolean writeLogoFile(String logoFile, Path logoPath) { try (InputStream is = DesktopEntryUtils.class.getResourceAsStream("/logos/" + logoFile)) { FileUtils.writeFile(logoPath, is); } catch (Exception e) { LOG.error("Failed to write logo file at: {}", logoPath, e); return false; } LOG.debug("Wrote logo file to: {}", logoPath); return true; } public static @Nullable String getLaunchScriptPath() { String launchScriptPath = System.getProperty("jadx.launchScript.path"); if (launchScriptPath == null || launchScriptPath.isEmpty()) { LOG.error( "The jadx.launchScript.path property is not set. Please launch JADX with the bundled launch script or set it to the appropriate value yourself."); return null; } LOG.debug("JADX launch script path: {}", launchScriptPath); return launchScriptPath; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/FontUtils.java ================================================ package jadx.gui.utils; import java.awt.Font; import java.io.InputStream; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.exceptions.JadxRuntimeException; public class FontUtils { private static final Logger LOG = LoggerFactory.getLogger(FontUtils.class); public static Font loadByStr(String fontDesc) { String[] parts = fontDesc.split("/"); if (parts.length != 3) { throw new JadxRuntimeException("Unsupported font description format: " + fontDesc); } String family = parts[0]; int style = parseFontStyle(parts[1]); int size = Integer.parseInt(parts[2]); Font font = FontUtils.getCompositeFont(family, style, size); if (font == null) { throw new JadxRuntimeException("Font not found: " + fontDesc); } return font; } public static String convertToStr(@Nullable Font font) { if (font == null) { return ""; } if (font.getSize() < 1) { throw new JadxRuntimeException("Bad font size: " + font.getSize()); } return font.getFamily() + '/' + convertFontStyleToString(font.getStyle()) + '/' + font.getSize(); } public static String convertFontStyleToString(int style) { if (style == 0) { return "plain"; } StringBuilder sb = new StringBuilder(); if ((style & Font.BOLD) != 0) { sb.append("bold"); } if ((style & Font.ITALIC) != 0) { sb.append(" italic"); } return sb.toString().trim(); } private static int parseFontStyle(String str) { int style = 0; if (str.contains("bold")) { style |= Font.BOLD; } if (str.contains("italic")) { style |= Font.ITALIC; } return style; } @Nullable public static Font openFontTTF(String name) { String fontPath = "/fonts/" + name + ".ttf"; try (InputStream is = UiUtils.class.getResourceAsStream(fontPath)) { Font font = Font.createFont(Font.TRUETYPE_FONT, is); return font.deriveFont(12f); } catch (Exception e) { LOG.error("Failed load font by path: {}", fontPath, e); return null; } } public static boolean canStringBeDisplayed(String str, Font font) { if (str == null || str.isEmpty()) { return true; } int offset = 0; while (offset < str.length()) { int codePoint = str.codePointAt(offset); if (!font.canDisplay(codePoint)) { return false; } offset += Character.charCount(codePoint); } return true; } /** * https://github.com/JFormDesigner/FlatLaf/issues/923 * When changing fonts, use font.deriveFont() or FontUtils.getCompositeFont * instead of new Font(), otherwise FlatLaf's CJKs support will be lost */ public static Font getCompositeFont(String family, int style, int size) { return com.formdev.flatlaf.util.FontUtils.getCompositeFont(family, style, size); } private FontUtils() { } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/HexUtils.java ================================================ package jadx.gui.utils; public class HexUtils { public static boolean isValidHexString(String hexString) { String cleanS = hexString.replace(" ", ""); int len = cleanS.length(); try { boolean isPair = len % 2 == 0; if (isPair) { Long.parseLong(cleanS, 16); return true; } } catch (NumberFormatException ex) { // ignore error return false; } return false; } public static byte[] hexStringToByteArray(String hexString) { if (hexString == null || hexString.isEmpty()) { return new byte[0]; } String cleanS = hexString.replace(" ", ""); int len = cleanS.length(); if (!isValidHexString(hexString)) { throw new IllegalArgumentException("Hex string must have even length. Input length: " + len); } byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { String byteString = cleanS.substring(i, i + 2); try { int intValue = Integer.parseInt(byteString, 16); data[i / 2] = (byte) intValue; } catch (NumberFormatException e) { throw new IllegalArgumentException("Input string contains non-hex characters at index " + i + ": " + byteString, e); } } return data; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/ILoadListener.java ================================================ package jadx.gui.utils; public interface ILoadListener { /** * Update files/project loaded state * * @return true to remove listener */ boolean update(boolean loaded); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/IOUtils.java ================================================ package jadx.gui.utils; import java.io.IOException; import java.io.InputStream; import org.jetbrains.annotations.Nullable; public class IOUtils { /** * This method can be deleted once Jadx is Java11+ */ @Nullable public static byte[] readNBytes(InputStream inputStream, int len) throws IOException { byte[] payload = new byte[len]; int readSize = 0; while (true) { int read = inputStream.read(payload, readSize, len - readSize); if (read == -1) { return null; } readSize += read; if (readSize == len) { return payload; } } } public static int read(InputStream inputStream, byte[] buf) throws IOException { return read(inputStream, buf, 0, buf.length); } public static int read(InputStream inputStream, byte[] buf, int off, int len) throws IOException { int remainingBytes = len; while (remainingBytes > 0) { int start = len - remainingBytes; int bytesRead = inputStream.read(buf, off + start, remainingBytes); if (bytesRead == -1) { break; } remainingBytes -= bytesRead; } return len - remainingBytes; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/Icons.java ================================================ package jadx.gui.utils; import javax.swing.ImageIcon; import static jadx.gui.utils.UiUtils.openSvgIcon; public class Icons { public static final ImageIcon OPEN = openSvgIcon("ui/openDisk"); public static final ImageIcon OPEN_PROJECT = openSvgIcon("ui/projectDirectory"); public static final ImageIcon NEW_PROJECT = openSvgIcon("ui/newFolder"); public static final ImageIcon CLOSE = openSvgIcon("ui/closeHovered"); public static final ImageIcon CLOSE_INACTIVE = openSvgIcon("ui/close"); public static final ImageIcon SAVE_ALL = UiUtils.openSvgIcon("ui/menu-saveall"); public static final ImageIcon FLAT_PKG = UiUtils.openSvgIcon("ui/moduleGroup"); public static final ImageIcon QUICK_TABS = UiUtils.openSvgIcon("ui/dataView"); public static final ImageIcon PIN = UiUtils.openSvgIcon("nodes/pin"); public static final ImageIcon PIN_DARK = UiUtils.openSvgIcon("nodes/pin_dark"); public static final ImageIcon PIN_HOVERED = UiUtils.openSvgIcon("nodes/pinHovered"); public static final ImageIcon PIN_HOVERED_DARK = UiUtils.openSvgIcon("nodes/pinHovered_dark"); public static final ImageIcon BOOKMARK = UiUtils.openSvgIcon("nodes/bookmark"); public static final ImageIcon BOOKMARK_OVERLAY = UiUtils.openSvgIcon("nodes/bookmark_overlay"); public static final ImageIcon BOOKMARK_DARK = UiUtils.openSvgIcon("nodes/bookmark_dark"); public static final ImageIcon BOOKMARK_OVERLAY_DARK = UiUtils.openSvgIcon("nodes/bookmark_overlay_dark"); public static final ImageIcon STATIC = openSvgIcon("nodes/staticMark"); public static final ImageIcon FINAL = openSvgIcon("nodes/finalMark"); public static final ImageIcon START_PAGE = openSvgIcon("nodes/newWindow"); public static final ImageIcon FOLDER = UiUtils.openSvgIcon("nodes/folder"); public static final ImageIcon FILE = UiUtils.openSvgIcon("nodes/file_any_type"); public static final ImageIcon PACKAGE = UiUtils.openSvgIcon("nodes/package"); public static final ImageIcon CLASS = UiUtils.openSvgIcon("nodes/class"); public static final ImageIcon METHOD = UiUtils.openSvgIcon("nodes/method"); public static final ImageIcon FIELD = UiUtils.openSvgIcon("nodes/field"); public static final ImageIcon PROPERTY = UiUtils.openSvgIcon("nodes/property"); public static final ImageIcon PARAMETER = UiUtils.openSvgIcon("nodes/parameter"); public static final ImageIcon RUN = UiUtils.openSvgIcon("ui/run"); public static final ImageIcon CHECK = UiUtils.openSvgIcon("ui/checkConstraint"); public static final ImageIcon FORMAT = UiUtils.openSvgIcon("ui/toolWindowMessages"); public static final ImageIcon RESET = UiUtils.openSvgIcon("ui/reset"); public static final ImageIcon FONT = UiUtils.openSvgIcon("nodes/fontFile"); public static final ImageIcon ICON_MARK = UiUtils.openSvgIcon("search/mark"); public static final ImageIcon ICON_MARK_SELECTED = UiUtils.openSvgIcon("search/previewSelected"); public static final ImageIcon ICON_REGEX = UiUtils.openSvgIcon("search/regexHovered"); public static final ImageIcon ICON_REGEX_SELECTED = UiUtils.openSvgIcon("search/regexSelected"); public static final ImageIcon ICON_WORDS = UiUtils.openSvgIcon("search/wordsHovered"); public static final ImageIcon ICON_WORDS_SELECTED = UiUtils.openSvgIcon("search/wordsSelected"); public static final ImageIcon ICON_MATCH = UiUtils.openSvgIcon("search/matchCaseHovered"); public static final ImageIcon ICON_MATCH_SELECTED = UiUtils.openSvgIcon("search/matchCaseSelected"); public static final ImageIcon ICON_UP = UiUtils.openSvgIcon("ui/top"); public static final ImageIcon ICON_DOWN = UiUtils.openSvgIcon("ui/bottom"); public static final ImageIcon ICON_CLOSE = UiUtils.openSvgIcon("ui/close"); public static final ImageIcon ICON_FIND_TYPE_TXT = UiUtils.openSvgIcon("search/text"); public static final ImageIcon ICON_FIND_TYPE_HEX = UiUtils.openSvgIcon("search/hexSerial"); public static final ImageIcon ICON_ACTIVE_TAB = UiUtils.openSvgIcon("search/activeTab"); public static final ImageIcon ICON_ACTIVE_TAB_SELECTED = UiUtils.openSvgIcon("search/activeTabSelected"); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/IconsCache.java ================================================ package jadx.gui.utils; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.swing.ImageIcon; public class IconsCache { private static final Map SVG_ICONS = new ConcurrentHashMap<>(); public static ImageIcon getSVGIcon(String name) { return SVG_ICONS.computeIfAbsent(name, UiUtils::openSvgIcon); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java ================================================ package jadx.gui.utils; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import jadx.api.JavaClass; import jadx.api.JavaField; import jadx.api.JavaMethod; import jadx.api.JavaNode; import jadx.api.JavaPackage; import jadx.api.JavaVariable; import jadx.api.metadata.ICodeNodeRef; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.JadxWrapper; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JField; import jadx.gui.treemodel.JMethod; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JPackage; import jadx.gui.treemodel.JVariable; public class JNodeCache { private final JadxWrapper wrapper; private final Map cache = new ConcurrentHashMap<>(); public JNodeCache(JadxWrapper wrapper) { this.wrapper = wrapper; } public JNode makeFrom(ICodeNodeRef nodeRef) { if (nodeRef == null) { return null; } // don't use 'computeIfAbsent' method here, it will cause 'Recursive update' exception JNode jNode = cache.get(nodeRef); if (jNode == null || jNode.getJavaNode().getCodeNodeRef() != nodeRef) { jNode = convert(nodeRef); cache.put(nodeRef, jNode); } return jNode; } public void put(ICodeNodeRef nodeRef, JNode jNode) { cache.put(nodeRef, jNode); } public void put(JavaNode javaNode, JNode jNode) { cache.put(javaNode.getCodeNodeRef(), jNode); } public JNode makeFrom(JavaNode javaNode) { if (javaNode == null) { return null; } return makeFrom(javaNode.getCodeNodeRef()); } public JClass makeFrom(JavaClass javaCls) { if (javaCls == null) { return null; } ICodeNodeRef nodeRef = javaCls.getCodeNodeRef(); JClass jCls = (JClass) cache.get(nodeRef); if (jCls == null || jCls.getCls() != javaCls) { jCls = convert(javaCls); cache.put(nodeRef, jCls); } return jCls; } public JPackage newJPackage(JavaPackage javaPkg, boolean synthetic, boolean pkgEnabled, List classes) { JPackage jPackage = new JPackage(javaPkg, pkgEnabled, classes, new ArrayList<>(), synthetic); put(javaPkg, jPackage); return jPackage; } public void remove(JavaNode javaNode) { cache.remove(javaNode.getCodeNodeRef()); } public void removeWholeClass(JavaClass javaCls) { remove(javaCls); /* * These javaCls.get...() calls require the class to be loaded, or will force it to load, generating * a potentially large decompilation task if needed, before throwing away that work when the class * is unloaded. To avoid this, which is very slow, we only bother to remove things from the cache if * the class is already loaded. If it's not then there either isn't going to be anything relevant in * the node cache or decompilation would regenerate the cache anyway. */ // if (true) { if (!javaCls.loadingWouldRequireDecompilation()) { javaCls.getMethods().forEach(this::remove); javaCls.getFields().forEach(this::remove); javaCls.getInnerClasses().forEach(this::remove); javaCls.getInlinedClasses().forEach(this::remove); } } public void reset() { cache.clear(); } private JClass convert(JavaClass cls) { JavaClass parentCls = cls.getDeclaringClass(); if (parentCls == cls) { return new JClass(cls, null, this); } return new JClass(cls, makeFrom(parentCls), this); } private JNode convert(ICodeNodeRef nodeRef) { JavaNode javaNode = wrapper.getDecompiler().getJavaNodeByRef(nodeRef); return convert(javaNode); } private JNode convert(JavaNode node) { if (node == null) { return null; } if (node instanceof JavaClass) { return convert((JavaClass) node); } if (node instanceof JavaMethod) { return new JMethod((JavaMethod) node, makeFrom(node.getDeclaringClass())); } if (node instanceof JavaField) { return new JField((JavaField) node, makeFrom(node.getDeclaringClass())); } if (node instanceof JavaVariable) { JavaVariable javaVar = (JavaVariable) node; JMethod jMth = (JMethod) makeFrom(javaVar.getMth()); return new JVariable(jMth, javaVar); } if (node instanceof JavaPackage) { throw new JadxRuntimeException("Unexpected JPackage (missing from cache): " + node); } throw new JadxRuntimeException("Unknown type for JavaNode: " + node.getClass()); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/JumpManager.java ================================================ package jadx.gui.utils; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; public class JumpManager { /** * Maximum number of elements to store in a jump list */ private static final int MAX_JUMPS = 100; /** * This number of elements will be removed from the start if a list becomes bigger than MAX_JUMPS. * List grow most of the time, so removing should be done in big batches to not run very often. * Because of this, an effective jump history size will vary * from (MAX_JUMPS - LIST_SHRINK_COUNT) to MAX_JUMPS over time. */ private static final int LIST_SHRINK_COUNT = 50; private final List list = new ArrayList<>(MAX_JUMPS); private int currentPos = 0; public void addPosition(@Nullable JumpPosition pos) { if (pos == null || ignoreJump(pos)) { return; } currentPos++; if (currentPos >= list.size()) { list.add(pos); if (list.size() >= MAX_JUMPS) { list.subList(0, LIST_SHRINK_COUNT).clear(); } currentPos = list.size() - 1; } else { // discard forward history after navigating back and jumping to a new place list.set(currentPos, pos); list.subList(currentPos + 1, list.size()).clear(); } } public int size() { return list.size(); } private boolean ignoreJump(JumpPosition pos) { JumpPosition current = getCurrent(); if (current == null) { return false; } return pos.equals(current); } public @Nullable JumpPosition getCurrent() { if (currentPos >= 0 && currentPos < list.size()) { return list.get(currentPos); } return null; } @Nullable public JumpPosition getPrev() { if (currentPos == 0) { return null; } currentPos--; return list.get(currentPos); } @Nullable public JumpPosition getNext() { int size = list.size(); if (size == 0) { currentPos = 0; return null; } int newPos = currentPos + 1; if (newPos >= size) { currentPos = size - 1; return null; } JumpPosition position = list.get(newPos); if (position == null) { return null; } currentPos = newPos; return position; } public void reset() { list.clear(); currentPos = 0; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/JumpPosition.java ================================================ package jadx.gui.utils; import jadx.core.utils.Utils; import jadx.gui.treemodel.JNode; public class JumpPosition { private final JNode node; private int pos; public JumpPosition(JNode node) { this(node, node.getPos()); } public JumpPosition(JNode node, int pos) { this.node = Utils.getOrElse(node.getRootClass(), node); this.pos = pos; } public int getPos() { return pos; } public void setPos(int pos) { this.pos = pos; } public JNode getNode() { return node; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof JumpPosition)) { return false; } JumpPosition jump = (JumpPosition) obj; return pos == jump.pos && node.equals(jump.node); } @Override public int hashCode() { return 31 * node.hashCode() + pos; } @Override public String toString() { return "Jump{" + node + ":" + pos + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/LafManager.java ================================================ package jadx.gui.utils; import java.util.LinkedHashMap; import java.util.Map; import javax.swing.UIManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.formdev.flatlaf.FlatDarculaLaf; import com.formdev.flatlaf.FlatDarkLaf; import com.formdev.flatlaf.FlatIntelliJLaf; import com.formdev.flatlaf.FlatLightLaf; import com.formdev.flatlaf.intellijthemes.FlatAllIJThemes; import com.formdev.flatlaf.themes.FlatMacDarkLaf; import com.formdev.flatlaf.themes.FlatMacLightLaf; import jadx.gui.settings.JadxSettings; public class LafManager { private static final Logger LOG = LoggerFactory.getLogger(LafManager.class); public static final String INITIAL_THEME_NAME = FlatLightLaf.NAME; private static final Map THEMES_MAP = initThemesMap(); public static void init(JadxSettings settings) { String preferredThemeClass = getThemeClass(settings); // reset if settings refers to missing theme if (preferredThemeClass == null) { settings.setLafTheme(INITIAL_THEME_NAME); preferredThemeClass = getThemeClass(settings); } if (setupLaf(preferredThemeClass)) { return; } setupLaf(INITIAL_THEME_NAME); settings.setLafTheme(INITIAL_THEME_NAME); settings.sync(); } public static boolean updateLaf(JadxSettings settings) { return setupLaf(getThemeClass(settings)); } public static String[] getThemes() { return THEMES_MAP.keySet().toArray(new String[0]); } private static String getThemeClass(JadxSettings settings) { return THEMES_MAP.get(settings.getLafTheme()); } private static boolean setupLaf(String themeClass) { if (themeClass != null && !themeClass.isEmpty()) { return applyLaf(themeClass); } return false; } private static Map initThemesMap() { Map map = new LinkedHashMap<>(); // default flatlaf themes map.put(FlatLightLaf.NAME, FlatLightLaf.class.getName()); map.put(FlatDarkLaf.NAME, FlatDarkLaf.class.getName()); map.put(FlatMacLightLaf.NAME, FlatMacLightLaf.class.getName()); map.put(FlatMacDarkLaf.NAME, FlatMacDarkLaf.class.getName()); map.put(FlatIntelliJLaf.NAME, FlatIntelliJLaf.class.getName()); map.put(FlatDarculaLaf.NAME, FlatDarculaLaf.class.getName()); // themes from flatlaf-intellij-themes for (FlatAllIJThemes.FlatIJLookAndFeelInfo themeInfo : FlatAllIJThemes.INFOS) { map.put(themeInfo.getName(), themeInfo.getClassName()); } return map; } private static boolean applyLaf(String theme) { try { UIManager.setLookAndFeel(theme); return true; } catch (Exception e) { LOG.error("Failed to set laf to {}", theme, e); return false; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/LangLocale.java ================================================ package jadx.gui.utils; import java.util.Locale; public class LangLocale { private Locale locale; // Don't remove. Used in json serialization public LangLocale() { } public LangLocale(Locale locale) { this.locale = locale; } public LangLocale(String l, String c) { this.locale = new Locale(l, c); } public Locale get() { return locale; } public Locale getLocale() { return locale; } public void setLocale(Locale locale) { this.locale = locale; } @Override public String toString() { return NLS.str("language.name", this); } @Override public boolean equals(Object obj) { return obj instanceof LangLocale && locale.equals(((LangLocale) obj).get()); } @Override public int hashCode() { return locale.hashCode(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/Link.java ================================================ package jadx.gui.utils; import java.awt.Cursor; import java.awt.Desktop; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Map; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JTextArea; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.commons.app.JadxSystemInfo; import static java.awt.Desktop.Action; public class Link extends JLabel { private static final long serialVersionUID = 3655322136444908178L; private static final Logger LOG = LoggerFactory.getLogger(Link.class); private String url; public Link() { super(); init(); } public Link(String text, String url) { super(text); init(); setUrl(url); } private void init() { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); this.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { browse(); } }); } public void setUrl(String url) { this.url = url; setToolTipText("Open " + url + " in your browser"); } private void browse() { if (Desktop.isDesktopSupported()) { Desktop desktop = Desktop.getDesktop(); if (desktop.isSupported(Action.BROWSE)) { try { desktop.browse(new java.net.URI(url)); return; } catch (Exception e) { LOG.debug("Open url error", e); } } } try { if (JadxSystemInfo.IS_WINDOWS) { new ProcessBuilder() .command(new String[] { "rundll32", "url.dll,FileProtocolHandler", url }) .start(); return; } if (JadxSystemInfo.IS_MAC) { new ProcessBuilder() .command(new String[] { "open", url }) .start(); return; } Map env = System.getenv(); if (env.get("BROWSER") != null) { new ProcessBuilder() .command(new String[] { env.get("BROWSER"), url }) .start(); return; } } catch (Exception e) { LOG.debug("Open url error", e); } showUrlDialog(); } private void showUrlDialog() { JTextArea urlArea = new JTextArea("Can't open browser. Please browse to:\n" + url); JOptionPane.showMessageDialog(null, urlArea); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/NLS.java ================================================ package jadx.gui.utils; import java.io.InputStreamReader; import java.io.Reader; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; import java.util.Vector; import jadx.core.utils.exceptions.JadxRuntimeException; public class NLS { private static final Vector LANG_LOCALES = new Vector<>(); private static final Map LANG_LOCALES_MAP = new HashMap<>(); private static final ResourceBundle FALLBACK_MESSAGES_MAP; private static final LangLocale LOCAL_LOCALE; // Use these two fields to avoid invoking Map.get() method twice. private static ResourceBundle localizedMessagesMap; private static LangLocale currentLocale; static { LOCAL_LOCALE = new LangLocale(Locale.getDefault()); LANG_LOCALES.add(new LangLocale("en", "US")); // As default language LANG_LOCALES.add(new LangLocale("zh", "CN")); LANG_LOCALES.add(new LangLocale("zh", "TW")); LANG_LOCALES.add(new LangLocale("es", "ES")); LANG_LOCALES.add(new LangLocale("de", "DE")); LANG_LOCALES.add(new LangLocale("ko", "KR")); LANG_LOCALES.add(new LangLocale("pt", "BR")); LANG_LOCALES.add(new LangLocale("ru", "RU")); LANG_LOCALES.add(new LangLocale("id", "ID")); LANG_LOCALES.forEach(NLS::load); LangLocale defLang = LANG_LOCALES.get(0); FALLBACK_MESSAGES_MAP = LANG_LOCALES_MAP.get(defLang); localizedMessagesMap = LANG_LOCALES_MAP.get(defLang); } private NLS() { } private static void load(LangLocale lang) { Locale locale = lang.get(); String resName = String.format("i18n/Messages_%s.properties", locale.toLanguageTag().replace('-', '_')); URL bundleUrl = NLS.class.getClassLoader().getResource(resName); if (bundleUrl == null) { throw new JadxRuntimeException("Locale resource not found: " + resName); } ResourceBundle bundle; try (Reader reader = new InputStreamReader(bundleUrl.openStream(), StandardCharsets.UTF_8)) { bundle = new PropertyResourceBundle(reader); } catch (Exception e) { throw new JadxRuntimeException("Failed to load " + resName, e); } LANG_LOCALES_MAP.put(lang, bundle); } public static String str(String key) { try { return localizedMessagesMap.getString(key); } catch (Exception e) { return FALLBACK_MESSAGES_MAP.getString(key); } } public static String str(String key, Object... parameters) { return String.format(str(key), parameters); } public static String str(String key, LangLocale locale) { ResourceBundle bundle = LANG_LOCALES_MAP.get(locale); if (bundle != null) { try { return bundle.getString(key); } catch (MissingResourceException ignored) { // use fallback string } } return FALLBACK_MESSAGES_MAP.getString(key); // definitely exists } public static void setLocale(LangLocale locale) { if (LANG_LOCALES_MAP.containsKey(locale)) { currentLocale = locale; } else { currentLocale = LANG_LOCALES.get(0); } localizedMessagesMap = LANG_LOCALES_MAP.get(currentLocale); } public static Vector getLangLocales() { return LANG_LOCALES; } public static LangLocale currentLocale() { return currentLocale; } public static LangLocale defaultLocale() { if (LANG_LOCALES_MAP.containsKey(LOCAL_LOCALE)) { return LOCAL_LOCALE; } // fallback to english if unsupported return LANG_LOCALES.get(0); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/ObjectPool.java ================================================ package jadx.gui.utils; import java.lang.ref.WeakReference; import java.util.concurrent.ConcurrentLinkedQueue; public class ObjectPool { private final ConcurrentLinkedQueue> pool = new ConcurrentLinkedQueue<>(); private final Creator creator; public interface Creator { T create(); } public ObjectPool(Creator creator) { this.creator = creator; } public T get() { T node; do { WeakReference wNode = pool.poll(); if (wNode == null) { return creator.create(); } node = wNode.get(); } while (node == null); return node; } public void put(T node) { pool.add(new WeakReference<>(node)); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/OverlayIcon.java ================================================ package jadx.gui.utils; import java.awt.Component; import java.awt.Graphics; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.swing.Icon; public class OverlayIcon implements Icon { private final Icon icon; private final List icons = new ArrayList<>(4); private static final double A = 0.8; private static final double B = 0.2; private static final double[] OVERLAY_POS = new double[] { A, B, B, B, A, A, B, A }; public OverlayIcon(Icon icon) { this.icon = icon; } public OverlayIcon(Icon icon, Icon... ovrIcons) { this.icon = icon; Collections.addAll(icons, ovrIcons); } @Override public int getIconHeight() { return icon.getIconHeight(); } @Override public int getIconWidth() { return icon.getIconWidth(); } @Override public void paintIcon(Component c, Graphics g, int x, int y) { int w = getIconWidth(); int h = getIconHeight(); icon.paintIcon(c, g, x, y); int k = 0; for (Icon subIcon : icons) { int dx = (int) (OVERLAY_POS[k++] * (w - subIcon.getIconWidth())); int dy = (int) (OVERLAY_POS[k++] * (h - subIcon.getIconHeight())); subIcon.paintIcon(c, g, x + dx, y + dy); } } public void add(Icon icon) { icons.add(icon); } public void remove(Icon icon) { icons.remove(icon); } public void clear() { icons.clear(); } public List getIcons() { return icons; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/PathTypeAdapter.java ================================================ package jadx.gui.utils; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; public class PathTypeAdapter { private static final TypeAdapter SINGLETON = new TypeAdapter<>() { @Override public void write(JsonWriter out, Path value) throws IOException { if (value == null) { out.nullValue(); } else { out.value(value.toAbsolutePath().toString()); } } @Override public Path read(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } return Paths.get(in.nextString()); } }; public static TypeAdapter singleton() { return SINGLETON; } private PathTypeAdapter() { } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/RectangleTypeAdapter.java ================================================ package jadx.gui.utils; import java.awt.Rectangle; import java.io.IOException; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; public class RectangleTypeAdapter { private static final TypeAdapter SINGLETON = new TypeAdapter<>() { @Override public void write(JsonWriter out, Rectangle value) throws IOException { if (value == null) { out.nullValue(); } else { out.beginObject(); out.name("x").value(value.getX()); out.name("y").value(value.getY()); out.name("width").value(value.getWidth()); out.name("height").value(value.getHeight()); out.endObject(); } } @Override public Rectangle read(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } in.beginObject(); Rectangle rectangle = new Rectangle(); while (in.hasNext()) { String name = in.nextName(); switch (name) { case "x": rectangle.x = in.nextInt(); break; case "y": rectangle.y = in.nextInt(); break; case "width": rectangle.width = in.nextInt(); break; case "height": rectangle.height = in.nextInt(); break; default: throw new IllegalArgumentException("Unknown field in Rectangle: " + name); } } in.endObject(); return rectangle; } }; public static TypeAdapter singleton() { return SINGLETON; } private RectangleTypeAdapter() { } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/RelativePathTypeAdapter.java ================================================ package jadx.gui.utils; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; public class RelativePathTypeAdapter extends TypeAdapter { private static final Logger LOG = LoggerFactory.getLogger(RelativePathTypeAdapter.class); private final Path basePath; public RelativePathTypeAdapter(Path basePath) { this.basePath = Objects.requireNonNull(basePath); } @Override public void write(JsonWriter out, Path value) throws IOException { if (value == null) { out.nullValue(); } else { value = value.toAbsolutePath().normalize(); Path resultPath; try { resultPath = basePath.relativize(value); } catch (IllegalArgumentException e) { LOG.warn("Unable to build a relative path to {} - using absolute path", value); resultPath = value; } out.value(resultPath.toString()); } } @Override public Path read(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } Path p = Paths.get(in.nextString()); if (p.isAbsolute()) { return p; } return basePath.resolve(p); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/SimpleListener.java ================================================ package jadx.gui.utils; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; public class SimpleListener { private final List> listeners = new ArrayList<>(); public void sendUpdate(T data) { for (Consumer listener : listeners) { listener.accept(data); } } public void addListener(Consumer listener) { listeners.add(listener); } public boolean removeListener(Consumer listener) { return listeners.remove(listener); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/TextStandardActions.java ================================================ package jadx.gui.utils; import java.awt.Toolkit; import java.awt.datatransfer.DataFlavor; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JPopupMenu; import javax.swing.KeyStroke; import javax.swing.text.JTextComponent; import javax.swing.undo.UndoManager; public class TextStandardActions { private final JTextComponent textComponent; private final JPopupMenu popup = new JPopupMenu(); private final UndoManager undoManager; private Action undoAction; private Action redoAction; private Action cutAction; private Action copyAction; private Action pasteAction; private Action deleteAction; private Action selectAllAction; public static void attach(JTextComponent textComponent) { new TextStandardActions(textComponent); } public TextStandardActions(JTextComponent textComponent) { this.textComponent = textComponent; this.undoManager = new UndoManager(); initActions(); addPopupItems(); addKeyActions(); registerListeners(); } private void initActions() { undoAction = new AbstractAction(NLS.str("popup.undo")) { @Override public void actionPerformed(ActionEvent ae) { if (undoManager.canUndo()) { undoManager.undo(); } } }; redoAction = new AbstractAction(NLS.str("popup.redo")) { @Override public void actionPerformed(ActionEvent ae) { if (undoManager.canRedo()) { undoManager.redo(); } } }; cutAction = new AbstractAction(NLS.str("popup.cut")) { @Override public void actionPerformed(ActionEvent ae) { textComponent.cut(); } }; copyAction = new AbstractAction(NLS.str("popup.copy")) { @Override public void actionPerformed(ActionEvent ae) { textComponent.copy(); } }; pasteAction = new AbstractAction(NLS.str("popup.paste")) { @Override public void actionPerformed(ActionEvent ae) { textComponent.paste(); } }; deleteAction = new AbstractAction(NLS.str("popup.delete")) { @Override public void actionPerformed(ActionEvent ae) { textComponent.replaceSelection(""); } }; selectAllAction = new AbstractAction(NLS.str("popup.select_all")) { @Override public void actionPerformed(ActionEvent ae) { textComponent.selectAll(); } }; } void addPopupItems() { popup.add(undoAction); popup.add(redoAction); popup.addSeparator(); popup.add(cutAction); popup.add(copyAction); popup.add(pasteAction); popup.add(deleteAction); popup.addSeparator(); popup.add(selectAllAction); } private void addKeyActions() { KeyStroke undoKey = KeyStroke.getKeyStroke(KeyEvent.VK_Z, UiUtils.ctrlButton()); textComponent.getInputMap().put(undoKey, undoAction); KeyStroke redoKey = KeyStroke.getKeyStroke(KeyEvent.VK_R, UiUtils.ctrlButton()); textComponent.getInputMap().put(redoKey, redoAction); } private void registerListeners() { textComponent.addMouseListener(new MouseAdapter() { @Override public void mouseReleased(MouseEvent e) { if (e.getButton() == 3 && e.getSource() == textComponent) { process(e); } } }); textComponent.getDocument().addUndoableEditListener(event -> undoManager.addEdit(event.getEdit())); } private void process(MouseEvent e) { textComponent.requestFocus(); boolean enabled = textComponent.isEnabled(); boolean editable = textComponent.isEditable(); boolean nonempty = !(textComponent.getText() == null || textComponent.getText().isEmpty()); boolean marked = textComponent.getSelectedText() != null; boolean pasteAvailable = Toolkit.getDefaultToolkit().getSystemClipboard() .getContents(null).isDataFlavorSupported(DataFlavor.stringFlavor); undoAction.setEnabled(enabled && editable && undoManager.canUndo()); redoAction.setEnabled(enabled && editable && undoManager.canRedo()); cutAction.setEnabled(enabled && editable && marked); copyAction.setEnabled(enabled && marked); pasteAction.setEnabled(enabled && editable && pasteAvailable); deleteAction.setEnabled(enabled && editable && marked); selectAllAction.setEnabled(enabled && nonempty); int nx = e.getX(); if (nx > 500) { nx = nx - popup.getSize().width; } popup.show(e.getComponent(), nx, e.getY() - popup.getSize().height); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java ================================================ package jadx.gui.utils; import java.awt.Color; import java.awt.Component; import java.awt.Image; import java.awt.MouseInfo; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.io.File; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.JTextField; import javax.swing.JTree; import javax.swing.KeyStroke; import javax.swing.RootPaneContainer; import javax.swing.SwingUtilities; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.TestOnly; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.formdev.flatlaf.extras.FlatSVGIcon; import jadx.commons.app.JadxCommonEnv; import jadx.commons.app.JadxSystemInfo; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.jobs.ITaskProgress; import jadx.gui.ui.codearea.AbstractCodeArea; public class UiUtils { private static final Logger LOG = LoggerFactory.getLogger(UiUtils.class); public static final boolean JADX_GUI_DEBUG = JadxCommonEnv.getBool("JADX_GUI_DEBUG", false); /** * The minimum about of memory in bytes we are trying to keep free, otherwise the application may * run out of heap * which ends up in a Java garbage collector running "amok" (CPU utilization 100% for each core and * the UI is * not responsive). *

* We can calculate and store this value here as the maximum heap is fixed for each JVM instance * and can't be changed at runtime. */ public static final long MIN_FREE_MEMORY = calculateMinFreeMemory(); public static final Runnable EMPTY_RUNNABLE = new Runnable() { @Override public void run() { } @Override public String toString() { return "EMPTY_RUNNABLE"; } }; private static final ExecutorService BACKGROUND_THREAD = Executors.newSingleThreadExecutor(Utils.simpleThreadFactory("utils-bg")); private UiUtils() { } public static ImageIcon openSvgIcon(String name) { String iconPath = "icons/" + name + ".svg"; FlatSVGIcon icon = new FlatSVGIcon(iconPath); boolean found; try { found = icon.hasFound(); } catch (Exception e) { throw new JadxRuntimeException("Failed to load icon: " + iconPath, e); } if (!found) { throw new JadxRuntimeException("Icon not found: " + iconPath); } return icon; } public static ImageIcon openIcon(String name) { String iconPath = "/icons-16/" + name + ".png"; URL resource = UiUtils.class.getResource(iconPath); if (resource == null) { throw new JadxRuntimeException("Icon not found: " + iconPath); } return new ImageIcon(resource); } public static Image openImage(String path) { URL resource = UiUtils.class.getResource(path); if (resource == null) { throw new JadxRuntimeException("Image not found: " + path); } return Toolkit.getDefaultToolkit().createImage(resource); } public static void addKeyBinding(JComponent comp, KeyStroke key, String id, Runnable action) { addKeyBinding(comp, key, id, new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { action.run(); } }); } public static void addKeyBinding(JComponent comp, KeyStroke key, String id, Action action) { comp.getInputMap().put(key, id); comp.getActionMap().put(id, action); } public static void removeKeyBinding(JComponent comp, KeyStroke key, String id) { comp.getInputMap().remove(key); comp.getActionMap().remove(id); } public static String typeFormat(String name, ArgType type) { return name + " " + typeStr(type); } public static String typeFormatHtml(String name, ArgType type) { return wrapHtml(escapeHtml(name) + ' ' + fadeHtml(escapeHtml(typeStr(type)))); } public static String fadeHtml(String htmlStr) { return "" + htmlStr + ""; // TODO: get color from theme } public static String wrapHtml(String htmlStr) { return "" + htmlStr + ""; } public static String escapeHtml(String str) { return str.replace("<", "<").replace(">", ">"); } private static final String CUT_STR_REPLACE = "..."; public static String limitStringLength(String str, int maxLength) { if (str.length() <= maxLength) { return str; } char fileSepChar = File.separatorChar; int firstFileSep = str.indexOf(fileSepChar); if (firstFileSep != -1) { // remove path parts int lastFileSep = str.lastIndexOf(fileSepChar); if (firstFileSep == lastFileSep) { // single path char => cut before str = CUT_STR_REPLACE + str.substring(lastFileSep - 1); } else { // cut middle str = str.substring(0, firstFileSep + 1) + CUT_STR_REPLACE + str.substring(lastFileSep); } if (str.length() < maxLength) { return str; } } // cut end by default return str.substring(0, maxLength - CUT_STR_REPLACE.length()) + CUT_STR_REPLACE; } public static String typeStr(ArgType type) { if (type == null) { return "null"; } if (type.isObject()) { if (type.isGenericType()) { return type.getObject(); } ArgType wt = type.getWildcardType(); if (wt != null) { ArgType.WildcardBound bound = type.getWildcardBound(); if (bound == ArgType.WildcardBound.UNBOUND) { return bound.getStr(); } return bound.getStr() + typeStr(wt); } String objName = objectShortName(type.getObject()); ArgType outerType = type.getOuterType(); if (outerType != null) { return typeStr(outerType) + '.' + objName; } List genericTypes = type.getGenericTypes(); if (genericTypes != null) { String generics = Utils.listToString(genericTypes, ", ", UiUtils::typeStr); return objName + '<' + generics + '>'; } return objName; } if (type.isArray()) { return typeStr(type.getArrayElement()) + "[]"; } return type.toString(); } private static String objectShortName(String obj) { int dot = obj.lastIndexOf('.'); if (dot != -1) { return obj.substring(dot + 1); } return obj; } public static OverlayIcon makeIcon(AccessInfo af, Icon pub, Icon pri, Icon pro, Icon def) { Icon icon; if (af.isPublic()) { icon = pub; } else if (af.isPrivate()) { icon = pri; } else if (af.isProtected()) { icon = pro; } else { icon = def; } OverlayIcon overIcon = new OverlayIcon(icon); if (af.isFinal()) { overIcon.add(Icons.FINAL); } if (af.isStatic()) { overIcon.add(Icons.STATIC); } return overIcon; } /** * @return 20% of the maximum heap size limited to 512 MB (bytes) */ public static long calculateMinFreeMemory() { Runtime runtime = Runtime.getRuntime(); long minFree = (long) (runtime.maxMemory() * 0.2); return Math.min(minFree, 512 * 1024L * 1024L); } public static boolean isFreeMemoryAvailable() { Runtime runtime = Runtime.getRuntime(); long maxMemory = runtime.maxMemory(); long totalFree = runtime.freeMemory() + (maxMemory - runtime.totalMemory()); return totalFree > MIN_FREE_MEMORY; } public static String memoryInfo() { Runtime runtime = Runtime.getRuntime(); long maxMemory = runtime.maxMemory(); long allocatedMemory = runtime.totalMemory(); long freeMemory = runtime.freeMemory(); return "heap: " + format(allocatedMemory - freeMemory) + ", allocated: " + format(allocatedMemory) + ", free: " + format(freeMemory) + ", total free: " + format(freeMemory + maxMemory - allocatedMemory) + ", max: " + format(maxMemory); } private static String format(long mem) { return (long) (mem / (double) (1024L * 1024L)) + "MB"; } public static void setClipboardString(String text) { try { Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); Transferable transferable = new StringSelection(text); clipboard.setContents(transferable, null); LOG.debug("String '{}' copied to clipboard", text); } catch (Exception e) { LOG.error("Failed copy string '{}' to clipboard", text, e); } } public static void setWindowIcons(Window window) { List icons = new ArrayList<>(); icons.add(UiUtils.openImage("/logos/jadx-logo-16px.png")); icons.add(UiUtils.openImage("/logos/jadx-logo-32px.png")); icons.add(UiUtils.openImage("/logos/jadx-logo-48px.png")); icons.add(UiUtils.openImage("/logos/jadx-logo.png")); window.setIconImages(icons); } public static final int CTRL_BNT_KEY = getCtrlButton(); @SuppressWarnings("deprecation") @MagicConstant(flagsFromClass = InputEvent.class) private static int getCtrlButton() { if (JadxSystemInfo.IS_MAC) { return Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); } else { return InputEvent.CTRL_DOWN_MASK; } } @MagicConstant(flagsFromClass = InputEvent.class) public static int ctrlButton() { return CTRL_BNT_KEY; } public static boolean isCtrlDown(KeyEvent keyEvent) { return keyEvent.getModifiersEx() == CTRL_BNT_KEY; } public static void addEscapeShortCutToDispose(T window) { KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); window.getRootPane().registerKeyboardAction(e -> window.dispose(), stroke, JComponent.WHEN_IN_FOCUSED_WINDOW); } /** * Get closest offset at mouse position * * @return -1 on error */ @SuppressWarnings("deprecation") public static int getOffsetAtMousePosition(AbstractCodeArea codeArea) { try { Point mousePos = getMousePosition(codeArea); return codeArea.viewToModel(mousePos); } catch (Exception e) { LOG.error("Failed to get offset at mouse position", e); return -1; } } public static Point getMousePosition(Component comp) { Point pos = MouseInfo.getPointerInfo().getLocation(); SwingUtilities.convertPointFromScreen(pos, comp); return pos; } public static TreeNode getTreeNodeUnderMouse(JTree tree, MouseEvent mouseEvent) { TreePath path = tree.getClosestPathForLocation(mouseEvent.getX(), mouseEvent.getY()); if (path == null) { return null; } // allow 'closest' path only at the right of the item row Rectangle pathBounds = tree.getPathBounds(path); if (pathBounds != null) { int y = mouseEvent.getY(); if (y < pathBounds.y || y > (pathBounds.y + pathBounds.height)) { return null; } if (mouseEvent.getX() < pathBounds.x) { // exclude expand/collapse events return null; } } Object obj = path.getLastPathComponent(); if (obj instanceof TreeNode) { tree.setSelectionPath(path); return (TreeNode) obj; } return null; } public static void showMessageBox(Component parent, String msg) { JOptionPane.showMessageDialog(parent, msg); } public static void errorMessage(Component parent, String message) { errorMessage(parent, NLS.str("message.errorTitle"), message); } public static void errorMessage(Component parent, String title, String message) { LOG.error(message); JOptionPane.showMessageDialog(parent, message, title, JOptionPane.ERROR_MESSAGE); } public static void copyToClipboard(String text) { if (StringUtils.isEmpty(text)) { return; } try { Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); StringSelection selection = new StringSelection(text); clipboard.setContents(selection, selection); } catch (Exception e) { LOG.error("Failed copy text to clipboard", e); } } /** * Owner field in Clipboard class can store reference to CodeArea. * This prevents from garbage collection whole jadx object tree and cause memory leak. * Trying to lost ownership by new empty selection. */ public static void resetClipboardOwner() { try { Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemSelection(); if (clipboard != null) { StringSelection selection = new StringSelection(""); clipboard.setContents(selection, selection); } } catch (Exception e) { LOG.error("Failed to reset clipboard owner", e); } } public static int calcProgress(ITaskProgress taskProgress) { return calcProgress(taskProgress.progress(), taskProgress.total()); } public static int calcProgress(long done, long total) { if (done > total) { LOG.debug("Task progress has invalid values: done={}, total={}", done, total); return 100; } return Math.round(done * 100 / (float) total); } public static void sleep(int ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { // ignore } } public static void uiRun(Runnable runnable) { SwingUtilities.invokeLater(runnable); } public static void uiRunAndWait(Runnable runnable) { if (SwingUtilities.isEventDispatchThread()) { runnable.run(); return; } try { SwingUtilities.invokeAndWait(runnable); } catch (InterruptedException e) { LOG.warn("UI thread interrupted, runnable: {}", runnable, e); } catch (Exception e) { throw new RuntimeException(e); } } /** * Run task in background thread. * Uses single thread, so all tasks are ordered. */ public static void bgRun(Runnable runnable) { BACKGROUND_THREAD.execute(runnable); } public static void uiThreadGuard() { if (JADX_GUI_DEBUG && !SwingUtilities.isEventDispatchThread()) { LOG.warn("Expect UI thread, got: {}", Thread.currentThread(), new JadxRuntimeException()); } } public static void notUiThreadGuard() { if (JADX_GUI_DEBUG && SwingUtilities.isEventDispatchThread()) { LOG.warn("Expect background thread, got: {}", Thread.currentThread(), new JadxRuntimeException()); } } @TestOnly public static void debugTimer(int periodInSeconds, Runnable action) { if (!LOG.isDebugEnabled()) { return; } Timer timer = new Timer(); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { action.run(); } }, 0, periodInSeconds * 1000L); } @TestOnly public static void printStackTrace(String label) { LOG.debug("StackTrace: {}", label, new Exception(label)); } public static boolean isDarkTheme(Color background) { double brightness = (background.getRed() * 0.299 + background.getGreen() * 0.587 + background.getBlue() * 0.114) / 255; return brightness < 0.5; } /** * Adjusts the brightness of a given {@code Color} object without altering its hue or saturation. * *

* This method converts the input RGB color to the HSB (Hue, Saturation, Brightness) color model, * multiplies its brightness component by the specified {@code factor}, and then converts it back * to a new RGB {@code Color} object. *

* *

* The new brightness value is capped at {@code 1.0f} (maximum HSB brightness) to prevent * colors from becoming invalid or exceeding full brightness. *

* * How to use: *
    *
  • To make a color **brighter**: Use a {@code factor} greater than {@code 1.0f} (e.g., * {@code 1.2f}, {@code 1.5f}).
  • *
  • To make a color **darker**: Use a {@code factor} less than {@code 1.0f} (e.g., {@code 0.8f}, * {@code 0.5f}).
  • *
  • To keep the brightness **unchanged**: Use a {@code factor} of {@code 1.0f}.
  • *
* *
{@code
	 * // Example usage:
	 * Color originalColor = Color.BLUE;
	 *
	 * // Make the blue color 50% brighter (factor 1.5)
	 * Color brighterBlue = adjustBrightness(originalColor, 1.5f);
	 *
	 * // Make the blue color 30% darker (factor 0.7)
	 * Color darkerBlue = adjustBrightness(originalColor, 0.7f);
	 *
	 * // Get the brightest possible version of the color (will cap at 1.0 brightness)
	 * Color maxBrightnessBlue = adjustBrightness(originalColor, 10.0f);
	 *
	 * // Get a very dark, almost black version
	 * Color veryDarkBlue = adjustBrightness(originalColor, 0.1f);
	 * }
* * @param color The original {@code Color} object whose brightness needs to be adjusted. * @param factor The multiplier for the brightness. * @return A new {@code Color} object with the adjusted brightness. * @see java.awt.Color#RGBtoHSB(int, int, int, float[]) * @see java.awt.Color#getHSBColor(float, float, float) */ public static Color adjustBrightness(Color color, float factor) { float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); hsb[2] = Math.min(1.0f, hsb[2] * factor); // Adjust brightness return Color.getHSBColor(hsb[0], hsb[1], hsb[2]); } public static void highlightAsErrorField(final JTextField field, boolean isError) { if (isError) { field.putClientProperty("JComponent.outline", "error"); } else { field.putClientProperty("JComponent.outline", ""); } field.repaint(); } public static boolean nearlyEqual(float a, float b) { return Math.abs(a - b) < 1E-6f; } // Formats a string to be in a .DOT node public static String toDotNodeName(String fullName) { String newName = fullName.replace("<", "\\<"); newName = newName.replace(">", "\\>"); return newName; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/cache/ValueCache.java ================================================ package jadx.gui.utils.cache; import java.util.function.Function; /** * Simple store for values depending on 'key' object. * * @param key object type * @param stored object type */ public class ValueCache { private K key; private V value; /** * Return a stored object if key not changed, load a new object overwise. */ public synchronized V get(K requestKey, Function loadFunc) { if (key != null && key.equals(requestKey)) { return value; } V newValue = loadFunc.apply(requestKey); key = requestKey; value = newValue; return newValue; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/dbg/UIWatchDog.java ================================================ package jadx.gui.utils.dbg; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.SwingUtilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.utils.UiUtils; /** * Watch for UI thread state, if it stuck log a warning with stacktrace */ public class UIWatchDog { private static final Logger LOG = LoggerFactory.getLogger(UIWatchDog.class); private static final int UI_MAX_DELAY_MS = 200; private static final int CHECK_INTERVAL_MS = 50; public static boolean onStart() { UiUtils.uiRunAndWait(UIWatchDog::toggle); return INSTANCE.isEnabled(); } public static synchronized void toggle() { if (SwingUtilities.isEventDispatchThread()) { INSTANCE.toggleState(Thread.currentThread()); } else { throw new JadxRuntimeException("This method should be called in UI thread"); } } private static final UIWatchDog INSTANCE = new UIWatchDog(); private final AtomicBoolean enabled = new AtomicBoolean(false); private final ExecutorService executor = Executors.newSingleThreadExecutor(); private Future taskFuture; private UIWatchDog() { // singleton } private void toggleState(Thread uiThread) { if (enabled.get()) { // stop enabled.set(false); if (taskFuture != null) { try { taskFuture.get(CHECK_INTERVAL_MS * 5, TimeUnit.MILLISECONDS); } catch (Throwable e) { LOG.warn("Stopping UI watchdog error", e); } } } else { // start enabled.set(true); taskFuture = executor.submit(() -> start(uiThread)); } } private boolean isEnabled() { return enabled.get(); } @SuppressWarnings("BusyWait") private void start(Thread uiThread) { LOG.debug("UI watchdog started"); try { Exception e = new JadxRuntimeException("at"); TimeMeasure tm = new TimeMeasure(); boolean stuck = false; long reportTime = 0; while (enabled.get()) { if (uiThread.getState() == Thread.State.TIMED_WAITING) { if (!stuck) { tm.start(); stuck = true; reportTime = UI_MAX_DELAY_MS; } else { tm.end(); long time = tm.getTime(); if (time > reportTime) { e.setStackTrace(uiThread.getStackTrace()); LOG.warn("UI events thread stuck for {}ms", time, e); reportTime += UI_MAX_DELAY_MS; } } } else { stuck = false; } Thread.sleep(CHECK_INTERVAL_MS); } } catch (Throwable e) { LOG.error("UI watchdog fail", e); } LOG.debug("UI watchdog stopped"); } private static final class TimeMeasure { private long start; private long end; public void start() { start = System.currentTimeMillis(); } public void end() { end = System.currentTimeMillis(); } public long getTime() { return end - start; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/files/JadxFiles.java ================================================ package jadx.gui.utils.files; import java.nio.file.Path; import jadx.commons.app.JadxCommonFiles; public class JadxFiles { private static final Path CONFIG_DIR = JadxCommonFiles.getConfigDir(); public static final Path GUI_CONF = CONFIG_DIR.resolve("gui.json"); public static final Path CACHES_LIST = CONFIG_DIR.resolve("caches.json"); public static final Path CACHE_DIR = JadxCommonFiles.getCacheDir(); public static final Path PROJECTS_CACHE_DIR = CACHE_DIR.resolve("projects"); } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/fileswatcher/FilesWatcher.java ================================================ package jadx.gui.utils.fileswatcher; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.nio.file.attribute.BasicFileAttributes; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.Utils; import static java.nio.file.LinkOption.NOFOLLOW_LINKS; import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; import static java.nio.file.StandardWatchEventKinds.OVERFLOW; public class FilesWatcher { private static final Logger LOG = LoggerFactory.getLogger(FilesWatcher.class); private final WatchService watcher = FileSystems.getDefault().newWatchService(); private final Map keys = new HashMap<>(); private final Map> files = new HashMap<>(); private final AtomicBoolean cancel = new AtomicBoolean(false); private final BiConsumer> listener; public FilesWatcher(List paths, BiConsumer> listener) throws IOException { this.listener = listener; for (Path path : paths) { if (Files.isDirectory(path, NOFOLLOW_LINKS)) { registerDirs(path); } else { Path parentDir = path.toAbsolutePath().getParent(); register(parentDir); files.merge(parentDir, Collections.singleton(path), Utils::mergeSets); } } } public void cancel() { cancel.set(true); } @SuppressWarnings("unchecked") public void watch() { cancel.set(false); LOG.debug("File watcher started for {} dirs", keys.size()); while (!cancel.get()) { WatchKey key; try { key = watcher.take(); } catch (InterruptedException e) { LOG.debug("File watcher interrupted"); return; } Path dir = keys.get(key); if (dir == null) { LOG.warn("Unknown directory key: {}", key); continue; } for (WatchEvent event : key.pollEvents()) { if (cancel.get() || Thread.interrupted()) { return; } WatchEvent.Kind kind = event.kind(); if (kind == OVERFLOW) { continue; } Path fileName = ((WatchEvent) event).context(); Path path = dir.resolve(fileName); Set files = this.files.get(dir); if (files == null || files.contains(path)) { listener.accept(path, (WatchEvent.Kind) kind); } if (kind == ENTRY_CREATE) { try { if (Files.isDirectory(path, NOFOLLOW_LINKS)) { registerDirs(path); } } catch (Exception e) { LOG.warn("Failed to update directory watch: {}", path, e); } } } boolean valid = key.reset(); if (!valid) { keys.remove(key); if (keys.isEmpty()) { LOG.debug("File watcher stopped: all watch keys removed"); return; } } } } private void registerDirs(Path start) throws IOException { Files.walkFileTree(start, new SimpleFileVisitor() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { register(dir); return FileVisitResult.CONTINUE; } }); } private void register(Path dir) throws IOException { WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); keys.put(key, dir); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/fileswatcher/LiveReloadWorker.java ================================================ package jadx.gui.utils.fileswatcher; import java.nio.file.Path; import java.nio.file.WatchEvent; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.reactivex.rxjava3.processors.PublishProcessor; import jadx.gui.ui.MainWindow; import jadx.gui.utils.UiUtils; public class LiveReloadWorker { private static final Logger LOG = LoggerFactory.getLogger(LiveReloadWorker.class); private final MainWindow mainWindow; private final PublishProcessor processor; private volatile boolean started = false; private ExecutorService executor; private FilesWatcher watcher; @SuppressWarnings("ResultOfMethodCallIgnored") public LiveReloadWorker(MainWindow mainWindow) { this.mainWindow = mainWindow; this.processor = PublishProcessor.create(); this.processor .debounce(1, TimeUnit.SECONDS) .subscribe(path -> { LOG.debug("Reload triggered"); UiUtils.uiRun(mainWindow::reopen); }); } public boolean isStarted() { return started; } public synchronized void updateState(boolean enabled) { if (this.started == enabled) { return; } if (enabled) { LOG.debug("Starting live reload worker"); start(); } else { LOG.debug("Stopping live reload worker"); stop(); } } private void onUpdate(Path path, WatchEvent.Kind pathKind) { LOG.debug("Path updated: {}", path); processor.onNext(path); } private synchronized void start() { try { watcher = new FilesWatcher(mainWindow.getProject().getFilePaths(), this::onUpdate); executor = Executors.newSingleThreadExecutor(); started = true; executor.submit(watcher::watch); } catch (Exception e) { LOG.warn("Failed to start live reload worker", e); resetState(); } } private synchronized void stop() { try { watcher.cancel(); executor.shutdownNow(); boolean canceled = executor.awaitTermination(5, TimeUnit.SECONDS); if (!canceled) { LOG.warn("Failed to cancel live reload worker"); } } catch (Exception e) { LOG.warn("Failed to stop live reload worker", e); } finally { resetState(); } } private void resetState() { started = false; executor = null; watcher = null; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/layout/WrapLayout.java ================================================ package jadx.gui.utils.layout; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Insets; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import org.intellij.lang.annotations.MagicConstant; /** * FlowLayout subclass that fully supports wrapping of components. */ public class WrapLayout extends FlowLayout { private static final long serialVersionUID = 6109752116520941346L; private Dimension preferredLayoutSize; /** * Constructs a new WrapLayout with a left * alignment and a default 5-unit horizontal and vertical gap. */ public WrapLayout() { super(); } /** * Constructs a new FlowLayout with the specified * alignment and a default 5-unit horizontal and vertical gap. * The value of the alignment argument must be one of * WrapLayout, WrapLayout, * or WrapLayout. * * @param align the alignment value */ public WrapLayout(@MagicConstant(valuesFromClass = FlowLayout.class) int align) { super(align); } /** * Creates a new flow layout manager with the indicated alignment * and the indicated horizontal and vertical gaps. *

* The value of the alignment argument must be one of * WrapLayout, WrapLayout, * or WrapLayout. * * @param align the alignment value * @param hgap the horizontal gap between components * @param vgap the vertical gap between components */ public WrapLayout(int align, int hgap, int vgap) { super(align, hgap, vgap); } /** * Returns the preferred dimensions for this layout given the * visible components in the specified target container. * * @param target the component which needs to be laid out * @return the preferred dimensions to lay out the * subcomponents of the specified container */ @Override public Dimension preferredLayoutSize(Container target) { return layoutSize(target, true); } /** * Returns the minimum dimensions needed to layout the visible * components contained in the specified target container. * * @param target the component which needs to be laid out * @return the minimum dimensions to lay out the * subcomponents of the specified container */ @Override public Dimension minimumLayoutSize(Container target) { Dimension minimum = layoutSize(target, false); minimum.width -= (getHgap() + 1); return minimum; } /** * Returns the minimum or preferred dimension needed to layout the target * container. * * @param target target to get layout size for * @param preferred should preferred size be calculated * @return the dimension to layout the target container */ private Dimension layoutSize(Container target, boolean preferred) { synchronized (target.getTreeLock()) { // Each row must fit with the width allocated to the container. // When the container width = 0, the preferred width of the container // has not yet been calculated so lets ask for the maximum. int targetWidth = target.getSize().width; Container container = target; while (container.getSize().width == 0 && container.getParent() != null) { container = container.getParent(); } targetWidth = container.getSize().width; if (targetWidth == 0) { targetWidth = Integer.MAX_VALUE; } int hgap = getHgap(); int vgap = getVgap(); Insets insets = target.getInsets(); int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2); int maxWidth = targetWidth - horizontalInsetsAndGap; // Fit components into the allowed width Dimension dim = new Dimension(0, 0); int rowWidth = 0; int rowHeight = 0; int nmembers = target.getComponentCount(); for (int i = 0; i < nmembers; i++) { Component m = target.getComponent(i); if (m.isVisible()) { Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize(); int width = d.width; // Can't add the component to current row. Start a new row. if (rowWidth + width >= maxWidth) { addRow(dim, rowWidth, rowHeight); rowWidth = 0; rowHeight = 0; } // Add a horizontal gap for all components after the first if (rowWidth != 0) { rowWidth += hgap; } rowWidth += width; rowHeight = Math.max(rowHeight, d.height); } } addRow(dim, rowWidth, rowHeight); dim.width += horizontalInsetsAndGap; dim.height += insets.top + insets.bottom + vgap * 2; // When using a scroll pane or the DecoratedLookAndFeel we need to // make sure the preferred size is less than the size of the // target container so shrinking the container size works // correctly. Removing the horizontal gap is an easy way to do this. Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target); if (scrollPane != null && target.isValid()) { dim.width -= (hgap + 1); } return dim; } } /* * A new row has been completed. Use the dimensions of this row * to update the preferred size for the container. * @param dim update the width and height when appropriate * @param rowWidth the width of the row to add * @param rowHeight the height of the row to add */ private void addRow(Dimension dim, int rowWidth, int rowHeight) { dim.width = Math.max(dim.width, rowWidth); if (dim.height > 0) { dim.height += getVgap(); } dim.height += rowHeight; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/pkgs/JRenamePackage.java ================================================ package jadx.gui.utils.pkgs; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.Icon; import org.apache.commons.lang3.StringUtils; import jadx.api.JavaNode; import jadx.api.JavaPackage; import jadx.api.data.ICodeRename; import jadx.api.data.impl.JadxCodeRename; import jadx.api.data.impl.JadxNodeRef; import jadx.core.deobf.NameMapper; import jadx.gui.treemodel.JRenameNode; import jadx.gui.ui.MainWindow; import jadx.gui.utils.Icons; import static jadx.core.deobf.NameMapper.VALID_JAVA_IDENTIFIER; public class JRenamePackage implements JRenameNode { private final JavaPackage refPkg; private final String rawFullName; private final String fullName; private final String name; public JRenamePackage(JavaPackage refPkg, String rawFullName, String fullName, String name) { this.refPkg = refPkg; this.rawFullName = rawFullName; this.fullName = fullName; this.name = name; } @Override public JavaNode getJavaNode() { return refPkg; } @Override public String getTitle() { return fullName; } @Override public String getName() { return name; } @Override public Icon getIcon() { return Icons.PACKAGE; } @Override public boolean canRename() { return true; } @Override public ICodeRename buildCodeRename(String newName, Set renames) { return new JadxCodeRename(JadxNodeRef.forPkg(rawFullName), newName); } @Override public boolean isValidName(String newName) { return isValidPackageName(newName); } private static final Pattern PACKAGE_RENAME_PATTERN = Pattern.compile( "(\\.)?PKG(\\.PKG)*".replace("PKG", VALID_JAVA_IDENTIFIER.pattern())); static boolean isValidPackageName(String newName) { if (newName == null || newName.isEmpty() || NameMapper.isReserved(newName)) { return false; } Matcher matcher = PACKAGE_RENAME_PATTERN.matcher(newName); if (!matcher.matches()) { return false; } for (String part : StringUtils.split(newName, '.')) { if (NameMapper.isReserved(part)) { return false; } } return true; } @Override public void removeAlias() { refPkg.removeAlias(); } @Override public void addUpdateNodes(List toUpdate) { refPkg.addUseIn(toUpdate); } @Override public void reload(MainWindow mainWindow) { mainWindow.rebuildPackagesTree(); mainWindow.reloadTreePreservingState(); } @Override public String toString() { return refPkg.toString(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/pkgs/PackageHelper.java ================================================ package jadx.gui.utils.pkgs; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JavaPackage; import jadx.core.dex.info.PackageInfo; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.gui.JadxWrapper; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JPackage; import jadx.gui.utils.JNodeCache; public class PackageHelper { private static final Logger LOG = LoggerFactory.getLogger(PackageHelper.class); private static final Comparator CLASS_COMPARATOR = Comparator.comparing(JClass::getName, String.CASE_INSENSITIVE_ORDER); private static final Comparator PKG_COMPARATOR = Comparator.comparing(JPackage::getName, String.CASE_INSENSITIVE_ORDER); private final JadxWrapper wrapper; private final JNodeCache nodeCache; private List excludedPackages; private final Map pkgInfoMap = new HashMap<>(); public PackageHelper(JadxWrapper wrapper, JNodeCache jNodeCache) { this.wrapper = wrapper; this.nodeCache = jNodeCache; } public List getRoots(boolean flatPackages) { excludedPackages = wrapper.getExcludedPackages(); pkgInfoMap.clear(); if (flatPackages) { return prepareFlatPackages(); } long start = System.currentTimeMillis(); List roots = prepareHierarchyPackages(); if (LOG.isDebugEnabled()) { LOG.debug("Prepare hierarchy packages in {} ms", System.currentTimeMillis() - start); } return roots; } public List getRenameNodes(JPackage pkg) { List list = new ArrayList<>(); PackageInfo pkgInfo = pkg.getPkg().getPkgNode().getAliasPkgInfo(); Set added = new HashSet<>(); do { JPackage jPkg = pkgInfoMap.get(pkgInfo); if (jPkg != null && !jPkg.isSynthetic()) { JavaPackage javaPkg = jPkg.getPkg(); if (!javaPkg.isDefault()) { JRenamePackage renamePkg = new JRenamePackage(javaPkg, javaPkg.getRawFullName(), javaPkg.getFullName(), javaPkg.getName()); if (added.add(javaPkg.getFullName())) { list.add(renamePkg); } } } pkgInfo = pkgInfo.getParentPkg(); } while (pkgInfo != null); return list; } private List prepareFlatPackages() { List list = new ArrayList<>(); for (JavaPackage javaPkg : wrapper.getPackages()) { if (javaPkg.isLeaf() || !javaPkg.getClasses().isEmpty()) { JPackage pkg = buildJPackage(javaPkg, false); pkg.setName(javaPkg.getFullName()); list.add(pkg); pkgInfoMap.put(javaPkg.getPkgNode().getAliasPkgInfo(), pkg); } } list.sort(PKG_COMPARATOR); return list; } private List prepareHierarchyPackages() { JPackage root = JPackage.makeTmpRoot(); List packages = wrapper.getPackages(); List jPackages = new ArrayList<>(packages.size()); // create nodes for exists packages for (JavaPackage javaPkg : packages) { JPackage jPkg = buildJPackage(javaPkg, false); jPackages.add(jPkg); PackageInfo aliasPkgInfo = javaPkg.getPkgNode().getAliasPkgInfo(); jPkg.setName(aliasPkgInfo.getName()); pkgInfoMap.put(aliasPkgInfo, jPkg); if (aliasPkgInfo.isRoot()) { root.getSubPackages().add(jPkg); } } // link subpackages, create missing packages created by renames for (JPackage jPkg : jPackages) { if (jPkg.getPkg().isLeaf()) { buildLeafPath(jPkg, root, pkgInfoMap); } } List toMerge = new ArrayList<>(); traverseMiddlePackages(root, toMerge); Utils.treeDfsVisit(root, JPackage::getSubPackages, v -> v.getSubPackages().sort(PKG_COMPARATOR)); return root.getSubPackages(); } private void buildLeafPath(JPackage jPkg, JPackage root, Map pkgMap) { JPackage currentJPkg = jPkg; PackageInfo current = jPkg.getPkg().getPkgNode().getAliasPkgInfo(); while (true) { current = current.getParentPkg(); if (current == null) { break; } JPackage parentJPkg = pkgMap.get(current); if (parentJPkg == null) { parentJPkg = buildJPackage(currentJPkg.getPkg(), true); parentJPkg.setName(current.getName()); pkgMap.put(current, parentJPkg); if (current.isRoot()) { root.getSubPackages().add(parentJPkg); } } List subPackages = parentJPkg.getSubPackages(); String pkgName = currentJPkg.getName(); if (ListUtils.noneMatch(subPackages, p -> p.getName().equals(pkgName))) { subPackages.add(currentJPkg); } currentJPkg = parentJPkg; } } private static void traverseMiddlePackages(JPackage pkg, List toMerge) { List subPackages = pkg.getSubPackages(); int count = subPackages.size(); for (int i = 0; i < count; i++) { JPackage subPackage = subPackages.get(i); JPackage replacePkg = mergeMiddlePackages(subPackage, toMerge); if (replacePkg != subPackage) { subPackages.set(i, replacePkg); } traverseMiddlePackages(replacePkg, toMerge); } } private static JPackage mergeMiddlePackages(JPackage jPkg, List merged) { List subPackages = jPkg.getSubPackages(); if (subPackages.size() == 1 && jPkg.getClasses().isEmpty()) { merged.add(jPkg); JPackage endPkg = mergeMiddlePackages(subPackages.get(0), merged); merged.clear(); return endPkg; } if (!merged.isEmpty()) { merged.add(jPkg); jPkg.setName(Utils.listToString(merged, ".", JPackage::getName)); } return jPkg; } private JPackage buildJPackage(JavaPackage javaPkg, boolean synthetic) { boolean pkgEnabled = isPkgEnabled(javaPkg.getRawFullName(), excludedPackages); List classes; if (synthetic) { classes = Collections.emptyList(); } else { classes = Utils.collectionMap(javaPkg.getClassesNoDup(), nodeCache::makeFrom); classes.sort(CLASS_COMPARATOR); } return nodeCache.newJPackage(javaPkg, synthetic, pkgEnabled, classes); } private static boolean isPkgEnabled(String fullPkgName, List excludedPackages) { return excludedPackages.isEmpty() || excludedPackages.stream().noneMatch(p -> isPkgMatch(fullPkgName, p)); } private static boolean isPkgMatch(String fullPkgName, String filterPkg) { if (fullPkgName.equals(filterPkg)) { return true; } // optimized check, same as `fullPkgName.startsWith(filterPkg + '.')` int filterPkgLen = filterPkg.length(); return fullPkgName.length() > filterPkgLen && fullPkgName.charAt(filterPkgLen) == '.' && fullPkgName.startsWith(filterPkg); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/plugins/CloseablePlugins.java ================================================ package jadx.gui.utils.plugins; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.core.plugins.PluginContext; public class CloseablePlugins { private final List list; private final @Nullable Runnable closeable; public CloseablePlugins(List list, @Nullable Runnable closeable) { this.list = list; this.closeable = closeable; } public void close() { if (closeable != null) { closeable.run(); } } public @Nullable Runnable getCloseable() { return closeable; } public List getList() { return list; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/plugins/CollectPlugins.java ================================================ package jadx.gui.utils.plugins; import java.util.ArrayList; import java.util.Optional; import java.util.SortedSet; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.cli.plugins.JadxFilesGetter; import jadx.core.plugins.AppContext; import jadx.core.plugins.JadxPluginManager; import jadx.core.plugins.PluginContext; import jadx.gui.ui.MainWindow; import jadx.plugins.tools.JadxExternalPluginsLoader; /** * Collect all plugins. * Init not yet loaded plugins in new temporary context. * Support a case if decompiler in wrapper is not initialized yet. */ public class CollectPlugins { private final MainWindow mainWindow; public CollectPlugins(MainWindow mainWindow) { this.mainWindow = mainWindow; } public CloseablePlugins build() { Optional currentDecompiler = mainWindow.getWrapper().getCurrentDecompiler(); if (currentDecompiler.isPresent()) { JadxDecompiler decompiler = currentDecompiler.get(); SortedSet plugins = decompiler.getPluginManager().getResolvedPluginContexts(); return new CloseablePlugins(new ArrayList<>(plugins), null); } // collect and init plugins in new temp context JadxArgs jadxArgs = mainWindow.getSettings().toJadxArgs(); jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE); try (JadxDecompiler decompiler = new JadxDecompiler(jadxArgs)) { JadxPluginManager pluginManager = decompiler.getPluginManager(); pluginManager.registerAddPluginListener(pluginContext -> { AppContext appContext = new AppContext(); appContext.setGuiContext(null); // load temp plugins without UI context appContext.setFilesGetter(jadxArgs.getFilesGetter()); pluginContext.setAppContext(appContext); }); pluginManager.load(new JadxExternalPluginsLoader()); SortedSet allPlugins = pluginManager.getAllPluginContexts(); pluginManager.init(allPlugins); Runnable closeable = () -> pluginManager.unload(allPlugins); return new CloseablePlugins(new ArrayList<>(allPlugins), closeable); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/plugins/PluginWithOptions.java ================================================ package jadx.gui.utils.plugins; import org.jetbrains.annotations.NotNull; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.options.JadxPluginOptions; public class PluginWithOptions implements Comparable { public static final PluginWithOptions NULL = new PluginWithOptions(null, null); private final JadxPlugin plugin; private final JadxPluginOptions options; public PluginWithOptions(JadxPlugin plugin, JadxPluginOptions options) { this.plugin = plugin; this.options = options; } public JadxPlugin getPlugin() { return plugin; } public JadxPluginOptions getOptions() { return options; } @Override public int compareTo(@NotNull PluginWithOptions other) { return plugin.getClass().getName().compareTo(other.getClass().getName()); } @Override public String toString() { return "PluginWithOptions{plugin=" + plugin + ", options=" + options + '}'; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/plugins/SettingsGroupPluginWrap.java ================================================ package jadx.gui.utils.plugins; import java.util.Collections; import java.util.List; import javax.swing.JComponent; import javax.swing.JLabel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.gui.ISettingsGroup; public class SettingsGroupPluginWrap implements ISettingsGroup { private static final Logger LOG = LoggerFactory.getLogger(SettingsGroupPluginWrap.class); private final String pluginId; private final ISettingsGroup pluginSettingGroup; public SettingsGroupPluginWrap(String pluginId, ISettingsGroup pluginSettingGroup) { this.pluginId = pluginId; this.pluginSettingGroup = pluginSettingGroup; } @Override public String getTitle() { try { return pluginSettingGroup.getTitle(); } catch (Throwable t) { LOG.warn("Failed to get settings group title for plugin: {}", pluginId, t); return ""; } } @Override public JComponent buildComponent() { try { return pluginSettingGroup.buildComponent(); } catch (Throwable t) { LOG.warn("Failed to build settings group component for plugin: {}", pluginId, t); return new JLabel(""); } } @Override public List getSubGroups() { try { return pluginSettingGroup.getSubGroups(); } catch (Throwable t) { LOG.warn("Failed to get settings group sub-groups for plugin: {}", pluginId, t); return Collections.emptyList(); } } @Override public void close(boolean save) { try { pluginSettingGroup.close(save); } catch (Throwable t) { LOG.warn("Failed to close settings group for plugin: {}", pluginId, t); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/plugins/TreeInputsHelper.java ================================================ package jadx.gui.utils.plugins; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.gui.plugins.context.ITreeInputCategory; import jadx.gui.treemodel.JNode; import jadx.gui.ui.MainWindow; public class TreeInputsHelper { private static final Logger LOG = LoggerFactory.getLogger(TreeInputsHelper.class); private final List categoryData; private List simpleFiles; public TreeInputsHelper(MainWindow mainWindow) { categoryData = mainWindow.getWrapper().getGuiPluginsContext() .getTreeInputCategories() .stream() .map(CategoryData::new) .collect(Collectors.toList()); } public void processInputs(List files) { simpleFiles = new ArrayList<>(files.size()); for (Path file : files) { boolean added = false; for (CategoryData data : categoryData) { if (data.filesFilter(file)) { added = true; break; } } if (!added) { simpleFiles.add(file); } } } public List getCustomNodes() { return categoryData.stream() .map(CategoryData::buildInputNode) .filter(Objects::nonNull) .collect(Collectors.toList()); } public List getSimpleFiles() { return simpleFiles; } private static final class CategoryData { private final ITreeInputCategory provider; private final List collectedFiles = new ArrayList<>(); private CategoryData(ITreeInputCategory provider) { this.provider = provider; } public boolean filesFilter(Path file) { try { if (provider.filesFilter(file)) { collectedFiles.add(file); return true; } } catch (Exception e) { LOG.error("Failed to filter input files", e); } return false; } public @Nullable JNode buildInputNode() { try { return provider.buildInputNode(collectedFiles); } catch (Exception e) { LOG.error("Failed to build custom input node", e); return null; } } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/res/ResTableHelper.java ================================================ package jadx.gui.utils.res; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeInfo; import jadx.api.ResourceFileContent; import jadx.api.ResourceType; import jadx.core.xmlgen.ResContainer; import jadx.gui.treemodel.JResource; import jadx.gui.treemodel.JSubResource; public class ResTableHelper { /** * Build UI tree for resource table container. * * @return root nodes */ public static List buildTree(JResource resTableRes, ResContainer resTable) { ResTableHelper resTableHelper = new ResTableHelper(resTableRes); resTableHelper.process(resTable); return resTableHelper.roots; } private final List roots = new ArrayList<>(); private final Map dirs = new HashMap<>(); private final JResource resTableRes; private ResTableHelper(JResource resTableRes) { this.resTableRes = resTableRes; } private void process(ResContainer resTable) { for (ResContainer subFile : resTable.getSubFiles()) { loadSubNodes(subFile); } } private void loadSubNodes(ResContainer rc) { String resName = rc.getName(); int split = resName.lastIndexOf('/'); String dir; String name; if (split == -1) { dir = null; name = resName; } else { dir = resName.substring(0, split); name = resName.substring(split + 1); } ICodeInfo code = rc.getText(); ResourceFileContent fileContent = new ResourceFileContent(name, ResourceType.XML, code); JResource resFile = new JSubResource(resTableRes, fileContent, resName, name, JResource.JResType.FILE); addResFile(dir, resFile); for (ResContainer subFile : rc.getSubFiles()) { loadSubNodes(subFile); } } private void addResFile(@Nullable String dir, JResource resFile) { if (dir == null) { roots.add(resFile); return; } JResource dirRes = dirs.get(dir); if (dirRes != null) { dirRes.addSubNode(resFile); return; } JResource parentDir = null; int splitPos = -1; while (true) { int prevStart = splitPos + 1; splitPos = dir.indexOf('/', prevStart); boolean last = splitPos == -1; String path = last ? dir : dir.substring(0, splitPos); JResource curDir = dirs.get(path); if (curDir == null) { String dirName = last ? dir.substring(prevStart) : dir.substring(prevStart, splitPos); curDir = new JSubResource(resTableRes, null, path, dirName, JResource.JResType.DIR); dirs.put(path, curDir); if (parentDir == null) { roots.add(curDir); } else { parentDir.addSubNode(curDir); } } if (last) { curDir.addSubNode(resFile); return; } parentDir = curDir; } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/rx/CustomDisposable.java ================================================ package jadx.gui.utils.rx; import java.util.concurrent.atomic.AtomicBoolean; import io.reactivex.rxjava3.disposables.Disposable; public class CustomDisposable implements Disposable { private final AtomicBoolean disposed = new AtomicBoolean(false); private final Runnable disposeTask; public CustomDisposable(Runnable disposeTask) { this.disposeTask = disposeTask; } @Override public void dispose() { try { disposeTask.run(); } finally { disposed.set(true); } } @Override public boolean isDisposed() { return disposed.get(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/rx/DebounceUpdate.java ================================================ package jadx.gui.utils.rx; import java.util.concurrent.TimeUnit; import io.reactivex.rxjava3.core.BackpressureStrategy; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.FlowableEmitter; import io.reactivex.rxjava3.core.FlowableOnSubscribe; import io.reactivex.rxjava3.disposables.Disposable; public class DebounceUpdate { private FlowableEmitter emitter; private final Disposable disposable; public DebounceUpdate(int timeMs, Runnable action) { FlowableOnSubscribe source = emitter -> this.emitter = emitter; disposable = Flowable.create(source, BackpressureStrategy.LATEST) .debounce(timeMs, TimeUnit.MILLISECONDS) .subscribe(v -> action.run()); } public void requestUpdate() { emitter.onNext(Boolean.TRUE); } public void dispose() { disposable.dispose(); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/rx/RxUtils.java ================================================ package jadx.gui.utils.rx; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.function.Supplier; import javax.swing.JSpinner; import javax.swing.JTextField; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentListener; import org.jetbrains.annotations.NotNull; import io.reactivex.rxjava3.core.BackpressureStrategy; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.FlowableEmitter; import io.reactivex.rxjava3.core.FlowableOnSubscribe; import jadx.gui.utils.ui.DocumentUpdateListener; public class RxUtils { public static Flowable textFieldChanges(final JTextField textField) { FlowableOnSubscribe source = emitter -> { DocumentListener listener = new DocumentUpdateListener(ev -> emitter.onNext(textField.getText())); textField.getDocument().addDocumentListener(listener); emitter.setDisposable(new CustomDisposable(() -> textField.getDocument().removeDocumentListener(listener))); }; return Flowable.create(source, BackpressureStrategy.LATEST).distinctUntilChanged(); } public static Flowable textFieldEnterPress(final JTextField textField) { FlowableOnSubscribe source = emitter -> { KeyListener keyListener = enterKeyListener(emitter, textField::getText); textField.addKeyListener(keyListener); emitter.setDisposable(new CustomDisposable(() -> textField.removeKeyListener(keyListener))); }; return Flowable.create(source, BackpressureStrategy.LATEST).distinctUntilChanged(); } public static Flowable spinnerChanges(final JSpinner spinner) { FlowableOnSubscribe source = emitter -> { ChangeListener changeListener = e -> emitter.onNext(String.valueOf(spinner.getValue())); spinner.addChangeListener(changeListener); emitter.setDisposable(new CustomDisposable(() -> spinner.removeChangeListener(changeListener))); }; return Flowable.create(source, BackpressureStrategy.LATEST).distinctUntilChanged(); } public static Flowable spinnerEnterPress(final JSpinner spinner) { FlowableOnSubscribe source = emitter -> { KeyListener keyListener = enterKeyListener(emitter, () -> String.valueOf(spinner.getValue())); spinner.addKeyListener(keyListener); emitter.setDisposable(new CustomDisposable(() -> spinner.removeKeyListener(keyListener))); }; return Flowable.create(source, BackpressureStrategy.LATEST).distinctUntilChanged(); } private static @NotNull KeyListener enterKeyListener(FlowableEmitter emitter, Supplier supplier) { return new KeyAdapter() { @Override public void keyPressed(KeyEvent ev) { if (ev.getKeyCode() == KeyEvent.VK_ENTER) { emitter.onNext(supplier.get()); } } }; } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/shortcut/Shortcut.java ================================================ package jadx.gui.utils.shortcut; import java.awt.event.KeyEvent; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import javax.swing.KeyStroke; import jadx.commons.app.JadxSystemInfo; public class Shortcut { private static final Set FORBIDDEN_KEY_CODES = new HashSet<>(List.of( KeyEvent.VK_UNDEFINED, KeyEvent.VK_SHIFT, KeyEvent.VK_ALT, KeyEvent.VK_META, KeyEvent.VK_ALT_GRAPH)); private static final Set ALLOWED_MODIFIERS = new HashSet<>(List.of( KeyEvent.CTRL_DOWN_MASK, KeyEvent.META_DOWN_MASK, KeyEvent.ALT_DOWN_MASK, KeyEvent.ALT_GRAPH_DOWN_MASK, KeyEvent.SHIFT_DOWN_MASK)); private Integer keyCode = null; private Integer modifiers = null; private Integer mouseButton = null; private Shortcut() { } public static Shortcut keyboard(int keyCode) { return keyboard(keyCode, 0); } public static Shortcut keyboard(int keyCode, int modifiers) { Shortcut shortcut = new Shortcut(); shortcut.keyCode = keyCode; shortcut.modifiers = modifiers; return shortcut; } public static Shortcut mouse(int mouseButton) { Shortcut shortcut = new Shortcut(); shortcut.mouseButton = mouseButton; return shortcut; } public static Shortcut none() { Shortcut shortcut = new Shortcut(); // Must have at least one non-null attribute in order to be serialized // otherwise will roll back to default Shortcut shortcut.modifiers = 0; return shortcut; } public Integer getKeyCode() { return keyCode; } public Integer getModifiers() { return modifiers; } public Integer getMouseButton() { return mouseButton; } public boolean isKeyboard() { return keyCode != null; } public boolean isMouse() { return mouseButton != null; } public boolean isNone() { return !isMouse() && !isKeyboard(); } public boolean isValidKeyboard() { return isKeyboard() && !FORBIDDEN_KEY_CODES.contains(keyCode) && isValidModifiers(); } public boolean isValidModifiers() { int modifiersTest = modifiers; for (Integer modifier : ALLOWED_MODIFIERS) { modifiersTest &= ~modifier; } return modifiersTest == 0; } public KeyStroke toKeyStroke() { return isKeyboard() ? KeyStroke.getKeyStroke(keyCode, modifiers, modifiers != 0 && JadxSystemInfo.IS_MAC) : null; } @Override public String toString() { if (isKeyboard()) { return keyToString(); } else if (isMouse()) { return mouseToString(); } return "NONE"; } public String getTypeString() { if (isKeyboard()) { return "Keyboard"; } else if (isMouse()) { return "Mouse"; } return null; } private String mouseToString() { return "MouseButton" + mouseButton; } private String keyToString() { StringBuilder sb = new StringBuilder(); if (modifiers != null && modifiers > 0) { sb.append(KeyEvent.getModifiersExText(modifiers)); sb.append('+'); } if (keyCode != null && keyCode != 0) { sb.append(KeyEvent.getKeyText(keyCode)); } else { sb.append("UNDEFINED"); } return sb.toString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Shortcut shortcut = (Shortcut) o; return Objects.equals(keyCode, shortcut.keyCode) && Objects.equals(modifiers, shortcut.modifiers) && Objects.equals(mouseButton, shortcut.mouseButton); } @Override public int hashCode() { return Objects.hash(keyCode, modifiers, mouseButton); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/shortcut/ShortcutsController.java ================================================ package jadx.gui.utils.shortcut; import java.awt.AWTEvent; import java.awt.Toolkit; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.JComponent; import javax.swing.KeyStroke; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.data.ShortcutsWrapper; import jadx.gui.ui.MainWindow; import jadx.gui.ui.action.ActionCategory; import jadx.gui.ui.action.ActionModel; import jadx.gui.ui.action.IShortcutAction; import jadx.gui.utils.UiUtils; public class ShortcutsController { private static final Logger LOG = LoggerFactory.getLogger(ShortcutsController.class); private final JadxSettings settings; private final Map> boundActions = new EnumMap<>(ActionModel.class); private final Map> mouseActions = new HashMap<>(); private ShortcutsWrapper shortcuts; public ShortcutsController(JadxSettings settings) { this.settings = settings; } public void loadSettings() { shortcuts = settings.getShortcuts(); indexMouseActions(); boundActions.forEach((actionModel, actions) -> { if (actions != null) { Shortcut shortcut = get(actionModel); for (IShortcutAction action : actions) { action.setShortcut(shortcut); } } }); } @Nullable public Shortcut get(ActionModel actionModel) { return shortcuts.get(actionModel); } public KeyStroke getKeyStroke(ActionModel actionModel) { Shortcut shortcut = get(actionModel); if (shortcut != null && shortcut.isKeyboard()) { return shortcut.toKeyStroke(); } return null; } /** * Binds to an action and updates its shortcut every time loadSettings is called */ public void bind(IShortcutAction action) { if (action.getShortcutComponent() == null) { LOG.warn("No shortcut component in action: {}", action, new JadxRuntimeException()); return; } boundActions.computeIfAbsent(action.getActionModel(), k -> new HashSet<>()).add(action); } /* * Immediately sets the shortcut for an action */ public void bindImmediate(IShortcutAction action) { bind(action); Shortcut shortcut = get(action.getActionModel()); action.setShortcut(shortcut); } public static Map getDefault() { Map shortcuts = new HashMap<>(); for (ActionModel actionModel : ActionModel.values()) { shortcuts.put(actionModel, actionModel.getDefaultShortcut()); } return shortcuts; } public void registerMouseEventListener(MainWindow mw) { Toolkit.getDefaultToolkit().addAWTEventListener(event -> { if (mw.isSettingsOpen()) { return; } if (!(event instanceof MouseEvent)) { return; } MouseEvent mouseEvent = (MouseEvent) event; if (mouseEvent.getID() != MouseEvent.MOUSE_PRESSED) { return; } List actions = mouseActions.get(mouseEvent.getButton()); if (actions != null) { for (IShortcutAction action : actions) { if (action != null) { mouseEvent.consume(); UiUtils.uiRun(action::performAction); } } } }, AWTEvent.MOUSE_EVENT_MASK); } private void indexMouseActions() { mouseActions.clear(); for (ActionModel actionModel : ActionModel.values()) { Shortcut shortcut = shortcuts.get(actionModel); if (shortcut != null && shortcut.isMouse()) { Set actions = boundActions.get(actionModel); if (actions != null && !actions.isEmpty()) { mouseActions.computeIfAbsent(shortcut.getMouseButton(), i -> new ArrayList<>()) .addAll(actions); } } } } public void unbindActionsForComponent(JComponent component) { for (Set actions : boundActions.values()) { if (actions != null) { actions.removeIf(action -> action == null || action.getShortcutComponent() == null || action.getShortcutComponent() == component); } } } /** * Keep only actions bound to the main window. * Other actions will be added on demand. */ public void reset() { for (ActionModel actionModel : ActionModel.values()) { if (actionModel.getCategory() != ActionCategory.MENU_TOOLBAR) { boundActions.remove(actionModel); } } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/tools/SyncNLSLines.java ================================================ package jadx.gui.utils.tools; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Automatically synchronizes i18n files with a reference (EN) file. * - Adds new lines from the reference file (commented out) to other language files. * - Removes lines from other language files that are not present in the reference file. * - Updates commented-out empty translations in other files with the reference text (commented). */ public class SyncNLSLines { private static final Logger LOG = LoggerFactory.getLogger(SyncNLSLines.class); private static final Path I18N_PATH = Paths.get("src/main/resources/i18n/"); private static final String REFERENCE_FILE_NAME = "Messages_en_US.properties"; /** * Assumes this tool runs from the project root and jadx-gui is a direct subdirectory. * If jadx-gui is the project root, this might need adjustment or to be removed * if I18N_PATH is already relative to jadx-gui. */ private static final String GUI_MODULE_DIR_NAME = "jadx-gui"; private static final Path GUI_MODULE_PREFIX_PATH = Paths.get(GUI_MODULE_DIR_NAME); public static void main(String[] args) { try { process(); } catch (Exception e) { LOG.error("Failed to process i18n files", e); } } private static void process() throws Exception { Path refPath = getRefPath(REFERENCE_FILE_NAME); if (!Files.exists(refPath)) { LOG.error("Reference i18n file not found: {}", REFERENCE_FILE_NAME); return; } LOG.info("Using reference file: {}", refPath.toAbsolutePath()); Path i18nDir = refPath.toAbsolutePath().getParent(); if (i18nDir == null) { LOG.error("Could not determine i18n directory from reference path: {}", refPath); return; } List refFileLines = Files.readAllLines(refPath, StandardCharsets.UTF_8); try (Stream pathStream = Files.list(i18nDir)) { pathStream.filter(path -> { String fileName = path.getFileName().toString(); return !fileName.equals(REFERENCE_FILE_NAME) && fileName.startsWith("Messages_") && fileName.endsWith(".properties"); }) .forEach(targetPath -> { try { LOG.info("Processing target file: {}", targetPath.toAbsolutePath()); applySync(refFileLines, targetPath); } catch (Exception e) { LOG.error("Failed to sync file: {}", targetPath, e); } }); } LOG.info("I18N synchronization process finished."); } /** * Parses a list of lines from a properties file into a map of key-value pairs. * Preserves the full line as value. Comments and empty lines will have null keys. */ private static Map parseProperties(List lines) { Map properties = new LinkedHashMap<>(); for (String line : lines) { String key = extractKey(line); // If the key is null, it's a comment or blank line, we might not need these in the map. // If we iterate over the original refFileLines later. // For simplicity here, we only store actual properties. if (key != null) { properties.put(key, line); } } return properties; } /** * Extracts the key from a properties file line. * Returns null if the line is a comment, empty, or doesn't seem to be a key-value pair. */ private static String extractKey(String line) { String trimmedLine = line.trim(); if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) { // Comment or empty line return null; } int separatorIndex = trimmedLine.indexOf('='); if (separatorIndex == -1) { // Line without a separator could be a key with no value (less common in i18n) // or just a malformed line. For now, let's consider it a key if it's not empty. return trimmedLine; } return trimmedLine.substring(0, separatorIndex).trim(); } private static void applySync(List refFileLines, Path targetPath) throws IOException { List originalTargetLines = Files.readAllLines(targetPath, StandardCharsets.UTF_8); Map targetProperties = parseProperties(originalTargetLines); List newTargetLines = new ArrayList<>(refFileLines.size()); boolean updated = false; for (String refLine : refFileLines) { String refKey = extractKey(refLine); if (refKey == null) { // It's a comment or blank line from reference newTargetLines.add(refLine); } else { // It's a property line from reference if (targetProperties.containsKey(refKey)) { String targetLine = targetProperties.get(refKey); // Original logic: if target line is like "#key=" (commented, no value) // then use the commented reference value. String trimmed = targetLine.trim(); if (trimmed.startsWith("#") && trimmed.substring(1).trim().startsWith(refKey) // ensure it's the same key && trimmed.endsWith("=")) { // Use reference line, commented newTargetLines.add('#' + refLine.trim()); } else { // Use existing target line newTargetLines.add(targetLine); } } else { // Key from reference is missing in target, add it commented out newTargetLines.add('#' + refLine.trim()); } } } // Check if files are different if (originalTargetLines.size() != newTargetLines.size()) { updated = true; } else { for (int i = 0; i < originalTargetLines.size(); i++) { if (!originalTargetLines.get(i).equals(newTargetLines.get(i))) { updated = true; break; } } } if (updated) { LOG.info("Updating {} ({} lines -> {} lines)", targetPath.getFileName(), originalTargetLines.size(), newTargetLines.size()); Files.write(targetPath, newTargetLines, StandardCharsets.UTF_8); } else { LOG.info("No changes needed for {}", targetPath.getFileName()); } } private static Path getRefPath(String referenceFileName) { // Path relative to project root (where src/main/resources exists) Path projectRootRelative = I18N_PATH.resolve(referenceFileName); if (Files.exists(projectRootRelative)) { return projectRootRelative.toAbsolutePath(); } // Path relative to a module (e.g. jadx-gui/src/main/resources) // This assumes the script is run from one level above GUI_MODULE_DIR_NAME Path moduleRelative = GUI_MODULE_PREFIX_PATH.resolve(I18N_PATH).resolve(referenceFileName); if (Files.exists(moduleRelative)) { return moduleRelative.toAbsolutePath(); } // Path if tool runs from within the GUI_MODULE_DIR_NAME itself Path currentDirRelative = Paths.get(".").resolve(I18N_PATH).resolve(referenceFileName); if (Files.exists(currentDirRelative)) { return currentDirRelative.toAbsolutePath(); } throw new RuntimeException("Can't find reference I18N: " + referenceFileName); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/ui/ActionHandler.java ================================================ package jadx.gui.utils.ui; import java.awt.event.ActionEvent; import java.util.function.Consumer; import javax.swing.AbstractAction; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JToggleButton; import javax.swing.KeyStroke; import jadx.gui.utils.UiUtils; import jadx.gui.utils.shortcut.Shortcut; public class ActionHandler extends AbstractAction { private final Consumer consumer; public ActionHandler(Runnable action) { this.consumer = ev -> action.run(); } public ActionHandler(Consumer consumer) { this.consumer = consumer; } public ActionHandler(String name, Runnable action) { this(action); setName(name); } public ActionHandler() { this.consumer = ev -> { }; } public void setName(String name) { putValue(NAME, name); } public ActionHandler withNameAndDesc(String name) { setNameAndDesc(name); return this; } public void setNameAndDesc(String name) { setName(name); setShortDescription(name); } public void setShortDescription(String desc) { putValue(SHORT_DESCRIPTION, desc); } public void setIcon(Icon icon) { putValue(SMALL_ICON, icon); } public void setSelected(boolean selected) { putValue(SELECTED_KEY, selected); } public void setKeyBinding(KeyStroke keyStroke) { putValue(ACCELERATOR_KEY, keyStroke); } public void attachKeyBindingFor(JComponent component, KeyStroke keyStroke) { UiUtils.addKeyBinding(component, keyStroke, "run", this); setKeyBinding(keyStroke); } public void addKeyBindToDescription() { KeyStroke keyStroke = (KeyStroke) getValue(ACCELERATOR_KEY); if (keyStroke != null) { String keyText = Shortcut.keyboard(keyStroke.getKeyCode(), keyStroke.getModifiers()).toString(); String desc = (String) getValue(SHORT_DESCRIPTION); setShortDescription(desc + " (" + keyText + ")"); } } @Override public void actionPerformed(ActionEvent e) { consumer.accept(e); } public JButton makeButton() { addKeyBindToDescription(); return new JButton(this); } public JToggleButton makeToggleButton() { JToggleButton toggleButton = new JToggleButton(this); toggleButton.setText(""); return toggleButton; } public JCheckBoxMenuItem makeCheckBoxMenuItem() { return new JCheckBoxMenuItem(this); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/ui/DocumentUpdateListener.java ================================================ package jadx.gui.utils.ui; import java.util.function.Consumer; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; public class DocumentUpdateListener implements DocumentListener { private final Consumer listener; public DocumentUpdateListener(Consumer listener) { this.listener = listener; } @Override public void insertUpdate(DocumentEvent event) { this.listener.accept(event); } @Override public void removeUpdate(DocumentEvent event) { this.listener.accept(event); } @Override public void changedUpdate(DocumentEvent event) { // ignore attributes change } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/ui/FileOpenerHelper.java ================================================ package jadx.gui.utils.ui; import java.awt.Desktop; import java.awt.Frame; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ResourceFile; import jadx.api.ResourcesLoader; import jadx.core.plugins.files.TempFilesGetter; import jadx.gui.treemodel.JResource; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; public class FileOpenerHelper { private static final Logger LOG = LoggerFactory.getLogger(FileOpenerHelper.class); public static void exportBinary(JResource resource, Path savePath) { try (BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(savePath.toFile()))) { byte[] bytes = ResourcesLoader.decodeStream(resource.getResFile(), (size, is) -> is.readAllBytes()); if (bytes == null) { bytes = new byte[0]; } os.write(bytes); } catch (Exception e) { throw new RuntimeException("Error saving file " + resource.getName(), e); } } public static void openFile(Frame frame, JResource res) { if (Desktop.isDesktopSupported()) { Desktop desktop = Desktop.getDesktop(); Path tempDir = TempFilesGetter.INSTANCE.getTempDir(); ResourceFile resFile = res.getResFile(); Path path = Paths.get(resFile.getDeobfName()); Path fileNamePath = path.getFileName(); Path filePath = tempDir.resolve(fileNamePath); exportBinary(res, filePath); if (!Files.exists(filePath)) { UiUtils.errorMessage(frame, NLS.str("error_dialog.not_found_file", filePath)); return; } if (Files.isDirectory(filePath)) { UiUtils.errorMessage(frame, NLS.str("error_dialog.path_is_directory", filePath)); return; } if (!Files.isReadable(filePath)) { UiUtils.errorMessage(frame, NLS.str("error_dialog.cannot_read", filePath)); return; } try { desktop.open(filePath.toFile()); } catch (IOException ex) { UiUtils.errorMessage(frame, NLS.str("error_dialog.open_failed", ex.getMessage())); LOG.error("Unable to open file: {0}", ex); } catch (IllegalArgumentException ex) { UiUtils.errorMessage(frame, NLS.str("error_dialog.invalid_path_format", ex.getMessage())); LOG.error("Invalid file path: {0}", ex); } } else { UiUtils.errorMessage(frame, NLS.str("error_dialog.desktop_unsupported")); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/ui/MousePressedHandler.java ================================================ package jadx.gui.utils.ui; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.function.Consumer; public class MousePressedHandler extends MouseAdapter { private final Consumer listener; public MousePressedHandler(Consumer listener) { this.listener = listener; } @Override public void mousePressed(MouseEvent ev) { listener.accept(ev); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/ui/NodeLabel.java ================================================ package jadx.gui.utils.ui; import javax.swing.JLabel; import javax.swing.SwingConstants; import jadx.gui.treemodel.JNode; public class NodeLabel extends JLabel { public static NodeLabel longName(JNode node) { NodeLabel label = new NodeLabel(node.makeLongStringHtml(), node.disableHtml()); label.setIcon(node.getIcon()); label.setHorizontalAlignment(SwingConstants.LEFT); return label; } public static NodeLabel noHtml(String label) { return new NodeLabel(label, true); } public static void disableHtml(JLabel label, boolean disable) { label.putClientProperty("html.disable", disable); } private boolean htmlDisabled = false; public NodeLabel() { disableHtml(true); } public NodeLabel(String label) { disableHtml(true); setText(label); } public NodeLabel(String label, boolean disableHtml) { disableHtml(disableHtml); setText(label); } public void disableHtml(boolean disable) { if (htmlDisabled != disable) { htmlDisabled = disable; disableHtml(this, disable); } } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/ui/SimpleMenuItem.java ================================================ package jadx.gui.utils.ui; import javax.swing.JMenuItem; public class SimpleMenuItem extends JMenuItem { public SimpleMenuItem(String text, Runnable action) { super(text); addActionListener(ev -> action.run()); } } ================================================ FILE: jadx-gui/src/main/java/jadx/gui/utils/ui/ZoomActions.java ================================================ package jadx.gui.utils.ui; import java.awt.Container; import java.awt.Font; import java.awt.event.KeyEvent; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.KeyStroke; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.font.FontAdapter; import jadx.gui.settings.font.FontSettings; import jadx.gui.ui.codearea.SmaliArea; import jadx.gui.utils.UiUtils; public class ZoomActions { private final JComponent component; private final JadxSettings settings; private final Runnable update; public static void register(JComponent component, JadxSettings settings, Runnable update) { ZoomActions actions = new ZoomActions(component, settings, update); actions.register(); } private ZoomActions(JComponent component, JadxSettings settings, Runnable update) { this.component = component; this.settings = settings; this.update = update; } private void register() { String zoomIn = "TextZoomIn"; String zoomOut = "TextZoomOut"; int ctrlButton = UiUtils.ctrlButton(); InputMap inputMap = component.getInputMap(); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, ctrlButton), zoomIn); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, ctrlButton), zoomIn); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, ctrlButton), zoomIn); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, ctrlButton), zoomOut); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, ctrlButton), zoomOut); ActionMap actionMap = component.getActionMap(); actionMap.put(zoomIn, new ActionHandler(e -> textZoom(1))); actionMap.put(zoomOut, new ActionHandler(e -> textZoom(-1))); component.addMouseWheelListener(e -> { if (e.getModifiersEx() == UiUtils.ctrlButton()) { textZoom(e.getWheelRotation() < 0 ? 1 : -1); e.consume(); } else { // pass event to parent component, needed for scroll in JScrollPane Container parent = component.getParent(); if (parent != null) { parent.dispatchEvent(e); } } }); } private void textZoom(int change) { FontSettings fontSettings = settings.getFontSettings(); FontAdapter fontAdapter; if (component instanceof SmaliArea) { fontAdapter = fontSettings.getSmaliFontAdapter(); } else { fontAdapter = fontSettings.getCodeFontAdapter(); } fontAdapter.setFont(changeFontSize(fontAdapter.getFont(), change)); settings.sync(); update.run(); } private Font changeFontSize(Font font, int change) { float newSize = font.getSize() + change; if (newSize < 2) { // ignore change return font; } return font.deriveFont(newSize); } } ================================================ FILE: jadx-gui/src/main/resources/files/jadx-gui.desktop.tmpl ================================================ [Desktop Entry] Name=JADX GUI Comment=Dex to Java decompiler Icon=jadx Exec={{launchScriptPath}} %f Terminal=false Type=Application Categories=Development;Java; Keywords=Java;Decompiler; StartupWMClass=jadx-gui-JadxGUI MimeType=application/vnd.android.package-archive;application/java-archive; ================================================ FILE: jadx-gui/src/main/resources/i18n/Messages_de_DE.properties ================================================ language.name=Deutsch menu.file=Datei menu.view=Ansicht menu.recent_projects=Aktuelle Projekte menu.no_recent_projects=Keine aktuellen Projekte menu.preferences=Einstellungen menu.sync=Mit Editor synchronisieren menu.flatten=Codepaket erweitern #menu.enable_preview_tab=Enable Preview Tab menu.heapUsageBar=Speicherverbrauchsleiste anzeigen menu.alwaysSelectOpened=Immer geöffnete Datei/Klasse auswählen menu.dock_log=Dock-Protokollanzeige menu.dock_quick_tabs=Dock-Schnellzugriffsleisten menu.navigation=Navigation menu.text_search=Textsuche menu.class_search=Klassen-Suche menu.comment_search=Kommentar suchen menu.go_to_main_activity=Zur Hauptaktivität gehen menu.go_to_application=Zur Anwendung gehen menu.go_to_android_manifest=Zur AndroidManifest.xml gehen menu.tools=Werkzeuge menu.plugins=Plugins menu.decompile_all=Alle Klassen dekompilieren menu.reset_cache=Code-Cache zurücksetzen menu.deobfuscation=Deobfuskierung menu.log=Protokollanzeige menu.create_desktop_entry=Desktop-Eintrag erstellen menu.help=Hilfe menu.about=Über menu.quark=Quark Engine menu.update_label=Neue Version %s verfügbar! #menu.hex_viewer=Hex Viewer file.open_action=Datei öffnen… file.add_files_action=Dateien hinzufügen… file.open_title=Datei öffnen file.open_project=Projekt öffnen file.new_project=Neues Projekt file.save_project=Projekt speichern file.save_project_as=Projekt speichern als… file.reload=Dateien neu laden file.live_reload=Live nachladen file.live_reload_desc=Dateien bei Änderungen autom. neuladen file.open_mappings=Zuordnungen öffnen... file.save_mappings=Zuordnungen speichern file.save_mappings_as=Zuordnungen speichern als... file.close_mappings=Zuordnungen exportieren als… file.save_all=Alles speichern file.save=Speichern file.export=Projekt exportieren file.save_all_msg=Verzeichnis für das Speichern dekompilierter Ressourcen auswählen file.exit=Beenden file.export_node=Datei exportieren start_page.title=Startseite start_page.start=Start start_page.recent=Aktuelle Projekte #start_page.list.delete_recent_project=Delete project #start_page.list.delete_recent_project.tooltip=Delete project from recent list tree.inputs_title=Eingaben tree.input_files=Dateien tree.input_scripts=Skripte tree.sources_title=Quellcode tree.resources_title=Ressourcen tree.loading=Laden… tree.pinned_tabs=Angepinnte Registerkarten tree.open_tabs=Offene Registerkarten tree.bookmarked_tabs=Mit Lesezeichen versehene Registerkarten progress.load=Laden progress.save_mappings=Zuordnungen werden gespeichert progress.decompile=Dekompilieren progress.canceling=Breche ab error_dialog.title=Fehler error_dialog.not_found=%s nicht gefunden #error_dialog.not_found_file=File not found at path:\n%s #error_dialog.path_is_directory=The specified path is a directory:\n%s #error_dialog.cannot_read=Cannot read file:\n%s #error_dialog.open_failed=Failed to open file:\n%s #error_dialog.invalid_path_format=Invalid file path:\n%s #error_dialog.desktop_unsupported=Desktop is not supported in this environment. search.previous=Vorheriges search.next=Nächstes search.mark_all=Alles markieren search.regex=Regex search.match_case=Groß/Kleinschreibung beachten search.whole_word=Ganzes Wort search.find=Finden search.results=%s%d Ergebnisse #search.match_of=Match %d of %d #search.match_found=Match found #search.match_not_found=No Matches found #search.single_match=Single match found #search.find_type_text=Find by text #search.find_type_hex=Find by hex tabs.copy_class_name=Klassennamen kopieren tabs.close=Schließen tabs.closeOthers=Andere schließen tabs.unpin=Lösen tabs.unpin_all=Alle lösen tabs.bookmark=Lesezeichen setzen tabs.unbookmark=Lesezeichen entfernen tabs.unbookmark_all=Alle Lesezeichen entfernen tabs.pin=Anheften tabs.closeAll=Alles schließen tabs.closeAllRight=Alles rechts schließen #tabs.closeAllLeft=Close All Left tabs.code=Code tabs.smali=Smali tabs.smali_bytecode=Smali+Bytecode nav.back=Zurück nav.forward=Vorwärts message.taskTimeout=Aufgabe hat Zeitlimit von %d ms überschritten. message.userCancelTask=Aufgabe wurde vom Benutzer abgebrochen. message.memoryLow=Jadx hat zu wenig Speicherplatz. Bitte mit erhöhter maximaler Heap-Größe erneut starten. message.taskError=Die Aufgabe ist durch Fehler fehlgeschlagen (siehe Protokoll für Details). message.errorTitle=Fehler message.load_errors=Laden fehlgeschlagen.\nAnzahl der Fehler: %d\nKlicke auf OK, um die Protokollansicht zu öffnen. message.no_classes=Keine Klassen geladen, nichts zu dekompilieren! message.enter_valid_path=Gültigen Pfad zum Speichern eingeben! message.saveIncomplete=Speichern unvollständig.
%s
%d Klassen oder Ressourcen wurden nicht gespeichert! message.indexIncomplete=Index einiger Klassen übersprungen.
%s
%d Klassen wurden nicht indiziert und werden nicht in den Suchergebnissen erscheinen! message.indexingClassesSkipped=Jadx hat nur noch wenig Speicherplatz, daher wurden %d Klassen nicht indiziert.
Wenn du möchtest, dass alle Klassen indiziert werden, dann starte Jadx neu mit erhöhter maximaler Heap-Größe. message.enter_new_name=Neuen Namen eingeben message.could_not_rename=Die Datei kann nicht umbenannt werden message.confirm_remove_script=Willst du das Skript wirklich entfernen? message.desktop_entry_creation_error=Erstellen des Desktop-Eintrags fehlgeschlagen (Details findest du im Protokoll). message.desktop_entry_creation_success=Desktop-Eintrag erfolgreich erstellt! message.success_title=Erfolg #message.unable_preview_font=Unable preview font heapUsage.text=JADX-Speicherauslastung: %.2f GB von %.2f GB (%.2f GB Höhepunkt) common_dialog.ok=OK common_dialog.cancel=Abbrechen common_dialog.add=Hinzufügen common_dialog.update=Aktualisieren common_dialog.remove=Entfernen common_dialog.reset=Zurücksetzen file_dialog.supported_files=Unterstützte Dateien file_dialog.load_dir_title=Verzeichnis laden file_dialog.load_dir_confirm=Alle Dateien aus dem Verzeichnis laden? search_dialog.open=Öffnen search_dialog.cancel=Beenden search_dialog.open_by_name=Nach Text suchen: search_dialog.search_button=Suche search_dialog.search_history=Suchverlauf search_dialog.auto_search=Automatische Suche search_dialog.search_in=Suche in Definitionen von: search_dialog.class=Klasse search_dialog.method=Methode search_dialog.field=Feld search_dialog.code=Code search_dialog.ignorecase=Groß/Kleinschreibung ignorieren search_dialog.load_more=Mehr laden search_dialog.load_all=Alle laden search_dialog.stop=Stopp search_dialog.results_incomplete=%d+ gefunden search_dialog.results_complete=%d gefunden (komplett) search_dialog.resources_load_errors=Ladefehler: %d search_dialog.resources_skip_by_size=Nach Größe übersprungen: %d search_dialog.resources_check_logs=(Klicken, um die Protokolle zu überprüfen) search_dialog.col_node=Knoten search_dialog.col_code=Code search_dialog.sort_results=Ergebnisse sortieren search_dialog.regex=Regex search_dialog.active_tab=Nur aktive Registerkarte search_dialog.comments=Kommentare search_dialog.resource=Ressource search_dialog.keep_open=Offen halten search_dialog.tip_searching=Suchen… search_dialog.limit_package=Begrenzung auf Paket: #search_dialog.res_text=Text #search_dialog.res_binary=Binary search_dialog.package_not_found=Kein passendes Paket gefunden search_dialog.copy=alles kopieren usage_dialog.title=Verwendungssuche usage_dialog.label=Verwendungen von: #usage_dialog_plus.title=Usage tree search usage_dialog_plus.jump_to=Zur aktuellen Position springen usage_dialog_plus.copy_path=Verwendungspfad kopieren usage_dialog_plus.search_complete=Suche abgeschlossen #usage_dialog_plus.code_view=Code View #usage_dialog_plus.select_node=Select Node #usage_dialog_plus.code_for=Code for %s #usage_dialog_plus.expand_usages=Expand Data comment_dialog.title.add=Code-Kommentar hinzufügen comment_dialog.title.update=Code-Kommentar aktualisieren comment_dialog.label=Kommentar: comment_dialog.style=Stil: comment_dialog.usage=Umschalt + Eingabe verwenden, um eine neue Zeile zu beginnen rename_dialog.class_help=Gib den vollständigen Namen ein, um die Klasse in ein anderes Paket zu verschieben. Beginne mit '.', um die Klasse in das (leere) Vorgabepaket zu verschieben. export_dialog.title=In Quellcode exportieren export_dialog.save_path=Speicherpfad: export_dialog.browse=Durchsuchen export_dialog.export_options=Exportoptionen export_dialog.export_gradle=Als Gradle-Projekt exportieren #export_dialog.export_gradle_type=Gradle template: log_viewer.title=Protokollanzeige log_viewer.log_level=Protokollstufe: log_viewer.mode=Modus: log_viewer.modes=Alle|Alle Skripte|Aktuelles Skript log_viewer.hide=Ausblenden log_viewer.dock=Andocken log_viewer.undock=Abdocken log_viewer.clear=Löschen about_dialog.title=Über JADX preferences.title=Einstellungen preferences.deobfuscation=Deobfuskierung preferences.appearance=Erscheinung preferences.shortcuts=Tastenkürzel preferences.select_shortcuts=Wähle eine bestimmte Tastenkürzelgruppe aus preferences.decompile=Dekompilierung preferences.plugins=Plugins preferences.project=Projekt preferences.other=Sonstiges preferences.language=Sprache preferences.lineNumbersMode=Editor Zeilennummern-Modus preferences.jumpOnDoubleClick=Sprung bei Doppelklick aktivieren preferences.useAlternativeFileDialog=Alternativen Dateidialog verwenden preferences.check_for_updates=Beim Starten nach Updates suchen preferences.useDx=dx/d8 zur Konvertierung von Java-Bytecode verwenden preferences.decompilationMode=Dekompilierungsmodus preferences.codeCacheMode=Cache-Code-Modus #preferences.codeCacheMode.memory=Memory #preferences.codeCacheMode.memory.desc=Everything in memory: fast search, slow reopen, high memory usage #preferences.codeCacheMode.diskWithCache=Disk with cache #preferences.codeCacheMode.diskWithCache.desc=Code saved on disk with in memory cache: medium search, fast reopen, medium memory usage #preferences.codeCacheMode.disk=Disk #preferences.codeCacheMode.disk.desc=Everything on disk: slow search, fast reopen, low memory usage preferences.usageCacheMode=Nutzungsdaten-Cache-Modus preferences.showInconsistentCode=Inkonsistenten Code anzeigen preferences.escapeUnicode=Unicode maskieren preferences.replaceConsts=Konstanten ersetzen preferences.respectBytecodeAccessModifiers=Modifikatoren für Bytecode-Zugriff beachten preferences.useImports=Importanweisungen verwenden preferences.useDebugInfo=Debug-Infos verwenden preferences.inlineAnonymous=Anonyme Inline-Klassen preferences.inlineMethods=Inline-Methoden #preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas #preferences.moveInnerClasses=Move inner classes into parent #preferences.extractFinally=Extract finally block #preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=Dateisystem unterscheidet zwischen Groß/Kleinschreibung preferences.skipResourcesDecode=Ressourcen nicht dekodieren preferences.skipSourcesDecode=Quellcode nicht dekompilieren preferences.useKotlinMethodsForVarNames=Kotlin-Methoden für die Umbenennung von Variablen verwenden preferences.commentsLevel=Stufe der Code-Kommentare preferences.saveOption=Einstellungen automatisch speichern preferences.threads=Verarbeitungs-Thread-Anzahl preferences.excludedPackages=Ausgeschlossene Pakete preferences.excludedPackages.tooltip=Liste der durch Leerzeichen getrennten Paketnamen, die nicht dekompiliert oder indiziert werden (spart RAM) preferences.excludedPackages.button=Bearbeiten preferences.excludedPackages.editDialog=Liste der durch Leerzeichen getrennten Paketnamen, die nicht dekompiliert oder indiziert werden. (spart RAM)
z.B. android.support preferences.cfg=CFG-Grafiken für Methoden generieren (im 'dot'-Format) preferences.raw_cfg=RAW CFG-Grafiken generieren preferences.xposed_codegen_language=Xposed-Code-Generierungssprache preferences.update_channel=Jadx-Updatekanal #preferences.disable_tooltip_on_hover=Disable tooltip on hover preferences.integerFormat=Ganzzahlformat #preferences.typeUpdatesCountLimit=Update type limit count #preferences.ui_zoom=UI Zoom factor #preferences.apply_ui_zoom_to_fonts=Apply UI zoom to fonts #preferences.ui_font=UI font preferences.code_font=Schriftart ändern preferences.smali_font=Monospaced-Schriftart (Smali/Hex) preferences.laf_theme=Thema #preferences.dynamic_editor_theme=Use UI theme colors preferences.theme=Thema ändern preferences.start_jobs=Autom. Hintergrunddekompilierung starten preferences.select_font=Ändern preferences.deobfuscation_on=Deobfuskierung einschalten preferences.generated_renames_mapping_file_mode=Umgang mit Map-Dateien preferences.deobfuscation_min_len=Minimale Namenlänge preferences.deobfuscation_max_len=Maximale Namenlänge preferences.deobfuscation_res_name_source=Bessere Ressourcennamenquelle #preferences.deobfuscation_res_use_headers=Use headers for detect resource extensions preferences.deobfuscation_whitelist=Pakete und Klassen von Deobfuskierung ausschließen preferences.deobfuscation_whitelist.editDialog=Weiße Liste für Deobfuskierung preferences.deobfuscation_whitelist.tooltip=Liste der durch ':' getrennten Pakete (Suffix '.*') und Klassenamen, die nicht deobfuskiert werden sollen preferences.save=Speichern preferences.cancel=Abbrechen preferences.reset=Zurücksetzen preferences.reset_message=Einstellungen auf Vorgabewerte zurücksetzen? preferences.reset_title=Einstellungen zurücksetzen preferences.copy=In Zwischenablage kopieren preferences.copy_message=Alle Einstellungswerte wurden in die Zwischenablage kopiert preferences.rename=Bezeichner umbenennen preferences.rename_case=Um Probleme mit der Groß-/Kleinschreibung zu beheben preferences.rename_valid=Um sie gültig zu machen preferences.rename_printable=Um druckbar zu machen preferences.rename_use_source_name_as_class_name_alias=Quelldateiname als Klassennamen-Alias verwenden preferences.rename_source_name_repeat_limit=Verwendung des Quellnamens erlauben, wenn dieser seltener vorkommt als preferences.search_results_per_page=Ergebnisse pro Seite (0 - keine Begrenzung) preferences.res_file_ext=Ressourcendatei-Erweiterungen ('xml|html', * für alle) preferences.res_skip_file=Ressourcendateien überspringen, wenn sie größer sind (MB) (0 - ausschalten) preferences.tab_dnd_appearance=Erscheinungsbild der Registerkarte "Verschieben" #preferences.plugins.manage=Manage plugins preferences.plugins.install=Plugin installieren preferences.plugins.install_btn=Installieren preferences.plugins.uninstall_btn=Deinstallieren preferences.plugins.disable_btn=Ausschalten preferences.plugins.enable_btn=Einschalten preferences.plugins.location_id_label=Standort-ID: preferences.plugins.plugin_jar=Plugin-Jar auswählen preferences.plugins.plugin_jar_label=oder preferences.plugins.update_all=Alle aktualisieren preferences.plugins.details=Plugin-Details preferences.plugins.task.installing=Installation des Plugins preferences.plugins.task.uninstalling=Deinstallation des Plugins preferences.plugins.task.updating=Aktualisierung der Plugins preferences.plugins.task.downloading_list=Herunterladen der Plugin-Liste preferences.plugins.task.status=Ändern des Plugin-Status preferences.cache=Cache preferences.cache.location=Cache-Speicherort preferences.cache.location_default=App-Cache-Systemverzeichnis preferences.cache.location_local=Gleiches Verzeichnis wie Projektdatei preferences.cache.location_custom=Benutzerdefinierter Speicherort: preferences.cache.change_notice=* für vorhandene Caches wird die Änderung nach Cache-Entfernung oder manuellem Zurücksetzen angewendet preferences.cache.table.title=Cache-Liste preferences.cache.table.project=Cache für Projekt preferences.cache.table.size=Datenträgernutzung preferences.cache.btn.usage=Nutzung berechnen preferences.cache.btn.delete_selected=Ausgewähltes löschen preferences.cache.btn.delete_all=Alles löschen preferences.cache.task.usage=Berechnung der Cache-Größe preferences.cache.task.delete=Löschen der Cache msg.open_file=Bitte Datei öffnen msg.saving_sources=Quellcodes werden gespeichert msg.language_changed_title=Sprache geändert msg.language_changed=Die neue Sprache wird beim nächsten Start der Anwendung angezeigt. msg.warning_title=Warnung msg.common_mouse_shortcut=Dies ist eine häufig verwendete Taste. Möchtest du sie wirklich an eine Aktion binden? msg.duplicate_shortcut=Der Tastenkürzel %s ist bereits in der Aktion „%s“ aus der Kategorie „%s“ festgelegt, fortfahren? msg.cmd_select_class_error=Klasse\n%s auswählen nicht möglich\nSie existiert nicht. msg.cant_add_comment=Kann hier keinen Kommentar hinzufügen #msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars? #msg.non_displayable_chars.title=Undisplayed Strings #methods_dialog.title=Select methods popup.bytecode_col=Dalvik-Bytecode anzeigen popup.line_wrap=Zeilenumbruch popup.undo=Rückgängig popup.redo=Wiederholen popup.cut=Ausschneiden popup.copy=Kopieren popup.paste=Einfügen popup.delete=Löschen popup.select_all=Alle auswählen popup.frida=Als Frida-Schnipsel kopieren popup.xposed=Als Xposed-Schnipsel kopieren popup.find_usage=Verwendung suchen popup.go_to_declaration=Zur Erklärung gehen popup.exclude=Ausschließen popup.exclude_packages=Pakete ausschließen popup.convert_number=Conversion als Kommentar hinzufügen #popup.view_call_graph=View call graph #popup.view_call_graph_description=Show call chains to this function #popup.view_class_graph=View inheritance graph #popup.view_class_graph_description=Show inheritance tree for this class #popup.view_class_method_graph=View methods graph #popup.view_class_method_graph_description=Show all methods for this class #popup.cfg_submenu=View control flow graph #popup.view_cfg=Regular #popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) #popup.view_raw_cfg=Raw #popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) #popup.view_region_cfg=Region #popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=Kommentar popup.update_comment=Kommentar aktualisieren popup.search_comment=Kommentar suchen popup.rename=Umbennen popup.search=Suche "%s" popup.search_global=Globale Suche "%s" popup.remove=Entfernen popup.add_files=Dateien hinzufügen popup.add_scripts=Skripte hinzufügen popup.new_script=Neues Skript popup.json_prettify=JSON-Verschönerung popup.export=Exportieren #popup.usage_dialog_plus=Usage Tree Search #popup.copy_as=Copy as... #popup.copy_as_hex=Copy as HEX #popup.copy_as_string=Copy as String #popup.copy_offset=Copy offset script.run=Ausführen script.save=Speichern script.auto_complete=Automatisch vervollständigen script.check=Überprüfen script.format=Neu formatieren script.log=Protokoll anzeigen #encoding_dialog.title=Encoding #encoding_dialog.message=Select encoding: exclude_dialog.title=Paketauswahl exclude_dialog.select_all=Alles auswählen exclude_dialog.deselect=Abwählen exclude_dialog.invert=Invertieren confirm.save_as_title=Speichern unter bestätigen confirm.save_as_message=%s existiert bereits.\nErsetzen? confirm.not_saved_title=Projekt speichern confirm.not_saved_message=Das aktuelle Projekt speichern, bevor du fortfährst? confirm.remember=Meine Entscheidung merken certificate.cert_type=Typ certificate.serialSigVer=Version certificate.serialNumber=Seriennummer certificate.cert_subject=Subjekt certificate.serialValidFrom=Gültig ab certificate.serialValidUntil=Gültig bis certificate.serialPubKeyType=Öffentlicher Schlüsseltyp certificate.serialPubKeyExponent=Exponent certificate.serialPubKeyModulus=Modulus certificate.serialPubKeyModulusSize=Modulusgröße (Bits) certificate.serialSigType=Signaturtyp certificate.serialSigOID=Signatur-OID certificate.serialMD5=MD5-Fingerabdruck certificate.serialSHA1=SHA-1-Fingerabdruck certificate.serialSHA256=SHA-256-Fingerabdruck certificate.serialPubKeyY=Y apkSignature.signer=Signierer #apkSignature.loading=Loading signature... apkSignature.verificationSuccess=Signaturprüfung erfolgreich abgeschlossen apkSignature.verificationFailed=Signaturprüfung fehlgeschlagen apkSignature.signatureSuccess=Gültige APK-Signatur v%d gefunden apkSignature.signatureFailed=Ungültige APK-Signatur v%d gefunden apkSignature.errors=Fehler apkSignature.warnings=Warnhinweise apkSignature.exception=APK-Verifizierung fehlgeschlagen apkSignature.unprotectedEntry=Dateien, die nicht durch eine APK-Signatur v1 geschützt sind. Unbefugte Änderungen an diesen Einträgen werden nur von einer APK-Signatur v2 oder neuer erkannt. issues_panel.label=Probleme: issues_panel.errors=%d Fehler issues_panel.warnings=%d Warnungen issues_panel.tooltip=In Protokollansicht öffnen debugger.process_selector=Zu debuggenden Prozess auswählen debugger.step_into=Einzelschritt (F7) debugger.step_over=Prozedurschritt (F8) debugger.step_out=>Rücksprung (Umschalt + F8) debugger.run=Ausführen (F9) debugger.stop=Debugger anhalten und App beenden debugger.pause=Pause debugger.rerun=Wiederholen debugger.cfm_dialog_title=Während der Fehlersuche beenden debugger.cfm_dialog_msg=Bist du dir sicher, dass du den Debugger beenden willst? debugger.popup_set_value=Wert einstellen debugger.popup_change_to_zero=Wechsel zu 0 debugger.popup_change_to_one=Wechsel zu 1 debugger.popup_copy_value=Wert kopieren logcat.pause=Logcat anhalten logcat.start=Logcat fortsetzen logcat.clear=Logcat löschen logcat.error_fail_start=Logcat konnte nicht gestartet werden logcat.process=Prozess logcat.level=Level logcat.default=Vorgabe logcat.verbose=Ausführlich logcat.debug=Debug logcat.info=Info logcat.warn=Warnen logcat.error=Fehler logcat.fatal=Fatal logcat.silent=Still logcat.logcat=Logcat logcat.select_attached=Angehängtes auswählen logcat.select_all=Alles auswählen logcat.unselect_all=Alles abwählen set_value_dialog.label_value=Wert set_value_dialog.btn_set=Wert einstellen set_value_dialog.title=Wert einstellen set_value_dialog.neg_msg=Wert einstellen fehlgeschlagen set_value_dialog.sel_type=Wähle einen Typ aus, um einen Wert einzustellen. adb_dialog.addr=ADB-Adresse adb_dialog.port=ADB-Port adb_dialog.path=ADB-Pfad adb_dialog.launch_app=App starten adb_dialog.start_server=ADB-Server starten adb_dialog.refresh=Aktualisieren adb_dialog.tip_devices=%d Geräte adb_dialog.device_node=Gerät adb_dialog.missing_path=Muss ADB-Pfad angeben, um einen ADB-Server zu starten. adb_dialog.waiting=Auf Verbindung zum ADB-Server warten… adb_dialog.connecting=Mit ADB-Server verbinden, Adresse: %s:%s… adb_dialog.connect_okay=ADB-Server verbunden, Adresse: %s:%s adb_dialog.connect_fail=ADB-Serververbindung fehlgeschlagen. adb_dialog.disconnected=ADB-Server getrennt. adb_dialog.start_okay=ADB-Server auf Port gestartet: %s. adb_dialog.start_fail=ADB-Server auf Port starten fehlgeschlagen: %s! adb_dialog.forward_fail=Weiterleitung ist aus verschiedenen Gründen fehlgeschlagen. adb_dialog.being_debugged_msg=Dieser Prozess scheint fehlerhaft zu sein, sollen wir fortfahren? adb_dialog.unknown_android_ver=Android-Version nicht erhalten, Android 8 als Vorgabe verwenden? adb_dialog.being_debugged_title=Es wird debuggt durch andere. adb_dialog.init_dbg_fail=Debugger starten fehlgeschlagen. adb_dialog.msg_read_mani_fail=AndroidManifest.xml dekodieren fehlgeschlagen adb_dialog.no_devices=Konnte kein Gerät finden, um die App zu starten. adb_dialog.restart_while_debugging_title=Neustart während der Fehlersuche adb_dialog.restart_while_debugging_msg=Du debuggst eine App, möchtest du wirklich eine Sitzung neu starten? adb_dialog.starting_debugger=Debugger starten… action.variant=%s (Variante) action_category.menu_toolbar=Menü / Symbolleiste #action_category.hex_viewer=View / Hex Viewer action_category.code_area=Code-Bereich action_category.plugin_script=Plugin-Skript #hex_viewer.show_inspector=Show Inspector #hex_viewer.change_encoding=Change Encoding #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find #graph_viewer.long_names=Show full names #graph_viewer.overrides=Show overrides #graph_viewer.callee_depth=Down depth #graph_viewer.caller_depth=Up depth #graph_viewer.default_error=Failed to view graph #graph_viewer.file_not_found_error=Failed to load graph file #graph_viewer.image_too_large=Failed to render graph: graph too large #graph_viewer.image_too_small=Failed to render graph: graph too small #graph_viewer.file_failure=Error in File Operation #graph_viewer.save_graph=Save graph #graph_viewer.default_title=Graph Viewer #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph #graph_viewer.cfg.title=Control Flow Graph ================================================ FILE: jadx-gui/src/main/resources/i18n/Messages_en_US.properties ================================================ language.name=English menu.file=File menu.view=View menu.recent_projects=Recent projects menu.no_recent_projects=No recent projects menu.preferences=Preferences menu.sync=Select in Tree menu.flatten=Use Flatten Packages menu.enable_preview_tab=Enable Preview Tab menu.heapUsageBar=Show Memory Usage Bar menu.alwaysSelectOpened=Auto Select in Tree menu.dock_log=Dock Log Viewer menu.dock_quick_tabs=Show Quick Tabs menu.navigation=Navigation menu.text_search=Text search menu.class_search=Class search menu.comment_search=Comment search menu.go_to_main_activity=Go to main Activity menu.go_to_application=Go to Application menu.go_to_android_manifest=Go to AndroidManifest.xml menu.tools=Tools menu.plugins=Plugins menu.decompile_all=Decompile all classes menu.reset_cache=Reset code cache menu.deobfuscation=Deobfuscation menu.log=Log Viewer menu.create_desktop_entry=Create Desktop Entry menu.help=Help menu.about=About menu.quark=Quark Engine menu.update_label=New version %s available! menu.hex_viewer=Hex Viewer file.open_action=Open files ... file.add_files_action=Add files file.open_title=Open file file.open_project=Open project file.new_project=New project file.save_project=Save project file.save_project_as=Save project as... file.reload=Reload files file.live_reload=Live reload file.live_reload_desc=Auto reload files on changes file.open_mappings=Open mappings... file.save_mappings=Save mappings file.save_mappings_as=Save mappings as... file.close_mappings=Close mappings file.save_all=Save all file.save=Save file.export=Export project file.save_all_msg=Select directory for save decompiled sources file.exit=Exit file.export_node=Export file start_page.title=Start page start_page.start=Start start_page.recent=Recent projects start_page.list.delete_recent_project=Delete project start_page.list.delete_recent_project.tooltip=Delete project from recent list tree.inputs_title=Inputs tree.input_files=Files tree.input_scripts=Scripts tree.sources_title=Source code tree.resources_title=Resources tree.loading=Loading... tree.pinned_tabs=Pinned Tabs tree.open_tabs=Open Tabs tree.bookmarked_tabs=Bookmarked Tabs progress.load=Loading progress.save_mappings=Saving mappings progress.decompile=Decompiling progress.canceling=Canceling error_dialog.title=Error error_dialog.not_found=%s not found error_dialog.not_found_file=File not found at path:\n%s error_dialog.path_is_directory=The specified path is a directory:\n%s error_dialog.cannot_read=Cannot read file:\n%s error_dialog.open_failed=Failed to open file:\n%s error_dialog.invalid_path_format=Invalid file path:\n%s error_dialog.desktop_unsupported=Desktop is not supported in this environment. search.previous=Previous search.next=Next search.mark_all=Mark All search.regex=Regex search.match_case=Match Case search.whole_word=Whole word search.find=Find search.results=%s%d results search.match_of=Match %d of %d search.match_found=Match found search.match_not_found=No Matches found search.single_match=Single match found search.find_type_text=Find by text search.find_type_hex=Find by hex tabs.copy_class_name=Copy Name tabs.close=Close tabs.closeOthers=Close Others tabs.unpin=Unpin tabs.unpin_all=Unpin All tabs.bookmark=Bookmark tabs.unbookmark=Unbookmark tabs.unbookmark_all=Unbookmark All tabs.pin=Pin tabs.closeAll=Close All tabs.closeAllRight=Close All Right tabs.closeAllLeft=Close All Left tabs.code=Code tabs.smali=Smali tabs.smali_bytecode=Smali+Bytecode nav.back=Back nav.forward=Forward message.taskTimeout=Task exceeded time limit of %d ms. message.userCancelTask=Task was canceled by user. message.memoryLow=Jadx is running low on memory. Please restart with increased maximum heap size. message.taskError=Task failed with error (check log for details). message.errorTitle=Error message.load_errors=Load failed.\nErrors count: %d\nClick OK to open log viewer. message.no_classes=No classes loaded, nothing to decompile! message.enter_valid_path=Enter valid path to save! message.saveIncomplete=Save incomplete.
%s
%d classes or resources were not saved! message.indexIncomplete=Index of some classes skipped.
%s
%d classes were not indexed and will not appear in search results! message.indexingClassesSkipped=Jadx is running low on memory. Therefore %d classes were not indexed.
If you want all classes to be indexed restart Jadx with increased maximum heap size. message.enter_new_name=Enter new name message.could_not_rename=Can't rename the file message.confirm_remove_script=Do you really want to remove script? message.desktop_entry_creation_error=Failed to create desktop entry (check log for details). message.desktop_entry_creation_success=Desktop entry created successfully! message.success_title=Success message.unable_preview_font=Unable preview font heapUsage.text=JADX memory usage: %.2f GB of %.2f GB (%.2f GB peak) common_dialog.ok=OK common_dialog.cancel=Cancel common_dialog.add=Add common_dialog.update=Update common_dialog.remove=Remove common_dialog.reset=Reset file_dialog.supported_files=Supported files file_dialog.load_dir_title=Load directory file_dialog.load_dir_confirm=Load all files from directory? search_dialog.open=Open search_dialog.cancel=Cancel search_dialog.open_by_name=Search for text: search_dialog.search_button=Search search_dialog.search_history=Search history search_dialog.auto_search=Auto search search_dialog.search_in=Search definitions of: search_dialog.class=Class search_dialog.method=Method search_dialog.field=Field search_dialog.code=Code search_dialog.ignorecase=Case-insensitive search_dialog.load_more=Load more search_dialog.load_all=Load all search_dialog.stop=Stop search_dialog.results_incomplete=Found %d+ search_dialog.results_complete=Found %d (complete) search_dialog.resources_load_errors=Load errors: %d search_dialog.resources_skip_by_size=Skipped by size: %d search_dialog.resources_check_logs=(click to check logs) search_dialog.col_node=Node search_dialog.col_code=Code search_dialog.sort_results=Sort results search_dialog.regex=Regex search_dialog.active_tab=Active tab only search_dialog.comments=Comments search_dialog.resource=Resource search_dialog.keep_open=Keep open search_dialog.tip_searching=Searching search_dialog.limit_package=Limit to package: search_dialog.res_text=Text search_dialog.res_binary=Binary search_dialog.package_not_found=No matching package found search_dialog.copy=Copy All usage_dialog.title=Usage search usage_dialog.label=Usage for: usage_dialog_plus.title=Usage tree search usage_dialog_plus.jump_to=Jump to current location usage_dialog_plus.copy_path=Copy usage tree path usage_dialog_plus.search_complete=Search completed usage_dialog_plus.code_view=Code View usage_dialog_plus.select_node=Select Node usage_dialog_plus.code_for=Code for %s usage_dialog_plus.expand_usages=Expand Data comment_dialog.title.add=Add code comment comment_dialog.title.update=Update code comment comment_dialog.label=Comment: comment_dialog.style=Style: comment_dialog.usage=Use 'Shift + Enter' to start a new line rename_dialog.class_help=Enter full name to move class to another package. Start with '.' to move to default (empty) package export_dialog.title=Export export_dialog.save_path=Save path: export_dialog.browse=Browse export_dialog.export_options=Export options export_dialog.export_gradle=Export as a Gradle project export_dialog.export_gradle_type=Gradle template: log_viewer.title=Log Viewer log_viewer.log_level=Log level: log_viewer.mode=Mode: log_viewer.modes=All|All scripts|Current script log_viewer.hide=Hide log_viewer.dock=Dock log_viewer.undock=Undock log_viewer.clear=Clear about_dialog.title=About JADX preferences.title=Preferences preferences.deobfuscation=Deobfuscation preferences.appearance=Appearance preferences.shortcuts=Shortcuts preferences.select_shortcuts=Select a specific shortcuts group preferences.decompile=Decompilation preferences.plugins=Plugins preferences.project=Project preferences.other=Other preferences.language=Language preferences.lineNumbersMode=Editor line numbers mode preferences.jumpOnDoubleClick=Enable jump on double click preferences.useAlternativeFileDialog=Use alternative file dialog preferences.check_for_updates=Check for updates on startup preferences.useDx=Use dx/d8 to convert java bytecode preferences.decompilationMode=Decompilation mode preferences.codeCacheMode=Code cache mode preferences.codeCacheMode.memory=Memory preferences.codeCacheMode.memory.desc=Everything in memory: fast search, slow reopen, high memory usage preferences.codeCacheMode.diskWithCache=Disk with cache preferences.codeCacheMode.diskWithCache.desc=Code saved on disk with in memory cache: medium search, fast reopen, medium memory usage preferences.codeCacheMode.disk=Disk preferences.codeCacheMode.disk.desc=Everything on disk: slow search, fast reopen, low memory usage preferences.usageCacheMode=Usage data cache mode preferences.showInconsistentCode=Show inconsistent code preferences.escapeUnicode=Escape unicode preferences.replaceConsts=Replace constants preferences.respectBytecodeAccessModifiers=Respect bytecode access modifiers preferences.useImports=Use import statements preferences.useDebugInfo=Use debug info preferences.inlineAnonymous=Inline anonymous classes preferences.inlineMethods=Inline methods preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas preferences.moveInnerClasses=Move inner classes into parent preferences.extractFinally=Extract finally block preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=File system is case-sensitive preferences.skipResourcesDecode=Don't decode resources preferences.skipSourcesDecode=Don't decompile source code preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename preferences.commentsLevel=Code comments level preferences.saveOption=Auto-save settings preferences.threads=Processing threads count preferences.excludedPackages=Excluded packages preferences.excludedPackages.tooltip=List of space separated package names that will not be decompiled or indexed (saves RAM) preferences.excludedPackages.button=Edit preferences.excludedPackages.editDialog=List of space separated package names that will not be decompiled or indexed (saves RAM)
e.g. android.support preferences.cfg=Generate methods CFG graphs (in 'dot' format) preferences.raw_cfg=Generate RAW CFG graphs preferences.xposed_codegen_language=Xposed code generation language preferences.update_channel=Jadx update channel preferences.disable_tooltip_on_hover=Disable tooltip on hover preferences.integerFormat=Integer format preferences.typeUpdatesCountLimit=Update type limit count preferences.ui_zoom=UI Zoom factor preferences.apply_ui_zoom_to_fonts=Apply UI zoom to fonts preferences.ui_font=UI font preferences.code_font=Editor font preferences.smali_font=Monospaced font (Smali/Hex) preferences.laf_theme=Theme preferences.dynamic_editor_theme=Use UI theme colors preferences.theme=Editor theme preferences.start_jobs=Auto start background decompilation preferences.select_font=Change preferences.deobfuscation_on=Enable deobfuscation preferences.generated_renames_mapping_file_mode=Map file handle mode preferences.deobfuscation_min_len=Minimum name length preferences.deobfuscation_max_len=Maximum name length preferences.deobfuscation_res_name_source=Better resources name source preferences.deobfuscation_res_use_headers=Use headers for detect resource extensions preferences.deobfuscation_whitelist=Exclude packages and classes from deobfuscation preferences.deobfuscation_whitelist.editDialog=Whitelist for deobfuscation preferences.deobfuscation_whitelist.tooltip=List of ':' separated packages (suffix '.*') and class names that will not be deobfuscated preferences.save=Save preferences.cancel=Cancel preferences.reset=Reset preferences.reset_message=Reset settings to default values? preferences.reset_title=Reset settings preferences.copy=Copy to clipboard preferences.copy_message=All settings values has been copied to clipboard preferences.rename=Rename identifiers preferences.rename_case=To fix case sensitivity issues preferences.rename_valid=To make them valid preferences.rename_printable=To make printable preferences.rename_use_source_name_as_class_name_alias=Use source file name as class name alias preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number preferences.search_results_per_page=Results per page (0 - no limit) preferences.res_file_ext=Resource files extensions ('xml|html', * for all) preferences.res_skip_file=Skip resources files if larger (MB) (0 - disable) preferences.tab_dnd_appearance=Dragging tab appearance preferences.plugins.manage=Manage plugins preferences.plugins.install=Install plugin preferences.plugins.install_btn=Install preferences.plugins.uninstall_btn=Uninstall preferences.plugins.disable_btn=Disable preferences.plugins.enable_btn=Enable preferences.plugins.location_id_label=Location id: preferences.plugins.plugin_jar=Select Plugin jar preferences.plugins.plugin_jar_label=or preferences.plugins.update_all=Update All preferences.plugins.details=Plugin details preferences.plugins.task.installing=Installing plugin preferences.plugins.task.uninstalling=Uninstalling plugin preferences.plugins.task.updating=Updating plugins preferences.plugins.task.downloading_list=Downloading plugins list preferences.plugins.task.status=Changing plugin status preferences.cache=Cache preferences.cache.location=Cache location preferences.cache.location_default=App cache system directory preferences.cache.location_local=Same directory as project file preferences.cache.location_custom=Custom location: preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset preferences.cache.table.title=Caches list preferences.cache.table.project=Cache for project preferences.cache.table.size=Disk usage preferences.cache.btn.usage=Calculate usage preferences.cache.btn.delete_selected=Delete Selected preferences.cache.btn.delete_all=Delete All preferences.cache.task.usage=Calculating cache size preferences.cache.task.delete=Deleting caches msg.open_file=Please open file msg.saving_sources=Saving sources msg.language_changed_title=Language changed msg.language_changed=New language will be displayed the next time application starts. msg.warning_title=Warning msg.common_mouse_shortcut=This is a commonly used key, are you sure you would like to bind it to an action? msg.duplicate_shortcut=The shortcut %s is already set in action "%s" from category "%s", continue ? msg.cmd_select_class_error=Failed to select the class\n%s\nThe class does not exist. msg.cant_add_comment=Can't add comment here msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars? msg.non_displayable_chars.title=Undisplayed Strings methods_dialog.title=Select methods popup.bytecode_col=Show Dalvik Bytecode popup.line_wrap=Line Wrap popup.undo=Undo popup.redo=Redo popup.cut=Cut popup.copy=Copy popup.paste=Paste popup.delete=Delete popup.select_all=Select All popup.frida=Copy as frida snippet popup.xposed=Copy as xposed snippet popup.find_usage=Find Usage popup.go_to_declaration=Go to declaration popup.exclude=Exclude popup.exclude_packages=Exclude packages popup.convert_number=Add conversion as comment popup.view_call_graph=View call graph popup.view_call_graph_description=Show call chains to this function popup.view_class_graph=View inheritance graph popup.view_class_graph_description=Show inheritance tree for this class popup.view_class_method_graph=View methods graph popup.view_class_method_graph_description=Show all methods for this class popup.cfg_submenu=View control flow graph popup.view_cfg=Regular popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) popup.view_raw_cfg=Raw popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) popup.view_region_cfg=Region popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=Comment popup.update_comment=Update comment popup.search_comment=Search comments popup.rename=Rename popup.search=Search "%s" popup.search_global=Global Search "%s" popup.remove=Remove popup.add_files=Add files popup.add_scripts=Add scripts popup.new_script=New script popup.json_prettify=JSON Prettify popup.export=Export popup.usage_dialog_plus=Usage Tree Search popup.copy_as=Copy as... popup.copy_as_hex=Copy as HEX popup.copy_as_string=Copy as String popup.copy_offset=Copy offset script.run=Run script.save=Save script.auto_complete=Auto Complete script.check=Check script.format=Reformat script.log=Show log encoding_dialog.title=Encoding encoding_dialog.message=Select encoding: exclude_dialog.title=Package Selector exclude_dialog.select_all=Select all exclude_dialog.deselect=Deselect exclude_dialog.invert=Invert confirm.save_as_title=Confirm Save as confirm.save_as_message=%s already exists.\nDo you want to replace it? confirm.not_saved_title=Save project confirm.not_saved_message=Save the current project before proceeding? confirm.remember=Remember my decision certificate.cert_type=Type certificate.serialSigVer=Version certificate.serialNumber=Serial number certificate.cert_subject=Subject certificate.serialValidFrom=Valid from certificate.serialValidUntil=Valid until certificate.serialPubKeyType=Public key type certificate.serialPubKeyExponent=Exponent certificate.serialPubKeyModulus=Modulus certificate.serialPubKeyModulusSize=Modulus size (bits) certificate.serialSigType=Signature type certificate.serialSigOID=Signature OID certificate.serialMD5=MD5 Fingerprint certificate.serialSHA1=SHA-1 Fingerprint certificate.serialSHA256=SHA-256 Fingerprint certificate.serialPubKeyY=Y apkSignature.signer=Signer apkSignature.loading=Loading signature... apkSignature.verificationSuccess=Signature verification succeeded apkSignature.verificationFailed=Signature verification failed apkSignature.signatureSuccess=Valid APK signature v%d found apkSignature.signatureFailed=Invalid APK signature v%d found apkSignature.errors=Errors apkSignature.warnings=Warnings apkSignature.exception=APK verification failed apkSignature.unprotectedEntry=Files that are not protected by APK signature v1. Unauthorized modifications to these entries can only be detected by APK signature v2 and higher. issues_panel.label=Issues: issues_panel.errors=%d errors issues_panel.warnings=%d warnings issues_panel.tooltip=Open in log viewer debugger.process_selector=Select a process to debug debugger.step_into=Step Into (F7) debugger.step_over=Step Over (F8) debugger.step_out=Step Out (Shift + F8) debugger.run=Run (F9) debugger.stop=Stop debugger and kill app debugger.pause=Pause debugger.rerun=Rerun debugger.cfm_dialog_title=Exit while debugging debugger.cfm_dialog_msg=Are you sure to terminate debugger? debugger.popup_set_value=Set Value debugger.popup_change_to_zero=Change to 0 debugger.popup_change_to_one=Change to 1 debugger.popup_copy_value=Copy Value logcat.pause=Pause Logcat logcat.start=Resume Logcat logcat.clear=Clear Logcat logcat.error_fail_start=Failed to start logcat logcat.process=Process logcat.level=Level logcat.default=Default logcat.verbose=Verbose logcat.debug=Debug logcat.info=Info logcat.warn=Warn logcat.error=Error logcat.fatal=Fatal logcat.silent=Silent logcat.logcat=Logcat logcat.select_attached=Select Attached logcat.select_all=Select All logcat.unselect_all=Unselect All set_value_dialog.label_value=Value set_value_dialog.btn_set=Set Value set_value_dialog.title=Set Value set_value_dialog.neg_msg=Failed to set value. set_value_dialog.sel_type=Select a type to set value. adb_dialog.addr=ADB Addr adb_dialog.port=ADB Port adb_dialog.path=ADB Path adb_dialog.launch_app=Launch App adb_dialog.start_server=Start ADB Server adb_dialog.refresh=Refresh adb_dialog.tip_devices=%d devices adb_dialog.device_node=Device adb_dialog.missing_path=Must provide the ADB path to start an ADB server. adb_dialog.waiting=Waiting to connect to ADB server... adb_dialog.connecting=Connecting to ADB server, addr: %s:%s... adb_dialog.connect_okay=ADB server connected, addr: %s:%s adb_dialog.connect_fail=Failed to connect to ADB server. adb_dialog.disconnected=ADB server disconnected. adb_dialog.start_okay=ADB server started on port: %s. adb_dialog.start_fail=Failed to start ADB server on port: %s! adb_dialog.forward_fail=Failed to forward for some reasons. adb_dialog.being_debugged_msg=This process seems like it's being debugged, should we proceed? adb_dialog.unknown_android_ver=Failed to get Android release version, use Android 8 as default? adb_dialog.being_debugged_title=It's Debugging by other. adb_dialog.init_dbg_fail=Failed to init debugger. adb_dialog.msg_read_mani_fail=Failed to decode AndroidManifest.xml adb_dialog.no_devices=Can't found any device to start app. adb_dialog.restart_while_debugging_title=Restart while debugging adb_dialog.restart_while_debugging_msg=You're debugging an app, are you sure to restart a session? adb_dialog.starting_debugger=Starting debugger... action.variant=%s (variant) action_category.menu_toolbar=Menu / Toolbar action_category.hex_viewer=View / Hex Viewer action_category.code_area=Code Area action_category.plugin_script=Plugin Script hex_viewer.show_inspector=Show Inspector hex_viewer.change_encoding=Change Encoding hex_viewer.goto_address=Go To Address hex_viewer.enter_address=Enter address range: hex_viewer.find=Find graph_viewer.long_names=Show full names graph_viewer.overrides=Show overrides graph_viewer.callee_depth=Down depth graph_viewer.caller_depth=Up depth graph_viewer.default_error=Failed to view graph graph_viewer.file_not_found_error=Failed to load graph file graph_viewer.image_too_large=Failed to render graph: graph too large graph_viewer.image_too_small=Failed to render graph: graph too small graph_viewer.file_failure=Error in File Operation graph_viewer.save_graph=Save graph graph_viewer.default_title=Graph Viewer graph_viewer.method_graph.title=Methods Graph graph_viewer.call_graph.title=Call Graph graph_viewer.inheritance_graph.title=Inheritance Graph graph_viewer.cfg.title=Control Flow Graph ================================================ FILE: jadx-gui/src/main/resources/i18n/Messages_es_ES.properties ================================================ language.name=Español menu.file=Archivo menu.view=Vista #menu.recent_projects=Recent projects #menu.no_recent_projects=No recent projects menu.preferences=Preferencias menu.sync=Sincronizar con el editor menu.flatten=Mostrar paquetes en vista plana #menu.enable_preview_tab=Enable Preview Tab #menu.heapUsageBar=Show Memory Usage Bar #menu.alwaysSelectOpened=Auto Select in Tree #menu.dock_log=Dock Log Viewer #menu.dock_quick_tabs=Show Quick Tabs menu.navigation=Navegación menu.text_search=Buscar texto menu.class_search=Buscar clase #menu.comment_search=Comment search #menu.go_to_main_activity=Go to main Activity #menu.go_to_application=Go to Application #menu.go_to_android_manifest=Go to AndroidManifest.xml menu.tools=Herramientas #menu.plugins=Plugins #menu.decompile_all=Decompile all classes #menu.reset_cache=Reset code cache menu.deobfuscation=Desofuscación menu.log=Visor log #menu.create_desktop_entry=Create Desktop Entry menu.help=Ayuda menu.about=Acerca de... #menu.quark=Quark Engine menu.update_label=¡Nueva versión %s disponible! #menu.hex_viewer=Hex Viewer file.open_action=Abrir archivo... #file.add_files_action=Add files file.open_title=Abrir archivo #file.open_project=Open project #file.new_project=New project #file.save_project=Save project #file.save_project_as=Save project as... #file.reload=Reload files #file.live_reload=Live reload #file.live_reload_desc=Auto reload files on changes #file.open_mappings=Open mappings... #file.save_mappings=Save mappings #file.save_mappings_as=Save mappings as... #file.close_mappings=Close mappings file.save_all=Guardar todo #file.save=Save #file.export=Export project file.save_all_msg=Seleccionar carpeta para guardar fuentes descompiladas file.exit=Salir #file.export_node=Export file #start_page.title=Start page #start_page.start=Start #start_page.recent=Recent projects #start_page.list.delete_recent_project=Delete project #start_page.list.delete_recent_project.tooltip=Delete project from recent list #tree.inputs_title=Inputs #tree.input_files=Files #tree.input_scripts=Scripts tree.sources_title=Código fuente tree.resources_title=Recursos tree.loading=Cargando... #tree.pinned_tabs=Pinned Tabs #tree.open_tabs=Open Tabs #tree.bookmarked_tabs=Bookmarked Tabs progress.load=Cargando #progress.save_mappings=Saving mappings progress.decompile=Decompiling #progress.canceling=Canceling #error_dialog.title=Error #error_dialog.not_found=%s not found #error_dialog.not_found_file=File not found at path:\n%s #error_dialog.path_is_directory=The specified path is a directory:\n%s #error_dialog.cannot_read=Cannot read file:\n%s #error_dialog.open_failed=Failed to open file:\n%s #error_dialog.invalid_path_format=Invalid file path:\n%s #error_dialog.desktop_unsupported=Desktop is not supported in this environment. search.previous=Anterior search.next=Siguiente search.mark_all=Marcar todo search.regex=Regex search.match_case=Sensible a minúsculas/mayúsculas search.whole_word=Palabra entera search.find=Buscar #search.results=%s%d results #search.match_of=Match %d of %d #search.match_found=Match found #search.match_not_found=No Matches found #search.single_match=Single match found #search.find_type_text=Find by text #search.find_type_hex=Find by hex tabs.copy_class_name=Copy Name tabs.close=Cerrar tabs.closeOthers=Cerrar otros #tabs.unpin=Unpin #tabs.unpin_all=Unpin All #tabs.bookmark=Bookmark #tabs.unbookmark=Unbookmark #tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=Cerrar todo tabs.closeAllRight=Cierra todo a la derecha #tabs.closeAllLeft=Close All Left #tabs.code=Code #tabs.smali=Smali #tabs.smali_bytecode=Smali+Bytecode nav.back=Atrás nav.forward=Adelante #message.taskTimeout=Task exceeded time limit of %d ms. #message.userCancelTask=Task was canceled by user. #message.memoryLow=Jadx is running low on memory. Please restart with increased maximum heap size. #message.taskError=Task failed with error (check log for details). #message.errorTitle=Error #message.load_errors=Load failed.\nErrors count: %d\nClick OK to open log viewer. #message.no_classes=No classes loaded, nothing to decompile! #message.enter_valid_path=Enter valid path to save! #message.saveIncomplete=Save incomplete.
%s
%d classes or resources were not saved! #message.indexIncomplete=Index of some classes skipped.
%s
%d classes were not indexed and will not appear in search results! #message.indexingClassesSkipped=Jadx is running low on memory. Therefore %d classes were not indexed.
If you want all classes to be indexed restart Jadx with increased maximum heap size. #message.enter_new_name=Enter new name #message.could_not_rename=Can't rename the file #message.confirm_remove_script=Do you really want to remove script? #message.desktop_entry_creation_error=Failed to create desktop entry (check log for details). #message.desktop_entry_creation_success=Desktop entry created successfully! #message.success_title=Success #message.unable_preview_font=Unable preview font #heapUsage.text=JADX memory usage: %.2f GB of %.2f GB (%.2f GB peak) #common_dialog.ok=OK #common_dialog.cancel=Cancel #common_dialog.add=Add #common_dialog.update=Update #common_dialog.remove=Remove #common_dialog.reset=Reset #file_dialog.supported_files=Supported files #file_dialog.load_dir_title=Load directory #file_dialog.load_dir_confirm=Load all files from directory? search_dialog.open=Abrir search_dialog.cancel=Cancelar search_dialog.open_by_name=Buscar texto: #search_dialog.search_button=Search #search_dialog.search_history=Search history #search_dialog.auto_search=Auto search search_dialog.search_in=Buscar definiciones de: search_dialog.class=Clase search_dialog.method=Método search_dialog.field=Campo search_dialog.code=Código search_dialog.ignorecase=Ignorar minúsculas/mayúsculas #search_dialog.load_more=Load more #search_dialog.load_all=Load all #search_dialog.stop=Stop #search_dialog.results_incomplete=Found %d+ #search_dialog.results_complete=Found %d (complete) #search_dialog.resources_load_errors=Load errors: %d #search_dialog.resources_skip_by_size=Skipped by size: %d #search_dialog.resources_check_logs=(click to check logs) search_dialog.col_node=Nodo search_dialog.col_code=Código #search_dialog.sort_results=Sort results search_dialog.regex=Regex #search_dialog.active_tab=Active tab only #search_dialog.comments=Comments #search_dialog.resource=Resource #search_dialog.keep_open=Keep open #search_dialog.tip_searching=Searching #search_dialog.limit_package=Limit to package: #search_dialog.res_text=Text #search_dialog.res_binary=Binary #search_dialog.package_not_found=No matching package found search_dialog.copy=copiar todo usage_dialog.title=Usage search usage_dialog.label=Usage for: #usage_dialog_plus.title=Usage tree search #usage_dialog_plus.jump_to=Jump to current location #usage_dialog_plus.copy_path=Copy usage tree path #usage_dialog_plus.search_complete=Search completed #usage_dialog_plus.code_view=Code View #usage_dialog_plus.select_node=Select Node #usage_dialog_plus.code_for=Code for %s #usage_dialog_plus.expand_usages=Expand Data #comment_dialog.title.add=Add code comment #comment_dialog.title.update=Update code comment #comment_dialog.label=Comment: #comment_dialog.style=Style: #comment_dialog.usage=Use 'Shift + Enter' to start a new line #rename_dialog.class_help=Enter full name to move class to another package. Start with '.' to move to default (empty) package #export_dialog.title=Export #export_dialog.save_path=Save path: #export_dialog.browse=Browse #export_dialog.export_options=Export options #export_dialog.export_gradle=Export as a Gradle project #export_dialog.export_gradle_type=Gradle template: log_viewer.title=Visor log log_viewer.log_level=Nivel log: #log_viewer.mode=Mode: #log_viewer.modes=All|All scripts|Current script #log_viewer.hide=Hide #log_viewer.dock=Dock #log_viewer.undock=Undock #log_viewer.clear=Clear about_dialog.title=Sobre JADX preferences.title=Preferencias preferences.deobfuscation=Desofuscación #preferences.appearance=Appearance #preferences.shortcuts=Shortcuts #preferences.select_shortcuts=Select a specific shortcuts group preferences.decompile=Descompilación #preferences.plugins=Plugins #preferences.project=Project preferences.other=Otros preferences.language=Idioma #preferences.lineNumbersMode=Editor line numbers mode #preferences.jumpOnDoubleClick=Enable jump on double click #preferences.useAlternativeFileDialog=Use alternative file dialog preferences.check_for_updates=Buscar actualizaciones al iniciar #preferences.useDx=Use dx/d8 to convert java bytecode #preferences.decompilationMode=Decompilation mode #preferences.codeCacheMode=Code cache mode #preferences.codeCacheMode.memory=Memory #preferences.codeCacheMode.memory.desc=Everything in memory: fast search, slow reopen, high memory usage #preferences.codeCacheMode.diskWithCache=Disk with cache #preferences.codeCacheMode.diskWithCache.desc=Code saved on disk with in memory cache: medium search, fast reopen, medium memory usage #preferences.codeCacheMode.disk=Disk #preferences.codeCacheMode.disk.desc=Everything on disk: slow search, fast reopen, low memory usage #preferences.usageCacheMode=Usage data cache mode preferences.showInconsistentCode=Mostrar código inconsistente preferences.escapeUnicode=Escape unicode preferences.replaceConsts=Reemplazar constantes #preferences.respectBytecodeAccessModifiers=Respect bytecode access modifiers #preferences.useImports=Use import statements #preferences.useDebugInfo=Use debug info #preferences.inlineAnonymous=Inline anonymous classes #preferences.inlineMethods=Inline methods #preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas #preferences.moveInnerClasses=Move inner classes into parent #preferences.extractFinally=Extract finally block #preferences.restoreSwitchOverString=Restore switch over string #preferences.fsCaseSensitive=File system is case-sensitive preferences.skipResourcesDecode=No descodificar recursos #preferences.skipSourcesDecode=Don't decompile source code #preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename #preferences.commentsLevel=Code comments level #preferences.saveOption=Auto-save settings preferences.threads=Número de hilos a procesar #preferences.excludedPackages=Excluded packages #preferences.excludedPackages.tooltip=List of space separated package names that will not be decompiled or indexed (saves RAM) #preferences.excludedPackages.button=Edit #preferences.excludedPackages.editDialog=List of space separated package names that will not be decompiled or indexed (saves RAM)
e.g. android.support preferences.cfg=Generar methods CFG graphs (in 'dot' format) preferences.raw_cfg=Generate RAW CFG graphs #preferences.xposed_codegen_language=Xposed code generation language #preferences.update_channel=Jadx update channel #preferences.disable_tooltip_on_hover=Disable tooltip on hover #preferences.integerFormat=Integer format #preferences.typeUpdatesCountLimit=Update type limit count #preferences.ui_zoom=UI Zoom factor #preferences.apply_ui_zoom_to_fonts=Apply UI zoom to fonts #preferences.ui_font=UI font preferences.code_font=Fuente del editor #preferences.smali_font=Monospaced font (Smali/Hex) #preferences.laf_theme=Theme #preferences.dynamic_editor_theme=Use UI theme colors preferences.theme=Tema del editor preferences.start_jobs=Inicio autom. descompilación de fondo preferences.select_font=Seleccionar preferences.deobfuscation_on=Activar desobfuscación #preferences.generated_renames_mapping_file_mode=Map file handle mode preferences.deobfuscation_min_len=Longitud mínima del nombre preferences.deobfuscation_max_len=Longitud máxima del nombre #preferences.deobfuscation_res_name_source=Better resources name source #preferences.deobfuscation_res_use_headers=Use headers for detect resource extensions #preferences.deobfuscation_whitelist=Exclude packages and classes from deobfuscation #preferences.deobfuscation_whitelist.editDialog=Whitelist for deobfuscation #preferences.deobfuscation_whitelist.tooltip=List of ':' separated packages (suffix '.*') and class names that will not be deobfuscated preferences.save=Guardar preferences.cancel=Cancelar preferences.reset=Reestablecer preferences.reset_message=¿Reestablecer preferencias a valores por defecto? preferences.reset_title=Reestablecer preferencias #preferences.copy=Copy to clipboard #preferences.copy_message=All settings values has been copied to clipboard #preferences.rename=Rename identifiers #preferences.rename_case=To fix case sensitivity issues #preferences.rename_valid=To make them valid #preferences.rename_printable=To make printable preferences.rename_use_source_name_as_class_name_alias=Usar el nombre del source como alias para la clase #preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number #preferences.search_results_per_page=Results per page (0 - no limit) #preferences.res_file_ext=Resource files extensions ('xml|html', * for all) #preferences.res_skip_file=Skip resources files if larger (MB) (0 - disable) #preferences.tab_dnd_appearance=Dragging tab appearance #preferences.plugins.manage=Manage plugins #preferences.plugins.install=Install plugin #preferences.plugins.install_btn=Install #preferences.plugins.uninstall_btn=Uninstall #preferences.plugins.disable_btn=Disable #preferences.plugins.enable_btn=Enable #preferences.plugins.location_id_label=Location id: #preferences.plugins.plugin_jar=Select Plugin jar #preferences.plugins.plugin_jar_label=or #preferences.plugins.update_all=Update All #preferences.plugins.details=Plugin details #preferences.plugins.task.installing=Installing plugin #preferences.plugins.task.uninstalling=Uninstalling plugin #preferences.plugins.task.updating=Updating plugins #preferences.plugins.task.downloading_list=Downloading plugins list #preferences.plugins.task.status=Changing plugin status #preferences.cache=Cache #preferences.cache.location=Cache location #preferences.cache.location_default=App cache system directory #preferences.cache.location_local=Same directory as project file #preferences.cache.location_custom=Custom location: #preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset #preferences.cache.table.title=Caches list #preferences.cache.table.project=Cache for project #preferences.cache.table.size=Disk usage #preferences.cache.btn.usage=Calculate usage #preferences.cache.btn.delete_selected=Delete Selected #preferences.cache.btn.delete_all=Delete All #preferences.cache.task.usage=Calculating cache size #preferences.cache.task.delete=Deleting caches msg.open_file=Por favor, abra un archivo msg.saving_sources=Guardando fuente msg.language_changed_title=Idioma cambiado msg.language_changed=El nuevo idioma se mostrará la próxima vez que la aplicación se inicie. #msg.warning_title=Warning #msg.common_mouse_shortcut=This is a commonly used key, are you sure you would like to bind it to an action? #msg.duplicate_shortcut=The shortcut %s is already set in action "%s" from category "%s", continue ? #msg.cmd_select_class_error=Failed to select the class\n%s\nThe class does not exist. #msg.cant_add_comment=Can't add comment here #msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars? #msg.non_displayable_chars.title=Undisplayed Strings #methods_dialog.title=Select methods #popup.bytecode_col=Show Dalvik Bytecode #popup.line_wrap=Line Wrap popup.undo=Deshacer popup.redo=Rehacer popup.cut=Cortar popup.copy=Copiar popup.paste=Pegar popup.delete=Borrar popup.select_all=Seleccionar todo popup.frida=Copiar como fragmento de frida popup.xposed=Copiar como fragmento de xposed #popup.find_usage=Find Usage #popup.go_to_declaration=Go to declaration #popup.exclude=Exclude #popup.exclude_packages=Exclude packages #popup.convert_number=Add conversion as comment #popup.view_call_graph=View call graph #popup.view_call_graph_description=Show call chains to this function #popup.view_class_graph=View inheritance graph #popup.view_class_graph_description=Show inheritance tree for this class #popup.view_class_method_graph=View methods graph #popup.view_class_method_graph_description=Show all methods for this class #popup.cfg_submenu=View control flow graph #popup.view_cfg=Regular #popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) #popup.view_raw_cfg=Raw #popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) #popup.view_region_cfg=Region #popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) #popup.add_comment=Comment #popup.update_comment=Update comment #popup.search_comment=Search comments popup.rename=Renombrar #popup.search=Search "%s" #popup.search_global=Global Search "%s" #popup.remove=Remove #popup.add_files=Add files #popup.add_scripts=Add scripts #popup.new_script=New script #popup.json_prettify=JSON Prettify #popup.export=Export #popup.usage_dialog_plus=Usage Tree Search #popup.copy_as=Copy as... #popup.copy_as_hex=Copy as HEX #popup.copy_as_string=Copy as String #popup.copy_offset=Copy offset #script.run=Run #script.save=Save #script.auto_complete=Auto Complete #script.check=Check #script.format=Reformat #script.log=Show log #encoding_dialog.title=Encoding #encoding_dialog.message=Select encoding: #exclude_dialog.title=Package Selector #exclude_dialog.select_all=Select all #exclude_dialog.deselect=Deselect #exclude_dialog.invert=Invert #confirm.save_as_title=Confirm Save as #confirm.save_as_message=%s already exists.\nDo you want to replace it? #confirm.not_saved_title=Save project #confirm.not_saved_message=Save the current project before proceeding? #confirm.remember=Remember my decision certificate.cert_type=Tipo certificate.serialSigVer=Versión certificate.serialNumber=Número de serial certificate.cert_subject=Subject certificate.serialValidFrom=Válido desde certificate.serialValidUntil=Válido hasta certificate.serialPubKeyType=Tipo de clave pública certificate.serialPubKeyExponent=Exponente certificate.serialPubKeyModulus=Módulo #certificate.serialPubKeyModulusSize=Modulus size (bits) certificate.serialSigType=Tipo de firma certificate.serialSigOID=Firma OID certificate.serialMD5=Huella MD5 certificate.serialSHA1=Huella SHA-1 certificate.serialSHA256=Huella SHA-256 certificate.serialPubKeyY=Y #apkSignature.signer=Signer #apkSignature.loading=Loading signature... #apkSignature.verificationSuccess=Signature verification succeeded #apkSignature.verificationFailed=Signature verification failed #apkSignature.signatureSuccess=Valid APK signature v%d found #apkSignature.signatureFailed=Invalid APK signature v%d found #apkSignature.errors=Errors #apkSignature.warnings=Warnings #apkSignature.exception=APK verification failed #apkSignature.unprotectedEntry=Files that are not protected by APK signature v1. Unauthorized modifications to these entries can only be detected by APK signature v2 and higher. #issues_panel.label=Issues: #issues_panel.errors=%d errors #issues_panel.warnings=%d warnings #issues_panel.tooltip=Open in log viewer #debugger.process_selector=Select a process to debug #debugger.step_into=Step Into (F7) #debugger.step_over=Step Over (F8) #debugger.step_out=Step Out (Shift + F8) #debugger.run=Run (F9) #debugger.stop=Stop debugger and kill app #debugger.pause=Pause #debugger.rerun=Rerun #debugger.cfm_dialog_title=Exit while debugging #debugger.cfm_dialog_msg=Are you sure to terminate debugger? #debugger.popup_set_value=Set Value #debugger.popup_change_to_zero=Change to 0 #debugger.popup_change_to_one=Change to 1 #debugger.popup_copy_value=Copy Value #logcat.pause=Pause Logcat #logcat.start=Resume Logcat #logcat.clear=Clear Logcat #logcat.error_fail_start=Failed to start logcat #logcat.process=Process #logcat.level=Level #logcat.default=Default #logcat.verbose=Verbose #logcat.debug=Debug #logcat.info=Info #logcat.warn=Warn #logcat.error=Error #logcat.fatal=Fatal #logcat.silent=Silent #logcat.logcat=Logcat #logcat.select_attached=Select Attached #logcat.select_all=Select All #logcat.unselect_all=Unselect All #set_value_dialog.label_value=Value #set_value_dialog.btn_set=Set Value #set_value_dialog.title=Set Value #set_value_dialog.neg_msg=Failed to set value. #set_value_dialog.sel_type=Select a type to set value. #adb_dialog.addr=ADB Addr #adb_dialog.port=ADB Port #adb_dialog.path=ADB Path #adb_dialog.launch_app=Launch App #adb_dialog.start_server=Start ADB Server #adb_dialog.refresh=Refresh #adb_dialog.tip_devices=%d devices #adb_dialog.device_node=Device #adb_dialog.missing_path=Must provide the ADB path to start an ADB server. #adb_dialog.waiting=Waiting to connect to ADB server... #adb_dialog.connecting=Connecting to ADB server, addr: %s:%s... #adb_dialog.connect_okay=ADB server connected, addr: %s:%s #adb_dialog.connect_fail=Failed to connect to ADB server. #adb_dialog.disconnected=ADB server disconnected. #adb_dialog.start_okay=ADB server started on port: %s. #adb_dialog.start_fail=Failed to start ADB server on port: %s! #adb_dialog.forward_fail=Failed to forward for some reasons. #adb_dialog.being_debugged_msg=This process seems like it's being debugged, should we proceed? #adb_dialog.unknown_android_ver=Failed to get Android release version, use Android 8 as default? #adb_dialog.being_debugged_title=It's Debugging by other. #adb_dialog.init_dbg_fail=Failed to init debugger. #adb_dialog.msg_read_mani_fail=Failed to decode AndroidManifest.xml #adb_dialog.no_devices=Can't found any device to start app. #adb_dialog.restart_while_debugging_title=Restart while debugging #adb_dialog.restart_while_debugging_msg=You're debugging an app, are you sure to restart a session? #adb_dialog.starting_debugger=Starting debugger... #action.variant=%s (variant) #action_category.menu_toolbar=Menu / Toolbar #action_category.hex_viewer=View / Hex Viewer #action_category.code_area=Code Area #action_category.plugin_script=Plugin Script #hex_viewer.show_inspector=Show Inspector #hex_viewer.change_encoding=Change Encoding #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find #graph_viewer.long_names=Show full names #graph_viewer.overrides=Show overrides #graph_viewer.callee_depth=Down depth #graph_viewer.caller_depth=Up depth #graph_viewer.default_error=Failed to view graph #graph_viewer.file_not_found_error=Failed to load graph file #graph_viewer.image_too_large=Failed to render graph: graph too large #graph_viewer.image_too_small=Failed to render graph: graph too small #graph_viewer.file_failure=Error in File Operation #graph_viewer.save_graph=Save graph #graph_viewer.default_title=Graph Viewer #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph #graph_viewer.cfg.title=Control Flow Graph ================================================ FILE: jadx-gui/src/main/resources/i18n/Messages_id_ID.properties ================================================ language.name=Indonesian menu.file=Berkas menu.view=Tampilan menu.recent_projects=Proyek Terbaru menu.no_recent_projects=Tidak ada proyek terbaru menu.preferences=Preferensi menu.sync=Sinkronisasi dengan editor menu.flatten=Tampilkan paket yang diratakan #menu.enable_preview_tab=Enable Preview Tab menu.heapUsageBar=Tampilkan penggunaan memori menu.alwaysSelectOpened=Selalu Pilih Berkas/Kelas yang Terbuka menu.dock_log=Kaitkan pemantau log #menu.dock_quick_tabs=Show Quick Tabs menu.navigation=Navigasi menu.text_search=Pencarian Teks menu.class_search=Pencarian Kelas menu.comment_search=Pencarian Komentar menu.go_to_main_activity=Pergi ke Activitas Utama #menu.go_to_application=Go to Application #menu.go_to_android_manifest=Go to AndroidManifest.xml menu.tools=Alat menu.plugins=Plugin menu.decompile_all=Deskompilasi semua kelas menu.reset_cache=Reset cache kode menu.deobfuscation=Deobfikasi menu.log=Pemantau Log #menu.create_desktop_entry=Create Desktop Entry menu.help=Bantuan menu.about=Tentang menu.quark=Mesin Quark menu.update_label=Versi baru %s tersedia! #menu.hex_viewer=Hex Viewer file.open_action=Buka berkas ... file.add_files_action=Tambahkan berkas file.open_title=Buka berkas file.open_project=Buka proyek file.new_project=Proyek baru file.save_project=Simpan proyek file.save_project_as=Simpan proyek sebagai... file.reload=Muat ulang berkas file.live_reload=Muat ulang otomatis file.live_reload_desc=Muat ulang berkas secara otomatis saat ada perubahan file.open_mappings=Buka pemetaan... file.save_mappings=Simpan pemetaan file.save_mappings_as=Simpan pemetaan sebagai... file.close_mappings=Tutup pemetaan file.save_all=Simpan semua file.save=Simpan #file.export=Export project file.save_all_msg=Pilih direktori untuk menyimpan sumber yang telah didekompilasi file.exit=Keluar #file.export_node=Export file start_page.title=Halaman Awal start_page.start=Mulai start_page.recent=Proyek Terbaru #start_page.list.delete_recent_project=Delete project #start_page.list.delete_recent_project.tooltip=Delete project from recent list tree.inputs_title=Input tree.input_files=Berkas tree.input_scripts=Skrip tree.sources_title=Kode Sumber tree.resources_title=Sumber Daya tree.loading=Memuat... #tree.pinned_tabs=Pinned Tabs #tree.open_tabs=Open Tabs #tree.bookmarked_tabs=Bookmarked Tabs progress.load=Memuat progress.save_mappings=Menyimpan pemetaan progress.decompile=Deskompilasi progress.canceling=Membatalkan error_dialog.title=Kesalahan error_dialog.not_found=%s tidak ditemukan #error_dialog.not_found_file=File not found at path:\n%s #error_dialog.path_is_directory=The specified path is a directory:\n%s #error_dialog.cannot_read=Cannot read file:\n%s #error_dialog.open_failed=Failed to open file:\n%s #error_dialog.invalid_path_format=Invalid file path:\n%s #error_dialog.desktop_unsupported=Desktop is not supported in this environment. search.previous=Sebelumnya search.next=Berikutnya search.mark_all=Tandai Semua search.regex=Regex search.match_case=Sama huruf search.whole_word=Seluruh kata search.find=Cari search.results=%s%d hasil #search.match_of=Match %d of %d #search.match_found=Match found #search.match_not_found=No Matches found #search.single_match=Single match found #search.find_type_text=Find by text #search.find_type_hex=Find by hex tabs.copy_class_name=Salin Nama tabs.close=Tutup tabs.closeOthers=Tutup yang Lain #tabs.unpin=Unpin #tabs.unpin_all=Unpin All #tabs.bookmark=Bookmark #tabs.unbookmark=Unbookmark #tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=Tutup Semua tabs.closeAllRight=Tutup Semua yang Kanan #tabs.closeAllLeft=Close All Left tabs.code=Kode tabs.smali=Smali tabs.smali_bytecode=Smali+Bytecode nav.back=Kembali nav.forward=Maju message.taskTimeout=Tugas melebihi batas waktu %d ms. message.userCancelTask=Tugas dibatalkan oleh pengguna. message.memoryLow=JADX kekurangan memori. Harap restart dengan peningkatan ukuran heap maksimum. message.taskError=Tugas gagal dengan kesalahan (periksa log untuk detailnya). message.errorTitle=Kesalahan message.load_errors=Pemuatan gagal.\nJumlah kesalahan: %d\nKlik OK untuk membuka penampil log. message.no_classes=Tidak ada kelas yang dimuat, tidak ada yang dapat didekompilasi! #message.enter_valid_path=Enter valid path to save! message.saveIncomplete=Simpan tidak lengkap.
%s
%d kelas atau sumber daya tidak disimpan! message.indexIncomplete=Indeks beberapa kelas dilewati.
%s
%d kelas tidak diindeks dan tidak akan muncul dalam hasil pencarian! message.indexingClassesSkipped=JADX kekurangan memori. Oleh karena itu %d kelas tidak diindeks.
Jika Anda ingin semua kelas diindeks, restart JADX dengan ukuran heap maksimum yang lebih besar. #message.enter_new_name=Enter new name #message.could_not_rename=Can't rename the file #message.confirm_remove_script=Do you really want to remove script? #message.desktop_entry_creation_error=Failed to create desktop entry (check log for details). #message.desktop_entry_creation_success=Desktop entry created successfully! #message.success_title=Success #message.unable_preview_font=Unable preview font heapUsage.text=Penggunaan memori JADX: %.2f GB dari %.2f GB (%.2f GB tertinggi) common_dialog.ok=OK common_dialog.cancel=Batal common_dialog.add=Tambah common_dialog.update=Perbarui common_dialog.remove=Hapus common_dialog.reset=Reset file_dialog.supported_files=Berkas yang didukung file_dialog.load_dir_title=Muat direktori file_dialog.load_dir_confirm=Muat semua berkas dari direktori? search_dialog.open=Buka search_dialog.cancel=Batal search_dialog.open_by_name=Cari teks: search_dialog.search_button=Cari search_dialog.search_history=Riwayat pencarian search_dialog.auto_search=Pencarian otomatis search_dialog.search_in=Cari definisi dari: search_dialog.class=Kelas search_dialog.method=Metode search_dialog.field=Bidang search_dialog.code=Kode search_dialog.ignorecase=Non-ketetapan huruf besar kecil search_dialog.load_more=Muat lebih banyak search_dialog.load_all=Muat semua search_dialog.stop=Berhenti search_dialog.results_incomplete=Ditemukan %d+ search_dialog.results_complete=Ditemukan %d (lengkap) search_dialog.resources_load_errors=Kesalahan pemuatan: %d search_dialog.resources_skip_by_size=Dilewati karena ukuran: %d search_dialog.resources_check_logs=(klik untuk memeriksa log) search_dialog.col_node=Node search_dialog.col_code=Kode search_dialog.sort_results=Sortir hasil search_dialog.regex=Regex search_dialog.active_tab=Hanya tab aktif search_dialog.comments=Komentar search_dialog.resource=Sumber daya search_dialog.keep_open=Tetap terbuka search_dialog.tip_searching=Mencari #search_dialog.limit_package=Limit to package: #search_dialog.res_text=Text #search_dialog.res_binary=Binary #search_dialog.package_not_found=No matching package found search_dialog.copy=salin semua usage_dialog.title=Pencarian penggunaan usage_dialog.label=Penggunaan untuk: #usage_dialog_plus.title=Usage tree search #usage_dialog_plus.jump_to=Jump to current location #usage_dialog_plus.copy_path=Copy usage tree path #usage_dialog_plus.search_complete=Search completed #usage_dialog_plus.code_view=Code View #usage_dialog_plus.select_node=Select Node #usage_dialog_plus.code_for=Code for %s #usage_dialog_plus.expand_usages=Expand Data comment_dialog.title.add=Tambahkan komentar kode comment_dialog.title.update=Perbarui komentar kode comment_dialog.label=Komentar: #comment_dialog.style=Style: comment_dialog.usage=Gunakan Shift + Enter untuk memulai baris baru #rename_dialog.class_help=Enter full name to move class to another package. Start with '.' to move to default (empty) package #export_dialog.title=Export #export_dialog.save_path=Save path: #export_dialog.browse=Browse #export_dialog.export_options=Export options #export_dialog.export_gradle=Export as a Gradle project #export_dialog.export_gradle_type=Gradle template: log_viewer.title=Pemantau Log log_viewer.log_level=Tingkat log: log_viewer.mode=Mode: log_viewer.modes=Semua|Semua skrip|Skrip saat ini log_viewer.hide=Sembunyikan log_viewer.dock=Kaitkan log_viewer.undock=Lepas kaitan log_viewer.clear=Bersihkan about_dialog.title=Tentang JADX preferences.title=Preferensi preferences.deobfuscation=Deobfikasi preferences.appearance=Tampilan preferences.shortcuts=Pintas preferences.select_shortcuts=Pilih grup pintas tertentu preferences.decompile=Deskompilasi preferences.plugins=Plugin preferences.project=Proyek preferences.other=Lainnya preferences.language=Bahasa preferences.lineNumbersMode=Mode nomor baris editor preferences.jumpOnDoubleClick=Aktifkan lompat saat dua kali klik preferences.useAlternativeFileDialog=Gunakan dialog berkas alternatif preferences.check_for_updates=Periksa pembaruan saat memulai preferences.useDx=Gunakan dx/d8 untuk mengonversi bytecode Java preferences.decompilationMode=Mode deskompilasi preferences.codeCacheMode=Mode cache kode #preferences.codeCacheMode.memory=Memory #preferences.codeCacheMode.memory.desc=Everything in memory: fast search, slow reopen, high memory usage #preferences.codeCacheMode.diskWithCache=Disk with cache #preferences.codeCacheMode.diskWithCache.desc=Code saved on disk with in memory cache: medium search, fast reopen, medium memory usage #preferences.codeCacheMode.disk=Disk #preferences.codeCacheMode.disk.desc=Everything on disk: slow search, fast reopen, low memory usage preferences.usageCacheMode=Mode cache data penggunaan preferences.showInconsistentCode=Tampilkan kode yang tidak konsisten preferences.escapeUnicode=Escape unicode preferences.replaceConsts=Ganti konstanta preferences.respectBytecodeAccessModifiers=Hormati pengubah akses bytecode preferences.useImports=Gunakan pernyataan impor preferences.useDebugInfo=Gunakan info debug preferences.inlineAnonymous=Inline kelas anonim preferences.inlineMethods=Inline metode preferences.inlineKotlinLambdas=Izinkan untuk menggantikan Lambdas Kotlin preferences.moveInnerClasses=Pindahkan kelas dalam ke kelas induk preferences.extractFinally=Ekstrak blok finally #preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=File sistem bersifat sensitif huruf besar-kecil preferences.skipResourcesDecode=Jangan dekode sumber daya #preferences.skipSourcesDecode=Don't decompile source code preferences.useKotlinMethodsForVarNames=Gunakan metode Kotlin untuk mengganti nama variabel preferences.commentsLevel=Tingkat komentar kode #preferences.saveOption=Auto-save settings preferences.threads=Jumlah utas pemrosesan preferences.excludedPackages=Package yang dikecualikan preferences.excludedPackages.tooltip=Daftar nama paket yang dipisahkan spasi yang tidak akan di deskompilasi atau diindeks (menghemat RAM) preferences.excludedPackages.button=Edit preferences.excludedPackages.editDialog=Daftar nama paket yang dipisahkan spasi yang tidak akan di deskompilasi atau diindeks (menghemat RAM)
contoh: android.support preferences.cfg=Hasilkan grafik CFG metode (dalam format 'dot') preferences.raw_cfg=Hasilkan grafik CFG mentah #preferences.xposed_codegen_language=Xposed code generation language #preferences.update_channel=Jadx update channel #preferences.disable_tooltip_on_hover=Disable tooltip on hover preferences.integerFormat=Format bilangan bulat #preferences.typeUpdatesCountLimit=Update type limit count #preferences.ui_zoom=UI Zoom factor #preferences.apply_ui_zoom_to_fonts=Apply UI zoom to fonts #preferences.ui_font=UI font preferences.code_font=Font editor preferences.smali_font=Font monospasi (Smali/Hex) preferences.laf_theme=Tema #preferences.dynamic_editor_theme=Use UI theme colors preferences.theme=Tema editor preferences.start_jobs=Deskompilasi latar belakang otomatis preferences.select_font=Ubah preferences.deobfuscation_on=Aktifkan deobfikasi preferences.generated_renames_mapping_file_mode=Mode penanganan file pemetaan preferences.deobfuscation_min_len=Panjang nama minimum preferences.deobfuscation_max_len=Panjang nama maksimum preferences.deobfuscation_res_name_source=Sumber nama sumber daya yang lebih baik #preferences.deobfuscation_res_use_headers=Use headers for detect resource extensions #preferences.deobfuscation_whitelist=Exclude packages and classes from deobfuscation #preferences.deobfuscation_whitelist.editDialog=Whitelist for deobfuscation #preferences.deobfuscation_whitelist.tooltip=List of ':' separated packages (suffix '.*') and class names that will not be deobfuscated preferences.save=Simpan preferences.cancel=Batal preferences.reset=Reset preferences.reset_message=Reset pengaturan ke nilai default? preferences.reset_title=Reset pengaturan preferences.copy=Salin ke papan klip preferences.copy_message=Semua nilai pengaturan telah disalin ke papan klip preferences.rename=Ganti nama identitas preferences.rename_case=Untuk memperbaiki masalah sensitivitas huruf besar-kecil preferences.rename_valid=Untuk membuatnya valid preferences.rename_printable=Untuk membuatnya dapat dicetak preferences.rename_use_source_name_as_class_name_alias=Gunakan nama berkas sumber sebagai alias nama kelas #preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number preferences.search_results_per_page=Hasil per halaman (0 - tanpa batas) preferences.res_file_ext=Ekstensi berkas sumber daya ('xml|html', * untuk semua) preferences.res_skip_file=Lewati berkas sumber daya jika lebih besar (MB) (0 - nonaktifkan) preferences.tab_dnd_appearance=Dragging tab appearance #preferences.plugins.manage=Manage plugins preferences.plugins.install=Instal plugin preferences.plugins.install_btn=Instal preferences.plugins.uninstall_btn=Uninstal #preferences.plugins.disable_btn=Disable #preferences.plugins.enable_btn=Enable preferences.plugins.location_id_label=Lokasi id: preferences.plugins.plugin_jar=Pilih berkas jar Plugin preferences.plugins.plugin_jar_label=atau preferences.plugins.update_all=Perbarui Semua preferences.plugins.details=Detail Plugin preferences.plugins.task.installing=Menginstal plugin preferences.plugins.task.uninstalling=Menguninstal plugin preferences.plugins.task.updating=Mengperbarui plugin preferences.plugins.task.downloading_list=Mengunduh daftar plugin #preferences.plugins.task.status=Changing plugin status preferences.cache=Cache preferences.cache.location=Lokasi cache preferences.cache.location_default=Direktori sistem cache aplikasi preferences.cache.location_local=Direktori yang sama dengan berkas proyek preferences.cache.location_custom=Lokasi kustom: preferences.cache.change_notice=* untuk cache yang sudah ada, perubahan akan diterapkan setelah cache dihapus atau direset secara manual preferences.cache.table.title=Daftar Cache preferences.cache.table.project=Cache untuk proyek preferences.cache.table.size=Penggunaan disk preferences.cache.btn.usage=Hitung penggunaan preferences.cache.btn.delete_selected=Hapus yang Dipilih preferences.cache.btn.delete_all=Hapus Semua preferences.cache.task.usage=Menghitung ukuran cache preferences.cache.task.delete=Menghapus cache msg.open_file=Harap buka berkas msg.saving_sources=Menyimpan sumber msg.language_changed_title=Bahasa diubah msg.language_changed=Bahasa baru akan ditampilkan saat aplikasi dimulai kembali. msg.warning_title=Peringatan msg.common_mouse_shortcut=Ini adalah tombol yang sering digunakan, apakah Anda yakin ingin mengaitkannya dengan tindakan? msg.duplicate_shortcut=Pintas %s sudah diatur pada tindakan "%s" dari kategori "%s", lanjutkan? msg.cmd_select_class_error=Gagal memilih kelas\n%s\nKelas tidak ada. msg.cant_add_comment=Tidak dapat menambahkan komentar di sini #msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars? #msg.non_displayable_chars.title=Undisplayed Strings #methods_dialog.title=Select methods popup.bytecode_col=Tampilkan Bytecode Dalvik popup.line_wrap=Baris Wrap popup.undo=Kembalikan popup.redo=Ulang popup.cut=Potong popup.copy=Salin popup.paste=Tempel popup.delete=Hapus popup.select_all=Pilih Semua popup.frida=Salin sebagai potongan Frida popup.xposed=Salin sebagai potongan Xposed popup.find_usage=Cari Penggunaan popup.go_to_declaration=Pergi ke Deklarasi popup.exclude=Kecualikan popup.exclude_packages=Kecualikan paket #popup.convert_number=Add conversion as comment #popup.view_call_graph=View call graph #popup.view_call_graph_description=Show call chains to this function #popup.view_class_graph=View inheritance graph #popup.view_class_graph_description=Show inheritance tree for this class #popup.view_class_method_graph=View methods graph #popup.view_class_method_graph_description=Show all methods for this class #popup.cfg_submenu=View control flow graph #popup.view_cfg=Regular #popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) #popup.view_raw_cfg=Raw #popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) #popup.view_region_cfg=Region #popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=Komentar #popup.update_comment=Update comment popup.search_comment=Cari komentar popup.rename=Ganti nama popup.search=Cari "%s" popup.search_global=Cari Global "%s" popup.remove=Hapus popup.add_files=Tambahkan berkas popup.add_scripts=Tambahkan skrip popup.new_script=Skrip baru popup.json_prettify=JSON Prettify #popup.export=Export #popup.usage_dialog_plus=Usage Tree Search #popup.copy_as=Copy as... #popup.copy_as_hex=Copy as HEX #popup.copy_as_string=Copy as String #popup.copy_offset=Copy offset script.run=Jalankan script.save=Simpan script.auto_complete=Auto Lengkapi script.check=Periksa script.format=Format Ulang script.log=Tampilkan log #encoding_dialog.title=Encoding #encoding_dialog.message=Select encoding: exclude_dialog.title=Selektor Paket exclude_dialog.select_all=Pilih semua exclude_dialog.deselect=Batal pilih exclude_dialog.invert=Balik pilihan confirm.save_as_title=Konfirmasi Simpan Sebagai confirm.save_as_message=%s sudah ada.\nApakah Anda ingin menggantinya? confirm.not_saved_title=Simpan proyek confirm.not_saved_message=Simpan proyek saat ini sebelum melanjutkan? #confirm.remember=Remember my decision certificate.cert_type=Tipe certificate.serialSigVer=Versi Tanda Tangan certificate.serialNumber=Nomor Seri certificate.cert_subject=Subjek certificate.serialValidFrom=Berlaku dari certificate.serialValidUntil=Berlaku hingga certificate.serialPubKeyType=Tipe Kunci Publik certificate.serialPubKeyExponent=Eksponen certificate.serialPubKeyModulus=Modulus certificate.serialPubKeyModulusSize=Ukuran Modulus (bit) certificate.serialSigType=Tipe Tanda Tangan certificate.serialSigOID=OID Tanda Tangan certificate.serialMD5=Sidik Jari MD5 certificate.serialSHA1=Sidik Jari SHA-1 certificate.serialSHA256=Sidik Jari SHA-256 certificate.serialPubKeyY=Y apkSignature.signer=Penandatangan #apkSignature.loading=Loading signature... apkSignature.verificationSuccess=Verifikasi tanda tangan berhasil apkSignature.verificationFailed=Verifikasi tanda tangan gagal apkSignature.signatureSuccess=Tanda tangan APK yang valid v%d ditemukan apkSignature.signatureFailed=Tanda tangan APK yang tidak valid v%d ditemukan apkSignature.errors=Kesalahan apkSignature.warnings=Peringatan apkSignature.exception=Verifikasi APK gagal apkSignature.unprotectedEntry=Berkas yang tidak dilindungi oleh tanda tangan APK v1. Modifikasi yang tidak sah pada entri-entri ini hanya dapat terdeteksi oleh tanda tangan APK v2 dan yang lebih tinggi. issues_panel.label=Masalah: issues_panel.errors=%d kesalahan issues_panel.warnings=%d peringatan issues_panel.tooltip=Buka di penampil log debugger.process_selector=Pilih proses untuk debugging debugger.step_into=Langkah Masuk (F7) debugger.step_over=Langkah Atas (F8) debugger.step_out=Langkah Keluar (Shift + F8) debugger.run=Jalankan (F9) debugger.stop=Hentikan debugger dan hentikan aplikasi debugger.pause=Jeda debugger.rerun=Jalankan Ulang debugger.cfm_dialog_title=Keluar saat debugging debugger.cfm_dialog_msg=Apakah Anda yakin ingin mengakhiri debugger? debugger.popup_set_value=Tetapkan Nilai debugger.popup_change_to_zero=Ubah menjadi 0 debugger.popup_change_to_one=Ubah menjadi 1 debugger.popup_copy_value=Salin Nilai logcat.pause=Jeda Logcat logcat.start=Lanjutkan Logcat logcat.clear=Kosongkan Logcat logcat.error_fail_start=Gagal memulai Logcat logcat.process=Proses logcat.level=Tingkat logcat.default=Bawaan logcat.verbose=Verbose logcat.debug=Debug logcat.info=Info logcat.warn=Peringatan logcat.error=Kesalahan logcat.fatal=Fatal logcat.silent=Diam logcat.logcat=Logcat logcat.select_attached=Pilih Terlampir logcat.select_all=Pilih Semua logcat.unselect_all=Batal Pilih Semua set_value_dialog.label_value=Nilai set_value_dialog.btn_set=Tetapkan Nilai set_value_dialog.title=Tetapkan Nilai set_value_dialog.neg_msg=Gagal menetapkan nilai. set_value_dialog.sel_type=Pilih jenis untuk menetapkan nilai. adb_dialog.addr=Alamat ADB adb_dialog.port=Port ADB adb_dialog.path=Path ADB adb_dialog.launch_app=Jalankan Aplikasi adb_dialog.start_server=Menajalankan Server ADB adb_dialog.refresh=Segarkan adb_dialog.tip_devices=%d perangkat adb_dialog.device_node=Perangkat adb_dialog.missing_path=Harus memberikan path ADB untuk menjalankan server ADB. adb_dialog.waiting=Menunggu untuk terhubung ke server ADB... adb_dialog.connecting=Menghubungkan ke server ADB, alamat: %s:%s... adb_dialog.connect_okay=Server ADB terhubung, alamat: %s:%s adb_dialog.connect_fail=Gagal menghubungkan ke server ADB. adb_dialog.disconnected=Server ADB terputus. adb_dialog.start_okay=Server ADB dijalankan pada port: %s. adb_dialog.start_fail=Gagal menjalankan server ADB pada port: %s! adb_dialog.forward_fail=Gagal meneruskan dengan alasan tertentu. adb_dialog.being_debugged_msg=Proses ini sepertinya sedang di-debug, apakah kita harus melanjutkan? adb_dialog.unknown_android_ver=Gagal mendapatkan versi rilis Android, gunakan Android 8 sebagai default? adb_dialog.being_debugged_title=Sedang Didebug oleh Pihak Lain. adb_dialog.init_dbg_fail=Gagal menginisialisasi debugger. adb_dialog.msg_read_mani_fail=Gagal mendekode AndroidManifest.xml adb_dialog.no_devices=Tidak dapat menemukan perangkat apa pun untuk memulai aplikasi. adb_dialog.restart_while_debugging_title=Memulai Ulang Saat Debugging adb_dialog.restart_while_debugging_msg=Anda sedang debugging sebuah aplikasi, apakah Anda yakin ingin memulai ulang sesi? adb_dialog.starting_debugger=Memulai debugger... action.variant=%s (variant) action_category.menu_toolbar=Menu / Toolbar #action_category.hex_viewer=View / Hex Viewer action_category.code_area=Area Kode action_category.plugin_script=Plugin Script #hex_viewer.show_inspector=Show Inspector #hex_viewer.change_encoding=Change Encoding #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find #graph_viewer.long_names=Show full names #graph_viewer.overrides=Show overrides #graph_viewer.callee_depth=Down depth #graph_viewer.caller_depth=Up depth #graph_viewer.default_error=Failed to view graph #graph_viewer.file_not_found_error=Failed to load graph file #graph_viewer.image_too_large=Failed to render graph: graph too large #graph_viewer.image_too_small=Failed to render graph: graph too small #graph_viewer.file_failure=Error in File Operation #graph_viewer.save_graph=Save graph #graph_viewer.default_title=Graph Viewer #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph #graph_viewer.cfg.title=Control Flow Graph ================================================ FILE: jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties ================================================ language.name=한국어 menu.file=파일 menu.view=보기 menu.recent_projects=최근 프로젝트 menu.no_recent_projects=최근 프로젝트 없음 menu.preferences=설정 menu.sync=에디터와 동기화 menu.flatten=플랫 패키지 표시 #menu.enable_preview_tab=Enable Preview Tab menu.heapUsageBar=메모리 사용량 표시 menu.alwaysSelectOpened=항상 열린 파일/클래스 선택 #menu.dock_log=Dock Log Viewer #menu.dock_quick_tabs=Show Quick Tabs menu.navigation=네비게이션 menu.text_search=텍스트 검색 menu.class_search=클래스 검색 menu.comment_search=주석 검색 #menu.go_to_main_activity=Go to main Activity #menu.go_to_application=Go to Application #menu.go_to_android_manifest=Go to AndroidManifest.xml menu.tools=도구 #menu.plugins=Plugins #menu.decompile_all=Decompile all classes #menu.reset_cache=Reset code cache menu.deobfuscation=난독화 해제 menu.log=로그 뷰어 #menu.create_desktop_entry=Create Desktop Entry menu.help=도움말 menu.about=정보 #menu.quark=Quark Engine menu.update_label=새 버전 %s 이(가) 존재합니다! #menu.hex_viewer=Hex Viewer file.open_action=파일 열기 ... file.add_files_action=파일 추가 file.open_title=파일 열기 file.open_project=프로젝트 열기 file.new_project=새 프로젝트 file.save_project=프로젝트 저장 file.save_project_as=다른 이름으로 프로젝트 저장... file.reload=파일 다시 로드 file.live_reload=라이브 로드 file.live_reload_desc=파일 내용 변경 시 자동으로 다시 로드 #file.open_mappings=Open mappings... file.save_mappings=다른 이름으로 매핑 내보내기... #file.save_mappings_as=Save mappings as... #file.close_mappings=Close mappings file.save_all=모두 저장 #file.save=Save #file.export=Export project file.save_all_msg=디컴파일된 소스를 저장할 디렉토리 선택 file.exit=나가기 #file.export_node=Export file start_page.title=페이지 시작 start_page.start=시작 start_page.recent=최근 프로젝트 #start_page.list.delete_recent_project=Delete project #start_page.list.delete_recent_project.tooltip=Delete project from recent list #tree.inputs_title=Inputs #tree.input_files=Files #tree.input_scripts=Scripts tree.sources_title=소스코드 tree.resources_title=리소스 tree.loading=로딩중... #tree.pinned_tabs=Pinned Tabs #tree.open_tabs=Open Tabs #tree.bookmarked_tabs=Bookmarked Tabs progress.load=로딩중 progress.save_mappings=매핑 내보내는 중 progress.decompile=디컴파일 중 progress.canceling=취소 중 error_dialog.title=오류 #error_dialog.not_found=%s not found #error_dialog.not_found_file=File not found at path:\n%s #error_dialog.path_is_directory=The specified path is a directory:\n%s #error_dialog.cannot_read=Cannot read file:\n%s #error_dialog.open_failed=Failed to open file:\n%s #error_dialog.invalid_path_format=Invalid file path:\n%s #error_dialog.desktop_unsupported=Desktop is not supported in this environment. search.previous=이전 search.next=다음 search.mark_all=모두 선택 search.regex=정규식 search.match_case=매치 케이스 search.whole_word=전체 단어 search.find=찾기 #search.results=%s%d results #search.match_of=Match %d of %d #search.match_found=Match found #search.match_not_found=No Matches found #search.single_match=Single match found #search.find_type_text=Find by text #search.find_type_hex=Find by hex tabs.copy_class_name=이름 복사 tabs.close=닫기 tabs.closeOthers=이 탭을 제외하고 닫기 #tabs.unpin=Unpin #tabs.unpin_all=Unpin All #tabs.bookmark=Bookmark #tabs.unbookmark=Unbookmark #tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=모두 닫기 tabs.closeAllRight=오른쪽의 모든 것을 닫으십시오 #tabs.closeAllLeft=Close All Left tabs.code=코드 tabs.smali=Smali tabs.smali_bytecode=Smali+Bytecode nav.back=뒤로 nav.forward=앞으로 message.taskTimeout=작업이 %d ms의 시간 제한을 초과했습니다. message.userCancelTask=사용자가 작업을 취소했습니다. message.memoryLow=Jadx의 메모리가 부족합니다. 최대 힙 크기를 늘린 후 다시 시작하십시오. message.taskError=작업이 오류로 실패했습니다. (자세한 내용은 로그 확인) message.errorTitle=오류 message.load_errors=로드하지 못했습니다.\n오류 수: %d\n로그 뷰어를 열려면 확인을 클릭하십시오. message.no_classes=로드된 클래스 없음, 디컴파일 대상 존재하지 않음 #message.enter_valid_path=Enter valid path to save! message.saveIncomplete=저장이 완료되지 않았습니다.
%s
%d개의 클래스 또는 리소스가 저장되지 않았습니다! message.indexIncomplete=일부 클래스의 색인을 건너뛰었습니다.
%s
%d개의 클래스는 색인이 생성되지 않았으며 검색 결과에 나타나지 않습니다. message.indexingClassesSkipped=Jadx의 메모리가 부족합니다. 따라서 %d 개의 클래스가 인덱싱되지 않았습니다.
모든 클래스를 인덱싱하려면 최대 힙 크기를 늘린 상태로 Jadx를 다시 시작하십시오. #message.enter_new_name=Enter new name #message.could_not_rename=Can't rename the file #message.confirm_remove_script=Do you really want to remove script? #message.desktop_entry_creation_error=Failed to create desktop entry (check log for details). #message.desktop_entry_creation_success=Desktop entry created successfully! #message.success_title=Success #message.unable_preview_font=Unable preview font heapUsage.text=JADX 메모리 사용량 : %.2f GB / %.2f GB (%.2f GB 첨단) common_dialog.ok=확인 common_dialog.cancel=취소 common_dialog.add=추가 common_dialog.update=업데이트 common_dialog.remove=삭제 #common_dialog.reset=Reset file_dialog.supported_files=지원되는 파일 file_dialog.load_dir_title=디렉토리 불러오기 file_dialog.load_dir_confirm=디렉토리에서 모든 파일을 불러오시겠습니까? search_dialog.open=열기 search_dialog.cancel=취소 search_dialog.open_by_name=텍스트 검색 : search_dialog.search_button=검색 search_dialog.search_history=검색 기록 search_dialog.auto_search=자동 검색 search_dialog.search_in=정의 검색 : search_dialog.class=클래스 search_dialog.method=메소드 search_dialog.field=필드 search_dialog.code=코드 search_dialog.ignorecase=대소문자 구분 안함 search_dialog.load_more=더보기 search_dialog.load_all=모두 로드 search_dialog.stop=정지 search_dialog.results_incomplete=%d+개 찾음 search_dialog.results_complete=%d개 찾음 (검색 완료) #search_dialog.resources_load_errors=Load errors: %d #search_dialog.resources_skip_by_size=Skipped by size: %d #search_dialog.resources_check_logs=(click to check logs) search_dialog.col_node=노드 search_dialog.col_code=코드 search_dialog.sort_results=결과 정렬 search_dialog.regex=정규식 search_dialog.active_tab=열려 있는 탭에서만 검색 search_dialog.comments=주석 search_dialog.resource=리소스 search_dialog.keep_open=열어 두기 search_dialog.tip_searching=검색 중... #search_dialog.limit_package=Limit to package: #search_dialog.res_text=Text #search_dialog.res_binary=Binary #search_dialog.package_not_found=No matching package found search_dialog.copy=모두 복사 usage_dialog.title=사용 검색 usage_dialog.label=다음의 사용 검색 결과: #usage_dialog_plus.title=Usage tree search #usage_dialog_plus.jump_to=Jump to current location #usage_dialog_plus.copy_path=Copy usage tree path #usage_dialog_plus.search_complete=Search completed #usage_dialog_plus.code_view=Code View #usage_dialog_plus.select_node=Select Node #usage_dialog_plus.code_for=Code for %s #usage_dialog_plus.expand_usages=Expand Data comment_dialog.title.add=주석 추가 comment_dialog.title.update=주석 업데이트 comment_dialog.label=주석: #comment_dialog.style=Style: comment_dialog.usage=Shift + Enter 를 입력해 새 라인에 입력 #rename_dialog.class_help=Enter full name to move class to another package. Start with '.' to move to default (empty) package #export_dialog.title=Export #export_dialog.save_path=Save path: #export_dialog.browse=Browse #export_dialog.export_options=Export options #export_dialog.export_gradle=Export as a Gradle project #export_dialog.export_gradle_type=Gradle template: log_viewer.title=로그 뷰어 log_viewer.log_level=로그 레벨: #log_viewer.mode=Mode: #log_viewer.modes=All|All scripts|Current script #log_viewer.hide=Hide #log_viewer.dock=Dock #log_viewer.undock=Undock #log_viewer.clear=Clear about_dialog.title=JADX 정보 preferences.title=설정 preferences.deobfuscation=난독화 해제 preferences.appearance=외관 #preferences.shortcuts=Shortcuts #preferences.select_shortcuts=Select a specific shortcuts group preferences.decompile=디컴파일 preferences.plugins=플러그인 preferences.project=프로젝트 preferences.other=기타 preferences.language=언어 preferences.lineNumbersMode=편집기 줄 번호 모드 preferences.jumpOnDoubleClick=더블 클릭 시 점프 활성화 #preferences.useAlternativeFileDialog=Use alternative file dialog preferences.check_for_updates=시작시 업데이트 확인 preferences.useDx=dx/d8을 사용하여 Java 바이트 코드 변환 preferences.decompilationMode=디컴파일 모드 preferences.codeCacheMode=코드 캐시 모드 #preferences.codeCacheMode.memory=Memory #preferences.codeCacheMode.memory.desc=Everything in memory: fast search, slow reopen, high memory usage #preferences.codeCacheMode.diskWithCache=Disk with cache #preferences.codeCacheMode.diskWithCache.desc=Code saved on disk with in memory cache: medium search, fast reopen, medium memory usage #preferences.codeCacheMode.disk=Disk #preferences.codeCacheMode.disk.desc=Everything on disk: slow search, fast reopen, low memory usage #preferences.usageCacheMode=Usage data cache mode preferences.showInconsistentCode=디컴파일 안된 코드 표시 preferences.escapeUnicode=유니코드 이스케이프 preferences.replaceConsts=상수 바꾸기 preferences.respectBytecodeAccessModifiers=바이트코드 액세스 수정자 존중 preferences.useImports=import 문 사용 preferences.useDebugInfo=디버그 정보 사용 preferences.inlineAnonymous=인라인 익명 클래스 preferences.inlineMethods=인라인 메서드 #preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas #preferences.moveInnerClasses=Move inner classes into parent preferences.extractFinally=finally 블록 추출 #preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=파일 시스템 대소문자 구별 preferences.skipResourcesDecode=리소스 디코딩 하지 않기 #preferences.skipSourcesDecode=Don't decompile source code preferences.useKotlinMethodsForVarNames=변수 이름 바꾸기에 kotlin 메서드 사용 preferences.commentsLevel=코드 주석 수준 #preferences.saveOption=Auto-save settings preferences.threads=처리 스레드 수 preferences.excludedPackages=제외할 패키지 preferences.excludedPackages.tooltip=RAM 절약을 위해 디컴파일되거나 인덱싱하지 않을 패키지 이름 목록 (공백으로 항목 구분) preferences.excludedPackages.button=Edit preferences.excludedPackages.editDialog=RAM 절약을 위해 디컴파일되거나 인덱싱하지 않을 패키지 이름 목록 (공백으로 항목 구분)
예: android.support preferences.cfg=메소드 CFG 그래프 생성 ('dot' 포맷) preferences.raw_cfg=RAW CFG 그래프 생성 #preferences.xposed_codegen_language=Xposed code generation language #preferences.update_channel=Jadx update channel #preferences.disable_tooltip_on_hover=Disable tooltip on hover #preferences.integerFormat=Integer format #preferences.typeUpdatesCountLimit=Update type limit count #preferences.ui_zoom=UI Zoom factor #preferences.apply_ui_zoom_to_fonts=Apply UI zoom to fonts #preferences.ui_font=UI font preferences.code_font=에디터 글씨체 #preferences.smali_font=Monospaced font (Smali/Hex) preferences.laf_theme=테마 #preferences.dynamic_editor_theme=Use UI theme colors preferences.theme=에디터 테마 preferences.start_jobs=백그라운드에서 디컴파일 자동 시작 preferences.select_font=변경 preferences.deobfuscation_on=난독 해제 활성화 preferences.generated_renames_mapping_file_mode=맵 파일 처리 모드 preferences.deobfuscation_min_len=최소 이름 길이 preferences.deobfuscation_max_len=최대 이름 길이 preferences.deobfuscation_res_name_source=더 나은 리소스 이름 소스 #preferences.deobfuscation_res_use_headers=Use headers for detect resource extensions #preferences.deobfuscation_whitelist=Exclude packages and classes from deobfuscation #preferences.deobfuscation_whitelist.editDialog=Whitelist for deobfuscation #preferences.deobfuscation_whitelist.tooltip=List of ':' separated packages (suffix '.*') and class names that will not be deobfuscated preferences.save=저장 preferences.cancel=취소 preferences.reset=재설정 preferences.reset_message=설정을 기본값으로 재설정 하시겠습니까? preferences.reset_title=재설정 preferences.copy=클립보드에 복사 preferences.copy_message=모든 설정 값이 클립 보드에 복사되었습니다. preferences.rename=이름 바꾸기 preferences.rename_case=시스템 대소문자 구분 preferences.rename_valid=유효한 식별자로 바꾸기 preferences.rename_printable=출력 가능하게 바꾸기 preferences.rename_use_source_name_as_class_name_alias=소스 파일 이름을 클래스 이름 별칭으로 사용 #preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number #preferences.search_results_per_page=Results per page (0 - no limit) preferences.res_file_ext=파일 확장자 (예: .xml|.html) (* 은 전체를 의미) preferences.res_skip_file=이 옵션보다 큰 파일 건너 뛰기 (MB) preferences.tab_dnd_appearance=Dragging tab appearance #preferences.plugins.manage=Manage plugins #preferences.plugins.install=Install plugin #preferences.plugins.install_btn=Install #preferences.plugins.uninstall_btn=Uninstall #preferences.plugins.disable_btn=Disable #preferences.plugins.enable_btn=Enable #preferences.plugins.location_id_label=Location id: #preferences.plugins.plugin_jar=Select Plugin jar #preferences.plugins.plugin_jar_label=or #preferences.plugins.update_all=Update All #preferences.plugins.details=Plugin details #preferences.plugins.task.installing=Installing plugin #preferences.plugins.task.uninstalling=Uninstalling plugin #preferences.plugins.task.updating=Updating plugins #preferences.plugins.task.downloading_list=Downloading plugins list #preferences.plugins.task.status=Changing plugin status #preferences.cache=Cache #preferences.cache.location=Cache location #preferences.cache.location_default=App cache system directory #preferences.cache.location_local=Same directory as project file #preferences.cache.location_custom=Custom location: #preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset #preferences.cache.table.title=Caches list #preferences.cache.table.project=Cache for project #preferences.cache.table.size=Disk usage #preferences.cache.btn.usage=Calculate usage #preferences.cache.btn.delete_selected=Delete Selected #preferences.cache.btn.delete_all=Delete All #preferences.cache.task.usage=Calculating cache size #preferences.cache.task.delete=Deleting caches msg.open_file=파일을 여십시오 msg.saving_sources=소스 저장 중 msg.language_changed_title=언어 변경됨 msg.language_changed=다음에 응용 프로그램이 시작되면 새 언어가 표시됩니다. #msg.warning_title=Warning #msg.common_mouse_shortcut=This is a commonly used key, are you sure you would like to bind it to an action? #msg.duplicate_shortcut=The shortcut %s is already set in action "%s" from category "%s", continue ? msg.cmd_select_class_error=클래스를 선택하지 못했습니다.\n%s\n클래스가 없습니다. msg.cant_add_comment=여기에 주석을 추가할수 없음 #msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars? #msg.non_displayable_chars.title=Undisplayed Strings #methods_dialog.title=Select methods popup.bytecode_col=Dalvik Bytecode 보이기 popup.line_wrap=줄 바꿈 popup.undo=실행 취소 popup.redo=다시 실행 popup.cut=자르기 popup.copy=붙여넣기 popup.paste=복사 popup.delete=삭제 popup.select_all=모두 선택 popup.frida=frida 스니펫으로 복사 popup.xposed=xposed 스니펫으로 복사 popup.find_usage=사용 찾기 popup.go_to_declaration=선언문으로 이동 popup.exclude=제외 popup.exclude_packages=패키지 제외 #popup.convert_number=Add conversion as comment #popup.view_call_graph=View call graph #popup.view_call_graph_description=Show call chains to this function #popup.view_class_graph=View inheritance graph #popup.view_class_graph_description=Show inheritance tree for this class #popup.view_class_method_graph=View methods graph #popup.view_class_method_graph_description=Show all methods for this class #popup.cfg_submenu=View control flow graph #popup.view_cfg=Regular #popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) #popup.view_raw_cfg=Raw #popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) #popup.view_region_cfg=Region #popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=주석 #popup.update_comment=Update comment popup.search_comment=주석 검색 popup.rename=이름 바꾸기 popup.search="%s" 검색 popup.search_global="%s" 전역 검색 #popup.remove=Remove #popup.add_files=Add files #popup.add_scripts=Add scripts #popup.new_script=New script #popup.json_prettify=JSON Prettify #popup.export=Export #popup.usage_dialog_plus=Usage Tree Search #popup.copy_as=Copy as... #popup.copy_as_hex=Copy as HEX #popup.copy_as_string=Copy as String #popup.copy_offset=Copy offset #script.run=Run #script.save=Save #script.auto_complete=Auto Complete #script.check=Check #script.format=Reformat #script.log=Show log #encoding_dialog.title=Encoding #encoding_dialog.message=Select encoding: exclude_dialog.title=패키지 선택기 exclude_dialog.select_all=모두 선택 exclude_dialog.deselect=선택 해제 exclude_dialog.invert=반전 confirm.save_as_title=다른 이름으로 저장 확인 confirm.save_as_message=%s이(가) 이미 있습니다.\n바꾸시겠습니까? confirm.not_saved_title=프로젝트 저장 confirm.not_saved_message=계속하기 전에 현재 프로젝트를 저장 하시겠습니까? #confirm.remember=Remember my decision certificate.cert_type=유형 certificate.serialSigVer=버전 certificate.serialNumber=시리얼 번호 certificate.cert_subject=소유자 certificate.serialValidFrom=유효 시작 시각 certificate.serialValidUntil=유효 종료 시각 certificate.serialPubKeyType=공개키 타입 certificate.serialPubKeyExponent=지수 certificate.serialPubKeyModulus=모듈러스 certificate.serialPubKeyModulusSize=모듈러스 크기 (비트) certificate.serialSigType=서명 유형 certificate.serialSigOID=서명 OID certificate.serialMD5=MD5 지문 certificate.serialSHA1=SHA-1 지문 certificate.serialSHA256=SHA-256 지문 certificate.serialPubKeyY=Y apkSignature.signer=서명자 #apkSignature.loading=Loading signature... apkSignature.verificationSuccess=서명 검증 성공 apkSignature.verificationFailed=서명 검증 실패 apkSignature.signatureSuccess=유효한 APK 서명 v%d을(를) 찾았습니다. apkSignature.signatureFailed=유효하지 않은 APK 서명 v%d을(를) 찾았습니다. apkSignature.errors=오류 apkSignature.warnings=경고 apkSignature.exception=APK 검증 실패 apkSignature.unprotectedEntry=APK 서명 v1에 의해 보호되지 않는 파일. 이러한 항목에 대한 승인되지 않은 수정은 APK 서명 v2 이상에서만 감지할 수 있습니다. issues_panel.label=이슈: issues_panel.errors=오류 %d개 issues_panel.warnings=경고 %d개 issues_panel.tooltip=로그 뷰어에서 열기 debugger.process_selector=디버깅 할 프로세스 선택 debugger.step_into=한 단계씩 코드 실행 (F7) debugger.step_over=프로시저 단위 실행 (F8) debugger.step_out=프로시저 나가기 (Shift + F8) debugger.run=실행 (F9) debugger.stop=디버거 중지 후 앱 종료 debugger.pause=일시 중지 debugger.rerun=재실행 debugger.cfm_dialog_title=디버깅 중 종료 debugger.cfm_dialog_msg=디버거를 종료 하시겠습니까? debugger.popup_set_value=값 설정 debugger.popup_change_to_zero=0으로 변경 debugger.popup_change_to_one=1로 변경 debugger.popup_copy_value=값 복사 #logcat.pause=Pause Logcat #logcat.start=Resume Logcat #logcat.clear=Clear Logcat #logcat.error_fail_start=Failed to start logcat #logcat.process=Process #logcat.level=Level #logcat.default=Default #logcat.verbose=Verbose #logcat.debug=Debug #logcat.info=Info #logcat.warn=Warn #logcat.error=Error #logcat.fatal=Fatal #logcat.silent=Silent #logcat.logcat=Logcat #logcat.select_attached=Select Attached #logcat.select_all=Select All #logcat.unselect_all=Unselect All set_value_dialog.label_value=값 set_value_dialog.btn_set=값 설정 set_value_dialog.title=값 설정 set_value_dialog.neg_msg=값 설정에 실패했습니다. set_value_dialog.sel_type=값을 설정할 유형을 선택하십시오. adb_dialog.addr=ADB 주소 adb_dialog.port=ADB 포트 adb_dialog.path=ADB 경로 adb_dialog.launch_app=앱 실행 adb_dialog.start_server=ADB 서버 시작 adb_dialog.refresh=새로고침 adb_dialog.tip_devices=기기 %d 대 adb_dialog.device_node=기기 adb_dialog.missing_path=ADB 서버를 시작하려면 ADB 경로를 제공해야합니다. adb_dialog.waiting=ADB 서버 연결 대기 중 ... adb_dialog.connecting=ADB 서버에 연결 중입니다. 주소: %s:%s... adb_dialog.connect_okay=ADB 서버가 연결되었습니다. 주소: %s:%s adb_dialog.connect_fail=ADB 서버에 연결하지 못했습니다. adb_dialog.disconnected=ADB 서버 연결이 끊어졌습니다. adb_dialog.start_okay=ADB 서버가 포트 %s 에서 시작되었습니다. adb_dialog.start_fail=포트 %s 에서 ADB 서버를 시작하지 못했습니다. adb_dialog.forward_fail=알수 없는 이유로 전달하지 못했습니다. adb_dialog.being_debugged_msg=이 프로세스는 디버깅중인 것 같습니다. 계속 진행합니까? adb_dialog.unknown_android_ver=Android 릴리스 버전을 가져 오지 못했습니다. Android 8을 기본값으로 사용 하시겠습니까? adb_dialog.being_debugged_title=이미 다른 세션에 의해 디버깅중인 프로세스입니다. adb_dialog.init_dbg_fail=디버거를 초기화하는데 실패했습니다. adb_dialog.msg_read_mani_fail=AndroidManifest.xml을 디코딩하는데 실패했습니다. adb_dialog.no_devices=앱을 실행할 기기를 찾을 수 없습니다. adb_dialog.restart_while_debugging_title=디버깅중 다시 시작 adb_dialog.restart_while_debugging_msg=앱을 디버깅하고 있습니다. 세션을 다시 시작 하시겠습니까? adb_dialog.starting_debugger=디버거 시작 중 ... #action.variant=%s (variant) #action_category.menu_toolbar=Menu / Toolbar #action_category.hex_viewer=View / Hex Viewer #action_category.code_area=Code Area #action_category.plugin_script=Plugin Script #hex_viewer.show_inspector=Show Inspector #hex_viewer.change_encoding=Change Encoding #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find #graph_viewer.long_names=Show full names #graph_viewer.overrides=Show overrides #graph_viewer.callee_depth=Down depth #graph_viewer.caller_depth=Up depth #graph_viewer.default_error=Failed to view graph #graph_viewer.file_not_found_error=Failed to load graph file #graph_viewer.image_too_large=Failed to render graph: graph too large #graph_viewer.image_too_small=Failed to render graph: graph too small #graph_viewer.file_failure=Error in File Operation #graph_viewer.save_graph=Save graph #graph_viewer.default_title=Graph Viewer #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph #graph_viewer.cfg.title=Control Flow Graph ================================================ FILE: jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties ================================================ language.name=Português menu.file=Arquivo menu.view=Ver menu.recent_projects=Projetos recentes menu.no_recent_projects=Nenhum projeto recente menu.preferences=Preferências menu.sync=Sincronizar com editor menu.flatten=Mostrar pacotes achatados #menu.enable_preview_tab=Enable Preview Tab menu.heapUsageBar=Mostrar uso de memória menu.alwaysSelectOpened=Sempre selecionar arquivo/classe aberta #menu.dock_log=Dock Log Viewer #menu.dock_quick_tabs=Show Quick Tabs menu.navigation=Navegação menu.text_search=Buscar por texto menu.class_search=Buscar por classe menu.comment_search=Busca por comentário #menu.go_to_main_activity=Go to main Activity #menu.go_to_application=Go to Application #menu.go_to_android_manifest=Go to AndroidManifest.xml menu.tools=Ferramentas #menu.plugins=Plugins #menu.decompile_all=Decompile all classes #menu.reset_cache=Reset code cache menu.deobfuscation=Desofuscar menu.log=Visualizador de log #menu.create_desktop_entry=Create Desktop Entry menu.help=Ajuda menu.about=Sobre #menu.quark=Quark Engine menu.update_label=Nova versão %s disponível! #menu.hex_viewer=Hex Viewer file.open_action=Abrir arquivos... file.add_files_action=Adicionar arquivos file.open_title=Abrir arquivo file.open_project=Abrir projeto file.new_project=Novo projeto file.save_project=Salvar projeto file.save_project_as=Salvar projeto como... file.reload=Recarregar arquivos file.live_reload=Recarregar em tempo real file.live_reload_desc=Recarregar arquivos automaticamente ao serem alterados #file.open_mappings=Open mappings... #file.save_mappings=Save mappings #file.save_mappings_as=Save mappings as... #file.close_mappings=Close mappings file.save_all=Salvar tudo #file.save=Save #file.export=Export project file.save_all_msg=Selecionar diretório para salvar arquivos descompilados file.exit=Sair #file.export_node=Export file start_page.title=Página inicial start_page.start=Começar start_page.recent=Projetos recentes #start_page.list.delete_recent_project=Delete project #start_page.list.delete_recent_project.tooltip=Delete project from recent list #tree.inputs_title=Inputs #tree.input_files=Files #tree.input_scripts=Scripts tree.sources_title=Código fonte tree.resources_title=Recursos tree.loading=Carregando... #tree.pinned_tabs=Pinned Tabs #tree.open_tabs=Open Tabs #tree.bookmarked_tabs=Bookmarked Tabs progress.load=Carregando #progress.save_mappings=Saving mappings progress.decompile=Descompilando progress.canceling=Cancelando error_dialog.title=Erro #error_dialog.not_found=%s not found #error_dialog.not_found_file=File not found at path:\n%s #error_dialog.path_is_directory=The specified path is a directory:\n%s #error_dialog.cannot_read=Cannot read file:\n%s #error_dialog.open_failed=Failed to open file:\n%s #error_dialog.invalid_path_format=Invalid file path:\n%s #error_dialog.desktop_unsupported=Desktop is not supported in this environment. search.previous=Anterior search.next=Próximo search.mark_all=Marcar todos search.regex=Regex search.match_case=Match Case search.whole_word=Palavra inteira search.find=Encontrar #search.results=%s%d results #search.match_of=Match %d of %d #search.match_found=Match found #search.match_not_found=No Matches found #search.single_match=Single match found #search.find_type_text=Find by text #search.find_type_hex=Find by hex tabs.copy_class_name=Copiar nome tabs.close=Fechar tabs.closeOthers=Fechar outros #tabs.unpin=Unpin #tabs.unpin_all=Unpin All #tabs.bookmark=Bookmark #tabs.unbookmark=Unbookmark #tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=Fechar todos tabs.closeAllRight=Feche tudo à direita #tabs.closeAllLeft=Close All Left tabs.code=Código tabs.smali=Smali tabs.smali_bytecode=Smali+Bytecode nav.back=Voltar nav.forward=Avançar message.taskTimeout=Tarefa excedeu limite de tempo de %d ms. message.userCancelTask=Tarefa foi cancelada pelo usuário. message.memoryLow=Jadx está rodando com pouca memória. Por favor reinicie com um limite de memória heap maior. message.taskError=Tarefa falhou com erro (cheque o log para detalhes). message.errorTitle=Erro message.load_errors=Carregamento falhou.\nNúmero de erros: %d\nClique em ok para abrir o visualizador de log. message.no_classes=Nenhuma classe carregada, nada para ser descompilado! #message.enter_valid_path=Enter valid path to save! message.saveIncomplete=Operação não foi completa.
%s
%d classes ou recursos não foram salvos! message.indexIncomplete=Indexação de algumas classes foram ignoradas.
%s
%d classes não foram indexadas não vão aparecer nos resultados de busca! message.indexingClassesSkipped=Jadx está rodando com pouca memória. Por conta disso, %d classes não foram indexadas.
Se você deseja que todas classes sejam indexadas, reinicie com um limite de memória heap maior. #message.enter_new_name=Enter new name #message.could_not_rename=Can't rename the file #message.confirm_remove_script=Do you really want to remove script? #message.desktop_entry_creation_error=Failed to create desktop entry (check log for details). #message.desktop_entry_creation_success=Desktop entry created successfully! #message.success_title=Success #message.unable_preview_font=Unable preview font heapUsage.text=Uso de memória do JADX: %.2f GB of %.2f GB (%.2f GB pico) common_dialog.ok=Ok common_dialog.cancel=Cancelar common_dialog.add=Adicionar common_dialog.update=Atualizar common_dialog.remove=Remover #common_dialog.reset=Reset file_dialog.supported_files=Arquivos suportados file_dialog.load_dir_title=Carregar diretório file_dialog.load_dir_confirm=Carregar todos arquivos do diretório? search_dialog.open=Abrir search_dialog.cancel=Cancelar search_dialog.open_by_name=Buscar por texto: search_dialog.search_button=Buscar search_dialog.search_history=Buscar histórico search_dialog.auto_search=Busca automática search_dialog.search_in=Buscar definições de: search_dialog.class=Classe search_dialog.method=Método search_dialog.field=Propriedade search_dialog.code=Código search_dialog.ignorecase=Não diferencia maiúsculas de minúsculas search_dialog.load_more=Carregar mais search_dialog.load_all=Carregar todas search_dialog.stop=Parar search_dialog.results_incomplete=Encontradas %d+ search_dialog.results_complete=Encontradas %d (completos) #search_dialog.resources_load_errors=Load errors: %d #search_dialog.resources_skip_by_size=Skipped by size: %d #search_dialog.resources_check_logs=(click to check logs) search_dialog.col_node=Nó search_dialog.col_code=Código search_dialog.sort_results=Ordenar resultados search_dialog.regex=Expressão regular search_dialog.active_tab=Apenas abas ativas search_dialog.comments=Comentários search_dialog.resource=Recursos search_dialog.keep_open=Manter aberto search_dialog.tip_searching=Buscando #search_dialog.limit_package=Limit to package: #search_dialog.res_text=Text #search_dialog.res_binary=Binary #search_dialog.package_not_found=No matching package found search_dialog.copy=skopiuj wszystko usage_dialog.title=Busca por utilização usage_dialog.label=Usado por: #usage_dialog_plus.title=Usage tree search #usage_dialog_plus.jump_to=Jump to current location #usage_dialog_plus.copy_path=Copy usage tree path #usage_dialog_plus.search_complete=Search completed #usage_dialog_plus.code_view=Code View #usage_dialog_plus.select_node=Select Node #usage_dialog_plus.code_for=Code for %s #usage_dialog_plus.expand_usages=Expand Data comment_dialog.title.add=Adicionar comentário ao código comment_dialog.title.update=Atualizar comentário do código comment_dialog.label=Comentário: #comment_dialog.style=Style: comment_dialog.usage=Use Shift + Enter para pular uma linha #rename_dialog.class_help=Enter full name to move class to another package. Start with '.' to move to default (empty) package #export_dialog.title=Export #export_dialog.save_path=Save path: #export_dialog.browse=Browse #export_dialog.export_options=Export options #export_dialog.export_gradle=Export as a Gradle project #export_dialog.export_gradle_type=Gradle template: log_viewer.title=Visualizador de log log_viewer.log_level=Nível do log: #log_viewer.mode=Mode: #log_viewer.modes=All|All scripts|Current script #log_viewer.hide=Hide #log_viewer.dock=Dock #log_viewer.undock=Undock #log_viewer.clear=Clear about_dialog.title=Sobre o JADX preferences.title=Preferências preferences.deobfuscation=Desofuscar preferences.appearance=Aparência #preferences.shortcuts=Shortcuts #preferences.select_shortcuts=Select a specific shortcuts group preferences.decompile=Descompilação preferences.plugins=Plugins preferences.project=Projeto preferences.other=Outro preferences.language=Idioma preferences.lineNumbersMode=Modo do contador de linhas do editor preferences.jumpOnDoubleClick=Ativar salto no duplo clique #preferences.useAlternativeFileDialog=Use alternative file dialog preferences.check_for_updates=Verificar por atualizações ao inicializar preferences.useDx=Usar dx/d8 para converter bytecode Java preferences.decompilationMode=Modo de descompilação preferences.codeCacheMode=Modo de cachê do código #preferences.codeCacheMode.memory=Memory #preferences.codeCacheMode.memory.desc=Everything in memory: fast search, slow reopen, high memory usage #preferences.codeCacheMode.diskWithCache=Disk with cache #preferences.codeCacheMode.diskWithCache.desc=Code saved on disk with in memory cache: medium search, fast reopen, medium memory usage #preferences.codeCacheMode.disk=Disk #preferences.codeCacheMode.disk.desc=Everything on disk: slow search, fast reopen, low memory usage #preferences.usageCacheMode=Usage data cache mode preferences.showInconsistentCode=Mostrar código inconsistent preferences.escapeUnicode=Escapar unicode preferences.replaceConsts=Substituir constantes preferences.respectBytecodeAccessModifiers=Respeitar modificadores de acesso do bytecode preferences.useImports=Utilizar declaração de imports preferences.useDebugInfo=Utilizar informação de depuração preferences.inlineAnonymous=Classes anônimas de uma linha preferences.inlineMethods=Métodos de uma linha #preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas #preferences.moveInnerClasses=Move inner classes into parent preferences.extractFinally=Extrair blocos finally #preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=Sistema de arquivo diferencia maiúsculas de minúsculas preferences.skipResourcesDecode=Não decodificar recursos #preferences.skipSourcesDecode=Don't decompile source code preferences.useKotlinMethodsForVarNames=Usar métodos do kotlin para renomear variáveis preferences.commentsLevel=Nível de comentários do código #preferences.saveOption=Auto-save settings preferences.threads=Número de threads no processo preferences.excludedPackages=Pacotes ignorados preferences.excludedPackages.tooltip=Lista espaço de pacotes que não vão ser descompilados ou indexados (economiza RAM) preferences.excludedPackages.button=Editar preferences.excludedPackages.editDialog=Lista espaço de pacotes que não vão ser descompilados ou indexados (economiza RAM)
ex: android.support preferences.cfg=Gera gráficos de métodos CFG no formato de pontos ('dot') preferences.raw_cfg=Gera gráficos CFG no formato RAW #preferences.xposed_codegen_language=Xposed code generation language #preferences.update_channel=Jadx update channel #preferences.disable_tooltip_on_hover=Disable tooltip on hover #preferences.integerFormat=Integer format #preferences.typeUpdatesCountLimit=Update type limit count #preferences.ui_zoom=UI Zoom factor #preferences.apply_ui_zoom_to_fonts=Apply UI zoom to fonts #preferences.ui_font=UI font preferences.code_font=Fonte do editor #preferences.smali_font=Monospaced font (Smali/Hex) preferences.laf_theme=Tema #preferences.dynamic_editor_theme=Use UI theme colors preferences.theme=Tema do editor preferences.start_jobs=Inicializar descompilação automaticamente em segundo-plano preferences.select_font=Alterar preferences.deobfuscation_on=Ativar desofuscação #preferences.generated_renames_mapping_file_mode=Map file handle mode preferences.deobfuscation_min_len=Tamanho mínimo do nome preferences.deobfuscation_max_len=Tamanho máximo do nome preferences.deobfuscation_res_name_source=Melhora nome da fonte dos recursos #preferences.deobfuscation_res_use_headers=Use headers for detect resource extensions #preferences.deobfuscation_whitelist=Exclude packages and classes from deobfuscation #preferences.deobfuscation_whitelist.editDialog=Whitelist for deobfuscation #preferences.deobfuscation_whitelist.tooltip=List of ':' separated packages (suffix '.*') and class names that will not be deobfuscated preferences.save=Salvar preferences.cancel=Cancelar preferences.reset=Redefinir preferences.reset_message=Redefinir configurações para o padrão? preferences.reset_title=Redefinir configurações preferences.copy=Copy to clipboard preferences.copy_message=Todas configurações foram copiadas para área de transferência preferences.rename=Renomear identificadores preferences.rename_case=Corrigir problemas de capitalização (case sensitivity) preferences.rename_valid=Deixá-las válidas preferences.rename_printable=Deixá-las imprimíveis (printable) preferences.rename_use_source_name_as_class_name_alias=Utilizar nome do arquivo como apelido da classe #preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number #preferences.search_results_per_page=Results per page (0 - no limit) preferences.res_file_ext=Extensões de arquivos (ex: .xml|.html), * significa todas preferences.res_skip_file=Pular arquivos excedidos preferences.tab_dnd_appearance=Dragging tab appearance #preferences.plugins.manage=Manage plugins #preferences.plugins.install=Install plugin #preferences.plugins.install_btn=Install #preferences.plugins.uninstall_btn=Uninstall #preferences.plugins.disable_btn=Disable #preferences.plugins.enable_btn=Enable #preferences.plugins.location_id_label=Location id: #preferences.plugins.plugin_jar=Select Plugin jar #preferences.plugins.plugin_jar_label=or #preferences.plugins.update_all=Update All #preferences.plugins.details=Plugin details #preferences.plugins.task.installing=Installing plugin #preferences.plugins.task.uninstalling=Uninstalling plugin #preferences.plugins.task.updating=Updating plugins #preferences.plugins.task.downloading_list=Downloading plugins list #preferences.plugins.task.status=Changing plugin status #preferences.cache=Cache #preferences.cache.location=Cache location #preferences.cache.location_default=App cache system directory #preferences.cache.location_local=Same directory as project file #preferences.cache.location_custom=Custom location: #preferences.cache.change_notice=* for exists caches change will be applied after cache remove or manual reset #preferences.cache.table.title=Caches list #preferences.cache.table.project=Cache for project #preferences.cache.table.size=Disk usage #preferences.cache.btn.usage=Calculate usage #preferences.cache.btn.delete_selected=Delete Selected #preferences.cache.btn.delete_all=Delete All #preferences.cache.task.usage=Calculating cache size #preferences.cache.task.delete=Deleting caches msg.open_file=Abra um arquivo msg.saving_sources=Salvando recursos msg.language_changed_title=Idioma alterado msg.language_changed=Novo idioma será mostrado na próxima inicialização. #msg.warning_title=Warning #msg.common_mouse_shortcut=This is a commonly used key, are you sure you would like to bind it to an action? #msg.duplicate_shortcut=The shortcut %s is already set in action "%s" from category "%s", continue ? msg.cmd_select_class_error=Falha ao selecionar classe\n%s\nA classe não existe. msg.cant_add_comment=Não é possível adicionar comentários aqui #msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars? #msg.non_displayable_chars.title=Undisplayed Strings #methods_dialog.title=Select methods popup.bytecode_col=Mostrar Dalvik Bytecode popup.line_wrap=Quebra de linha popup.undo=Desfazer popup.redo=Refazer popup.cut=Cortar popup.copy=Copiar popup.paste=Colar popup.delete=Apagar popup.select_all=Selecionar todos popup.frida=Copiar como snippet frida popup.xposed=Copiar como snippet xposed popup.find_usage=Buscar uso popup.go_to_declaration=Ir para declaração popup.exclude=Ignorar popup.exclude_packages=Pacotes ignorados #popup.convert_number=Add conversion as comment #popup.view_call_graph=View call graph #popup.view_call_graph_description=Show call chains to this function #popup.view_class_graph=View inheritance graph #popup.view_class_graph_description=Show inheritance tree for this class #popup.view_class_method_graph=View methods graph #popup.view_class_method_graph_description=Show all methods for this class #popup.cfg_submenu=View control flow graph #popup.view_cfg=Regular #popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) #popup.view_raw_cfg=Raw #popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) #popup.view_region_cfg=Region #popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=Comentar #popup.update_comment=Update comment popup.search_comment=Buscar comentários popup.rename=Renomear popup.search=Buscar "%s" popup.search_global=Busca global "%s" #popup.remove=Remove #popup.add_files=Add files #popup.add_scripts=Add scripts #popup.new_script=New script #popup.json_prettify=JSON Prettify #popup.export=Export #popup.usage_dialog_plus=Usage Tree Search #popup.copy_as=Copy as... #popup.copy_as_hex=Copy as HEX #popup.copy_as_string=Copy as String #popup.copy_offset=Copy offset #script.run=Run #script.save=Save #script.auto_complete=Auto Complete #script.check=Check #script.format=Reformat #script.log=Show log #encoding_dialog.title=Encoding #encoding_dialog.message=Select encoding: exclude_dialog.title=Selecionar pacote exclude_dialog.select_all=Selecionar tudo exclude_dialog.deselect=Remover seleção exclude_dialog.invert=Inverter confirm.save_as_title=Confirmar operação confirm.save_as_message=%s Já existe.\nVocê deseja substituir? confirm.not_saved_title=Salvar projeto confirm.not_saved_message=Salvar projeto atual antes de continuar? #confirm.remember=Remember my decision certificate.cert_type=Tipo certificate.serialSigVer=Versão certificate.serialNumber=Número de série (serial) certificate.cert_subject=Remetente certificate.serialValidFrom=Válido de certificate.serialValidUntil=Válido até certificate.serialPubKeyType=Tipo de chave pública certificate.serialPubKeyExponent=Expoente certificate.serialPubKeyModulus=Módulo certificate.serialPubKeyModulusSize=Tamanho do módulo (bits) certificate.serialSigType=Tipo de assinatura certificate.serialSigOID=Assinatura OID certificate.serialMD5=Assinatura digital MD5 certificate.serialSHA1=Assinatura digital SHA-1 certificate.serialSHA256=Assinatura digital SHA-256 certificate.serialPubKeyY=Y apkSignature.signer=Assinador #apkSignature.loading=Loading signature... apkSignature.verificationSuccess=Verificação de assinatura bem-sucedida apkSignature.verificationFailed=Verificação de assinatura falhou apkSignature.signatureSuccess=Assinatura válida de apk v%d encontrada apkSignature.signatureFailed=Assinatura inválida de apk v%d encontrada apkSignature.errors=Erros apkSignature.warnings=Avisos apkSignature.exception=Verificação do APK falhou apkSignature.unprotectedEntry=Arquivos que não são protegidos pela assinatura v1. Modificações não autorizadas a essas entradas só podem ser detectadas por uma assinatura de versão 2 ou maior. issues_panel.label=Problemas: issues_panel.errors=%d error issues_panel.warnings=%d avisos issues_panel.tooltip=Abrir no visualizador de log debugger.process_selector=Selecionar um processo para depurar debugger.step_into=Passar para próxima chamada de função (F7) debugger.step_over=Passar sobre próxima chamada de função (F8) debugger.step_out=Sair da função atual (Shift + F8) debugger.run=Executar (F9) debugger.stop=Parar depurador e matar o aplicativo debugger.pause=Pausar debugger.rerun=Reexecutar debugger.cfm_dialog_title=Sair enquanto depura debugger.cfm_dialog_msg=Você tem certeza de que deseja terminar o depurador? debugger.popup_set_value=Definir valor debugger.popup_change_to_zero=Alterar para 0 debugger.popup_change_to_one=Alterar para 1 debugger.popup_copy_value=Copiar valor #logcat.pause=Pause Logcat #logcat.start=Resume Logcat #logcat.clear=Clear Logcat #logcat.error_fail_start=Failed to start logcat #logcat.process=Process #logcat.level=Level #logcat.default=Default #logcat.verbose=Verbose #logcat.debug=Debug #logcat.info=Info #logcat.warn=Warn #logcat.error=Error #logcat.fatal=Fatal #logcat.silent=Silent #logcat.logcat=Logcat #logcat.select_attached=Select Attached #logcat.select_all=Select All #logcat.unselect_all=Unselect All set_value_dialog.label_value=Valor set_value_dialog.btn_set=Definir Valor set_value_dialog.title=Definir Valor set_value_dialog.neg_msg=Falha ao definir valor. set_value_dialog.sel_type=Selecionar um tipo para definir valor. adb_dialog.addr=Endereço do ADB adb_dialog.port=Porta do ADB adb_dialog.path=Caminho do ADB adb_dialog.launch_app=Iniciar Aplicativo adb_dialog.start_server=Iniciar servidor ADB adb_dialog.refresh=Recarregar adb_dialog.tip_devices=%d dispositivos adb_dialog.device_node=dispositivo adb_dialog.missing_path=Caminho do ADB deve ser especificado para iniciar o servidor. adb_dialog.waiting=Aguardando para conectar no servidor ADB... adb_dialog.connecting=Conectando ao servidor ADB, endereço: %s:%s... adb_dialog.connect_okay=Servidor ADB conectado, endereço: %s:%s adb_dialog.connect_fail=Falha ao se conectar ao servidor ADB. adb_dialog.disconnected=Servidor ADB desconectado. adb_dialog.start_okay=Servidor ADB inicializado na porta: %s. adb_dialog.start_fail=Falha ao iniciar servidor ADB na porta: %s! adb_dialog.forward_fail=Falha ao encaminhar por um motivo não especificado. adb_dialog.being_debugged_msg=Esse processo parece estar sendo depurado, deseja continuar? adb_dialog.unknown_android_ver=Falha ao obter versão do Android, deseja usar o Android 8 como padrão? adb_dialog.being_debugged_title=Está sendo depurado por outro processo. adb_dialog.init_dbg_fail=Falha ao iniciar depurador. adb_dialog.msg_read_mani_fail=Falha ao decodificar AndroidManifest.xml adb_dialog.no_devices=Não foi possível encontrar nenhum dispositivo para iniciar o aplicativo. adb_dialog.restart_while_debugging_title=Reiniciar enquanto depura adb_dialog.restart_while_debugging_msg=Você está depurando um aplicativo, você tem certeza que deseja reiniciar a sessão? adb_dialog.starting_debugger=Iniciando depurador... #action.variant=%s (variant) #action_category.menu_toolbar=Menu / Toolbar #action_category.hex_viewer=View / Hex Viewer #action_category.code_area=Code Area #action_category.plugin_script=Plugin Script #hex_viewer.show_inspector=Show Inspector #hex_viewer.change_encoding=Change Encoding #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find #graph_viewer.long_names=Show full names #graph_viewer.overrides=Show overrides #graph_viewer.callee_depth=Down depth #graph_viewer.caller_depth=Up depth #graph_viewer.default_error=Failed to view graph #graph_viewer.file_not_found_error=Failed to load graph file #graph_viewer.image_too_large=Failed to render graph: graph too large #graph_viewer.image_too_small=Failed to render graph: graph too small #graph_viewer.file_failure=Error in File Operation #graph_viewer.save_graph=Save graph #graph_viewer.default_title=Graph Viewer #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph #graph_viewer.cfg.title=Control Flow Graph ================================================ FILE: jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties ================================================ language.name=Русский menu.file=Файл menu.view=Вид menu.recent_projects=Недавние проекты menu.no_recent_projects=Нет недавно открытых проектов menu.preferences=Параметры menu.sync=Синхр. файлы с редактором menu.flatten=Плоская структура пакетов #menu.enable_preview_tab=Enable Preview Tab menu.heapUsageBar=Использование ОЗУ menu.alwaysSelectOpened=Выбирать открытый файл/класс menu.dock_log=Просмотр логов в панели #menu.dock_quick_tabs=Show Quick Tabs menu.navigation=Навигация menu.text_search=Поиск строк menu.class_search=Поиск классов menu.comment_search=Поиск комментариев menu.go_to_main_activity=Найти главное Activity #menu.go_to_application=Go to Application #menu.go_to_android_manifest=Go to AndroidManifest.xml menu.tools=Инструменты menu.plugins=Плагины menu.decompile_all=Декомпилировать все menu.reset_cache=Сбросить кэш menu.deobfuscation=Деобфускация menu.log=Просмотр логов #menu.create_desktop_entry=Create Desktop Entry menu.help=Помощь menu.about=О программе #menu.quark=Quark Engine menu.update_label=Версия %s уже доступна! #menu.hex_viewer=Hex Viewer file.open_action=Открыть файлы... file.add_files_action=Добавить файл file.open_title=Открыть файл file.open_project=Открыть проект file.new_project=Новый проект file.save_project=Сохранить проект file.save_project_as=Сохранить проект как... file.reload=Пересканировать файлы file.live_reload=Автосканирование file.live_reload_desc=Автоматически перезагружать файлы при внешних изменениях file.open_mappings=Открыть маппинги... file.save_mappings=Сохранить маппинги file.save_mappings_as=Сохранить маппинги как... file.close_mappings=Закрыть маппинги file.save_all=Сохранить все file.save=Сохранить #file.export=Export project file.save_all_msg=Выбрать папку для декомпилированных проектов file.exit=Выход #file.export_node=Export file start_page.title=Начальная страница start_page.start=Начать start_page.recent=Недавние проекты #start_page.list.delete_recent_project=Delete project #start_page.list.delete_recent_project.tooltip=Delete project from recent list tree.inputs_title=Входные файлы tree.input_files=Файлы tree.input_scripts=Скрипты tree.sources_title=Исходный код tree.resources_title=Ресурсы tree.loading=Загрузка... #tree.pinned_tabs=Pinned Tabs #tree.open_tabs=Open Tabs #tree.bookmarked_tabs=Bookmarked Tabs progress.load=Загрузка progress.save_mappings=Сохранить маппинги progress.decompile=Декомпиляция progress.canceling=Прерывание error_dialog.title=Ошибка error_dialog.not_found= %s не найден #error_dialog.not_found_file=File not found at path:\n%s #error_dialog.path_is_directory=The specified path is a directory:\n%s #error_dialog.cannot_read=Cannot read file:\n%s #error_dialog.open_failed=Failed to open file:\n%s #error_dialog.invalid_path_format=Invalid file path:\n%s #error_dialog.desktop_unsupported=Desktop is not supported in this environment. search.previous=Назад search.next=Вперед search.mark_all=Выбрать все search.regex=Регулярные выражения search.match_case=Учитывать регистр search.whole_word=Поиск по словам search.find=Найти search.results=%s%d результатов #search.match_of=Match %d of %d #search.match_found=Match found #search.match_not_found=No Matches found #search.single_match=Single match found #search.find_type_text=Find by text #search.find_type_hex=Find by hex tabs.copy_class_name=Копировать имя tabs.close=Закрыть tabs.closeOthers=Закрыть другие #tabs.unpin=Unpin #tabs.unpin_all=Unpin All #tabs.bookmark=Bookmark #tabs.unbookmark=Unbookmark #tabs.unbookmark_all=Unbookmark All #tabs.pin=Pin tabs.closeAll=Закрыть все tabs.closeAllRight=Закройте все справа #tabs.closeAllLeft=Close All Left tabs.code=Код tabs.smali=Smali tabs.smali_bytecode=Smali+Bytecode nav.back=Назад nav.forward=Вперед message.taskTimeout=Задача отменена по таймауту в %d мс. message.userCancelTask=Задача прервана пользователем. message.memoryLow=Jadx запущен с малым количеством доступной оперативной памяти. Перезапустите увеличив Heap Size message.taskError=Выполнение задачи закончено с ошибкой (смотрите лог для подробностей). message.errorTitle=Ошибка message.load_errors=Ошибка прогрузки.\nКоличество ошибок: %d\nНажмите OK, чтобы открыть лог. message.no_classes=Классы не найдены, нечего декомпилировать! #message.enter_valid_path=Enter valid path to save! message.saveIncomplete=Сохранение не завершено.
%s
%d классов или ресурсов не сохранено! message.indexIncomplete=Индексирование некоторых классов пропущено.
%s
%d классов не индексировано, и не будет отображаться в результатах поиска! message.indexingClassesSkipped=JaDX запущен с малым количеством ОЗУ. %d классов не индексировано.
Если вы хотите их индексировать, перезапустите JaDX с большим Heap Size. #message.enter_new_name=Enter new name #message.could_not_rename=Can't rename the file #message.confirm_remove_script=Do you really want to remove script? #message.desktop_entry_creation_error=Failed to create desktop entry (check log for details). #message.desktop_entry_creation_success=Desktop entry created successfully! #message.success_title=Success #message.unable_preview_font=Unable preview font heapUsage.text=JADX использует: %.2f ГБ из %.2f ГБ (%.2f GB пикпик) common_dialog.ok=Ok common_dialog.cancel=Отмена common_dialog.add=Добавить common_dialog.update=Обновить common_dialog.remove=Убрать common_dialog.reset=Сброс file_dialog.supported_files=Поддерж. файлы file_dialog.load_dir_title=Импортировать папку file_dialog.load_dir_confirm=Импортировать все файлы из директории? search_dialog.open=Открыть search_dialog.cancel=Отмена search_dialog.open_by_name=Поиск по тексту: search_dialog.search_button=Поиск search_dialog.search_history=История поиска search_dialog.auto_search=Автопоиск search_dialog.search_in=Поиск вхождений: search_dialog.class=Класс search_dialog.method=Метод search_dialog.field=Поле search_dialog.code=Код search_dialog.ignorecase=Игнорировать регистр search_dialog.load_more=Загрузить еще search_dialog.load_all=Загрузить все search_dialog.stop=Стоп search_dialog.results_incomplete=Найдено %d+ search_dialog.results_complete=Найдено %d (поиск завершен) search_dialog.resources_load_errors=Ошибки загрузки: %d search_dialog.resources_skip_by_size=Пропущено из-за размера: %d search_dialog.resources_check_logs=(нажмите для просмотра логов) search_dialog.col_node=Вхождения search_dialog.col_code=Код search_dialog.sort_results=Сортировка результатов search_dialog.regex=Регулярные выражения search_dialog.active_tab=Только активные вкладки search_dialog.comments=Комментарии search_dialog.resource=Ресурсы search_dialog.keep_open=Оставлять поиск открытым search_dialog.tip_searching=Поиск... #search_dialog.limit_package=Limit to package: #search_dialog.res_text=Text #search_dialog.res_binary=Binary #search_dialog.package_not_found=No matching package found search_dialog.copy=скопировать все usage_dialog.title=Поиск использований usage_dialog.label=Использования: #usage_dialog_plus.title=Usage tree search #usage_dialog_plus.jump_to=Jump to current location #usage_dialog_plus.copy_path=Copy usage tree path #usage_dialog_plus.search_complete=Search completed #usage_dialog_plus.code_view=Code View #usage_dialog_plus.select_node=Select Node #usage_dialog_plus.code_for=Code for %s #usage_dialog_plus.expand_usages=Expand Data comment_dialog.title.add=Добавить комментарий comment_dialog.title.update=Обновить comment_dialog.label=Комментарий: comment_dialog.style=Стиль: comment_dialog.usage=Используйте Shift + Enter для переноса строки rename_dialog.class_help=Введите полный путь к пакету, в который вы хотите переместить этот класс. Введите '.' для перемещения в пакет по умолчанию (пустой) пакет #export_dialog.title=Export #export_dialog.save_path=Save path: #export_dialog.browse=Browse #export_dialog.export_options=Export options #export_dialog.export_gradle=Export as a Gradle project #export_dialog.export_gradle_type=Gradle template: log_viewer.title=Просмотр логов log_viewer.log_level=Уровень лога: log_viewer.mode=Режим: log_viewer.modes=Все|Все скрипты|Текущий скрипт log_viewer.hide=Скрыть log_viewer.dock=Прикрепить к панели log_viewer.undock=Открепить от панели log_viewer.clear=Очистить about_dialog.title=О программе JADX preferences.title=Параметры preferences.deobfuscation=Деобфускация preferences.appearance=Внешний вид preferences.shortcuts=Горячие клавиши preferences.select_shortcuts=Выбрать горячие клавиши preferences.decompile=Декомпиляция preferences.plugins=Плагины preferences.project=Проект preferences.other=Прочее preferences.language=Язык preferences.lineNumbersMode=Тип переноса строк preferences.jumpOnDoubleClick=Переход по двойному клику preferences.useAlternativeFileDialog=Использовать альтернативный файлпикер preferences.check_for_updates=Проверять наличие новых версий preferences.useDx=DX/D8 для конвертации java байткода preferences.decompilationMode=Режим декомпиляции preferences.codeCacheMode=Кеширование кода #preferences.codeCacheMode.memory=Memory #preferences.codeCacheMode.memory.desc=Everything in memory: fast search, slow reopen, high memory usage #preferences.codeCacheMode.diskWithCache=Disk with cache #preferences.codeCacheMode.diskWithCache.desc=Code saved on disk with in memory cache: medium search, fast reopen, medium memory usage #preferences.codeCacheMode.disk=Disk #preferences.codeCacheMode.disk.desc=Everything on disk: slow search, fast reopen, low memory usage preferences.usageCacheMode=Использование кэша preferences.showInconsistentCode=Показывать некорректный код preferences.escapeUnicode=Кодирование unicode preferences.replaceConsts=Замена констант preferences.respectBytecodeAccessModifiers=Исходные модификаторы доступа preferences.useImports=Использовать импорты preferences.useDebugInfo=Отладочная информация preferences.inlineAnonymous=Объединять анонимные классы preferences.inlineMethods=Объединять методы preferences.inlineKotlinLambdas=Разрешить инлайнить Kotlin лямбды preferences.moveInnerClasses=Помещать вложенные классы внутрь родительских preferences.extractFinally=Вычленять finally блоки #preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=Учитывать регистр в файловой системе preferences.skipResourcesDecode=Не декодировать ресурсы #preferences.skipSourcesDecode=Don't decompile source code preferences.useKotlinMethodsForVarNames=Kotlin методы как имена полей preferences.commentsLevel=Уровень лога операций preferences.saveOption=Автосохранение настроек preferences.threads=Количество используемых потоков preferences.excludedPackages=Исключенные пакеты preferences.excludedPackages.tooltip=Список пакетов, которые не будут декомпилироваться и индексироваться (экономит ОЗУ) preferences.excludedPackages.button=Изменить preferences.excludedPackages.editDialog=Список пакетов, которые не будут декомпилироваться и индексироваться (экономит ОЗУ)
например: android.support
Разделитель - одинарный пробел preferences.cfg=Методы генерации графиков CFG (в "dot" формате) preferences.raw_cfg=Генерировать необработанные графики CFG preferences.xposed_codegen_language=Язык генерации Xposed хуков preferences.update_channel=Канал обновления Jadx #preferences.disable_tooltip_on_hover=Disable tooltip on hover preferences.integerFormat=Формат чисел #preferences.typeUpdatesCountLimit=Update type limit count #preferences.ui_zoom=UI Zoom factor #preferences.apply_ui_zoom_to_fonts=Apply UI zoom to fonts #preferences.ui_font=UI font preferences.code_font=Шрифт редактора Java preferences.smali_font=Шрифт smali/HEX редактора preferences.laf_theme=Тема приложения #preferences.dynamic_editor_theme=Use UI theme colors preferences.theme=Тема редактора preferences.start_jobs=Автоматическая декомпиляция preferences.select_font=Изменить preferences.deobfuscation_on=Включить деобфускацию preferences.generated_renames_mapping_file_mode=Режим обработки маппингов preferences.deobfuscation_min_len=Минимальная длина имени preferences.deobfuscation_max_len=Максимальная длина имени preferences.deobfuscation_res_name_source=Расшифровка имен ресурсов #preferences.deobfuscation_res_use_headers=Use headers for detect resource extensions preferences.deobfuscation_whitelist=Исключить пакеты и классы из деобфускации preferences.deobfuscation_whitelist.editDialog=Белый список деобфускации preferences.deobfuscation_whitelist.tooltip=Разделяйте пакеты через ':' (суффикс '.*') и имя класса которое не будет деобфусцировано preferences.save=Сохранить preferences.cancel=Отмена preferences.reset=Сброс preferences.reset_message=Сбросить настройки на значения по умолчанию? preferences.reset_title=Сбросить настройки preferences.copy=Скопировать в буфер обмена preferences.copy_message=Все настройки скопированы в буфер обмена preferences.rename=Переименовать идентификаторы preferences.rename_case=И исправить проблемы именования preferences.rename_valid=И сделать их верными preferences.rename_printable=И сделать их доступными для печати preferences.rename_use_source_name_as_class_name_alias=Иcпользовать атрибут SOURCE #preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number preferences.search_results_per_page=Результатов на страницу (0 - без лимита) preferences.res_file_ext=Расширения файлов ресурсов ('xml|html', * для всех) preferences.res_skip_file=Пропускать ресурсы больше чем (в МБ) preferences.tab_dnd_appearance=Dragging tab appearance #preferences.plugins.manage=Manage plugins preferences.plugins.install=Установить плагин preferences.plugins.install_btn=Установить preferences.plugins.uninstall_btn=Удалить #preferences.plugins.disable_btn=Disable #preferences.plugins.enable_btn=Enable preferences.plugins.location_id_label=ID источика: preferences.plugins.plugin_jar=Выберите jar-файл плагина preferences.plugins.plugin_jar_label=или preferences.plugins.update_all=Обновить все preferences.plugins.details=О плагине preferences.plugins.task.installing=Установка плагина preferences.plugins.task.uninstalling=Удаление плагина preferences.plugins.task.updating=Обновление плагинов preferences.plugins.task.downloading_list=Загрузка списка плагинов #preferences.plugins.task.status=Changing plugin status preferences.cache=Кэш preferences.cache.location=Директория кэша preferences.cache.location_default=Системная папка приложения preferences.cache.location_local=Совпадает с папкой проекта preferences.cache.location_custom=Кастомная папка: preferences.cache.change_notice=* для существующего кеша, потребуется удаление или ручной сброс preferences.cache.table.title=Список папок с кэшем preferences.cache.table.project=Кэш проекта preferences.cache.table.size=Место на диске preferences.cache.btn.usage=Подсчитать место preferences.cache.btn.delete_selected=Удалить выбранное preferences.cache.btn.delete_all=Удалить все preferences.cache.task.usage=Подсчет занимаемого места preferences.cache.task.delete=Удаление кэша msg.open_file=Пожалуйста, откройте файл msg.saving_sources=Сохранение ресурсов msg.language_changed_title=Язык изменен msg.language_changed=Новый язык применится при следующем запуске программы msg.warning_title=Внимание msg.common_mouse_shortcut=Это часто распространенная комбинация, вы действительно хотите назначить ее? msg.duplicate_shortcut=Действие %s уже выполняет "%s" в категории "%s", продолжить? msg.cmd_select_class_error=Ошибка выбора класса\n%s\nЭтот класс не существует. msg.cant_add_comment=Невозможно добавить комментарий сюда #msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars? #msg.non_displayable_chars.title=Undisplayed Strings #methods_dialog.title=Select methods popup.bytecode_col=Показать Dalvik байткод popup.line_wrap=Перенос строк popup.undo=Отменить popup.redo=Вернуть popup.cut=Вырезать popup.copy=Копировать popup.paste=Вставить popup.delete=Удалить popup.select_all=Выбрать все popup.frida=Копировать как хук Frida popup.xposed=Копировать как хук Xposed popup.find_usage=Найти использования popup.go_to_declaration=Перейти к объявлению popup.exclude=Исключить popup.exclude_packages=Исключить пакеты #popup.convert_number=Add conversion as comment #popup.view_call_graph=View call graph #popup.view_call_graph_description=Show call chains to this function #popup.view_class_graph=View inheritance graph #popup.view_class_graph_description=Show inheritance tree for this class #popup.view_class_method_graph=View methods graph #popup.view_class_method_graph_description=Show all methods for this class #popup.cfg_submenu=View control flow graph #popup.view_cfg=Regular #popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) #popup.view_raw_cfg=Raw #popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) #popup.view_region_cfg=Region #popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=Комментарий #popup.update_comment=Update comment popup.search_comment=Поиск комментариев popup.rename=Переименовать popup.search=Найти "%s" popup.search_global=Глобальный поиск "%s" popup.remove=Удалить popup.add_files=Добавить файлы popup.add_scripts=Добавить скрипты popup.new_script=Новый скрипт popup.json_prettify=Форматировать JSON #popup.export=Export #popup.usage_dialog_plus=Usage Tree Search #popup.copy_as=Copy as... #popup.copy_as_hex=Copy as HEX #popup.copy_as_string=Copy as String #popup.copy_offset=Copy offset script.run=Запустить script.save=Сохранить script.auto_complete=Автозавершение script.check=Проверить script.format=Форматировать script.log=Показать лог #encoding_dialog.title=Encoding #encoding_dialog.message=Select encoding: exclude_dialog.title=Выбор пакетов exclude_dialog.select_all=Выбрать все exclude_dialog.deselect=Убрать exclude_dialog.invert=Инвертировать confirm.save_as_title=Подтверджение сохранения confirm.save_as_message=%s уже существует.\nВы хотите его перезаписать? confirm.not_saved_title=Сохранить проект confirm.not_saved_message=Сохранить текущий проект перед выходом? confirm.remember=Запомнить выбор certificate.cert_type=Type certificate.serialSigVer=Version certificate.serialNumber=Serial number certificate.cert_subject=Subject certificate.serialValidFrom=Valid from certificate.serialValidUntil=Valid until certificate.serialPubKeyType=Public key type certificate.serialPubKeyExponent=Exponent certificate.serialPubKeyModulus=Modulus certificate.serialPubKeyModulusSize=Modulus size (bits) certificate.serialSigType=Signature type certificate.serialSigOID=Signature OID certificate.serialMD5=MD5 Fingerprint certificate.serialSHA1=SHA-1 Fingerprint certificate.serialSHA256=SHA-256 Fingerprint certificate.serialPubKeyY=Y apkSignature.signer=Signer #apkSignature.loading=Loading signature... apkSignature.verificationSuccess=Signature verification succeeded apkSignature.verificationFailed=Signature verification failed apkSignature.signatureSuccess=Valid APK signature v%d found apkSignature.signatureFailed=Invalid APK signature v%d found apkSignature.errors=Errors apkSignature.warnings=Warnings apkSignature.exception=APK verification failed apkSignature.unprotectedEntry=Files that are not protected by APK signature v1. Unauthorized modifications to these entries can only be detected by APK signature v2 and higher. issues_panel.label=Проблемы: issues_panel.errors=Ошибки: %d issues_panel.warnings=Предупреждения: %d issues_panel.tooltip=Открыть просмотр логов debugger.process_selector=Выбрать процесс для отладки debugger.step_into=Перейти (F7) debugger.step_over=Прыжок (F8) debugger.step_out=Выйти (Shift + F8) debugger.run=Запустить (F9) debugger.stop=Остановить debugger.pause=Пауза debugger.rerun=Повторить debugger.cfm_dialog_title=Выйти во время отладки debugger.cfm_dialog_msg=Вы действительно хотите остановить дебаггер? debugger.popup_set_value=Задать значение debugger.popup_change_to_zero=Изменить на 0 debugger.popup_change_to_one=Изменить на 1 debugger.popup_copy_value=Копировать значение logcat.pause=Остановить logcat.start=Возобновить logcat.clear=Очистить logcat.error_fail_start=Ошибка запуска логгера logcat.process=Процесс logcat.level=Уровень лога logcat.default=По умолчанию logcat.verbose=Verbose logcat.debug=Debug logcat.info=Info logcat.warn=Warn logcat.error=Error logcat.fatal=Fatal logcat.silent=Silent logcat.logcat=Лог logcat.select_attached=Прикрепленный процесс logcat.select_all=Выделить все logcat.unselect_all=Отменить выделение set_value_dialog.label_value=Значение set_value_dialog.btn_set=Присвоить set_value_dialog.title=Присвоить значение set_value_dialog.neg_msg=Невозможно присвоить значение set_value_dialog.sel_type=Выберите тип значения adb_dialog.addr=Адрес adb_dialog.port=Порт adb_dialog.path=Путь к ADB adb_dialog.launch_app=Запустить приложение adb_dialog.start_server=Запустить ADB сервер adb_dialog.refresh=Обновить adb_dialog.tip_devices=%d устройств adb_dialog.device_node=Устройство adb_dialog.missing_path=Вы должны указать путь к ADB, для того чтобы запустить сервер adb_dialog.waiting=Ожидание подключения к ADB... adb_dialog.connecting=Подключение к ADB серверу %s на порту %s... adb_dialog.connect_okay=Подключено к серверу, адрес: %s:%s adb_dialog.connect_fail=Невозможно подключиться к ADB adb_dialog.disconnected=ADB сервер отключен adb_dialog.start_okay=ADB запущен на порту: %s. adb_dialog.start_fail=Ошибка запуска ADB сервера на порту: %s! adb_dialog.forward_fail=Не удалось перейти из-за неизвестной ошибки adb_dialog.being_debugged_msg=Похоже, что к этому процессу уже подключен отладчик. Вы хотите продолжить? adb_dialog.unknown_android_ver=Ошибка парсинга версии Android. Использовать Android 8 по умолчанию? adb_dialog.being_debugged_title=Этот процесс уже занят adb_dialog.init_dbg_fail=Ошибка инициализации отладчика adb_dialog.msg_read_mani_fail=Ошибка парсинга AndroidManifest.xml adb_dialog.no_devices=Нет устройств для запуска приложения adb_dialog.restart_while_debugging_title=Перезапустить отладчик adb_dialog.restart_while_debugging_msg=Запущен процесс отдадки приложения. Вы действительно хотите перезапустить сессию? adb_dialog.starting_debugger=Запуск отладки... action.variant=%s (вариант) action_category.menu_toolbar=Панель инструментов #action_category.hex_viewer=View / Hex Viewer action_category.code_area=Редактор кода action_category.plugin_script=Скрипты и плагины #hex_viewer.show_inspector=Show Inspector #hex_viewer.change_encoding=Change Encoding #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find #graph_viewer.long_names=Show full names #graph_viewer.overrides=Show overrides #graph_viewer.callee_depth=Down depth #graph_viewer.caller_depth=Up depth #graph_viewer.default_error=Failed to view graph #graph_viewer.file_not_found_error=Failed to load graph file #graph_viewer.image_too_large=Failed to render graph: graph too large #graph_viewer.image_too_small=Failed to render graph: graph too small #graph_viewer.file_failure=Error in File Operation #graph_viewer.save_graph=Save graph #graph_viewer.default_title=Graph Viewer #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph #graph_viewer.cfg.title=Control Flow Graph ================================================ FILE: jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties ================================================ language.name=中文(简体) menu.file=文件 menu.view=视图 menu.recent_projects=最近的项目 menu.no_recent_projects=没有最近的项目 menu.preferences=首选项 menu.sync=与编辑器同步 menu.flatten=展开显示代码包 menu.enable_preview_tab=启用预览标签页 menu.heapUsageBar=显示内存使用栏 menu.alwaysSelectOpened=始终选中打开的文件/类 menu.dock_log=停靠日志查看器 menu.dock_quick_tabs=停靠快速标签 menu.navigation=导航 menu.text_search=文本搜索 menu.class_search=类名搜索 menu.comment_search=注释搜索 menu.go_to_main_activity=前往 主Activity menu.go_to_application=前往 Application menu.go_to_android_manifest=前往 AndroidManifest menu.tools=工具 menu.plugins=插件 menu.decompile_all=反编译所有类 menu.reset_cache=重置代码缓存 menu.deobfuscation=反混淆 menu.log=日志查看器 menu.create_desktop_entry=创建桌面入口 menu.help=帮助 menu.about=关于 menu.quark=Quark 引擎 menu.update_label=发现新版本 %s! menu.hex_viewer=Hex 查看器 file.open_action=打开文件… file.add_files_action=添加文件 file.open_title=打开文件 file.open_project=打开项目 file.new_project=新建项目 file.save_project=保存项目 file.save_project_as=另存项目为… file.reload=重新加载文件 file.live_reload=实时重加载 file.live_reload_desc=文件变动时自动重载 file.open_mappings=打开映射… file.save_mappings=保存映射 file.save_mappings_as=映射另存为… file.close_mappings=关闭映射 file.save_all=保存伪代码 file.save=保存 file.export=导出项目 file.save_all_msg=请选择保存反编译资源的目录 file.exit=退出 file.export_node=导出文件 start_page.title=开始页面 start_page.start=开始 start_page.recent=最近项目 start_page.list.delete_recent_project=删除项目 start_page.list.delete_recent_project.tooltip=从最近项目列表中删除项目 tree.inputs_title=输入 tree.input_files=文件 tree.input_scripts=脚本 tree.sources_title=源代码 tree.resources_title=资源文件 tree.loading=加载中… tree.pinned_tabs=固定标签 tree.open_tabs=打开标签 tree.bookmarked_tabs=收藏标签 progress.load=正在加载 progress.save_mappings=导出映射 progress.decompile=反编译中 progress.canceling=正在取消 error_dialog.title=错误 error_dialog.not_found=未找到 %s error_dialog.not_found_file=未找到指定路径的文件:\n%s error_dialog.path_is_directory=指定路径是目录:\n%s error_dialog.cannot_read=无法读取文件:\n%s error_dialog.open_failed=文件打开失败:\n%s error_dialog.invalid_path_format=文件路径无效:\n%s error_dialog.desktop_unsupported=桌面在此环境中不受支持。 search.previous=上一个 search.next=下一个 search.mark_all=标记全部 search.regex=正则表达式 search.match_case=区分大小写 search.whole_word=全词匹配 search.find=查找 search.results=%s %d 个结果 search.match_of=匹配 %d / %d search.match_found=找到匹配项 search.match_not_found=未找到匹配项 search.single_match=找到单个匹配项 search.find_type_text=通过文本搜寻 search.find_type_hex=通过HEX搜寻 tabs.copy_class_name=复制名称 tabs.close=关闭 tabs.closeOthers=关闭其他 tabs.unpin=取消固定 tabs.unpin_all=取消所有固定 tabs.bookmark=收藏 tabs.unbookmark=取消收藏 tabs.unbookmark_all=取消所有收藏 tabs.pin=固定 tabs.closeAll=关闭全部 tabs.closeAllRight=关闭右边的所有 tabs.closeAllLeft=关闭左边的所有 tabs.code=代码 tabs.smali=Smali tabs.smali_bytecode=Smali+Bytecode nav.back=后退 nav.forward=前进 message.taskTimeout=任务超过了 %d 毫秒时间限制。 message.userCancelTask=任务被用户取消。 message.memoryLow=Jadx内存不足,请增加最大堆空间后重新启动。 message.taskError=任务失败,出现错误(详情请查看日志)。 message.errorTitle=错误 message.load_errors=加载失败。\n错误数:%d\n点击确定查看日志 message.no_classes=无类被加载,没什么可反编译! message.enter_valid_path=输入有效的保存路径! message.saveIncomplete=保存未完成。
%s
%d 类或资源未被保存! message.indexIncomplete=已跳过某些类索引。
%s
%d 类未被索引,不会出现在搜索结果中! message.indexingClassesSkipped=Jadx 内存不足,因此有 %d 个类未编入索引。
如果要将所有类编入索引,请增大最大堆空间后重启 Jadx。 message.enter_new_name=输入新名称 message.could_not_rename=无法重命名文件 message.confirm_remove_script=您真的要删除脚本吗? message.desktop_entry_creation_error=创建桌面入口失败(请检查日志以了解详细信息)。 message.desktop_entry_creation_success=创建桌面入口成功! message.success_title=成功 message.unable_preview_font=无法预览字体 heapUsage.text=JADX 内存使用率:%.2f GB / %.2f GB (%.2f GB 顶点) common_dialog.ok=确定 common_dialog.cancel=取消 common_dialog.add=添加 common_dialog.update=更新 common_dialog.remove=移除 common_dialog.reset=重置 file_dialog.supported_files=支持的文件 file_dialog.load_dir_title=加载目录 file_dialog.load_dir_confirm=从目录中加载所有文件? search_dialog.open=转到 search_dialog.cancel=取消 search_dialog.open_by_name=搜索文本: search_dialog.search_button=搜索 search_dialog.search_history=搜索历史 search_dialog.auto_search=自动搜索 search_dialog.search_in=在以下位置搜索: search_dialog.class=类名 search_dialog.method=方法名 search_dialog.field=字段名 search_dialog.code=代码 search_dialog.ignorecase=忽略大小写 search_dialog.load_more=加载更多 search_dialog.load_all=加载所有 search_dialog.stop=停止 search_dialog.results_incomplete=已找到 %d+ search_dialog.results_complete=全部找到 %d search_dialog.resources_load_errors=加载错误:%d search_dialog.resources_skip_by_size=按大小跳过:%d search_dialog.resources_check_logs=(点击查看日志) search_dialog.col_node=节点 search_dialog.col_code=代码 search_dialog.sort_results=结果排序 search_dialog.regex=正则表达式 search_dialog.active_tab=只在当前页搜索 search_dialog.comments=注释 search_dialog.resource=资源 search_dialog.keep_open=保持窗口 search_dialog.tip_searching=搜索中… search_dialog.limit_package=限制package: search_dialog.res_text=文本 search_dialog.res_binary=字节码 search_dialog.package_not_found=没有找到匹配的package search_dialog.copy=复制全部 usage_dialog.title=查找 usage_dialog.label=查找用例: usage_dialog_plus.title=树状用例查找 usage_dialog_plus.jump_to=跳转到当前位置 usage_dialog_plus.copy_path=复制用例路径 usage_dialog_plus.search_complete=搜索完成 usage_dialog_plus.code_view=代码预览 usage_dialog_plus.select_node=选择节点 usage_dialog_plus.code_for=代码信息 %s usage_dialog_plus.expand_usages=展开数据 comment_dialog.title.add=添加代码注释 comment_dialog.title.update=更新代码注释 comment_dialog.label=注释: comment_dialog.style=格式: comment_dialog.usage=使用 Shift + Enter 换行 rename_dialog.class_help=输入包名全路径,将类移动到另一个包。以‘.’开始则移到默认(空)包下。 export_dialog.title=导出为源代码 export_dialog.save_path=保存路径: export_dialog.browse=浏览 export_dialog.export_options=导出选项 export_dialog.export_gradle=导出为 Gradle 项目 export_dialog.export_gradle_type=Gradle 模板: log_viewer.title=日志查看器 log_viewer.log_level=日志级别: log_viewer.mode=模式: log_viewer.modes=所有|所有脚本|当前脚本 log_viewer.hide=隐藏 log_viewer.dock=停靠 log_viewer.undock=取消停靠 log_viewer.clear=清除 about_dialog.title=关于 JADX preferences.title=首选项 preferences.deobfuscation=反混淆 preferences.appearance=界面 preferences.shortcuts=快捷键 preferences.select_shortcuts=选择特定的快捷键组合 preferences.decompile=反编译 preferences.plugins=插件 preferences.project=项目 preferences.other=其他 preferences.language=语言 preferences.lineNumbersMode=编辑器行号模式 preferences.jumpOnDoubleClick=启用双击跳转 preferences.useAlternativeFileDialog=使用选择文件对话框 preferences.check_for_updates=启动时检查更新 preferences.useDx=使用 dx/d8 来转换java字节码 preferences.decompilationMode=反编译模式 preferences.codeCacheMode=代码缓存模式 preferences.codeCacheMode.memory=内存 preferences.codeCacheMode.memory.desc=全部保存在内存中:搜索快,重新打开慢,内存占用高 preferences.codeCacheMode.diskWithCache=磁盘加缓存 preferences.codeCacheMode.diskWithCache.desc=代码保存在磁盘中并带有内存缓存:搜索速度中等,重新打开快,内存占用中等 preferences.codeCacheMode.disk=磁盘 preferences.codeCacheMode.disk.desc=全部保存在磁盘中:搜索慢,重新打开快,内存占用低 preferences.usageCacheMode=数据缓存模式 preferences.showInconsistentCode=显示不一致的代码 preferences.escapeUnicode=Unicode 字符转义 preferences.replaceConsts=替换常量 preferences.respectBytecodeAccessModifiers=遵守字节码访问修饰符 preferences.useImports=使用 import 语句 preferences.useDebugInfo=启用调试信息 preferences.inlineAnonymous=内联匿名类 preferences.inlineMethods=内联方法 preferences.inlineKotlinLambdas=允许内联Kotlin Lambda preferences.moveInnerClasses=将内部类移到父类 preferences.extractFinally=提取finally块 preferences.restoreSwitchOverString=恢复switch字符串 preferences.fsCaseSensitive=文件系统区分大小写 preferences.skipResourcesDecode=不反编译资源文件 preferences.skipSourcesDecode=不反编译源代码 preferences.useKotlinMethodsForVarNames=使用Kotlin方法来重命名变量 preferences.commentsLevel=代码注释级别 preferences.saveOption=自动保存设置 preferences.threads=并行线程数 preferences.excludedPackages=排除的包 preferences.excludedPackages.tooltip=排除于反编译或索引的以空格分隔的包名列表(节省 RAM) preferences.excludedPackages.button=编辑 preferences.excludedPackages.editDialog=排除于反编译或索引的以空格分隔的包名列表(节省 RAM)
例如android.support preferences.cfg=生成方法的 CFG 图(‘.dot’) preferences.raw_cfg=生成原始的 CFG 图 preferences.xposed_codegen_language=Xposed代码生成语言 preferences.update_channel=Jadx 更新通道 preferences.disable_tooltip_on_hover=禁用悬停提示 preferences.integerFormat=数值格式化 preferences.typeUpdatesCountLimit=类型更新次数上限 preferences.ui_zoom=UI 缩放系数 preferences.apply_ui_zoom_to_fonts=将 UI 缩放应用到字体 preferences.ui_font=UI 字体 preferences.code_font=编辑器字体 preferences.smali_font=等宽字体(Smali/Hex) preferences.laf_theme=主题 preferences.dynamic_editor_theme=使用 UI 主题色彩 preferences.theme=编辑器主题 preferences.start_jobs=自动进行后台反编译 preferences.select_font=修改 preferences.deobfuscation_on=启用反混淆 preferences.generated_renames_mapping_file_mode=映射文件句柄模式 preferences.deobfuscation_min_len=最小命名长度 preferences.deobfuscation_max_len=最大命名长度 preferences.deobfuscation_res_name_source=更好的资源名称来源 preferences.deobfuscation_res_use_headers=使用文件头检测资源扩展 preferences.deobfuscation_whitelist=从反混淆中排除包和类 preferences.deobfuscation_whitelist.editDialog=反混淆白名单 preferences.deobfuscation_whitelist.tooltip=以‘:’分隔的包(后缀‘.*’)和类名列表,它们不会被反混淆。 preferences.save=保存 preferences.cancel=取消 preferences.reset=重置 preferences.reset_message=要恢复为默认设置吗? preferences.reset_title=重置设置 preferences.copy=复制到剪切板 preferences.copy_message=所有设置都已复制 preferences.rename=重命名标识符 preferences.rename_case=标识符要能够区分大小写 preferences.rename_valid=标识符应该符合标准规范 preferences.rename_printable=标识符必须要能正常显示 preferences.rename_use_source_name_as_class_name_alias=使用资源名作为类的别名 preferences.rename_source_name_repeat_limit=如果源名称少于限制数,则允许使用源名称 preferences.search_results_per_page=每页结果数(0 - 无限制) preferences.res_file_ext=文件扩展名(比如 .xml|.html),* 表示所有 preferences.res_skip_file=跳过文件大小(MB) preferences.tab_dnd_appearance=拖拽选项卡外观 preferences.plugins.manage=管理插件 preferences.plugins.install=安装插件 preferences.plugins.install_btn=安装 preferences.plugins.uninstall_btn=卸载 preferences.plugins.disable_btn=禁用 preferences.plugins.enable_btn=启用 preferences.plugins.location_id_label=位置ID: preferences.plugins.plugin_jar=选择插件 jar preferences.plugins.plugin_jar_label=或 preferences.plugins.update_all=更新所有 preferences.plugins.details=插件详情 preferences.plugins.task.installing=安装插件中 preferences.plugins.task.uninstalling=卸载插件中 preferences.plugins.task.updating=更新插件中 preferences.plugins.task.downloading_list=正在下载插件列表 preferences.plugins.task.status=更改插件状态 preferences.cache=缓存 preferences.cache.location=缓存位置 preferences.cache.location_default=应用缓存的系统目录 preferences.cache.location_local=与项目文件同级的目录 preferences.cache.location_custom=自定义目录: preferences.cache.change_notice=* 对于已存在缓存,改动将在缓存删除或手动重置后生效 preferences.cache.table.title=缓存列表 preferences.cache.table.project=项目缓存 preferences.cache.table.size=磁盘使用率 preferences.cache.btn.usage=计算使用率 preferences.cache.btn.delete_selected=删除所选 preferences.cache.btn.delete_all=删除所有 preferences.cache.task.usage=计算缓存大小中 preferences.cache.task.delete=删除缓存中 msg.open_file=请打开文件 msg.saving_sources=正在导出源代码 msg.language_changed_title=语言已更改 msg.language_changed=新的语言将在下次应用程序启动时显示。 msg.warning_title=警告 msg.common_mouse_shortcut=这是常用按键,您确定要绑定操作吗? msg.duplicate_shortcut=快捷键 %s 已被操作“%s”(类别“%s”)占用,是否继续? msg.cmd_select_class_error=无法选择类\n%s\n该类不存在。 msg.cant_add_comment=无法在此添加注释 msg.non_displayable_chars=部分字符在当前使用的字体“%s”中无法显示,是否显示这些字符? msg.non_displayable_chars.title=未显示的字符串 methods_dialog.title=选择方法 popup.bytecode_col=显示Dalvik字节码 popup.line_wrap=自动换行 popup.undo=撤销 popup.redo=重做 popup.cut=剪切 popup.copy=复制 popup.paste=粘贴 popup.delete=删除 popup.select_all=全选 popup.frida=复制为 frida 片段 popup.xposed=复制为 xposed 片段 popup.find_usage=查找用例 popup.go_to_declaration=跳到声明 popup.exclude=排除此包 popup.exclude_packages=排除包 popup.convert_number=将数值转换添加为注释 popup.view_call_graph=查看调用图 popup.view_call_graph_description=显示到此函数的调用链 popup.view_class_graph=查看继承图 popup.view_class_graph_description=显示该类的继承树 popup.view_class_method_graph=查看方法图 popup.view_class_method_graph_description=显示该类的所有方法 popup.cfg_submenu=查看控制图 popup.view_cfg=常规 popup.view_cfg_description=显示该函数的常规控制流图(可在 文件->首选项->其他 中启用) popup.view_raw_cfg=原始 popup.view_raw_cfg_description=显示该函数包含原始指令的控制流图(可在 文件->首选项->其他 中启用) popup.view_region_cfg=区域 popup.view_region_cfg_description=显示该函数的区域化控制流图(可在 文件->首选项->其他 中启用) popup.add_comment=添加注释 popup.update_comment=更新注释 popup.search_comment=搜索注释 popup.rename=重命名 popup.search=搜索 “%s” popup.search_global=全局搜索 “%s” popup.remove=移除 popup.add_files=添加文件 popup.add_scripts=添加脚本 popup.new_script=新建脚本 popup.json_prettify=JSON 格式化 popup.export=导出 popup.usage_dialog_plus=树状用例查找 popup.copy_as=另存为… popup.copy_as_hex=另存为 HEX popup.copy_as_string=另存为字符串 popup.copy_offset=复制偏移 script.run=运行 script.save=保存 script.auto_complete=自动补全 script.check=检查 script.format=重新格式化 script.log=显示日志 encoding_dialog.title=编码 encoding_dialog.message=选择编码: exclude_dialog.title=选择要排除的包 exclude_dialog.select_all=全选 exclude_dialog.deselect=取消选择 exclude_dialog.invert=反选 confirm.save_as_title=确认另存为 confirm.save_as_message=%s 已存在。\n你想替换它吗? confirm.not_saved_title=保存项目 confirm.not_saved_message=在继续之前保存当前项目? confirm.remember=记住我的决定 certificate.cert_type=类型 certificate.serialSigVer=版本 certificate.serialNumber=序列号 certificate.cert_subject=主题 certificate.serialValidFrom=有效期始 certificate.serialValidUntil=有效期至 certificate.serialPubKeyType=公钥类型 certificate.serialPubKeyExponent=指数 certificate.serialPubKeyModulus=模数 certificate.serialPubKeyModulusSize=模数大小(位) certificate.serialSigType=签名算法 certificate.serialSigOID=签名 OID certificate.serialMD5=MD5 签名 certificate.serialSHA1=SHA-1 签名 certificate.serialSHA256=SHA-256 签名 certificate.serialPubKeyY=Y apkSignature.signer=签名者 apkSignature.loading=加载签名中… apkSignature.verificationSuccess=签名验证成功 apkSignature.verificationFailed=签名验证失败 apkSignature.signatureSuccess=找到有效的 APK 签名 v%d apkSignature.signatureFailed=找到无效的 APK 签名 v%d apkSignature.errors=错误 apkSignature.warnings=警告 apkSignature.exception=APK 验证失败 apkSignature.unprotectedEntry=未受 v1签名 保护的文件。对这些条目的未经授权的修改,只能通过 v2签名 或更高版本来检测。 issues_panel.label=问题: issues_panel.errors=%d 错误 issues_panel.warnings=%d 警告 issues_panel.tooltip=点击查看日志 debugger.process_selector=选择要调试的进程 debugger.step_into=步入(F7) debugger.step_over=步过(F8) debugger.step_out=步出(Shift + F8) debugger.run=运行(F9) debugger.stop=停止调试并终止应用 debugger.pause=暂停 debugger.rerun=重新运行 debugger.cfm_dialog_title=在调试时退出 debugger.cfm_dialog_msg=您确定要终止调试器吗? debugger.popup_set_value=修改值 debugger.popup_change_to_zero=修改为0 debugger.popup_change_to_one=修改为1 debugger.popup_copy_value=复制值 logcat.pause=暂停 Logcat logcat.start=恢复 Logcat logcat.clear=清空 Logcat logcat.error_fail_start=无法启动 Logcat logcat.process=进程 logcat.level=级别 logcat.default=默认 logcat.verbose=Verbose logcat.debug=Debug logcat.info=Info logcat.warn=Warn logcat.error=Error logcat.fatal=Fatal logcat.silent=Silent logcat.logcat=Logcat logcat.select_attached=选择附加 logcat.select_all=选择所有 logcat.unselect_all=反选所有 set_value_dialog.label_value=值 set_value_dialog.btn_set=修改 set_value_dialog.title=设置新值 set_value_dialog.neg_msg=修改数值失败。 set_value_dialog.sel_type=选择要修改值的类型。 adb_dialog.addr=ADB IP adb_dialog.port=ADB 端口 adb_dialog.path=ADB 路径 adb_dialog.launch_app=运行APP adb_dialog.start_server=启动ADB服务 adb_dialog.refresh=刷新 adb_dialog.tip_devices=%d 台设备 adb_dialog.device_node=设备 adb_dialog.missing_path=必须提供ADB路径才能启动ADB服务。 adb_dialog.waiting=正在等待连接到ADB服务… adb_dialog.connecting=正在连接ADB服务,地址:%s:%s… adb_dialog.connect_okay=已连接ADB服务,地址:%s:%s adb_dialog.connect_fail=连接ADB服务失败。 adb_dialog.disconnected=ADB服务已断开连接。 adb_dialog.start_okay=ADB服务启动成功 端口:%s。 adb_dialog.start_fail=ADB服务启动失败 端口:%s! adb_dialog.forward_fail=由于某些原因无法转发。 adb_dialog.being_debugged_msg=这个进程似乎正在调试,是否继续? adb_dialog.unknown_android_ver=获取安卓版本失败,是否默认使用Android 8? adb_dialog.being_debugged_title=它正在由其他人调试。 adb_dialog.init_dbg_fail=初始化调试器失败。 adb_dialog.msg_read_mani_fail=解码AndroidManifest.xml失败 adb_dialog.no_devices=找不到任何设备用来启动APP。 adb_dialog.restart_while_debugging_title=调试时重新启动 adb_dialog.restart_while_debugging_msg=你正在调试一个APP,确定要重新启动一个会话吗? adb_dialog.starting_debugger=正在启动调试器… action.variant=%s(次要) action_category.menu_toolbar=菜单/工具栏 action_category.hex_viewer=查看器/HEX查看器 action_category.code_area=代码区 action_category.plugin_script=插件脚本 hex_viewer.show_inspector=显示检查器 hex_viewer.change_encoding=更改编码 hex_viewer.goto_address=跳转到地址 hex_viewer.enter_address=输入地址范围: hex_viewer.find=查找 graph_viewer.long_names=显示完整名称 graph_viewer.overrides=显示重写关系 graph_viewer.callee_depth=向下深度 graph_viewer.caller_depth=向上深度 graph_viewer.default_error=图形显示失败 graph_viewer.file_not_found_error=图形文件加载失败 graph_viewer.image_too_large=图形渲染失败:图过大 graph_viewer.image_too_small=图形渲染失败:图过小 graph_viewer.file_failure=文件操作错误 graph_viewer.save_graph=保存图形 graph_viewer.default_title=图形查看器 graph_viewer.method_graph.title=方法图 graph_viewer.call_graph.title=调用图 graph_viewer.inheritance_graph.title=继承图 graph_viewer.cfg.title=控制图 ================================================ FILE: jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties ================================================ language.name=中文 (繁體) menu.file=檔案 menu.view=檢視 menu.recent_projects=近期專案 menu.no_recent_projects=無近期專案 menu.preferences=選項 menu.sync=與編輯器同步 menu.flatten=展開顯示套件 #menu.enable_preview_tab=Enable Preview Tab menu.heapUsageBar=顯示記憶體使用率條 menu.alwaysSelectOpened=總是選擇已開啟的檔案/類別 menu.dock_log=固定記錄檔檢視器 menu.dock_quick_tabs=固定快速分頁 menu.navigation=瀏覽 menu.text_search=文字搜尋 menu.class_search=類別搜尋 menu.comment_search=註解搜尋 menu.go_to_main_activity=前往主 Activity menu.go_to_application=前往應用程式 menu.go_to_android_manifest=前往 AndroidManifest.xml menu.tools=工具 menu.plugins=外掛程式 menu.decompile_all=反編譯所有類別 menu.reset_cache=重設程式碼快取 menu.deobfuscation=去模糊化 menu.log=記錄檔檢視器 menu.create_desktop_entry=建立桌面項目 menu.help=幫助 menu.about=關於 menu.quark= Quark 引擎 menu.update_label=新版本 %s 可供下載! #menu.hex_viewer=Hex Viewer file.open_action=開啟檔案... file.add_files_action=新增檔案 file.open_title=開啟檔案 file.open_project=開啟專案 file.new_project=新建專案 file.save_project=儲存專案 file.save_project_as=另存專案... file.reload=重新載入檔案 file.live_reload=實時重新載入 file.live_reload_desc=更動後自動重新載入檔案 file.open_mappings=開啟對映檔 file.save_mappings=儲存對映檔 file.save_mappings_as=儲存對映檔為... file.close_mappings=關閉對映檔 file.save_all=全部儲存 file.save=儲存 file.export=匯出專案 file.save_all_msg=選擇儲存反編譯原始碼的路徑 file.exit=離開 file.export_node=匯出檔案 start_page.title=開始頁面 start_page.start=開始 start_page.recent=近期專案 start_page.list.delete_recent_project=刪除項目 start_page.list.delete_recent_project.tooltip=從最近列表中刪除 tree.inputs_title=輸入 tree.input_files=檔案 tree.input_scripts=腳本 tree.sources_title=原始碼 tree.resources_title=資源 tree.loading=載入中... tree.pinned_tabs=已釘選的分頁 tree.open_tabs=已開啟的分頁 tree.bookmarked_tabs=已加入書籤的分頁 progress.load=載入中 progress.save_mappings=正在匯出對應 progress.decompile=正在反編譯 progress.canceling=正在取消 error_dialog.title=錯誤 error_dialog.not_found=找不到 %s error_dialog.not_found_file=找不到檔案:\n%s error_dialog.path_is_directory=指定路徑是目錄:\n%s error_dialog.cannot_read=無法讀取檔案:\n%s error_dialog.open_failed=開啟檔案失敗:\n%s error_dialog.invalid_path_format=檔案路徑格式無效:\n%s error_dialog.desktop_unsupported=此環境不支援桌面操作。 search.previous=上一個 search.next=下一個 search.mark_all=全部標記 search.regex=Regex search.match_case=大小寫須相符 search.whole_word=全字拼寫須相符 search.find=尋找 search.results=%s%d 個結果 search.match_of=第 %d 個,共 %d 個 search.match_found=找到相符結果 search.match_not_found=為找到相符結果 search.single_match=找到一個相符結果 #search.find_type_text=Find by text #search.find_type_hex=Find by hex tabs.copy_class_name=複製名稱 tabs.close=關閉 tabs.closeOthers=關閉其他 tabs.unpin=取消釘選 tabs.unpin_all=全部取消釘選 tabs.bookmark=加上書籤 tabs.unbookmark=移除書籤 tabs.unbookmark_all=全部移除書籤 tabs.pin=釘選 tabs.closeAll=關閉全部 tabs.closeAllRight=關閉右邊的所有 #tabs.closeAllLeft=Close All Left tabs.code=程式碼 tabs.smali=Smali tabs.smali_bytecode=Smali+Bytecode nav.back=返回 nav.forward=向前 message.taskTimeout=工作超過時間限制 (%d 毫秒)。 message.userCancelTask=工作被使用者取消了。 message.memoryLow=Jadx 的記憶體不足。請增加最大堆疊大小並重新啟動。 message.taskError=工作發生錯誤 (查看記錄檔以了解詳情)。 message.errorTitle=錯誤 message.load_errors=載入失敗。\n錯誤數:%d\n點擊 OK 以開啟記錄檔檢視器 message.no_classes=未載入任何類別,沒有東西可以反編譯! message.enter_valid_path=輸入有效的路徑以儲存! message.saveIncomplete=儲存未完成。
%s
%d 個類別或資源尚未儲存! message.indexIncomplete=某些類別的索引被略過。
%s
%d 個類別未被索引,故不會出現在搜尋結果中。 message.indexingClassesSkipped=Jadx 的記憶體不足。故 %d 個類別未被索引。
如果您想要索引所有類別,請增加最大堆疊大小並重新啟動。 message.enter_new_name=輸入新名稱 message.could_not_rename=無法重新命名檔案 message.confirm_remove_script=您確定要移除指令碼嗎? message.desktop_entry_creation_error=建立桌面項目失敗(詳情請查閱日誌)。 message.desktop_entry_creation_success=成功建立桌面項目! message.success_title=成功 #message.unable_preview_font=Unable preview font heapUsage.text=JADX 記憶體使用率:%.2f GB / %.2f GB (%.2f GB 潼) common_dialog.ok=Ok common_dialog.cancel=取消 common_dialog.add=新增 common_dialog.update=更新 common_dialog.remove=移除 common_dialog.reset=重設 file_dialog.supported_files=支援的檔案 file_dialog.load_dir_title=載入目錄 file_dialog.load_dir_confirm=要載入目錄中的所有檔案嗎? search_dialog.open=開啟 search_dialog.cancel=取消 search_dialog.open_by_name=搜尋文字: search_dialog.search_button=搜尋 search_dialog.search_history=搜尋記錄 search_dialog.auto_search=自動搜尋 search_dialog.search_in=搜尋定義: search_dialog.class=類別 search_dialog.method=方法 search_dialog.field=欄位 search_dialog.code=程式碼 search_dialog.ignorecase=不區分大小寫 search_dialog.load_more=載入更多 search_dialog.load_all=載入全部 search_dialog.stop=停止 search_dialog.results_incomplete=找到 %d+ search_dialog.results_complete=找到 %d (完整) search_dialog.resources_load_errors=載入錯誤:%d search_dialog.resources_skip_by_size=因大小跳過:%d search_dialog.resources_check_logs=(點擊以查看記錄檔) search_dialog.col_node=無 search_dialog.col_code=程式碼 search_dialog.sort_results=排序結果 search_dialog.regex=Regex search_dialog.active_tab=僅使用中分頁 search_dialog.comments=註解 search_dialog.resource=資源 search_dialog.keep_open=保持開啟 search_dialog.tip_searching=正在搜尋 search_dialog.limit_package=限制至套件: #search_dialog.res_text=Text #search_dialog.res_binary=Binary search_dialog.package_not_found=找不到符合的套件 search_dialog.copy=複製全部 usage_dialog.title=使用情況搜尋 usage_dialog.label=使用情況: usage_dialog_plus.title=使用情況搜尋 usage_dialog_plus.jump_to=前往目前位置 usage_dialog_plus.copy_path=複製使用情況路徑 usage_dialog_plus.search_complete=搜尋完成 usage_dialog_plus.code_view=程式碼預覽 usage_dialog_plus.select_node=選擇節點 usage_dialog_plus.code_for=程式碼資訊 %s usage_dialog_plus.expand_usages=展開資料 comment_dialog.title.add=新增程式碼註解 comment_dialog.title.update=更新程式碼註解 comment_dialog.label=註解: comment_dialog.style=風格: comment_dialog.usage=按下 Shift + Enter 來換行 rename_dialog.class_help=輸入全名以將類別移動至其他套件。以 '.' 開頭將移動至預設(空)套件。 export_dialog.title=匯出至原始碼 export_dialog.save_path=儲存路徑: export_dialog.browse=瀏覽 export_dialog.export_options=匯出選項 export_dialog.export_gradle=匯出成 Gradle 專案 export_dialog.export_gradle_type=Gradle 模板: log_viewer.title=記錄檔檢視器 log_viewer.log_level=記錄層級: log_viewer.mode=模式: log_viewer.modes=所有|所有腳本|目前腳本 log_viewer.hide=隱藏 log_viewer.dock=固定 log_viewer.undock=解除固定 log_viewer.clear=清除 about_dialog.title=關於 JADX preferences.title=選項 preferences.deobfuscation=去模糊化 preferences.appearance=外觀 preferences.shortcuts=快捷鍵 preferences.select_shortcuts=選擇特定的快捷鍵群組 preferences.decompile=反編譯 preferences.plugins=外掛程式 preferences.project=專案 preferences.other=其他 preferences.language=語言 preferences.lineNumbersMode=編輯器行號模式 preferences.jumpOnDoubleClick=啟用點擊兩下時跳躍 preferences.useAlternativeFileDialog=使用替代檔案對話框 preferences.check_for_updates=啟動時檢查更新 preferences.useDx=使用 dx/d8 來轉換 Java 位元組碼 preferences.decompilationMode=反編譯模式 preferences.codeCacheMode=程式碼快取模式 #preferences.codeCacheMode.memory=Memory #preferences.codeCacheMode.memory.desc=Everything in memory: fast search, slow reopen, high memory usage #preferences.codeCacheMode.diskWithCache=Disk with cache #preferences.codeCacheMode.diskWithCache.desc=Code saved on disk with in memory cache: medium search, fast reopen, medium memory usage #preferences.codeCacheMode.disk=Disk #preferences.codeCacheMode.disk.desc=Everything on disk: slow search, fast reopen, low memory usage preferences.usageCacheMode=使用資料快取模式 preferences.showInconsistentCode=顯示不一致的程式碼 preferences.escapeUnicode=Unicode 逸出 preferences.replaceConsts=替換常數 preferences.respectBytecodeAccessModifiers=遵守位元組碼存取修飾詞 preferences.useImports=使用 import 陳述式 preferences.useDebugInfo=使用除錯資訊 preferences.inlineAnonymous=內嵌匿名類別 preferences.inlineMethods=內嵌方法 preferences.inlineKotlinLambdas=允許內嵌 Kotlin 匿名函式 preferences.moveInnerClasses=將內部類別移動至父類別 preferences.extractFinally=擷取 finally 區塊 preferences.restoreSwitchOverString=將字串復原成 switch preferences.fsCaseSensitive=檔案系統區分大小寫 preferences.skipResourcesDecode=不要為資源解碼 preferences.skipSourcesDecode=不要反編譯原始碼 preferences.useKotlinMethodsForVarNames=使用 Kotlin 方法來為變數重新命名 preferences.commentsLevel=程式碼註解層級 preferences.saveOption=自動儲存設定 preferences.threads=執行緒數 preferences.excludedPackages=被排除的套件 preferences.excludedPackages.tooltip=排除於索引或反編譯外的套件列表 (以空格分隔) (節省 RAM) preferences.excludedPackages.button=編輯 preferences.excludedPackages.editDialog=排除於索引或反編譯外的套件列表 (以空格分隔) (節省 RAM)
例如 android.support preferences.cfg=產生方法 CFG 圖表 ('dot' 格式) preferences.raw_cfg=產生 RAW CFG 圖表 preferences.xposed_codegen_language=Xposed 程式碼產生語言 preferences.update_channel=Jadx 更新頻道 #preferences.disable_tooltip_on_hover=Disable tooltip on hover preferences.integerFormat=整數模式 #preferences.typeUpdatesCountLimit=Update type limit count #preferences.ui_zoom=UI Zoom factor #preferences.apply_ui_zoom_to_fonts=Apply UI zoom to fonts #preferences.ui_font=UI font preferences.code_font=編輯器字型 preferences.smali_font=等寬字型 (Smali/Hex) preferences.laf_theme=主題 #preferences.dynamic_editor_theme=Use UI theme colors preferences.theme=編輯器主題 preferences.start_jobs=自動開始背景反編譯 preferences.select_font=變更 preferences.deobfuscation_on=啟用去模糊化 preferences.generated_renames_mapping_file_mode=Map 檔案處理模式 preferences.deobfuscation_min_len=最小名稱長度 preferences.deobfuscation_max_len=最大名稱長度 preferences.deobfuscation_res_name_source=較佳的資源名稱來源 #preferences.deobfuscation_res_use_headers=Use headers for detect resource extensions preferences.deobfuscation_whitelist=去模糊化時排除套件和類別 preferences.deobfuscation_whitelist.editDialog=去模糊化白名單 preferences.deobfuscation_whitelist.tooltip=將不會被去模糊化的套件(後綴 '.*')和類別名稱清單,以 ':' 分隔 preferences.save=儲存 preferences.cancel=取消 preferences.reset=重設 preferences.reset_message=要將設定重設為預設值嗎? preferences.reset_title=重設設定 preferences.copy=複製至剪貼簿 preferences.copy_message=已將所有設定值複製至剪貼簿 preferences.rename=重新命名識別碼 preferences.rename_case=以修復區分大小寫問題 preferences.rename_valid=以使其有效 preferences.rename_printable=以使其可列印 preferences.rename_use_source_name_as_class_name_alias=將原始檔案名稱作為類別別名 preferences.rename_source_name_repeat_limit=若出現次數少於限制數便允許使用來源名稱 preferences.search_results_per_page=每頁的搜尋結果數 (0 - 無限制) preferences.res_file_ext=副檔名 (e.g. .xml|.html), * 表示全部 preferences.res_skip_file=略過大於此值的檔案 (MB) preferences.tab_dnd_appearance=拖動分頁時外觀 preferences.plugins.manage=管理外掛程式 preferences.plugins.install=安裝外掛程式 preferences.plugins.install_btn=安裝 preferences.plugins.uninstall_btn=解除安裝 preferences.plugins.disable_btn=停用 preferences.plugins.enable_btn=啟用 preferences.plugins.location_id_label=位置 id: preferences.plugins.plugin_jar=選擇外掛程式 jar preferences.plugins.plugin_jar_label=或 preferences.plugins.update_all=全部更新 preferences.plugins.details=外掛程式詳細資訊 preferences.plugins.task.installing=正在安裝外掛程式 preferences.plugins.task.uninstalling=正在解除安裝外掛程式 preferences.plugins.task.updating=正在更新外掛程式 preferences.plugins.task.downloading_list=正在下載外掛程式列表 preferences.plugins.task.status=正在變更外掛程式狀態 preferences.cache=快取 preferences.cache.location=快取位置 preferences.cache.location_default=應用程式快取系統目錄 preferences.cache.location_local=與專案檔案同目錄 preferences.cache.location_custom=自訂位置: preferences.cache.change_notice=* 現有的快取將在刪除或手動重設後套用變更 preferences.cache.table.title=快取列表 preferences.cache.table.project=專案快取 preferences.cache.table.size=硬碟使用量 preferences.cache.btn.usage=計算使用量 preferences.cache.btn.delete_selected=刪除所選 preferences.cache.btn.delete_all=全部刪除 preferences.cache.task.usage=正在計算快取大小 preferences.cache.task.delete=正在刪除快取 msg.open_file=請開啟檔案 msg.saving_sources=正在儲存原始碼 msg.language_changed_title=已更改語言 msg.language_changed=新語言將於下次應用程式啟動時套用。 msg.warning_title=警告 msg.common_mouse_shortcut=這是常用按鍵,您確定要綁定操作嗎? msg.duplicate_shortcut=快捷鍵 %s 已設為操作 "%s",位於群組 "%s"。確定要繼續嗎? msg.cmd_select_class_error=無法選擇類別\n%s\n類別不存在。 msg.cant_add_comment=無法在此新增註解 #msg.non_displayable_chars=Some characters were found in code are not displayable by currently used font "%s". Show chars? #msg.non_displayable_chars.title=Undisplayed Strings methods_dialog.title=選擇方法 popup.bytecode_col=顯示 Dalvik 位元組碼 popup.line_wrap=自動換行 popup.undo=復原 popup.redo=重做 popup.cut=剪下 popup.copy=複製 popup.paste=貼上 popup.delete=刪除 popup.select_all=全選 popup.frida=複製為 frida 片段 popup.xposed=複製為 xposed 片段 popup.find_usage=尋找使用情況 popup.go_to_declaration=前往宣告 popup.exclude=排除 popup.exclude_packages=排除套件 #popup.convert_number=Add conversion as comment #popup.view_call_graph=View call graph #popup.view_call_graph_description=Show call chains to this function #popup.view_class_graph=View inheritance graph #popup.view_class_graph_description=Show inheritance tree for this class #popup.view_class_method_graph=View methods graph #popup.view_class_method_graph_description=Show all methods for this class #popup.cfg_submenu=View control flow graph #popup.view_cfg=Regular #popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) #popup.view_raw_cfg=Raw #popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) #popup.view_region_cfg=Region #popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=註解 popup.update_comment=更新註解 popup.search_comment=搜尋註解 popup.rename=重新命名 popup.search=搜尋 "%s" popup.search_global=全域搜尋 "%s" popup.remove=移除 popup.add_files=新增檔案 popup.add_scripts=加入腳本 popup.new_script=新增腳本 popup.json_prettify=JSON 格式化 popup.export=匯出 popup.usage_dialog_plus=查找引用 popup.copy_as=另存為... popup.copy_as_hex=另存爲 HEX popup.copy_as_string=另存爲 字符串 popup.copy_offset=複製 偏移量 script.run=執行 script.save=儲存 script.auto_complete=自動完成 script.check=檢查 script.format=重新格式化 script.log=顯示記錄檔 encoding_dialog.title=編碼 encoding_dialog.message=選擇編碼: exclude_dialog.title=套件選擇 exclude_dialog.select_all=全選 exclude_dialog.deselect=取消選取 exclude_dialog.invert=反轉選取 confirm.save_as_title=確認另存為 confirm.save_as_message=%s 已存在。\n您要覆寫它嗎? confirm.not_saved_title=儲存專案 confirm.not_saved_message=在繼續進行前要先儲存目前專案嗎? confirm.remember=記住我的選擇 certificate.cert_type=類型 certificate.serialSigVer=版本 certificate.serialNumber=序號 certificate.cert_subject=主體 certificate.serialValidFrom=有效期自 certificate.serialValidUntil=有效期限 certificate.serialPubKeyType=公鑰類型 certificate.serialPubKeyExponent=指數 certificate.serialPubKeyModulus=模數 certificate.serialPubKeyModulusSize=模數大小 (位元) certificate.serialSigType=簽名類型 certificate.serialSigOID=簽名 OID certificate.serialMD5=MD5 指紋 certificate.serialSHA1=SHA-1 指紋 certificate.serialSHA256=SHA-256 指紋 certificate.serialPubKeyY=Y apkSignature.signer=簽署者 apkSignature.loading=載入簽名中... apkSignature.verificationSuccess=簽名驗證成功 apkSignature.verificationFailed=簽名驗證失敗 apkSignature.signatureSuccess=找到可用的 APK 簽名 v%d apkSignature.signatureFailed=找到無效的 APK 簽名 v%d apkSignature.errors=錯誤 apkSignature.warnings=警告 apkSignature.exception=APK 驗證失敗 apkSignature.unprotectedEntry=未被 APK 簽名 v1 保護的檔案。對這些項目進行未經授權的更動僅能由 APK 簽名 v2 及以上版本偵測。 issues_panel.label=問題: issues_panel.errors=%d 錯誤 issues_panel.warnings=%d 警告 issues_panel.tooltip=在記錄檔檢視器中開啟 debugger.process_selector=選擇要偵錯的處理程序 debugger.step_into=逐步執行 (F7) debugger.step_over=不進入函式 (F8) debugger.step_out=跳離函式 (Shift + F8) debugger.run=執行 (F9) debugger.stop=停止偵錯工具並終止應用程式 debugger.pause=暫停 debugger.rerun=重新執行 debugger.cfm_dialog_title=偵錯時離開 debugger.cfm_dialog_msg=您確定要終止偵錯工具嗎? debugger.popup_set_value=設置數值 debugger.popup_change_to_zero=修改為 0 debugger.popup_change_to_one=修改為 1 debugger.popup_copy_value=複製數值 logcat.pause=暫停 Logcat logcat.start=繼續 Logcat logcat.clear=清除 Logcat logcat.error_fail_start=無法啟動 Logcat logcat.process=程序 logcat.level=等級 logcat.default=預設 logcat.verbose=詳細資訊 logcat.debug=偵錯 logcat.info=資訊 logcat.warn=警告 logcat.error=錯誤 logcat.fatal=嚴重錯誤 logcat.silent=靜音 logcat.logcat=Logcat logcat.select_attached=選取附件 logcat.select_all=全選 logcat.unselect_all=取消全選 set_value_dialog.label_value=數值 set_value_dialog.btn_set=設置數值 set_value_dialog.title=設置數值 set_value_dialog.neg_msg=設置數值失敗。 set_value_dialog.sel_type=選擇要設置數值的類型。 adb_dialog.addr=ADB 位址 adb_dialog.port=ADB 連接埠 adb_dialog.path=ADB 路徑 adb_dialog.launch_app=啟動應用程式 adb_dialog.start_server=啟動 ADB 伺服器 adb_dialog.refresh=重新整理 adb_dialog.tip_devices=%d 裝置 adb_dialog.device_node=裝置 adb_dialog.missing_path=必須提供 ADB 路徑才能啟動 ADB 伺服器。 adb_dialog.waiting=正在等待連接至 ADB 伺服器... adb_dialog.connecting=正在連接至 ADB 伺服器,位址:%s:%s... adb_dialog.connect_okay=已連接至 ADB 伺服器,位址:%s:%s adb_dialog.connect_fail=無法連接至 ADB 伺服器。 adb_dialog.disconnected=已與 ADB 伺服器斷開連線。 adb_dialog.start_okay=ADB 伺服器已於此連接埠啟動:%s。 adb_dialog.start_fail=無法於此連接埠啟動 ADB 伺服器:%s! adb_dialog.forward_fail=因某些原因而無法轉發。 adb_dialog.being_debugged_msg=此執行程序貌似已在偵錯,要繼續嗎? adb_dialog.unknown_android_ver=無法取得 Android 發行版本,是否要使用預設版本 Android 8? adb_dialog.being_debugged_title=正在由他人偵錯。 adb_dialog.init_dbg_fail=無法初始化偵錯工具。 adb_dialog.msg_read_mani_fail=無法解碼 AndroidManifest.xml adb_dialog.no_devices=無法找到任何可以啟動應用程式的裝置。 adb_dialog.restart_while_debugging_title=偵錯時重新啟動 adb_dialog.restart_while_debugging_msg=您正在為應用程式偵錯,您確定要重新啟動工作階段嗎? adb_dialog.starting_debugger=正在啟動偵錯工具... action.variant=%s (variant) action_category.menu_toolbar=選單 / 工具列 #action_category.hex_viewer=View / Hex Viewer action_category.code_area=程式碼區域 action_category.plugin_script=外掛程式腳本 #hex_viewer.show_inspector=Show Inspector #hex_viewer.change_encoding=Change Encoding #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find #graph_viewer.long_names=Show full names #graph_viewer.overrides=Show overrides #graph_viewer.callee_depth=Down depth #graph_viewer.caller_depth=Up depth #graph_viewer.default_error=Failed to view graph #graph_viewer.file_not_found_error=Failed to load graph file #graph_viewer.image_too_large=Failed to render graph: graph too large #graph_viewer.image_too_small=Failed to render graph: graph too small #graph_viewer.file_failure=Error in File Operation #graph_viewer.save_graph=Save graph #graph_viewer.default_title=Graph Viewer #graph_viewer.method_graph.title=Methods Graph #graph_viewer.call_graph.title=Call Graph #graph_viewer.inheritance_graph.title=Inheritance Graph #graph_viewer.cfg.title=Control Flow Graph ================================================ FILE: jadx-gui/src/test/java/jadx/gui/TestI18n.java ================================================ package jadx.gui; import java.io.IOException; import java.io.Reader; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import jadx.gui.utils.LangLocale; import jadx.gui.utils.NLS; import static java.nio.file.Paths.get; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; public class TestI18n { private static final String DEFAULT_LANG_FILE = "Messages_en_US.properties"; private static Path i18nPath; private static Path refPath; private static Path guiJavaPath; @BeforeAll public static void init() { i18nPath = get("src/main/resources/i18n"); assertThat(i18nPath).exists(); refPath = i18nPath.resolve(DEFAULT_LANG_FILE); assertThat(refPath).exists(); guiJavaPath = get("src/main/java"); assertThat(guiJavaPath).exists(); } @Test public void verifyLocales() { for (LangLocale lang : NLS.getLangLocales()) { Locale locale = lang.get(); System.out.println("Language: " + locale.getLanguage() + " - " + locale.getDisplayLanguage() + ", country: " + locale.getCountry() + " - " + locale.getDisplayCountry() + ", language tag: " + locale.toLanguageTag()); } } @Test public void filesExactlyMatch() throws IOException { List reference = Files.readAllLines(refPath) .stream() .map(TestI18n::getPrefix) .collect(Collectors.toList()); try (Stream list = Files.list(i18nPath)) { list.filter(p -> !p.equals(refPath)) .forEach(path -> compareToReference(path, reference)); } } /** * Extract prefix: 'key=' */ private static String getPrefix(String line) { if (line.isBlank()) { return ""; } int sep = line.indexOf('='); if (sep == -1) { return line; } if (line.startsWith("#")) { fail(DEFAULT_LANG_FILE + " shouldn't contain commented values: " + line); } return line.substring(0, sep + 1); } private void compareToReference(Path path, List reference) { try { List lines = Files.readAllLines(path); for (int i = 0; i < reference.size(); i++) { String prefix = reference.get(i); if (prefix.isEmpty()) { continue; } if (i >= lines.size()) { fail("File '" + path.getFileName() + "' contains unexpected lines at end"); } String line = lines.get(i); if (!trimComment(line).startsWith(prefix)) { failLine(path, i + 1); } if (line.startsWith("#")) { int sep = line.indexOf('='); if (line.substring(sep + 1).isBlank()) { fail("File '" + path.getFileName() + "' has empty ref text at line " + (i + 1) + ": " + line); } } } if (lines.size() != reference.size()) { failLine(path, reference.size()); } } catch (IOException e) { fail("Process error ", e); } } private static String trimComment(String string) { return string.startsWith("#") ? string.substring(1) : string; } private void failLine(Path path, int line) { fail("I18n file: " + path.getFileName() + " and " + DEFAULT_LANG_FILE + " differ in line " + line); } /** * Temporary solution to allow use I18N strings in plugins until proper API implemented */ private static final List EXCLUDED_KEYS = Arrays.asList( // keys from `jadx-script-kotlin` "file.save", "tree.input_scripts", "popup.new_script", "popup.add_scripts", "script.log", "script.format", "script.check"); @Test public void keyIsUsed() throws IOException { Properties properties = new Properties(); try (Reader reader = Files.newBufferedReader(i18nPath.resolve(DEFAULT_LANG_FILE))) { properties.load(reader); } EXCLUDED_KEYS.forEach(properties.keySet()::remove); Set keys = new HashSet<>(); for (Object key : properties.keySet()) { keys.add("\"" + key + '"'); } try (Stream walk = Files.walk(guiJavaPath)) { walk.filter(Files::isRegularFile).forEach(p -> { try { for (String line : Files.readAllLines(p)) { keys.removeIf(line::contains); } } catch (Exception e) { throw new RuntimeException(e); } }); } assertThat(keys).as("keys not used").isEmpty(); } } ================================================ FILE: jadx-gui/src/test/java/jadx/gui/device/debugger/smali/DbgSmaliTest.java ================================================ package jadx.gui.device.debugger.smali; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static org.assertj.core.api.Assertions.assertThat; class DbgSmaliTest extends SmaliTest { private static final Logger LOG = LoggerFactory.getLogger(DbgSmaliTest.class); @BeforeEach public void initProject() { setCurrentProject("jadx-gui"); } @Test void testSwitch() { disableCompilation(); ClassNode cls = getClassNodeFromSmali("switch", "SwitchTest"); Smali disasm = Smali.disassemble(cls); LOG.debug("{}", disasm.getCode()); } @Test void testParams() { disableCompilation(); ClassNode cls = getClassNodeFromSmali("params", "ParamsTest"); Smali disasm = Smali.disassemble(cls); String code = disasm.getCode(); LOG.debug("{}", code); assertThat(code) .doesNotContain("Failed to write method") .doesNotContain(".param p1") .contains(".local p1, \"arg0\":Landroid/widget/AdapterView;, \"Landroid/widget/AdapterView<*>;\""); } } ================================================ FILE: jadx-gui/src/test/java/jadx/gui/ui/codearea/ConvertNumberActionTest.java ================================================ package jadx.gui.ui.codearea; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class ConvertNumberActionTest { @Test public void nonNumeric() { assertThat(ConvertNumberAction.getConversionsFromWord("non-numeric")).isNullOrEmpty(); assertThat(ConvertNumberAction.getConversionsFromWord("0xnon-numeric")).isNullOrEmpty(); assertThat(ConvertNumberAction.getConversionsFromWord("non-numericL")).isNullOrEmpty(); assertThat(ConvertNumberAction.getConversionsFromWord("-non-numeric")).isNullOrEmpty(); assertThat(ConvertNumberAction.getConversionsFromWord("ABCD")).isNullOrEmpty(); } @Test public void simpleDecimalToHex() { List expected = new ArrayList(); expected.add("0x7b"); expected.add("0b01111011"); expected.add("'{'"); List result = ConvertNumberAction.getConversionsFromWord("123"); assertThat(result).isNotEmpty(); expected.removeAll(result); assertThat(expected).isEmpty(); } @Test public void negativeDecimalToHex() { List expected = new ArrayList(); expected.add("0xffffff85"); expected.add("0b11111111111111111111111110000101"); List result = ConvertNumberAction.getConversionsFromWord("-123"); assertThat(result).isNotEmpty(); expected.removeAll(result); assertThat(expected).isEmpty(); } @Test public void negativeLongDecimalToHex() { List expected = new ArrayList(); expected.add("0xFFFFFFE8B7891800".toLowerCase()); expected.add("0b1111111111111111111111111110100010110111100010010001100000000000"); List result = ConvertNumberAction.getConversionsFromWord("-100000000000"); assertThat(result).isNotEmpty(); expected.removeAll(result); assertThat(expected).isEmpty(); } @Test public void simpleHexToDecimal() { List expected = new ArrayList(); expected.add("123"); expected.add("0b01111011"); expected.add("'{'"); List result = ConvertNumberAction.getConversionsFromWord("0x7b"); assertThat(result).isNotEmpty(); expected.removeAll(result); assertThat(expected).isEmpty(); } @Test public void zeroToHex() { List expected = new ArrayList(); expected.add("0x0"); expected.add("0b00000000"); List result = ConvertNumberAction.getConversionsFromWord(Integer.toString(0)); assertThat(result).isNotEmpty(); expected.removeAll(result); assertThat(expected).isEmpty(); } @Test public void minIntToHex() { List expected = new ArrayList(); expected.add("0x80000000"); expected.add("0b10000000000000000000000000000000"); List result = ConvertNumberAction.getConversionsFromWord(Integer.toString(Integer.MIN_VALUE)); assertThat(result).isNotEmpty(); expected.removeAll(result); assertThat(expected).isEmpty(); } @Test public void maxIntToHex() { List expected = new ArrayList(); expected.add("0x7fffffff"); expected.add("0b01111111111111111111111111111111"); List result = ConvertNumberAction.getConversionsFromWord(Integer.toString(Integer.MAX_VALUE)); assertThat(result).isNotEmpty(); expected.removeAll(result); assertThat(expected).isEmpty(); } @Test public void minLongToHex() { List expected = new ArrayList(); expected.add("0x8000000000000000"); expected.add("0b1000000000000000000000000000000000000000000000000000000000000000"); List result = ConvertNumberAction.getConversionsFromWord(Long.toString(Long.MIN_VALUE)); assertThat(result).isNotEmpty(); expected.removeAll(result); assertThat(expected).isEmpty(); } @Test public void maxLongToHex() { List expected = new ArrayList(); expected.add("0x7fffffffffffffff"); expected.add("0b0111111111111111111111111111111111111111111111111111111111111111"); List result = ConvertNumberAction.getConversionsFromWord(Long.toString(Long.MAX_VALUE)); assertThat(result).isNotEmpty(); expected.removeAll(result); assertThat(expected).isEmpty(); } @Test public void simpleLongSuffix() { List expected = new ArrayList(); expected.add("0x7b"); expected.add("0b01111011"); expected.add("'{'"); List result = ConvertNumberAction.getConversionsFromWord("123L"); assertThat(result).isNotEmpty(); expected.removeAll(result); assertThat(expected).isEmpty(); } @Test public void binaryPadding() { assertThat(ConvertNumberAction.getConversionsFromWord("0")).containsOnlyOnce("0b00000000"); assertThat(ConvertNumberAction.getConversionsFromWord("1")).containsOnlyOnce("0b00000001"); assertThat(ConvertNumberAction.getConversionsFromWord("127")).containsOnlyOnce("0b01111111"); assertThat(ConvertNumberAction.getConversionsFromWord("0xff")).containsOnlyOnce("0b11111111"); assertThat(ConvertNumberAction.getConversionsFromWord("0x7fff")).containsOnlyOnce("0b0111111111111111"); assertThat(ConvertNumberAction.getConversionsFromWord("0xffff")).containsOnlyOnce("0b1111111111111111"); assertThat(ConvertNumberAction.getConversionsFromWord("0x10000")).containsOnlyOnce("0b000000010000000000000000"); assertThat(ConvertNumberAction.getConversionsFromWord("0xffffffff")).containsOnlyOnce("0b11111111111111111111111111111111"); assertThat(ConvertNumberAction.getConversionsFromWord("0xffffffffffff")) .containsOnlyOnce("0b111111111111111111111111111111111111111111111111"); assertThat(ConvertNumberAction.getConversionsFromWord("0x7fffffffffff")) .containsOnlyOnce("0b011111111111111111111111111111111111111111111111"); assertThat(ConvertNumberAction.getConversionsFromWord("0x7fffffffffffffff")) .containsOnlyOnce("0b0111111111111111111111111111111111111111111111111111111111111111"); } @Test public void printableAscii() { for (int i = 32; i < 127; i++) { String printed = String.format("'%c'", i); assertThat(ConvertNumberAction.getConversionsFromWord(Integer.toString(i))).containsOnlyOnce(printed); } } } ================================================ FILE: jadx-gui/src/test/java/jadx/gui/update/TestJadxUpdate.kt ================================================ package jadx.gui.update import jadx.gui.settings.JadxUpdateChannel import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test /** * Test updates fetch. * All tests disabled because of network requests, run manually on JadxUpdate changes */ @Disabled("Network requests") class TestJadxUpdate { @Test fun testStableCheck() { JadxUpdate("1.5.0").checkForNewRelease(JadxUpdateChannel.STABLE)?.let { println("Latest release: $it") } } @Test fun testUnstableCheck() { JadxUpdate("r2000").checkForNewRelease(JadxUpdateChannel.UNSTABLE)?.let { println("Latest unstable: $it") } } } ================================================ FILE: jadx-gui/src/test/java/jadx/gui/utils/CertificateManagerTest.java ================================================ package jadx.gui.utils; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.security.cert.Certificate; import java.util.Collection; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class CertificateManagerTest { private static final String CERTIFICATE_TEST_DIR = "certificate-test/"; private static final String DSA = "CERT.DSA"; private static final String RSA = "CERT.RSA"; private static final String EMPTY = "EMPTY.txt"; private String emptyPath; private CertificateManager certificateManagerRSA; private CertificateManager certificateManagerDSA; private CertificateManager getCertificateManger(String resName) { String certPath = getResourcePath(resName); try (InputStream in = new FileInputStream(certPath)) { Collection certificates = CertificateManager.readCertificates(in); Certificate cert = certificates.iterator().next(); return new CertificateManager(cert); } catch (Exception e) { throw new RuntimeException("Failed to create CertificateManager"); } } @BeforeEach public void setUp() { emptyPath = getResourcePath(EMPTY); certificateManagerRSA = getCertificateManger(RSA); certificateManagerDSA = getCertificateManger(DSA); } @Test public void decodeNotCertificateFile() throws IOException { try (InputStream in = new FileInputStream(emptyPath)) { String result = CertificateManager.decode(in); assertThat(result).isEmpty(); } } @Test public void decodeRSAKeyHeader() { assertThat(certificateManagerRSA.generateHeader()) .contains("X.509") .contains("0x4bd68052") .contains("CN=test cert, OU=test unit, O=OOO TestOrg, L=St.Peterburg, ST=Russia, C=123456"); } @Test public void decodeDSAKeyHeader() { assertThat(certificateManagerDSA.generateHeader()) .contains("X.509") .contains("0x16420ba2") .contains("O=\"UJMRFVV CN=EDCVBGT C=TG\""); } @Test public void decodeRSAKeySignature() { assertThat(certificateManagerRSA.generateSignature()) .contains("SHA256withRSA") .contains("1.2.840.113549.1.1.11"); } @Test public void decodeDSAKeySignature() { assertThat(certificateManagerDSA.generateSignature()) .contains("SHA1withDSA") .contains("1.2.840.10040.4.3"); } @Test public void decodeRSAFingerprint() { assertThat(certificateManagerRSA.generateFingerprint()) .contains("61 18 0A 71 3F C9 55 16 4E 04 E3 C5 45 08 D9 11") .contains("A0 6E A6 06 DB 2C 6F 3A 16 56 7F 75 97 7B AE 85 C2 13 09 37") .contains("12 53 E8 BB C8 AA 27 A8 49 9B F8 0D 6E 68 CE 32 35 50 DE 55 A7 E7 8C 29 51 00 96 D7 56 F4 54 44"); } @Test public void decodeDSAFingerprint() { assertThat(certificateManagerDSA.generateFingerprint()) .contains("D9 06 A6 2D 1F 79 8C 9D A6 EF 40 C7 2E C2 EA 0B") .contains("18 E9 9C D4 A1 40 8F 63 FA EC 2E 62 A0 F2 AE B7 3F C3 C2 04") .contains("74 F9 48 64 EE AC 92 26 53 2C 7A 0E 55 BE 5E D8 2F A7 D9 A9 99 F5 D5 21 2C 51 21 C4 31 AD 73 40"); } @Test public void decodeRSAPubKey() { assertThat(certificateManagerRSA.generatePublicKey()) .contains("RSA") .contains("65537") .contains("1681953129031804462554643735709908030601939275292568895111488068832920121318010916" + "889038430576806710152191447376363866950356097752126932858298006033288814768019331823126004318941179" + "4465899645633586173494259691101582064441956032924396850221679489313043628562082670183392670094163371" + "8586841184804093747497905514737738452134274762361473284344272721776230189352829291523087538543142199" + "8761760403746876947208990209024335828599173964217021197086277312193991177728010193707324300633538463" + "6193260583579409760790138329893534549366882523130765297472656435892831796545149793228897111760122091" + "442123535919361963075454640516520743"); } @Test public void decodeDSAPubKey() { assertThat(certificateManagerDSA.generatePublicKey()) .contains("DSA") .contains("193233676050581546825633012823454532222793121048898990016982096262547255815113" + "7546996381246109049596383861577383286736433045701055397423798599190480095839416942148507037843474" + "67923797088055637932532829952742936211625049432875384559446523443782422268975073691469424116922209" + "22477368782490423187845815262510366"); } private String getResourcePath(String resName) { URL resource = getClass().getClassLoader().getResource(CERTIFICATE_TEST_DIR + resName); if (resource == null) { throw new RuntimeException("Resource not found: " + resName); } return resource.getPath(); } } ================================================ FILE: jadx-gui/src/test/java/jadx/gui/utils/JumpManagerTest.java ================================================ package jadx.gui.utils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import jadx.gui.treemodel.TextNode; import static org.assertj.core.api.Assertions.assertThat; class JumpManagerTest { private JumpManager jm; @BeforeEach public void setup() { jm = new JumpManager(); } @Test public void testEmptyHistory() { assertThat(jm.getPrev()).isNull(); assertThat(jm.getNext()).isNull(); } @Test public void testEmptyHistory2() { assertThat(jm.getPrev()).isNull(); assertThat(jm.getNext()).isNull(); assertThat(jm.getPrev()).isNull(); assertThat(jm.getNext()).isNull(); assertThat(jm.getPrev()).isNull(); } @Test public void testOneElement() { jm.addPosition(makeJumpPos()); assertThat(jm.getPrev()).isNull(); assertThat(jm.getNext()).isNull(); } @Test public void testTwoElements() { JumpPosition pos1 = makeJumpPos(); jm.addPosition(pos1); JumpPosition pos2 = makeJumpPos(); jm.addPosition(pos2); assertThat(jm.getPrev()).isSameAs(pos1); assertThat(jm.getPrev()).isNull(); assertThat(jm.getNext()).isSameAs(pos2); assertThat(jm.getNext()).isNull(); } @Test public void testNavigation() { JumpPosition pos1 = makeJumpPos(); jm.addPosition(pos1); // 1@ JumpPosition pos2 = makeJumpPos(); jm.addPosition(pos2); // 1 - 2@ assertThat(jm.getPrev()).isSameAs(pos1); // 1@ - 2 JumpPosition pos3 = makeJumpPos(); jm.addPosition(pos3); // 1 - 3@ assertThat(jm.getNext()).isNull(); assertThat(jm.getPrev()).isSameAs(pos1); // 1@ - 3 assertThat(jm.getNext()).isSameAs(pos3); } @Test public void testNavigation2() { JumpPosition pos1 = makeJumpPos(); jm.addPosition(pos1); // 1@ JumpPosition pos2 = makeJumpPos(); jm.addPosition(pos2); // 1 - 2@ JumpPosition pos3 = makeJumpPos(); jm.addPosition(pos3); // 1 - 2 - 3@ JumpPosition pos4 = makeJumpPos(); jm.addPosition(pos4); // 1 - 2 - 3 - 4@ assertThat(jm.getPrev()).isSameAs(pos3); // 1 - 2 - 3@ - 4 assertThat(jm.getPrev()).isSameAs(pos2); // 1 - 2@ - 3 - 4 JumpPosition pos5 = makeJumpPos(); jm.addPosition(pos5); // 1 - 2 - 5@ assertThat(jm.getNext()).isNull(); assertThat(jm.getNext()).isNull(); assertThat(jm.getPrev()).isSameAs(pos2); // 1 - 2@ - 5 assertThat(jm.getPrev()).isSameAs(pos1); // 1@ - 2 - 5 assertThat(jm.getPrev()).isNull(); assertThat(jm.getNext()).isSameAs(pos2); // 1 - 2@ - 5 assertThat(jm.getNext()).isSameAs(pos5); // 1 - 2 - 5@ assertThat(jm.getNext()).isNull(); } @Test public void addSame() { JumpPosition pos = makeJumpPos(); jm.addPosition(pos); jm.addPosition(pos); assertThat(jm.getPrev()).isNull(); assertThat(jm.getNext()).isNull(); } private JumpPosition makeJumpPos() { return new JumpPosition(new TextNode(""), 0); } } ================================================ FILE: jadx-gui/src/test/java/jadx/gui/utils/cache/code/DiskCodeCacheTest.java ================================================ package jadx.gui.utils.cache.code; import java.io.IOException; import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.impl.NoOpCodeCache; import jadx.core.dex.nodes.ClassNode; import jadx.gui.cache.code.disk.DiskCodeCache; import jadx.tests.api.IntegrationTest; import static org.assertj.core.api.Assertions.assertThat; class DiskCodeCacheTest extends IntegrationTest { private static final Logger LOG = LoggerFactory.getLogger(DiskCodeCacheTest.class); @TempDir public Path tempDir; @Test public void test() throws IOException { disableCompilation(); getArgs().setCodeCache(NoOpCodeCache.INSTANCE); ClassNode clsNode = getClassNode(DiskCodeCacheTest.class); ICodeInfo codeInfo = clsNode.getCode(); DiskCodeCache cache = new DiskCodeCache(clsNode.root(), tempDir); String clsKey = clsNode.getFullName(); cache.add(clsKey, codeInfo); ICodeInfo readCodeInfo = cache.get(clsKey); assertThat(readCodeInfo).isNotNull(); assertThat(readCodeInfo.getCodeStr()).isEqualTo(codeInfo.getCodeStr()); assertThat(readCodeInfo.getCodeMetadata().getLineMapping()).isEqualTo(codeInfo.getCodeMetadata().getLineMapping()); LOG.info("Disk code annotations: {}", readCodeInfo.getCodeMetadata().getAsMap()); assertThat(readCodeInfo.getCodeMetadata().getAsMap()).hasSameSizeAs(codeInfo.getCodeMetadata().getAsMap()); cache.close(); } } ================================================ FILE: jadx-gui/src/test/java/jadx/gui/utils/cache/code/disk/adapters/DataAdapterHelperTest.java ================================================ package jadx.gui.utils.cache.code.disk.adapters; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import org.junit.jupiter.api.Test; import jadx.gui.cache.code.disk.adapters.DataAdapterHelper; import static org.assertj.core.api.Assertions.assertThat; class DataAdapterHelperTest { @Test void uvInt() throws IOException { checkUVIntFor(0); checkUVIntFor(7); checkUVIntFor(0x7f); checkUVIntFor(0x80); checkUVIntFor(0x256); checkUVIntFor(Byte.MAX_VALUE); checkUVIntFor(Short.MAX_VALUE); checkUVIntFor(Integer.MAX_VALUE); } private void checkUVIntFor(int val) throws IOException { assertThat(writeReadUVInt(val)).isEqualTo(val); } private int writeReadUVInt(int val) throws IOException { ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(byteOut); DataAdapterHelper.writeUVInt(out, val); DataInput in = new DataInputStream(new ByteArrayInputStream(byteOut.toByteArray())); return DataAdapterHelper.readUVInt(in); } } ================================================ FILE: jadx-gui/src/test/java/jadx/gui/utils/pkgs/TestJRenamePackage.java ================================================ package jadx.gui.utils.pkgs; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class TestJRenamePackage { @Test void isValidName() { valid("foo"); valid("foo.bar"); valid(".bar"); invalid(""); invalid("0foo"); invalid("foo."); invalid("do"); invalid("foo.if"); invalid("foo.if.bar"); } private void valid(String name) { assertThat(JRenamePackage.isValidPackageName(name)) .as("expect valid: %s", name) .isTrue(); } private void invalid(String name) { assertThat(JRenamePackage.isValidPackageName(name)) .as("expect invalid: %s", name) .isFalse(); } } ================================================ FILE: jadx-gui/src/test/resources/certificate-test/EMPTY.txt ================================================ test ================================================ FILE: jadx-gui/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss} %-5level - %msg%n ================================================ FILE: jadx-gui/src/test/smali/params.smali ================================================ .class LParamsTest; .super Ljava/lang/Object; .method public test(Landroid/widget/AdapterView;Landroid/view/View;IJ)V .registers 10 .param p2, "arg1" # Landroid/view/View; .param p3, "arg2" # I .param p4, "arg3" # J .annotation system Ldalvik/annotation/Signature; value = { "(", "Landroid/widget/AdapterView", "<*>;", "Landroid/view/View;", "IJ)V" } .end annotation .prologue .line 69 .local p1, "arg0":Landroid/widget/AdapterView;, "Landroid/widget/AdapterView<*>;" iget-object v2, p0, LParamsTest;->this$0:Ltest/ColorListActivity; .line 72 iget-object v2, v2, Ltest/ColorListActivity;->mSortedColorList:[Ljava/lang/String; .line 75 aget-object v0, v2, p3 .line 80 .local v0, "colorString":Ljava/lang/String; new-instance v1, Landroid/content/Intent; .line 83 iget-object v2, p0, LParamsTest;->this$0:Ltest/ColorListActivity; .line 86 const-class v3, Ltest/ColorItemActivity; .line 89 invoke-direct {v1, v2, v3}, Landroid/content/Intent;->(Landroid/content/Context;Ljava/lang/Class;)V .line 94 .local v1, "intent":Landroid/content/Intent; const-string v2, "colorString" .line 97 invoke-virtual {v1, v2, v0}, Landroid/content/Intent;->putExtra(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent; .line 101 iget-object v2, p0, LParamsTest;->this$0:Ltest/ColorListActivity; .line 104 invoke-virtual {v2, v1}, Ltest/ColorListActivity;->startActivity(Landroid/content/Intent;)V .line 108 return-void .end method ================================================ FILE: jadx-gui/src/test/smali/switch.smali ================================================ .class public final LSwitchTest; .super Ljava/lang/Object; .field public static final synthetic a:LSwitchTest; .field public static final synthetic b:LSwitchTest; .field private final synthetic c:I .method public final test(Ljava/lang/Runnable;)Ljava/lang/Thread; .registers 4 const v0, 0xa const v1, 0xa add-int v0, v0, v1 rem-int v0, v0, v1 if-gtz v0, :cond_f goto/32 :goto_d2 :cond_f :goto_f goto/32 :goto_bf :goto_18 const-string v1, "A" goto/32 :goto_68 :goto_26 const-string v1, "B" goto/32 :goto_7c :goto_33 return-object v0 :pswitch_38 goto/32 :goto_4e :goto_41 new-instance v0, Labo; goto/32 :goto_84 :goto_4e new-instance v0, Lwf; goto/32 :goto_ab :goto_5b new-instance v0, Ljava/lang/Thread; goto/32 :goto_18 :goto_68 invoke-direct {v0, p1, v1}, Ljava/lang/Thread;->(Ljava/lang/Runnable;Ljava/lang/String;)V goto/32 :goto_71 :goto_71 return-object v0 :pswitch_75 goto/32 :goto_b3 :goto_7c invoke-direct {v0, p1, v1}, Ljava/lang/Thread;->(Ljava/lang/Runnable;Ljava/lang/String;)V goto/32 :goto_33 :goto_84 invoke-direct {v0, p1}, Labo;->(Ljava/lang/Runnable;)V goto/32 :goto_90 :goto_90 return-object v0 :pswitch_data_96 .packed-switch 0x0 :pswitch_a3 :pswitch_38 :pswitch_75 .end packed-switch :goto_a0 return-object v0 :pswitch_a3 goto/32 :goto_41 :goto_ab invoke-direct {v0, p1}, Lwf;->(Ljava/lang/Runnable;)V goto/32 :goto_a0 :goto_b3 new-instance v0, Ljava/lang/Thread; goto/32 :goto_26 :goto_bf iget v0, p0, LSwitchTest;->c:I packed-switch v0, :pswitch_data_96 goto/32 :goto_5b :goto_d2 goto/32 :goto_f .end method ================================================ FILE: jadx-plugins/jadx-aab-input/build.gradle.kts ================================================ plugins { id("jadx-library") } dependencies { compileOnly(project(":jadx-core")) implementation("com.android.tools.build:aapt2-proto:8.13.2-14304508") implementation("com.google.protobuf:protobuf-java") { version { require("3.25.3") // version 4 conflict with `aapt2-proto` } } implementation("com.android.tools.build:bundletool:1.18.3") { // All of this is unnecessary for parsing BundleConfig.pb except for protobuf exclude(group = "com.android.tools.build") exclude(group = "com.google.protobuf") exclude(group = "com.google.guava") exclude(group = "org.bitbucket.b_c") exclude(group = "org.slf4j") exclude(group = "com.google.auto.value") exclude(group = "com.google.dagger") exclude(group = "com.google.errorprone") } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/AabInputPlugin.java ================================================ package jadx.plugins.input.aab; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.resources.IResourcesLoader; import jadx.plugins.input.aab.factories.ProtoAppDependenciesResContainerFactory; import jadx.plugins.input.aab.factories.ProtoAssetsConfigResContainerFactory; import jadx.plugins.input.aab.factories.ProtoBundleConfigResContainerFactory; import jadx.plugins.input.aab.factories.ProtoNativeConfigResContainerFactory; import jadx.plugins.input.aab.factories.ProtoTableResContainerFactory; import jadx.plugins.input.aab.factories.ProtoXmlResContainerFactory; public class AabInputPlugin implements JadxPlugin { public static final String PLUGIN_ID = "aab-input"; @Override public JadxPluginInfo getPluginInfo() { return new JadxPluginInfo( PLUGIN_ID, ".AAB Input", "Loads .AAB files."); } @Override public synchronized void init(JadxPluginContext context) { IResourcesLoader resourcesLoader = context.getResourcesLoader(); ResTableProtoParserProvider tableParserProvider = new ResTableProtoParserProvider(); resourcesLoader.addResTableParserProvider(tableParserProvider); resourcesLoader.addResContainerFactory(new ProtoTableResContainerFactory(tableParserProvider)); resourcesLoader.addResContainerFactory(new ProtoXmlResContainerFactory()); resourcesLoader.addResContainerFactory(new ProtoBundleConfigResContainerFactory()); resourcesLoader.addResContainerFactory(new ProtoAssetsConfigResContainerFactory()); resourcesLoader.addResContainerFactory(new ProtoNativeConfigResContainerFactory()); resourcesLoader.addResContainerFactory(new ProtoAppDependenciesResContainerFactory()); } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/ResTableProtoParserProvider.java ================================================ package jadx.plugins.input.aab; import org.jetbrains.annotations.Nullable; import jadx.api.ResourceFile; import jadx.api.plugins.resources.IResTableParserProvider; import jadx.core.dex.nodes.RootNode; import jadx.core.xmlgen.IResTableParser; import jadx.plugins.input.aab.parsers.ResTableProtoParser; public class ResTableProtoParserProvider implements IResTableParserProvider { private RootNode root; @Override public void init(RootNode root) { this.root = root; } @Override public @Nullable IResTableParser getParser(ResourceFile resFile) { String fileName = resFile.getOriginalName(); if (!fileName.endsWith("resources.pb")) { return null; } return new ResTableProtoParser(root); } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/factories/ProtoAppDependenciesResContainerFactory.java ================================================ package jadx.plugins.input.aab.factories; import java.io.IOException; import java.io.InputStream; import org.jetbrains.annotations.Nullable; import com.android.bundle.AppDependenciesOuterClass; import jadx.api.ICodeInfo; import jadx.api.ResourceFile; import jadx.api.impl.SimpleCodeInfo; import jadx.api.plugins.resources.IResContainerFactory; import jadx.core.xmlgen.ResContainer; public class ProtoAppDependenciesResContainerFactory implements IResContainerFactory { @Override public @Nullable ResContainer create(ResourceFile resFile, InputStream inputStream) throws IOException { if (!resFile.getOriginalName().endsWith("BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb")) { return null; } AppDependenciesOuterClass.AppDependencies appDependencies = AppDependenciesOuterClass.AppDependencies.parseFrom(inputStream); ICodeInfo content = new SimpleCodeInfo(appDependencies.toString()); return ResContainer.textResource(resFile.getDeobfName(), content); } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/factories/ProtoAssetsConfigResContainerFactory.java ================================================ package jadx.plugins.input.aab.factories; import java.io.IOException; import java.io.InputStream; import org.jetbrains.annotations.Nullable; import com.android.bundle.Files; import jadx.api.ICodeInfo; import jadx.api.ResourceFile; import jadx.api.impl.SimpleCodeInfo; import jadx.api.plugins.resources.IResContainerFactory; import jadx.core.xmlgen.ResContainer; public class ProtoAssetsConfigResContainerFactory implements IResContainerFactory { @Override public @Nullable ResContainer create(ResourceFile resFile, InputStream inputStream) throws IOException { if (!resFile.getOriginalName().endsWith("assets.pb")) { return null; } Files.Assets assetsConfig = Files.Assets.parseFrom(inputStream); ICodeInfo content = new SimpleCodeInfo(assetsConfig.toString()); return ResContainer.textResource(resFile.getDeobfName(), content); } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/factories/ProtoBundleConfigResContainerFactory.java ================================================ package jadx.plugins.input.aab.factories; import java.io.IOException; import java.io.InputStream; import org.jetbrains.annotations.Nullable; import com.android.bundle.Config.BundleConfig; import jadx.api.ICodeInfo; import jadx.api.ResourceFile; import jadx.api.impl.SimpleCodeInfo; import jadx.api.plugins.resources.IResContainerFactory; import jadx.core.xmlgen.ResContainer; public class ProtoBundleConfigResContainerFactory implements IResContainerFactory { @Override public @Nullable ResContainer create(ResourceFile resFile, InputStream inputStream) throws IOException { if (!resFile.getOriginalName().endsWith("BundleConfig.pb")) { return null; } BundleConfig bundleConfig = BundleConfig.parseFrom(inputStream); ICodeInfo content = new SimpleCodeInfo(bundleConfig.toString()); return ResContainer.textResource(resFile.getDeobfName(), content); } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/factories/ProtoNativeConfigResContainerFactory.java ================================================ package jadx.plugins.input.aab.factories; import java.io.IOException; import java.io.InputStream; import org.jetbrains.annotations.Nullable; import com.android.bundle.Files; import jadx.api.ICodeInfo; import jadx.api.ResourceFile; import jadx.api.impl.SimpleCodeInfo; import jadx.api.plugins.resources.IResContainerFactory; import jadx.core.xmlgen.ResContainer; public class ProtoNativeConfigResContainerFactory implements IResContainerFactory { @Override public @Nullable ResContainer create(ResourceFile resFile, InputStream inputStream) throws IOException { if (!resFile.getOriginalName().endsWith("native.pb")) { return null; } Files.NativeLibraries nativeConfig = Files.NativeLibraries.parseFrom(inputStream); ICodeInfo content = new SimpleCodeInfo(nativeConfig.toString()); return ResContainer.textResource(resFile.getDeobfName(), content); } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/factories/ProtoTableResContainerFactory.java ================================================ package jadx.plugins.input.aab.factories; import java.io.IOException; import java.io.InputStream; import org.jetbrains.annotations.Nullable; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.api.plugins.resources.IResContainerFactory; import jadx.api.plugins.resources.IResTableParserProvider; import jadx.core.xmlgen.IResTableParser; import jadx.core.xmlgen.ResContainer; public class ProtoTableResContainerFactory implements IResContainerFactory { private final IResTableParserProvider provider; public ProtoTableResContainerFactory(IResTableParserProvider provider) { this.provider = provider; } @Override public @Nullable ResContainer create(ResourceFile resFile, InputStream inputStream) throws IOException { if (!resFile.getOriginalName().endsWith(".pb") || resFile.getType() != ResourceType.ARSC) { return null; } IResTableParser parser = provider.getParser(resFile); if (parser == null) { return null; } parser.decode(inputStream); return parser.decodeFiles(); } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/factories/ProtoXmlResContainerFactory.java ================================================ package jadx.plugins.input.aab.factories; import java.io.IOException; import java.io.InputStream; import org.jetbrains.annotations.Nullable; import jadx.api.ICodeInfo; import jadx.api.ResourceFile; import jadx.api.ResourceType; import jadx.api.plugins.resources.IResContainerFactory; import jadx.core.dex.nodes.RootNode; import jadx.core.xmlgen.ResContainer; import jadx.plugins.input.aab.parsers.ResXmlProtoParser; import jadx.zip.IZipEntry; public class ProtoXmlResContainerFactory implements IResContainerFactory { private ResXmlProtoParser xmlParser; @Override public void init(RootNode root) { xmlParser = new ResXmlProtoParser(root); } @Override public @Nullable ResContainer create(ResourceFile resFile, InputStream inputStream) throws IOException { ResourceType type = resFile.getType(); if (type != ResourceType.XML && type != ResourceType.MANIFEST) { return null; } IZipEntry zipEntry = resFile.getZipEntry(); if (zipEntry == null) { return null; } boolean isFromAab = zipEntry.getZipFile().getPath().toLowerCase().endsWith(".aab"); if (!isFromAab) { return null; } ICodeInfo content = xmlParser.parse(inputStream); return ResContainer.textResource(resFile.getDeobfName(), content); } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/parsers/CommonProtoParser.java ================================================ package jadx.plugins.input.aab.parsers; import java.util.ArrayList; import java.util.List; import com.android.aapt.ConfigurationOuterClass; import com.android.aapt.Resources; import jadx.core.xmlgen.ParserConstants; import jadx.core.xmlgen.XmlGenUtils; import jadx.core.xmlgen.entry.EntryConfig; import jadx.core.xmlgen.entry.ProtoValue; public class CommonProtoParser extends ParserConstants { protected ProtoValue parse(Resources.Style s) { List namedValues = new ArrayList<>(s.getEntryCount()); String parent = s.getParent().getName(); if (parent.isEmpty()) { parent = null; } else { parent = '@' + parent; } for (int i = 0; i < s.getEntryCount(); i++) { Resources.Style.Entry entry = s.getEntry(i); String name = entry.getKey().getName(); String value = parse(entry.getItem()); namedValues.add(new ProtoValue(value).setName(name)); } return new ProtoValue().setNamedValues(namedValues).setParent(parent); } protected ProtoValue parse(Resources.Styleable s) { List namedValues = new ArrayList<>(s.getEntryCount()); for (int i = 0; i < s.getEntryCount(); i++) { Resources.Styleable.Entry e = s.getEntry(i); namedValues.add(new ProtoValue('@' + e.getAttr().getName())); } return new ProtoValue().setNamedValues(namedValues); } protected ProtoValue parse(Resources.Array a) { List namedValues = new ArrayList<>(a.getElementCount()); for (int i = 0; i < a.getElementCount(); i++) { Resources.Array.Element e = a.getElement(i); String value = parse(e.getItem()); namedValues.add(new ProtoValue(value)); } return new ProtoValue().setNamedValues(namedValues); } protected ProtoValue parse(Resources.Attribute a) { String format = XmlGenUtils.getAttrTypeAsString(a.getFormatFlags()); List namedValues = new ArrayList<>(a.getSymbolCount()); for (int i = 0; i < a.getSymbolCount(); i++) { Resources.Attribute.Symbol s = a.getSymbol(i); int type = s.getType(); String name = s.getName().getName(); String value = String.valueOf(s.getValue()); namedValues.add(new ProtoValue(value).setName(name).setType(type)); } return new ProtoValue(format).setNamedValues(namedValues); } protected ProtoValue parse(Resources.Plural p) { List namedValues = new ArrayList<>(p.getEntryCount()); for (int i = 0; i < p.getEntryCount(); i++) { Resources.Plural.Entry e = p.getEntry(i); String name = e.getArity().name(); String value = parse(e.getItem()); namedValues.add(new ProtoValue(value).setName(name)); } return new ProtoValue().setNamedValues(namedValues); } protected ProtoValue parse(Resources.CompoundValue c) { switch (c.getValueCase()) { case STYLE: return parse(c.getStyle()); case STYLEABLE: return parse(c.getStyleable()); case ARRAY: return parse(c.getArray()); case ATTR: return parse(c.getAttr()); case PLURAL: return parse(c.getPlural()); default: return new ProtoValue("Unresolved value"); } } protected String parse(ConfigurationOuterClass.Configuration c) { char[] language = c.getLocale().toCharArray(); if (language.length == 0) { language = new char[] { '\00' }; } short mcc = (short) c.getMcc(); short mnc = (short) c.getMnc(); byte orientation = (byte) c.getOrientationValue(); short screenWidth = (short) c.getScreenWidth(); short screenHeight = (short) c.getScreenHeight(); short screenWidthDp = (short) c.getScreenWidthDp(); short screenHeightDp = (short) c.getScreenHeightDp(); short smallestScreenWidthDp = (short) c.getSmallestScreenWidthDp(); short sdkVersion = (short) c.getSdkVersion(); byte keyboard = (byte) c.getKeyboardValue(); byte touchscreen = (byte) c.getTouchscreenValue(); int density = c.getDensity(); byte screenLayout = (byte) c.getScreenLayoutLongValue(); byte colorMode = (byte) (c.getHdrValue() | c.getWideColorGamutValue()); byte screenLayout2 = (byte) (c.getLayoutDirectionValue() | c.getScreenRoundValue()); byte navigation = (byte) c.getNavigationValue(); byte inputFlags = (byte) (c.getKeysHiddenValue() | c.getNavHiddenValue()); byte grammaticalInflection = (byte) c.getGrammaticalGenderValue(); int size = c.getSerializedSize(); byte uiMode = (byte) (c.getUiModeNightValue() | c.getUiModeTypeValue()); c.getScreenLayoutSize(); // unknown field c.getProduct(); // unknown field return new EntryConfig(mcc, mnc, language, new char[] { '\00' }, orientation, touchscreen, density, keyboard, navigation, inputFlags, grammaticalInflection, screenWidth, screenHeight, sdkVersion, screenLayout, uiMode, smallestScreenWidthDp, screenWidthDp, screenHeightDp, new char[] { '\00' }, new char[] { '\00' }, screenLayout2, colorMode, false, size).getQualifiers(); } protected String parse(Resources.Item i) { if (i.hasRawStr()) { return i.getRawStr().getValue(); } if (i.hasStr()) { return i.getStr().getValue(); } if (i.hasStyledStr()) { return i.getStyledStr().getValue(); } if (i.hasPrim()) { Resources.Primitive prim = i.getPrim(); switch (prim.getOneofValueCase()) { case NULL_VALUE: return null; case INT_DECIMAL_VALUE: return String.valueOf(prim.getIntDecimalValue()); case INT_HEXADECIMAL_VALUE: return Integer.toHexString(prim.getIntHexadecimalValue()); case BOOLEAN_VALUE: return String.valueOf(prim.getBooleanValue()); case FLOAT_VALUE: return String.valueOf(prim.getFloatValue()); case COLOR_ARGB4_VALUE: return String.format("#%04x", prim.getColorArgb4Value()); case COLOR_ARGB8_VALUE: return String.format("#%08x", prim.getColorArgb8Value()); case COLOR_RGB4_VALUE: return String.format("#%03x", prim.getColorRgb4Value()); case COLOR_RGB8_VALUE: return String.format("#%06x", prim.getColorRgb8Value()); case DIMENSION_VALUE: return XmlGenUtils.decodeComplex(prim.getDimensionValue(), false); case FRACTION_VALUE: return XmlGenUtils.decodeComplex(prim.getDimensionValue(), true); case EMPTY_VALUE: default: return ""; } } if (i.hasRef()) { Resources.Reference ref = i.getRef(); String value = ref.getName(); if (value.isEmpty()) { value = "id/" + ref.getId(); } return '@' + value; } if (i.hasFile()) { return i.getFile().getPath(); } return ""; } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/parsers/ResTableProtoParser.java ================================================ package jadx.plugins.input.aab.parsers; import java.io.IOException; import java.io.InputStream; import java.util.List; import com.android.aapt.Resources.ConfigValue; import com.android.aapt.Resources.Entry; import com.android.aapt.Resources.Package; import com.android.aapt.Resources.ResourceTable; import com.android.aapt.Resources.Type; import com.android.aapt.Resources.Value; import jadx.api.ICodeInfo; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.files.FileUtils; import jadx.core.xmlgen.BinaryXMLStrings; import jadx.core.xmlgen.IResTableParser; import jadx.core.xmlgen.ResContainer; import jadx.core.xmlgen.ResXmlGen; import jadx.core.xmlgen.ResourceStorage; import jadx.core.xmlgen.XmlGenUtils; import jadx.core.xmlgen.entry.ProtoValue; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.core.xmlgen.entry.ValuesParser; public class ResTableProtoParser extends CommonProtoParser implements IResTableParser { private final RootNode root; private ResourceStorage resStorage; private String baseFileName = ""; public ResTableProtoParser(RootNode root) { this.root = root; } @Override public void setBaseFileName(String fileName) { this.baseFileName = fileName; } @Override public void decode(InputStream inputStream) throws IOException { resStorage = new ResourceStorage(root.getArgs().getSecurity()); ResourceTable table = ResourceTable.parseFrom(FileUtils.streamToByteArray(inputStream)); for (Package p : table.getPackageList()) { parse(p); } resStorage.finish(); } @Override public synchronized ResContainer decodeFiles() { ValuesParser vp = new ValuesParser(new BinaryXMLStrings(), resStorage.getResourcesNames()); ResXmlGen resGen = new ResXmlGen(resStorage, vp, root.initManifestAttributes()); ICodeInfo content = XmlGenUtils.makeXmlDump(root.makeCodeWriter(), resStorage); List xmlFiles = resGen.makeResourcesXml(root.getArgs()); return ResContainer.resourceTable(baseFileName, xmlFiles, content); } private void parse(Package p) { String packageName = p.getPackageName(); resStorage.setAppPackage(packageName); List types = p.getTypeList(); for (Type type : types) { String typeName = type.getName(); for (Entry entry : type.getEntryList()) { int id = p.getPackageId().getId() << 24 | type.getTypeId().getId() << 16 | entry.getEntryId().getId(); String entryName = entry.getName(); for (ConfigValue configValue : entry.getConfigValueList()) { String config = parse(configValue.getConfig()); ResourceEntry resEntry = new ResourceEntry(id, packageName, typeName, entryName, config); resStorage.add(resEntry); ProtoValue protoValue; if (configValue.getValue().getValueCase() == Value.ValueCase.ITEM) { protoValue = new ProtoValue(parse(configValue.getValue().getItem())); } else { protoValue = parse(configValue.getValue().getCompoundValue()); } resEntry.setProtoValue(protoValue); } } } } @Override public ResourceStorage getResStorage() { return resStorage; } @Override public BinaryXMLStrings getStrings() { return new BinaryXMLStrings(); } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/java/jadx/plugins/input/aab/parsers/ResXmlProtoParser.java ================================================ package jadx.plugins.input.aab.parsers; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Random; import java.util.Set; import com.android.aapt.Resources.XmlAttribute; import com.android.aapt.Resources.XmlElement; import com.android.aapt.Resources.XmlNamespace; import com.android.aapt.Resources.XmlNode; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.StringUtils; import jadx.core.utils.android.AndroidResourcesMap; import jadx.core.xmlgen.XMLChar; import jadx.core.xmlgen.XmlDeobf; import jadx.core.xmlgen.XmlGenUtils; public class ResXmlProtoParser extends CommonProtoParser { private Map nsMap; private final Map tagAttrDeobfNames = new HashMap<>(); private ICodeWriter writer; private final RootNode rootNode; private String currentTag; private String appPackageName; private final boolean isPrettyPrint; public ResXmlProtoParser(RootNode rootNode) { this.rootNode = rootNode; this.isPrettyPrint = !rootNode.getArgs().isSkipXmlPrettyPrint(); } public synchronized ICodeInfo parse(InputStream inputStream) throws IOException { nsMap = new HashMap<>(); writer = rootNode.makeCodeWriter(); writer.add(""); decode(decodeProto(inputStream)); nsMap = null; return writer.finish(); } private void decode(XmlNode n) throws IOException { if (n.hasSource()) { writer.attachSourceLine(n.getSource().getLineNumber()); } writer.add(StringUtils.escapeXML(n.getText().trim())); if (n.hasElement()) { decode(n.getElement()); } } private void decode(XmlElement e) throws IOException { String tag = deobfClassName(e.getName()); tag = getValidTagAttributeName(tag); currentTag = tag; writer.startLine('<').add(tag); decodeNamespaces(e); decodeAttributes(e); if (e.getChildCount() > 0) { writer.add('>'); writer.incIndent(); for (int i = 0; i < e.getChildCount(); i++) { Map oldNsMap = new HashMap<>(nsMap); decode(e.getChild(i)); nsMap = oldNsMap; } writer.decIndent(); writer.startLine("'); } else { writer.add(" />"); } } private void decodeNamespaces(XmlElement e) { int nsCount = e.getNamespaceDeclarationCount(); boolean newLine = nsCount != 1 && isPrettyPrint; if (nsCount > 0) { writer.add(' '); } for (int i = 0; i < nsCount; i++) { decodeNamespace(e.getNamespaceDeclaration(i), newLine, i == nsCount - 1); } } private void decodeNamespace(XmlNamespace n, boolean newLine, boolean isLastElement) { String prefix = n.getPrefix(); String uri = n.getUri(); nsMap.put(uri, prefix); writer.add("xmlns:").add(prefix).add("=\"").add(uri).add('"'); if (isLastElement) { return; } if (newLine) { writer.startLine().addIndent(); } else { writer.add(' '); } } private void decodeAttributes(XmlElement e) { int attrsCount = e.getAttributeCount(); boolean newLine = attrsCount != 1 && isPrettyPrint; if (attrsCount > 0) { writer.add(' '); if (isPrettyPrint) { writer.startLine().addIndent(); } } Set attrCache = new HashSet<>(); for (int i = 0; i < attrsCount; i++) { decodeAttribute(e.getAttribute(i), attrCache, newLine, i == attrsCount - 1); } } private void decodeAttribute(XmlAttribute a, Set attrCache, boolean newLine, boolean isLastElement) { String name = getAttributeFullName(a); if (XmlDeobf.isDuplicatedAttr(name, attrCache)) { return; } String value = deobfClassName(getAttributeValue(a)); writer.add(name).add("=\"").add(StringUtils.escapeXML(value)).add('\"'); memorizePackageName(name, value); if (isLastElement) { return; } if (newLine) { writer.startLine().addIndent(); } else { writer.add(' '); } } private String getAttributeFullName(XmlAttribute a) { String namespaceUri = a.getNamespaceUri(); String namespace = null; if (!namespaceUri.isEmpty()) { namespace = nsMap.get(namespaceUri); } String attrName = a.getName(); if (attrName.isEmpty()) { // some optimization tools clear the name because the Android platform doesn't need it int resId = a.getResourceId(); String str = AndroidResourcesMap.getResName(resId); if (str != null) { namespace = nsMap.get(ANDROID_NS_URL); // cut type before / int typeEnd = str.indexOf('/'); if (typeEnd != -1) { attrName = str.substring(typeEnd + 1); } else { attrName = str; } } else { attrName = "_unknown_"; } } return namespace != null ? namespace + ":" + attrName : attrName; } private String getAttributeValue(XmlAttribute a) { if (!a.getValue().isEmpty()) { return a.getValue(); } return parse(a.getCompiledItem()); } private void memorizePackageName(String attrName, String attrValue) { if ("manifest".equals(currentTag) && "package".equals(attrName)) { appPackageName = attrValue; } } private String deobfClassName(String className) { String newName = XmlDeobf.deobfClassName(rootNode, className, appPackageName); if (newName != null) { return newName; } return className; } private String getValidTagAttributeName(String originalName) { if (XMLChar.isValidName(originalName)) { return originalName; } if (tagAttrDeobfNames.containsKey(originalName)) { return tagAttrDeobfNames.get(originalName); } String generated; do { generated = generateTagAttrName(); } while (tagAttrDeobfNames.containsValue(generated)); tagAttrDeobfNames.put(originalName, generated); return generated; } private static String generateTagAttrName() { final int length = 6; Random r = new Random(); StringBuilder sb = new StringBuilder(); for (int i = 1; i <= length; i++) { sb.append((char) (r.nextInt(26) + 'a')); } return sb.toString(); } private XmlNode decodeProto(InputStream inputStream) throws IOException { return XmlNode.parseFrom(XmlGenUtils.readData(inputStream)); } } ================================================ FILE: jadx-plugins/jadx-aab-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.input.aab.AabInputPlugin ================================================ FILE: jadx-plugins/jadx-apkm-input/build.gradle.kts ================================================ plugins { id("jadx-library") id("jadx-kotlin") } dependencies { api(project(":jadx-core")) implementation(project(":jadx-plugins:jadx-dex-input")) implementation("com.google.code.gson:gson:2.13.2") } ================================================ FILE: jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomCodeInput.kt ================================================ package jadx.plugins.input.apkm import jadx.api.plugins.input.ICodeLoader import jadx.api.plugins.input.JadxCodeInput import jadx.api.plugins.utils.CommonFileUtils import jadx.plugins.input.dex.DexInputPlugin import jadx.zip.ZipReader import java.io.File import java.nio.file.Path class ApkmCustomCodeInput( private val dexInputPlugin: DexInputPlugin, private val zipReader: ZipReader, ) : JadxCodeInput { override fun loadFiles(input: List): ICodeLoader { val apkFiles = mutableListOf() for (file in input.map { it.toFile() }) { if (!file.name.endsWith(".apkm")) continue // Check if this is a valid APKM file val manifest = ApkmUtils.getManifest(file, zipReader) ?: continue if (!ApkmUtils.isSupported(manifest)) continue // Load all files ending with .apk zipReader.visitEntries(file) { entry -> if (entry.name.endsWith(".apk")) { val tmpFile = entry.inputStream.use { CommonFileUtils.saveToTempFile(it, ".apk").toFile() } apkFiles.add(tmpFile) } null } } val codeLoader = dexInputPlugin.loadFiles(apkFiles.map { it.toPath() }) apkFiles.forEach { CommonFileUtils.safeDeleteFile(it) } return codeLoader } } ================================================ FILE: jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomResourcesLoader.kt ================================================ package jadx.plugins.input.apkm import jadx.api.ResourceFile import jadx.api.ResourcesLoader import jadx.api.plugins.CustomResourcesLoader import jadx.api.plugins.utils.CommonFileUtils import jadx.zip.ZipReader import java.io.File class ApkmCustomResourcesLoader( private val zipReader: ZipReader, ) : CustomResourcesLoader { private val tmpFiles = mutableListOf() override fun load(loader: ResourcesLoader, list: MutableList, file: File): Boolean { if (!file.name.endsWith(".apkm")) return false // Check if this is a valid APKM file val manifest = ApkmUtils.getManifest(file, zipReader) ?: return false if (!ApkmUtils.isSupported(manifest)) return false // Load all files ending with .apk zipReader.visitEntries(file) { entry -> if (entry.name.endsWith(".apk")) { val tmpFile = entry.inputStream.use { CommonFileUtils.saveToTempFile(it, ".apk").toFile() } loader.defaultLoadFile(list, tmpFile, entry.name + "/") tmpFiles += tmpFile } null } return true } override fun close() { tmpFiles.forEach(CommonFileUtils::safeDeleteFile) tmpFiles.clear() } } ================================================ FILE: jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmInputPlugin.kt ================================================ package jadx.plugins.input.apkm import jadx.api.plugins.JadxPlugin import jadx.api.plugins.JadxPluginContext import jadx.api.plugins.JadxPluginInfo import jadx.plugins.input.dex.DexInputPlugin class ApkmInputPlugin : JadxPlugin { override fun getPluginInfo() = JadxPluginInfo( "apkm-input", "APKM Input", "Load .apkm files", ) override fun init(context: JadxPluginContext) { val dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java) context.addCodeInput(ApkmCustomCodeInput(dexInputPlugin, context.zipReader)) context.decompiler.addCustomResourcesLoader(ApkmCustomResourcesLoader(context.zipReader)) } } ================================================ FILE: jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmManifest.kt ================================================ package jadx.plugins.input.apkm import com.google.gson.annotations.SerializedName data class ApkmManifest( @SerializedName("apkm_version") var apkmVersion: Int = -1, ) ================================================ FILE: jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmUtils.kt ================================================ package jadx.plugins.input.apkm import jadx.core.utils.GsonUtils.buildGson import jadx.core.utils.files.FileUtils import jadx.zip.ZipReader import java.io.File import java.io.InputStreamReader object ApkmUtils { fun getManifest(file: File, zipReader: ZipReader): ApkmManifest? { if (!FileUtils.isZipFile(file)) return null try { zipReader.open(file).use { zip -> val manifestEntry = zip.searchEntry("info.json") ?: return null return InputStreamReader(manifestEntry.inputStream).use { buildGson().fromJson(it, ApkmManifest::class.java) } } } catch (e: Exception) { return null } } fun isSupported(manifest: ApkmManifest): Boolean { return manifest.apkmVersion != -1 } } ================================================ FILE: jadx-plugins/jadx-apkm-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.input.apkm.ApkmInputPlugin ================================================ FILE: jadx-plugins/jadx-apks-input/build.gradle.kts ================================================ plugins { id("jadx-library") id("jadx-kotlin") } dependencies { api(project(":jadx-core")) implementation(project(":jadx-plugins:jadx-dex-input")) } ================================================ FILE: jadx-plugins/jadx-apks-input/src/main/java/jadx/plugins/input/apks/ApksCustomCodeInput.kt ================================================ package jadx.plugins.input.apks import jadx.api.plugins.input.ICodeLoader import jadx.api.plugins.input.JadxCodeInput import jadx.api.plugins.utils.CommonFileUtils import jadx.plugins.input.dex.DexInputPlugin import jadx.zip.ZipReader import java.io.File import java.nio.file.Path class ApksCustomCodeInput( private val dexInputPlugin: DexInputPlugin, private val zipReader: ZipReader, ) : JadxCodeInput { override fun loadFiles(input: List): ICodeLoader { val apkFiles = mutableListOf() for (file in input.map { it.toFile() }) { if (!file.name.endsWith(".apks")) continue // Load all files ending with .apk zipReader.visitEntries(file) { entry -> if (entry.name.endsWith(".apk")) { val tmpFile = entry.inputStream.use { CommonFileUtils.saveToTempFile(it, ".apk").toFile() } apkFiles.add(tmpFile) } null } } val codeLoader = dexInputPlugin.loadFiles(apkFiles.map { it.toPath() }) apkFiles.forEach { CommonFileUtils.safeDeleteFile(it) } return codeLoader } } ================================================ FILE: jadx-plugins/jadx-apks-input/src/main/java/jadx/plugins/input/apks/ApksCustomResourcesLoader.kt ================================================ package jadx.plugins.input.apks import jadx.api.ResourceFile import jadx.api.ResourcesLoader import jadx.api.plugins.CustomResourcesLoader import jadx.api.plugins.utils.CommonFileUtils import jadx.zip.ZipReader import java.io.File class ApksCustomResourcesLoader( private val zipReader: ZipReader, ) : CustomResourcesLoader { private val tmpFiles = mutableListOf() override fun load(loader: ResourcesLoader, list: MutableList, file: File): Boolean { if (!file.name.endsWith(".apks")) return false // Load all files ending with .apk zipReader.visitEntries(file) { entry -> if (entry.name.endsWith(".apk")) { val tmpFile = entry.inputStream.use { CommonFileUtils.saveToTempFile(it, ".apk").toFile() } loader.defaultLoadFile(list, tmpFile, entry.name + "/") tmpFiles += tmpFile } null } return true } override fun close() { tmpFiles.forEach(CommonFileUtils::safeDeleteFile) tmpFiles.clear() } } ================================================ FILE: jadx-plugins/jadx-apks-input/src/main/java/jadx/plugins/input/apks/ApksInputPlugin.kt ================================================ package jadx.plugins.input.apks import jadx.api.plugins.JadxPlugin import jadx.api.plugins.JadxPluginContext import jadx.api.plugins.JadxPluginInfo import jadx.plugins.input.dex.DexInputPlugin class ApksInputPlugin : JadxPlugin { override fun getPluginInfo() = JadxPluginInfo( "apks-input", "APKS Input", "Load .apks files", ) override fun init(context: JadxPluginContext) { val dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java) context.addCodeInput(ApksCustomCodeInput(dexInputPlugin, context.zipReader)) context.decompiler.addCustomResourcesLoader(ApksCustomResourcesLoader(context.zipReader)) } } ================================================ FILE: jadx-plugins/jadx-apks-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.input.apks.ApksInputPlugin ================================================ FILE: jadx-plugins/jadx-dex-input/build.gradle.kts ================================================ plugins { id("jadx-library") } dependencies { api(project(":jadx-core")) // TODO: finish own smali printer implementation("com.android.tools.smali:smali-baksmali:3.0.9") { exclude(group = "com.beust", module = "jcommander") // exclude old jcommander namespace } implementation("com.google.guava:guava:33.5.0-jre") // force the latest version for smali // compile smali files in tests testImplementation("com.android.tools.smali:smali:3.0.9") { exclude(group = "com.beust", module = "jcommander") // exclude old jcommander namespace } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexException.java ================================================ package jadx.plugins.input.dex; public class DexException extends RuntimeException { private static final long serialVersionUID = -5575702801815409269L; public DexException(String message, Throwable cause) { super(message, cause); } public DexException(String message) { super(message); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java ================================================ package jadx.plugins.input.dex; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.utils.CommonFileUtils; import jadx.core.utils.files.FileUtils; import jadx.plugins.input.dex.sections.DexConsts; import jadx.plugins.input.dex.sections.DexHeaderV41; import jadx.plugins.input.dex.utils.DexCheckSum; import jadx.zip.IZipEntry; import jadx.zip.ZipContent; import jadx.zip.ZipReader; public class DexFileLoader { private static final Logger LOG = LoggerFactory.getLogger(DexFileLoader.class); // sharing between all instances (can be used in other plugins) // TODO: private static int dexUniqId = 1; private final DexInputOptions options; private ZipReader zipReader = new ZipReader(); public DexFileLoader(DexInputOptions options) { this.options = options; } public void setZipReader(ZipReader zipReader) { this.zipReader = zipReader; } public List collectDexFiles(List pathsList) { return pathsList.stream() .map(Path::toFile) .map(this::loadDexFromFile) .filter(list -> !list.isEmpty()) .flatMap(Collection::stream) .peek(dr -> LOG.debug("Loading dex: {}", dr)) .collect(Collectors.toList()); } private List loadDexFromFile(File file) { try (InputStream inputStream = new FileInputStream(file)) { return load(file, inputStream, file.getAbsolutePath()); } catch (Exception e) { LOG.error("File open error: {}", file.getAbsolutePath(), e); return Collections.emptyList(); } } private List load(@Nullable File file, InputStream inputStream, String fileName) throws IOException { try (InputStream in = inputStream.markSupported() ? inputStream : new BufferedInputStream(inputStream)) { byte[] magic = new byte[DexConsts.MAX_MAGIC_SIZE]; in.mark(magic.length); if (in.read(magic) != magic.length) { return Collections.emptyList(); } if (isStartWithBytes(magic, DexConsts.DEX_FILE_MAGIC)) { in.reset(); byte[] content = readAllBytes(in); return loadDexReaders(fileName, content); } if (fileName.endsWith(".dex")) { // report invalid magic in '.dex' file String hex = FileUtils.bytesToHex(magic); String str = new String(magic, StandardCharsets.US_ASCII); LOG.warn("Invalid DEX magic: 0x{}(\"{}\") in file: {}", hex, str, fileName); } if (file != null) { // allow only top level zip files if (isStartWithBytes(magic, DexConsts.ZIP_FILE_MAGIC) || CommonFileUtils.isZipFileExt(fileName)) { return collectDexFromZip(file); } } return Collections.emptyList(); } } private List loadFromZipEntry(byte[] content, String fileName) { if (isStartWithBytes(content, DexConsts.DEX_FILE_MAGIC) || fileName.endsWith(".dex")) { return loadDexReaders(fileName, content); } return Collections.emptyList(); } public List loadDexReaders(String fileName, byte[] content) { DexHeaderV41 dexHeaderV41 = DexHeaderV41.readIfPresent(content); if (dexHeaderV41 != null) { return DexHeaderV41.readSubDexOffsets(content, dexHeaderV41) .stream() .map(offset -> loadSingleDex(fileName, content, offset)) .collect(Collectors.toList()); } DexReader dexReader = loadSingleDex(fileName, content, 0); return Collections.singletonList(dexReader); } private DexReader loadSingleDex(String fileName, byte[] content, int offset) { if (options.isVerifyChecksum()) { DexCheckSum.verify(fileName, content, offset); } return new DexReader(getNextUniqId(), fileName, content, offset); } /** * Since DEX v41, several sub DEX structures can be stored inside container of a single DEX file * Use {@link DexFileLoader#loadDexReaders(String, byte[])} instead. */ @Deprecated public DexReader loadDexReader(String fileName, byte[] content) { return loadSingleDex(fileName, content, 0); } private List collectDexFromZip(File file) { List result = new ArrayList<>(); try (ZipContent zip = zipReader.open(file)) { for (IZipEntry entry : zip.getEntries()) { if (entry.isDirectory()) { continue; } try { List readers; if (entry.preferBytes()) { readers = loadFromZipEntry(entry.getBytes(), entry.getName()); } else { readers = load(null, entry.getInputStream(), entry.getName()); } if (!readers.isEmpty()) { result.addAll(readers); } } catch (Exception e) { LOG.error("Failed to read zip entry: {}", entry, e); } } } catch (Exception e) { LOG.error("Failed to process zip file: {}", file.getAbsolutePath(), e); } return result; } private static boolean isStartWithBytes(byte[] fileMagic, byte[] expectedBytes) { int len = expectedBytes.length; if (fileMagic.length < len) { return false; } for (int i = 0; i < len; i++) { if (fileMagic[i] != expectedBytes[i]) { return false; } } return true; } private static byte[] readAllBytes(InputStream in) throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); byte[] data = new byte[8192]; while (true) { int read = in.read(data); if (read == -1) { break; } buf.write(data, 0, read); } return buf.toByteArray(); } private static synchronized int getNextUniqId() { dexUniqId++; if (dexUniqId >= 0xFFFF) { dexUniqId = 1; } return dexUniqId; } private static synchronized void resetDexUniqId() { dexUniqId = 1; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java ================================================ package jadx.plugins.input.dex; import jadx.api.plugins.options.impl.BasePluginOptionsBuilder; public class DexInputOptions extends BasePluginOptionsBuilder { private boolean verifyChecksum; @Override public void registerOptions() { boolOption(DexInputPlugin.PLUGIN_ID + ".verify-checksum") .description("verify dex file checksum before load") .defaultValue(true) .setter(v -> verifyChecksum = v); } public boolean isVerifyChecksum() { return verifyChecksum; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java ================================================ package jadx.plugins.input.dex; import java.io.Closeable; import java.io.InputStream; import java.nio.file.Path; import java.util.List; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.data.impl.EmptyCodeLoader; import jadx.api.plugins.utils.CommonFileUtils; import jadx.plugins.input.dex.utils.IDexData; public class DexInputPlugin implements JadxPlugin { public static final String PLUGIN_ID = "dex-input"; private final DexInputOptions options = new DexInputOptions(); private final DexFileLoader loader = new DexFileLoader(options); @Override public JadxPluginInfo getPluginInfo() { return new JadxPluginInfo(PLUGIN_ID, "Dex Input", "Load .dex and .apk files"); } @Override public void init(JadxPluginContext context) { context.registerOptions(options); context.addCodeInput(this::loadFiles); loader.setZipReader(context.getZipReader()); } public ICodeLoader loadFiles(List input) { return loadFiles(input, null); } public ICodeLoader loadFiles(List inputFiles, @Nullable Closeable closeable) { List dexReaders = loader.collectDexFiles(inputFiles); if (dexReaders.isEmpty()) { return EmptyCodeLoader.INSTANCE; } return new DexLoadResult(dexReaders, closeable); } public ICodeLoader loadDex(byte[] content, @Nullable String fileName) { String fileLabel = fileName == null ? "input.dex" : fileName; List dexReaders = loader.loadDexReaders(fileLabel, content); return new DexLoadResult(dexReaders, null); } public ICodeLoader loadDexFromInputStream(InputStream in, @Nullable String fileLabel) { try { return loadDex(CommonFileUtils.loadBytes(in), fileLabel); } catch (Exception e) { throw new DexException("Failed to read input stream", e); } } public ICodeLoader loadDexData(List list) { List readers = list.stream() .flatMap(data -> loader.loadDexReaders(data.getFileName(), data.getContent()).stream()) .collect(Collectors.toList()); return new DexLoadResult(readers, null); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexLoadResult.java ================================================ package jadx.plugins.input.dex; import java.io.Closeable; import java.io.IOException; import java.util.List; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.data.IClassData; public class DexLoadResult implements ICodeLoader { private final List dexReaders; @Nullable private final Closeable closeable; public DexLoadResult(List dexReaders, @Nullable Closeable closeable) { this.dexReaders = dexReaders; this.closeable = closeable; } @Override public void visitClasses(Consumer consumer) { for (DexReader dexReader : dexReaders) { dexReader.visitClasses(consumer); } } @Override public void close() throws IOException { if (closeable != null) { closeable.close(); } } @Override public boolean isEmpty() { return dexReaders.isEmpty(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexReader.java ================================================ package jadx.plugins.input.dex; import java.nio.ByteBuffer; import java.util.function.Consumer; import jadx.api.plugins.input.data.IClassData; import jadx.plugins.input.dex.sections.DexClassData; import jadx.plugins.input.dex.sections.DexHeader; import jadx.plugins.input.dex.sections.SectionReader; import jadx.plugins.input.dex.sections.annotations.AnnotationsParser; public class DexReader { private final int uniqId; private final String inputFileName; private final ByteBuffer buf; private final DexHeader header; public DexReader(int uniqId, String inputFileName, byte[] content, int offset) { this.uniqId = uniqId; this.inputFileName = inputFileName; this.buf = ByteBuffer.wrap(content); this.header = new DexHeader(new SectionReader(this, offset)); } public void visitClasses(Consumer consumer) { int count = header.getClassDefsSize(); if (count == 0) { return; } int classDefsOff = header.getClassDefsOff(); SectionReader in = new SectionReader(this, classDefsOff); AnnotationsParser annotationsParser = new AnnotationsParser(in.copy(), in.copy()); DexClassData classData = new DexClassData(in, annotationsParser); for (int i = 0; i < count; i++) { consumer.accept(classData); in.shiftOffset(DexClassData.SIZE); } } public ByteBuffer getBuf() { return buf; } public DexHeader getHeader() { return header; } public String getInputFileName() { return inputFileName; } public int getUniqId() { return uniqId; } @Override public String toString() { return inputFileName; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexInsnData.java ================================================ package jadx.plugins.input.dex.insns; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ICallSite; import jadx.api.plugins.input.data.IFieldRef; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.insns.InsnData; import jadx.api.plugins.input.insns.InsnIndexType; import jadx.api.plugins.input.insns.Opcode; import jadx.api.plugins.input.insns.custom.ICustomPayload; import jadx.plugins.input.dex.sections.DexCodeReader; import jadx.plugins.input.dex.sections.SectionReader; public class DexInsnData implements InsnData { private final DexCodeReader codeData; private final SectionReader externalReader; private final SectionReader secondExtReader; private DexInsnInfo insnInfo; private boolean decoded; private int opcodeUnit; private int length; private int insnStart; private int offset; private int[] argsReg = new int[5]; private int regsCount; private long literal; private int target; private int index; @Nullable private ICustomPayload payload; public DexInsnData(DexCodeReader codeData, SectionReader externalReader) { this.codeData = codeData; this.externalReader = externalReader; this.secondExtReader = externalReader.copy(); } @Override public void decode() { if (insnInfo != null && !decoded) { codeData.decode(this); } } @Override public int getOffset() { return offset; } @Override public int getFileOffset() { return insnStart; } @Override public Opcode getOpcode() { DexInsnInfo info = this.insnInfo; if (info == null) { return Opcode.UNKNOWN; } return info.getApiOpcode(); } @Override public String getOpcodeMnemonic() { return DexInsnMnemonics.get(opcodeUnit); } @Override public byte[] getByteCode() { return externalReader.getByteCode(insnStart, length * 2); // a unit is 2 bytes } @Override public int getRawOpcodeUnit() { return opcodeUnit; } @Override public int getRegsCount() { return regsCount; } @Override public int getReg(int argNum) { return argsReg[argNum]; } @Override public int getResultReg() { return -1; } @Override public long getLiteral() { return literal; } @Override public int getTarget() { return target; } @Override public int getIndex() { return index; } @Override public InsnIndexType getIndexType() { return insnInfo.getIndexType(); } @Override public String getIndexAsString() { return externalReader.getString(index); } @Override public String getIndexAsType() { return externalReader.getType(index); } @Override public IFieldRef getIndexAsField() { return externalReader.getFieldRef(index); } @Override public IMethodRef getIndexAsMethod() { return externalReader.getMethodRef(index); } @Override public ICallSite getIndexAsCallSite() { return externalReader.getCallSite(index, secondExtReader); } /** * Currently, protoIndex is either being stored at index or target, index for const-method-type, * target for invoke-polymorphic(/range) */ @Override public IMethodProto getIndexAsProto(int protoIndex) { return externalReader.getMethodProto(protoIndex); } @Override public IMethodHandle getIndexAsMethodHandle() { return externalReader.getMethodHandle(index); } @Nullable @Override public ICustomPayload getPayload() { return payload; } public int[] getArgsReg() { return argsReg; } public void setArgsReg(int[] argsReg) { this.argsReg = argsReg; } public void setRegsCount(int regsCount) { this.regsCount = regsCount; } public int getLength() { return length; } public void setLength(int length) { this.length = length; } public void setInsnStart(int start) { this.insnStart = start; } public void setLiteral(long literal) { this.literal = literal; } public void setTarget(int target) { this.target = target; } public void setIndex(int index) { this.index = index; } public boolean isDecoded() { return decoded; } public void setDecoded(boolean decoded) { this.decoded = decoded; } public void setOffset(int offset) { this.offset = offset; } public DexInsnInfo getInsnInfo() { return insnInfo; } public void setInsnInfo(DexInsnInfo insnInfo) { this.insnInfo = insnInfo; } public DexCodeReader getCodeData() { return codeData; } public int getOpcodeUnit() { return opcodeUnit; } public void setOpcodeUnit(int opcodeUnit) { this.opcodeUnit = opcodeUnit; } public void setPayload(ICustomPayload payload) { this.payload = payload; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(String.format("0x%04X", offset)); sb.append(": ").append(getOpcode()); if (insnInfo == null) { sb.append(String.format("(0x%04X)", opcodeUnit)); } else { int regsCount = getRegsCount(); if (isDecoded()) { sb.append(' '); for (int i = 0; i < regsCount; i++) { if (i != 0) { sb.append(", "); } sb.append("r").append(argsReg[i]); } } } return sb.toString(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexInsnFormat.java ================================================ package jadx.plugins.input.dex.insns; import jadx.api.plugins.input.insns.custom.impl.SwitchPayload; import jadx.plugins.input.dex.DexException; import jadx.plugins.input.dex.insns.payloads.DexArrayPayload; import jadx.plugins.input.dex.sections.SectionReader; public abstract class DexInsnFormat { public static final DexInsnFormat FORMAT_10X = new DexInsnFormat(1, 0) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { // no op } }; public static final DexInsnFormat FORMAT_12X = new DexInsnFormat(1, 2) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = nibble2(opcodeUnit); regs[1] = nibble3(opcodeUnit); } }; public static final DexInsnFormat FORMAT_11N = new DexInsnFormat(1, 1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = nibble2(opcodeUnit); insn.setLiteral(signedNibble3(opcodeUnit)); } }; public static final DexInsnFormat FORMAT_11X = new DexInsnFormat(1, 1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); } }; public static final DexInsnFormat FORMAT_10T = new DexInsnFormat(1, 0) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { insn.setTarget(insn.getOffset() + signedByte1(opcodeUnit)); } }; public static final DexInsnFormat FORMAT_20T = new DexInsnFormat(2, 0) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { insn.setTarget(insn.getOffset() + in.readShort()); } }; public static final DexInsnFormat FORMAT_20BC = new DexInsnFormat(2, 0) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { insn.setLiteral(byte1(opcodeUnit)); insn.setIndex(in.readUShort()); } }; public static final DexInsnFormat FORMAT_22X = new DexInsnFormat(2, 2) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); regs[1] = in.readUShort(); } }; public static final DexInsnFormat FORMAT_21T = new DexInsnFormat(2, 1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); insn.setTarget(insn.getOffset() + in.readShort()); } }; public static final DexInsnFormat FORMAT_21S = new DexInsnFormat(2, 1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); insn.setLiteral(in.readShort()); } }; public static final DexInsnFormat FORMAT_21H = new DexInsnFormat(2, 1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); long literal = in.readShort(); literal <<= byte0(opcodeUnit) == DexOpcodes.CONST_HIGH16 ? 16 : 48; insn.setLiteral(literal); } }; public static final DexInsnFormat FORMAT_21C = new DexInsnFormat(2, 1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); insn.setIndex(in.readUShort()); } }; public static final DexInsnFormat FORMAT_23X = new DexInsnFormat(2, 3) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); int next = in.readUShort(); regs[1] = byte0(next); regs[2] = byte1(next); } }; public static final DexInsnFormat FORMAT_22B = new DexInsnFormat(2, 2) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); int next = in.readUShort(); regs[1] = byte0(next); insn.setLiteral(signedByte1(next)); } }; public static final DexInsnFormat FORMAT_22T = new DexInsnFormat(2, 2) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = nibble2(opcodeUnit); regs[1] = nibble3(opcodeUnit); insn.setTarget(insn.getOffset() + in.readShort()); } }; public static final DexInsnFormat FORMAT_22S = new DexInsnFormat(2, 2) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = nibble2(opcodeUnit); regs[1] = nibble3(opcodeUnit); insn.setLiteral(in.readShort()); } }; public static final DexInsnFormat FORMAT_22C = new DexInsnFormat(2, 2) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = nibble2(opcodeUnit); regs[1] = nibble3(opcodeUnit); insn.setIndex(in.readUShort()); insn.setLiteral(0L); } }; public static final DexInsnFormat FORMAT_22CS = FORMAT_22C; public static final DexInsnFormat FORMAT_30T = new DexInsnFormat(3, 0) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { insn.setTarget(insn.getOffset() + in.readInt()); } }; public static final DexInsnFormat FORMAT_32X = new DexInsnFormat(3, 2) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = in.readUShort(); regs[1] = in.readUShort(); } }; public static final DexInsnFormat FORMAT_31I = new DexInsnFormat(3, 1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); insn.setLiteral(in.readInt()); } }; public static final DexInsnFormat FORMAT_31T = new DexInsnFormat(3, 1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); insn.setTarget(insn.getOffset() + in.readInt()); } }; public static final DexInsnFormat FORMAT_31C = new DexInsnFormat(3, 1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); insn.setIndex(in.readInt()); } }; public static final DexInsnFormat FORMAT_35C = new DexInsnFormat(3, -1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { readRegsList(insn, opcodeUnit, in); } }; public static final DexInsnFormat FORMAT_35MS = FORMAT_35C; public static final DexInsnFormat FORMAT_35MI = FORMAT_35C; public static final DexInsnFormat FORMAT_3RC = new DexInsnFormat(3, -1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { readRegsRange(insn, opcodeUnit, in); } }; public static final DexInsnFormat FORMAT_3RMS = FORMAT_3RC; public static final DexInsnFormat FORMAT_3RMI = FORMAT_3RC; public static final DexInsnFormat FORMAT_45CC = new DexInsnFormat(4, -1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { readRegsList(insn, opcodeUnit, in); insn.setTarget(in.readUShort()); } }; public static final DexInsnFormat FORMAT_4RCC = new DexInsnFormat(4, -1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { readRegsRange(insn, opcodeUnit, in); insn.setTarget(in.readUShort()); } }; public static final DexInsnFormat FORMAT_51I = new DexInsnFormat(5, 1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int[] regs = insn.getArgsReg(); regs[0] = byte1(opcodeUnit); insn.setLiteral(in.readLong()); } }; public static final DexInsnFormat FORMAT_PACKED_SWITCH_PAYLOAD = new DexInsnFormat(-1, -1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int size = in.readUShort(); int firstKey = in.readInt(); int[] keys = new int[size]; int[] targets = new int[size]; for (int i = 0; i < size; i++) { targets[i] = in.readInt(); keys[i] = firstKey + i; } insn.setPayload(new SwitchPayload(size, keys, targets)); insn.setLength(size * 2 + 4); } @Override public void skip(DexInsnData insn, SectionReader in) { int size = in.readUShort(); in.skip(4 + size * 4); insn.setLength(size * 2 + 4); } }; public static final DexInsnFormat FORMAT_SPARSE_SWITCH_PAYLOAD = new DexInsnFormat(-1, -1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int size = in.readUShort(); int[] keys = new int[size]; for (int i = 0; i < size; i++) { keys[i] = in.readInt(); } int[] targets = new int[size]; for (int i = 0; i < size; i++) { targets[i] = in.readInt(); } insn.setPayload(new SwitchPayload(size, keys, targets)); insn.setLength(size * 4 + 2); } @Override public void skip(DexInsnData insn, SectionReader in) { int size = in.readUShort(); in.skip(size * 8); insn.setLength(size * 4 + 2); } }; public static final DexInsnFormat FORMAT_FILL_ARRAY_DATA_PAYLOAD = new DexInsnFormat(-1, -1) { @Override public void decode(DexInsnData insn, int opcodeUnit, SectionReader in) { int elemSize = in.readUShort(); int size = in.readInt(); Object data; switch (elemSize) { case 1: { data = in.readByteArray(size); if (size % 2 != 0) { in.readUByte(); } break; } case 2: { short[] array = new short[size]; for (int i = 0; i < size; i++) { array[i] = (short) in.readShort(); } data = array; break; } case 4: { int[] array = new int[size]; for (int i = 0; i < size; i++) { array[i] = in.readInt(); } data = array; break; } case 8: { long[] array = new long[size]; for (int i = 0; i < size; i++) { array[i] = in.readLong(); } data = array; break; } case 0: { data = new byte[0]; break; } default: throw new DexException("Unexpected element size in FILL_ARRAY_DATA_PAYLOAD: " + elemSize); } insn.setLength((size * elemSize + 1) / 2 + 4); insn.setPayload(new DexArrayPayload(size, elemSize, data)); } @Override public void skip(DexInsnData insn, SectionReader in) { int elemSize = in.readUShort(); int size = in.readInt(); if (elemSize == 1) { in.skip(size + size % 2); } else { in.skip(size * elemSize); } insn.setLength((size * elemSize + 1) / 2 + 4); } }; protected void readRegsList(DexInsnData insn, int opcodeUnit, SectionReader in) { int regsCount1 = nibble3(opcodeUnit); int index = in.readUShort(); int rs = in.readUShort(); int[] regs = insn.getArgsReg(); regs[0] = nibble0(rs); regs[1] = nibble1(rs); regs[2] = nibble2(rs); regs[3] = nibble3(rs); regs[4] = nibble2(opcodeUnit); insn.setRegsCount(regsCount1); insn.setIndex(index); } protected void readRegsRange(DexInsnData insn, int opcodeUnit, SectionReader in) { int regsCount = byte1(opcodeUnit); int index = in.readUShort(); int startReg = in.readUShort(); int[] regs = insn.getArgsReg(); if (regs.length < regsCount) { regs = new int[regsCount]; insn.setArgsReg(regs); } int regNum = startReg; for (int i = 0; i < regsCount; i++) { regs[i] = regNum; regNum++; } insn.setRegsCount(regsCount); insn.setIndex(index); } private final int length; private final int regsCount; protected DexInsnFormat(int length, int regsCount) { this.length = length; this.regsCount = regsCount; } public abstract void decode(DexInsnData insn, int opcodeUnit, SectionReader in); public void skip(DexInsnData insn, SectionReader in) { int len = this.length; if (len == 1) { return; } in.skip((len - 1) * 2); } public int getLength() { return length; } public int getRegsCount() { return regsCount; } private static int byte0(int value) { return value & 0xFF; } private static int byte1(int value) { return (value >> 8) & 0xFF; } private static int signedByte1(int value) { return (byte) (value >> 8); } private static int nibble0(int value) { return value & 0xF; } private static int nibble1(int value) { return (value >> 4) & 0xF; } private static int nibble2(int value) { return (value >> 8) & 0xF; } private static int nibble3(int value) { return (value >> 12) & 0xF; } private static int signedNibble3(int value) { return (((value >> 12) & 0xF) << 28) >> 28; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexInsnInfo.java ================================================ package jadx.plugins.input.dex.insns; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.insns.InsnIndexType; import jadx.api.plugins.input.insns.Opcode; public class DexInsnInfo { private static final DexInsnInfo[] INSN_INFO; private static final Map PAYLOAD_INFO; static { DexInsnInfo[] arr = new DexInsnInfo[0x100]; INSN_INFO = arr; register(arr, DexOpcodes.NOP, Opcode.NOP, DexInsnFormat.FORMAT_10X); register(arr, DexOpcodes.MOVE, Opcode.MOVE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.MOVE_FROM16, Opcode.MOVE, DexInsnFormat.FORMAT_22X); register(arr, DexOpcodes.MOVE_16, Opcode.MOVE, DexInsnFormat.FORMAT_32X); register(arr, DexOpcodes.MOVE_WIDE, Opcode.MOVE_WIDE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.MOVE_WIDE_FROM16, Opcode.MOVE_WIDE, DexInsnFormat.FORMAT_22X); register(arr, DexOpcodes.MOVE_WIDE_16, Opcode.MOVE_WIDE, DexInsnFormat.FORMAT_32X); register(arr, DexOpcodes.MOVE_OBJECT, Opcode.MOVE_OBJECT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.MOVE_OBJECT_FROM16, Opcode.MOVE_OBJECT, DexInsnFormat.FORMAT_22X); register(arr, DexOpcodes.MOVE_OBJECT_16, Opcode.MOVE_OBJECT, DexInsnFormat.FORMAT_32X); register(arr, DexOpcodes.MOVE_RESULT, Opcode.MOVE_RESULT, DexInsnFormat.FORMAT_11X); register(arr, DexOpcodes.MOVE_RESULT_WIDE, Opcode.MOVE_RESULT, DexInsnFormat.FORMAT_11X); register(arr, DexOpcodes.MOVE_RESULT_OBJECT, Opcode.MOVE_RESULT, DexInsnFormat.FORMAT_11X); register(arr, DexOpcodes.MOVE_EXCEPTION, Opcode.MOVE_EXCEPTION, DexInsnFormat.FORMAT_11X); register(arr, DexOpcodes.RETURN_VOID, Opcode.RETURN_VOID, DexInsnFormat.FORMAT_10X); register(arr, DexOpcodes.RETURN, Opcode.RETURN, DexInsnFormat.FORMAT_11X); register(arr, DexOpcodes.RETURN_WIDE, Opcode.RETURN, DexInsnFormat.FORMAT_11X); register(arr, DexOpcodes.RETURN_OBJECT, Opcode.RETURN, DexInsnFormat.FORMAT_11X); register(arr, DexOpcodes.CONST_4, Opcode.CONST, DexInsnFormat.FORMAT_11N); register(arr, DexOpcodes.CONST_16, Opcode.CONST, DexInsnFormat.FORMAT_21S); register(arr, DexOpcodes.CONST, Opcode.CONST, DexInsnFormat.FORMAT_31I); register(arr, DexOpcodes.CONST_HIGH16, Opcode.CONST, DexInsnFormat.FORMAT_21H); register(arr, DexOpcodes.CONST_WIDE_16, Opcode.CONST_WIDE, DexInsnFormat.FORMAT_21S); register(arr, DexOpcodes.CONST_WIDE_32, Opcode.CONST_WIDE, DexInsnFormat.FORMAT_31I); register(arr, DexOpcodes.CONST_WIDE, Opcode.CONST_WIDE, DexInsnFormat.FORMAT_51I); register(arr, DexOpcodes.CONST_WIDE_HIGH16, Opcode.CONST_WIDE, DexInsnFormat.FORMAT_21H); register(arr, DexOpcodes.CONST_STRING, Opcode.CONST_STRING, DexInsnFormat.FORMAT_21C, InsnIndexType.STRING_REF); register(arr, DexOpcodes.CONST_STRING_JUMBO, Opcode.CONST_STRING, DexInsnFormat.FORMAT_31C, InsnIndexType.STRING_REF); register(arr, DexOpcodes.CONST_CLASS, Opcode.CONST_CLASS, DexInsnFormat.FORMAT_21C, InsnIndexType.TYPE_REF); register(arr, DexOpcodes.MONITOR_ENTER, Opcode.MONITOR_ENTER, DexInsnFormat.FORMAT_11X); register(arr, DexOpcodes.MONITOR_EXIT, Opcode.MONITOR_EXIT, DexInsnFormat.FORMAT_11X); register(arr, DexOpcodes.CHECK_CAST, Opcode.CHECK_CAST, DexInsnFormat.FORMAT_21C, InsnIndexType.TYPE_REF); register(arr, DexOpcodes.INSTANCE_OF, Opcode.INSTANCE_OF, DexInsnFormat.FORMAT_22C, InsnIndexType.TYPE_REF); register(arr, DexOpcodes.ARRAY_LENGTH, Opcode.ARRAY_LENGTH, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.NEW_INSTANCE, Opcode.NEW_INSTANCE, DexInsnFormat.FORMAT_21C, InsnIndexType.TYPE_REF); register(arr, DexOpcodes.NEW_ARRAY, Opcode.NEW_ARRAY, DexInsnFormat.FORMAT_22C, InsnIndexType.TYPE_REF); register(arr, DexOpcodes.FILLED_NEW_ARRAY, Opcode.FILLED_NEW_ARRAY, DexInsnFormat.FORMAT_35C, InsnIndexType.TYPE_REF); register(arr, DexOpcodes.FILLED_NEW_ARRAY_RANGE, Opcode.FILLED_NEW_ARRAY_RANGE, DexInsnFormat.FORMAT_3RC, InsnIndexType.TYPE_REF); register(arr, DexOpcodes.FILL_ARRAY_DATA, Opcode.FILL_ARRAY_DATA, DexInsnFormat.FORMAT_31T); register(arr, DexOpcodes.THROW, Opcode.THROW, DexInsnFormat.FORMAT_11X); register(arr, DexOpcodes.GOTO, Opcode.GOTO, DexInsnFormat.FORMAT_10T); register(arr, DexOpcodes.GOTO_16, Opcode.GOTO, DexInsnFormat.FORMAT_20T); register(arr, DexOpcodes.GOTO_32, Opcode.GOTO, DexInsnFormat.FORMAT_30T); register(arr, DexOpcodes.PACKED_SWITCH, Opcode.PACKED_SWITCH, DexInsnFormat.FORMAT_31T); register(arr, DexOpcodes.SPARSE_SWITCH, Opcode.SPARSE_SWITCH, DexInsnFormat.FORMAT_31T); register(arr, DexOpcodes.CMPL_FLOAT, Opcode.CMPL_FLOAT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.CMPG_FLOAT, Opcode.CMPG_FLOAT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.CMPL_DOUBLE, Opcode.CMPL_DOUBLE, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.CMPG_DOUBLE, Opcode.CMPG_DOUBLE, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.CMP_LONG, Opcode.CMP_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.IF_EQ, Opcode.IF_EQ, DexInsnFormat.FORMAT_22T); register(arr, DexOpcodes.IF_NE, Opcode.IF_NE, DexInsnFormat.FORMAT_22T); register(arr, DexOpcodes.IF_LT, Opcode.IF_LT, DexInsnFormat.FORMAT_22T); register(arr, DexOpcodes.IF_GE, Opcode.IF_GE, DexInsnFormat.FORMAT_22T); register(arr, DexOpcodes.IF_GT, Opcode.IF_GT, DexInsnFormat.FORMAT_22T); register(arr, DexOpcodes.IF_LE, Opcode.IF_LE, DexInsnFormat.FORMAT_22T); register(arr, DexOpcodes.IF_EQZ, Opcode.IF_EQZ, DexInsnFormat.FORMAT_21T); register(arr, DexOpcodes.IF_NEZ, Opcode.IF_NEZ, DexInsnFormat.FORMAT_21T); register(arr, DexOpcodes.IF_LTZ, Opcode.IF_LTZ, DexInsnFormat.FORMAT_21T); register(arr, DexOpcodes.IF_GEZ, Opcode.IF_GEZ, DexInsnFormat.FORMAT_21T); register(arr, DexOpcodes.IF_GTZ, Opcode.IF_GTZ, DexInsnFormat.FORMAT_21T); register(arr, DexOpcodes.IF_LEZ, Opcode.IF_LEZ, DexInsnFormat.FORMAT_21T); register(arr, DexOpcodes.AGET, Opcode.AGET, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.AGET_WIDE, Opcode.AGET_WIDE, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.AGET_OBJECT, Opcode.AGET_OBJECT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.AGET_BOOLEAN, Opcode.AGET_BOOLEAN, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.AGET_BYTE, Opcode.AGET_BYTE, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.AGET_CHAR, Opcode.AGET_CHAR, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.AGET_SHORT, Opcode.AGET_SHORT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.APUT, Opcode.APUT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.APUT_WIDE, Opcode.APUT_WIDE, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.APUT_OBJECT, Opcode.APUT_OBJECT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.APUT_BOOLEAN, Opcode.APUT_BOOLEAN, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.APUT_BYTE, Opcode.APUT_BYTE, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.APUT_CHAR, Opcode.APUT_CHAR, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.APUT_SHORT, Opcode.APUT_SHORT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.IGET, Opcode.IGET, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IGET_WIDE, Opcode.IGET, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IGET_OBJECT, Opcode.IGET, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IGET_BOOLEAN, Opcode.IGET, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IGET_BYTE, Opcode.IGET, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IGET_CHAR, Opcode.IGET, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IGET_SHORT, Opcode.IGET, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IPUT, Opcode.IPUT, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IPUT_WIDE, Opcode.IPUT, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IPUT_OBJECT, Opcode.IPUT, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IPUT_BOOLEAN, Opcode.IPUT, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IPUT_BYTE, Opcode.IPUT, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IPUT_CHAR, Opcode.IPUT, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.IPUT_SHORT, Opcode.IPUT, DexInsnFormat.FORMAT_22C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SGET, Opcode.SGET, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SGET_WIDE, Opcode.SGET, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SGET_OBJECT, Opcode.SGET, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SGET_BOOLEAN, Opcode.SGET, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SGET_BYTE, Opcode.SGET, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SGET_CHAR, Opcode.SGET, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SGET_SHORT, Opcode.SGET, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SPUT, Opcode.SPUT, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SPUT_WIDE, Opcode.SPUT, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SPUT_OBJECT, Opcode.SPUT, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SPUT_BOOLEAN, Opcode.SPUT, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SPUT_BYTE, Opcode.SPUT, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SPUT_CHAR, Opcode.SPUT, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.SPUT_SHORT, Opcode.SPUT, DexInsnFormat.FORMAT_21C, InsnIndexType.FIELD_REF); register(arr, DexOpcodes.INVOKE_VIRTUAL, Opcode.INVOKE_VIRTUAL, DexInsnFormat.FORMAT_35C, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.INVOKE_SUPER, Opcode.INVOKE_SUPER, DexInsnFormat.FORMAT_35C, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.INVOKE_DIRECT, Opcode.INVOKE_DIRECT, DexInsnFormat.FORMAT_35C, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.INVOKE_STATIC, Opcode.INVOKE_STATIC, DexInsnFormat.FORMAT_35C, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.INVOKE_INTERFACE, Opcode.INVOKE_INTERFACE, DexInsnFormat.FORMAT_35C, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.INVOKE_VIRTUAL_RANGE, Opcode.INVOKE_VIRTUAL_RANGE, DexInsnFormat.FORMAT_3RC, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.INVOKE_SUPER_RANGE, Opcode.INVOKE_SUPER_RANGE, DexInsnFormat.FORMAT_3RC, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.INVOKE_DIRECT_RANGE, Opcode.INVOKE_DIRECT_RANGE, DexInsnFormat.FORMAT_3RC, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.INVOKE_STATIC_RANGE, Opcode.INVOKE_STATIC_RANGE, DexInsnFormat.FORMAT_3RC, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.INVOKE_INTERFACE_RANGE, Opcode.INVOKE_INTERFACE_RANGE, DexInsnFormat.FORMAT_3RC, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.NEG_INT, Opcode.NEG_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.NOT_INT, Opcode.NOT_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.NEG_LONG, Opcode.NEG_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.NOT_LONG, Opcode.NOT_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.NEG_FLOAT, Opcode.NEG_FLOAT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.NEG_DOUBLE, Opcode.NEG_DOUBLE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.INT_TO_LONG, Opcode.INT_TO_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.INT_TO_FLOAT, Opcode.INT_TO_FLOAT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.INT_TO_DOUBLE, Opcode.INT_TO_DOUBLE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.LONG_TO_INT, Opcode.LONG_TO_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.LONG_TO_FLOAT, Opcode.LONG_TO_FLOAT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.LONG_TO_DOUBLE, Opcode.LONG_TO_DOUBLE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.FLOAT_TO_INT, Opcode.FLOAT_TO_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.FLOAT_TO_LONG, Opcode.FLOAT_TO_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.FLOAT_TO_DOUBLE, Opcode.FLOAT_TO_DOUBLE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.DOUBLE_TO_INT, Opcode.DOUBLE_TO_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.DOUBLE_TO_LONG, Opcode.DOUBLE_TO_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.DOUBLE_TO_FLOAT, Opcode.DOUBLE_TO_FLOAT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.INT_TO_BYTE, Opcode.INT_TO_BYTE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.INT_TO_CHAR, Opcode.INT_TO_CHAR, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.INT_TO_SHORT, Opcode.INT_TO_SHORT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.ADD_INT, Opcode.ADD_INT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.SUB_INT, Opcode.SUB_INT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.MUL_INT, Opcode.MUL_INT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.DIV_INT, Opcode.DIV_INT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.REM_INT, Opcode.REM_INT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.AND_INT, Opcode.AND_INT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.OR_INT, Opcode.OR_INT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.XOR_INT, Opcode.XOR_INT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.SHL_INT, Opcode.SHL_INT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.SHR_INT, Opcode.SHR_INT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.USHR_INT, Opcode.USHR_INT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.ADD_LONG, Opcode.ADD_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.SUB_LONG, Opcode.SUB_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.MUL_LONG, Opcode.MUL_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.DIV_LONG, Opcode.DIV_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.REM_LONG, Opcode.REM_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.AND_LONG, Opcode.AND_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.OR_LONG, Opcode.OR_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.XOR_LONG, Opcode.XOR_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.SHL_LONG, Opcode.SHL_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.SHR_LONG, Opcode.SHR_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.USHR_LONG, Opcode.USHR_LONG, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.ADD_FLOAT, Opcode.ADD_FLOAT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.SUB_FLOAT, Opcode.SUB_FLOAT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.MUL_FLOAT, Opcode.MUL_FLOAT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.DIV_FLOAT, Opcode.DIV_FLOAT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.REM_FLOAT, Opcode.REM_FLOAT, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.ADD_DOUBLE, Opcode.ADD_DOUBLE, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.SUB_DOUBLE, Opcode.SUB_DOUBLE, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.MUL_DOUBLE, Opcode.MUL_DOUBLE, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.DIV_DOUBLE, Opcode.DIV_DOUBLE, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.REM_DOUBLE, Opcode.REM_DOUBLE, DexInsnFormat.FORMAT_23X); register(arr, DexOpcodes.ADD_INT_2ADDR, Opcode.ADD_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.SUB_INT_2ADDR, Opcode.SUB_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.MUL_INT_2ADDR, Opcode.MUL_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.DIV_INT_2ADDR, Opcode.DIV_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.REM_INT_2ADDR, Opcode.REM_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.AND_INT_2ADDR, Opcode.AND_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.OR_INT_2ADDR, Opcode.OR_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.XOR_INT_2ADDR, Opcode.XOR_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.SHL_INT_2ADDR, Opcode.SHL_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.SHR_INT_2ADDR, Opcode.SHR_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.USHR_INT_2ADDR, Opcode.USHR_INT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.ADD_LONG_2ADDR, Opcode.ADD_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.SUB_LONG_2ADDR, Opcode.SUB_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.MUL_LONG_2ADDR, Opcode.MUL_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.DIV_LONG_2ADDR, Opcode.DIV_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.REM_LONG_2ADDR, Opcode.REM_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.AND_LONG_2ADDR, Opcode.AND_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.OR_LONG_2ADDR, Opcode.OR_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.XOR_LONG_2ADDR, Opcode.XOR_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.SHL_LONG_2ADDR, Opcode.SHL_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.SHR_LONG_2ADDR, Opcode.SHR_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.USHR_LONG_2ADDR, Opcode.USHR_LONG, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.ADD_FLOAT_2ADDR, Opcode.ADD_FLOAT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.SUB_FLOAT_2ADDR, Opcode.SUB_FLOAT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.MUL_FLOAT_2ADDR, Opcode.MUL_FLOAT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.DIV_FLOAT_2ADDR, Opcode.DIV_FLOAT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.REM_FLOAT_2ADDR, Opcode.REM_FLOAT, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.ADD_DOUBLE_2ADDR, Opcode.ADD_DOUBLE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.SUB_DOUBLE_2ADDR, Opcode.SUB_DOUBLE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.MUL_DOUBLE_2ADDR, Opcode.MUL_DOUBLE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.DIV_DOUBLE_2ADDR, Opcode.DIV_DOUBLE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.REM_DOUBLE_2ADDR, Opcode.REM_DOUBLE, DexInsnFormat.FORMAT_12X); register(arr, DexOpcodes.ADD_INT_LIT16, Opcode.ADD_INT_LIT, DexInsnFormat.FORMAT_22S); register(arr, DexOpcodes.RSUB_INT, Opcode.RSUB_INT, DexInsnFormat.FORMAT_22S); register(arr, DexOpcodes.MUL_INT_LIT16, Opcode.MUL_INT_LIT, DexInsnFormat.FORMAT_22S); register(arr, DexOpcodes.DIV_INT_LIT16, Opcode.DIV_INT_LIT, DexInsnFormat.FORMAT_22S); register(arr, DexOpcodes.REM_INT_LIT16, Opcode.REM_INT_LIT, DexInsnFormat.FORMAT_22S); register(arr, DexOpcodes.AND_INT_LIT16, Opcode.AND_INT_LIT, DexInsnFormat.FORMAT_22S); register(arr, DexOpcodes.OR_INT_LIT16, Opcode.OR_INT_LIT, DexInsnFormat.FORMAT_22S); register(arr, DexOpcodes.XOR_INT_LIT16, Opcode.XOR_INT_LIT, DexInsnFormat.FORMAT_22S); register(arr, DexOpcodes.ADD_INT_LIT8, Opcode.ADD_INT_LIT, DexInsnFormat.FORMAT_22B); register(arr, DexOpcodes.RSUB_INT_LIT8, Opcode.RSUB_INT, DexInsnFormat.FORMAT_22B); register(arr, DexOpcodes.MUL_INT_LIT8, Opcode.MUL_INT_LIT, DexInsnFormat.FORMAT_22B); register(arr, DexOpcodes.DIV_INT_LIT8, Opcode.DIV_INT_LIT, DexInsnFormat.FORMAT_22B); register(arr, DexOpcodes.REM_INT_LIT8, Opcode.REM_INT_LIT, DexInsnFormat.FORMAT_22B); register(arr, DexOpcodes.AND_INT_LIT8, Opcode.AND_INT_LIT, DexInsnFormat.FORMAT_22B); register(arr, DexOpcodes.OR_INT_LIT8, Opcode.OR_INT_LIT, DexInsnFormat.FORMAT_22B); register(arr, DexOpcodes.XOR_INT_LIT8, Opcode.XOR_INT_LIT, DexInsnFormat.FORMAT_22B); register(arr, DexOpcodes.SHL_INT_LIT8, Opcode.SHL_INT_LIT, DexInsnFormat.FORMAT_22B); register(arr, DexOpcodes.SHR_INT_LIT8, Opcode.SHR_INT_LIT, DexInsnFormat.FORMAT_22B); register(arr, DexOpcodes.USHR_INT_LIT8, Opcode.USHR_INT_LIT, DexInsnFormat.FORMAT_22B); register(arr, DexOpcodes.INVOKE_POLYMORPHIC, Opcode.INVOKE_POLYMORPHIC, DexInsnFormat.FORMAT_45CC, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.INVOKE_POLYMORPHIC_RANGE, Opcode.INVOKE_POLYMORPHIC_RANGE, DexInsnFormat.FORMAT_4RCC, InsnIndexType.METHOD_REF); register(arr, DexOpcodes.INVOKE_CUSTOM, Opcode.INVOKE_CUSTOM, DexInsnFormat.FORMAT_35C, InsnIndexType.CALL_SITE); register(arr, DexOpcodes.INVOKE_CUSTOM_RANGE, Opcode.INVOKE_CUSTOM_RANGE, DexInsnFormat.FORMAT_3RC, InsnIndexType.CALL_SITE); register(arr, DexOpcodes.CONST_METHOD_HANDLE, Opcode.CONST_METHOD_HANDLE, DexInsnFormat.FORMAT_21C); register(arr, DexOpcodes.CONST_METHOD_TYPE, Opcode.CONST_METHOD_TYPE, DexInsnFormat.FORMAT_21C); PAYLOAD_INFO = new ConcurrentHashMap<>(3); registerPayload(DexOpcodes.PACKED_SWITCH_PAYLOAD, Opcode.PACKED_SWITCH_PAYLOAD, DexInsnFormat.FORMAT_PACKED_SWITCH_PAYLOAD); registerPayload(DexOpcodes.SPARSE_SWITCH_PAYLOAD, Opcode.SPARSE_SWITCH_PAYLOAD, DexInsnFormat.FORMAT_SPARSE_SWITCH_PAYLOAD); registerPayload(DexOpcodes.FILL_ARRAY_DATA_PAYLOAD, Opcode.FILL_ARRAY_DATA_PAYLOAD, DexInsnFormat.FORMAT_FILL_ARRAY_DATA_PAYLOAD); } private static void register(DexInsnInfo[] arr, int opcode, Opcode apiOpcode, DexInsnFormat format) { arr[opcode] = new DexInsnInfo(opcode, apiOpcode, format, InsnIndexType.NONE); } private static void register(DexInsnInfo[] arr, int opcode, Opcode apiOpcode, DexInsnFormat format, InsnIndexType indexType) { arr[opcode] = new DexInsnInfo(opcode, apiOpcode, format, indexType); } private static void registerPayload(int opcode, Opcode apiOpcode, DexInsnFormat format) { PAYLOAD_INFO.put(opcode, new DexInsnInfo(opcode, apiOpcode, format, InsnIndexType.NONE)); } @Nullable public static DexInsnInfo get(int opcodeUnit) { int opcode = opcodeUnit & 0xFF; if (opcode == 0 && opcodeUnit != 0) { return PAYLOAD_INFO.get(opcodeUnit); } return INSN_INFO[opcode]; } private final int opcode; private final Opcode apiOpcode; private final DexInsnFormat format; private final InsnIndexType indexType; public DexInsnInfo(int opcode, Opcode apiOpcode, DexInsnFormat format, InsnIndexType indexType) { this.opcode = opcode; this.apiOpcode = apiOpcode; this.format = format; this.indexType = indexType; } public int getOpcode() { return opcode; } public Opcode getApiOpcode() { return apiOpcode; } public DexInsnFormat getFormat() { return format; } public InsnIndexType getIndexType() { return indexType; } @Override public String toString() { return String.format("0x%X :%d%d", opcode, format.getLength(), format.getRegsCount()); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexInsnMnemonics.java ================================================ package jadx.plugins.input.dex.insns; public class DexInsnMnemonics { public static String get(int opcode) { return MNEMONICS[opcode & 0xFF]; } private static final String[] MNEMONICS = new String[] { "nop", "move", "move/from16", "move/16", "move-wide", "move-wide/from16", "move-wide/16", "move-object", "move-object/from16", "move-object/16", "move-result", "move-result-wide", "move-result-object", "move-exception", "return-void", "return", "return-wide", "return-object", "const/4", "const/16", "const", "const/high16", "const-wide/16", "const-wide/32", "const-wide", "const-wide/high16", "const-string", "const-string/jumbo", "const-class", "monitor-enter", "monitor-exit", "check-cast", "instance-of", "array-length", "new-instance", "new-array", "filled-new-array", "filled-new-array/range", "fill-array-data", "throw", "goto", "goto/16", "goto/32", "packed-switch", "sparse-switch", "cmpl-float", "cmpg-float", "cmpl-double", "cmpg-double", "cmp-long", "if-eq", "if-ne", "if-lt", "if-ge", "if-gt", "if-le", "if-eqz", "if-nez", "if-ltz", "if-gez", "if-gtz", "if-lez", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "aget", "aget-wide", "aget-object", "aget-boolean", "aget-byte", "aget-char", "aget-short", "aput", "aput-wide", "aput-object", "aput-boolean", "aput-byte", "aput-char", "aput-short", "iget", "iget-wide", "iget-object", "iget-boolean", "iget-byte", "iget-char", "iget-short", "iput", "iput-wide", "iput-object", "iput-boolean", "iput-byte", "iput-char", "iput-short", "sget", "sget-wide", "sget-object", "sget-boolean", "sget-byte", "sget-char", "sget-short", "sput", "sput-wide", "sput-object", "sput-boolean", "sput-byte", "sput-char", "sput-short", "invoke-virtual", "invoke-super", "invoke-direct", "invoke-static", "invoke-interface", "(unused)", "invoke-virtual/range", "invoke-super/range", "invoke-direct/range", "invoke-static/range", "invoke-interface/range", "(unused)", "(unused)", "neg-int", "not-int", "neg-long", "not-long", "neg-float", "neg-double", "int-to-long", "int-to-float", "int-to-double", "long-to-int", "long-to-float", "long-to-double", "float-to-int", "float-to-long", "float-to-double", "double-to-int", "double-to-long", "double-to-float", "int-to-byte", "int-to-char", "int-to-short", "add-int", "sub-int", "mul-int", "div-int", "rem-int", "and-int", "or-int", "xor-int", "shl-int", "shr-int", "ushr-int", "add-long", "sub-long", "mul-long", "div-long", "rem-long", "and-long", "or-long", "xor-long", "shl-long", "shr-long", "ushr-long", "add-float", "sub-float", "mul-float", "div-float", "rem-float", "add-double", "sub-double", "mul-double", "div-double", "rem-double", "add-int/2addr", "sub-int/2addr", "mul-int/2addr", "div-int/2addr", "rem-int/2addr", "and-int/2addr", "or-int/2addr", "xor-int/2addr", "shl-int/2addr", "shr-int/2addr", "ushr-int/2addr", "add-long/2addr", "sub-long/2addr", "mul-long/2addr", "div-long/2addr", "rem-long/2addr", "and-long/2addr", "or-long/2addr", "xor-long/2addr", "shl-long/2addr", "shr-long/2addr", "ushr-long/2addr", "add-float/2addr", "sub-float/2addr", "mul-float/2addr", "div-float/2addr", "rem-float/2addr", "add-double/2addr", "sub-double/2addr", "mul-double/2addr", "div-double/2addr", "rem-double/2addr", "add-int/lit16", "rsub-int", "mul-int/lit16", "div-int/lit16", "rem-int/lit16", "and-int/lit16", "or-int/lit16", "xor-int/lit16", "add-int/lit8", "rsub-int/lit8", "mul-int/lit8", "div-int/lit8", "rem-int/lit8", "and-int/lit8", "or-int/lit8", "xor-int/lit8", "shl-int/lit8", "shr-int/lit8", "ushr-int/lit8", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "(unused)", "invoke-polymorphic", "invoke-polymorphic/range", "invoke-custom", "invoke-custom/range", "const-method-handle", "const-method-type" }; } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexOpcodes.java ================================================ package jadx.plugins.input.dex.insns; public class DexOpcodes { public static final int NOP = 0x00; public static final int MOVE = 0x01; public static final int MOVE_FROM16 = 0x02; public static final int MOVE_16 = 0x03; public static final int MOVE_WIDE = 0x04; public static final int MOVE_WIDE_FROM16 = 0x05; public static final int MOVE_WIDE_16 = 0x06; public static final int MOVE_OBJECT = 0x07; public static final int MOVE_OBJECT_FROM16 = 0x08; public static final int MOVE_OBJECT_16 = 0x09; public static final int MOVE_RESULT = 0x0a; public static final int MOVE_RESULT_WIDE = 0x0b; public static final int MOVE_RESULT_OBJECT = 0x0c; public static final int MOVE_EXCEPTION = 0x0d; public static final int RETURN_VOID = 0x0e; public static final int RETURN = 0x0f; public static final int RETURN_WIDE = 0x10; public static final int RETURN_OBJECT = 0x11; public static final int CONST_4 = 0x12; public static final int CONST_16 = 0x13; public static final int CONST = 0x14; public static final int CONST_HIGH16 = 0x15; public static final int CONST_WIDE_16 = 0x16; public static final int CONST_WIDE_32 = 0x17; public static final int CONST_WIDE = 0x18; public static final int CONST_WIDE_HIGH16 = 0x19; public static final int CONST_STRING = 0x1a; public static final int CONST_STRING_JUMBO = 0x1b; public static final int CONST_CLASS = 0x1c; public static final int MONITOR_ENTER = 0x1d; public static final int MONITOR_EXIT = 0x1e; public static final int CHECK_CAST = 0x1f; public static final int INSTANCE_OF = 0x20; public static final int ARRAY_LENGTH = 0x21; public static final int NEW_INSTANCE = 0x22; public static final int NEW_ARRAY = 0x23; public static final int FILLED_NEW_ARRAY = 0x24; public static final int FILLED_NEW_ARRAY_RANGE = 0x25; public static final int FILL_ARRAY_DATA = 0x26; public static final int THROW = 0x27; public static final int GOTO = 0x28; public static final int GOTO_16 = 0x29; public static final int GOTO_32 = 0x2a; public static final int PACKED_SWITCH = 0x2b; public static final int SPARSE_SWITCH = 0x2c; public static final int CMPL_FLOAT = 0x2d; public static final int CMPG_FLOAT = 0x2e; public static final int CMPL_DOUBLE = 0x2f; public static final int CMPG_DOUBLE = 0x30; public static final int CMP_LONG = 0x31; public static final int IF_EQ = 0x32; public static final int IF_NE = 0x33; public static final int IF_LT = 0x34; public static final int IF_GE = 0x35; public static final int IF_GT = 0x36; public static final int IF_LE = 0x37; public static final int IF_EQZ = 0x38; public static final int IF_NEZ = 0x39; public static final int IF_LTZ = 0x3a; public static final int IF_GEZ = 0x3b; public static final int IF_GTZ = 0x3c; public static final int IF_LEZ = 0x3d; public static final int AGET = 0x44; public static final int AGET_WIDE = 0x45; public static final int AGET_OBJECT = 0x46; public static final int AGET_BOOLEAN = 0x47; public static final int AGET_BYTE = 0x48; public static final int AGET_CHAR = 0x49; public static final int AGET_SHORT = 0x4a; public static final int APUT = 0x4b; public static final int APUT_WIDE = 0x4c; public static final int APUT_OBJECT = 0x4d; public static final int APUT_BOOLEAN = 0x4e; public static final int APUT_BYTE = 0x4f; public static final int APUT_CHAR = 0x50; public static final int APUT_SHORT = 0x51; public static final int IGET = 0x52; public static final int IGET_WIDE = 0x53; public static final int IGET_OBJECT = 0x54; public static final int IGET_BOOLEAN = 0x55; public static final int IGET_BYTE = 0x56; public static final int IGET_CHAR = 0x57; public static final int IGET_SHORT = 0x58; public static final int IPUT = 0x59; public static final int IPUT_WIDE = 0x5a; public static final int IPUT_OBJECT = 0x5b; public static final int IPUT_BOOLEAN = 0x5c; public static final int IPUT_BYTE = 0x5d; public static final int IPUT_CHAR = 0x5e; public static final int IPUT_SHORT = 0x5f; public static final int SGET = 0x60; public static final int SGET_WIDE = 0x61; public static final int SGET_OBJECT = 0x62; public static final int SGET_BOOLEAN = 0x63; public static final int SGET_BYTE = 0x64; public static final int SGET_CHAR = 0x65; public static final int SGET_SHORT = 0x66; public static final int SPUT = 0x67; public static final int SPUT_WIDE = 0x68; public static final int SPUT_OBJECT = 0x69; public static final int SPUT_BOOLEAN = 0x6a; public static final int SPUT_BYTE = 0x6b; public static final int SPUT_CHAR = 0x6c; public static final int SPUT_SHORT = 0x6d; public static final int INVOKE_VIRTUAL = 0x6e; public static final int INVOKE_SUPER = 0x6f; public static final int INVOKE_DIRECT = 0x70; public static final int INVOKE_STATIC = 0x71; public static final int INVOKE_INTERFACE = 0x72; public static final int INVOKE_VIRTUAL_RANGE = 0x74; public static final int INVOKE_SUPER_RANGE = 0x75; public static final int INVOKE_DIRECT_RANGE = 0x76; public static final int INVOKE_STATIC_RANGE = 0x77; public static final int INVOKE_INTERFACE_RANGE = 0x78; public static final int NEG_INT = 0x7b; public static final int NOT_INT = 0x7c; public static final int NEG_LONG = 0x7d; public static final int NOT_LONG = 0x7e; public static final int NEG_FLOAT = 0x7f; public static final int NEG_DOUBLE = 0x80; public static final int INT_TO_LONG = 0x81; public static final int INT_TO_FLOAT = 0x82; public static final int INT_TO_DOUBLE = 0x83; public static final int LONG_TO_INT = 0x84; public static final int LONG_TO_FLOAT = 0x85; public static final int LONG_TO_DOUBLE = 0x86; public static final int FLOAT_TO_INT = 0x87; public static final int FLOAT_TO_LONG = 0x88; public static final int FLOAT_TO_DOUBLE = 0x89; public static final int DOUBLE_TO_INT = 0x8a; public static final int DOUBLE_TO_LONG = 0x8b; public static final int DOUBLE_TO_FLOAT = 0x8c; public static final int INT_TO_BYTE = 0x8d; public static final int INT_TO_CHAR = 0x8e; public static final int INT_TO_SHORT = 0x8f; public static final int ADD_INT = 0x90; public static final int SUB_INT = 0x91; public static final int MUL_INT = 0x92; public static final int DIV_INT = 0x93; public static final int REM_INT = 0x94; public static final int AND_INT = 0x95; public static final int OR_INT = 0x96; public static final int XOR_INT = 0x97; public static final int SHL_INT = 0x98; public static final int SHR_INT = 0x99; public static final int USHR_INT = 0x9a; public static final int ADD_LONG = 0x9b; public static final int SUB_LONG = 0x9c; public static final int MUL_LONG = 0x9d; public static final int DIV_LONG = 0x9e; public static final int REM_LONG = 0x9f; public static final int AND_LONG = 0xa0; public static final int OR_LONG = 0xa1; public static final int XOR_LONG = 0xa2; public static final int SHL_LONG = 0xa3; public static final int SHR_LONG = 0xa4; public static final int USHR_LONG = 0xa5; public static final int ADD_FLOAT = 0xa6; public static final int SUB_FLOAT = 0xa7; public static final int MUL_FLOAT = 0xa8; public static final int DIV_FLOAT = 0xa9; public static final int REM_FLOAT = 0xaa; public static final int ADD_DOUBLE = 0xab; public static final int SUB_DOUBLE = 0xac; public static final int MUL_DOUBLE = 0xad; public static final int DIV_DOUBLE = 0xae; public static final int REM_DOUBLE = 0xaf; public static final int ADD_INT_2ADDR = 0xb0; public static final int SUB_INT_2ADDR = 0xb1; public static final int MUL_INT_2ADDR = 0xb2; public static final int DIV_INT_2ADDR = 0xb3; public static final int REM_INT_2ADDR = 0xb4; public static final int AND_INT_2ADDR = 0xb5; public static final int OR_INT_2ADDR = 0xb6; public static final int XOR_INT_2ADDR = 0xb7; public static final int SHL_INT_2ADDR = 0xb8; public static final int SHR_INT_2ADDR = 0xb9; public static final int USHR_INT_2ADDR = 0xba; public static final int ADD_LONG_2ADDR = 0xbb; public static final int SUB_LONG_2ADDR = 0xbc; public static final int MUL_LONG_2ADDR = 0xbd; public static final int DIV_LONG_2ADDR = 0xbe; public static final int REM_LONG_2ADDR = 0xbf; public static final int AND_LONG_2ADDR = 0xc0; public static final int OR_LONG_2ADDR = 0xc1; public static final int XOR_LONG_2ADDR = 0xc2; public static final int SHL_LONG_2ADDR = 0xc3; public static final int SHR_LONG_2ADDR = 0xc4; public static final int USHR_LONG_2ADDR = 0xc5; public static final int ADD_FLOAT_2ADDR = 0xc6; public static final int SUB_FLOAT_2ADDR = 0xc7; public static final int MUL_FLOAT_2ADDR = 0xc8; public static final int DIV_FLOAT_2ADDR = 0xc9; public static final int REM_FLOAT_2ADDR = 0xca; public static final int ADD_DOUBLE_2ADDR = 0xcb; public static final int SUB_DOUBLE_2ADDR = 0xcc; public static final int MUL_DOUBLE_2ADDR = 0xcd; public static final int DIV_DOUBLE_2ADDR = 0xce; public static final int REM_DOUBLE_2ADDR = 0xcf; public static final int ADD_INT_LIT16 = 0xd0; public static final int RSUB_INT = 0xd1; public static final int MUL_INT_LIT16 = 0xd2; public static final int DIV_INT_LIT16 = 0xd3; public static final int REM_INT_LIT16 = 0xd4; public static final int AND_INT_LIT16 = 0xd5; public static final int OR_INT_LIT16 = 0xd6; public static final int XOR_INT_LIT16 = 0xd7; public static final int ADD_INT_LIT8 = 0xd8; public static final int RSUB_INT_LIT8 = 0xd9; public static final int MUL_INT_LIT8 = 0xda; public static final int DIV_INT_LIT8 = 0xdb; public static final int REM_INT_LIT8 = 0xdc; public static final int AND_INT_LIT8 = 0xdd; public static final int OR_INT_LIT8 = 0xde; public static final int XOR_INT_LIT8 = 0xdf; public static final int SHL_INT_LIT8 = 0xe0; public static final int SHR_INT_LIT8 = 0xe1; public static final int USHR_INT_LIT8 = 0xe2; public static final int INVOKE_POLYMORPHIC = 0xfa; public static final int INVOKE_POLYMORPHIC_RANGE = 0xfb; public static final int INVOKE_CUSTOM = 0xfc; public static final int INVOKE_CUSTOM_RANGE = 0xfd; public static final int CONST_METHOD_HANDLE = 0xfe; public static final int CONST_METHOD_TYPE = 0xff; // payload pseudo-instructions public static final int PACKED_SWITCH_PAYLOAD = 0x0100; public static final int SPARSE_SWITCH_PAYLOAD = 0x0200; public static final int FILL_ARRAY_DATA_PAYLOAD = 0x0300; } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/payloads/DexArrayPayload.java ================================================ package jadx.plugins.input.dex.insns.payloads; import jadx.api.plugins.input.insns.custom.IArrayPayload; public class DexArrayPayload implements IArrayPayload { private final int size; private final int elemSize; private final Object data; public DexArrayPayload(int size, int elemSize, Object data) { this.size = size; this.elemSize = elemSize; this.data = data; } @Override public int getSize() { return size; } @Override public int getElementSize() { return elemSize; } @Override public Object getData() { return data; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexAnnotationsConvert.java ================================================ package jadx.plugins.input.dex.sections; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.annotations.AnnotationVisibility; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.types.AnnotationDefaultClassAttr; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.api.plugins.input.data.attributes.types.ExceptionsAttr; import jadx.api.plugins.input.data.attributes.types.InnerClassesAttr; import jadx.api.plugins.input.data.attributes.types.InnerClsInfo; import jadx.api.plugins.input.data.attributes.types.MethodParametersAttr; import jadx.api.plugins.input.data.attributes.types.SignatureAttr; import jadx.api.plugins.utils.Utils; import jadx.plugins.input.dex.sections.annotations.AnnotationsUtils; public class DexAnnotationsConvert { private static final Logger LOG = LoggerFactory.getLogger(DexAnnotationsConvert.class); public static void forClass(String cls, List list, List annotationList) { appendAnnotations(cls, list, annotationList); } public static void forMethod(List list, List annotationList) { appendAnnotations(null, list, annotationList); } public static void forField(List list, List annotationList) { appendAnnotations(null, list, annotationList); } private static void appendAnnotations(@Nullable String cls, List attributes, List annotations) { if (annotations.isEmpty()) { return; } for (IAnnotation annotation : annotations) { if (annotation.getVisibility() == AnnotationVisibility.SYSTEM) { convertSystemAnnotations(cls, attributes, annotation); } } Utils.addToList(attributes, AnnotationsAttr.pack(annotations)); } @SuppressWarnings("unchecked") private static void convertSystemAnnotations(@Nullable String cls, List attributes, IAnnotation annotation) { switch (annotation.getAnnotationClass()) { case "Ldalvik/annotation/Signature;": attributes.add(new SignatureAttr(extractSignature(annotation))); break; case "Ldalvik/annotation/InnerClass;": try { String name = AnnotationsUtils.getValue(annotation, "name", EncodedType.ENCODED_STRING, null); int accFlags = AnnotationsUtils.getValue(annotation, "accessFlags", EncodedType.ENCODED_INT, 0); if (name != null || accFlags != 0) { InnerClsInfo innerClsInfo = new InnerClsInfo(cls, null, name, accFlags); attributes.add(new InnerClassesAttr(Collections.singletonMap(cls, innerClsInfo))); } } catch (Exception e) { LOG.warn("Failed to parse annotation: {}", annotation, e); } break; case "Ldalvik/annotation/AnnotationDefault;": EncodedValue annValue = annotation.getDefaultValue(); if (annValue != null && annValue.getType() == EncodedType.ENCODED_ANNOTATION) { IAnnotation defAnnotation = (IAnnotation) annValue.getValue(); attributes.add(new AnnotationDefaultClassAttr(defAnnotation.getValues())); } break; case "Ldalvik/annotation/Throws;": try { EncodedValue defaultValue = annotation.getDefaultValue(); if (defaultValue != null) { List excs = ((List) defaultValue.getValue()) .stream() .map(ev -> ((String) ev.getValue())) .collect(Collectors.toList()); attributes.add(new ExceptionsAttr(excs)); } } catch (Exception e) { LOG.warn("Failed to convert dalvik throws annotation", e); } break; case "Ldalvik/annotation/MethodParameters;": try { List names = AnnotationsUtils.getArray(annotation, "names"); List accFlags = AnnotationsUtils.getArray(annotation, "accessFlags"); if (!names.isEmpty() && names.size() == accFlags.size()) { int size = names.size(); List list = new ArrayList<>(size); for (int i = 0; i < size; i++) { String name = (String) names.get(i).getValue(); int accFlag = (int) accFlags.get(i).getValue(); list.add(new MethodParametersAttr.Info(accFlag, name)); } attributes.add(new MethodParametersAttr(list)); } } catch (Exception e) { LOG.warn("Failed to parse annotation: {}", annotation, e); } break; } } @SuppressWarnings({ "unchecked", "ConstantConditions" }) private static String extractSignature(IAnnotation annotation) { List values = (List) annotation.getDefaultValue().getValue(); if (values.size() == 1) { return (String) values.get(0).getValue(); } StringBuilder sb = new StringBuilder(); for (EncodedValue part : values) { sb.append((String) part.getValue()); } return sb.toString(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java ================================================ package jadx.plugins.input.dex.sections; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.input.data.IFieldData; import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.data.ISeqConsumer; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.types.SourceFileAttr; import jadx.plugins.input.dex.sections.annotations.AnnotationsParser; import jadx.plugins.input.dex.utils.SmaliUtils; public class DexClassData implements IClassData { private static final Logger LOG = LoggerFactory.getLogger(DexClassData.class); public static final int SIZE = 8 * 4; private final SectionReader in; private final AnnotationsParser annotationsParser; private final int inputFileOffset; public DexClassData(SectionReader sectionReader, AnnotationsParser annotationsParser) { this.in = sectionReader; this.annotationsParser = annotationsParser; this.inputFileOffset = this.in.getOffset(); } @Override public int getInputFileOffset() { return this.inputFileOffset; } @Override public IClassData copy() { return new DexClassData(in.copy(), annotationsParser.copy()); } @Override public String getType() { int typeIdx = in.pos(0).readInt(); String clsType = in.getType(typeIdx); if (clsType == null) { throw new NullPointerException("Unknown class type"); } return clsType; } @Override public int getAccessFlags() { return in.pos(4).readInt(); } @Nullable @Override public String getSuperType() { int typeIdx = in.pos(2 * 4).readInt(); return in.getType(typeIdx); } @Override public List getInterfacesTypes() { int offset = in.pos(3 * 4).readInt(); if (offset == 0) { return Collections.emptyList(); } return in.absPos(offset).readTypeList(); } @Nullable private String getSourceFile() { int strIdx = in.pos(4 * 4).readInt(); return in.getString(strIdx); } @Override public String getInputFileName() { return in.getDexReader().getInputFileName(); } public int getAnnotationsOff() { return in.pos(5 * 4).readInt(); } public int getClassDataOff() { return in.pos(6 * 4).readInt(); } public int getStaticValuesOff() { return in.pos(7 * 4).readInt(); } @Override public void visitFieldsAndMethods(ISeqConsumer fieldConsumer, ISeqConsumer mthConsumer) { int classDataOff = getClassDataOff(); if (classDataOff == 0) { return; } SectionReader data = in.copy(classDataOff); int staticFieldsCount = data.readUleb128(); int instanceFieldsCount = data.readUleb128(); int directMthCount = data.readUleb128(); int virtualMthCount = data.readUleb128(); fieldConsumer.init(staticFieldsCount + instanceFieldsCount); mthConsumer.init(directMthCount + virtualMthCount); annotationsParser.setOffset(getAnnotationsOff()); visitFields(fieldConsumer, data, staticFieldsCount, instanceFieldsCount); visitMethods(mthConsumer, data, directMthCount, virtualMthCount); } private void visitFields(Consumer fieldConsumer, SectionReader data, int staticFieldsCount, int instanceFieldsCount) { Map annotationOffsetMap = annotationsParser.readFieldsAnnotationOffsetMap(); DexFieldData fieldData = new DexFieldData(annotationsParser); fieldData.setParentClassType(getType()); readFields(fieldConsumer, data, fieldData, staticFieldsCount, annotationOffsetMap, true); readFields(fieldConsumer, data, fieldData, instanceFieldsCount, annotationOffsetMap, false); } private void readFields(Consumer fieldConsumer, SectionReader data, DexFieldData fieldData, int count, Map annOffsetMap, boolean staticFields) { List constValues = staticFields ? getStaticFieldInitValues(data.copy()) : null; int fieldId = 0; for (int i = 0; i < count; i++) { fieldId += data.readUleb128(); int accFlags = data.readUleb128(); in.fillFieldData(fieldData, fieldId); fieldData.setAccessFlags(accFlags); fieldData.setAnnotationsOffset(getOffsetFromMap(fieldId, annOffsetMap)); fieldData.setConstValue(staticFields && i < constValues.size() ? constValues.get(i) : null); fieldConsumer.accept(fieldData); } } private void visitMethods(Consumer mthConsumer, SectionReader data, int directMthCount, int virtualMthCount) { DexMethodData methodData = new DexMethodData(annotationsParser); methodData.setMethodRef(new DexMethodRef()); Map annotationOffsetMap = annotationsParser.readMethodsAnnotationOffsetMap(); Map paramsAnnOffsetMap = annotationsParser.readMethodParamsAnnRefOffsetMap(); readMethods(mthConsumer, data, methodData, directMthCount, annotationOffsetMap, paramsAnnOffsetMap); readMethods(mthConsumer, data, methodData, virtualMthCount, annotationOffsetMap, paramsAnnOffsetMap); } private void readMethods(Consumer mthConsumer, SectionReader data, DexMethodData methodData, int count, Map annotationOffsetMap, Map paramsAnnOffsetMap) { DexCodeReader dexCodeReader = new DexCodeReader(in.copy()); int mthIdx = 0; for (int i = 0; i < count; i++) { mthIdx += data.readUleb128(); int accFlags = data.readUleb128(); int codeOff = data.readUleb128(); DexMethodRef methodRef = methodData.getMethodRef(); methodRef.reset(); in.initMethodRef(mthIdx, methodRef); methodData.setAccessFlags(accFlags); if (codeOff == 0) { methodData.setCodeReader(null); } else { dexCodeReader.setMthId(mthIdx); dexCodeReader.setOffset(codeOff); methodData.setCodeReader(dexCodeReader); } methodData.setAnnotationsOffset(getOffsetFromMap(mthIdx, annotationOffsetMap)); methodData.setParamAnnotationsOffset(getOffsetFromMap(mthIdx, paramsAnnOffsetMap)); mthConsumer.accept(methodData); } } private static int getOffsetFromMap(int idx, Map annOffsetMap) { Integer offset = annOffsetMap.get(idx); return offset != null ? offset : 0; } private List getStaticFieldInitValues(SectionReader reader) { int staticValuesOff = getStaticValuesOff(); if (staticValuesOff == 0) { return Collections.emptyList(); } reader.absPos(staticValuesOff); return annotationsParser.parseEncodedArray(reader); } private List getAnnotations() { annotationsParser.setOffset(getAnnotationsOff()); return annotationsParser.readClassAnnotations(); } @Override public List getAttributes() { List list = new ArrayList<>(); String sourceFile = getSourceFile(); if (sourceFile != null && !sourceFile.isEmpty()) { list.add(new SourceFileAttr(sourceFile)); } DexAnnotationsConvert.forClass(getType(), list, getAnnotations()); return list; } public int getClassDefOffset() { return in.pos(0).getAbsPos(); } @Override public String getDisassembledCode() { byte[] dexBuf = in.getDexReader().getBuf().array(); return SmaliUtils.getSmaliCode(dexBuf, getClassDefOffset()); } @Override public String toString() { return getType(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexCodeReader.java ================================================ package jadx.plugins.input.dex.sections; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ICatch; import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IDebugInfo; import jadx.api.plugins.input.data.ITry; import jadx.api.plugins.input.data.impl.CatchData; import jadx.api.plugins.input.data.impl.TryData; import jadx.api.plugins.input.insns.InsnData; import jadx.core.utils.exceptions.InvalidDataException; import jadx.plugins.input.dex.DexException; import jadx.plugins.input.dex.insns.DexInsnData; import jadx.plugins.input.dex.insns.DexInsnFormat; import jadx.plugins.input.dex.insns.DexInsnInfo; import jadx.plugins.input.dex.sections.debuginfo.DebugInfoParser; public class DexCodeReader implements ICodeReader { private final SectionReader in; private int mthId; public DexCodeReader(SectionReader in) { this.in = in; } @Override public DexCodeReader copy() { DexCodeReader copy = new DexCodeReader(in.copy()); copy.setMthId(this.getMthId()); return copy; } public void setOffset(int offset) { this.in.setOffset(offset); } @Override public int getRegistersCount() { return in.pos(0).readUShort(); } @Override public int getArgsStartReg() { return -1; } @Override public int getUnitsCount() { return in.pos(12).readInt(); } @Override public void visitInstructions(Consumer insnConsumer) { DexInsnData insnData = new DexInsnData(this, in.copy()); in.pos(12); int size = in.readInt(); int offset = 0; // in code units (2 byte) while (offset < size) { int insnStart = in.getAbsPos(); int opcodeUnit = in.readUShort(); DexInsnInfo insnInfo = DexInsnInfo.get(opcodeUnit); insnData.setInsnStart(insnStart); insnData.setOffset(offset); insnData.setInsnInfo(insnInfo); insnData.setOpcodeUnit(opcodeUnit); insnData.setPayload(null); insnData.setDecoded(false); if (insnInfo != null) { DexInsnFormat format = insnInfo.getFormat(); insnData.setRegsCount(format.getRegsCount()); insnData.setLength(format.getLength()); } else { insnData.setRegsCount(0); insnData.setLength(1); } insnConsumer.accept(insnData); if (!insnData.isDecoded()) { skip(insnData); } offset += insnData.getLength(); } } public void decode(DexInsnData insn) { DexInsnFormat format = insn.getInsnInfo().getFormat(); format.decode(insn, insn.getOpcodeUnit(), insn.getCodeData().in); insn.setDecoded(true); } public void skip(DexInsnData insn) { DexInsnInfo insnInfo = insn.getInsnInfo(); if (insnInfo != null) { DexCodeReader codeReader = insn.getCodeData(); insnInfo.getFormat().skip(insn, codeReader.in); } } @Nullable @Override public IDebugInfo getDebugInfo() { int debugOff = in.pos(8).readInt(); if (debugOff == 0) { return null; } if (debugOff < 0 || debugOff > in.size()) { throw new InvalidDataException("Invalid debug info offset"); } int regsCount = getRegistersCount(); DebugInfoParser debugInfoParser = new DebugInfoParser(in, regsCount, getUnitsCount()); debugInfoParser.initMthArgs(regsCount, in.getMethodParamTypes(mthId)); return debugInfoParser.process(debugOff); } private int getTriesCount() { return in.pos(6).readUShort(); } private int getTriesOffset() { int triesCount = getTriesCount(); if (triesCount == 0) { return -1; } int insnsCount = getUnitsCount(); int padding = insnsCount % 2 == 1 ? 2 : 0; return 4 * 4 + insnsCount * 2 + padding; } @Override public List getTries() { int triesOffset = getTriesOffset(); if (triesOffset == -1) { return Collections.emptyList(); } int triesCount = getTriesCount(); Map catchHandlers = getCatchHandlers(triesOffset + 8 * triesCount, in.copy()); in.pos(triesOffset); List triesList = new ArrayList<>(triesCount); for (int i = 0; i < triesCount; i++) { int startAddr = in.readInt(); int insnsCount = in.readUShort(); int handlerOff = in.readUShort(); ICatch catchHandler = catchHandlers.get(handlerOff); if (catchHandler == null) { throw new DexException("Catch handler not found by byte offset: " + handlerOff); } triesList.add(new TryData(startAddr, startAddr + insnsCount - 1, catchHandler)); } return triesList; } private Map getCatchHandlers(int offset, SectionReader ext) { in.pos(offset); int byteOffsetStart = in.getAbsPos(); int size = in.readUleb128(); Map map = new HashMap<>(size); for (int i = 0; i < size; i++) { int byteIndex = in.getAbsPos() - byteOffsetStart; int sizeAndType = in.readSleb128(); int handlersLen = Math.abs(sizeAndType); int[] addr = new int[handlersLen]; String[] types = new String[handlersLen]; for (int h = 0; h < handlersLen; h++) { types[h] = ext.getType(in.readUleb128()); addr[h] = in.readUleb128(); } int catchAllAddr; if (sizeAndType <= 0) { catchAllAddr = in.readUleb128(); } else { catchAllAddr = -1; } map.put(byteIndex, new CatchData(addr, types, catchAllAddr)); } return map; } @Override public int getCodeOffset() { return in.getOffset(); } public void setMthId(int mthId) { this.mthId = mthId; } public int getMthId() { return mthId; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexConsts.java ================================================ package jadx.plugins.input.dex.sections; public class DexConsts { public static final byte[] DEX_FILE_MAGIC = { 0x64, 0x65, 0x78, 0x0a }; // 'dex\n' public static final byte[] ZIP_FILE_MAGIC = { 0x50, 0x4B, 0x03, 0x04 }; public static final int MAX_MAGIC_SIZE = 4; public static final int ENDIAN_CONSTANT = 0x12345678; public static final int NO_INDEX = -1; } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexFieldData.java ================================================ package jadx.plugins.input.dex.sections; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.IFieldData; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.utils.Utils; import jadx.plugins.input.dex.sections.annotations.AnnotationsParser; public class DexFieldData implements IFieldData { @Nullable private final AnnotationsParser annotationsParser; private String parentClassType; private String type; private String name; private int accessFlags; private int annotationsOffset; private EncodedValue constValue; public DexFieldData(@Nullable AnnotationsParser parser) { this.annotationsParser = parser; } @Override public String getParentClassType() { return parentClassType; } public void setParentClassType(String parentClassType) { this.parentClassType = parentClassType; } @Override public String getType() { return type; } public void setType(String type) { this.type = type; } @Override public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public int getAccessFlags() { return accessFlags; } public void setAccessFlags(int accessFlags) { this.accessFlags = accessFlags; } public void setAnnotationsOffset(int annotationsOffset) { this.annotationsOffset = annotationsOffset; } public void setConstValue(EncodedValue constValue) { this.constValue = constValue; } private List getAnnotations() { if (annotationsParser == null) { throw new NullPointerException("Annotation parser not initialized"); } return annotationsParser.readAnnotationList(annotationsOffset); } @Override public List getAttributes() { List list = new ArrayList<>(2); Utils.addToList(list, constValue); DexAnnotationsConvert.forField(list, getAnnotations()); return list; } @Override public String toString() { return getParentClassType() + "->" + getName() + ":" + getType(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexHeader.java ================================================ package jadx.plugins.input.dex.sections; import jadx.plugins.input.dex.DexException; public class DexHeader { private final String version; private final int classDefsSize; private final int classDefsOff; private final int stringIdsOff; private final int typeIdsOff; private final int typeIdsSize; private final int fieldIdsSize; private final int fieldIdsOff; private final int protoIdsSize; private final int protoIdsOff; private final int methodIdsOff; private final int methodIdsSize; private int callSiteOff; private int methodHandleOff; public DexHeader(SectionReader buf) { byte[] magic = buf.readByteArray(4); version = buf.readString(3); buf.skip(1); int checksum = buf.readInt(); byte[] signature = buf.readByteArray(20); int fileSize = buf.readInt(); int headerSize = buf.readInt(); int endianTag = buf.readInt(); if (endianTag != DexConsts.ENDIAN_CONSTANT) { throw new DexException("Unexpected endian tag: 0x" + Integer.toHexString(endianTag)); } int linkSize = buf.readInt(); int linkOff = buf.readInt(); int mapListOff = buf.readInt(); int stringIdsSize = buf.readInt(); stringIdsOff = buf.readInt(); typeIdsSize = buf.readInt(); typeIdsOff = buf.readInt(); protoIdsSize = buf.readInt(); protoIdsOff = buf.readInt(); fieldIdsSize = buf.readInt(); fieldIdsOff = buf.readInt(); methodIdsSize = buf.readInt(); methodIdsOff = buf.readInt(); classDefsSize = buf.readInt(); classDefsOff = buf.readInt(); int dataSize = buf.readInt(); int dataOff = buf.readInt(); readMapList(buf, mapListOff); } private void readMapList(SectionReader buf, int mapListOff) { buf.absPos(mapListOff); int size = buf.readInt(); for (int i = 0; i < size; i++) { int type = buf.readUShort(); buf.skip(6); int offset = buf.readInt(); switch (type) { case 0x0007: callSiteOff = offset; break; case 0x0008: methodHandleOff = offset; break; } } } public String getVersion() { return version; } public int getClassDefsSize() { return classDefsSize; } public int getClassDefsOff() { return classDefsOff; } public int getStringIdsOff() { return stringIdsOff; } public int getTypeIdsOff() { return typeIdsOff; } public int getTypeIdsSize() { return typeIdsSize; } public int getFieldIdsSize() { return fieldIdsSize; } public int getFieldIdsOff() { return fieldIdsOff; } public int getProtoIdsSize() { return protoIdsSize; } public int getProtoIdsOff() { return protoIdsOff; } public int getMethodIdsOff() { return methodIdsOff; } public int getMethodIdsSize() { return methodIdsSize; } public int getCallSiteOff() { return callSiteOff; } public int getMethodHandleOff() { return methodHandleOff; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexHeaderV41.java ================================================ package jadx.plugins.input.dex.sections; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; import static jadx.plugins.input.dex.utils.DataReader.readU4; public class DexHeaderV41 { public static @Nullable DexHeaderV41 readIfPresent(byte[] content) { int headerSize = readU4(content, 36); if (headerSize < 120) { return null; } int fileSize = readU4(content, 32); int containerSize = readU4(content, 112); int headerOffset = readU4(content, 116); return new DexHeaderV41(fileSize, containerSize, headerOffset); } public static List readSubDexOffsets(byte[] content, DexHeaderV41 header) { int start = 0; int end = header.getFileSize(); int limit = Math.min(header.getContainerSize(), content.length); List list = new ArrayList<>(); while (true) { list.add(start); start = end; if (start >= limit) { break; } int nextFileSize = readU4(content, start + 32); end = start + nextFileSize; } return list; } private final int fileSize; private final int containerSize; private final int headerOffset; public DexHeaderV41(int fileSize, int containerSize, int headerOffset) { this.fileSize = fileSize; this.containerSize = containerSize; this.headerOffset = headerOffset; } public int getFileSize() { return fileSize; } public int getContainerSize() { return containerSize; } public int getHeaderOffset() { return headerOffset; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexMethodData.java ================================================ package jadx.plugins.input.dex.sections; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr; import jadx.api.plugins.utils.Utils; import jadx.plugins.input.dex.sections.annotations.AnnotationsParser; import jadx.plugins.input.dex.smali.SmaliPrinter; public class DexMethodData implements IMethodData { @Nullable private final AnnotationsParser annotationsParser; private DexMethodRef methodRef; private int accessFlags; private int annotationsOffset; private int paramAnnotationsOffset; @Nullable private DexCodeReader codeReader; public DexMethodData(@Nullable AnnotationsParser annotationsParser) { this.annotationsParser = annotationsParser; } @Override public DexMethodRef getMethodRef() { return methodRef; } public void setMethodRef(DexMethodRef methodRef) { this.methodRef = methodRef; } @Override public int getAccessFlags() { return accessFlags; } public void setAccessFlags(int accessFlags) { this.accessFlags = accessFlags; } @Nullable @Override public ICodeReader getCodeReader() { return codeReader; } public void setCodeReader(@Nullable DexCodeReader codeReader) { this.codeReader = codeReader; } @Override public String disassembleMethod() { return SmaliPrinter.printMethod(this); } public void setAnnotationsOffset(int annotationsOffset) { this.annotationsOffset = annotationsOffset; } public void setParamAnnotationsOffset(int paramAnnotationsOffset) { this.paramAnnotationsOffset = paramAnnotationsOffset; } private List getAnnotations() { return getAnnotationsParser().readAnnotationList(annotationsOffset); } private List> getParamsAnnotations() { return getAnnotationsParser().readAnnotationRefList(paramAnnotationsOffset); } @Override public List getAttributes() { List list = new ArrayList<>(); DexAnnotationsConvert.forMethod(list, getAnnotations()); Utils.addToList(list, AnnotationMethodParamsAttr.pack(getParamsAnnotations())); return list; } private AnnotationsParser getAnnotationsParser() { if (annotationsParser == null) { throw new NullPointerException("Annotation parser not initialized"); } return annotationsParser; } @Override public String toString() { return getMethodRef().toString(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexMethodProto.java ================================================ package jadx.plugins.input.dex.sections; import java.util.List; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.utils.Utils; public class DexMethodProto implements IMethodProto { private final List argTypes; private final String returnType; public DexMethodProto(List argTypes, String returnType) { this.returnType = returnType; this.argTypes = argTypes; } @Override public List getArgTypes() { return argTypes; } @Override public String getReturnType() { return returnType; } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof IMethodProto)) { return false; } IMethodProto that = (IMethodProto) other; return argTypes.equals(that.getArgTypes()) && returnType.equals(that.getReturnType()); } @Override public int hashCode() { return 31 * argTypes.hashCode() + returnType.hashCode(); } @Override public String toString() { return "(" + Utils.listToStr(argTypes) + ")" + returnType; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexMethodRef.java ================================================ package jadx.plugins.input.dex.sections; import java.util.List; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.utils.Utils; import jadx.plugins.input.dex.DexReader; public class DexMethodRef implements IMethodRef { private int uniqId; private String name; private String parentClassType; private String returnType; private List argTypes; // lazy loading info private int dexIdx; private SectionReader sectionReader; public void initUniqId(DexReader dexReader, int idx) { this.uniqId = (dexReader.getUniqId() & 0xFFFF) << 16 | (idx & 0xFFFF); } @Override public void load() { if (sectionReader != null) { sectionReader.loadMethodRef(this, dexIdx); sectionReader = null; } } public void setDexIdx(int dexIdx) { this.dexIdx = dexIdx; } public void setSectionReader(SectionReader sectionReader) { this.sectionReader = sectionReader; } @Override public int getUniqId() { return uniqId; } public void reset() { name = null; parentClassType = null; returnType = null; argTypes = null; } @Override public String getParentClassType() { return parentClassType; } public void setParentClassType(String parentClassType) { this.parentClassType = parentClassType; } @Override public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String getReturnType() { return returnType; } public void setReturnType(String returnType) { this.returnType = returnType; } @Override public List getArgTypes() { return argTypes; } public void setArgTypes(List argTypes) { this.argTypes = argTypes; } @Override public String toString() { if (name == null) { // not loaded return Integer.toHexString(uniqId); } return getParentClassType() + "->" + getName() + '(' + Utils.listToStr(getArgTypes()) + ")" + getReturnType(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/SectionReader.java ================================================ package jadx.plugins.input.dex.sections; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ICallSite; import jadx.api.plugins.input.data.IFieldRef; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.MethodHandleType; import jadx.api.plugins.input.data.impl.CallSite; import jadx.api.plugins.input.data.impl.FieldRefHandle; import jadx.api.plugins.input.data.impl.MethodRefHandle; import jadx.plugins.input.dex.DexReader; import jadx.plugins.input.dex.sections.annotations.EncodedValueParser; import jadx.plugins.input.dex.utils.Leb128; import jadx.plugins.input.dex.utils.MUtf8; import static jadx.plugins.input.dex.sections.DexConsts.NO_INDEX; public class SectionReader { private final ByteBuffer buf; private final DexReader dexReader; private int offset; public SectionReader(DexReader dexReader, int off) { this.dexReader = dexReader; this.offset = off; this.buf = duplicate(dexReader.getBuf(), off); } private SectionReader(SectionReader sectionReader, int off) { this(sectionReader.dexReader, off); } public SectionReader copy() { return new SectionReader(this, offset); } public SectionReader copy(int off) { return new SectionReader(this, off); } public byte[] getByteCode(int start, int len) { int pos = buf.position(); buf.position(start); byte[] bytes = readByteArray(len); buf.position(pos); return bytes; } private static ByteBuffer duplicate(ByteBuffer baseBuffer, int off) { ByteBuffer dupBuf = baseBuffer.duplicate(); dupBuf.order(ByteOrder.LITTLE_ENDIAN); dupBuf.position(off); return dupBuf; } public void setOffset(int offset) { this.offset = offset; } public int getOffset() { return offset; } public void shiftOffset(int shift) { this.offset += shift; } public SectionReader pos(int pos) { buf.position(offset + pos); return this; } public SectionReader absPos(int pos) { buf.position(pos); return this; } public int getAbsPos() { return buf.position(); } public void skip(int skip) { int pos = buf.position(); buf.position(pos + skip); } public int readInt() { return buf.getInt(); } public long readLong() { return buf.getLong(); } public byte readByte() { return buf.get(); } public int readUByte() { return buf.get() & 0xFF; } public int readUShort() { return buf.getShort() & 0xFFFF; } public int readShort() { return buf.getShort(); } public byte[] readByteArray(int len) { byte[] arr = new byte[len]; buf.get(arr); return arr; } public int[] readUShortArray(int size) { int[] arr = new int[size]; for (int i = 0; i < size; i++) { arr[i] = readUShort(); } return arr; } public String readString(int len) { return new String(readByteArray(len), StandardCharsets.US_ASCII); } private List readTypeListAt(int paramsOff) { if (paramsOff == 0) { return Collections.emptyList(); } return absPos(paramsOff).readTypeList(); } public List readTypeList() { int size = readInt(); if (size == 0) { return Collections.emptyList(); } int[] typeIds = readUShortArray(size); List types = new ArrayList<>(size); for (int typeId : typeIds) { types.add(getType(typeId)); } return types; } @Nullable public String getType(int idx) { if (idx == NO_INDEX) { return null; } int typeIdsOff = dexReader.getHeader().getTypeIdsOff(); absPos(typeIdsOff + idx * 4); int strIdx = readInt(); return getString(strIdx); } @Nullable public String getString(int idx) { if (idx == NO_INDEX) { return null; } // TODO: make string pool cache? int stringIdsOff = dexReader.getHeader().getStringIdsOff(); absPos(stringIdsOff + idx * 4); int strOff = readInt(); absPos(strOff); return MUtf8.decode(this); } public IFieldRef getFieldRef(int idx) { DexFieldData fieldData = new DexFieldData(null); int clsTypeIdx = fillFieldData(fieldData, idx); fieldData.setParentClassType(getType(clsTypeIdx)); return fieldData; } public int fillFieldData(DexFieldData fieldData, int idx) { int fieldIdsOff = dexReader.getHeader().getFieldIdsOff(); absPos(fieldIdsOff + idx * 8); int classTypeIdx = readUShort(); int typeIdx = readUShort(); int nameIdx = readInt(); fieldData.setType(getType(typeIdx)); fieldData.setName(getString(nameIdx)); return classTypeIdx; } public DexMethodRef getMethodRef(int idx) { DexMethodRef methodRef = new DexMethodRef(); initMethodRef(idx, methodRef); return methodRef; } public ICallSite getCallSite(int idx, SectionReader ext) { int callSiteOff = dexReader.getHeader().getCallSiteOff(); absPos(callSiteOff + idx * 4); absPos(readInt()); return new CallSite(EncodedValueParser.parseEncodedArray(this, ext)); } public IMethodHandle getMethodHandle(int idx) { int methodHandleOff = dexReader.getHeader().getMethodHandleOff(); absPos(methodHandleOff + idx * 8); MethodHandleType handleType = getMethodHandleType(readUShort()); skip(2); int refId = readUShort(); if (handleType.isField()) { return new FieldRefHandle(handleType, getFieldRef(refId)); } return new MethodRefHandle(handleType, getMethodRef(refId)); } private MethodHandleType getMethodHandleType(int type) { switch (type) { case 0x00: return MethodHandleType.STATIC_PUT; case 0x01: return MethodHandleType.STATIC_GET; case 0x02: return MethodHandleType.INSTANCE_PUT; case 0x03: return MethodHandleType.INSTANCE_GET; case 0x04: return MethodHandleType.INVOKE_STATIC; case 0x05: return MethodHandleType.INVOKE_INSTANCE; case 0x06: return MethodHandleType.INVOKE_CONSTRUCTOR; case 0x07: return MethodHandleType.INVOKE_DIRECT; case 0x08: return MethodHandleType.INVOKE_INTERFACE; default: throw new IllegalArgumentException("Unknown method handle type: 0x" + Integer.toHexString(type)); } } public void initMethodRef(int idx, DexMethodRef methodRef) { methodRef.initUniqId(dexReader, idx); methodRef.setDexIdx(idx); methodRef.setSectionReader(this); } public void loadMethodRef(DexMethodRef methodRef, int idx) { DexHeader header = dexReader.getHeader(); int methodIdsOff = header.getMethodIdsOff(); absPos(methodIdsOff + idx * 8); int classTypeIdx = readUShort(); int protoIdx = readUShort(); int nameIdx = readInt(); int protoIdsOff = header.getProtoIdsOff(); absPos(protoIdsOff + protoIdx * 12); skip(4); // shortyIdx int returnTypeIdx = readInt(); int paramsOff = readInt(); List argTypes = readTypeListAt(paramsOff); methodRef.setParentClassType(getType(classTypeIdx)); methodRef.setName(getString(nameIdx)); methodRef.setReturnType(getType(returnTypeIdx)); methodRef.setArgTypes(argTypes); } public DexMethodProto getMethodProto(int idx) { int protoIdsOff = dexReader.getHeader().getProtoIdsOff(); absPos(protoIdsOff + idx * 12); skip(4); // shortyIdx int returnTypeIdx = readInt(); int paramsOff = readInt(); return new DexMethodProto(readTypeListAt(paramsOff), getType(returnTypeIdx)); } public List getMethodParamTypes(int idx) { DexHeader header = dexReader.getHeader(); int methodIdsOff = header.getMethodIdsOff(); absPos(methodIdsOff + idx * 8 + 2); int protoIdx = readUShort(); int protoIdsOff = header.getProtoIdsOff(); absPos(protoIdsOff + protoIdx * 12 + 8); int paramsOff = readInt(); if (paramsOff == 0) { return Collections.emptyList(); } return absPos(paramsOff).readTypeList(); } public DexReader getDexReader() { return dexReader; } public int readUleb128() { return Leb128.readUnsignedLeb128(this); } public int readUleb128p1() { return Leb128.readUnsignedLeb128(this) - 1; } public int readSleb128() { return Leb128.readSignedLeb128(this); } public int size() { return buf.capacity(); } @Override public String toString() { return "SectionReader{buf=" + buf + ", offset=" + offset + '}'; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/annotations/AnnotationsParser.java ================================================ package jadx.plugins.input.dex.sections.annotations; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import jadx.api.plugins.input.data.annotations.AnnotationVisibility; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.annotations.JadxAnnotation; import jadx.plugins.input.dex.DexException; import jadx.plugins.input.dex.sections.SectionReader; public class AnnotationsParser { private final SectionReader in; private final SectionReader ext; private int offset; private int fieldsCount; private int methodsCount; private int paramsRefCount; public AnnotationsParser(SectionReader in, SectionReader ext) { this.in = in; this.ext = ext; } public AnnotationsParser copy() { return new AnnotationsParser(in.copy(), ext.copy()); } public void setOffset(int offset) { this.offset = offset; if (offset == 0) { this.fieldsCount = 0; this.methodsCount = 0; this.paramsRefCount = 0; } else { in.setOffset(offset); in.pos(4); this.fieldsCount = in.readInt(); this.methodsCount = in.readInt(); this.paramsRefCount = in.readInt(); } } public List readClassAnnotations() { if (offset == 0) { return Collections.emptyList(); } int classAnnotationsOffset = in.absPos(offset).readInt(); return readAnnotationList(classAnnotationsOffset); } public Map readFieldsAnnotationOffsetMap() { if (fieldsCount == 0) { return Collections.emptyMap(); } in.pos(4 * 4); Map map = new HashMap<>(fieldsCount); for (int i = 0; i < fieldsCount; i++) { int fieldIdx = in.readInt(); int fieldAnnOffset = in.readInt(); map.put(fieldIdx, fieldAnnOffset); } return map; } public Map readMethodsAnnotationOffsetMap() { if (methodsCount == 0) { return Collections.emptyMap(); } in.pos(4 * 4 + fieldsCount * 2 * 4); Map map = new HashMap<>(methodsCount); for (int i = 0; i < methodsCount; i++) { int methodIdx = in.readInt(); int methodAnnOffset = in.readInt(); map.put(methodIdx, methodAnnOffset); } return map; } public Map readMethodParamsAnnRefOffsetMap() { if (paramsRefCount == 0) { return Collections.emptyMap(); } in.pos(4 * 4 + fieldsCount * 2 * 4 + methodsCount * 2 * 4); Map map = new HashMap<>(paramsRefCount); for (int i = 0; i < paramsRefCount; i++) { int methodIdx = in.readInt(); int methodAnnRefOffset = in.readInt(); map.put(methodIdx, methodAnnRefOffset); } return map; } public List readAnnotationList(int offset) { if (offset == 0) { return Collections.emptyList(); } in.absPos(offset); int size = in.readInt(); if (size == 0) { return Collections.emptyList(); } List list = new ArrayList<>(size); int pos = in.getAbsPos(); for (int i = 0; i < size; i++) { in.absPos(pos + i * 4); int annOffset = in.readInt(); in.absPos(annOffset); list.add(readAnnotation(in, ext, true)); } return list; } public List> readAnnotationRefList(int offset) { if (offset == 0) { return Collections.emptyList(); } in.absPos(offset); int size = in.readInt(); if (size == 0) { return Collections.emptyList(); } List> list = new ArrayList<>(size); for (int i = 0; i < size; i++) { int refOff = in.readInt(); int pos = in.getAbsPos(); list.add(readAnnotationList(refOff)); in.absPos(pos); } return list; } public static IAnnotation readAnnotation(SectionReader in, SectionReader ext, boolean readVisibility) { AnnotationVisibility visibility = null; if (readVisibility) { int v = in.readUByte(); visibility = getVisibilityValue(v); } int typeIndex = in.readUleb128(); int size = in.readUleb128(); Map values = new LinkedHashMap<>(size); for (int i = 0; i < size; i++) { String name = ext.getString(in.readUleb128()); values.put(name, EncodedValueParser.parseValue(in, ext)); } String type = ext.getType(typeIndex); return new JadxAnnotation(visibility, type, values); } private static AnnotationVisibility getVisibilityValue(int value) { switch (value) { case 0: return AnnotationVisibility.BUILD; case 1: return AnnotationVisibility.RUNTIME; case 2: return AnnotationVisibility.SYSTEM; default: throw new DexException("Unknown annotation visibility value: " + value); } } public EncodedValue parseEncodedValue(SectionReader in) { return EncodedValueParser.parseValue(in, ext); } public List parseEncodedArray(SectionReader in) { return EncodedValueParser.parseEncodedArray(in, ext); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/annotations/AnnotationsUtils.java ================================================ package jadx.plugins.input.dex.sections.annotations; import java.util.Collections; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; public class AnnotationsUtils { @SuppressWarnings("unchecked") public static T getValue(IAnnotation ann, String name, EncodedType type, T defValue) { if (ann == null || ann.getValues() == null || ann.getValues().isEmpty()) { return defValue; } EncodedValue encodedValue = ann.getValues().get(name); if (encodedValue == null || encodedValue.getType() != type) { return defValue; } return (T) encodedValue.getValue(); } @Nullable public static Object getValue(IAnnotation ann, String name, EncodedType type) { if (ann == null || ann.getValues() == null || ann.getValues().isEmpty()) { return null; } EncodedValue encodedValue = ann.getValues().get(name); if (encodedValue == null || encodedValue.getType() != type) { return null; } return encodedValue.getValue(); } @SuppressWarnings("unchecked") public static List getArray(IAnnotation ann, String name) { if (ann == null || ann.getValues() == null || ann.getValues().isEmpty()) { return Collections.emptyList(); } EncodedValue encodedValue = ann.getValues().get(name); if (encodedValue == null || encodedValue.getType() != EncodedType.ENCODED_ARRAY) { return Collections.emptyList(); } return (List) encodedValue.getValue(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/annotations/EncodedValueParser.java ================================================ package jadx.plugins.input.dex.sections.annotations; import java.util.ArrayList; import java.util.List; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.plugins.input.dex.DexException; import jadx.plugins.input.dex.sections.SectionReader; public class EncodedValueParser { private static final int ENCODED_BYTE = 0x00; private static final int ENCODED_SHORT = 0x02; private static final int ENCODED_CHAR = 0x03; private static final int ENCODED_INT = 0x04; private static final int ENCODED_LONG = 0x06; private static final int ENCODED_FLOAT = 0x10; private static final int ENCODED_DOUBLE = 0x11; private static final int ENCODED_METHOD_TYPE = 0x15; private static final int ENCODED_METHOD_HANDLE = 0x16; private static final int ENCODED_STRING = 0x17; private static final int ENCODED_TYPE = 0x18; private static final int ENCODED_FIELD = 0x19; private static final int ENCODED_ENUM = 0x1b; private static final int ENCODED_METHOD = 0x1a; private static final int ENCODED_ARRAY = 0x1c; private static final int ENCODED_ANNOTATION = 0x1d; private static final int ENCODED_NULL = 0x1e; private static final int ENCODED_BOOLEAN = 0x1f; static EncodedValue parseValue(SectionReader in, SectionReader ext) { int argAndType = in.readUByte(); int type = argAndType & 0x1F; int arg = (argAndType & 0xE0) >> 5; int size = arg + 1; switch (type) { case ENCODED_NULL: return EncodedValue.NULL; case ENCODED_BOOLEAN: return new EncodedValue(EncodedType.ENCODED_BOOLEAN, arg == 1); case ENCODED_BYTE: return new EncodedValue(EncodedType.ENCODED_BYTE, in.readByte()); case ENCODED_SHORT: return new EncodedValue(EncodedType.ENCODED_SHORT, (short) parseNumber(in, size, true)); case ENCODED_CHAR: return new EncodedValue(EncodedType.ENCODED_CHAR, (char) parseUnsignedInt(in, size)); case ENCODED_INT: return new EncodedValue(EncodedType.ENCODED_INT, (int) parseNumber(in, size, true)); case ENCODED_LONG: return new EncodedValue(EncodedType.ENCODED_LONG, parseNumber(in, size, true)); case ENCODED_FLOAT: return new EncodedValue(EncodedType.ENCODED_FLOAT, Float.intBitsToFloat((int) parseNumber(in, size, false, 4))); case ENCODED_DOUBLE: return new EncodedValue(EncodedType.ENCODED_DOUBLE, Double.longBitsToDouble(parseNumber(in, size, false, 8))); case ENCODED_STRING: return new EncodedValue(EncodedType.ENCODED_STRING, ext.getString(parseUnsignedInt(in, size))); case ENCODED_TYPE: return new EncodedValue(EncodedType.ENCODED_TYPE, ext.getType(parseUnsignedInt(in, size))); case ENCODED_FIELD: case ENCODED_ENUM: return new EncodedValue(EncodedType.ENCODED_FIELD, ext.getFieldRef(parseUnsignedInt(in, size))); case ENCODED_ARRAY: return new EncodedValue(EncodedType.ENCODED_ARRAY, parseEncodedArray(in, ext)); case ENCODED_ANNOTATION: return new EncodedValue(EncodedType.ENCODED_ANNOTATION, AnnotationsParser.readAnnotation(in, ext, false)); case ENCODED_METHOD: return new EncodedValue(EncodedType.ENCODED_METHOD, ext.getMethodRef(parseUnsignedInt(in, size))); case ENCODED_METHOD_TYPE: return new EncodedValue(EncodedType.ENCODED_METHOD_TYPE, ext.getMethodProto(parseUnsignedInt(in, size))); case ENCODED_METHOD_HANDLE: return new EncodedValue(EncodedType.ENCODED_METHOD_HANDLE, ext.getMethodHandle(parseUnsignedInt(in, size))); default: throw new DexException("Unknown encoded value type: 0x" + Integer.toHexString(type)); } } public static List parseEncodedArray(SectionReader in, SectionReader ext) { int count = in.readUleb128(); List values = new ArrayList<>(count); for (int i = 0; i < count; i++) { values.add(parseValue(in, ext)); } return values; } private static int parseUnsignedInt(SectionReader in, int byteCount) { return (int) parseNumber(in, byteCount, false, 0); } private static long parseNumber(SectionReader in, int byteCount, boolean isSignExtended) { return parseNumber(in, byteCount, isSignExtended, 0); } private static long parseNumber(SectionReader in, int byteCount, boolean isSignExtended, int fillOnRight) { long result = 0; long last = 0; for (int i = 0; i < byteCount; i++) { last = in.readUByte(); result |= last << (i * 8); } if (fillOnRight != 0) { for (int i = byteCount; i < fillOnRight; i++) { result <<= 8; } } else { if (isSignExtended && (last & 0x80) != 0) { for (int i = byteCount; i < 8; i++) { result |= (long) 0xFF << (i * 8); } } } return result; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/debuginfo/DebugInfoParser.java ================================================ package jadx.plugins.input.dex.sections.debuginfo; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ILocalVar; import jadx.api.plugins.input.data.impl.DebugInfo; import jadx.plugins.input.dex.sections.DexConsts; import jadx.plugins.input.dex.sections.SectionReader; public class DebugInfoParser { private static final int DBG_END_SEQUENCE = 0x00; private static final int DBG_ADVANCE_PC = 0x01; private static final int DBG_ADVANCE_LINE = 0x02; private static final int DBG_START_LOCAL = 0x03; private static final int DBG_START_LOCAL_EXTENDED = 0x04; private static final int DBG_END_LOCAL = 0x05; private static final int DBG_RESTART_LOCAL = 0x06; private static final int DBG_SET_PROLOGUE_END = 0x07; private static final int DBG_SET_EPILOGUE_BEGIN = 0x08; private static final int DBG_SET_FILE = 0x09; // the smallest special opcode private static final int DBG_FIRST_SPECIAL = 0x0a; // the smallest line number increment private static final int DBG_LINE_BASE = -4; // the number of line increments represented private static final int DBG_LINE_RANGE = 15; private final SectionReader in; private final SectionReader ext; private final DexLocalVar[] locals; private final int codeSize; private List resultList; private Map linesMap; @Nullable private String sourceFile; private List argTypes; private int[] argRegs; public DebugInfoParser(SectionReader in, int regsCount, int codeSize) { this.in = in; this.ext = in.copy(); this.locals = new DexLocalVar[regsCount]; this.codeSize = codeSize; } public void initMthArgs(int regsCount, List argTypes) { if (argTypes.isEmpty()) { this.argTypes = Collections.emptyList(); return; } int argsCount = argTypes.size(); int[] argRegsArr = new int[argsCount]; int regNum = regsCount; for (int i = argsCount - 1; i >= 0; i--) { regNum -= getTypeLen(argTypes.get(i)); argRegsArr[i] = regNum; } this.argRegs = argRegsArr; this.argTypes = argTypes; } public static int getTypeLen(String type) { switch (type.charAt(0)) { case 'J': case 'D': return 2; default: return 1; } } public DebugInfo process(int debugOff) { in.absPos(debugOff); boolean varsInfoFound = false; resultList = new ArrayList<>(); linesMap = new HashMap<>(); int addr = 0; int line = in.readUleb128(); int paramsCount = in.readUleb128(); int argsCount = argTypes.size(); for (int i = 0; i < paramsCount; i++) { int nameId = in.readUleb128p1(); String name = ext.getString(nameId); if (name != null && i < argsCount) { DexLocalVar paramVar = new DexLocalVar(argRegs[i], name, argTypes.get(i)); startVar(paramVar, addr); paramVar.markAsParameter(); varsInfoFound = true; } } while (true) { int c = in.readUByte(); if (c == DBG_END_SEQUENCE) { break; } switch (c) { case DBG_ADVANCE_PC: { int addrInc = in.readUleb128(); addr = addrChange(addr, addrInc); break; } case DBG_ADVANCE_LINE: { line += in.readSleb128(); break; } case DBG_START_LOCAL: { int regNum = in.readUleb128(); int nameId = in.readUleb128() - 1; int type = in.readUleb128() - 1; DexLocalVar var = new DexLocalVar(ext, regNum, nameId, type, DexConsts.NO_INDEX); startVar(var, addr); varsInfoFound = true; break; } case DBG_START_LOCAL_EXTENDED: { int regNum = in.readUleb128(); int nameId = in.readUleb128p1(); int type = in.readUleb128p1(); int sign = in.readUleb128p1(); DexLocalVar var = new DexLocalVar(ext, regNum, nameId, type, sign); startVar(var, addr); varsInfoFound = true; break; } case DBG_RESTART_LOCAL: { int regNum = in.readUleb128(); restartVar(regNum, addr); varsInfoFound = true; break; } case DBG_END_LOCAL: { int regNum = in.readUleb128(); DexLocalVar var = locals[regNum]; if (var != null) { endVar(var, addr); } varsInfoFound = true; break; } case DBG_SET_PROLOGUE_END: case DBG_SET_EPILOGUE_BEGIN: { // do nothing break; } case DBG_SET_FILE: { int idx = in.readUleb128() - 1; this.sourceFile = ext.getString(idx); break; } default: { int adjustedOpCode = c - DBG_FIRST_SPECIAL; int addrInc = adjustedOpCode / DBG_LINE_RANGE; addr = addrChange(addr, addrInc); line += DBG_LINE_BASE + adjustedOpCode % DBG_LINE_RANGE; setLine(addr, line); break; } } } if (varsInfoFound) { for (DexLocalVar var : locals) { if (var != null && !var.isEnd()) { endVar(var, codeSize - 1); } } } return new DebugInfo(linesMap, resultList); } private int addrChange(int addr, int addrInc) { return Math.min(addr + addrInc, codeSize - 1); } private void setLine(int offset, int line) { linesMap.put(offset, line); } private void restartVar(int regNum, int addr) { DexLocalVar prev = locals[regNum]; if (prev != null) { endVar(prev, addr); DexLocalVar newVar = new DexLocalVar(regNum, prev.getName(), prev.getType(), prev.getSignature()); startVar(newVar, addr); } } private void startVar(DexLocalVar newVar, int addr) { int regNum = newVar.getRegNum(); DexLocalVar prev = locals[regNum]; if (prev != null) { endVar(prev, addr); } newVar.start(addr); locals[regNum] = newVar; } private void endVar(DexLocalVar var, int addr) { if (var.end(addr)) { resultList.add(var); } } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/debuginfo/DexLocalVar.java ================================================ package jadx.plugins.input.dex.sections.debuginfo; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ILocalVar; import jadx.api.plugins.utils.Utils; import jadx.plugins.input.dex.sections.SectionReader; public class DexLocalVar implements ILocalVar { private static final int PARAM_START_OFFSET = -1; private final int regNum; private final String name; private final String type; @Nullable private final String sign; private boolean isEnd; private int startOffset; private int endOffset; public DexLocalVar(SectionReader dex, int regNum, int nameId, int typeId, int signId) { this(regNum, dex.getString(nameId), dex.getType(typeId), dex.getString(signId)); } public DexLocalVar(int regNum, String name, String type) { this(regNum, name, type, null); } public DexLocalVar(int regNum, String name, String type, @Nullable String sign) { this.regNum = regNum; this.name = name; this.type = type; this.sign = sign; } public void start(int addr) { this.isEnd = false; this.startOffset = addr; } /** * Sets end address of local variable * * @param addr address * @return true if local variable was active, else false */ public boolean end(int addr) { if (isEnd) { return false; } this.isEnd = true; this.endOffset = addr; return true; } @Override public int getRegNum() { return regNum; } @Override public String getName() { return name; } @Override public String getType() { return type; } @Nullable @Override public String getSignature() { return sign; } @Override public int getStartOffset() { return startOffset; } public void markAsParameter() { startOffset = PARAM_START_OFFSET; } @Override public boolean isMarkedAsParameter() { return startOffset == PARAM_START_OFFSET; } @Override public int getEndOffset() { return endOffset; } public boolean isEnd() { return isEnd; } @Override public boolean equals(Object obj) { return super.equals(obj); } @Override public int hashCode() { return super.hashCode(); } @Override public String toString() { return (startOffset == -1 ? "-1 " : Utils.formatOffset(startOffset)) + '-' + (isEnd ? Utils.formatOffset(endOffset) : " ") + ": r" + regNum + " '" + name + "' " + type + (sign != null ? ", signature: " + sign : ""); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/smali/InsnFormatter.java ================================================ package jadx.plugins.input.dex.smali; interface InsnFormatter { void format(InsnFormatterInfo insnFormatInfo); } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/smali/InsnFormatterInfo.java ================================================ package jadx.plugins.input.dex.smali; import java.util.Objects; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.insns.InsnData; public class InsnFormatterInfo { private final SmaliCodeWriter codeWriter; @Nullable private IMethodData mth; @Nullable private InsnData insn; public InsnFormatterInfo(SmaliCodeWriter codeWriter, IMethodData mth) { this.codeWriter = codeWriter; this.mth = Objects.requireNonNull(mth); } public InsnFormatterInfo(SmaliCodeWriter codeWriter, InsnData insn) { this.codeWriter = codeWriter; this.insn = Objects.requireNonNull(insn); } public SmaliCodeWriter getCodeWriter() { return codeWriter; } public void setMth(IMethodData mth) { this.mth = mth; } public IMethodData getMth() { return mth; } public InsnData getInsn() { if (insn == null) { throw new NullPointerException("Instruction not set for formatter"); } return insn; } public void setInsn(InsnData insn) { this.insn = insn; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/smali/SmaliCodeWriter.java ================================================ package jadx.plugins.input.dex.smali; import java.util.List; public class SmaliCodeWriter { public static final String NL = System.getProperty("line.separator"); public static final String INDENT_STR = " "; private final StringBuilder code = new StringBuilder(); private int indent; private String indentStr = ""; public SmaliCodeWriter startLine(String line) { startLine(); code.append(line); return this; } public SmaliCodeWriter startLine() { if (code.length() != 0) { code.append(NL); code.append(indentStr); } return this; } public SmaliCodeWriter add(Object obj) { code.append(obj); return this; } public SmaliCodeWriter add(int i) { code.append(i); return this; } public SmaliCodeWriter add(char c) { code.append(c); return this; } public SmaliCodeWriter add(String str) { code.append(str); return this; } public SmaliCodeWriter addArgs(List argTypes) { for (String type : argTypes) { code.append(type); } return this; } public void incIndent() { this.indent++; buildIndent(); } public void decIndent() { this.indent--; buildIndent(); } private void buildIndent() { StringBuilder s = new StringBuilder(indent * INDENT_STR.length()); for (int i = 0; i < indent; i++) { s.append(INDENT_STR); } this.indentStr = s.toString(); } public String getCode() { return code.toString(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/smali/SmaliInsnFormat.java ================================================ package jadx.plugins.input.dex.smali; import java.util.HashMap; import java.util.Map; import org.jetbrains.annotations.NotNull; import jadx.api.plugins.input.insns.InsnData; import jadx.plugins.input.dex.insns.DexOpcodes; public class SmaliInsnFormat { private static SmaliInsnFormat instance; public static synchronized SmaliInsnFormat getInstance() { SmaliInsnFormat instance = SmaliInsnFormat.instance; if (instance == null) { instance = new SmaliInsnFormat(); SmaliInsnFormat.instance = instance; } return instance; } private final Map formatters; public SmaliInsnFormat() { formatters = registerFormatters(); } private Map registerFormatters() { Map map = new HashMap<>(); map.put(DexOpcodes.NOP, fi -> fi.getCodeWriter().add("nop")); map.put(DexOpcodes.SGET_OBJECT, staticFieldInsn("sget-object")); map.put(DexOpcodes.SPUT_BOOLEAN, staticFieldInsn("sput-boolean")); map.put(DexOpcodes.CONST, constInsn("const")); map.put(DexOpcodes.CONST_HIGH16, constInsn("const/high16")); map.put(DexOpcodes.CONST_STRING, stringInsn("const-string")); map.put(DexOpcodes.INVOKE_VIRTUAL, invokeInsn("invoke-virtual")); map.put(DexOpcodes.INVOKE_DIRECT, invokeInsn("invoke-direct")); map.put(DexOpcodes.INVOKE_SUPER, invokeInsn("invoke-super")); map.put(DexOpcodes.INVOKE_STATIC, invokeInsn("invoke-static")); map.put(DexOpcodes.MOVE_RESULT, oneArgsInsn("move-result")); map.put(DexOpcodes.RETURN_VOID, noArgsInsn("return-void")); map.put(DexOpcodes.GOTO, gotoInsn("goto")); map.put(DexOpcodes.GOTO_16, gotoInsn("goto-16")); map.put(DexOpcodes.MOVE, simpleInsn("move")); // TODO: complete list return map; } private InsnFormatter simpleInsn(String name) { return fi -> { SmaliCodeWriter code = fi.getCodeWriter(); code.add(name); InsnData insn = fi.getInsn(); int regsCount = insn.getRegsCount(); for (int i = 0; i < regsCount; i++) { if (i == 0) { code.add(' '); } else { code.add(", "); } code.add(regAt(fi, i)); } }; } private InsnFormatter gotoInsn(String name) { return fi -> fi.getCodeWriter().add(name).add(" :goto").add(Integer.toHexString(fi.getInsn().getTarget())); } @NotNull private InsnFormatter staticFieldInsn(String name) { return fi -> fi.getCodeWriter().add(name).add(' ').add(regAt(fi, 0)).add(", ").add(field(fi)); } @NotNull private InsnFormatter constInsn(String name) { return fi -> fi.getCodeWriter().add(name).add(' ').add(regAt(fi, 0)).add(", ").add(literal(fi)); } @NotNull private InsnFormatter stringInsn(String name) { return fi -> fi.getCodeWriter().add(name).add(' ').add(regAt(fi, 0)).add(", ").add(str(fi)); } @NotNull private InsnFormatter invokeInsn(String name) { return fi -> { SmaliCodeWriter code = fi.getCodeWriter(); code.add(name).add(' '); regsList(code, fi.getInsn()); code.add(", ").add(method(fi)); }; } private InsnFormatter oneArgsInsn(String name) { return fi -> fi.getCodeWriter().add(name).add(' ').add(regAt(fi, 0)); } private InsnFormatter noArgsInsn(String name) { return fi -> fi.getCodeWriter().add(name); } private String literal(InsnFormatterInfo fi) { return "0x" + Long.toHexString(fi.getInsn().getLiteral()); } private String str(InsnFormatterInfo fi) { return "\"" + fi.getInsn().getIndexAsString() + "\""; } private String field(InsnFormatterInfo fi) { return fi.getInsn().getIndexAsField().toString(); } private String method(InsnFormatterInfo fi) { return fi.getInsn().getIndexAsMethod().toString(); } private void regsList(SmaliCodeWriter code, InsnData insn) { int argsCount = insn.getRegsCount(); code.add('{'); for (int i = 0; i < argsCount; i++) { if (i != 0) { code.add(", "); } code.add("v").add(insn.getReg(i)); } code.add('}'); } private String regAt(InsnFormatterInfo fi, int argNum) { return "v" + fi.getInsn().getReg(argNum); } public void format(InsnFormatterInfo formatInfo) { InsnData insn = formatInfo.getInsn(); insn.decode(); int rawOpcodeUnit = insn.getRawOpcodeUnit(); int opcode = rawOpcodeUnit & 0xFF; InsnFormatter insnFormatter = formatters.get(opcode); if (insnFormatter != null) { insnFormatter.format(formatInfo); } else { formatInfo.getCodeWriter().add("# ").add(insn.getOpcode()).add(" (?0x").add(Integer.toHexString(rawOpcodeUnit)).add(')'); } } public String format(InsnData insn) { InsnFormatterInfo formatInfo = new InsnFormatterInfo(new SmaliCodeWriter(), insn); format(formatInfo); return formatInfo.getCodeWriter().getCode(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/smali/SmaliPrinter.java ================================================ package jadx.plugins.input.dex.smali; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.ICodeReader; import jadx.plugins.input.dex.sections.DexMethodData; import jadx.plugins.input.dex.sections.DexMethodRef; import static jadx.api.plugins.input.data.AccessFlagsScope.METHOD; // TODO: not finished public class SmaliPrinter { public static String printMethod(DexMethodData mth) { SmaliCodeWriter codeWriter = new SmaliCodeWriter(); codeWriter.startLine(".method "); codeWriter.add(AccessFlags.format(mth.getAccessFlags(), METHOD)); DexMethodRef methodRef = mth.getMethodRef(); methodRef.load(); codeWriter.add(methodRef.getName()); codeWriter.add('(').addArgs(methodRef.getArgTypes()).add(')'); codeWriter.add(methodRef.getReturnType()); codeWriter.incIndent(); ICodeReader codeReader = mth.getCodeReader(); if (codeReader != null) { codeWriter.startLine(".registers ").add(codeReader.getRegistersCount()); SmaliInsnFormat insnFormat = SmaliInsnFormat.getInstance(); InsnFormatterInfo formatterInfo = new InsnFormatterInfo(codeWriter, mth); codeReader.visitInstructions(insn -> { codeWriter.startLine(); formatterInfo.setInsn(insn); insnFormat.format(formatterInfo); }); codeWriter.decIndent(); } codeWriter.startLine(".end method"); return codeWriter.getCode(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/utils/DataReader.java ================================================ package jadx.plugins.input.dex.utils; public class DataReader { public static int readU4(byte[] data, int pos) { byte b1 = data[pos++]; byte b2 = data[pos++]; byte b3 = data[pos++]; byte b4 = data[pos]; return (b4 & 0xFF) << 24 | (b3 & 0xFF) << 16 | (b2 & 0xFF) << 8 | b1 & 0xFF; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/utils/DexCheckSum.java ================================================ package jadx.plugins.input.dex.utils; import java.util.zip.Adler32; import jadx.plugins.input.dex.DexException; public class DexCheckSum { public static void verify(String fileName, byte[] content, int offset) { if (offset + 32 + 4 > content.length) { throw new DexException("Dex file truncated, can't read file length, file: " + fileName); } int len = DataReader.readU4(content, offset + 32); if (offset + len > content.length) { throw new DexException("Dex file truncated, length in header: " + len + ", file: " + fileName); } int checksum = DataReader.readU4(content, offset + 8); Adler32 adler32 = new Adler32(); adler32.update(content, offset + 12, len - 12); int fileChecksum = (int) adler32.getValue(); if (checksum != fileChecksum) { throw new DexException(String.format("Bad dex file checksum: 0x%08x, expected: 0x%08x, file: %s", fileChecksum, checksum, fileName)); } } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/utils/IDexData.java ================================================ package jadx.plugins.input.dex.utils; public interface IDexData { String getFileName(); byte[] getContent(); } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/utils/Leb128.java ================================================ package jadx.plugins.input.dex.utils; import jadx.plugins.input.dex.DexException; import jadx.plugins.input.dex.sections.SectionReader; public final class Leb128 { public static int readSignedLeb128(SectionReader in) { int result = 0; int cur; int count = 0; int signBits = -1; do { cur = in.readUByte(); result |= (cur & 0x7f) << (count * 7); signBits <<= 7; count++; } while (((cur & 0x80) == 0x80) && count < 5); if ((cur & 0x80) == 0x80) { throw new DexException("Invalid LEB128 sequence"); } // Sign extend if appropriate if (((signBits >> 1) & result) != 0) { result |= signBits; } return result; } public static int readUnsignedLeb128(SectionReader in) { int result = 0; int cur; int count = 0; do { cur = in.readUByte(); result |= (cur & 0x7f) << (count * 7); count++; } while (((cur & 0x80) == 0x80) && count < 5); if ((cur & 0x80) == 0x80) { throw new DexException("Invalid LEB128 sequence"); } return result; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/utils/MUtf8.java ================================================ package jadx.plugins.input.dex.utils; import jadx.plugins.input.dex.DexException; import jadx.plugins.input.dex.sections.SectionReader; public class MUtf8 { public static String decode(SectionReader in) { int len = in.readUleb128(); char[] out = new char[len]; int k = 0; while (true) { char a = (char) (in.readUByte() & 0xff); if (a == 0) { return new String(out, 0, k); } out[k] = a; if (a < '\u0080') { k++; } else if ((a & 0xE0) == 0xC0) { int b = in.readUByte(); if ((b & 0xC0) != 0x80) { throw new DexException("Bad second byte"); } out[k] = (char) (((a & 0x1F) << 6) | (b & 0x3F)); k++; } else if ((a & 0xF0) == 0xE0) { int b = in.readUByte(); int c = in.readUByte(); if (((b & 0xC0) != 0x80) || ((c & 0xC0) != 0x80)) { throw new DexException("Bad second or third byte"); } out[k] = (char) (((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F)); k++; } else { throw new DexException("Bad byte"); } } } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/utils/SimpleDexData.java ================================================ package jadx.plugins.input.dex.utils; import java.util.Objects; public class SimpleDexData implements IDexData { private final String fileName; private final byte[] content; public SimpleDexData(String fileName, byte[] content) { this.fileName = Objects.requireNonNull(fileName); this.content = Objects.requireNonNull(content); } @Override public String getFileName() { return fileName; } @Override public byte[] getContent() { return content; } @Override public String toString() { return "DexData{" + fileName + ", size=" + content.length + '}'; } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/utils/SmaliUtils.java ================================================ package jadx.plugins.input.dex.utils; import java.io.PrintWriter; import java.io.StringWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.android.tools.smali.baksmali.Adaptors.ClassDefinition; import com.android.tools.smali.baksmali.BaksmaliOptions; import com.android.tools.smali.baksmali.formatter.BaksmaliWriter; import com.android.tools.smali.dexlib2.dexbacked.DexBackedClassDef; import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile; public class SmaliUtils { private static final Logger LOG = LoggerFactory.getLogger(SmaliUtils.class); public static String getSmaliCode(byte[] dexBuf, int clsDefOffset) { StringWriter stringWriter = new StringWriter(); try { DexBackedDexFile dexFile = new DexBackedDexFile(null, dexBuf); DexBackedClassDef dexBackedClassDef = new DexBackedClassDef(dexFile, clsDefOffset, 0); ClassDefinition classDefinition = new ClassDefinition(new BaksmaliOptions(), dexBackedClassDef); classDefinition.writeTo(new BaksmaliWriter(stringWriter)); } catch (Exception e) { LOG.error("Error generating smali", e); stringWriter.append("Error generating smali code: "); stringWriter.append(e.getMessage()); stringWriter.append(System.lineSeparator()); e.printStackTrace(new PrintWriter(stringWriter, true)); } return stringWriter.toString(); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.input.dex.DexInputPlugin ================================================ FILE: jadx-plugins/jadx-dex-input/src/test/java/jadx/plugins/input/dex/DexInputPluginTest.java ================================================ package jadx.plugins.input.dex; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.AccessFlagsScope; import jadx.api.plugins.input.data.ICodeReader; import jadx.plugins.input.dex.utils.SmaliTestUtils; import static org.assertj.core.api.Assertions.assertThat; class DexInputPluginTest { @Test public void loadSampleApk() throws Exception { processFile(Paths.get(ClassLoader.getSystemResource("samples/app-with-fake-dex.apk").toURI())); } @Test public void loadHelloWorld() throws Exception { processFile(Paths.get(ClassLoader.getSystemResource("samples/hello.dex").toURI())); } @Test public void loadTestSmali() throws Exception { processFile(SmaliTestUtils.compileSmaliFromResource("samples/test.smali")); } private static void processFile(Path sample) throws IOException { System.out.println("Input file: " + sample.toAbsolutePath()); long start = System.currentTimeMillis(); List files = Collections.singletonList(sample); try (ICodeLoader result = new DexInputPlugin().loadFiles(files)) { AtomicInteger count = new AtomicInteger(); result.visitClasses(cls -> { System.out.println(); System.out.println("Class: " + cls.getType()); System.out.println("AccessFlags: " + AccessFlags.format(cls.getAccessFlags(), AccessFlagsScope.CLASS)); System.out.println("SuperType: " + cls.getSuperType()); System.out.println("Interfaces: " + cls.getInterfacesTypes()); System.out.println("Attributes: " + cls.getAttributes()); count.getAndIncrement(); cls.visitFieldsAndMethods( System.out::println, mth -> { System.out.println("---"); System.out.println(mth); ICodeReader codeReader = mth.getCodeReader(); if (codeReader != null) { codeReader.visitInstructions(insn -> { insn.decode(); System.out.println(insn); }); } System.out.println("---"); System.out.println(mth.disassembleMethod()); System.out.println("---"); }); System.out.println("----"); System.out.println(cls.getDisassembledCode()); System.out.println("----"); }); assertThat(count.get()).isGreaterThan(0); } System.out.println("Time: " + (System.currentTimeMillis() - start) + "ms"); } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/test/java/jadx/plugins/input/dex/utils/SmaliTestUtils.java ================================================ package jadx.plugins.input.dex.utils; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import com.android.tools.smali.smali.Smali; import com.android.tools.smali.smali.SmaliOptions; public class SmaliTestUtils { public static Path compileSmaliFromResource(String res) { try { Path input = Paths.get(ClassLoader.getSystemResource(res).toURI()); return compileSmali(input); } catch (Exception e) { throw new AssertionError("Smali assemble error", e); } } public static Path compileSmali(Path input) { try { Path tempFile = Files.createTempFile("jadx", "smali.dex"); compileSmali(tempFile, Collections.singletonList(input)); return tempFile; } catch (Exception e) { throw new AssertionError("Smali assemble error", e); } } private static void compileSmali(Path output, List inputFiles) { try { SmaliOptions options = new SmaliOptions(); options.outputDexFile = output.toAbsolutePath().toString(); List inputFileNames = inputFiles.stream() .map(Path::toAbsolutePath) .map(Path::toString) .collect(Collectors.toList()); Smali.assemble(options, inputFileNames); } catch (Exception e) { throw new AssertionError("Smali assemble error", e); } } } ================================================ FILE: jadx-plugins/jadx-dex-input/src/test/resources/samples/test.smali ================================================ .class LHello; .super Ljava/lang/Object; .source "test.java" .method public main([Ljava/lang/String;)V .registers 2 .line 3 nop move v0, v1 .line 4 return-void .end method ================================================ FILE: jadx-plugins/jadx-input-api/README.md ================================================ # jadx-input-api Base API for code input used in jadx. Contains common data structures to support both java and dex bytecode. ================================================ FILE: jadx-plugins/jadx-input-api/build.gradle.kts ================================================ plugins { id("jadx-library") } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/ICodeLoader.java ================================================ package jadx.api.plugins.input; import java.io.Closeable; import java.util.function.Consumer; import jadx.api.plugins.input.data.IClassData; public interface ICodeLoader extends Closeable { void visitClasses(Consumer consumer); boolean isEmpty(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/JadxCodeInput.java ================================================ package jadx.api.plugins.input; import java.nio.file.Path; import java.util.List; public interface JadxCodeInput { ICodeLoader loadFiles(List input); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/AccessFlags.java ================================================ package jadx.api.plugins.input.data; public class AccessFlags { public static final int PUBLIC = 0x1; public static final int PRIVATE = 0x2; public static final int PROTECTED = 0x4; public static final int STATIC = 0x8; public static final int FINAL = 0x10; public static final int SYNCHRONIZED = 0x20; public static final int SUPER = 0x20; public static final int VOLATILE = 0x40; public static final int BRIDGE = 0x40; public static final int TRANSIENT = 0x80; public static final int VARARGS = 0x80; public static final int NATIVE = 0x100; public static final int INTERFACE = 0x200; public static final int ABSTRACT = 0x400; public static final int STRICT = 0x800; public static final int SYNTHETIC = 0x1000; public static final int ANNOTATION = 0x2000; public static final int ENUM = 0x4000; public static final int MODULE = 0x8000; public static final int CONSTRUCTOR = 0x10000; public static final int DECLARED_SYNCHRONIZED = 0x20000; public static final int DATA = 0x40000; public static boolean hasFlag(int flags, int flagValue) { return (flags & flagValue) != 0; } public static String format(int flags, AccessFlagsScope scope) { StringBuilder code = new StringBuilder(); if (hasFlag(flags, PUBLIC)) { code.append("public "); } if (hasFlag(flags, PRIVATE)) { code.append("private "); } if (hasFlag(flags, PROTECTED)) { code.append("protected "); } if (hasFlag(flags, STATIC)) { code.append("static "); } if (hasFlag(flags, FINAL)) { code.append("final "); } if (hasFlag(flags, ABSTRACT)) { code.append("abstract "); } if (hasFlag(flags, NATIVE)) { code.append("native "); } switch (scope) { case METHOD: if (hasFlag(flags, SYNCHRONIZED)) { code.append("synchronized "); } if (hasFlag(flags, BRIDGE)) { code.append("bridge "); } if (hasFlag(flags, VARARGS)) { code.append("varargs "); } break; case FIELD: if (hasFlag(flags, VOLATILE)) { code.append("volatile "); } if (hasFlag(flags, TRANSIENT)) { code.append("transient "); } break; case CLASS: if (hasFlag(flags, MODULE)) { code.append("module "); } if (hasFlag(flags, STRICT)) { code.append("strict "); } if (hasFlag(flags, SUPER)) { code.append("super "); } if (hasFlag(flags, ENUM)) { code.append("enum "); } if (hasFlag(flags, DATA)) { code.append("data "); } break; } if (hasFlag(flags, SYNTHETIC)) { code.append("synthetic "); } return code.toString(); } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/AccessFlagsScope.java ================================================ package jadx.api.plugins.input.data; public enum AccessFlagsScope { CLASS, FIELD, METHOD } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/ICallSite.java ================================================ package jadx.api.plugins.input.data; import java.util.List; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.insns.custom.ICustomPayload; public interface ICallSite extends ICustomPayload { List getValues(); void load(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/ICatch.java ================================================ package jadx.api.plugins.input.data; public interface ICatch { String[] getTypes(); int[] getHandlers(); int getCatchAllHandler(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IClassData.java ================================================ package jadx.api.plugins.input.data; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.attributes.IJadxAttribute; public interface IClassData { IClassData copy(); String getInputFileName(); String getType(); int getAccessFlags(); int getInputFileOffset(); @Nullable String getSuperType(); List getInterfacesTypes(); void visitFieldsAndMethods(ISeqConsumer fieldsConsumer, ISeqConsumer mthConsumer); List getAttributes(); String getDisassembledCode(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/ICodeReader.java ================================================ package jadx.api.plugins.input.data; import java.util.List; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.insns.InsnData; public interface ICodeReader { ICodeReader copy(); void visitInstructions(Consumer insnConsumer); int getRegistersCount(); int getArgsStartReg(); int getUnitsCount(); @Nullable IDebugInfo getDebugInfo(); int getCodeOffset(); List getTries(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IDebugInfo.java ================================================ package jadx.api.plugins.input.data; import java.util.List; import java.util.Map; public interface IDebugInfo { /** * Map instruction offset to source line number */ Map getSourceLineMapping(); List getLocalVars(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IFieldData.java ================================================ package jadx.api.plugins.input.data; import java.util.List; import jadx.api.plugins.input.data.attributes.IJadxAttribute; public interface IFieldData extends IFieldRef { int getAccessFlags(); List getAttributes(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IFieldRef.java ================================================ package jadx.api.plugins.input.data; public interface IFieldRef { String getParentClassType(); String getName(); String getType(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/ILocalVar.java ================================================ package jadx.api.plugins.input.data; import org.jetbrains.annotations.Nullable; public interface ILocalVar { String getName(); int getRegNum(); String getType(); @Nullable String getSignature(); int getStartOffset(); int getEndOffset(); /** * Hint if variable is a method parameter. * Can be incorrect and shouldn't be trusted. */ boolean isMarkedAsParameter(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IMethodData.java ================================================ package jadx.api.plugins.input.data; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.attributes.IJadxAttribute; public interface IMethodData { IMethodRef getMethodRef(); int getAccessFlags(); @Nullable ICodeReader getCodeReader(); String disassembleMethod(); List getAttributes(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IMethodHandle.java ================================================ package jadx.api.plugins.input.data; public interface IMethodHandle { MethodHandleType getType(); IFieldRef getFieldRef(); IMethodRef getMethodRef(); void load(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IMethodProto.java ================================================ package jadx.api.plugins.input.data; import java.util.List; public interface IMethodProto { String getReturnType(); List getArgTypes(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IMethodRef.java ================================================ package jadx.api.plugins.input.data; import jadx.api.plugins.input.insns.custom.ICustomPayload; public interface IMethodRef extends IMethodProto, ICustomPayload { /** * Method unique id (will be used for caching). * * @return 0 if can't calculate good unique identifier (disable caching) */ int getUniqId(); /** * Lazy loading for method info, until load() is called only getUniqId() can be used */ void load(); String getParentClassType(); String getName(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IResourceData.java ================================================ package jadx.api.plugins.input.data; public interface IResourceData { } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/ISeqConsumer.java ================================================ package jadx.api.plugins.input.data; import java.util.function.Consumer; /** * "Sequence consumer" allows getting count of elements available */ public interface ISeqConsumer extends Consumer { default void init(int count) { // no-op implementation } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/ITry.java ================================================ package jadx.api.plugins.input.data; public interface ITry { ICatch getCatch(); int getStartOffset(); int getEndOffset(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/MethodHandleType.java ================================================ package jadx.api.plugins.input.data; public enum MethodHandleType { STATIC_PUT, STATIC_GET, INSTANCE_PUT, INSTANCE_GET, INVOKE_STATIC, INVOKE_INSTANCE, INVOKE_DIRECT, INVOKE_CONSTRUCTOR, INVOKE_INTERFACE; public boolean isField() { switch (this) { case STATIC_PUT: case STATIC_GET: case INSTANCE_PUT: case INSTANCE_GET: return true; default: return false; } } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/annotations/AnnotationVisibility.java ================================================ package jadx.api.plugins.input.data.annotations; public enum AnnotationVisibility { BUILD, RUNTIME, SYSTEM } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/annotations/EncodedType.java ================================================ package jadx.api.plugins.input.data.annotations; public enum EncodedType { ENCODED_NULL, ENCODED_BOOLEAN, ENCODED_BYTE, ENCODED_SHORT, ENCODED_CHAR, ENCODED_INT, ENCODED_LONG, ENCODED_FLOAT, ENCODED_DOUBLE, ENCODED_STRING, ENCODED_TYPE, ENCODED_ENUM, ENCODED_FIELD, ENCODED_METHOD, ENCODED_METHOD_TYPE, ENCODED_METHOD_HANDLE, ENCODED_ARRAY, ENCODED_ANNOTATION } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/annotations/EncodedValue.java ================================================ package jadx.api.plugins.input.data.annotations; import java.util.Objects; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.PinnedAttribute; public class EncodedValue extends PinnedAttribute { public static final EncodedValue NULL = new EncodedValue(EncodedType.ENCODED_NULL, null); private final EncodedType type; private final Object value; public EncodedValue(EncodedType type, Object value) { this.type = type; this.value = value; } public EncodedType getType() { return type; } public Object getValue() { return value; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } EncodedValue that = (EncodedValue) o; return type == that.getType() && Objects.equals(value, that.getValue()); } @Override public IJadxAttrType getAttrType() { return JadxAttrType.CONSTANT_VALUE; } @Override public int hashCode() { return Objects.hash(getType(), getValue()); } @Override public String toString() { switch (type) { case ENCODED_NULL: return "null"; case ENCODED_ARRAY: return "[" + value + "]"; case ENCODED_STRING: return "{STRING: \"" + value + "\"}"; default: return "{" + type.toString().substring(8) + ": " + value + '}'; } } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/annotations/IAnnotation.java ================================================ package jadx.api.plugins.input.data.annotations; import java.util.Map; import org.jetbrains.annotations.Nullable; public interface IAnnotation { String getAnnotationClass(); AnnotationVisibility getVisibility(); Map getValues(); @Nullable default EncodedValue getDefaultValue() { return getValues().get("value"); } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/annotations/JadxAnnotation.java ================================================ package jadx.api.plugins.input.data.annotations; import java.util.Map; public class JadxAnnotation implements IAnnotation { private final AnnotationVisibility visibility; private final String type; private final Map values; public JadxAnnotation(AnnotationVisibility visibility, String type, Map values) { this.visibility = visibility; this.type = type; this.values = values; } @Override public String getAnnotationClass() { return type; } @Override public AnnotationVisibility getVisibility() { return visibility; } @Override public Map getValues() { return values; } @Override public String toString() { return "Annotation{" + visibility + ", type=" + type + ", values=" + values + '}'; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/IJadxAttrType.java ================================================ package jadx.api.plugins.input.data.attributes; /** * Marker interface for attribute type. * Similar to enumeration but extensible. *

* Used for attach attribute instance class information (T). * T - class of attribute instance *

* To create new one define static field like this: * {@code * static final IJadxAttrType ATTR_TYPE = IJadxAttrType.create(); * } */ public interface IJadxAttrType { static IJadxAttrType create() { return new IJadxAttrType<>() { }; } static IJadxAttrType create(String name) { return new IJadxAttrType<>() { @Override public String toString() { return name; } }; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/IJadxAttribute.java ================================================ package jadx.api.plugins.input.data.attributes; /** * Jadx attribute: custom data container, can be added to most jadx nodes. */ public interface IJadxAttribute { IJadxAttrType getAttrType(); /** * Mark type to skip unloading on node unload event */ default boolean keepLoaded() { return false; } default String toAttrString() { return this.toString(); } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/JadxAttrType.java ================================================ package jadx.api.plugins.input.data.attributes; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.types.AnnotationDefaultAttr; import jadx.api.plugins.input.data.attributes.types.AnnotationDefaultClassAttr; import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.api.plugins.input.data.attributes.types.ExceptionsAttr; import jadx.api.plugins.input.data.attributes.types.InnerClassesAttr; import jadx.api.plugins.input.data.attributes.types.MethodParametersAttr; import jadx.api.plugins.input.data.attributes.types.SignatureAttr; import jadx.api.plugins.input.data.attributes.types.SourceFileAttr; public final class JadxAttrType implements IJadxAttrType { // class, method, field public static final JadxAttrType ANNOTATION_LIST = bind(); public static final JadxAttrType SIGNATURE = bind(); // class public static final JadxAttrType SOURCE_FILE = bind(); public static final JadxAttrType INNER_CLASSES = bind(); public static final JadxAttrType ANNOTATION_DEFAULT_CLASS = bind(); // dex specific // field public static final JadxAttrType CONSTANT_VALUE = bind(); // method public static final JadxAttrType ANNOTATION_MTH_PARAMETERS = bind(); public static final JadxAttrType ANNOTATION_DEFAULT = bind(); public static final JadxAttrType EXCEPTIONS = bind(); public static final JadxAttrType METHOD_PARAMETERS = bind(); private static JadxAttrType bind() { return new JadxAttrType<>(); } private JadxAttrType() { } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/PinnedAttribute.java ================================================ package jadx.api.plugins.input.data.attributes; public abstract class PinnedAttribute implements IJadxAttribute { @Override public final boolean keepLoaded() { return true; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/types/AnnotationDefaultAttr.java ================================================ package jadx.api.plugins.input.data.attributes.types; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.PinnedAttribute; public class AnnotationDefaultAttr extends PinnedAttribute { private final EncodedValue value; public AnnotationDefaultAttr(EncodedValue value) { this.value = value; } public EncodedValue getValue() { return value; } @Override public IJadxAttrType getAttrType() { return JadxAttrType.ANNOTATION_DEFAULT; } @Override public String toString() { return "ANNOTATION_DEFAULT: " + value; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/types/AnnotationDefaultClassAttr.java ================================================ package jadx.api.plugins.input.data.attributes.types; import java.util.Map; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.PinnedAttribute; public class AnnotationDefaultClassAttr extends PinnedAttribute { private final Map values; public AnnotationDefaultClassAttr(Map values) { this.values = values; } public Map getValues() { return values; } @Override public IJadxAttrType getAttrType() { return JadxAttrType.ANNOTATION_DEFAULT_CLASS; } @Override public String toString() { return "ANNOTATION_DEFAULT_CLASS: " + values; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/types/AnnotationMethodParamsAttr.java ================================================ package jadx.api.plugins.input.data.attributes.types; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.PinnedAttribute; public class AnnotationMethodParamsAttr extends PinnedAttribute { @Nullable public static AnnotationMethodParamsAttr pack(List> annotationRefList) { if (annotationRefList.isEmpty()) { return null; } List list = new ArrayList<>(annotationRefList.size()); for (List annList : annotationRefList) { list.add(AnnotationsAttr.pack(annList)); } return new AnnotationMethodParamsAttr(list); } private final List paramList; private AnnotationMethodParamsAttr(List paramsList) { this.paramList = paramsList; } public List getParamList() { return paramList; } @Override public JadxAttrType getAttrType() { return JadxAttrType.ANNOTATION_MTH_PARAMETERS; } @Override public String toString() { return paramList.toString(); } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/types/AnnotationsAttr.java ================================================ package jadx.api.plugins.input.data.attributes.types; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.annotations.AnnotationVisibility; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.PinnedAttribute; public class AnnotationsAttr extends PinnedAttribute { @Nullable public static AnnotationsAttr pack(List annotationList) { if (annotationList.isEmpty()) { return null; } Map annMap = new HashMap<>(annotationList.size()); for (IAnnotation ann : annotationList) { if (ann.getVisibility() != AnnotationVisibility.SYSTEM) { annMap.put(ann.getAnnotationClass(), ann); } } if (annMap.isEmpty()) { return null; } return new AnnotationsAttr(annMap); } private final Map map; public AnnotationsAttr(Map map) { this.map = map; } public IAnnotation get(String className) { return map.get(className); } public Collection getAll() { return map.values(); } public List getList() { return map.isEmpty() ? Collections.emptyList() : new ArrayList<>(map.values()); } public int size() { return map.size(); } public boolean isEmpty() { return map.isEmpty(); } @Override public JadxAttrType getAttrType() { return JadxAttrType.ANNOTATION_LIST; } @Override public String toString() { return map.toString(); } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/types/ExceptionsAttr.java ================================================ package jadx.api.plugins.input.data.attributes.types; import java.util.List; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.PinnedAttribute; public class ExceptionsAttr extends PinnedAttribute { private final List list; public ExceptionsAttr(List list) { this.list = list; } public List getList() { return list; } @Override public IJadxAttrType getAttrType() { return JadxAttrType.EXCEPTIONS; } @Override public String toString() { return "EXCEPTIONS:" + list; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/types/InnerClassesAttr.java ================================================ package jadx.api.plugins.input.data.attributes.types; import java.util.Map; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.PinnedAttribute; public class InnerClassesAttr extends PinnedAttribute { private final Map map; public InnerClassesAttr(Map map) { this.map = map; } public Map getMap() { return map; } @Override public IJadxAttrType getAttrType() { return JadxAttrType.INNER_CLASSES; } @Override public String toString() { return "INNER_CLASSES:" + map; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/types/InnerClsInfo.java ================================================ package jadx.api.plugins.input.data.attributes.types; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.AccessFlagsScope; public class InnerClsInfo { private final String innerCls; private final @Nullable String outerCls; private final @Nullable String name; private final int accessFlags; public InnerClsInfo(String innerCls, @Nullable String outerCls, @Nullable String name, int accessFlags) { this.innerCls = innerCls; this.outerCls = outerCls; this.name = name; this.accessFlags = accessFlags; } public String getInnerCls() { return innerCls; } public @Nullable String getOuterCls() { return outerCls; } public @Nullable String getName() { return name; } public int getAccessFlags() { return accessFlags; } @Override public String toString() { return "InnerCls{" + innerCls + ", outerCls=" + outerCls + ", name=" + name + ", accessFlags=" + AccessFlags.format(accessFlags, AccessFlagsScope.CLASS) + '}'; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/types/MethodParametersAttr.java ================================================ package jadx.api.plugins.input.data.attributes.types; import java.util.List; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.AccessFlagsScope; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.PinnedAttribute; public class MethodParametersAttr extends PinnedAttribute { public static class Info { private final int accFlags; private final String name; public Info(int accFlags, String name) { this.accFlags = accFlags; this.name = name; } public int getAccFlags() { return accFlags; } public String getName() { return name; } public String toString() { return AccessFlags.format(accFlags, AccessFlagsScope.METHOD) + name; } } private final List list; public MethodParametersAttr(List list) { this.list = list; } public List getList() { return list; } @Override public IJadxAttrType getAttrType() { return JadxAttrType.METHOD_PARAMETERS; } @Override public String toString() { return "METHOD_PARAMETERS: " + list; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/types/SignatureAttr.java ================================================ package jadx.api.plugins.input.data.attributes.types; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.PinnedAttribute; public class SignatureAttr extends PinnedAttribute { private final String signature; public SignatureAttr(String signature) { this.signature = signature; } public String getSignature() { return signature; } @Override public IJadxAttrType getAttrType() { return JadxAttrType.SIGNATURE; } @Override public String toString() { return "SIGNATURE: " + signature; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/attributes/types/SourceFileAttr.java ================================================ package jadx.api.plugins.input.data.attributes.types; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.PinnedAttribute; public class SourceFileAttr extends PinnedAttribute { private final String fileName; public SourceFileAttr(String fileName) { this.fileName = fileName; } public String getFileName() { return fileName; } @Override public IJadxAttrType getAttrType() { return JadxAttrType.SOURCE_FILE; } @Override public String toString() { return "SOURCE:" + fileName; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/impl/CallSite.java ================================================ package jadx.api.plugins.input.data.impl; import java.util.List; import jadx.api.plugins.input.data.ICallSite; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.annotations.EncodedValue; public class CallSite implements ICallSite { private final List values; public CallSite(List values) { this.values = values; } @Override public void load() { for (EncodedValue value : values) { switch (value.getType()) { case ENCODED_METHOD_HANDLE: ((IMethodHandle) value.getValue()).load(); break; case ENCODED_METHOD: ((IMethodRef) value.getValue()).load(); break; } } } @Override public List getValues() { return values; } @Override public String toString() { return "CallSite{" + values + '}'; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/impl/CatchData.java ================================================ package jadx.api.plugins.input.data.impl; import jadx.api.plugins.input.data.ICatch; import static jadx.api.plugins.input.data.impl.InputUtils.formatOffset; public class CatchData implements ICatch { private final int[] handlers; private final String[] types; private final int allHandler; public CatchData(int[] handlers, String[] types, int allHandler) { this.handlers = handlers; this.types = types; this.allHandler = allHandler; } @Override public int[] getHandlers() { return handlers; } @Override public String[] getTypes() { return types; } @Override public int getCatchAllHandler() { return allHandler; } @Override public String toString() { StringBuilder sb = new StringBuilder("Catch:"); int size = types.length; for (int i = 0; i < size; i++) { sb.append(' ').append(types[i]).append("->").append(formatOffset(handlers[i])); } if (allHandler != -1) { sb.append(" all->").append(formatOffset(allHandler)); } return sb.toString(); } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/impl/DebugInfo.java ================================================ package jadx.api.plugins.input.data.impl; import java.util.List; import java.util.Map; import jadx.api.plugins.input.data.IDebugInfo; import jadx.api.plugins.input.data.ILocalVar; public class DebugInfo implements IDebugInfo { private final Map sourceLineMap; private final List localVars; public DebugInfo(Map sourceLineMap, List localVars) { this.sourceLineMap = sourceLineMap; this.localVars = localVars; } @Override public Map getSourceLineMapping() { return sourceLineMap; } @Override public List getLocalVars() { return localVars; } @Override public String toString() { return "DebugInfo{lines=" + sourceLineMap + ", localVars=" + localVars + '}'; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/impl/EmptyCodeLoader.java ================================================ package jadx.api.plugins.input.data.impl; import java.io.IOException; import java.util.function.Consumer; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.data.IClassData; public class EmptyCodeLoader implements ICodeLoader { public static final EmptyCodeLoader INSTANCE = new EmptyCodeLoader(); @Override public boolean isEmpty() { return true; } @Override public void visitClasses(Consumer consumer) { } @Override public void close() throws IOException { } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/impl/FieldRefHandle.java ================================================ package jadx.api.plugins.input.data.impl; import jadx.api.plugins.input.data.IFieldRef; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.MethodHandleType; public class FieldRefHandle implements IMethodHandle { private final IFieldRef fieldRef; private final MethodHandleType type; public FieldRefHandle(MethodHandleType type, IFieldRef fieldRef) { this.fieldRef = fieldRef; this.type = type; } @Override public MethodHandleType getType() { return type; } @Override public IFieldRef getFieldRef() { return fieldRef; } @Override public IMethodRef getMethodRef() { return null; } @Override public void load() { // already loaded } @Override public String toString() { return type + ": " + fieldRef; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/impl/InputUtils.java ================================================ package jadx.api.plugins.input.data.impl; public class InputUtils { public static String formatOffset(int offset) { return String.format("0x%04x", offset); } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/impl/JadxFieldRef.java ================================================ package jadx.api.plugins.input.data.impl; import jadx.api.plugins.input.data.IFieldRef; public class JadxFieldRef implements IFieldRef { private String parentClassType; private String name; private String type; public JadxFieldRef() { } public JadxFieldRef(String parentClassType, String name, String type) { this.parentClassType = parentClassType; this.name = name; this.type = type; } @Override public String getParentClassType() { return parentClassType; } public void setParentClassType(String parentClassType) { this.parentClassType = parentClassType; } @Override public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String getType() { return type; } public void setType(String type) { this.type = type; } @Override public String toString() { return parentClassType + "->" + name + ":" + type; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/impl/ListConsumer.java ================================================ package jadx.api.plugins.input.data.impl; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.Function; import jadx.api.plugins.input.data.ISeqConsumer; public class ListConsumer implements ISeqConsumer { private final Function convert; private List list; public ListConsumer(Function convert) { this.convert = convert; } @Override public void init(int count) { list = count == 0 ? Collections.emptyList() : new ArrayList<>(count); } @Override public void accept(T t) { list.add(convert.apply(t)); } public List getResult() { if (list == null) { // init not called return Collections.emptyList(); } return list; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/impl/MergeCodeLoader.java ================================================ package jadx.api.plugins.input.data.impl; import java.io.Closeable; import java.io.IOException; import java.util.List; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.data.IClassData; public class MergeCodeLoader implements ICodeLoader { private final List codeLoaders; private final @Nullable Closeable closeable; public MergeCodeLoader(List codeLoaders) { this(codeLoaders, null); } public MergeCodeLoader(List codeLoaders, @Nullable Closeable closeable) { this.codeLoaders = codeLoaders; this.closeable = closeable; } @Override public void visitClasses(Consumer consumer) { for (ICodeLoader codeLoader : codeLoaders) { codeLoader.visitClasses(consumer); } } @Override public boolean isEmpty() { for (ICodeLoader codeLoader : codeLoaders) { if (!codeLoader.isEmpty()) { return false; } } return true; } @Override public void close() throws IOException { for (ICodeLoader codeLoader : codeLoaders) { codeLoader.close(); } if (closeable != null) { closeable.close(); } } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/impl/MethodRefHandle.java ================================================ package jadx.api.plugins.input.data.impl; import jadx.api.plugins.input.data.IFieldData; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.MethodHandleType; public class MethodRefHandle implements IMethodHandle { private final MethodHandleType type; private final IMethodRef methodRef; public MethodRefHandle(MethodHandleType type, IMethodRef methodRef) { this.type = type; this.methodRef = methodRef; } @Override public MethodHandleType getType() { return type; } @Override public IMethodRef getMethodRef() { return methodRef; } @Override public IFieldData getFieldRef() { return null; } @Override public void load() { methodRef.load(); } @Override public String toString() { return type + ": " + methodRef; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/impl/TryData.java ================================================ package jadx.api.plugins.input.data.impl; import jadx.api.plugins.input.data.ICatch; import jadx.api.plugins.input.data.ITry; import static jadx.api.plugins.input.data.impl.InputUtils.formatOffset; public class TryData implements ITry { private final int startOffset; private final int endOffset; private final ICatch catchHandler; public TryData(int startOffset, int endOffset, ICatch catchHandler) { this.startOffset = startOffset; this.endOffset = endOffset; this.catchHandler = catchHandler; } @Override public ICatch getCatch() { return catchHandler; } @Override public int getStartOffset() { return startOffset; } @Override public int getEndOffset() { return endOffset; } @Override public String toString() { return "Try{" + formatOffset(startOffset) + " - " + formatOffset(endOffset) + ": " + catchHandler + '}'; } } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/insns/InsnData.java ================================================ package jadx.api.plugins.input.insns; import jadx.api.plugins.input.data.ICallSite; import jadx.api.plugins.input.data.IFieldRef; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.insns.custom.ICustomPayload; public interface InsnData { void decode(); int getOffset(); // offset within method int getFileOffset(); // offset within dex file Opcode getOpcode(); String getOpcodeMnemonic(); byte[] getByteCode(); InsnIndexType getIndexType(); int getRawOpcodeUnit(); int getRegsCount(); int getReg(int argNum); /** * Workaround to set result reg without additional move-result insn * * @return result reg number or -1 if not needed */ int getResultReg(); long getLiteral(); int getTarget(); int getIndex(); String getIndexAsString(); String getIndexAsType(); IFieldRef getIndexAsField(); IMethodRef getIndexAsMethod(); ICallSite getIndexAsCallSite(); IMethodProto getIndexAsProto(int protoIndex); IMethodHandle getIndexAsMethodHandle(); ICustomPayload getPayload(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/insns/InsnIndexType.java ================================================ package jadx.api.plugins.input.insns; public enum InsnIndexType { NONE, TYPE_REF, STRING_REF, FIELD_REF, METHOD_REF, CALL_SITE } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/insns/Opcode.java ================================================ package jadx.api.plugins.input.insns; public enum Opcode { UNKNOWN, NOP, ADD_DOUBLE, ADD_FLOAT, ADD_INT, ADD_INT_LIT, ADD_LONG, AND_INT, AND_INT_LIT, AND_LONG, AGET, AGET_BOOLEAN, AGET_BYTE, AGET_BYTE_BOOLEAN, AGET_CHAR, AGET_OBJECT, AGET_SHORT, AGET_WIDE, APUT, APUT_BOOLEAN, APUT_BYTE, APUT_BYTE_BOOLEAN, APUT_CHAR, APUT_OBJECT, APUT_SHORT, APUT_WIDE, ARITH, ARRAY_LENGTH, CAST, CHECK_CAST, CMPG_DOUBLE, CMPG_FLOAT, CMPL_DOUBLE, CMPL_FLOAT, CMP_LONG, CONST, CONST_CLASS, CONST_STRING, CONST_WIDE, DIV_DOUBLE, DIV_FLOAT, DIV_INT, DIV_INT_LIT, DIV_LONG, DOUBLE_TO_FLOAT, DOUBLE_TO_INT, DOUBLE_TO_LONG, FLOAT_TO_DOUBLE, FLOAT_TO_INT, FLOAT_TO_LONG, GOTO, IF, IF_EQ, IF_EQZ, IF_GE, IF_GEZ, IF_GT, IF_GTZ, IF_LE, IF_LEZ, IF_LT, IF_LTZ, IF_NE, IF_NEZ, INSTANCE_OF, INT_TO_BYTE, INT_TO_CHAR, INT_TO_DOUBLE, INT_TO_FLOAT, INT_TO_LONG, INT_TO_SHORT, INVOKE_DIRECT, INVOKE_DIRECT_RANGE, INVOKE_INTERFACE, INVOKE_INTERFACE_RANGE, INVOKE_STATIC, INVOKE_STATIC_RANGE, INVOKE_SUPER, INVOKE_SUPER_RANGE, INVOKE_VIRTUAL, INVOKE_VIRTUAL_RANGE, INVOKE_SPECIAL, IGET, IPUT, SGET, SPUT, LONG_TO_DOUBLE, LONG_TO_FLOAT, LONG_TO_INT, MONITOR_ENTER, MONITOR_EXIT, MOVE, MOVE_MULTI, MOVE_EXCEPTION, MOVE_OBJECT, MOVE_RESULT, MOVE_WIDE, MUL_DOUBLE, MUL_FLOAT, MUL_INT, MUL_INT_LIT, MUL_LONG, NEG, NEG_DOUBLE, NEG_FLOAT, NEG_INT, NEG_LONG, NEW_INSTANCE, NOT_INT, NOT_LONG, OR_INT, OR_INT_LIT, OR_LONG, REM_DOUBLE, REM_FLOAT, REM_INT, REM_INT_LIT, REM_LONG, RETURN, RETURN_VOID, RSUB_INT, SHL_INT, SHL_INT_LIT, SHL_LONG, SHR_INT, SHR_INT_LIT, SHR_LONG, SUB_DOUBLE, SUB_FLOAT, SUB_INT, SUB_LONG, THROW, USHR_INT, USHR_INT_LIT, USHR_LONG, XOR_INT, XOR_INT_LIT, XOR_LONG, NEW_ARRAY, FILLED_NEW_ARRAY, FILLED_NEW_ARRAY_RANGE, FILL_ARRAY_DATA, FILL_ARRAY_DATA_PAYLOAD, PACKED_SWITCH, PACKED_SWITCH_PAYLOAD, SPARSE_SWITCH, SPARSE_SWITCH_PAYLOAD, INVOKE_POLYMORPHIC, INVOKE_POLYMORPHIC_RANGE, INVOKE_CUSTOM, INVOKE_CUSTOM_RANGE, CONST_METHOD_HANDLE, CONST_METHOD_TYPE, // Java specific dynamic jump instructions JAVA_JSR, JAVA_RET, } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/insns/custom/IArrayPayload.java ================================================ package jadx.api.plugins.input.insns.custom; public interface IArrayPayload extends ICustomPayload { int getSize(); int getElementSize(); Object getData(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/insns/custom/ICustomPayload.java ================================================ package jadx.api.plugins.input.insns.custom; public interface ICustomPayload { } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/insns/custom/ISwitchPayload.java ================================================ package jadx.api.plugins.input.insns.custom; public interface ISwitchPayload extends ICustomPayload { int getSize(); int[] getKeys(); int[] getTargets(); } ================================================ FILE: jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/insns/custom/impl/SwitchPayload.java ================================================ package jadx.api.plugins.input.insns.custom.impl; import jadx.api.plugins.input.insns.custom.ISwitchPayload; public class SwitchPayload implements ISwitchPayload { private final int size; private final int[] keys; private final int[] targets; public SwitchPayload(int size, int[] keys, int[] targets) { this.size = size; this.keys = keys; this.targets = targets; } @Override public int getSize() { return size; } @Override public int[] getKeys() { return keys; } @Override public int[] getTargets() { return targets; } } ================================================ FILE: jadx-plugins/jadx-java-convert/build.gradle.kts ================================================ plugins { id("jadx-library") } dependencies { api(project(":jadx-core")) implementation(project(":jadx-plugins:jadx-dex-input")) implementation("com.jakewharton.android.repackaged:dalvik-dx:16.0.1") implementation("com.android.tools:r8:8.13.17") implementation("org.ow2.asm:asm:9.9.1") } ================================================ FILE: jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/AsmUtils.java ================================================ package jadx.plugins.input.javaconvert; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import org.objectweb.asm.ClassReader; public class AsmUtils { public static String getNameFromClassFile(Path file) throws IOException { try (InputStream in = Files.newInputStream(file)) { return getClassFullName(new ClassReader(in)); } } public static String getNameFromClassFile(byte[] content) throws IOException { return getClassFullName(new ClassReader(content)); } private static String getClassFullName(ClassReader classReader) { return classReader.getClassName(); } } ================================================ FILE: jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/ConvertResult.java ================================================ package jadx.plugins.input.javaconvert; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ConvertResult implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(ConvertResult.class); private final List converted = new ArrayList<>(); private final List tmpPaths = new ArrayList<>(); public List getConverted() { return converted; } public void addConvertedFiles(List paths) { converted.addAll(paths); } public void addTempPath(Path path) { tmpPaths.add(path); } public boolean isEmpty() { return converted.isEmpty(); } @Override public void close() { for (Path tmpPath : tmpPaths) { try { delete(tmpPath); } catch (Exception e) { LOG.warn("Failed to delete temp path: {}", tmpPath, e); } } } @SuppressWarnings("ResultOfMethodCallIgnored") private static void delete(Path path) throws IOException { if (Files.isRegularFile(path)) { Files.delete(path); return; } if (Files.isDirectory(path)) { try (Stream pathStream = Files.walk(path)) { pathStream .sorted(Comparator.reverseOrder()) .map(Path::toFile) .forEach(File::delete); } } } @Override public String toString() { return "ConvertResult{converted=" + converted + ", tmpPaths=" + tmpPaths + '}'; } } ================================================ FILE: jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/D8Converter.java ================================================ package jadx.plugins.input.javaconvert; import java.nio.file.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.android.tools.r8.CompilationFailedException; import com.android.tools.r8.CompilationMode; import com.android.tools.r8.D8; import com.android.tools.r8.D8Command; import com.android.tools.r8.Diagnostic; import com.android.tools.r8.DiagnosticsHandler; import com.android.tools.r8.OutputMode; public class D8Converter { private static final Logger LOG = LoggerFactory.getLogger(D8Converter.class); public static void run(Path path, Path tempDirectory, JavaConvertOptions options) throws CompilationFailedException { D8Command d8Command = D8Command.builder(new LogHandler()) .addProgramFiles(path) .setOutput(tempDirectory, OutputMode.DexIndexed) .setMode(CompilationMode.DEBUG) .setMinApiLevel(30) .setIntermediate(true) .setDisableDesugaring(!options.isD8Desugar()) .build(); D8.run(d8Command); } private static class LogHandler implements DiagnosticsHandler { @Override public void error(Diagnostic diagnostic) { LOG.error("D8 error: {}", format(diagnostic)); } @Override public void warning(Diagnostic diagnostic) { LOG.warn("D8 warning: {}", format(diagnostic)); } @Override public void info(Diagnostic diagnostic) { LOG.info("D8 info: {}", format(diagnostic)); } public static String format(Diagnostic diagnostic) { return diagnostic.getDiagnosticMessage() + ", origin: " + diagnostic.getOrigin() + ", position: " + diagnostic.getPosition(); } } } ================================================ FILE: jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/DxConverter.java ================================================ package jadx.plugins.input.javaconvert; import java.io.ByteArrayOutputStream; import java.nio.file.Path; import com.android.dx.command.dexer.DxContext; import com.android.dx.command.dexer.Main; public class DxConverter { private static final String CHARSET_NAME = "UTF-8"; private static class DxArgs extends com.android.dx.command.dexer.Main.Arguments { public DxArgs(DxContext context, String dexDir, String[] input) { super(context); outName = dexDir; fileNames = input; jarOutput = false; multiDex = true; optimize = true; localInfo = true; coreLibrary = true; debug = true; warnings = true; minSdkVersion = 28; } } public static void run(Path path, Path tempDirectory) { int result; String dxErrors; try (ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream errOut = new ByteArrayOutputStream()) { DxContext context = new DxContext(out, errOut); DxArgs args = new DxArgs( context, tempDirectory.toAbsolutePath().toString(), new String[] { path.toAbsolutePath().toString() }); result = new Main(context).runDx(args); dxErrors = errOut.toString(CHARSET_NAME); } catch (Exception e) { throw new RuntimeException("dx exception: " + e.getMessage(), e); } if (result != 0) { throw new RuntimeException("Java to dex conversion error, code: " + result + ", errors: " + dxErrors); } } } ================================================ FILE: jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java ================================================ package jadx.plugins.input.javaconvert; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.attribute.FileTime; import java.util.List; import java.util.Objects; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.stream.Collectors; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.utils.CommonFileUtils; import jadx.api.security.IJadxSecurity; import jadx.zip.ZipReader; public class JavaConvertLoader { private static final Logger LOG = LoggerFactory.getLogger(JavaConvertLoader.class); private final JavaConvertOptions options; private final ZipReader zipReader; private final IJadxSecurity security; public JavaConvertLoader(JavaConvertOptions options, JadxPluginContext context) { this.options = options; this.zipReader = context.getZipReader(); this.security = context.getArgs().getSecurity(); } public ConvertResult process(List input) { ConvertResult result = new ConvertResult(); processJars(input, result); processAars(input, result); processClassFiles(input, result); return result; } private void processJars(List input, ConvertResult result) { PathMatcher jarMatcher = FileSystems.getDefault().getPathMatcher("glob:**.jar"); input.stream() .filter(jarMatcher::matches) .forEach(path -> { try { convertJar(result, path); } catch (Exception e) { LOG.error("Failed to convert file: {}", path.toAbsolutePath(), e); } }); } private void processClassFiles(List input, ConvertResult result) { PathMatcher jarMatcher = FileSystems.getDefault().getPathMatcher("glob:**.class"); List clsFiles = input.stream() .filter(jarMatcher::matches) .collect(Collectors.toList()); if (clsFiles.isEmpty()) { return; } try { LOG.debug("Converting class files ..."); Path jarFile = Files.createTempFile("jadx-", ".jar"); try (JarOutputStream jo = new JarOutputStream(Files.newOutputStream(jarFile))) { for (Path file : clsFiles) { String clsName = AsmUtils.getNameFromClassFile(file); if (clsName == null) { throw new IOException("Can't read class name from file: " + file); } if (!security.isValidEntryName(clsName)) { LOG.warn("Skip class with invalid name: {}", clsName); continue; } addFileToJar(jo, file, clsName + ".class"); } } result.addTempPath(jarFile); LOG.debug("Packed {} class files into jar: {}", clsFiles.size(), jarFile); convertJar(result, jarFile); } catch (Exception e) { LOG.error("Error process class files", e); } } private void processAars(List input, ConvertResult result) { PathMatcher aarMatcher = FileSystems.getDefault().getPathMatcher("glob:**.aar"); input.stream() .filter(aarMatcher::matches) .forEach(path -> zipReader.readEntries(path.toFile(), (entry, in) -> { try { String entryName = entry.getName(); if (entryName.endsWith(".jar")) { Path tempJar = CommonFileUtils.saveToTempFile(in, ".jar"); result.addTempPath(tempJar); LOG.debug("Loading jar: {} ...", entryName); convertJar(result, tempJar); } } catch (Exception e) { LOG.error("Failed to process zip entry: {}", entry, e); } })); } private void convertJar(ConvertResult result, Path path) throws Exception { if (repackAndConvertJar(result, path)) { return; } convertSimpleJar(result, path); } private boolean repackAndConvertJar(ConvertResult result, Path path) throws Exception { // check if jar needs a full repackaging Boolean repackNeeded = zipReader.visitEntries(path.toFile(), zipEntry -> { String entryName = zipEntry.getName(); if (zipEntry.isDirectory()) { if (entryName.equals("BOOT-INF/")) { return true; // Spring Boot jar } if (entryName.equals("META-INF/versions/")) { return true; // exclude duplicated classes } } if (entryName.endsWith(".jar")) { return true; // contains sub jars } if (entryName.endsWith("module-info.class")) { return true; // need to exclude module files } return null; }); if (!Objects.equals(repackNeeded, Boolean.TRUE)) { return false; } LOG.debug("Repacking jar file: {} ...", path.toAbsolutePath()); Path jarFile = Files.createTempFile("jadx-classes-", ".jar"); result.addTempPath(jarFile); try (JarOutputStream jo = new JarOutputStream(Files.newOutputStream(jarFile))) { zipReader.readEntries(path.toFile(), (entry, in) -> { try { String entryName = entry.getName(); if (entryName.endsWith(".class")) { if (entryName.endsWith("module-info.class") || entryName.startsWith("META-INF/versions/")) { LOG.debug(" exclude: {}", entryName); return; } byte[] clsFileContent = CommonFileUtils.loadBytes(in); String clsName = AsmUtils.getNameFromClassFile(clsFileContent); if (clsName == null) { throw new IOException("Can't read class name from file: " + entryName); } if (!security.isValidEntryName(clsName)) { LOG.warn("Ignore class with invalid name: {} from {}", clsName, entry); } else { addJarEntry(jo, clsName + ".class", clsFileContent, null); } } else if (entryName.endsWith(".jar")) { Path tempJar = CommonFileUtils.saveToTempFile(in, ".jar"); result.addTempPath(tempJar); convertJar(result, tempJar); } } catch (Exception e) { LOG.error("Failed to process jar entry: {} in {}", entry, path, e); } }); } convertSimpleJar(result, jarFile); return true; } private void convertSimpleJar(ConvertResult result, Path path) throws Exception { Path tempDirectory = Files.createTempDirectory("jadx-"); result.addTempPath(tempDirectory); LOG.debug("Converting to dex ..."); convert(path, tempDirectory); List dexFiles = collectFilesInDir(tempDirectory); LOG.debug("Converted {} to {} dex", path.toAbsolutePath(), dexFiles.size()); result.addConvertedFiles(dexFiles); } private void convert(Path path, Path tempDirectory) { JavaConvertOptions.Mode mode = options.getMode(); switch (mode) { case DX: try { DxConverter.run(path, tempDirectory); } catch (Throwable e) { LOG.error("DX convert failed, path: {}", path, e); } break; case D8: try { D8Converter.run(path, tempDirectory, options); } catch (Throwable e) { LOG.error("D8 convert failed, path: {}", path, e); } break; case BOTH: try { DxConverter.run(path, tempDirectory); } catch (Throwable e) { LOG.warn("DX convert failed, trying D8, path: {}", path); try { D8Converter.run(path, tempDirectory, options); } catch (Throwable ex) { LOG.error("D8 convert failed: {}", ex.getMessage()); } } break; } } private static List collectFilesInDir(Path tempDirectory) throws IOException { PathMatcher dexMatcher = FileSystems.getDefault().getPathMatcher("glob:**.dex"); try (Stream pathStream = Files.walk(tempDirectory, 1)) { return pathStream .filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) .filter(dexMatcher::matches) .collect(Collectors.toList()); } } private static void addFileToJar(JarOutputStream jar, Path source, String entryName) throws IOException { byte[] fileContent = Files.readAllBytes(source); FileTime lastModifiedTime = Files.getLastModifiedTime(source, LinkOption.NOFOLLOW_LINKS); addJarEntry(jar, entryName, fileContent, lastModifiedTime); } private static void addJarEntry(JarOutputStream jar, String entryName, byte[] content, FileTime modTime) throws IOException { JarEntry entry = new JarEntry(entryName); if (modTime != null) { entry.setTime(modTime.toMillis()); } jar.putNextEntry(entry); jar.write(content); jar.closeEntry(); } } ================================================ FILE: jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java ================================================ package jadx.plugins.input.javaconvert; import jadx.api.plugins.options.impl.BasePluginOptionsBuilder; import static jadx.plugins.input.javaconvert.JavaConvertPlugin.PLUGIN_ID; public class JavaConvertOptions extends BasePluginOptionsBuilder { public enum Mode { DX, D8, BOTH } private Mode mode; private boolean d8Desugar; @Override public void registerOptions() { enumOption(PLUGIN_ID + ".mode", Mode.values(), Mode::valueOf) .description("convert mode") .defaultValue(Mode.BOTH) .setter(v -> mode = v); boolOption(PLUGIN_ID + ".d8-desugar") .description("use desugar in d8") .defaultValue(false) .setter(v -> d8Desugar = v); } public Mode getMode() { return mode; } public boolean isD8Desugar() { return d8Desugar; } } ================================================ FILE: jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java ================================================ package jadx.plugins.input.javaconvert; import java.nio.file.Path; import java.util.List; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.JadxPluginInfoBuilder; import jadx.api.plugins.data.JadxPluginRuntimeData; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.input.data.impl.EmptyCodeLoader; import jadx.plugins.input.dex.DexInputPlugin; public class JavaConvertPlugin implements JadxPlugin, JadxCodeInput { public static final String PLUGIN_ID = "java-convert"; private final JavaConvertOptions options = new JavaConvertOptions(); private JadxPluginRuntimeData dexInput; private JavaConvertLoader loader; @Override public JadxPluginInfo getPluginInfo() { return JadxPluginInfoBuilder.pluginId(PLUGIN_ID) .name("Java Convert") .description("Convert .class, .jar and .aar files to dex") .provides("java-input") .build(); } @Override public void init(JadxPluginContext context) { context.registerOptions(options); dexInput = context.plugins().getById(DexInputPlugin.PLUGIN_ID); loader = new JavaConvertLoader(options, context); context.addCodeInput(this); } @Override public ICodeLoader loadFiles(List input) { ConvertResult result = loader.process(input); if (result.isEmpty()) { result.close(); return EmptyCodeLoader.INSTANCE; } return dexInput.loadCodeFiles(result.getConverted(), result); } } ================================================ FILE: jadx-plugins/jadx-java-convert/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.input.javaconvert.JavaConvertPlugin ================================================ FILE: jadx-plugins/jadx-java-input/build.gradle.kts ================================================ plugins { id("jadx-library") } dependencies { api(project(":jadx-core")) // show bytecode disassemble implementation("io.github.skylot:raung-disasm:0.1.1") testImplementation(project(":jadx-core")) } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaClassReader.java ================================================ package jadx.plugins.input.java; import jadx.api.plugins.input.data.IClassData; import jadx.plugins.input.java.data.JavaClassData; public class JavaClassReader { private final int id; private final String fileName; private final byte[] data; public JavaClassReader(int id, String fileName, byte[] data) { this.id = id; this.fileName = fileName; this.data = data; } public IClassData loadClassData() { return new JavaClassData(this); } public int getId() { return id; } public String getFileName() { return fileName; } public byte[] getData() { return data; } @Override public String toString() { return fileName; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputLoader.java ================================================ package jadx.plugins.input.java; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.utils.CommonFileUtils; import jadx.core.plugins.files.TempFilesGetter; import jadx.core.utils.files.FileUtils; import jadx.zip.IZipEntry; import jadx.zip.ZipContent; import jadx.zip.ZipReader; public class JavaInputLoader { private static final Logger LOG = LoggerFactory.getLogger(JavaInputLoader.class); private static final int MAX_MAGIC_SIZE = 4; private static final byte[] JAVA_CLASS_FILE_MAGIC = { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE }; private static final byte[] ZIP_FILE_MAGIC = { 0x50, 0x4B, 0x03, 0x04 }; private final ZipReader zipReader; private final Path tempPath; private int classUniqId = 1; public JavaInputLoader(ZipReader zipReader, Path tempPath) { this.zipReader = zipReader; this.tempPath = tempPath; } /** * This will use zip reader with default options and ignore provided in jadx args */ @Deprecated public JavaInputLoader() { this(new ZipReader(), TempFilesGetter.INSTANCE.getTempDir()); } public List collectFiles(List inputFiles) { return inputFiles.stream() .map(Path::toFile) .map(this::loadFromFile) .filter(list -> !list.isEmpty()) .flatMap(Collection::stream) .collect(Collectors.toList()); } public List loadInputStream(InputStream in, String name) throws IOException { return loadReader(in, name, null, null); } public JavaClassReader loadClass(byte[] content, String fileName) { return new JavaClassReader(getNextUniqId(), fileName, content); } private List loadFromFile(File file) { try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { return loadReader(inputStream, file.getName(), file, null); } catch (Exception e) { LOG.error("File open error: {}", file.getAbsolutePath(), e); return Collections.emptyList(); } } private List loadReader(InputStream in, String name, @Nullable File file, @Nullable String parentFileName) throws IOException { byte[] magic = new byte[MAX_MAGIC_SIZE]; if (in.read(magic) != magic.length) { return Collections.emptyList(); } if (isStartWithBytes(magic, JAVA_CLASS_FILE_MAGIC) || name.endsWith(".class")) { byte[] data = CommonFileUtils.loadBytes(magic, in); String source = concatSource(parentFileName, name); JavaClassReader reader = new JavaClassReader(getNextUniqId(), source, data); return Collections.singletonList(reader); } if (isStartWithBytes(magic, ZIP_FILE_MAGIC) || CommonFileUtils.isZipFileExt(name)) { if (file != null) { return collectFromZip(file, name); } File zipFile = CommonFileUtils.saveToTempFile(magic, in, ".zip").toFile(); List readers = collectFromZip(zipFile, concatSource(parentFileName, name)); CommonFileUtils.safeDeleteFile(zipFile); return readers; } return Collections.emptyList(); } private List loadReaderFromZipEntry(byte[] content, String name, String parentFileName) throws IOException { if (isStartWithBytes(content, JAVA_CLASS_FILE_MAGIC) || name.endsWith(".class")) { String source = concatSource(parentFileName, name); JavaClassReader reader = new JavaClassReader(getNextUniqId(), source, content); return Collections.singletonList(reader); } if (isStartWithBytes(content, ZIP_FILE_MAGIC) || CommonFileUtils.isZipFileExt(name)) { Path tempZip = Files.createTempFile(tempPath, "temp", ".zip"); FileUtils.writeFile(tempZip, content); File zipFile = tempZip.toFile(); List readers = collectFromZip(zipFile, concatSource(parentFileName, name)); CommonFileUtils.safeDeleteFile(zipFile); return readers; } return Collections.emptyList(); } private static String concatSource(@Nullable String parentFileName, String name) { if (parentFileName == null) { return name; } return parentFileName + ':' + name; } private List collectFromZip(File file, String name) { List result = new ArrayList<>(); try (ZipContent zip = zipReader.open(file)) { for (IZipEntry entry : zip.getEntries()) { if (entry.isDirectory()) { continue; } String entryName = entry.getName(); if (entryName.startsWith("META-INF/versions/")) { // skip classes for different java versions continue; } try { List readers; if (entry.preferBytes()) { readers = loadReaderFromZipEntry(entry.getBytes(), entryName, name); } else { readers = loadReader(entry.getInputStream(), entryName, null, name); } result.addAll(readers); } catch (Exception e) { LOG.error("Failed to read zip entry: {}", entry, e); } } } catch (Exception e) { LOG.error("Failed to process zip file: {}", name, e); } return result; } public static boolean isStartWithBytes(byte[] fileMagic, byte[] expectedBytes) { int len = expectedBytes.length; if (fileMagic.length < len) { return false; } for (int i = 0; i < len; i++) { if (fileMagic[i] != expectedBytes[i]) { return false; } } return true; } private int getNextUniqId() { return classUniqId++; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java ================================================ package jadx.plugins.input.java; import java.io.Closeable; import java.io.InputStream; import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.function.Function; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.data.impl.EmptyCodeLoader; import jadx.plugins.input.java.utils.JavaClassParseException; public class JavaInputPlugin implements JadxPlugin { @Override public JadxPluginInfo getPluginInfo() { return new JadxPluginInfo("java-input", "Java Input", "Load .class and .jar files"); } @Override public void init(JadxPluginContext context) { context.addCodeInput(inputFiles -> { JavaInputLoader loader = new JavaInputLoader(context.getZipReader(), context.files().getPluginTempDir()); List readers = loader.collectFiles(inputFiles); if (readers.isEmpty()) { return EmptyCodeLoader.INSTANCE; } return new JavaLoadResult(readers, null); }); } public static ICodeLoader loadClassFiles(List inputFiles) { return loadClassFiles(inputFiles, null); } public static ICodeLoader loadClassFiles(List inputFiles, @Nullable Closeable closeable) { List readers = new JavaInputLoader().collectFiles(inputFiles); if (readers.isEmpty()) { return EmptyCodeLoader.INSTANCE; } return new JavaLoadResult(readers, closeable); } /** * Method for provide several inputs by using load methods from {@link JavaInputLoader} class. */ public static ICodeLoader load(Function> loader) { return wrapClassReaders(loader.apply(new JavaInputLoader())); } /** * Convenient method for load class file or jar from input stream. * Should be used only once per JadxDecompiler instance. * For load several times use {@link JavaInputPlugin#load(Function)} method. */ public static ICodeLoader loadFromInputStream(InputStream in, String fileName) { try { return wrapClassReaders(new JavaInputLoader().loadInputStream(in, fileName)); } catch (Exception e) { throw new JavaClassParseException("Failed to read input stream", e); } } /** * Convenient method for load single class file by content. * Should be used only once per JadxDecompiler instance. * For load several times use {@link JavaInputPlugin#load(Function)} method. */ public static ICodeLoader loadSingleClass(byte[] content, String fileName) { JavaClassReader reader = new JavaInputLoader().loadClass(content, fileName); return new JavaLoadResult(Collections.singletonList(reader)); } public static ICodeLoader wrapClassReaders(List readers) { if (readers.isEmpty()) { return EmptyCodeLoader.INSTANCE; } return new JavaLoadResult(readers); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaLoadResult.java ================================================ package jadx.plugins.input.java; import java.io.Closeable; import java.io.IOException; import java.util.List; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.data.IClassData; public class JavaLoadResult implements ICodeLoader { private static final Logger LOG = LoggerFactory.getLogger(JavaLoadResult.class); private final List readers; @Nullable private final Closeable closeable; public JavaLoadResult(List readers) { this(readers, null); } public JavaLoadResult(List readers, @Nullable Closeable closeable) { this.readers = readers; this.closeable = closeable; } @Override public void visitClasses(Consumer consumer) { for (JavaClassReader reader : readers) { try { consumer.accept(reader.loadClassData()); } catch (Exception e) { LOG.error("Failed to load class data for file: {}", reader.getFileName(), e); } } } @Override public boolean isEmpty() { return readers.isEmpty(); } @Override public void close() throws IOException { if (closeable != null) { closeable.close(); } } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/ClassOffsets.java ================================================ package jadx.plugins.input.java.data; public class ClassOffsets { private final int[] constPoolOffsets; private final int constPoolEnd; private final int interfacesEnd; private final int attributesOffset; public ClassOffsets(DataReader data) { this.constPoolOffsets = readConstPool(data); this.constPoolEnd = data.getOffset(); int interfacesCount = data.absPos(constPoolEnd + 6).readU2(); data.skip(interfacesCount * 2); this.interfacesEnd = data.getOffset(); skipFields(data); skipMethods(data); this.attributesOffset = data.getOffset(); } private static int[] readConstPool(DataReader data) { int cpSize = data.absPos(8).readU2(); int[] cpOffsets = new int[cpSize + 1]; for (int i = 1; i < cpSize; i++) { int tag = data.readU1(); cpOffsets[i] = data.getOffset(); ConstantType constType = ConstantType.getTypeByTag(tag); switch (constType) { case UTF8: data.skip(data.readU2()); break; case LONG: case DOUBLE: data.skip(8); i++; break; default: data.skip(constType.getDataSize()); break; } } return cpOffsets; } private void skipFields(DataReader data) { int fieldsCount = data.readU2(); for (int i = 0; i < fieldsCount; i++) { data.skip(6); skipAttributes(data); } } private void skipMethods(DataReader data) { int methodsCount = data.readU2(); for (int i = 0; i < methodsCount; i++) { data.skip(6); skipAttributes(data); } } private void skipAttributes(DataReader data) { int attrCount = data.readU2(); for (int i = 0; i < attrCount; i++) { data.skip(2); int len = data.readU4(); data.skip(len); } } public int getOffsetOfConstEntry(int num) { return constPoolOffsets[num]; } public int getAccessFlagsOffset() { return constPoolEnd; } public int getClsTypeOffset() { return constPoolEnd + 2; } public int getSuperTypeOffset() { return constPoolEnd + 4; } public int getInterfacesOffset() { return constPoolEnd + 6; } public int getFieldsOffset() { return interfacesEnd; } public int getAttributesOffset() { return attributesOffset; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/ConstPoolReader.java ================================================ package jadx.plugins.input.java.data; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ICallSite; import jadx.api.plugins.input.data.IFieldRef; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.MethodHandleType; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.impl.CallSite; import jadx.api.plugins.input.data.impl.FieldRefHandle; import jadx.api.plugins.input.data.impl.MethodRefHandle; import jadx.plugins.input.java.JavaClassReader; import jadx.plugins.input.java.data.attributes.JavaAttrType; import jadx.plugins.input.java.data.attributes.types.JavaBootstrapMethodsAttr; import jadx.plugins.input.java.data.attributes.types.data.RawBootstrapMethod; import jadx.plugins.input.java.utils.DescriptorParser; import jadx.plugins.input.java.utils.JavaClassParseException; import jadx.plugins.input.java.utils.ModifiedUTF8Decoder; public class ConstPoolReader { private final JavaClassReader clsReader; private final JavaClassData clsData; private final DataReader data; private final ClassOffsets offsets; public ConstPoolReader(JavaClassReader clsReader, JavaClassData javaClassData, DataReader data, ClassOffsets offsets) { this.clsReader = clsReader; this.clsData = javaClassData; this.data = data; this.offsets = offsets; } @Nullable public String getClass(int idx) { jumpToData(idx); int nameIdx = data.readU2(); return fixType(getUtf8(nameIdx)); } public IFieldRef getFieldRef(int idx) { jumpToData(idx); int clsIdx = data.readU2(); int nameTypeIdx = data.readU2(); jumpToData(nameTypeIdx); int nameIdx = data.readU2(); int typeIdx = data.readU2(); JavaFieldData fieldData = new JavaFieldData(); fieldData.setParentClassType(getClass(clsIdx)); fieldData.setName(getUtf8(nameIdx)); fieldData.setType(getUtf8(typeIdx)); return fieldData; } public String getFieldType(int idx) { jumpToData(idx); data.skip(2); int nameTypeIdx = data.readU2(); jumpToData(nameTypeIdx); data.skip(2); int typeIdx = data.readU2(); return getUtf8(typeIdx); } public IMethodRef getMethodRef(int idx) { jumpToData(idx); int clsIdx = data.readU2(); int nameTypeIdx = data.readU2(); jumpToData(nameTypeIdx); int nameIdx = data.readU2(); int descIdx = data.readU2(); JavaMethodRef mthRef = new JavaMethodRef(); mthRef.initUniqId(clsReader, idx, true); mthRef.setParentClassType(getClass(clsIdx)); mthRef.setName(getUtf8(nameIdx)); mthRef.setDescr(getUtf8(descIdx)); return mthRef; } public ICallSite getCallSite(int idx) { ConstantType constType = jumpToConst(idx); switch (constType) { case INVOKE_DYNAMIC: int bootstrapMthIdx = data.readU2(); int nameAndTypeIdx = data.readU2(); jumpToData(nameAndTypeIdx); int nameIdx = data.readU2(); int descIdx = data.readU2(); return resolveMethodCallSite(bootstrapMthIdx, nameIdx, descIdx); case DYNAMIC: throw new JavaClassParseException("Field call site not yet implemented"); default: throw new JavaClassParseException("Unexpected tag type for call site: " + constType); } } private CallSite resolveMethodCallSite(int bootstrapMthIdx, int nameIdx, int descIdx) { JavaBootstrapMethodsAttr bootstrapMethodsAttr = clsData.loadClassAttribute(data, JavaAttrType.BOOTSTRAP_METHODS); if (bootstrapMethodsAttr == null) { throw new JavaClassParseException("Unexpected missing BootstrapMethods attribute"); } RawBootstrapMethod rawBootstrapMethod = bootstrapMethodsAttr.getList().get(bootstrapMthIdx); List values = new ArrayList<>(6); values.add(new EncodedValue(EncodedType.ENCODED_METHOD_HANDLE, getMethodHandle(rawBootstrapMethod.getMethodHandleIdx()))); values.add(new EncodedValue(EncodedType.ENCODED_STRING, getUtf8(nameIdx))); values.add(new EncodedValue(EncodedType.ENCODED_METHOD_TYPE, DescriptorParser.parseToMethodProto(getUtf8(descIdx)))); for (int argConstIdx : rawBootstrapMethod.getArgs()) { values.add(readAsEncodedValue(argConstIdx)); } return new CallSite(values); } private IMethodHandle getMethodHandle(int idx) { jumpToData(idx); int kind = data.readU1(); int refIdx = data.readU2(); MethodHandleType handleType = convertMethodHandleKind(kind); if (handleType.isField()) { return new FieldRefHandle(handleType, getFieldRef(refIdx)); } return new MethodRefHandle(handleType, getMethodRef(refIdx)); } private MethodHandleType convertMethodHandleKind(int kind) { switch (kind) { case 1: return MethodHandleType.STATIC_PUT; case 2: return MethodHandleType.STATIC_GET; case 3: return MethodHandleType.INSTANCE_PUT; case 4: return MethodHandleType.INSTANCE_GET; case 5: return MethodHandleType.INVOKE_INSTANCE; case 6: return MethodHandleType.INVOKE_STATIC; case 7: return MethodHandleType.INVOKE_DIRECT; case 8: return MethodHandleType.INVOKE_CONSTRUCTOR; case 9: return MethodHandleType.INVOKE_INTERFACE; default: throw new IllegalArgumentException("Unknown method handle type: " + kind); } } public String getUtf8(int idx) { if (idx == 0) { return null; } jumpToData(idx); return readString(); } public ConstantType jumpToConst(int idx) { jumpToTag(idx); return ConstantType.getTypeByTag(data.readU1()); } public String readString() { int len = data.readU2(); byte[] bytes = data.readBytes(len); return parseString(bytes); } public int readU2() { return data.readU2(); } public int readU4() { return data.readU4(); } public long readU8() { return data.readU8(); } public int getInt(int idx) { jumpToData(idx); return data.readS4(); } public long getLong(int idx) { jumpToData(idx); return data.readS8(); } public double getDouble(int idx) { jumpToData(idx); return Double.longBitsToDouble(data.readU8()); } public float getFloat(int idx) { jumpToData(idx); return Float.intBitsToFloat(data.readU4()); } public EncodedValue readAsEncodedValue(int idx) { ConstantType constantType = jumpToConst(idx); switch (constantType) { case UTF8: return new EncodedValue(EncodedType.ENCODED_STRING, readString()); case STRING: return new EncodedValue(EncodedType.ENCODED_STRING, getUtf8(readU2())); case INTEGER: return new EncodedValue(EncodedType.ENCODED_INT, data.readS4()); case FLOAT: return new EncodedValue(EncodedType.ENCODED_FLOAT, Float.intBitsToFloat(data.readU4())); case LONG: return new EncodedValue(EncodedType.ENCODED_LONG, data.readS8()); case DOUBLE: return new EncodedValue(EncodedType.ENCODED_DOUBLE, Double.longBitsToDouble(data.readU8())); case CLASS: return new EncodedValue(EncodedType.ENCODED_TYPE, getClass(idx)); case METHOD_TYPE: return new EncodedValue(EncodedType.ENCODED_METHOD_TYPE, DescriptorParser.parseToMethodProto(getUtf8(readU2()))); case METHOD_HANDLE: return new EncodedValue(EncodedType.ENCODED_METHOD_HANDLE, getMethodHandle(idx)); default: throw new JavaClassParseException("Can't encode constant " + constantType + " as encoded value"); } } @NotNull private String parseString(byte[] bytes) { return ModifiedUTF8Decoder.decodeString(bytes); } private String fixType(String clsName) { switch (clsName.charAt(0)) { case '[': return clsName; case 'L': case 'T': if (clsName.endsWith(";")) { return clsName; } break; } return 'L' + clsName + ';'; } private void jumpToData(int idx) { data.absPos(offsets.getOffsetOfConstEntry(idx)); } private void jumpToTag(int idx) { data.absPos(offsets.getOffsetOfConstEntry(idx) - 1); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/ConstantType.java ================================================ package jadx.plugins.input.java.data; import java.util.stream.Stream; import jadx.plugins.input.java.utils.JavaClassParseException; public enum ConstantType { UTF8(1, -1), INTEGER(3, 4), FLOAT(4, 4), LONG(5, 8), DOUBLE(6, 8), CLASS(7, 2), STRING(8, 2), FIELD_REF(9, 4), METHOD_REF(10, 4), INTERFACE_METHOD_REF(11, 4), NAME_AND_TYPE(12, 4), METHOD_HANDLE(15, 3), METHOD_TYPE(16, 2), DYNAMIC(17, 4), INVOKE_DYNAMIC(18, 4), MODULE(19, 2), PACKAGE(20, 2); private static final ConstantType[] TAG_MAP; static { ConstantType[] values = ConstantType.values(); int maxVal = Stream.of(values) .mapToInt(ConstantType::getTag) .max() .orElseThrow(() -> new IllegalArgumentException("Empty ConstantType enum")); ConstantType[] map = new ConstantType[maxVal + 1]; for (ConstantType value : values) { map[value.getTag()] = value; } TAG_MAP = map; } public static ConstantType getTypeByTag(int tag) { ConstantType type = TAG_MAP[tag]; if (type == null) { throw new JavaClassParseException("Unknown constant pool tag: " + tag); } return type; } private final byte tag; private final int dataSize; ConstantType(int tag, int dataSize) { this.tag = (byte) tag; this.dataSize = dataSize; } public byte getTag() { return tag; } public int getDataSize() { return dataSize; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/DataReader.java ================================================ package jadx.plugins.input.java.data; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; public class DataReader { private final byte[] data; private int offset; public DataReader(byte[] data) { this(data, 0); } public DataReader(byte[] data, int offset) { this.data = data; this.offset = offset; } public DataReader copy() { return new DataReader(data, offset); } public DataReader absPos(int offset) { this.offset = offset; return this; } public int getOffset() { return offset; } public void skip(int size) { this.offset += size; } public int readS1() { int pos = this.offset; byte b1 = this.data[pos]; this.offset = pos + 1; return b1; } public int readU1() { int pos = this.offset; byte b1 = this.data[pos]; this.offset = pos + 1; return b1 & 0xFF; } public int readS2() { int pos = this.offset; byte[] d = this.data; byte b1 = d[pos++]; byte b2 = d[pos++]; this.offset = pos; return b1 << 8 | (b2 & 0xFF); } public int readU2() { int pos = this.offset; byte[] d = this.data; byte b1 = d[pos++]; byte b2 = d[pos++]; this.offset = pos; return (b1 & 0xFF) << 8 | (b2 & 0xFF); } public int readS4() { int pos = this.offset; byte[] d = this.data; byte b1 = d[pos++]; byte b2 = d[pos++]; byte b3 = d[pos++]; byte b4 = d[pos++]; this.offset = pos; return b1 << 24 | (b2 & 0xFF) << 16 | (b3 & 0xFF) << 8 | (b4 & 0xFF); } public int readU4() { int pos = this.offset; byte[] d = this.data; byte b1 = d[pos++]; byte b2 = d[pos++]; byte b3 = d[pos++]; byte b4 = d[pos++]; this.offset = pos; return (b1 & 0xFF) << 24 | (b2 & 0xFF) << 16 | (b3 & 0xFF) << 8 | (b4 & 0xFF); } public long readS8() { long high = readS4(); long low = readU4() & 0xFFFF_FFFFL; return high << 32 | low; } public long readU8() { long high = readU4() & 0xFFFF_FFFFL; long low = readU4() & 0xFFFF_FFFFL; return high << 32 | low; } public byte[] readBytes(int len) { int pos = this.offset; this.offset = pos + len; return Arrays.copyOfRange(data, pos, pos + len); } public List readClassesList(ConstPoolReader constPool) { int len = readU2(); if (len == 0) { return Collections.emptyList(); } List list = new ArrayList<>(len); for (int i = 0; i < len; i++) { list.add(constPool.getClass(readU2())); } return list; } public byte[] getBytes() { return data; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaClassData.java ================================================ package jadx.plugins.input.java.data; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.input.data.IFieldData; import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.data.ISeqConsumer; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.utils.Utils; import jadx.plugins.input.java.JavaClassReader; import jadx.plugins.input.java.data.attributes.AttributesReader; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.JavaAttrStorage; import jadx.plugins.input.java.data.attributes.JavaAttrType; import jadx.plugins.input.java.data.attributes.types.JavaAnnotationsAttr; import jadx.plugins.input.java.utils.DisasmUtils; public class JavaClassData implements IClassData { private final JavaClassReader clsReader; private final DataReader data; private final ClassOffsets offsets; private final ConstPoolReader constPoolReader; private final AttributesReader attributesReader; public JavaClassData(JavaClassReader clsReader) { this.clsReader = clsReader; this.data = new DataReader(clsReader.getData()); this.offsets = new ClassOffsets(this.data); this.constPoolReader = new ConstPoolReader(clsReader, this, this.data.copy(), this.offsets); this.attributesReader = new AttributesReader(this, this.constPoolReader); } @Override public int getInputFileOffset() { return offsets.getAccessFlagsOffset(); } @Override public IClassData copy() { return this; } @Override public int getAccessFlags() { return data.absPos(offsets.getAccessFlagsOffset()).readU2(); } @Override public String getType() { int idx = data.absPos(offsets.getClsTypeOffset()).readU2(); return constPoolReader.getClass(idx); } @Override @Nullable public String getSuperType() { int idx = data.absPos(offsets.getSuperTypeOffset()).readU2(); if (idx == 0) { return null; } return constPoolReader.getClass(idx); } @Override public List getInterfacesTypes() { data.absPos(offsets.getInterfacesOffset()); return data.readClassesList(constPoolReader); } @Override public String getInputFileName() { return this.clsReader.getFileName(); } @Override public void visitFieldsAndMethods(ISeqConsumer fieldsConsumer, ISeqConsumer mthConsumer) { int clsIdx = data.absPos(offsets.getClsTypeOffset()).readU2(); String classType = constPoolReader.getClass(clsIdx); DataReader reader = data.absPos(offsets.getFieldsOffset()).copy(); int fieldsCount = reader.readU2(); fieldsConsumer.init(fieldsCount); if (fieldsCount != 0) { JavaFieldData field = new JavaFieldData(); field.setParentClassType(classType); for (int i = 0; i < fieldsCount; i++) { parseField(reader, field); fieldsConsumer.accept(field); } } int methodsCount = reader.readU2(); mthConsumer.init(methodsCount); if (methodsCount != 0) { JavaMethodRef methodRef = new JavaMethodRef(); methodRef.setParentClassType(classType); JavaMethodData method = new JavaMethodData(this, methodRef); for (int i = 0; i < methodsCount; i++) { parseMethod(reader, method, i); mthConsumer.accept(method); } } } private void parseField(DataReader reader, JavaFieldData field) { int accessFlags = reader.readU2(); int nameIdx = reader.readU2(); int typeIdx = reader.readU2(); JavaAttrStorage attributes = attributesReader.loadAll(reader); field.setAccessFlags(accessFlags); field.setName(constPoolReader.getUtf8(nameIdx)); field.setType(constPoolReader.getUtf8(typeIdx)); field.setAttributes(attributes); } private void parseMethod(DataReader reader, JavaMethodData method, int id) { int accessFlags = reader.readU2(); int nameIdx = reader.readU2(); int descriptorIdx = reader.readU2(); JavaAttrStorage attributes = attributesReader.loadAll(reader); JavaMethodRef methodRef = method.getMethodRef(); methodRef.reset(); methodRef.initUniqId(clsReader, id, false); methodRef.setName(constPoolReader.getUtf8(nameIdx)); methodRef.setDescr(constPoolReader.getUtf8(descriptorIdx)); if (methodRef.getName().equals("")) { accessFlags |= AccessFlags.CONSTRUCTOR; // java bytecode don't use that flag } method.setData(accessFlags, attributes); } public DataReader getData() { return data; } @Override public List getAttributes() { data.absPos(offsets.getAttributesOffset()); JavaAttrStorage attributes = attributesReader.loadAll(data); int size = attributes.size(); if (size == 0) { return Collections.emptyList(); } List list = new ArrayList<>(size); Utils.addToList(list, JavaAnnotationsAttr.merge(attributes)); Utils.addToList(list, attributes.get(JavaAttrType.INNER_CLASSES)); Utils.addToList(list, attributes.get(JavaAttrType.SOURCE_FILE)); Utils.addToList(list, attributes.get(JavaAttrType.SIGNATURE)); return list; } public T loadClassAttribute(DataReader reader, JavaAttrType type) { reader.absPos(offsets.getAttributesOffset()); return attributesReader.loadOne(reader, type); } @Override public String getDisassembledCode() { return DisasmUtils.get(data.getBytes()); } public JavaClassReader getClsReader() { return clsReader; } public ClassOffsets getOffsets() { return offsets; } public ConstPoolReader getConstPoolReader() { return constPoolReader; } public AttributesReader getAttributesReader() { return attributesReader; } @Override public String toString() { return getInputFileName(); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaFieldData.java ================================================ package jadx.plugins.input.java.data; import java.util.ArrayList; import java.util.Collections; import java.util.List; import jadx.api.plugins.input.data.IFieldData; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.utils.Utils; import jadx.plugins.input.java.data.attributes.JavaAttrStorage; import jadx.plugins.input.java.data.attributes.JavaAttrType; import jadx.plugins.input.java.data.attributes.types.ConstValueAttr; import jadx.plugins.input.java.data.attributes.types.JavaAnnotationsAttr; public class JavaFieldData implements IFieldData { private String name; private String parentClassType; private String type; private int accessFlags; private JavaAttrStorage attributes; @Override public String getParentClassType() { return parentClassType; } public void setParentClassType(String parentClassType) { this.parentClassType = parentClassType; } @Override public String getType() { return type; } public void setType(String type) { this.type = type; } @Override public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public int getAccessFlags() { return accessFlags; } public void setAccessFlags(int accessFlags) { this.accessFlags = accessFlags; } public void setAttributes(JavaAttrStorage attributes) { this.attributes = attributes; } @Override public List getAttributes() { int size = attributes.size(); if (size == 0) { return Collections.emptyList(); } List list = new ArrayList<>(size); Utils.addToList(list, JavaAnnotationsAttr.merge(attributes)); Utils.addToList(list, attributes.get(JavaAttrType.CONST_VALUE), ConstValueAttr::getValue); Utils.addToList(list, attributes.get(JavaAttrType.SIGNATURE)); return list; } @Override public String toString() { return parentClassType + "->" + name + ":" + type; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaMethodData.java ================================================ package jadx.plugins.input.java.data; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.utils.Utils; import jadx.plugins.input.java.data.attributes.JavaAttrStorage; import jadx.plugins.input.java.data.attributes.JavaAttrType; import jadx.plugins.input.java.data.attributes.types.CodeAttr; import jadx.plugins.input.java.data.attributes.types.JavaAnnotationDefaultAttr; import jadx.plugins.input.java.data.attributes.types.JavaAnnotationsAttr; import jadx.plugins.input.java.data.attributes.types.JavaParamAnnsAttr; import jadx.plugins.input.java.data.code.JavaCodeReader; public class JavaMethodData implements IMethodData { private final JavaClassData clsData; private final JavaMethodRef methodRef; private int accessFlags; private JavaAttrStorage attributes; public JavaMethodData(JavaClassData clsData, JavaMethodRef methodRef) { this.clsData = clsData; this.methodRef = methodRef; } public void setData(int accessFlags, JavaAttrStorage attributes) { this.accessFlags = accessFlags; this.attributes = attributes; } @Override public JavaMethodRef getMethodRef() { return methodRef; } @Override public int getAccessFlags() { return accessFlags; } @Override public @Nullable ICodeReader getCodeReader() { CodeAttr codeAttr = attributes.get(JavaAttrType.CODE); if (codeAttr == null) { return null; } return new JavaCodeReader(clsData, codeAttr.getOffset()); } @Override public String disassembleMethod() { return ""; } @Override public List getAttributes() { int size = attributes.size(); if (size == 0) { return Collections.emptyList(); } List list = new ArrayList<>(size); Utils.addToList(list, JavaAnnotationsAttr.merge(attributes)); Utils.addToList(list, JavaParamAnnsAttr.merge(attributes)); Utils.addToList(list, JavaAnnotationDefaultAttr.convert(attributes)); Utils.addToList(list, attributes.get(JavaAttrType.SIGNATURE)); Utils.addToList(list, attributes.get(JavaAttrType.EXCEPTIONS)); Utils.addToList(list, attributes.get(JavaAttrType.METHOD_PARAMETERS)); return list; } @Override public String toString() { return getMethodRef().toString(); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaMethodProto.java ================================================ package jadx.plugins.input.java.data; import java.util.List; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.utils.Utils; public class JavaMethodProto implements IMethodProto { private String returnType; private List argTypes; @Override public String getReturnType() { return returnType; } public void setReturnType(String returnType) { this.returnType = returnType; } @Override public List getArgTypes() { return argTypes; } public void setArgTypes(List argTypes) { this.argTypes = argTypes; } @Override public String toString() { return "(" + Utils.listToStr(argTypes) + ")" + returnType; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaMethodRef.java ================================================ package jadx.plugins.input.java.data; import jadx.api.plugins.input.data.IMethodRef; import jadx.plugins.input.java.JavaClassReader; import jadx.plugins.input.java.utils.DescriptorParser; public class JavaMethodRef extends JavaMethodProto implements IMethodRef { private int uniqId; private String parentClassType; private String name; private String descr; @Override public int getUniqId() { return uniqId; } public void initUniqId(JavaClassReader clsReader, int id, boolean fromConstPool) { int readerId = clsReader.getId(); if (readerId > 0xFFFF || id > 0x7FFF) { // loaded more than 65535 classes or more than 32767 methods in this class -> disable caching this.uniqId = 0; } else { int source = fromConstPool ? 0 : 0x8000; this.uniqId = (readerId & 0xFFFF) << 16 | source | id & 0x7FFF; } } @Override public String getParentClassType() { return parentClassType; } public void setParentClassType(String parentClassType) { this.parentClassType = parentClassType; } @Override public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescriptor() { return descr; } public void setDescr(String descr) { this.descr = descr; } public void reset() { this.setReturnType(null); this.setArgTypes(null); } @Override public void load() { if (getReturnType() == null) { DescriptorParser.fillMethodProto(descr, this); } } @Override public String toString() { return parentClassType + "->" + name + descr; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/AttributesReader.java ================================================ package jadx.plugins.input.java.data.attributes; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.function.Predicate; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.plugins.input.java.data.ConstPoolReader; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.JavaClassData; public class AttributesReader { private static final Logger LOG = LoggerFactory.getLogger(AttributesReader.class); private static final Predicate> LOAD_ALL = type -> true; private final JavaClassData clsData; private final ConstPoolReader constPool; private final Map> attrCache = new HashMap<>(JavaAttrType.size()); public AttributesReader(JavaClassData clsData, ConstPoolReader constPoolReader) { this.clsData = clsData; this.constPool = constPoolReader; } public JavaAttrStorage loadAll(DataReader reader) { return loadAttributes(reader, LOAD_ALL); } public JavaAttrStorage loadMulti(DataReader reader, Set> types) { return loadAttributes(reader, types::contains); } /** * Load attributes into storage * * @param reader - reader pos should be set to attributes section start * @param condition - check if attribute should be parsed and added to storage */ private JavaAttrStorage loadAttributes(DataReader reader, Predicate> condition) { int count = reader.readU2(); if (count == 0) { return JavaAttrStorage.EMPTY; } JavaAttrStorage storage = new JavaAttrStorage(); for (int i = 0; i < count; i++) { int nameIdx = reader.readU2(); int len = reader.readU4(); int end = reader.getOffset() + len; try { JavaAttrType attrType = resolveAttrReader(nameIdx); if (attrType != null && condition.test(attrType)) { IJavaAttributeReader attrReader = attrType.getReader(); if (attrReader != null) { IJavaAttribute attrValue = attrReader.read(clsData, reader); if (attrValue != null) { storage.add(attrType, attrValue); } } } } catch (Exception e) { LOG.error("Failed to parse attribute: {}", constPool.getUtf8(nameIdx), e); } finally { reader.absPos(end); } } return storage; } @SuppressWarnings("unchecked") public @Nullable T loadOne(DataReader reader, JavaAttrType type) { int count = reader.readU2(); for (int i = 0; i < count; i++) { int nameIdx = reader.readU2(); int len = reader.readU4(); int end = reader.getOffset() + len; try { JavaAttrType attrType = resolveAttrReader(nameIdx); if (attrType == type) { return (T) attrType.getReader().read(clsData, reader); } } catch (Exception e) { LOG.error("Failed to parse attribute: {}", constPool.getUtf8(nameIdx), e); } finally { reader.absPos(end); } } return null; } private JavaAttrType resolveAttrReader(int nameIdx) { return attrCache.computeIfAbsent(nameIdx, idx -> { String attrName = constPool.getUtf8(idx); JavaAttrType attrType = JavaAttrType.byName(attrName); if (attrType == null) { LOG.warn("Unknown java class attribute type: {}", attrName); return null; } return attrType; }); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/EncodedValueReader.java ================================================ package jadx.plugins.input.java.data.attributes; import java.util.ArrayList; import java.util.List; import jadx.api.plugins.input.data.annotations.AnnotationVisibility; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.impl.JadxFieldRef; import jadx.plugins.input.java.data.ConstPoolReader; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.JavaClassData; import jadx.plugins.input.java.data.attributes.types.JavaAnnotationsAttr; import jadx.plugins.input.java.utils.JavaClassParseException; public class EncodedValueReader { public static EncodedValue read(JavaClassData clsData, DataReader reader) { ConstPoolReader constPool = clsData.getConstPoolReader(); char tag = (char) reader.readU1(); switch (tag) { case 'B': return new EncodedValue(EncodedType.ENCODED_BYTE, (byte) constPool.getInt(reader.readU2())); case 'C': return new EncodedValue(EncodedType.ENCODED_CHAR, (char) constPool.getInt(reader.readU2())); case 'D': return new EncodedValue(EncodedType.ENCODED_DOUBLE, constPool.getDouble(reader.readU2())); case 'F': return new EncodedValue(EncodedType.ENCODED_FLOAT, constPool.getFloat(reader.readU2())); case 'I': return new EncodedValue(EncodedType.ENCODED_INT, constPool.getInt(reader.readU2())); case 'J': return new EncodedValue(EncodedType.ENCODED_LONG, constPool.getLong(reader.readU2())); case 'S': return new EncodedValue(EncodedType.ENCODED_SHORT, (short) constPool.getInt(reader.readU2())); case 'Z': return new EncodedValue(EncodedType.ENCODED_BOOLEAN, 1 == constPool.getInt(reader.readU2())); case 's': return new EncodedValue(EncodedType.ENCODED_STRING, constPool.getUtf8(reader.readU2())); case 'e': String cls = constPool.getUtf8(reader.readU2()); String name = constPool.getUtf8(reader.readU2()); return new EncodedValue(EncodedType.ENCODED_ENUM, new JadxFieldRef(cls, name, cls)); case 'c': return new EncodedValue(EncodedType.ENCODED_TYPE, constPool.getUtf8(reader.readU2())); case '@': return new EncodedValue(EncodedType.ENCODED_ANNOTATION, JavaAnnotationsAttr.readAnnotation(AnnotationVisibility.RUNTIME, clsData, reader)); case '[': int len = reader.readU2(); List values = new ArrayList<>(len); for (int i = 0; i < len; i++) { values.add(read(clsData, reader)); } return new EncodedValue(EncodedType.ENCODED_ARRAY, values); default: throw new JavaClassParseException("Unknown element value tag: " + tag); } } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/IJavaAttribute.java ================================================ package jadx.plugins.input.java.data.attributes; public interface IJavaAttribute { } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/IJavaAttributeReader.java ================================================ package jadx.plugins.input.java.data.attributes; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.JavaClassData; public interface IJavaAttributeReader { IJavaAttribute read(JavaClassData clsData, DataReader reader); } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/JavaAttrStorage.java ================================================ package jadx.plugins.input.java.data.attributes; import org.jetbrains.annotations.Nullable; public class JavaAttrStorage { public static final JavaAttrStorage EMPTY = new JavaAttrStorage(); private final IJavaAttribute[] map = new IJavaAttribute[JavaAttrType.size()]; public void add(JavaAttrType type, IJavaAttribute value) { map[type.getId()] = value; } @SuppressWarnings("unchecked") @Nullable public A get(JavaAttrType type) { return (A) map[type.getId()]; } public int size() { int size = 0; for (IJavaAttribute attr : map) { if (attr != null) { size++; } } return size; } @Override public String toString() { return "AttributesStorage{size=" + size() + '}'; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/JavaAttrType.java ================================================ package jadx.plugins.input.java.data.attributes; import java.util.HashMap; import java.util.Map; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.annotations.AnnotationVisibility; import jadx.plugins.input.java.data.attributes.debuginfo.LineNumberTableAttr; import jadx.plugins.input.java.data.attributes.debuginfo.LocalVarTypesAttr; import jadx.plugins.input.java.data.attributes.debuginfo.LocalVarsAttr; import jadx.plugins.input.java.data.attributes.stack.StackMapTableReader; import jadx.plugins.input.java.data.attributes.types.CodeAttr; import jadx.plugins.input.java.data.attributes.types.ConstValueAttr; import jadx.plugins.input.java.data.attributes.types.IgnoredAttr; import jadx.plugins.input.java.data.attributes.types.JavaAnnotationDefaultAttr; import jadx.plugins.input.java.data.attributes.types.JavaAnnotationsAttr; import jadx.plugins.input.java.data.attributes.types.JavaBootstrapMethodsAttr; import jadx.plugins.input.java.data.attributes.types.JavaExceptionsAttr; import jadx.plugins.input.java.data.attributes.types.JavaInnerClsAttr; import jadx.plugins.input.java.data.attributes.types.JavaMethodParametersAttr; import jadx.plugins.input.java.data.attributes.types.JavaParamAnnsAttr; import jadx.plugins.input.java.data.attributes.types.JavaSignatureAttr; import jadx.plugins.input.java.data.attributes.types.JavaSourceFileAttr; import jadx.plugins.input.java.data.attributes.types.StackMapTableAttr; public final class JavaAttrType { private static final Map> NAME_TO_TYPE_MAP; public static final JavaAttrType INNER_CLASSES; public static final JavaAttrType BOOTSTRAP_METHODS; public static final JavaAttrType CONST_VALUE; public static final JavaAttrType CODE; public static final JavaAttrType STACK_MAP_TABLE; public static final JavaAttrType LINE_NUMBER_TABLE; public static final JavaAttrType LOCAL_VAR_TABLE; public static final JavaAttrType LOCAL_VAR_TYPE_TABLE; public static final JavaAttrType RUNTIME_ANNOTATIONS; public static final JavaAttrType BUILD_ANNOTATIONS; public static final JavaAttrType RUNTIME_PARAMETER_ANNOTATIONS; public static final JavaAttrType BUILD_PARAMETER_ANNOTATIONS; public static final JavaAttrType RUNTIME_TYPE_ANNOTATIONS; public static final JavaAttrType BUILD_TYPE_ANNOTATIONS; public static final JavaAttrType ANNOTATION_DEFAULT; public static final JavaAttrType SOURCE_FILE; public static final JavaAttrType SIGNATURE; public static final JavaAttrType EXCEPTIONS; public static final JavaAttrType METHOD_PARAMETERS; public static final JavaAttrType DEPRECATED; public static final JavaAttrType SYNTHETIC; public static final JavaAttrType ENCLOSING_METHOD; public static final JavaAttrType MODULE; public static final JavaAttrType SOURCE_DEBUG_EXTENSION; public static final JavaAttrType NEST_HOST; public static final JavaAttrType NEST_MEMBERS; static { NAME_TO_TYPE_MAP = new HashMap<>(); CONST_VALUE = bind("ConstantValue", ConstValueAttr.reader()); CODE = bind("Code", CodeAttr.reader()); LINE_NUMBER_TABLE = bind("LineNumberTable", LineNumberTableAttr.reader()); LOCAL_VAR_TABLE = bind("LocalVariableTable", LocalVarsAttr.reader()); LOCAL_VAR_TYPE_TABLE = bind("LocalVariableTypeTable", LocalVarTypesAttr.reader()); INNER_CLASSES = bind("InnerClasses", JavaInnerClsAttr.reader()); BOOTSTRAP_METHODS = bind("BootstrapMethods", JavaBootstrapMethodsAttr.reader()); RUNTIME_ANNOTATIONS = bind("RuntimeVisibleAnnotations", JavaAnnotationsAttr.reader(AnnotationVisibility.RUNTIME)); BUILD_ANNOTATIONS = bind("RuntimeInvisibleAnnotations", JavaAnnotationsAttr.reader(AnnotationVisibility.BUILD)); RUNTIME_PARAMETER_ANNOTATIONS = bind("RuntimeVisibleParameterAnnotations", JavaParamAnnsAttr.reader(AnnotationVisibility.RUNTIME)); BUILD_PARAMETER_ANNOTATIONS = bind("RuntimeInvisibleParameterAnnotations", JavaParamAnnsAttr.reader(AnnotationVisibility.BUILD)); ANNOTATION_DEFAULT = bind("AnnotationDefault", JavaAnnotationDefaultAttr.reader()); SOURCE_FILE = bind("SourceFile", JavaSourceFileAttr.reader()); SIGNATURE = bind("Signature", JavaSignatureAttr.reader()); EXCEPTIONS = bind("Exceptions", JavaExceptionsAttr.reader()); METHOD_PARAMETERS = bind("MethodParameters", JavaMethodParametersAttr.reader()); STACK_MAP_TABLE = bind("StackMapTable", new StackMapTableReader()); // ignored DEPRECATED = bind("Deprecated", null); // duplicated by annotation SYNTHETIC = bind("Synthetic", null); // duplicated by access flag ENCLOSING_METHOD = bind("EnclosingMethod", null); // TODO: not supported yet RUNTIME_TYPE_ANNOTATIONS = bind("RuntimeVisibleTypeAnnotations", null); BUILD_TYPE_ANNOTATIONS = bind("RuntimeInvisibleTypeAnnotations", null); MODULE = bind("Module", null); NEST_HOST = bind("NestHost", null); NEST_MEMBERS = bind("NestMembers", null); SOURCE_DEBUG_EXTENSION = bind("SourceDebugExtension", null); } private static JavaAttrType bind(String name, IJavaAttributeReader reader) { JavaAttrType attrType = new JavaAttrType<>(NAME_TO_TYPE_MAP.size(), name, reader); NAME_TO_TYPE_MAP.put(name, attrType); return attrType; } @Nullable public static JavaAttrType byName(String name) { return NAME_TO_TYPE_MAP.get(name); } public static int size() { return NAME_TO_TYPE_MAP.size(); } private final int id; private final String name; private final IJavaAttributeReader reader; private JavaAttrType(int id, String name, IJavaAttributeReader reader) { this.id = id; this.name = name; this.reader = reader; } public int getId() { return id; } public String getName() { return name; } public IJavaAttributeReader getReader() { return reader; } @Override public int hashCode() { return id; } @Override public boolean equals(Object o) { if (this == o) { return true; } return id == ((JavaAttrType) o).id; } @Override public String toString() { return name; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/debuginfo/JavaLocalVar.java ================================================ package jadx.plugins.input.java.data.attributes.debuginfo; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ILocalVar; public class JavaLocalVar implements ILocalVar { private int regNum; private final String name; private final String type; @Nullable private String sign; private final int startOffset; private final int endOffset; public JavaLocalVar(int regNum, String name, @Nullable String type, @Nullable String sign, int startOffset, int endOffset) { this.regNum = regNum; this.name = name; this.type = type; this.sign = sign; this.startOffset = startOffset; this.endOffset = endOffset; } public void shiftRegNum(int maxStack) { this.regNum += maxStack; // convert local var to register } @Override public String getName() { return name; } @Override public int getRegNum() { return regNum; } @Override public String getType() { return type; } @Override public @Nullable String getSignature() { return sign; } public void setSignature(String sign) { this.sign = sign; } @Override public int getStartOffset() { return startOffset; } @Override public int getEndOffset() { return endOffset; } @Override public boolean isMarkedAsParameter() { return false; } @Override public int hashCode() { int result = regNum; result = 31 * result + name.hashCode(); result = 31 * result + startOffset; result = 31 * result + endOffset; return result; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof JavaLocalVar)) { return false; } JavaLocalVar other = (JavaLocalVar) o; return regNum == other.regNum && startOffset == other.startOffset && endOffset == other.endOffset && name.equals(other.name); } private static String formatOffset(int offset) { return String.format("0x%04x", offset); } @Override public String toString() { return formatOffset(startOffset) + '-' + formatOffset(endOffset) + ": r" + regNum + " '" + name + "' " + type + (sign != null ? ", signature: " + sign : ""); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/debuginfo/LineNumberTableAttr.java ================================================ package jadx.plugins.input.java.data.attributes.debuginfo; import java.util.HashMap; import java.util.Map; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; public class LineNumberTableAttr implements IJavaAttribute { private final Map lineMap; public LineNumberTableAttr(Map sourceLineMap) { this.lineMap = sourceLineMap; } public Map getLineMap() { return lineMap; } public static IJavaAttributeReader reader() { return (clsData, reader) -> { int len = reader.readU2(); Map map = new HashMap<>(len); for (int i = 0; i < len; i++) { int offset = reader.readU2(); int line = reader.readU2(); map.put(offset, line); } return new LineNumberTableAttr(map); }; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/debuginfo/LocalVarTypesAttr.java ================================================ package jadx.plugins.input.java.data.attributes.debuginfo; import java.util.ArrayList; import java.util.List; import jadx.plugins.input.java.data.ConstPoolReader; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; public class LocalVarTypesAttr implements IJavaAttribute { private final List vars; public LocalVarTypesAttr(List vars) { this.vars = vars; } public List getVars() { return vars; } public static IJavaAttributeReader reader() { return (clsData, reader) -> { ConstPoolReader constPool = clsData.getConstPoolReader(); int len = reader.readU2(); List varsList = new ArrayList<>(len); for (int i = 0; i < len; i++) { int startOffset = reader.readU2(); int endOffset = startOffset + reader.readU2() - 1; int nameIdx = reader.readU2(); int typeIdx = reader.readU2(); int varNum = reader.readU2(); varsList.add(new JavaLocalVar( varNum, constPool.getUtf8(nameIdx), null, constPool.getUtf8(typeIdx), startOffset, endOffset)); } return new LocalVarTypesAttr(varsList); }; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/debuginfo/LocalVarsAttr.java ================================================ package jadx.plugins.input.java.data.attributes.debuginfo; import java.util.ArrayList; import java.util.List; import jadx.plugins.input.java.data.ConstPoolReader; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; public class LocalVarsAttr implements IJavaAttribute { private final List vars; public LocalVarsAttr(List vars) { this.vars = vars; } public List getVars() { return vars; } public static IJavaAttributeReader reader() { return (clsData, reader) -> { ConstPoolReader constPool = clsData.getConstPoolReader(); int count = reader.readU2(); List varsList = new ArrayList<>(count); for (int i = 0; i < count; i++) { int startOffset = reader.readU2(); int length = reader.readU2(); int endOffset = startOffset + length - 1; int nameIdx = reader.readU2(); int typeIdx = reader.readU2(); int varNum = reader.readU2(); varsList.add(new JavaLocalVar(varNum, constPool.getUtf8(nameIdx), constPool.getUtf8(typeIdx), null, startOffset, endOffset)); } return new LocalVarsAttr(varsList); }; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/stack/StackFrame.java ================================================ package jadx.plugins.input.java.data.attributes.stack; public class StackFrame { private final int offset; private final StackFrameType type; private int stackSize; private StackValueType[] stackValueTypes; private int localsCount; public StackFrame(int offset, StackFrameType type) { this.offset = offset; this.type = type; } public int getOffset() { return offset; } public StackFrameType getType() { return type; } public int getLocalsCount() { return localsCount; } public void setLocalsCount(int localsCount) { this.localsCount = localsCount; } public int getStackSize() { return stackSize; } public void setStackSize(int stackSize) { this.stackSize = stackSize; } public StackValueType[] getStackValueTypes() { return stackValueTypes; } public void setStackValueTypes(StackValueType[] stackValueTypes) { this.stackValueTypes = stackValueTypes; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/stack/StackFrameType.java ================================================ package jadx.plugins.input.java.data.attributes.stack; import org.jetbrains.annotations.Nullable; public enum StackFrameType { SAME_FRAME(0, 63), SAME_LOCALS_1_STACK(64, 127), SAME_LOCALS_1_STACK_EXTENDED(247, 247), CHOP(248, 250), SAME_FRAME_EXTENDED(251, 251), APPEND(252, 254), FULL(255, 255); private final int start; private final int end; StackFrameType(int start, int end) { this.start = start; this.end = end; } private static final StackFrameType[] MAPPING = buildMapping(); private static StackFrameType[] buildMapping() { StackFrameType[] mapping = new StackFrameType[256]; for (StackFrameType value : StackFrameType.values()) { for (int i = value.start; i <= value.end; i++) { mapping[i] = value; } } return mapping; } public static @Nullable StackFrameType getType(int data) { return MAPPING[data]; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/stack/StackMapTableReader.java ================================================ package jadx.plugins.input.java.data.attributes.stack; import java.util.EnumMap; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.JavaClassData; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; import jadx.plugins.input.java.data.attributes.types.StackMapTableAttr; import jadx.plugins.input.java.utils.JavaClassParseException; public class StackMapTableReader implements IJavaAttributeReader { @Override public IJavaAttribute read(JavaClassData clsData, DataReader reader) { int count = reader.readU2(); Map map = new HashMap<>(count, 1); StackFrame prevFrame = null; for (int i = 0; i < count; i++) { StackFrame frame = readFrame(reader, prevFrame); map.put(frame.getOffset(), frame); prevFrame = frame; } return new StackMapTableAttr(map); } private static final Map> FRAME_READERS = registerReaders(); private static Map> registerReaders() { EnumMap> map = new EnumMap<>(StackFrameType.class); map.put(StackFrameType.SAME_FRAME, context -> readSame(context, false)); map.put(StackFrameType.SAME_FRAME_EXTENDED, context -> readSame(context, true)); map.put(StackFrameType.SAME_LOCALS_1_STACK, context -> readSL1S(context, false)); map.put(StackFrameType.SAME_LOCALS_1_STACK_EXTENDED, context -> readSL1S(context, true)); map.put(StackFrameType.CHOP, StackMapTableReader::readChop); map.put(StackFrameType.APPEND, StackMapTableReader::readAppend); map.put(StackFrameType.FULL, StackMapTableReader::readFull); return map; } private StackFrame readFrame(DataReader reader, StackFrame prevFrame) { int typeData = reader.readU1(); StackFrameType frameType = StackFrameType.getType(typeData); Consumer frameReader = FRAME_READERS.get(frameType); if (frameReader == null) { throw new JavaClassParseException("Found unsupported stack frame type: " + frameType); } FrameContext frameContext = new FrameContext(reader, typeData, prevFrame); frameReader.accept(frameContext); return Objects.requireNonNull(frameContext.getFrame()); } private static void readSame(FrameContext context, boolean extended) { int offsetDelta; StackFrameType type; if (extended) { type = StackFrameType.SAME_FRAME_EXTENDED; offsetDelta = context.getDataReader().readU2(); } else { type = StackFrameType.SAME_FRAME; offsetDelta = context.getTypeData(); } StackFrame frame = new StackFrame(calcOffset(context, offsetDelta), type); frame.setStackSize(0); frame.setLocalsCount(getPrevLocalsCount(context)); context.setFrame(frame); } private static void readSL1S(FrameContext context, boolean extended) { DataReader reader = context.getDataReader(); int offsetDelta; StackFrameType type; if (extended) { type = StackFrameType.SAME_LOCALS_1_STACK_EXTENDED; offsetDelta = reader.readU2(); } else { type = StackFrameType.SAME_LOCALS_1_STACK; offsetDelta = context.getTypeData() - 64; } StackValueType[] stackTypes = TypeInfoReader.readTypeInfoList(reader, 1); StackFrame frame = new StackFrame(calcOffset(context, offsetDelta), type); frame.setStackSize(1); frame.setStackValueTypes(stackTypes); frame.setLocalsCount(getPrevLocalsCount(context)); context.setFrame(frame); } private static void readChop(FrameContext context) { int k = 251 - context.getTypeData(); int offsetDelta = context.getDataReader().readU2(); StackFrame frame = new StackFrame(calcOffset(context, offsetDelta), StackFrameType.CHOP); frame.setStackSize(0); frame.setLocalsCount(getPrevLocalsCount(context) - k); context.setFrame(frame); } private static void readAppend(FrameContext context) { DataReader reader = context.getDataReader(); int k = context.getTypeData() - 251; int offsetDelta = reader.readU2(); TypeInfoReader.skipTypeInfoList(reader, k); StackFrame frame = new StackFrame(calcOffset(context, offsetDelta), StackFrameType.APPEND); frame.setStackSize(0); frame.setLocalsCount(getPrevLocalsCount(context) - k); context.setFrame(frame); } private static void readFull(FrameContext context) { DataReader reader = context.getDataReader(); int offsetDelta = reader.readU2(); int localsCount = reader.readU2(); TypeInfoReader.skipTypeInfoList(reader, localsCount); int stackSize = reader.readU2(); StackValueType[] stackTypes = TypeInfoReader.readTypeInfoList(reader, stackSize); StackFrame frame = new StackFrame(calcOffset(context, offsetDelta), StackFrameType.FULL); frame.setLocalsCount(localsCount); frame.setStackSize(stackSize); frame.setStackValueTypes(stackTypes); context.setFrame(frame); } private static int calcOffset(FrameContext context, int offsetDelta) { StackFrame prevFrame = context.getPrevFrame(); if (prevFrame == null) { return offsetDelta; } return prevFrame.getOffset() + offsetDelta + 1; } private static int getPrevLocalsCount(FrameContext context) { StackFrame prevFrame = context.getPrevFrame(); if (prevFrame == null) { return 0; } return prevFrame.getLocalsCount(); } private static final class FrameContext { private final DataReader dataReader; private final int typeData; private final StackFrame prevFrame; private StackFrame frame; private FrameContext(DataReader dataReader, int typeData, StackFrame prevFrame) { this.dataReader = dataReader; this.typeData = typeData; this.prevFrame = prevFrame; } public DataReader getDataReader() { return dataReader; } public int getTypeData() { return typeData; } public StackFrame getPrevFrame() { return prevFrame; } public StackFrame getFrame() { return frame; } public void setFrame(StackFrame frame) { this.frame = frame; } } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/stack/StackValueType.java ================================================ package jadx.plugins.input.java.data.attributes.stack; public enum StackValueType { NARROW, // int, float, etc WIDE, // long, double } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/stack/TypeInfoReader.java ================================================ package jadx.plugins.input.java.data.attributes.stack; import jadx.plugins.input.java.data.DataReader; public class TypeInfoReader { private static final int ITEM_TOP = 0; private static final int ITEM_INT = 1; private static final int ITEM_FLOAT = 2; private static final int ITEM_DOUBLE = 3; private static final int ITEM_LONG = 4; private static final int ITEM_NULL = 5; private static final int ITEM_UNINITIALIZED_THIS = 6; private static final int ITEM_OBJECT = 7; private static final int ITEM_UNINITIALIZED = 8; static StackValueType[] readTypeInfoList(DataReader reader, int count) { StackValueType[] types = new StackValueType[count]; for (int i = 0; i < count; i++) { int tag = reader.readU1(); StackValueType type; switch (tag) { case ITEM_DOUBLE: case ITEM_LONG: type = StackValueType.WIDE; break; case ITEM_OBJECT: case ITEM_UNINITIALIZED: reader.readU2(); // ignore type = StackValueType.NARROW; break; default: type = StackValueType.NARROW; break; } types[i] = type; } return types; } static void skipTypeInfoList(DataReader reader, int count) { for (int i = 0; i < count; i++) { int tag = reader.readU1(); if (tag == ITEM_OBJECT || tag == ITEM_UNINITIALIZED) { reader.readU2(); } } } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/CodeAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; public class CodeAttr implements IJavaAttribute { private final int offset; public CodeAttr(int offset) { this.offset = offset; } public int getOffset() { return offset; } public static IJavaAttributeReader reader() { return (clsData, reader) -> new CodeAttr(reader.getOffset()); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/ConstValueAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; public class ConstValueAttr implements IJavaAttribute { private final EncodedValue value; public ConstValueAttr(EncodedValue value) { this.value = value; } public EncodedValue getValue() { return value; } public static IJavaAttributeReader reader() { return (clsData, reader) -> new ConstValueAttr(clsData.getConstPoolReader().readAsEncodedValue(reader.readU2())); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/IgnoredAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import jadx.plugins.input.java.data.attributes.IJavaAttribute; @SuppressWarnings("unused") public class IgnoredAttr implements IJavaAttribute { } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/JavaAnnotationDefaultAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.types.AnnotationDefaultAttr; import jadx.plugins.input.java.data.attributes.EncodedValueReader; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; import jadx.plugins.input.java.data.attributes.JavaAttrStorage; import jadx.plugins.input.java.data.attributes.JavaAttrType; public class JavaAnnotationDefaultAttr extends AnnotationDefaultAttr implements IJavaAttribute { public JavaAnnotationDefaultAttr(EncodedValue value) { super(value); } public static IJavaAttributeReader reader() { return (clsData, reader) -> new JavaAnnotationDefaultAttr(EncodedValueReader.read(clsData, reader)); } public static AnnotationDefaultAttr convert(JavaAttrStorage attributes) { return attributes.get(JavaAttrType.ANNOTATION_DEFAULT); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/JavaAnnotationsAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import jadx.api.plugins.input.data.annotations.AnnotationVisibility; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.annotations.JadxAnnotation; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.api.plugins.utils.Utils; import jadx.plugins.input.java.data.ConstPoolReader; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.JavaClassData; import jadx.plugins.input.java.data.attributes.EncodedValueReader; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; import jadx.plugins.input.java.data.attributes.JavaAttrStorage; import jadx.plugins.input.java.data.attributes.JavaAttrType; public class JavaAnnotationsAttr implements IJavaAttribute { private final List list; public JavaAnnotationsAttr(List list) { this.list = list; } public List getList() { return list; } public static IJavaAttributeReader reader(AnnotationVisibility visibility) { return (clsData, reader) -> new JavaAnnotationsAttr(readAnnotationsList(visibility, clsData, reader)); } public static List readAnnotationsList(AnnotationVisibility visibility, JavaClassData clsData, DataReader reader) { int len = reader.readU2(); List list = new ArrayList<>(len); for (int i = 0; i < len; i++) { list.add(readAnnotation(visibility, clsData, reader)); } return list; } public static JadxAnnotation readAnnotation(AnnotationVisibility visibility, JavaClassData clsData, DataReader reader) { ConstPoolReader constPool = clsData.getConstPoolReader(); String type = constPool.getUtf8(reader.readU2()); int pairsCount = reader.readU2(); Map pairs = new LinkedHashMap<>(pairsCount); for (int j = 0; j < pairsCount; j++) { String name = constPool.getUtf8(reader.readU2()); EncodedValue value = EncodedValueReader.read(clsData, reader); pairs.put(name, value); } return new JadxAnnotation(visibility, type, pairs); } public static AnnotationsAttr merge(JavaAttrStorage storage) { JavaAnnotationsAttr runtimeAnnAttr = storage.get(JavaAttrType.RUNTIME_ANNOTATIONS); JavaAnnotationsAttr buildAnnAttr = storage.get(JavaAttrType.BUILD_ANNOTATIONS); if (runtimeAnnAttr == null && buildAnnAttr == null) { return null; } if (buildAnnAttr == null) { return AnnotationsAttr.pack(runtimeAnnAttr.getList()); } if (runtimeAnnAttr == null) { return AnnotationsAttr.pack(buildAnnAttr.getList()); } return AnnotationsAttr.pack(Utils.concat(runtimeAnnAttr.getList(), buildAnnAttr.getList())); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/JavaBootstrapMethodsAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import java.util.ArrayList; import java.util.List; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; import jadx.plugins.input.java.data.attributes.types.data.RawBootstrapMethod; public class JavaBootstrapMethodsAttr implements IJavaAttribute { private final List list; public JavaBootstrapMethodsAttr(List list) { this.list = list; } public List getList() { return list; } public static IJavaAttributeReader reader() { return (clsData, reader) -> { int len = reader.readU2(); List list = new ArrayList<>(len); for (int i = 0; i < len; i++) { int methodHandleIdx = reader.readU2(); int argsCount = reader.readU2(); int[] args = new int[argsCount]; for (int j = 0; j < argsCount; j++) { args[j] = reader.readU2(); } list.add(new RawBootstrapMethod(methodHandleIdx, args)); } return new JavaBootstrapMethodsAttr(list); }; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/JavaExceptionsAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import java.util.List; import jadx.api.plugins.input.data.attributes.types.ExceptionsAttr; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; public class JavaExceptionsAttr extends ExceptionsAttr implements IJavaAttribute { public JavaExceptionsAttr(List list) { super(list); } public static IJavaAttributeReader reader() { return (clsData, reader) -> new JavaExceptionsAttr(reader.readClassesList(clsData.getConstPoolReader())); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/JavaInnerClsAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import java.util.HashMap; import java.util.Map; import jadx.api.plugins.input.data.attributes.types.InnerClassesAttr; import jadx.api.plugins.input.data.attributes.types.InnerClsInfo; import jadx.plugins.input.java.data.ConstPoolReader; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; public class JavaInnerClsAttr extends InnerClassesAttr implements IJavaAttribute { public JavaInnerClsAttr(Map map) { super(map); } public static IJavaAttributeReader reader() { return (clsData, reader) -> { int len = reader.readU2(); ConstPoolReader constPool = clsData.getConstPoolReader(); Map clsMap = new HashMap<>(len); for (int i = 0; i < len; i++) { String innerCls = constPool.getClass(reader.readU2()); int outerClsIdx = reader.readU2(); String outerCls = outerClsIdx == 0 ? null : constPool.getClass(outerClsIdx); String name = constPool.getUtf8(reader.readU2()); int accFlags = reader.readU2(); clsMap.put(innerCls, new InnerClsInfo(innerCls, outerCls, name, accFlags)); } return new JavaInnerClsAttr(clsMap); }; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/JavaMethodParametersAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import java.util.ArrayList; import java.util.List; import jadx.api.plugins.input.data.attributes.types.MethodParametersAttr; import jadx.plugins.input.java.data.ConstPoolReader; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; public class JavaMethodParametersAttr extends MethodParametersAttr implements IJavaAttribute { public JavaMethodParametersAttr(List list) { super(list); } public static IJavaAttributeReader reader() { return (clsData, reader) -> { ConstPoolReader constPool = clsData.getConstPoolReader(); int count = reader.readU1(); List params = new ArrayList<>(count); for (int i = 0; i < count; i++) { String name = constPool.getUtf8(reader.readU2()); int accessFlags = reader.readU2(); params.add(new Info(accessFlags, name)); } return new JavaMethodParametersAttr(params); }; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/JavaParamAnnsAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import java.util.ArrayList; import java.util.Collections; import java.util.List; import jadx.api.plugins.input.data.annotations.AnnotationVisibility; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr; import jadx.api.plugins.utils.Utils; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; import jadx.plugins.input.java.data.attributes.JavaAttrStorage; import jadx.plugins.input.java.data.attributes.JavaAttrType; public class JavaParamAnnsAttr implements IJavaAttribute { private final List> list; public JavaParamAnnsAttr(List> list) { this.list = list; } public List> getList() { return list; } public static IJavaAttributeReader reader(AnnotationVisibility visibility) { return (clsData, reader) -> { int len = reader.readU1(); List> list = new ArrayList<>(len); for (int i = 0; i < len; i++) { list.add(JavaAnnotationsAttr.readAnnotationsList(visibility, clsData, reader)); } return new JavaParamAnnsAttr(list); }; } public static AnnotationMethodParamsAttr merge(JavaAttrStorage storage) { JavaParamAnnsAttr runtimeAnnAttr = storage.get(JavaAttrType.RUNTIME_PARAMETER_ANNOTATIONS); JavaParamAnnsAttr buildAnnAttr = storage.get(JavaAttrType.BUILD_PARAMETER_ANNOTATIONS); if (runtimeAnnAttr == null && buildAnnAttr == null) { return null; } if (buildAnnAttr == null) { return AnnotationMethodParamsAttr.pack(runtimeAnnAttr.getList()); } if (runtimeAnnAttr == null) { return AnnotationMethodParamsAttr.pack(buildAnnAttr.getList()); } return AnnotationMethodParamsAttr.pack(mergeParamLists(runtimeAnnAttr.getList(), buildAnnAttr.getList())); } private static List> mergeParamLists(List> first, List> second) { int firstSize = first.size(); int secondSize = second.size(); int size = Math.max(firstSize, secondSize); List> result = new ArrayList<>(size); for (int i = 0; i < size; i++) { List firstList = i < firstSize ? first.get(i) : Collections.emptyList(); List secondList = i < secondSize ? second.get(i) : Collections.emptyList(); result.add(Utils.concat(firstList, secondList)); } return result; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/JavaSignatureAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import jadx.api.plugins.input.data.attributes.types.SignatureAttr; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; public class JavaSignatureAttr extends SignatureAttr implements IJavaAttribute { public JavaSignatureAttr(String signature) { super(signature); } public static IJavaAttributeReader reader() { return (clsData, reader) -> new JavaSignatureAttr(clsData.getConstPoolReader().getUtf8(reader.readU2())); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/JavaSourceFileAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import jadx.api.plugins.input.data.attributes.types.SourceFileAttr; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.IJavaAttributeReader; public class JavaSourceFileAttr extends SourceFileAttr implements IJavaAttribute { public JavaSourceFileAttr(String fileName) { super(fileName); } public static IJavaAttributeReader reader() { return (clsData, reader) -> new JavaSourceFileAttr(clsData.getConstPoolReader().getUtf8(reader.readU2())); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/StackMapTableAttr.java ================================================ package jadx.plugins.input.java.data.attributes.types; import java.util.Collections; import java.util.Map; import org.jetbrains.annotations.Nullable; import jadx.plugins.input.java.data.attributes.IJavaAttribute; import jadx.plugins.input.java.data.attributes.stack.StackFrame; public class StackMapTableAttr implements IJavaAttribute { public static final StackMapTableAttr EMPTY = new StackMapTableAttr(Collections.emptyMap()); private final Map map; public StackMapTableAttr(Map map) { this.map = map; } public @Nullable StackFrame getFor(int offset) { return map.get(offset); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/types/data/RawBootstrapMethod.java ================================================ package jadx.plugins.input.java.data.attributes.types.data; public class RawBootstrapMethod { private final int methodHandleIdx; private final int[] args; public RawBootstrapMethod(int methodHandleIdx, int[] args) { this.methodHandleIdx = methodHandleIdx; this.args = args; } public int getMethodHandleIdx() { return methodHandleIdx; } public int[] getArgs() { return args; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/ArrayType.java ================================================ package jadx.plugins.input.java.data.code; import jadx.plugins.input.java.utils.JavaClassParseException; public class ArrayType { public static String byValue(int val) { switch (val) { case 4: return "Z"; case 5: return "C"; case 6: return "F"; case 7: return "D"; case 8: return "B"; case 9: return "S"; case 10: return "I"; case 11: return "J"; default: throw new JavaClassParseException("Unknown array type value: " + val); } } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/CodeDecodeState.java ================================================ package jadx.plugins.input.java.data.code; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.insns.Opcode; import jadx.core.utils.Utils; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.JavaClassData; import jadx.plugins.input.java.data.attributes.stack.StackFrame; import jadx.plugins.input.java.data.attributes.stack.StackValueType; import jadx.plugins.input.java.data.attributes.types.StackMapTableAttr; @SuppressWarnings("UnusedReturnValue") public class CodeDecodeState { private final JavaClassData clsData; private final DataReader reader; private final int maxStack; private final Set excHandlers; private final StackMapTableAttr stackMapTable; private final Map jumpStack = new HashMap<>(); // save current stack for jump target private JavaInsnData insn; private StackState stack; private boolean excHandler; public CodeDecodeState(JavaClassData clsData, DataReader reader, int maxStack, Set excHandlers, @Nullable StackMapTableAttr stackMapTable) { this.clsData = clsData; this.reader = reader; this.maxStack = maxStack; this.excHandlers = excHandlers; this.stack = new StackState(maxStack); this.stackMapTable = Utils.getOrElse(stackMapTable, StackMapTableAttr.EMPTY); } public void onInsn(int offset) { StackState newStack = loadStack(offset); if (newStack != null) { this.stack = newStack; } if (excHandlers.contains(offset)) { clear(); stack.push(StackValueType.NARROW); // push exception excHandler = true; } else { excHandler = false; } } private @Nullable StackState loadStack(int offset) { StackState stackState = jumpStack.get(offset); if (stackState != null) { return stackState.copy(); } StackFrame frame = stackMapTable.getFor(offset); if (frame != null) { return new StackState(maxStack).fillFromFrame(frame); } return null; } public void registerJump(int jumpOffset) { Integer key = jumpOffset; if (!jumpStack.containsKey(key)) { jumpStack.put(key, stack.copy()); } } public void decoded() { if (excHandler && insn.getOpcode() == Opcode.MOVE) { // replace first 'move' in exception handler with 'move-exception' insn.setOpcode(Opcode.MOVE_EXCEPTION); insn.setRegsCount(1); } } public JavaInsnData insn() { return insn; } public void setInsn(JavaInsnData insn) { this.insn = insn; } public DataReader reader() { return reader; } public JavaClassData clsData() { return clsData; } public CodeDecodeState local(int arg, int local) { insn.setArgReg(arg, localToReg(local)); return this; } public CodeDecodeState pop(int arg) { insn.setArgReg(arg, stack.pop()); return this; } public CodeDecodeState peek(int arg) { insn.setArgReg(arg, stack.peek()); return this; } public StackValueType peekType(int at) { return stack.peekTypeAt(at); } public CodeDecodeState peekFrom(int pos, int arg) { insn.setArgReg(arg, stack.peekAt(pos)); return this; } public CodeDecodeState push(int arg) { insn.setArgReg(arg, stack.push(StackValueType.NARROW)); return this; } public CodeDecodeState push(int arg, StackValueType type) { insn.setArgReg(arg, stack.push(type)); return this; } public CodeDecodeState pushWide(int arg) { insn.setArgReg(arg, stack.push(StackValueType.WIDE)); return this; } public int insert(int pos, StackValueType type) { return stack.insert(pos, type); } public void discard() { stack.pop(); } public void discardWord() { StackValueType type = stack.peekTypeAt(0); stack.pop(); if (type == StackValueType.NARROW) { stack.pop(); } } public CodeDecodeState clear() { stack.clear(); return this; } public int push(String type) { return stack.push(getSVType(type)); } /** * Must be after all pop and push */ public void jump(int offset) { int jumpOffset = insn.getOffset() + offset; insn.setTarget(jumpOffset); registerJump(jumpOffset); } public CodeDecodeState idx(int idx) { insn.setIndex(idx); return this; } public CodeDecodeState lit(long lit) { insn.setLiteral(lit); return this; } private int localToReg(int local) { return maxStack + local; } public StackValueType fieldType() { String type = insn.constPoolReader().getFieldType(insn().getIndex()); return getSVType(type); } public StackValueType getSVType(String type) { if (type.equals("J") || type.equals("D")) { return StackValueType.WIDE; } return StackValueType.NARROW; } public int u1() { return reader.readU1(); } public int u2() { return reader.readU2(); } public int s1() { return reader.readS1(); } public int s2() { return reader.readS2(); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaCodeReader.java ================================================ package jadx.plugins.input.java.data.code; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IDebugInfo; import jadx.api.plugins.input.data.ILocalVar; import jadx.api.plugins.input.data.ITry; import jadx.api.plugins.input.data.impl.CatchData; import jadx.api.plugins.input.data.impl.DebugInfo; import jadx.api.plugins.input.insns.InsnData; import jadx.plugins.input.java.data.ConstPoolReader; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.JavaClassData; import jadx.plugins.input.java.data.attributes.JavaAttrStorage; import jadx.plugins.input.java.data.attributes.JavaAttrType; import jadx.plugins.input.java.data.attributes.debuginfo.JavaLocalVar; import jadx.plugins.input.java.data.attributes.debuginfo.LineNumberTableAttr; import jadx.plugins.input.java.data.attributes.debuginfo.LocalVarTypesAttr; import jadx.plugins.input.java.data.attributes.debuginfo.LocalVarsAttr; import jadx.plugins.input.java.data.attributes.types.StackMapTableAttr; import jadx.plugins.input.java.data.code.trycatch.JavaSingleCatch; import jadx.plugins.input.java.data.code.trycatch.JavaTryData; import jadx.plugins.input.java.utils.JavaClassParseException; public class JavaCodeReader implements ICodeReader { private final JavaClassData clsData; private final DataReader reader; private final int codeOffset; public JavaCodeReader(JavaClassData clsData, int offset) { this.clsData = clsData; this.reader = clsData.getData(); this.codeOffset = offset; } @Override public ICodeReader copy() { return this; } @Override public void visitInstructions(Consumer insnConsumer) { Set excHandlers = getExcHandlers(); jumpToCodeAttributes(); StackMapTableAttr stackMapTable = clsData.getAttributesReader().loadOne(reader, JavaAttrType.STACK_MAP_TABLE); int maxStack = readMaxStack(); reader.skip(2); int codeSize = reader.readU4(); CodeDecodeState state = new CodeDecodeState(clsData, reader, maxStack, excHandlers, stackMapTable); JavaInsnData insn = new JavaInsnData(state); state.setInsn(insn); int offset = 0; while (offset < codeSize) { insn.setDecoded(false); insn.setOffset(offset); insn.setInsnStart(reader.getOffset()); int opcode = reader.readU1(); JavaInsnInfo insnInfo = JavaInsnsRegister.get(opcode); if (insnInfo == null) { throw new JavaClassParseException("Unknown opcode: 0x" + Integer.toHexString(opcode)); } insn.setOpcodeUnit(opcode); insn.setInsnInfo(insnInfo); insn.setRegsCount(insnInfo.getRegsCount()); insn.setOpcode(insnInfo.getApiOpcode()); insn.setPayloadSize(insnInfo.getPayloadSize()); insn.setPayload(null); state.onInsn(offset); insnConsumer.accept(insn); int payloadSize = insn.getPayloadSize(); if (!insn.isDecoded()) { if (payloadSize == -1) { insn.skip(); payloadSize = insn.getPayloadSize(); } else { reader.skip(payloadSize); } } offset += 1 + payloadSize; } } @Override public int getRegistersCount() { int maxStack = readMaxStack(); int maxLocals = reader.readU2(); return maxStack + maxLocals; } @Override public int getArgsStartReg() { return readMaxStack(); } private int readMaxStack() { reader.absPos(codeOffset); int maxStack = reader.readU2(); return maxStack + 1; // add one temporary register (for `swap` opcode) } @Override public int getUnitsCount() { return reader.absPos(codeOffset + 4).readU4(); } private static final Set> DEBUG_INFO_ATTRIBUTES = Set.of( JavaAttrType.LINE_NUMBER_TABLE, JavaAttrType.LOCAL_VAR_TABLE, JavaAttrType.LOCAL_VAR_TYPE_TABLE); @Override @Nullable public IDebugInfo getDebugInfo() { int maxStack = readMaxStack(); jumpToCodeAttributes(); JavaAttrStorage attrs = clsData.getAttributesReader().loadMulti(reader, DEBUG_INFO_ATTRIBUTES); LineNumberTableAttr linesAttr = attrs.get(JavaAttrType.LINE_NUMBER_TABLE); LocalVarsAttr varsAttr = attrs.get(JavaAttrType.LOCAL_VAR_TABLE); if (linesAttr == null && varsAttr == null) { return null; } Map linesMap = linesAttr != null ? linesAttr.getLineMap() : Collections.emptyMap(); List vars; if (varsAttr == null) { vars = Collections.emptyList(); } else { List javaVars = varsAttr.getVars(); LocalVarTypesAttr typedVars = attrs.get(JavaAttrType.LOCAL_VAR_TYPE_TABLE); if (typedVars != null && !typedVars.getVars().isEmpty()) { // merge signature from typedVars into javaVars Map varsMap = new HashMap<>(javaVars.size()); javaVars.forEach(v -> varsMap.put(v, v)); for (JavaLocalVar typedVar : typedVars.getVars()) { JavaLocalVar jv = varsMap.get(typedVar); if (jv != null) { jv.setSignature(typedVar.getSignature()); } } } javaVars.forEach(v -> v.shiftRegNum(maxStack)); vars = Collections.unmodifiableList(javaVars); } return new DebugInfo(linesMap, vars); } @Override public int getCodeOffset() { return codeOffset; } @Override public List getTries() { jumpToTries(); int excTableLen = reader.readU2(); if (excTableLen == 0) { return Collections.emptyList(); } ConstPoolReader constPool = clsData.getConstPoolReader(); Map> tries = new HashMap<>(excTableLen); for (int i = 0; i < excTableLen; i++) { int start = reader.readU2(); int end = reader.readU2(); int handler = reader.readU2(); int type = reader.readU2(); JavaTryData tryData = new JavaTryData(start, end); List catches = tries.computeIfAbsent(tryData, k -> new ArrayList<>()); if (type == 0) { catches.add(new JavaSingleCatch(handler, null)); } else { catches.add(new JavaSingleCatch(handler, constPool.getClass(type))); } } return tries.entrySet().stream() .map(e -> { JavaTryData tryData = e.getKey(); tryData.setCatch(convertSingleCatches(e.getValue())); return tryData; }) .collect(Collectors.toList()); } private static CatchData convertSingleCatches(List list) { int allHandler = -1; for (JavaSingleCatch singleCatch : list) { if (singleCatch.getType() == null) { allHandler = singleCatch.getHandler(); list.remove(singleCatch); break; } } int len = list.size(); int[] handlers = new int[len]; String[] types = new String[len]; for (int i = 0; i < len; i++) { JavaSingleCatch singleCatch = list.get(i); handlers[i] = singleCatch.getHandler(); types[i] = singleCatch.getType(); } return new CatchData(handlers, types, allHandler); } private Set getExcHandlers() { jumpToTries(); int excTableLen = reader.readU2(); if (excTableLen == 0) { return Collections.emptySet(); } Set set = new HashSet<>(excTableLen); for (int i = 0; i < excTableLen; i++) { reader.skip(4); int handler = reader.readU2(); reader.skip(2); set.add(handler); } return set; } private void jumpToTries() { reader.absPos(codeOffset + 4); reader.skip(reader.readU4()); // code length } private void jumpToCodeAttributes() { jumpToTries(); reader.skip(reader.readU2() * 8); // exceptions table } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaInsnData.java ================================================ package jadx.plugins.input.java.data.code; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.data.ICallSite; import jadx.api.plugins.input.data.IFieldRef; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.insns.InsnData; import jadx.api.plugins.input.insns.InsnIndexType; import jadx.api.plugins.input.insns.Opcode; import jadx.api.plugins.input.insns.custom.ICustomPayload; import jadx.plugins.input.java.data.ConstPoolReader; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.code.decoders.IJavaInsnDecoder; public class JavaInsnData implements InsnData { private final CodeDecodeState state; private JavaInsnInfo insnInfo; private Opcode opcode; private boolean decoded; private int opcodeUnit; private int payloadSize; private int insnStart; private int offset; private int regsCount; private int[] argsReg = new int[16]; private int resultReg; private long literal; private int target; private int index; @Nullable private ICustomPayload payload; public JavaInsnData(CodeDecodeState state) { this.state = state; } @Override public void decode() { IJavaInsnDecoder decoder = insnInfo.getDecoder(); if (decoder != null) { decoder.decode(state); state.decoded(); } decoded = true; } public void skip() { IJavaInsnDecoder decoder = insnInfo.getDecoder(); if (decoder != null) { decoder.skip(state); } } @Override public int getOffset() { return offset; } @Override public int getFileOffset() { return insnStart; } @Override public Opcode getOpcode() { return opcode; } public void setOpcode(Opcode opcode) { this.opcode = opcode; } @Override public String getOpcodeMnemonic() { return insnInfo.getName(); } @Override public byte[] getByteCode() { DataReader reader = state.reader(); int startOffset = reader.getOffset(); try { reader.absPos(insnStart); return reader.readBytes(1 + payloadSize); } finally { reader.absPos(startOffset); } } @Override public InsnIndexType getIndexType() { return insnInfo.getIndexType(); } @Override public int getRawOpcodeUnit() { return opcodeUnit; } @Override public int getRegsCount() { return regsCount; } @Override public int getReg(int argNum) { return argsReg[argNum]; } @Override public int getResultReg() { return resultReg; } public void setResultReg(int resultReg) { this.resultReg = resultReg; } @Override public long getLiteral() { return literal; } @Override public int getTarget() { return target; } @Override public int getIndex() { return index; } public int getPayloadSize() { return payloadSize; } @Override public String getIndexAsString() { return constPoolReader().getUtf8(index); } @Override public String getIndexAsType() { if (insnInfo.getOpcode() == 0xbc) { // newarray return ArrayType.byValue(index); } return constPoolReader().getClass(index); } @Override public IFieldRef getIndexAsField() { return constPoolReader().getFieldRef(index); } @Override public IMethodRef getIndexAsMethod() { return constPoolReader().getMethodRef(index); } @Override public ICallSite getIndexAsCallSite() { return constPoolReader().getCallSite(index); } @Override public IMethodProto getIndexAsProto(int protoIndex) { return null; } @Override public IMethodHandle getIndexAsMethodHandle() { return null; } @Override public @Nullable ICustomPayload getPayload() { return payload; } public void setInsnInfo(JavaInsnInfo insnInfo) { this.insnInfo = insnInfo; } public boolean isDecoded() { return decoded; } public void setDecoded(boolean decoded) { this.decoded = decoded; } public void setOpcodeUnit(int opcodeUnit) { this.opcodeUnit = opcodeUnit; } public void setPayloadSize(int payloadSize) { this.payloadSize = payloadSize; } public void setInsnStart(int insnStart) { this.insnStart = insnStart; } public void setOffset(int offset) { this.offset = offset; } public void setArgReg(int arg, int reg) { this.argsReg[arg] = reg; } public void setRegsCount(int regsCount) { this.regsCount = regsCount; if (argsReg.length < regsCount) { argsReg = new int[regsCount]; } } public int[] getRegsArray() { return argsReg; } public void setLiteral(long literal) { this.literal = literal; } public void setTarget(int target) { this.target = target; } public void setIndex(int index) { this.index = index; } public void setPayload(ICustomPayload payload) { this.payload = payload; } public ConstPoolReader constPoolReader() { return state.clsData().getConstPoolReader(); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(String.format("0x%04X", offset)); sb.append(": ").append(getOpcode()); if (insnInfo == null) { sb.append(String.format("(0x%04X)", opcodeUnit)); } else { int regsCount = getRegsCount(); if (isDecoded()) { sb.append(' '); for (int i = 0; i < regsCount; i++) { if (i != 0) { sb.append(", "); } sb.append("r").append(argsReg[i]); } } } return sb.toString(); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaInsnInfo.java ================================================ package jadx.plugins.input.java.data.code; import jadx.api.plugins.input.insns.InsnIndexType; import jadx.api.plugins.input.insns.Opcode; import jadx.plugins.input.java.data.code.decoders.IJavaInsnDecoder; public class JavaInsnInfo { private final int opcode; private final String name; private final int payloadSize; private final int regsCount; private final Opcode apiOpcode; private final InsnIndexType indexType; private final IJavaInsnDecoder decoder; public JavaInsnInfo(int opcode, String name, int payloadSize, int regsCount, Opcode apiOpcode, InsnIndexType indexType, IJavaInsnDecoder decoder) { this.opcode = opcode; this.name = name; this.payloadSize = payloadSize; this.regsCount = regsCount; this.apiOpcode = apiOpcode; this.indexType = indexType; this.decoder = decoder; } public int getOpcode() { return opcode; } public String getName() { return name; } public int getPayloadSize() { return payloadSize; } public int getRegsCount() { return regsCount; } public Opcode getApiOpcode() { return apiOpcode; } public InsnIndexType getIndexType() { return indexType; } public IJavaInsnDecoder getDecoder() { return decoder; } @Override public String toString() { return "0x" + Integer.toHexString(opcode) + ": " + name; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaInsnsRegister.java ================================================ package jadx.plugins.input.java.data.code; import org.jetbrains.annotations.Nullable; import jadx.api.plugins.input.insns.InsnIndexType; import jadx.api.plugins.input.insns.Opcode; import jadx.plugins.input.java.data.attributes.stack.StackValueType; import jadx.plugins.input.java.data.code.decoders.IJavaInsnDecoder; import jadx.plugins.input.java.data.code.decoders.InvokeDecoder; import jadx.plugins.input.java.data.code.decoders.LoadConstDecoder; import jadx.plugins.input.java.data.code.decoders.LookupSwitchDecoder; import jadx.plugins.input.java.data.code.decoders.TableSwitchDecoder; import jadx.plugins.input.java.data.code.decoders.WideDecoder; import static jadx.plugins.input.java.data.attributes.stack.StackValueType.NARROW; import static jadx.plugins.input.java.data.attributes.stack.StackValueType.WIDE; @SuppressWarnings("SpellCheckingInspection") public class JavaInsnsRegister { private static final JavaInsnInfo[] INSN_INFO; public static final long FLOAT_ZERO = Float.floatToIntBits(0.0f); public static final long FLOAT_ONE = Float.floatToIntBits(1.0f); public static final long FLOAT_TWO = Float.floatToIntBits(2.0f); public static final long DOUBLE_ZERO = Double.doubleToLongBits(0.0d); public static final long DOUBLE_ONE = Double.doubleToLongBits(1.0d); static { JavaInsnInfo[] arr = new JavaInsnInfo[0xCA]; INSN_INFO = arr; register(arr, 0x00, "nop", 0, 0, Opcode.NOP, null); constInsn(arr, 0x01, "aconst_null", Opcode.CONST, 0); constInsn(arr, 0x02, "iconst_m1", Opcode.CONST, -1); constInsn(arr, 0x03, "iconst_0", Opcode.CONST, 0); constInsn(arr, 0x04, "iconst_1", Opcode.CONST, 1); constInsn(arr, 0x05, "iconst_2", Opcode.CONST, 2); constInsn(arr, 0x06, "iconst_3", Opcode.CONST, 3); constInsn(arr, 0x07, "iconst_4", Opcode.CONST, 4); constInsn(arr, 0x08, "iconst_5", Opcode.CONST, 5); constInsn(arr, 0x09, "lconst_0", Opcode.CONST_WIDE, 0L); constInsn(arr, 0x0a, "lconst_1", Opcode.CONST_WIDE, 1L); constInsn(arr, 0x0b, "fconst_0", Opcode.CONST, FLOAT_ZERO); constInsn(arr, 0x0c, "fconst_1", Opcode.CONST, FLOAT_ONE); constInsn(arr, 0x0d, "fconst_2", Opcode.CONST, FLOAT_TWO); constInsn(arr, 0x0e, "dconst_0", Opcode.CONST_WIDE, DOUBLE_ZERO); constInsn(arr, 0x0f, "dconst_1", Opcode.CONST_WIDE, DOUBLE_ONE); register(arr, 0x10, "bipush", 1, 2, Opcode.CONST, s -> s.lit(s.s1()).push(0)); register(arr, 0x11, "sipush", 2, 2, Opcode.CONST, s -> s.lit(s.s2()).push(0)); loadConst(arr, 0x12, "ldc", false); loadConst(arr, 0x13, "ldc_w", true); loadConst(arr, 0x14, "ldc2_w", true); register(arr, 0x15, "iload", 1, 2, Opcode.MOVE, s -> s.local(1, s.u1()).push(0)); register(arr, 0x16, "lload", 1, 2, Opcode.MOVE_WIDE, s -> s.local(1, s.u1()).pushWide(0)); register(arr, 0x17, "fload", 1, 2, Opcode.MOVE, s -> s.local(1, s.u1()).push(0)); register(arr, 0x18, "dload", 1, 2, Opcode.MOVE_WIDE, s -> s.local(1, s.u1()).pushWide(0)); register(arr, 0x19, "aload", 1, 2, Opcode.MOVE, s -> s.local(1, s.u1()).push(0)); register(arr, 0x1a, "iload_0", 0, 2, Opcode.MOVE, s -> s.local(1, 0).push(0)); register(arr, 0x1b, "iload_1", 0, 2, Opcode.MOVE, s -> s.local(1, 1).push(0)); register(arr, 0x1c, "iload_2", 0, 2, Opcode.MOVE, s -> s.local(1, 2).push(0)); register(arr, 0x1d, "iload_3", 0, 2, Opcode.MOVE, s -> s.local(1, 3).push(0)); register(arr, 0x1e, "lload_0", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 0).pushWide(0)); register(arr, 0x1f, "lload_1", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 1).pushWide(0)); register(arr, 0x20, "lload_2", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 2).pushWide(0)); register(arr, 0x21, "lload_3", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 3).pushWide(0)); register(arr, 0x22, "fload_0", 0, 2, Opcode.MOVE, s -> s.local(1, 0).push(0)); register(arr, 0x23, "fload_1", 0, 2, Opcode.MOVE, s -> s.local(1, 1).push(0)); register(arr, 0x24, "fload_2", 0, 2, Opcode.MOVE, s -> s.local(1, 2).push(0)); register(arr, 0x25, "fload_3", 0, 2, Opcode.MOVE, s -> s.local(1, 3).push(0)); register(arr, 0x26, "dload_0", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 0).pushWide(0)); register(arr, 0x27, "dload_1", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 1).pushWide(0)); register(arr, 0x28, "dload_2", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 2).pushWide(0)); register(arr, 0x29, "dload_3", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 3).pushWide(0)); register(arr, 0x2a, "aload_0", 0, 2, Opcode.MOVE, s -> s.local(1, 0).push(0)); register(arr, 0x2b, "aload_1", 0, 2, Opcode.MOVE, s -> s.local(1, 1).push(0)); register(arr, 0x2c, "aload_2", 0, 2, Opcode.MOVE, s -> s.local(1, 2).push(0)); register(arr, 0x2d, "aload_3", 0, 2, Opcode.MOVE, s -> s.local(1, 3).push(0)); register(arr, 0x2e, "iaload", 0, 3, Opcode.AGET, aget()); register(arr, 0x2f, "laload", 0, 3, Opcode.AGET_WIDE, agetWide()); register(arr, 0x30, "faload", 0, 3, Opcode.AGET, aget()); register(arr, 0x31, "daload", 0, 3, Opcode.AGET_WIDE, agetWide()); register(arr, 0x32, "aaload", 0, 3, Opcode.AGET_OBJECT, aget()); register(arr, 0x33, "baload", 0, 3, Opcode.AGET_BYTE_BOOLEAN, aget()); register(arr, 0x34, "caload", 0, 3, Opcode.AGET_CHAR, aget()); register(arr, 0x35, "saload", 0, 3, Opcode.AGET_SHORT, aget()); register(arr, 0x36, "istore", 1, 2, Opcode.MOVE, s -> s.pop(1).local(0, s.u1())); register(arr, 0x37, "lstore", 1, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, s.u1())); register(arr, 0x38, "fstore", 1, 2, Opcode.MOVE, s -> s.pop(1).local(0, s.u1())); register(arr, 0x39, "dstore", 1, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, s.u1())); register(arr, 0x3a, "astore", 1, 2, Opcode.MOVE, s -> s.pop(1).local(0, s.u1())); register(arr, 0x3b, "istore_0", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 0)); register(arr, 0x3c, "istore_1", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 1)); register(arr, 0x3d, "istore_2", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 2)); register(arr, 0x3e, "istore_3", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 3)); register(arr, 0x3f, "lstore_0", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 0)); register(arr, 0x40, "lstore_1", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 1)); register(arr, 0x41, "lstore_2", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 2)); register(arr, 0x42, "lstore_3", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 3)); register(arr, 0x43, "fstore_0", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 0)); register(arr, 0x44, "fstore_1", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 1)); register(arr, 0x45, "fstore_2", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 2)); register(arr, 0x46, "fstore_3", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 3)); register(arr, 0x47, "dstore_0", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 0)); register(arr, 0x48, "dstore_1", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 1)); register(arr, 0x49, "dstore_2", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 2)); register(arr, 0x4a, "dstore_3", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 3)); register(arr, 0x4b, "astore_0", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 0)); register(arr, 0x4c, "astore_1", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 1)); register(arr, 0x4d, "astore_2", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 2)); register(arr, 0x4e, "astore_3", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 3)); register(arr, 0x4f, "iastore", 0, 3, Opcode.APUT, aput()); register(arr, 0x50, "lastore", 0, 3, Opcode.APUT_WIDE, aput()); register(arr, 0x51, "fastore", 0, 3, Opcode.APUT, aput()); register(arr, 0x52, "dastore", 0, 3, Opcode.APUT_WIDE, aput()); register(arr, 0x53, "aastore", 0, 3, Opcode.APUT_OBJECT, aput()); register(arr, 0x54, "bastore", 0, 3, Opcode.APUT_BYTE_BOOLEAN, aput()); register(arr, 0x55, "castore", 0, 3, Opcode.APUT_CHAR, aput()); register(arr, 0x56, "sastore", 0, 3, Opcode.APUT_SHORT, aput()); register(arr, 0x57, "pop", 0, 0, Opcode.NOP, CodeDecodeState::discard); register(arr, 0x58, "pop2", 0, 0, Opcode.NOP, CodeDecodeState::discardWord); register(arr, 0x59, "dup", 0, 2, Opcode.MOVE, s -> s.peek(1).push(0, s.peekType(1))); register(arr, 0x5a, "dup_x1", 0, 6, Opcode.MOVE_MULTI, s -> s.push(0, s.peekType(1)).peekFrom(1, 1) .peekFrom(1, 2).peekFrom(2, 3) .peekFrom(2, 4).peekFrom(0, 5)); register(arr, 0x5b, "dup_x2", 0, 8, Opcode.MOVE_MULTI, s -> s.push(0, s.peekType(1)).peekFrom(1, 1) .peekFrom(1, 2).peekFrom(2, 3) .peekFrom(2, 4).peekFrom(3, 5) .peekFrom(3, 6).peekFrom(0, 7)); register(arr, 0x5c, "dup2", 0, 4, Opcode.MOVE_MULTI, s -> { if (s.peekType(0) == NARROW) { s.peekFrom(0, 3).peekFrom(1, 1).push(0, NARROW).push(2, NARROW); } else { s.peek(1).push(0, s.peekType(1)); } }); register(arr, 0x5d, "dup2_x1", 0, 10, Opcode.MOVE_MULTI, JavaInsnsRegister::dup2x1); register(arr, 0x5e, "dup2_x2", 0, 12, Opcode.MOVE_MULTI, JavaInsnsRegister::dup2x2); register(arr, 0x5f, "swap", 0, 6, Opcode.MOVE_MULTI, s -> s.peekFrom(-1, 0).peekFrom(1, 1) .peekFrom(1, 2).peekFrom(0, 3) .peekFrom(0, 4).peekFrom(-1, 5)); register(arr, 0x60, "iadd", 0, 3, Opcode.ADD_INT, twoRegsWithResult(NARROW)); register(arr, 0x61, "ladd", 0, 3, Opcode.ADD_LONG, twoRegsWithResult(WIDE)); register(arr, 0x62, "fadd", 0, 3, Opcode.ADD_FLOAT, twoRegsWithResult(NARROW)); register(arr, 0x63, "dadd", 0, 3, Opcode.ADD_DOUBLE, twoRegsWithResult(WIDE)); register(arr, 0x64, "isub", 0, 3, Opcode.SUB_INT, twoRegsWithResult(NARROW)); register(arr, 0x65, "lsub", 0, 3, Opcode.SUB_LONG, twoRegsWithResult(WIDE)); register(arr, 0x66, "fsub", 0, 3, Opcode.SUB_FLOAT, twoRegsWithResult(NARROW)); register(arr, 0x67, "dsub", 0, 3, Opcode.SUB_DOUBLE, twoRegsWithResult(WIDE)); register(arr, 0x68, "imul", 0, 3, Opcode.MUL_INT, twoRegsWithResult(NARROW)); register(arr, 0x69, "lmul", 0, 3, Opcode.MUL_LONG, twoRegsWithResult(WIDE)); register(arr, 0x6a, "fmul", 0, 3, Opcode.MUL_FLOAT, twoRegsWithResult(NARROW)); register(arr, 0x6b, "dmul", 0, 3, Opcode.MUL_DOUBLE, twoRegsWithResult(WIDE)); register(arr, 0x6c, "idiv", 0, 3, Opcode.DIV_INT, twoRegsWithResult(NARROW)); register(arr, 0x6d, "ldiv", 0, 3, Opcode.DIV_LONG, twoRegsWithResult(WIDE)); register(arr, 0x6e, "fdiv", 0, 3, Opcode.DIV_FLOAT, twoRegsWithResult(NARROW)); register(arr, 0x6f, "ddiv", 0, 3, Opcode.DIV_DOUBLE, twoRegsWithResult(WIDE)); register(arr, 0x70, "irem", 0, 3, Opcode.REM_INT, twoRegsWithResult(NARROW)); register(arr, 0x71, "lrem", 0, 3, Opcode.REM_LONG, twoRegsWithResult(WIDE)); register(arr, 0x72, "frem", 0, 3, Opcode.REM_FLOAT, twoRegsWithResult(NARROW)); register(arr, 0x73, "drem", 0, 3, Opcode.REM_DOUBLE, twoRegsWithResult(WIDE)); register(arr, 0x74, "ineg", 0, 2, Opcode.NEG_INT, oneRegWithResult(NARROW)); register(arr, 0x75, "lneg", 0, 2, Opcode.NEG_LONG, oneRegWithResult(WIDE)); register(arr, 0x76, "fneg", 0, 2, Opcode.NEG_FLOAT, oneRegWithResult(NARROW)); register(arr, 0x77, "dneg", 0, 2, Opcode.NEG_DOUBLE, oneRegWithResult(WIDE)); register(arr, 0x78, "ishl", 0, 3, Opcode.SHL_INT, twoRegsWithResult(NARROW)); register(arr, 0x79, "lshl", 0, 3, Opcode.SHL_LONG, twoRegsWithResult(WIDE)); register(arr, 0x7a, "ishr", 0, 3, Opcode.SHR_INT, twoRegsWithResult(NARROW)); register(arr, 0x7b, "lshr", 0, 3, Opcode.SHR_LONG, twoRegsWithResult(WIDE)); register(arr, 0x7c, "iushr", 0, 3, Opcode.USHR_INT, twoRegsWithResult(NARROW)); register(arr, 0x7d, "lushr", 0, 3, Opcode.USHR_LONG, twoRegsWithResult(WIDE)); register(arr, 0x7e, "iand", 0, 3, Opcode.AND_INT, twoRegsWithResult(NARROW)); register(arr, 0x7f, "land", 0, 3, Opcode.AND_LONG, twoRegsWithResult(WIDE)); register(arr, 0x80, "ior", 0, 3, Opcode.OR_INT, twoRegsWithResult(NARROW)); register(arr, 0x81, "lor", 0, 3, Opcode.OR_LONG, twoRegsWithResult(WIDE)); register(arr, 0x82, "ixor", 0, 3, Opcode.XOR_INT, twoRegsWithResult(NARROW)); register(arr, 0x83, "lxor", 0, 3, Opcode.XOR_LONG, twoRegsWithResult(WIDE)); register(arr, 0x84, "iinc", 2, 2, Opcode.ADD_INT_LIT, s -> { int varNum = s.u1(); s.local(0, varNum).local(1, varNum).lit(s.reader().readS1()); }); register(arr, 0x85, "i2l", 0, 2, Opcode.INT_TO_LONG, oneRegWithResult(WIDE)); register(arr, 0x86, "i2f", 0, 2, Opcode.INT_TO_FLOAT, oneRegWithResult(NARROW)); register(arr, 0x87, "i2d", 0, 2, Opcode.INT_TO_DOUBLE, oneRegWithResult(WIDE)); register(arr, 0x88, "l2i", 0, 2, Opcode.LONG_TO_INT, oneRegWithResult(NARROW)); register(arr, 0x89, "l2f", 0, 2, Opcode.LONG_TO_FLOAT, oneRegWithResult(NARROW)); register(arr, 0x8a, "l2d", 0, 2, Opcode.LONG_TO_DOUBLE, oneRegWithResult(WIDE)); register(arr, 0x8b, "f2i", 0, 2, Opcode.FLOAT_TO_INT, oneRegWithResult(NARROW)); register(arr, 0x8c, "f2l", 0, 2, Opcode.FLOAT_TO_LONG, oneRegWithResult(WIDE)); register(arr, 0x8d, "f2d", 0, 2, Opcode.FLOAT_TO_DOUBLE, oneRegWithResult(WIDE)); register(arr, 0x8e, "d2i", 0, 2, Opcode.DOUBLE_TO_INT, oneRegWithResult(NARROW)); register(arr, 0x8f, "d2l", 0, 2, Opcode.DOUBLE_TO_LONG, oneRegWithResult(WIDE)); register(arr, 0x90, "d2f", 0, 2, Opcode.DOUBLE_TO_FLOAT, oneRegWithResult(NARROW)); register(arr, 0x91, "i2b", 0, 2, Opcode.INT_TO_BYTE, oneRegWithResult(NARROW)); register(arr, 0x92, "i2c", 0, 2, Opcode.INT_TO_CHAR, oneRegWithResult(NARROW)); register(arr, 0x93, "i2s", 0, 2, Opcode.INT_TO_SHORT, oneRegWithResult(NARROW)); register(arr, 0x94, "lcmp", 0, 3, Opcode.CMP_LONG, twoRegsWithResult(NARROW)); register(arr, 0x95, "fcmpl", 0, 3, Opcode.CMPL_FLOAT, twoRegsWithResult(NARROW)); register(arr, 0x96, "fcmpg", 0, 3, Opcode.CMPG_FLOAT, twoRegsWithResult(NARROW)); register(arr, 0x97, "dcmpl", 0, 3, Opcode.CMPL_DOUBLE, twoRegsWithResult(NARROW)); register(arr, 0x98, "dcmpg", 0, 3, Opcode.CMPG_DOUBLE, twoRegsWithResult(NARROW)); register(arr, 0x99, "ifeq", 2, 1, Opcode.IF_EQZ, zeroCmp()); register(arr, 0x9a, "ifne", 2, 1, Opcode.IF_NEZ, zeroCmp()); register(arr, 0x9b, "iflt", 2, 1, Opcode.IF_LTZ, zeroCmp()); register(arr, 0x9c, "ifge", 2, 1, Opcode.IF_GEZ, zeroCmp()); register(arr, 0x9d, "ifgt", 2, 1, Opcode.IF_GTZ, zeroCmp()); register(arr, 0x9e, "ifle", 2, 1, Opcode.IF_LEZ, zeroCmp()); register(arr, 0x9f, "if_icmpeq", 2, 2, Opcode.IF_EQ, cmp()); register(arr, 0xa0, "if_icmpne", 2, 2, Opcode.IF_NE, cmp()); register(arr, 0xa1, "if_icmplt", 2, 2, Opcode.IF_LT, cmp()); register(arr, 0xa2, "if_icmpge", 2, 2, Opcode.IF_GE, cmp()); register(arr, 0xa3, "if_icmpgt", 2, 2, Opcode.IF_GT, cmp()); register(arr, 0xa4, "if_icmple", 2, 2, Opcode.IF_LE, cmp()); register(arr, 0xa5, "if_acmpeq", 2, 2, Opcode.IF_EQ, cmp()); register(arr, 0xa6, "if_acmpne", 2, 2, Opcode.IF_NE, cmp()); register(arr, 0xa7, "goto", 2, 0, Opcode.GOTO, s -> s.jump(s.s2())); register(arr, 0xa8, "jsr", 2, 1, Opcode.JAVA_JSR, s -> s.push(0).jump(s.s2())); register(arr, 0xa9, "ret", 1, 1, Opcode.JAVA_RET, s -> s.local(0, s.u1())); register(arr, 0xaa, "tableswitch", -1, 1, Opcode.PACKED_SWITCH, new TableSwitchDecoder()); register(arr, 0xab, "lookupswitch", -1, 1, Opcode.SPARSE_SWITCH, new LookupSwitchDecoder()); register(arr, 0xac, "ireturn", 0, 1, Opcode.RETURN, s -> s.pop(0)); register(arr, 0xad, "lreturn", 0, 1, Opcode.RETURN, s -> s.pop(0)); register(arr, 0xae, "freturn", 0, 1, Opcode.RETURN, s -> s.pop(0)); register(arr, 0xaf, "dreturn", 0, 1, Opcode.RETURN, s -> s.pop(0)); register(arr, 0xb0, "areturn", 0, 1, Opcode.RETURN, s -> s.pop(0)); register(arr, 0xb1, "return", 0, 0, Opcode.RETURN_VOID, null); register(arr, 0xb2, "getstatic", 2, 1, Opcode.SGET, InsnIndexType.FIELD_REF, s -> s.idx(s.u2()).push(0, s.fieldType())); register(arr, 0xb3, "putstatic", 2, 1, Opcode.SPUT, InsnIndexType.FIELD_REF, s -> s.idx(s.u2()).pop(0)); register(arr, 0xb4, "getfield", 2, 2, Opcode.IGET, InsnIndexType.FIELD_REF, s -> s.idx(s.u2()).pop(1).push(0, s.fieldType())); register(arr, 0xb5, "putfield", 2, 2, Opcode.IPUT, InsnIndexType.FIELD_REF, s -> s.idx(s.u2()).pop(0).pop(1)); invoke(arr, 0xb6, "invokevirtual", 2, Opcode.INVOKE_VIRTUAL); invoke(arr, 0xb7, "invokespecial", 2, Opcode.INVOKE_SPECIAL); invoke(arr, 0xb8, "invokestatic", 2, Opcode.INVOKE_STATIC); invoke(arr, 0xb9, "invokeinterface", 4, Opcode.INVOKE_INTERFACE); invoke(arr, 0xba, "invokedynamic", 4, Opcode.INVOKE_CUSTOM); register(arr, 0xbb, "new", 2, 1, Opcode.NEW_INSTANCE, InsnIndexType.TYPE_REF, s -> s.idx(s.u2()).push(0)); register(arr, 0xbc, "newarray", 1, 2, Opcode.NEW_ARRAY, InsnIndexType.TYPE_REF, s -> s.idx(s.u1()).pop(1).push(0).lit(1)); register(arr, 0xbd, "anewarray", 2, 2, Opcode.NEW_ARRAY, InsnIndexType.TYPE_REF, s -> s.idx(s.u2()).pop(1).push(0).lit(1)); register(arr, 0xbe, "arraylength", 0, 2, Opcode.ARRAY_LENGTH, oneRegWithResult(NARROW)); register(arr, 0xbf, "athrow", 0, 1, Opcode.THROW, s -> s.pop(0).clear()); register(arr, 0xc0, "checkcast", 2, 2, Opcode.CHECK_CAST, InsnIndexType.TYPE_REF, s -> s.idx(s.u2()).pop(1).push(0)); register(arr, 0xc1, "instanceof", 2, 2, Opcode.INSTANCE_OF, InsnIndexType.TYPE_REF, s -> s.idx(s.u2()).pop(1).push(0)); register(arr, 0xc2, "monitorenter", 0, 1, Opcode.MONITOR_ENTER, s -> s.pop(0)); register(arr, 0xc3, "monitorexit", 0, 1, Opcode.MONITOR_EXIT, s -> s.pop(0)); register(arr, 0xc4, "wide", -1, -1, Opcode.NOP, new WideDecoder()); register(arr, 0xc5, "multianewarray", 3, -1, Opcode.NEW_ARRAY, InsnIndexType.TYPE_REF, newArrayMulti()); register(arr, 0xc6, "ifnull", 2, 1, Opcode.IF_EQZ, zeroCmp()); register(arr, 0xc7, "ifnonnull", 2, 1, Opcode.IF_NEZ, zeroCmp()); register(arr, 0xc8, "goto_w", 4, 0, Opcode.GOTO, s -> s.jump(s.reader().readS4())); register(arr, 0xc9, "jsr_w", 4, 1, Opcode.JAVA_JSR, s -> s.push(0).jump(s.reader().readS4())); } private static void dup2x1(CodeDecodeState s) { if (s.peekType(0) == NARROW) { s.insert(2, NARROW); s.insert(2, NARROW); s.peekFrom(0, 0).peekFrom(2, 1); s.peekFrom(1, 2).peekFrom(3, 3); s.peekFrom(2, 4).peekFrom(4, 5); s.peekFrom(3, 6).peekFrom(0, 7); s.peekFrom(4, 8).peekFrom(1, 9); } else { s.insn().setRegsCount(6); s.insert(1, WIDE); s.peekFrom(0, 0).peekFrom(1, 1); s.peekFrom(1, 2).peekFrom(2, 3); s.peekFrom(2, 4).peekFrom(0, 5); } } private static void dup2x2(CodeDecodeState s) { if (s.peekType(0) == NARROW) { s.insert(2, NARROW); s.insert(2, NARROW); s.peekFrom(0, 0).peekFrom(2, 1); s.peekFrom(1, 2).peekFrom(3, 3); s.peekFrom(2, 4).peekFrom(4, 5); s.peekFrom(3, 6).peekFrom(5, 7); s.peekFrom(4, 8).peekFrom(0, 9); s.peekFrom(5, 10).peekFrom(1, 11); } else { s.insn().setRegsCount(8); s.insert(2, WIDE); s.peekFrom(0, 0).peekFrom(1, 1); s.peekFrom(1, 2).peekFrom(2, 3); s.peekFrom(2, 4).peekFrom(3, 5); s.peekFrom(3, 6).peekFrom(0, 7); } } private static IJavaInsnDecoder newArrayMulti() { return s -> { s.idx(s.u2()); int dim = s.u1(); JavaInsnData insn = s.insn(); insn.setLiteral(dim); insn.setRegsCount(dim + 1); for (int i = dim; i > 0; i--) { s.pop(i); } s.push(0); }; } private static IJavaInsnDecoder oneRegWithResult(StackValueType type) { return s -> s.pop(1).push(0, type); } private static IJavaInsnDecoder twoRegsWithResult(StackValueType type) { return s -> s.pop(2).pop(1).push(0, type); } private static IJavaInsnDecoder aget() { return s -> s.pop(2).pop(1).push(0); } private static IJavaInsnDecoder agetWide() { return s -> s.pop(2).pop(1).pushWide(0); } private static IJavaInsnDecoder aput() { return s -> s.pop(0).pop(2).pop(1); } private static IJavaInsnDecoder zeroCmp() { return s -> s.pop(0).jump(s.s2()); } private static IJavaInsnDecoder cmp() { return s -> s.pop(1).pop(0).jump(s.s2()); } private static void invoke(JavaInsnInfo[] arr, int opcode, String name, int payloadSize, Opcode apiOpcode) { InsnIndexType indexType = apiOpcode == Opcode.INVOKE_CUSTOM ? InsnIndexType.CALL_SITE : InsnIndexType.METHOD_REF; register(arr, opcode, name, payloadSize, -1, apiOpcode, indexType, new InvokeDecoder(payloadSize, apiOpcode)); } private static void constInsn(JavaInsnInfo[] arr, int opcode, String name, Opcode apiOpcode, long literal) { register(arr, opcode, name, 0, 1, apiOpcode, InsnIndexType.NONE, state -> { state.insn().setLiteral(literal); state.push(0, apiOpcode == Opcode.CONST_WIDE ? StackValueType.WIDE : NARROW); }); } private static void loadConst(JavaInsnInfo[] arr, int opcode, String name, boolean wide) { register(arr, opcode, name, wide ? 2 : 1, 2, Opcode.CONST, InsnIndexType.NONE, new LoadConstDecoder(wide)); } private static void register(JavaInsnInfo[] arr, int opcode, String name, int payloadSize, int regsCount, Opcode apiOpcode, IJavaInsnDecoder decoder) { register(arr, opcode, name, payloadSize, regsCount, apiOpcode, InsnIndexType.NONE, decoder); } private static void register(JavaInsnInfo[] arr, int opcode, String name, int payloadSize, int regsCount, Opcode apiOpcode, InsnIndexType indexType, IJavaInsnDecoder decoder) { if (arr[opcode] != null) { throw new IllegalStateException("Duplicate opcode init: 0x" + Integer.toHexString(opcode)); } arr[opcode] = new JavaInsnInfo(opcode, name, payloadSize, regsCount, apiOpcode, indexType, decoder); } @Nullable public static JavaInsnInfo get(int opcode) { return INSN_INFO[opcode]; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/StackState.java ================================================ package jadx.plugins.input.java.data.code; import java.util.Arrays; import jadx.plugins.input.java.data.attributes.stack.StackFrame; import jadx.plugins.input.java.data.attributes.stack.StackValueType; public class StackState { private int pos = -1; private final StackValueType[] stack; public StackState(int maxStack) { this.stack = new StackValueType[maxStack]; } private StackState(int pos, StackValueType[] stack) { this.pos = pos; this.stack = stack; } public StackState copy() { return new StackState(pos, Arrays.copyOf(stack, stack.length)); } public StackState fillFromFrame(StackFrame frame) { int stackSize = frame.getStackSize(); this.pos = stackSize - 1; if (stackSize > 0) { System.arraycopy(frame.getStackValueTypes(), 0, this.stack, 0, stackSize); } return this; } public int peek() { return pos; } public int peekAt(int at) { return pos - at; } public StackValueType peekTypeAt(int at) { int p = pos - at; if (checkStackIndex(p)) { return stack[p]; } return StackValueType.NARROW; } public int insert(int at, StackValueType type) { int p = pos - at; System.arraycopy(stack, p, stack, p + 1, at); stack[p] = type; pos++; return p; } public int push(StackValueType type) { int p = ++pos; if (checkStackIndex(p)) { stack[p] = type; } return p; } private boolean checkStackIndex(int p) { return p >= 0 && p < stack.length; } public int pop() { return pos--; } public void clear() { pos = -1; } @Override public String toString() { int size = pos + 1; String arr; if (size == 0) { arr = "empty"; } else if (size > 0 && size < stack.length) { arr = Arrays.toString(Arrays.copyOf(stack, size)); } else { arr = Arrays.toString(stack) + " (max)"; } return "Stack: " + size + ": " + arr; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/decoders/IJavaInsnDecoder.java ================================================ package jadx.plugins.input.java.data.code.decoders; import jadx.plugins.input.java.data.code.CodeDecodeState; public interface IJavaInsnDecoder { void decode(CodeDecodeState state); default void skip(CodeDecodeState state) { } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/decoders/InvokeDecoder.java ================================================ package jadx.plugins.input.java.data.code.decoders; import jadx.api.plugins.input.data.ICallSite; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.insns.Opcode; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.code.CodeDecodeState; import jadx.plugins.input.java.data.code.JavaInsnData; public class InvokeDecoder implements IJavaInsnDecoder { private final int payloadSize; private final Opcode apiOpcode; public InvokeDecoder(int payloadSize, Opcode apiOpcode) { this.payloadSize = payloadSize; this.apiOpcode = apiOpcode; } @Override public void decode(CodeDecodeState state) { DataReader reader = state.reader(); int mthIdx = reader.readS2(); if (payloadSize == 4) { reader.skip(2); } JavaInsnData insn = state.insn(); insn.setIndex(mthIdx); boolean instanceCall; IMethodProto mthProto; if (apiOpcode == Opcode.INVOKE_CUSTOM) { ICallSite callSite = insn.getIndexAsCallSite(); insn.setPayload(callSite); mthProto = (IMethodProto) callSite.getValues().get(2).getValue(); instanceCall = false; // 'this' arg already included in proto args } else { IMethodRef mthRef = insn.getIndexAsMethod(); mthRef.load(); insn.setPayload(mthRef); mthProto = mthRef; instanceCall = apiOpcode != Opcode.INVOKE_STATIC; } int argsCount = mthProto.getArgTypes().size(); if (instanceCall) { argsCount++; } insn.setRegsCount(argsCount * 2); // allocate twice of the size for worst case int[] regs = insn.getRegsArray(); // calculate actual count of registers // set '1' in regs to be filled with stack values later, '0' for skip int regsCount = 0; if (instanceCall) { regs[regsCount++] = 1; } for (String type : mthProto.getArgTypes()) { int size = getRegsCountForType(type); regs[regsCount++] = 1; if (size == 2) { regs[regsCount++] = 0; } } insn.setRegsCount(regsCount); for (int i = regsCount - 1; i >= 0; i--) { if (regs[i] == 1) { state.pop(i); } } String returnType = mthProto.getReturnType(); if (!returnType.equals("V")) { insn.setResultReg(state.push(returnType)); } else { insn.setResultReg(-1); } } private int getRegsCountForType(String type) { char c = type.charAt(0); if (c == 'J' || c == 'D') { return 2; } return 1; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/decoders/LoadConstDecoder.java ================================================ package jadx.plugins.input.java.data.code.decoders; import jadx.api.plugins.input.insns.Opcode; import jadx.plugins.input.java.data.ConstPoolReader; import jadx.plugins.input.java.data.ConstantType; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.attributes.stack.StackValueType; import jadx.plugins.input.java.data.code.CodeDecodeState; import jadx.plugins.input.java.data.code.JavaInsnData; import jadx.plugins.input.java.utils.JavaClassParseException; public class LoadConstDecoder implements IJavaInsnDecoder { private final boolean wide; public LoadConstDecoder(boolean wide) { this.wide = wide; } @Override public void decode(CodeDecodeState state) { DataReader reader = state.reader(); JavaInsnData insn = state.insn(); int index; if (wide) { index = reader.readU2(); } else { index = reader.readU1(); } ConstPoolReader constPoolReader = insn.constPoolReader(); ConstantType constType = constPoolReader.jumpToConst(index); switch (constType) { case INTEGER: case FLOAT: insn.setLiteral(constPoolReader.readU4()); insn.setOpcode(Opcode.CONST); state.push(0, StackValueType.NARROW); break; case LONG: case DOUBLE: insn.setLiteral(constPoolReader.readU8()); insn.setOpcode(Opcode.CONST_WIDE); state.push(0, StackValueType.WIDE); break; case STRING: insn.setIndex(constPoolReader.readU2()); insn.setOpcode(Opcode.CONST_STRING); state.push(0, StackValueType.NARROW); break; case UTF8: insn.setIndex(index); insn.setOpcode(Opcode.CONST_STRING); state.push(0, StackValueType.NARROW); break; case CLASS: insn.setIndex(index); insn.setOpcode(Opcode.CONST_CLASS); state.push(0, StackValueType.NARROW); break; default: throw new JavaClassParseException("Unsupported constant type: " + constType); } } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/decoders/LookupSwitchDecoder.java ================================================ package jadx.plugins.input.java.data.code.decoders; import jadx.api.plugins.input.insns.custom.impl.SwitchPayload; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.code.CodeDecodeState; import jadx.plugins.input.java.data.code.JavaInsnData; public class LookupSwitchDecoder implements IJavaInsnDecoder { @Override public void decode(CodeDecodeState state) { read(state, false); } @Override public void skip(CodeDecodeState state) { read(state, true); } private static void read(CodeDecodeState state, boolean skip) { DataReader reader = state.reader(); JavaInsnData insn = state.insn(); int dataOffset = reader.getOffset(); int insnOffset = insn.getOffset(); reader.skip(3 - insnOffset % 4); int defTarget = insnOffset + reader.readS4(); int pairs = reader.readS4(); if (skip) { reader.skip(pairs * 8); } else { state.pop(0); int[] keys = new int[pairs]; int[] targets = new int[pairs]; for (int i = 0; i < pairs; i++) { keys[i] = reader.readS4(); int target = insnOffset + reader.readS4(); targets[i] = target; state.registerJump(target); } insn.setTarget(defTarget); state.registerJump(defTarget); insn.setPayload(new SwitchPayload(pairs, keys, targets)); } insn.setPayloadSize(reader.getOffset() - dataOffset); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/decoders/TableSwitchDecoder.java ================================================ package jadx.plugins.input.java.data.code.decoders; import jadx.api.plugins.input.insns.custom.impl.SwitchPayload; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.code.CodeDecodeState; import jadx.plugins.input.java.data.code.JavaInsnData; public class TableSwitchDecoder implements IJavaInsnDecoder { @Override public void decode(CodeDecodeState state) { read(state, false); } @Override public void skip(CodeDecodeState state) { read(state, true); } private static void read(CodeDecodeState state, boolean skip) { DataReader reader = state.reader(); JavaInsnData insn = state.insn(); int dataOffset = reader.getOffset(); int insnOffset = insn.getOffset(); reader.skip(3 - insnOffset % 4); int defTarget = insnOffset + reader.readS4(); int low = reader.readS4(); int high = reader.readS4(); int count = high - low + 1; if (skip) { reader.skip(count * 4); } else { state.pop(0); int[] keys = new int[count]; int[] targets = new int[count]; for (int i = 0; i < count; i++) { int target = insnOffset + reader.readS4(); keys[i] = low + i; targets[i] = target; state.registerJump(target); } insn.setTarget(defTarget); state.registerJump(defTarget); insn.setPayload(new SwitchPayload(count, keys, targets)); } insn.setPayloadSize(reader.getOffset() - dataOffset); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/decoders/WideDecoder.java ================================================ package jadx.plugins.input.java.data.code.decoders; import jadx.api.plugins.input.insns.Opcode; import jadx.plugins.input.java.data.DataReader; import jadx.plugins.input.java.data.code.CodeDecodeState; import jadx.plugins.input.java.data.code.JavaInsnData; import jadx.plugins.input.java.utils.JavaClassParseException; public class WideDecoder implements IJavaInsnDecoder { private static final int IINC = 0x84; @Override public void decode(CodeDecodeState state) { DataReader reader = state.reader(); JavaInsnData insn = state.insn(); int opcode = reader.readU1(); if (opcode == IINC) { int varNum = reader.readU2(); int constValue = reader.readS2(); state.local(0, varNum).local(1, varNum).lit(constValue); insn.setPayloadSize(5); insn.setRegsCount(2); insn.setOpcode(Opcode.ADD_INT_LIT); return; } int index = reader.readU2(); switch (opcode) { case 0x15: // iload, case 0x17: // fload case 0x19: // aload state.local(1, index).push(0); break; case 0x16: // lload case 0x18: // dload state.local(1, index).pushWide(0); break; case 0x36: case 0x37: case 0x38: case 0x39: case 0x3a: // *store state.pop(1).local(0, index); break; default: throw new JavaClassParseException("Unexpected opcode in 'wide': 0x" + Integer.toHexString(opcode)); } insn.setPayloadSize(3); insn.setRegsCount(2); insn.setOpcode(Opcode.MOVE); } @Override public void skip(CodeDecodeState state) { DataReader reader = state.reader(); JavaInsnData insn = state.insn(); int opcode = reader.readU1(); if (opcode == IINC) { reader.skip(4); insn.setPayloadSize(5); } else { reader.skip(2); insn.setPayloadSize(3); } } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/trycatch/JavaSingleCatch.java ================================================ package jadx.plugins.input.java.data.code.trycatch; import org.jetbrains.annotations.Nullable; public class JavaSingleCatch { private final int handler; private final @Nullable String type; public JavaSingleCatch(int handler, @Nullable String type) { this.handler = handler; this.type = type; } public int getHandler() { return handler; } public @Nullable String getType() { return type; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/trycatch/JavaTryData.java ================================================ package jadx.plugins.input.java.data.code.trycatch; import jadx.api.plugins.input.data.ICatch; import jadx.api.plugins.input.data.ITry; import jadx.api.plugins.utils.Utils; public class JavaTryData implements ITry { private final int startOffset; private final int endOffset; private ICatch catchHandler; public JavaTryData(int startOffset, int endOffset) { this.startOffset = startOffset; this.endOffset = endOffset; } @Override public ICatch getCatch() { return catchHandler; } public void setCatch(ICatch catchHandler) { this.catchHandler = catchHandler; } @Override public int getStartOffset() { return startOffset; } @Override public int getEndOffset() { return endOffset; } @Override public int hashCode() { return startOffset + 31 * endOffset; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof JavaTryData)) { return false; } JavaTryData that = (JavaTryData) o; return startOffset == that.startOffset && endOffset == that.endOffset; } @Override public String toString() { return "Try{" + Utils.formatOffset(startOffset) + " - " + Utils.formatOffset(endOffset) + ": " + catchHandler + '}'; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/utils/DescriptorParser.java ================================================ package jadx.plugins.input.java.utils; import java.util.ArrayList; import java.util.Collections; import java.util.List; import jadx.plugins.input.java.data.JavaMethodProto; public class DescriptorParser { public static void fillMethodProto(String mthDesc, JavaMethodProto mthProto) { new DescriptorParser(mthDesc).parseMethodDescriptor(mthProto); } public static JavaMethodProto parseToMethodProto(String mthDesc) { JavaMethodProto mthProto = new JavaMethodProto(); new DescriptorParser(mthDesc).parseMethodDescriptor(mthProto); return mthProto; } private final String desc; private int pos; private DescriptorParser(String desc) { this.desc = desc; } private void parseMethodDescriptor(JavaMethodProto mthProto) { validate('('); if (check(')')) { mthProto.setArgTypes(Collections.emptyList()); } else { mthProto.setArgTypes(readArgsList()); } validate(')'); mthProto.setReturnType(readType()); } private List readArgsList() { List list = new ArrayList<>(5); do { list.add(readType()); } while (!check(')')); return list; } private String readType() { int cur = pos; if (cur >= desc.length()) { return null; } char ch = desc.charAt(cur); switch (ch) { case 'L': int end = desc.indexOf(';', cur); if (end == -1) { throw new JavaClassParseException("Unexpected object type descriptor: " + desc); } int lastChar = end + 1; String type = desc.substring(cur, lastChar); pos = lastChar; return type; case '[': pos++; return "[" + readType(); default: String primitiveType = parsePrimitiveType(ch); pos = cur + 1; return primitiveType; } } public String parsePrimitiveType(char f) { switch (f) { case 'Z': return "Z"; case 'B': return "B"; case 'C': return "C"; case 'S': return "S"; case 'I': return "I"; case 'J': return "J"; case 'F': return "F"; case 'D': return "D"; case 'V': return "V"; default: throw new JavaClassParseException("Unexpected char '" + f + "' in descriptor " + desc); } } private boolean check(char exp) { return desc.charAt(pos) == exp; } private void validate(char exp) { if (!check(exp)) { throw new JavaClassParseException("Unexpected char in descriptor: " + desc + " at pos " + pos + ", expected: " + exp); } pos++; } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/utils/DisasmUtils.java ================================================ package jadx.plugins.input.java.utils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.github.skylot.raung.disasm.RaungDisasm; public class DisasmUtils { private static final Logger LOG = LoggerFactory.getLogger(DisasmUtils.class); public static String get(byte[] bytes) { return useRaung(bytes); } private static String useRaung(byte[] bytes) { return RaungDisasm.create().executeForBytes(bytes); } /** * Use javap as a temporary disassembler for java bytecode * Don't remove! Useful for debug. */ private static String useSystemJavaP(byte[] bytes) { try { Path tmpCls = null; try { tmpCls = Files.createTempFile("jadx", ".class"); Files.write(tmpCls, bytes, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); Process process = Runtime.getRuntime().exec(new String[] { "javap", "-constants", "-v", "-p", "-c", tmpCls.toAbsolutePath().toString() }); process.waitFor(2, TimeUnit.SECONDS); return inputStreamToString(process.getInputStream()); } finally { if (tmpCls != null) { Files.delete(tmpCls); } } } catch (Exception e) { LOG.error("Java class disasm error", e); return "error"; } } public static String inputStreamToString(InputStream in) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buf = new byte[8 * 1024]; while (true) { int r = in.read(buf); if (r == -1) { break; } out.write(buf, 0, r); } return out.toString(); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/utils/JavaClassParseException.java ================================================ package jadx.plugins.input.java.utils; public class JavaClassParseException extends RuntimeException { private static final long serialVersionUID = -8452845601753645491L; public JavaClassParseException(String message, Throwable cause) { super(message, cause); } public JavaClassParseException(String message) { super(message); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/utils/ModifiedUTF8Decoder.java ================================================ package jadx.plugins.input.java.utils; import java.nio.charset.StandardCharsets; public class ModifiedUTF8Decoder { public static String decodeString(byte[] bytes) { int len = bytes.length; // quick check if all chars are 7-bit boolean asciiStr = true; for (byte b : bytes) { if ((b & 0x80) != 0) { asciiStr = false; break; } } if (asciiStr) { return new String(bytes, StandardCharsets.US_ASCII); } // parse modified UTF-8 according jvms-4.4.7 StringBuilder sb = new StringBuilder(); for (int i = 0; i < len; i++) { int x = bytes[i] & 0xff; // 4.4 ascii characters 1-127 (0 is encoded as 0xc0 0x80) if ((x & 0x80) == 0) { // 1 byte 7-Bit ascii (Table 4.4./4.5) sb.append((char) x); } else { if (i + 1 >= len) { throw new JavaClassParseException("Inconsistent byte array structure: too short"); } int y = bytes[i + 1] & 0xff; // 0 is encoded as 0xc0 0x80 (jvms-4.4.7) if (x == 0xc0 && y == 0x80) { sb.appendCodePoint(0); i++; } else if ((x & 0xE0) == 0xC0 && (y & 0xC0) == 0x80) { // 2 byte char (Table 4.8./4.9 ) sb.appendCodePoint(((x & 0x1f) << 6) + (y & 0x3f)); i++; } else if (i + 2 < len) { int z = bytes[i + 2] & 0xff; if ((x & 0xF0) == 0xE0 && (y & 0xC0) == 0x80 && (z & 0xC0) == 0x80) { // 3 byte char (Table 4.11/4.12) sb.appendCodePoint(((x & 0xf) << 12) + ((y & 0x3f) << 6) + (z & 0x3f)); i += 2; } else if (i + 5 < len && x == 0xED // u && (y & 0xF0) == 0xA0 // v && (bytes[i + 3] & 0xff) == 0xED // x && (bytes[i + 4] & 0xF0) == 0xA0 // y ) { // 6 byte encoded Table 4.12. int u = x; // 0 int v = y; // 1 int w = z; // 2 x = bytes[i + 3] & 0xff; y = bytes[i + 4] & 0xff; z = bytes[i + 5] & 0xff; if (x == 0xED && (y & 0xF0) == 0xA0) { sb.appendCodePoint(0x10000 + ((v & 0x0f) << 16) + ((w & 0x3f) << 10) + ((y & 0x0f) << 6) + (z & 0x3f)); i += 5; } else { throw new JavaClassParseException("Inconsistent byte array structure: invalid 6 bytes char"); } } else { throw new JavaClassParseException("Inconsistent byte array structure: unexpected char"); } } } } return sb.toString(); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.input.java.JavaInputPlugin ================================================ FILE: jadx-plugins/jadx-java-input/src/test/java/jadx/plugins/input/java/CustomLoadTest.java ================================================ package jadx.plugins.input.java; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.plugins.input.ICodeLoader; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; class CustomLoadTest { private JadxDecompiler jadx; @BeforeEach void init() { jadx = new JadxDecompiler(new JadxArgs()); } @AfterEach void close() { jadx.close(); } @Test void loadFiles() { List files = Stream.of("HelloWorld.class", "HelloWorld$HelloInner.class") .map(this::getSample) .collect(Collectors.toList()); ICodeLoader loadResult = JavaInputPlugin.loadClassFiles(files); loadDecompiler(loadResult); assertThat(jadx.getClassesWithInners()) .hasSize(2) .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")) .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloInner")); } @Test void loadFromInputStream() throws IOException { String fileName = "HelloWorld$HelloInner.class"; try (InputStream in = Files.newInputStream(getSample(fileName))) { ICodeLoader loadResult = JavaInputPlugin.loadFromInputStream(in, fileName); loadDecompiler(loadResult); assertThat(jadx.getClassesWithInners()) .hasSize(1) .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld$HelloInner")); System.out.println(jadx.getClassesWithInners().get(0).getCode()); } } @Test void loadSingleClass() throws IOException { String fileName = "HelloWorld.class"; byte[] content = Files.readAllBytes(getSample(fileName)); ICodeLoader loadResult = JavaInputPlugin.loadSingleClass(content, fileName); loadDecompiler(loadResult); assertThat(jadx.getClassesWithInners()) .hasSize(1) .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")); System.out.println(jadx.getClassesWithInners().get(0).getCode()); } @Test void load() { ICodeLoader loadResult = JavaInputPlugin.load(loader -> { List inputs = new ArrayList<>(2); try { String hello = "HelloWorld.class"; byte[] content = Files.readAllBytes(getSample(hello)); inputs.add(loader.loadClass(content, hello)); String helloInner = "HelloWorld$HelloInner.class"; InputStream in = Files.newInputStream(getSample(helloInner)); inputs.addAll(loader.loadInputStream(in, helloInner)); } catch (Exception e) { fail(e); } return inputs; }); loadDecompiler(loadResult); assertThat(jadx.getClassesWithInners()) .hasSize(2) .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")) .satisfiesOnlyOnce(cls -> { assertThat(cls.getName()).isEqualTo("HelloInner"); assertThat(cls.getCode()).isEmpty(); // no code for moved inner class }); assertThat(jadx.getClasses()) .hasSize(1) .satisfiesOnlyOnce(cls -> assertThat(cls.getName()).isEqualTo("HelloWorld")) .satisfiesOnlyOnce(cls -> assertThat(cls.getInnerClasses()).hasSize(1) .satisfiesOnlyOnce(inner -> assertThat(inner.getName()).isEqualTo("HelloInner"))); jadx.getClassesWithInners().forEach(cls -> System.out.println(cls.getCode())); } public void loadDecompiler(ICodeLoader codeLoader) { try { jadx.addCustomCodeLoader(codeLoader); jadx.load(); } catch (Exception e) { fail("Failed to load sample", e); } } public Path getSample(String name) { try { return Paths.get(ClassLoader.getSystemResource("samples/" + name).toURI()); } catch (Exception e) { return fail("Failed to load sample", e); } } } ================================================ FILE: jadx-plugins/jadx-java-input/src/test/java/jadx/plugins/input/java/utils/DescriptorParserTest.java ================================================ package jadx.plugins.input.java.utils; import java.util.Arrays; import org.junit.jupiter.api.Test; import jadx.plugins.input.java.data.JavaMethodRef; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; class DescriptorParserTest { @Test public void testPrimitives() { check("()V", "V"); check("(I)D", "D", "I"); } @Test public void testObjects() { check("(Ljava/lang/String;Ljava/lang/Object;)V", "V", "Ljava/lang/String;", "Ljava/lang/Object;"); } @SuppressWarnings("CatchMayIgnoreException") private void check(String desc, String retType, String... argTypes) { JavaMethodRef mthRef = new JavaMethodRef(); try { DescriptorParser.fillMethodProto(desc, mthRef); } catch (Exception e) { fail("Parse failed for: " + desc, e); } assertThat(mthRef.getReturnType()).isEqualTo(retType); assertThat(mthRef.getArgTypes()).isEqualTo(Arrays.asList(argTypes)); } } ================================================ FILE: jadx-plugins/jadx-java-input/src/test/java/jadx/plugins/input/java/utils/ModifiedUTF8DecoderTest.java ================================================ package jadx.plugins.input.java.utils; import org.junit.jupiter.api.Test; import static jadx.plugins.input.java.utils.ModifiedUTF8Decoder.decodeString; import static org.assertj.core.api.Assertions.assertThat; /* * TODO: find a way to enter 6-bytes char decode branch */ class ModifiedUTF8DecoderTest { @Test public void test() { String str = "aÆřᛒቶ北𝄠😀🨄𐆙"; byte[] mUTF8Bytes = new byte[] { 97, -61, -122, -59, -103, -31, -101, -110, -31, -119, -74, -17, -91, -93, -19, -96, -76, -19, -76, -96, -19, -96, -67, -19, -72, -128, -19, -96, -66, -19, -72, -124, -19, -96, -128, -19, -74, -103 }; assertThat(decodeString(mUTF8Bytes)).isEqualTo(str); } @Test public void testASCIIOnly() { String str = "Hello, world!"; byte[] mUTF8Bytes = new byte[] { 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33 }; assertThat(decodeString(mUTF8Bytes)).isEqualTo(str); } } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/build.gradle.kts ================================================ plugins { id("jadx-library") id("jadx-kotlin") } dependencies { api(project(":jadx-core")) implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.10") testImplementation(project.project(":jadx-core").sourceSets.getByName("test").output) testImplementation("org.apache.commons:commons-lang3:3.20.0") testRuntimeOnly(project(":jadx-plugins:jadx-smali-input")) testRuntimeOnly(project(":jadx-plugins:jadx-java-input")) } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt ================================================ package jadx.plugins.kotlin.metadata import jadx.api.plugins.options.impl.BasePluginOptionsBuilder import jadx.plugins.kotlin.metadata.KotlinMetadataPlugin.Companion.PLUGIN_ID class KotlinMetadataOptions : BasePluginOptionsBuilder() { var isClassAlias: Boolean = true private set var isMethodArgs: Boolean = true private set var isFields: Boolean = true private set var isCompanion: Boolean = true private set var isDataClass: Boolean = true private set var isToString: Boolean = true private set var isGetters: Boolean = true private set override fun registerOptions() { boolOption(CLASS_ALIAS_OPT) .description("rename class alias") .defaultValue(true) .setter { isClassAlias = it } boolOption(METHOD_ARGS_OPT) .description("rename function arguments") .defaultValue(true) .setter { isMethodArgs = it } boolOption(FIELDS_OPT) .description("rename fields") .defaultValue(true) .setter { isFields = it } boolOption(COMPANION_OPT) .description("rename companion object") .defaultValue(true) .setter { isCompanion = it } boolOption(DATA_CLASS_OPT) .description("add data class modifier") .defaultValue(true) .setter { isDataClass = it } boolOption(TO_STRING_OPT) .description("rename fields using toString") .defaultValue(true) .setter { isToString = it } boolOption(GETTERS_OPT) .description("rename simple getters to field names") .defaultValue(true) .setter { isGetters = it } } fun isPreparePassNeeded(): Boolean { return isClassAlias } fun isDecompilePassNeeded(): Boolean { return isMethodArgs || isFields || isCompanion || isDataClass || isToString || isGetters } companion object { const val CLASS_ALIAS_OPT = "$PLUGIN_ID.class-alias" const val METHOD_ARGS_OPT = "$PLUGIN_ID.method-args" const val FIELDS_OPT = "$PLUGIN_ID.fields" const val COMPANION_OPT = "$PLUGIN_ID.companion" const val DATA_CLASS_OPT = "$PLUGIN_ID.data-class" const val TO_STRING_OPT = "$PLUGIN_ID.to-string" const val GETTERS_OPT = "$PLUGIN_ID.getters" } } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataPlugin.kt ================================================ package jadx.plugins.kotlin.metadata import jadx.api.plugins.JadxPlugin import jadx.api.plugins.JadxPluginContext import jadx.api.plugins.JadxPluginInfo import jadx.plugins.kotlin.metadata.pass.KotlinMetadataDecompilePass import jadx.plugins.kotlin.metadata.pass.KotlinMetadataPreparePass class KotlinMetadataPlugin : JadxPlugin { private val options = KotlinMetadataOptions() override fun getPluginInfo(): JadxPluginInfo { return JadxPluginInfo(PLUGIN_ID, "Kotlin Metadata", "Use kotlin.Metadata annotation for code generation") } override fun init(context: JadxPluginContext) { context.registerOptions(options) if (options.isPreparePassNeeded()) { context.addPass(KotlinMetadataPreparePass(options)) } if (options.isDecompilePassNeeded()) { context.addPass(KotlinMetadataDecompilePass(options)) } } companion object { const val PLUGIN_ID = "kotlin-metadata" } } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/model/KotlinMetadataConsts.kt ================================================ package jadx.plugins.kotlin.metadata.model object KotlinMetadataConsts { const val KOTLIN_METADATA_ANNOTATION = "Lkotlin/Metadata;" const val KOTLIN_METADATA_K_PARAMETER = "k" const val KOTLIN_METADATA_D1_PARAMETER = "d1" const val KOTLIN_METADATA_D2_PARAMETER = "d2" const val KOTLIN_METADATA_MV_PARAMETER = "mv" const val KOTLIN_METADATA_XS_PARAMETER = "xs" const val KOTLIN_METADATA_PN_PARAMETER = "pn" const val KOTLIN_METADATA_XI_PARAMETER = "xi" } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/model/KotlinRenameResults.kt ================================================ package jadx.plugins.kotlin.metadata.model import jadx.core.dex.instructions.args.RegisterArg import jadx.core.dex.nodes.ClassNode import jadx.core.dex.nodes.FieldNode import jadx.core.dex.nodes.MethodNode data class ClassAliasRename( val pkg: String, val name: String, ) data class MethodArgRename( val rArg: RegisterArg, val alias: String, ) data class FieldRename( val field: FieldNode, val alias: String, ) data class CompanionRename( val field: FieldNode, val cls: ClassNode, val hide: Boolean, ) data class ToStringRename( val cls: ClassNode, val clsAlias: String?, val fields: List, ) data class MethodRename( val mth: MethodNode, val alias: String, ) ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/pass/KotlinMetadataDecompilePass.kt ================================================ package jadx.plugins.kotlin.metadata.pass import jadx.api.plugins.input.data.AccessFlags import jadx.api.plugins.pass.JadxPassInfo import jadx.api.plugins.pass.impl.OrderedJadxPassInfo import jadx.api.plugins.pass.types.JadxDecompilePass import jadx.core.deobf.NameMapper import jadx.core.dex.attributes.AFlag import jadx.core.dex.attributes.nodes.RenameReasonAttr import jadx.core.dex.nodes.ClassNode import jadx.core.dex.nodes.MethodNode import jadx.core.dex.nodes.RootNode import jadx.plugins.kotlin.metadata.KotlinMetadataOptions import jadx.plugins.kotlin.metadata.utils.KmClassWrapper import jadx.plugins.kotlin.metadata.utils.KmClassWrapper.Companion.getWrapper class KotlinMetadataDecompilePass( private val options: KotlinMetadataOptions, ) : JadxDecompilePass { override fun getInfo(): JadxPassInfo { return OrderedJadxPassInfo( "KotlinMetadataDecompile", "Use kotlin.Metadata annotation perform various renames", ) .before("CodeRenameVisitor") } override fun init(root: RootNode) { } override fun visit(cls: ClassNode): Boolean { cls.innerClasses.forEach(::visit) val wrapper = cls.getWrapper() ?: return false if (options.isMethodArgs) renameMethodArgs(wrapper) if (options.isFields) renameFields(wrapper) if (options.isCompanion) renameCompanion(wrapper) if (options.isDataClass) fixDataClass(wrapper) if (options.isToString) renameToString(wrapper) if (options.isGetters) renameGetters(wrapper) return false } override fun visit(mth: MethodNode?) { /* no op */ } private fun renameMethodArgs(wrapper: KmClassWrapper) { val args = wrapper.getMethodArgs() args.forEach { (_, list) -> list.forEach { (rArg, alias) -> // TODO: comment not being added? RenameReasonAttr.forNode(rArg).append(METADATA_REASON) rArg.name = alias } } } private fun renameFields(wrapper: KmClassWrapper) { val fields = wrapper.getFields() fields.forEach { (field, alias) -> if (AFlag.DONT_RENAME !in field) { RenameReasonAttr.forNode(field).append(METADATA_REASON) field.rename(alias) } } } private fun renameCompanion(wrapper: KmClassWrapper) { val companion = wrapper.getCompanion() companion?.run { if (AFlag.DONT_RENAME !in field) { RenameReasonAttr.forNode(field).append(METADATA_REASON) field.rename(COMPANION_FIELD) } if (AFlag.DONT_RENAME !in cls) { RenameReasonAttr.forNode(cls).append(METADATA_REASON) cls.rename(COMPANION_CLASS) } if (hide) { field.add(AFlag.DONT_GENERATE) cls.add(AFlag.DONT_GENERATE) cls.add(AFlag.DONT_INLINE) } } } private fun fixDataClass(wrapper: KmClassWrapper) { val isData = wrapper.isDataClass() wrapper.cls.run { if (isData != accessFlags.isData) { accessFlags = accessFlags.run { if (isData) { add(AccessFlags.DATA) } else { remove(AccessFlags.DATA) } } } } } private fun renameToString(wrapper: KmClassWrapper) { val toString = wrapper.parseToString() toString?.run { clsAlias?.let { alias -> if (NameMapper.isValidIdentifier(alias) && AFlag.DONT_RENAME !in cls) { RenameReasonAttr.forNode(cls).append(TO_STRING_REASON) cls.rename(alias) } } fields.forEach { (field, alias) -> if (NameMapper.isValidIdentifier(alias) && AFlag.DONT_RENAME !in field) { RenameReasonAttr.forNode(field).append(TO_STRING_REASON) field.rename(alias) } } } } private fun renameGetters(wrapper: KmClassWrapper) { val getters = wrapper.getGetters() getters.forEach { (mth, alias) -> if (AFlag.DONT_RENAME !in mth) { RenameReasonAttr.forNode(mth).append(GETTER_REASON) mth.rename(alias) } } } companion object { private const val METADATA_REASON = "from kotlin metadata" private const val COMPANION_FIELD = "INSTANCE" private const val COMPANION_CLASS = "Companion" private const val TO_STRING_REASON = "from toString" private const val GETTER_REASON = "from getter" } } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/pass/KotlinMetadataPreparePass.kt ================================================ package jadx.plugins.kotlin.metadata.pass import jadx.api.plugins.pass.JadxPassInfo import jadx.api.plugins.pass.impl.OrderedJadxPassInfo import jadx.api.plugins.pass.types.JadxPreparePass import jadx.core.dex.attributes.AFlag import jadx.core.dex.nodes.RootNode import jadx.plugins.kotlin.metadata.KotlinMetadataOptions import jadx.plugins.kotlin.metadata.utils.KotlinMetadataUtils class KotlinMetadataPreparePass( private val options: KotlinMetadataOptions, ) : JadxPreparePass { override fun getInfo(): JadxPassInfo { return OrderedJadxPassInfo( "KotlinMetadataPrepare", "Use kotlin.Metadata annotation to rename class & package", ) .before("RenameVisitor") } override fun init(root: RootNode) { if (options.isClassAlias) { for (cls in root.classes) { if (cls.contains(AFlag.DONT_RENAME)) { continue } // rename class & package val kotlinCls = KotlinMetadataUtils.getAlias(cls) if (kotlinCls != null) { cls.rename(kotlinCls.name) cls.packageNode.rename(kotlinCls.pkg) } } } } } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KmClassWrapper.kt ================================================ package jadx.plugins.kotlin.metadata.utils import jadx.core.dex.nodes.ClassNode import kotlin.metadata.KmClass import kotlin.metadata.isData import kotlin.metadata.jvm.KotlinClassMetadata // don't expose kotlinx.metadata.* types ? class KmClassWrapper private constructor( val cls: ClassNode, private val kmCls: KmClass, ) { fun getMethodArgs() = KotlinMetadataUtils.mapMethodArgs(cls, kmCls) fun getFields() = KotlinMetadataUtils.mapFields(cls, kmCls) fun getCompanion() = KotlinMetadataUtils.mapCompanion(cls, kmCls) fun isDataClass() = kmCls.isData // does not require metadata, may be useful for plain java ? fun parseToString() = KotlinUtils.parseToString(cls) // does not require metadata, may be useful for plain java ? fun getGetters() = KotlinUtils.findGetters(cls) companion object { fun ClassNode.getWrapper(): KmClassWrapper? { val metadata = getKotlinClassMetadata() val kmCls = (metadata as? KotlinClassMetadata.Class)?.kmClass ?: return null return KmClassWrapper(this, kmCls) } } } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KmExt.kt ================================================ package jadx.plugins.kotlin.metadata.utils import kotlin.metadata.KmFunction import kotlin.metadata.KmProperty import kotlin.metadata.jvm.fieldSignature import kotlin.metadata.jvm.signature inline val KmFunction.shortId: String? get() = signature?.toString() inline val KmProperty.shortId: String? get() = fieldSignature?.toString() ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinMetadataExt.kt ================================================ @file:Suppress("UNCHECKED_CAST") package jadx.plugins.kotlin.metadata.utils import jadx.api.plugins.input.data.annotations.EncodedType import jadx.api.plugins.input.data.annotations.EncodedValue import jadx.api.plugins.input.data.annotations.IAnnotation import jadx.core.dex.nodes.ClassNode import jadx.plugins.kotlin.metadata.model.KotlinMetadataConsts import kotlin.metadata.jvm.KotlinClassMetadata import kotlin.metadata.jvm.Metadata fun ClassNode.getMetadata(): Metadata? { val annotation: IAnnotation? = getAnnotation(KotlinMetadataConsts.KOTLIN_METADATA_ANNOTATION) return annotation?.run { val k = getParamAsInt(KotlinMetadataConsts.KOTLIN_METADATA_K_PARAMETER) val mvArray = getParamAsIntArray(KotlinMetadataConsts.KOTLIN_METADATA_MV_PARAMETER) val d1Array = getParamAsStringArray(KotlinMetadataConsts.KOTLIN_METADATA_D1_PARAMETER) val d2Array = getParamAsStringArray(KotlinMetadataConsts.KOTLIN_METADATA_D2_PARAMETER) val xs = getParamAsString(KotlinMetadataConsts.KOTLIN_METADATA_XS_PARAMETER) val pn = getParamAsString(KotlinMetadataConsts.KOTLIN_METADATA_PN_PARAMETER) val xi = getParamAsInt(KotlinMetadataConsts.KOTLIN_METADATA_XI_PARAMETER) Metadata( kind = k, metadataVersion = mvArray, data1 = d1Array, data2 = d2Array, extraString = xs, packageName = pn, extraInt = xi, ) } } private fun IAnnotation.getParamsAsList(paramName: String): List? { val encodedValue = values[paramName] ?.takeIf { it.type == EncodedType.ENCODED_ARRAY && it.value is List<*> } return encodedValue?.value?.let { it as List } } private fun IAnnotation.getParamAsStringArray(paramName: String): Array? { return getParamsAsList(paramName) ?.map(EncodedValue::getValue) ?.onEach { if (it != null && it !is String) return@onEach } ?.map { "$it" } ?.toTypedArray() } private fun IAnnotation.getParamAsIntArray(paramName: String): IntArray? { return getParamsAsList(paramName) ?.map(EncodedValue::getValue) ?.map { it as Int } ?.toIntArray() } private fun IAnnotation.getParamAsInt(paramName: String): Int? { val encodedValue = values[paramName] ?.takeIf { it.type == EncodedType.ENCODED_INT && it.value is Int } return encodedValue?.value?.let { it as Int } } private fun IAnnotation.getParamAsString(paramName: String): String? { val encodedValue = values[paramName] ?.takeIf { it.type == EncodedType.ENCODED_STRING && it.value is String } return encodedValue?.value?.let { it as String } } fun ClassNode.getKotlinClassMetadata(): KotlinClassMetadata? { return getMetadata()?.let(KotlinClassMetadata::readLenient) } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinMetadataUtils.kt ================================================ package jadx.plugins.kotlin.metadata.utils import jadx.core.deobf.NameMapper import jadx.core.dex.attributes.nodes.RenameReasonAttr import jadx.core.dex.nodes.ClassNode import jadx.core.dex.nodes.MethodNode import jadx.core.utils.Utils import jadx.plugins.kotlin.metadata.model.ClassAliasRename import jadx.plugins.kotlin.metadata.model.CompanionRename import jadx.plugins.kotlin.metadata.model.FieldRename import jadx.plugins.kotlin.metadata.model.MethodArgRename import kotlin.metadata.KmClass object KotlinMetadataUtils { @JvmStatic fun getAlias(cls: ClassNode): ClassAliasRename? { val annotation = cls.getMetadata() ?: return null return getClassAlias(cls, annotation) } /** * Try to get class info from Kotlin Metadata annotation */ private fun getClassAlias(cls: ClassNode, annotation: Metadata): ClassAliasRename? { val firstValue = annotation.data2.getOrNull(0) ?: return null try { val clsName = firstValue.trim() .takeUnless(String::isEmpty) ?.let(Utils::cleanObjectName) ?: return null val alias = splitAndCheckClsName(cls, clsName) if (alias != null) { RenameReasonAttr.forNode(cls).append("from Kotlin metadata") return alias } } catch (e: Exception) { LOG.error("Failed to parse kotlin metadata", e) } return null } // Don't use ClassInfo facility to not pollute class into cache private fun splitAndCheckClsName(originCls: ClassNode, fullClsName: String): ClassAliasRename? { if (!NameMapper.isValidFullIdentifier(fullClsName)) { return null } val pkg: String val name: String val dot = fullClsName.lastIndexOf('.') if (dot == -1) { pkg = "" name = fullClsName } else { pkg = fullClsName.substring(0, dot) name = fullClsName.substring(dot + 1) } val originClsInfo = originCls.classInfo val originName = originClsInfo.shortName if (originName == name || name.contains("$") || !NameMapper.isValidIdentifier(name) || countPkgParts(originClsInfo.getPackage()) != countPkgParts(pkg) || pkg.startsWith("java.") ) { return null } val newClsNode = originCls.root().resolveClass(fullClsName) return if (newClsNode != null) { // class with alias name already exist null } else { ClassAliasRename(pkg, name) } } private fun countPkgParts(pkg: String): Int { if (pkg.isEmpty()) { return 0 } var count = 1 var pos = 0 while (true) { pos = pkg.indexOf('.', pos) if (pos == -1) { return count } pos++ count++ } } fun mapMethodArgs(cls: ClassNode, kmCls: KmClass): Map> { return buildMap { kmCls.functions.forEach { kmFunction -> val node: MethodNode = cls.searchMethodByShortId(kmFunction.shortId) ?: return@forEach val argCount = node.argTypes.size val paramCount = kmFunction.valueParameters.size if (argCount == paramCount) { // requires arg registers to be loaded, is this necessary ? val aliasList = node.argRegs.zip(kmFunction.valueParameters).map { (rArg, kmValueParameter) -> MethodArgRename(rArg = rArg, alias = kmValueParameter.name) } put(node, aliasList) } } } } fun mapFields(cls: ClassNode, kmCls: KmClass): List { return kmCls.properties.mapNotNull { kmProperty -> val node = cls.searchFieldByShortId(kmProperty.shortId) ?: return@mapNotNull null FieldRename(field = node, alias = kmProperty.name) } } fun mapCompanion(cls: ClassNode, kmCls: KmClass): CompanionRename? { val compName = kmCls.companionObject ?: return null val compField = cls.fields.firstOrNull { it.name == compName && it.accessFlags.run { isStatic && isFinal && isPublic } } ?: return null if (compField.type.isObject) { val compType = compField.type.`object` val compCls = cls.innerClasses.firstOrNull { it.classInfo.makeRawFullName() == compType } ?: return null val isOnlyInit = compField.useIn.size == 1 && compField.useIn[0].methodInfo.isClassInit val isEmpty = compCls.run { methods.all { it.isConstructor } && fields.isEmpty() } return CompanionRename( field = compField, cls = compCls, hide = isOnlyInit && isEmpty, ) } return null } } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinUtils.kt ================================================ package jadx.plugins.kotlin.metadata.utils import jadx.core.Consts import jadx.core.dex.info.FieldInfo import jadx.core.dex.instructions.IndexInsnNode import jadx.core.dex.instructions.InsnType import jadx.core.dex.instructions.InvokeNode import jadx.core.dex.instructions.args.PrimitiveType import jadx.core.dex.nodes.ClassNode import jadx.core.dex.nodes.FieldNode import jadx.core.dex.nodes.MethodNode import jadx.plugins.kotlin.metadata.model.MethodRename import jadx.plugins.kotlin.metadata.model.ToStringRename import java.util.Locale object KotlinUtils { fun parseToString(cls: ClassNode): ToStringRename? { val mthToString = cls.searchMethodByShortId(Consts.MTH_TOSTRING_SIGNATURE) ?: return null return ToStringParser.parse(mthToString) } fun findGetters(cls: ClassNode): List { return cls.fields.filter(FieldNode::isInstance).mapNotNull { field -> val mth = getFieldGetterMethod(cls, field.fieldInfo) ?: return@mapNotNull null MethodRename( mth = mth, alias = getGetterAlias(field.alias), ) } } private fun getFieldGetterMethod(cls: ClassNode, field: FieldInfo): MethodNode? { return cls.methods.firstOrNull { it.returnType == field.type && it.argTypes.isEmpty() && it.insnsCount == 3 && it.sVars.size == 2 && (it.sVars[1].assignInsn as? IndexInsnNode)?.index == field } } private fun getGetterAlias(fieldAlias: String): String { val capitalized = fieldAlias.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() } return "get$capitalized" } // untested & overly complicated fun parseDefaultMethods(cls: ClassNode): List { val possibleMthList = cls.methods.filter { it.accessFlags.isStatic && it.accessFlags.isSynthetic && it.argTypes.run { size > 3 && first().isObject && first().`object` == cls.fullName && get(size - 2).isPrimitive && get(size - 2).primitiveType == PrimitiveType.INT && last().isObject && last().`object` == Consts.CLASS_OBJECT } } val insnList = possibleMthList.filter { it.exitBlock.run { iDom != null && iDom.instructions.firstOrNull()?.type == InsnType.RETURN iDom.iDom != null } && it.exitBlock.iDom.iDom.run { instructions.firstOrNull() is InvokeNode } } val remapped = insnList.mapNotNull { val insn = it.exitBlock.iDom.iDom.instructions.first() as InvokeNode cls.searchMethodByShortId(insn.callMth.shortId)?.run { it to this } } return remapped.map { (defaultMethod, originalMethod) -> MethodRename( mth = defaultMethod, alias = getDefaultMethodAlias(originalMethod.alias), ) } } private fun getDefaultMethodAlias(alias: String): String { return "$alias\$default" } } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/LogExt.kt ================================================ package jadx.plugins.kotlin.metadata.utils import org.slf4j.Logger import org.slf4j.LoggerFactory inline val T.LOG: Logger get() = LoggerFactory.getLogger(T::class.java) inline fun T.runCatchingLog(msg: String? = null, block: () -> R) = runCatching(block) .onFailure { LOG.error(msg.orEmpty(), it) } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/ToStringParser.kt ================================================ package jadx.plugins.kotlin.metadata.utils import jadx.core.Consts import jadx.core.dex.info.FieldInfo import jadx.core.dex.instructions.ConstStringNode import jadx.core.dex.instructions.IndexInsnNode import jadx.core.dex.instructions.InsnType import jadx.core.dex.instructions.InvokeNode import jadx.core.dex.instructions.InvokeType import jadx.core.dex.instructions.args.InsnWrapArg import jadx.core.dex.instructions.args.RegisterArg import jadx.core.dex.instructions.mods.ConstructorInsn import jadx.core.dex.nodes.BlockNode import jadx.core.dex.nodes.InsnNode import jadx.core.dex.nodes.MethodNode import jadx.core.utils.BlockUtils import jadx.plugins.kotlin.metadata.model.FieldRename import jadx.plugins.kotlin.metadata.model.ToStringRename class ToStringParser private constructor(mthToString: MethodNode) { private var isStarted = false private var isFirstProcessed = false private var isFinished = false private var pendingAlias: String? = null private var clsAlias: String? = null private val list: MutableList> = mutableListOf() val isSuccess: Boolean get() = isStarted && isFinished init { val blocks: List = BlockUtils.buildSimplePath(mthToString.enterBlock) blocks.forEach { block -> block.instructions.forEach { insn -> process(insn) } } } private fun process(insn: InsnNode) { if (!isStarted) { isStarted = isStartStringBuilder(insn) return } if (isFinished) { return } if (isAppendInvoke(insn)) { val arg = insn.getArg(1) // invoke with const string if (arg.isInsnWrap && arg is InsnWrapArg && arg.wrapInsn.type == InsnType.CONST_STR) { val constStr: String? = (arg.wrapInsn as ConstStringNode).string handleString(requireNotNull(constStr) { "Failed to get const String" }) } // invoke with register if (arg.isRegister && arg is RegisterArg) { val assign = arg.sVar.assignInsn // basic argument if (assign is IndexInsnNode) { val info: FieldInfo? = (arg.sVar.assignInsn as IndexInsnNode).index as? FieldInfo handleFieldInfo(requireNotNull(info) { "Failed to get FieldInfo from index" }) } // string formatted argument, for rare cases like Arrays.toString(...) if (assign is InvokeNode && assign.invokeType == InvokeType.STATIC && assign.argsCount == 1) { val prevArg = assign.getArg(0) if (prevArg.isRegister && prevArg is RegisterArg) { if (prevArg.sVar.assignInsn is IndexInsnNode) { val info: FieldInfo? = (prevArg.sVar.assignInsn as IndexInsnNode).index as? FieldInfo handleFieldInfo(requireNotNull(info) { "Failed to get nested FieldInfo from index" }) } } } } return } isFinished = isToString(insn) } private fun handleString(string: String) { if (pendingAlias != null) { LOG.warn("Skipping pending alias: '$pendingAlias'") } if (!isFirstProcessed) { clsAlias = string.substringBefore('(') pendingAlias = string .substringAfter('(') .substringBeforeLast('=') isFirstProcessed = true } else { pendingAlias = string .substringAfter(", ") .substringBeforeLast('=') } } private fun handleFieldInfo(fieldInfo: FieldInfo) { list.add(requireNotNull(pendingAlias) { "No pending alias found" } to fieldInfo) pendingAlias = null } companion object { fun parse(mth: MethodNode): ToStringRename? { val parser = kotlin.runCatching { ToStringParser(mth) }.getOrNull() if (parser?.isSuccess != true) return null val cls = mth.parentClass return ToStringRename( cls = cls, clsAlias = parser.clsAlias, fields = parser.list.mapNotNull { (alias, fieldInfo) -> val field = cls.searchField(fieldInfo) ?: return@mapNotNull null FieldRename( field = field, alias = alias, ) }, ) } private fun isStartStringBuilder(inst: InsnNode): Boolean { return inst is ConstructorInsn && inst.isNewInstance && inst.callMth.declClass.fullName == Consts.CLASS_STRING_BUILDER } private fun isAppendInvoke(inst: InsnNode): Boolean { return inst is InvokeNode && inst.callMth.declClass.fullName == Consts.CLASS_STRING_BUILDER && inst.callMth.name == "append" && inst.argsCount == 2 } private fun isToString(inst: InsnNode): Boolean { return inst is InvokeNode && inst.callMth.declClass.fullName == Consts.CLASS_STRING_BUILDER && inst.callMth.shortId == Consts.MTH_TOSTRING_SIGNATURE } } } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.kotlin.metadata.KotlinMetadataPlugin ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/test/kotlin/TestJavaParser.kt ================================================ package jadx.plugins.kotlin.metadata.tests import jadx.tests.api.IntegrationTest import jadx.tests.api.utils.assertj.JadxAssertions.assertThat import org.junit.jupiter.api.Test class TestJavaParser : IntegrationTest() { @Test fun test() { val sampleCls = getResourceFile("samples/MainKt.class") assertThat(getClassNodeFromFiles(listOf(sampleCls), "MainKt")) .code() .doesNotContain("Exception occurred when reading Kotlin metadata") } } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/test/kotlin/TestKotlinMetadata.kt ================================================ package jadx.plugins.kotlin.metadata.tests import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.CLASS_ALIAS_OPT import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.COMPANION_OPT import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.DATA_CLASS_OPT import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.FIELDS_OPT import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.GETTERS_OPT import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.METHOD_ARGS_OPT import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.TO_STRING_OPT import jadx.tests.api.SmaliTest import jadx.tests.api.utils.assertj.JadxAssertions.assertThat import jadx.tests.api.utils.assertj.JadxCodeAssertions import org.junit.jupiter.api.Test class TestKotlinMetadata : SmaliTest() { // @formatter:off /* package deobf data class DataClassSample( val name: String, private val id: Int, ) { var inner: Short = 3 companion object { fun getTag(): String { return "TAG" } } } */ // @formatter:on @Test fun testMethodArgs() { setupArgs { this[METHOD_ARGS_OPT] = true } assertThatClass() .containsOne("public boolean equals(Object other) {") } @Test fun testIgnoreMethodArgs() { setupArgs() assertThatClass() .containsOne("public boolean equals(Object obj) {") } @Test fun testFields() { setupArgs { this[FIELDS_OPT] = true } assertThatClass() .containsOne("private final String name;") .containsOne("private final int id;") .containsOne("private short inner;") .countString(3, "reason: from kotlin metadata") } @Test fun testIgnoreFields() { setupArgs() assertThatClass() .containsOne("private final String a;") .containsOne("private final int b;") .containsOne("private short c;") .countString(0, "reason: from kotlin metadata") } @Test fun testCompanion() { setupArgs { this[COMPANION_OPT] = true } assertThatClass() .containsOne("public static final Companion INSTANCE = new Companion(null);") .containsOne("public static final class Companion {") .countString(2, "reason: from kotlin metadata") } @Test fun testIgnoreCompanion() { setupArgs() assertThatClass() .containsOne("public static final b d = new b(null);") .containsOne("public static final class b {") .countString(0, "reason: from kotlin metadata") } @Test fun testDataClass() { setupArgs { this[DATA_CLASS_OPT] = true } assertThatClass() .containsOne("/* data */") } @Test fun testIgnoreDataClass() { setupArgs() assertThatClass() .countString(0, "/* data */") } @Test fun testToString() { setupArgs { this[TO_STRING_OPT] = true } assertThatClass() .containsOne("public final class DataClassSample {") .containsOne("private final String name;") .containsOne("private final int id;") .countString(3, "reason: from toString") } @Test fun testIgnoreToString() { setupArgs() assertThatClass() .containsOne("public final class a {") .containsOne("private final String a;") .containsOne("private final int b;") .countString(0, "reason: from toString") } @Test fun testGetters() { setupArgs { this[GETTERS_OPT] = true } assertThatClass() .containsOne("public final String getA() {") .countString(1, "reason: from getter") } @Test fun testGettersAlias() { setupArgs { this[FIELDS_OPT] = true this[GETTERS_OPT] = true } assertThatClass() .containsOne("public final String getName() {") .countString(1, "reason: from getter") } @Test fun testIgnoreGetters() { setupArgs() assertThatClass() .countString(0, "reason: from getter") } private fun setupArgs(builder: MutableMap.() -> Unit = {}) { val allOff = mutableMapOf( CLASS_ALIAS_OPT to false, METHOD_ARGS_OPT to false, FIELDS_OPT to false, COMPANION_OPT to false, DATA_CLASS_OPT to false, TO_STRING_OPT to false, GETTERS_OPT to false, ) args.pluginOptions = allOff.apply(builder).mapValues { if (it.value) "yes" else "no" } } private fun assertThatClass(): JadxCodeAssertions = assertThat(getClassNodeFromSmaliFiles("deobf", "TestKotlinMetadata", "a")) .code() } ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/test/smali/deobf/TestKotlinMetadata/a$b.smali ================================================ .class public final Ldeobf/a$b; .super Ljava/lang/Object; .source "SourceFile" # annotations .annotation system Ldalvik/annotation/EnclosingClass; value = Ldeobf/a; .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x19 name = "b" .end annotation .annotation runtime Lkotlin/Metadata; d1 = { "\u0000\u0010\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0010\u000e\n\u0002\u0008\u0004\u0008\u0086\u0003\u0018\u00002\u00020\u0001B\t\u0008\u0002\u00a2\u0006\u0004\u0008\u0004\u0010\u0005J\u0006\u0010\u0003\u001a\u00020\u0002\u00a8\u0006\u0006" } d2 = { "Ldeobf/DataClassSample$Companion;", "", "", "a", "", "()V", "app_release" } k = 0x1 mv = { 0x1, 0x8, 0x0 } .end annotation # direct methods .method private constructor ()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;->()V return-void .end method .method public synthetic constructor (Lkotlin/jvm/internal/DefaultConstructorMarker;)V .registers 2 .line 1 invoke-direct {p0}, Ldeobf/a$b;->()V return-void .end method # virtual methods .method public final a()Ljava/lang/String; .registers 2 const-string v0, "TAG" return-object v0 .end method ================================================ FILE: jadx-plugins/jadx-kotlin-metadata/src/test/smali/deobf/TestKotlinMetadata/a.smali ================================================ .class public final Ldeobf/a; .super Ljava/lang/Object; .source "SourceFile" # annotations .annotation system Ldalvik/annotation/MemberClasses; value = { Ldeobf/a$b; } .end annotation .annotation runtime Lkotlin/Metadata; d1 = { "\u0000&\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0008\n\u0002\u0008\u0002\n\u0002\u0010\u000b\n\u0002\u0008\u0008\n\u0002\u0010\n\n\u0002\u0008\u000b\u0008\u0086\u0008\u0018\u0000 \u00192\u00020\u0001:\u0001\u001aB\u0017\u0012\u0006\u0010\u000c\u001a\u00020\u0002\u0012\u0006\u0010\u000f\u001a\u00020\u0004\u00a2\u0006\u0004\u0008\u0017\u0010\u0018J\t\u0010\u0003\u001a\u00020\u0002H\u00d6\u0001J\t\u0010\u0005\u001a\u00020\u0004H\u00d6\u0001J\u0013\u0010\u0008\u001a\u00020\u00072\u0008\u0010\u0006\u001a\u0004\u0018\u00010\u0001H\u00d6\u0003R\u0017\u0010\u000c\u001a\u00020\u00028\u0006\u00a2\u0006\u000c\n\u0004\u0008\t\u0010\n\u001a\u0004\u0008\t\u0010\u000bR\u0014\u0010\u000f\u001a\u00020\u00048\u0002X\u0082\u0004\u00a2\u0006\u0006\n\u0004\u0008\r\u0010\u000eR\"\u0010\u0016\u001a\u00020\u00108\u0006@\u0006X\u0086\u000e\u00a2\u0006\u0012\n\u0004\u0008\u0011\u0010\u0012\u001a\u0004\u0008\u0013\u0010\u0014\"\u0004\u0008\r\u0010\u0015\u00a8\u0006\u001b" } d2 = { "Ldeobf/DataClassSample;", "", "", "toString", "", "hashCode", "other", "", "equals", "a", "Ljava/lang/String;", "()Ljava/lang/String;", "name", "b", "I", "id", "", "c", "S", "getInner", "()S", "(S)V", "inner", "", "(Ljava/lang/String;I)V", "d", "Companion", "app_release" } k = 0x1 mv = { 0x1, 0x8, 0x0 } .end annotation # static fields .field public static final d:Ldeobf/a$b; # instance fields .field private final a:Ljava/lang/String; .field private final b:I .field private c:S # direct methods .method static constructor ()V .registers 2 new-instance v0, Ldeobf/a$b; const/4 v1, 0x0 invoke-direct {v0, v1}, Ldeobf/a$b;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V sput-object v0, Ldeobf/a;->d:Ldeobf/a$b; return-void .end method .method public constructor (Ljava/lang/String;I)V .registers 4 const-string v0, "name" invoke-static {p1, v0}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V invoke-direct {p0}, Ljava/lang/Object;->()V iput-object p1, p0, Ldeobf/a;->a:Ljava/lang/String; iput p2, p0, Ldeobf/a;->b:I const/4 p1, 0x3 iput-short p1, p0, Ldeobf/a;->c:S return-void .end method # virtual methods .method public final a()Ljava/lang/String; .registers 2 iget-object v0, p0, Ldeobf/a;->a:Ljava/lang/String; return-object v0 .end method .method public final b(S)V .registers 2 iput-short p1, p0, Ldeobf/a;->c:S return-void .end method .method public equals(Ljava/lang/Object;)Z .registers 6 const/4 v0, 0x1 if-ne p0, p1, :cond_4 return v0 :cond_4 instance-of v1, p1, Ldeobf/a; const/4 v2, 0x0 if-nez v1, :cond_a return v2 :cond_a check-cast p1, Ldeobf/a; iget-object v1, p0, Ldeobf/a;->a:Ljava/lang/String; iget-object v3, p1, Ldeobf/a;->a:Ljava/lang/String; invoke-static {v1, v3}, Lkotlin/jvm/internal/Intrinsics;->areEqual(Ljava/lang/Object;Ljava/lang/Object;)Z move-result v1 if-nez v1, :cond_17 return v2 :cond_17 iget v1, p0, Ldeobf/a;->b:I iget p1, p1, Ldeobf/a;->b:I if-eq v1, p1, :cond_1e return v2 :cond_1e return v0 .end method .method public hashCode()I .registers 3 iget-object v0, p0, Ldeobf/a;->a:Ljava/lang/String; invoke-virtual {v0}, Ljava/lang/String;->hashCode()I move-result v0 mul-int/lit8 v0, v0, 0x1f iget v1, p0, Ldeobf/a;->b:I add-int/2addr v0, v1 return v0 .end method .method public toString()Ljava/lang/String; .registers 5 iget-object v0, p0, Ldeobf/a;->a:Ljava/lang/String; iget v1, p0, Ldeobf/a;->b:I new-instance v2, Ljava/lang/StringBuilder; invoke-direct {v2}, Ljava/lang/StringBuilder;->()V const-string v3, "DataClassSample(name=" invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; const-string v0, ", id=" invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; const-string v0, ")" invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v0 return-object v0 .end method ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/build.gradle.kts ================================================ plugins { id("jadx-library") id("jadx-kotlin") } dependencies { api(project(":jadx-core")) testImplementation(project.project(":jadx-core").sourceSets.getByName("test").output) testImplementation("org.apache.commons:commons-lang3:3.20.0") testRuntimeOnly(project(":jadx-plugins:jadx-smali-input")) } ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/main/kotlin/jadx/plugins/kotlin/smap/KotlinSmapOptions.kt ================================================ package jadx.plugins.kotlin.smap import jadx.api.plugins.options.impl.BasePluginOptionsBuilder import jadx.plugins.kotlin.smap.KotlinSmapPlugin.Companion.PLUGIN_ID class KotlinSmapOptions : BasePluginOptionsBuilder() { var isClassAliasSourceDbg: Boolean = true private set override fun registerOptions() { boolOption(CLASS_ALIAS_SOURCE_DBG_OPT) .description("rename class alias from SourceDebugExtension") .defaultValue(false) .setter { isClassAliasSourceDbg = it } } fun isClassSourceDbg(): Boolean { return isClassAliasSourceDbg } companion object { const val CLASS_ALIAS_SOURCE_DBG_OPT = "$PLUGIN_ID.class-alias-source-dbg" } } ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/main/kotlin/jadx/plugins/kotlin/smap/KotlinSmapPlugin.kt ================================================ package jadx.plugins.kotlin.smap import jadx.api.plugins.JadxPlugin import jadx.api.plugins.JadxPluginContext import jadx.api.plugins.JadxPluginInfo import jadx.plugins.kotlin.smap.pass.KotlinSourceDebugExtensionPass class KotlinSmapPlugin : JadxPlugin { private val options = KotlinSmapOptions() override fun getPluginInfo(): JadxPluginInfo { return JadxPluginInfo(PLUGIN_ID, "Kotlin SMAP", "Use kotlin.SourceDebugExtension annotation for rename class alias") } override fun init(context: JadxPluginContext) { context.registerOptions(options) if (options.isClassSourceDbg()) { context.addPass(KotlinSourceDebugExtensionPass(options)) } } companion object { const val PLUGIN_ID = "kotlin-smap" } } ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/main/kotlin/jadx/plugins/kotlin/smap/model/ClassAliasRename.kt ================================================ package jadx.plugins.kotlin.smap.model data class ClassAliasRename( val pkg: String, val name: String, ) ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/main/kotlin/jadx/plugins/kotlin/smap/model/Constants.kt ================================================ package jadx.plugins.kotlin.smap.model object Constants { const val KOTLIN_SOURCE_DEBUG_EXTENSION = "Lkotlin/jvm/internal/SourceDebugExtension;" } ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/main/kotlin/jadx/plugins/kotlin/smap/model/SMAP.kt ================================================ /* * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package jadx.plugins.kotlin.smap.model import kotlin.math.max const val KOTLIN_STRATA_NAME = "Kotlin" const val KOTLIN_DEBUG_STRATA_NAME = "KotlinDebug" /** * Represents SMAP as a structure that is contained in `SourceDebugExtension` attribute of a class. * This structure is immutable, we can only query for a result. */ class SMAP(val fileMappings: List) { // assuming disjoint line mappings (otherwise binary search can't be used anyway) private val intervals = fileMappings.flatMap { it.lineMappings }.sortedBy { it.dest } fun findRange(lineNumber: Int): RangeMapping? { val index = intervals.binarySearch { if (lineNumber in it) 0 else it.dest - lineNumber } return if (index < 0) null else intervals[index] } companion object { const val FILE_SECTION = "*F" const val LINE_SECTION = "*L" const val STRATA_SECTION = "*S" const val END = "*E" } } class FileMapping(val name: String, val path: String) { val lineMappings = arrayListOf() fun toSourceInfo(): SourceInfo = SourceInfo( name, path, lineMappings.fold(0) { result, mapping -> max(result, mapping.source + mapping.range - 1) }, ) fun mapNewLineNumber(source: Int, currentIndex: Int, callSite: SourcePosition?): Int { // Save some space in the SMAP by reusing (or extending if it's the last one) the existing range. // TODO some *other* range may already cover `source`; probably too slow to check them all though. // Maybe keep the list ordered by `source` and use binary search to locate the closest range on the left? val mapping = lineMappings.lastOrNull()?.takeIf { it.canReuseFor(source, currentIndex, callSite) } ?: lineMappings.firstOrNull()?.takeIf { it.canReuseFor(source, currentIndex, callSite) } ?: mapNewInterval(source, currentIndex + 1, 1, callSite) mapping.range = max(mapping.range, source - mapping.source + 1) return mapping.mapSourceToDest(source) } private fun RangeMapping.canReuseFor(newSource: Int, globalMaxDest: Int, newCallSite: SourcePosition?): Boolean = callSite == newCallSite && (newSource - source) in 0 until range + (if (globalMaxDest in this) 10 else 0) fun mapNewInterval(source: Int, dest: Int, range: Int, callSite: SourcePosition? = null): RangeMapping = RangeMapping(source, dest, range, callSite, parent = this).also { lineMappings.add(it) } } data class RangeMapping(val source: Int, val dest: Int, var range: Int, val callSite: SourcePosition?, val parent: FileMapping) { operator fun contains(destLine: Int): Boolean = dest <= destLine && destLine < dest + range fun hasMappingForSource(sourceLine: Int): Boolean = source <= sourceLine && sourceLine < source + range fun mapDestToSource(destLine: Int): SourcePosition = SourcePosition(source + (destLine - dest), parent.name, parent.path) fun mapSourceToDest(sourceLine: Int): Int = dest + (sourceLine - source) } val RangeMapping.toRange: IntRange get() = dest until dest + range data class SourcePosition(val line: Int, val file: String, val path: String) ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/main/kotlin/jadx/plugins/kotlin/smap/model/SourceInfo.kt ================================================ package jadx.plugins.kotlin.smap.model data class SourceInfo( val sourceFileName: String?, val pathOrCleanFQN: String, val linesInFile: Int, ) ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/main/kotlin/jadx/plugins/kotlin/smap/pass/KotlinSourceDebugExtensionPass.kt ================================================ package jadx.plugins.kotlin.smap.pass import jadx.api.plugins.pass.JadxPassInfo import jadx.api.plugins.pass.impl.OrderedJadxPassInfo import jadx.api.plugins.pass.types.JadxPreparePass import jadx.core.dex.attributes.AFlag import jadx.core.dex.nodes.RootNode import jadx.plugins.kotlin.smap.KotlinSmapOptions import jadx.plugins.kotlin.smap.utils.KotlinSmapUtils class KotlinSourceDebugExtensionPass( private val options: KotlinSmapOptions, ) : JadxPreparePass { override fun getInfo(): JadxPassInfo { return OrderedJadxPassInfo( "SourceDebugExtensionPrepare", "Use kotlin.jvm.internal.SourceDebugExtension annotation to rename class & package", ) .before("RenameVisitor") } override fun init(root: RootNode) { if (options.isClassAliasSourceDbg) { for (cls in root.classes) { if (cls.contains(AFlag.DONT_RENAME)) { continue } // rename class & package val kotlinCls = KotlinSmapUtils.getClassAlias(cls) if (kotlinCls != null) { cls.rename(kotlinCls.name) cls.packageNode.rename(kotlinCls.pkg) } } } } } ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/main/kotlin/jadx/plugins/kotlin/smap/utils/Extensions.kt ================================================ @file:Suppress("UNCHECKED_CAST") package jadx.plugins.kotlin.smap.utils import jadx.api.plugins.input.data.annotations.EncodedType import jadx.api.plugins.input.data.annotations.EncodedValue import jadx.api.plugins.input.data.annotations.IAnnotation import jadx.core.dex.nodes.ClassNode import jadx.plugins.kotlin.smap.model.Constants import jadx.plugins.kotlin.smap.model.SMAP fun ClassNode.getSourceDebugExtension(): SMAP? { val annotation: IAnnotation? = getAnnotation(Constants.KOTLIN_SOURCE_DEBUG_EXTENSION) return annotation?.run { val smapParser = SMAPParser.parseOrNull(getParamsAsList("value")?.get(0)?.value.toString()) return smapParser } } private fun IAnnotation.getParamsAsList(paramName: String): List? { val encodedValue = values[paramName] ?.takeIf { it.type == EncodedType.ENCODED_ARRAY && it.value is List<*> } return encodedValue?.value?.let { it as List } } ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/main/kotlin/jadx/plugins/kotlin/smap/utils/KotlinSmapUtils.kt ================================================ package jadx.plugins.kotlin.smap.utils import jadx.core.deobf.NameMapper import jadx.core.dex.attributes.nodes.RenameReasonAttr import jadx.core.dex.nodes.ClassNode import jadx.core.utils.Utils import jadx.plugins.kotlin.smap.model.ClassAliasRename import jadx.plugins.kotlin.smap.model.SMAP import org.slf4j.Logger import org.slf4j.LoggerFactory import kotlin.jvm.java object KotlinSmapUtils { val LOG: Logger = LoggerFactory.getLogger(KotlinSmapUtils::class.java) @JvmStatic fun getClassAlias(cls: ClassNode): ClassAliasRename? { val annotation = cls.getSourceDebugExtension() ?: return null return getClassAlias(cls, annotation) } private fun getClassAlias(cls: ClassNode, annotation: SMAP): ClassAliasRename? { val firstValue = annotation.fileMappings[0].path.replace("/", ".") try { val clsName = firstValue.trim() .takeUnless(String::isEmpty) ?.let(Utils::cleanObjectName) ?: return null val alias = splitAndCheckClsName(cls, clsName) if (alias != null) { RenameReasonAttr.forNode(cls).append("from SourceDebugExtension") return alias } } catch (e: Exception) { LOG.error("Failed to parse SourceDebugExtension", e) } return null } // Don't use ClassInfo facility to not pollute class into cache private fun splitAndCheckClsName(originCls: ClassNode, fullClsName: String): ClassAliasRename? { if (!NameMapper.isValidFullIdentifier(fullClsName)) { return null } val pkg: String val name: String val dot = fullClsName.lastIndexOf('.') if (dot == -1) { pkg = "" name = fullClsName } else { pkg = fullClsName.substring(0, dot) name = fullClsName.substring(dot + 1) } val originClsInfo = originCls.classInfo val originName = originClsInfo.shortName if (originName == name || name.contains("$") || !NameMapper.isValidIdentifier(name) || pkg.startsWith("java.") ) { return null } val newClsNode = originCls.root().resolveClass(fullClsName) return if (newClsNode != null) { // class with alias name already exist null } else { ClassAliasRename(pkg, name) } } } ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/main/kotlin/jadx/plugins/kotlin/smap/utils/SMAPParser.kt ================================================ /* * Copyright 2010-2015 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jadx.plugins.kotlin.smap.utils import jadx.plugins.kotlin.smap.model.FileMapping import jadx.plugins.kotlin.smap.model.KOTLIN_DEBUG_STRATA_NAME import jadx.plugins.kotlin.smap.model.KOTLIN_STRATA_NAME import jadx.plugins.kotlin.smap.model.SMAP object SMAPParser { fun parseOrNull(mappingInfo: String): SMAP? = if (mappingInfo.isNotEmpty()) { parseStratum(mappingInfo, KOTLIN_STRATA_NAME, parseStratum(mappingInfo, KOTLIN_DEBUG_STRATA_NAME, null)) } else { null } private class SMAPTokenizer(private val text: String, private val headerString: String) : Iterator { private var pos = 0 private var currentLine: String? = null init { advance() while (currentLine != null && currentLine != headerString) { advance() } if (currentLine == headerString) { advance() } } private fun advance() { if (pos >= text.length) { currentLine = null return } val fromPos = pos while (pos < text.length && text[pos] != '\n' && text[pos] != '\r') pos++ currentLine = text.substring(fromPos, pos) pos++ } override fun hasNext(): Boolean { return currentLine != null } override fun next(): String { val res = currentLine ?: throw NoSuchElementException() advance() return res } } private fun parseStratum(mappingInfo: String, stratum: String, callSites: SMAP?): SMAP? { val fileMappings = linkedMapOf() val iterator = SMAPTokenizer(mappingInfo, "${SMAP.STRATA_SECTION} $stratum") // JSR-045 allows the line section to come before the file section, but we don't generate SMAPs like this. if (!iterator.hasNext() || iterator.next() != SMAP.FILE_SECTION) return null for (line in iterator) { when { line == SMAP.LINE_SECTION -> break line == SMAP.FILE_SECTION || line == SMAP.END || line.startsWith(SMAP.STRATA_SECTION) -> return null } val indexAndFileInternalName = if (line.startsWith("+ ")) line.substring(2) else line val fileIndex = indexAndFileInternalName.substringBefore(' ').toInt() val fileName = indexAndFileInternalName.substringAfter(' ') val path = if (line.startsWith("+ ")) iterator.next() else fileName fileMappings[fileIndex] = FileMapping(fileName, path) } for (line in iterator) { when { line == SMAP.LINE_SECTION || line == SMAP.FILE_SECTION -> return null line == SMAP.END || line.startsWith(SMAP.STRATA_SECTION) -> break } // #,:, val fileSeparator = line.indexOf('#') if (fileSeparator < 0) return null val destSeparator = line.indexOf(':', fileSeparator) if (destSeparator < 0) return null val sourceRangeSeparator = line.indexOf(',').let { if (it !in fileSeparator..destSeparator) destSeparator else it } val destMultiplierSeparator = line.indexOf(',', destSeparator).let { if (it < 0) line.length else it } val file = fileMappings[line.substring(fileSeparator + 1, sourceRangeSeparator).toInt()] ?: return null val source = line.substring(0, fileSeparator).toInt() val dest = line.substring(destSeparator + 1, destMultiplierSeparator).toInt() val range = when { // These two fields have a different meaning, but for compatibility we treat them the same. See `SMAPBuilder`. destMultiplierSeparator != line.length -> line.substring(destMultiplierSeparator + 1).toInt() sourceRangeSeparator != destSeparator -> line.substring(sourceRangeSeparator + 1, destSeparator).toInt() else -> 1 } // Here we assume that each range in `Kotlin` is entirely within at most one range in `KotlinDebug`. file.mapNewInterval(source, dest, range, callSites?.findRange(dest)?.let { it.mapDestToSource(it.dest) }) } return SMAP(fileMappings.values.toList()) } } ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.kotlin.smap.KotlinSmapPlugin ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/test/kotlin/TestSourceDebugExtension.kt ================================================ package jadx.plugins.kotlin.metadata.tests import jadx.plugins.kotlin.smap.KotlinSmapOptions.Companion.CLASS_ALIAS_SOURCE_DBG_OPT import jadx.tests.api.SmaliTest import jadx.tests.api.utils.assertj.JadxAssertions.assertThat import jadx.tests.api.utils.assertj.JadxCodeAssertions import org.junit.jupiter.api.Test class TestSourceDebugExtension : SmaliTest() { @Test fun testRenameClass() { setupArgs { this[CLASS_ALIAS_SOURCE_DBG_OPT] = true } assertThatClass() .containsOne("androidx.compose.ui") .containsOne("public final class ActualKt") .countString(1, "reason: from SourceDebugExtension") } private fun setupArgs(builder: MutableMap.() -> Unit = {}) { val allOff = mutableMapOf( CLASS_ALIAS_SOURCE_DBG_OPT to false, ) args.pluginOptions = allOff.apply(builder).mapValues { if (it.value) "yes" else "no" } } private fun assertThatClass(): JadxCodeAssertions = assertThat(getClassNodeFromSmaliFiles("deobf", "TestKotlinSourceDebugExtension", "C6")) .code() } ================================================ FILE: jadx-plugins/jadx-kotlin-source-debug-extension/src/test/smali/deobf/TestKotlinSourceDebugExtension/C6.smali ================================================ .class public final Ldeobf/C6; .super Ljava/lang/Object; .source "SourceFile" # annotations .annotation runtime Lkotlin/Metadata; d1 = { "\u0000\u000e\n\u0002\u0010\u0000\n\u0002\u0008\u0002\n\u0002\u0010\u000b\n\u0000\u001a\u0018\u0010\u0001\u001a\u00020\u00032\u0006\u0010\u0001\u001a\u00020\u00002\u0006\u0010\u0002\u001a\u00020\u0000H\u0000\u00a8\u0006\u0004" } d2 = { "", "a", "b", "", "ui_release" } k = 0x2 mv = { 0x1, 0x8, 0x0 } .end annotation .annotation build Lkotlin/jvm/internal/SourceDebugExtension; value = { "SMAP\nActual.kt\nKotlin\n*S Kotlin\n*F\n+ 1 Actual.kt\nandroidx/compose/ui/ActualKt\n+ 2 _Arrays.kt\nkotlin/collections/ArraysKt___ArraysKt\n+ 3 ListUtils.kt\nandroidx/compose/ui/util/ListUtilsKt\n*L\n1#1,50:1\n6442#2:51\n33#3,6:52\n*S KotlinDebug\n*F\n+ 1 Actual.kt\nandroidx/compose/ui/ActualKt\n*L\n35#1:51\n36#1:52,6\n*E\n" } .end annotation # direct methods .method public static final a(Ljava/lang/Object;Ljava/lang/Object;)Z .locals 1 const-string v0, "a" invoke-static {p0, v0}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V const-string v0, "b" invoke-static {p1, v0}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V invoke-virtual {p0}, Ljava/lang/Object;->getClass()Ljava/lang/Class; move-result-object p0 invoke-virtual {p1}, Ljava/lang/Object;->getClass()Ljava/lang/Class; move-result-object p1 if-ne p0, p1, :cond_0 const/4 p0, 0x1 goto :goto_0 :cond_0 const/4 p0, 0x0 :goto_0 return p0 .end method ================================================ FILE: jadx-plugins/jadx-raung-input/build.gradle.kts ================================================ plugins { id("jadx-library") } dependencies { api(project(":jadx-core")) implementation("io.github.skylot:raung-asm:0.1.1") } ================================================ FILE: jadx-plugins/jadx-raung-input/src/main/java/jadx/plugins/input/raung/RaungConvert.java ================================================ package jadx.plugins.input.raung; import java.io.Closeable; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.github.skylot.raung.asm.RaungAsm; public class RaungConvert implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(RaungConvert.class); @Nullable private Path tmpJar; public boolean execute(List input) { List raungInputs = filterRaungFiles(input); if (raungInputs.isEmpty()) { return false; } try { this.tmpJar = Files.createTempFile("jadx-raung-", ".jar"); RaungAsm.create() .output(tmpJar) .inputs(raungInputs) .execute(); return true; } catch (Exception e) { LOG.error("Raung process error", e); } close(); return false; } private List filterRaungFiles(List input) { PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**.raung"); return input.stream() .filter(matcher::matches) .collect(Collectors.toList()); } public List getFiles() { if (tmpJar == null) { return Collections.emptyList(); } return Collections.singletonList(tmpJar); } @Override public void close() { try { if (tmpJar != null) { Files.deleteIfExists(tmpJar); } } catch (Exception e) { LOG.error("Failed to remove tmp jar file: {}", tmpJar, e); } } } ================================================ FILE: jadx-plugins/jadx-raung-input/src/main/java/jadx/plugins/input/raung/RaungInputPlugin.java ================================================ package jadx.plugins.input.raung; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.data.JadxPluginRuntimeData; import jadx.api.plugins.input.data.impl.EmptyCodeLoader; public class RaungInputPlugin implements JadxPlugin { @Override public JadxPluginInfo getPluginInfo() { return new JadxPluginInfo("raung-input", "Raung Input", "Load .raung files"); } @Override public void init(JadxPluginContext context) { JadxPluginRuntimeData javaInput = context.plugins().getProviding("java-input"); context.addCodeInput(inputs -> { RaungConvert convert = new RaungConvert(); if (!convert.execute(inputs)) { return EmptyCodeLoader.INSTANCE; } return javaInput.loadCodeFiles(convert.getFiles(), convert); }); } } ================================================ FILE: jadx-plugins/jadx-raung-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.input.raung.RaungInputPlugin ================================================ FILE: jadx-plugins/jadx-rename-mappings/build.gradle.kts ================================================ plugins { id("jadx-library") } dependencies { api(project(":jadx-core")) api("net.fabricmc:mapping-io:0.8.0") { exclude("org.ow2.asm:asm") exclude("net.fabricmc:tiny-remapper") } testRuntimeOnly(project(":jadx-plugins:jadx-dex-input")) testRuntimeOnly(project(":jadx-plugins:jadx-smali-input")) } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsData.java ================================================ package jadx.plugins.mappings; import org.jetbrains.annotations.Nullable; import net.fabricmc.mappingio.tree.MappingTreeView; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.dex.nodes.RootNode; public class RenameMappingsData implements IJadxAttribute { private static final IJadxAttrType DATA = IJadxAttrType.create(); public static @Nullable RenameMappingsData getData(RootNode root) { return root.getAttributes().get(DATA); } public static @Nullable MappingTreeView getTree(RootNode root) { RenameMappingsData data = getData(root); return data == null ? null : data.getMappings(); } private final MappingTreeView mappings; public RenameMappingsData(MappingTreeView mappings) { this.mappings = mappings; } public MappingTreeView getMappings() { return mappings; } @Override public IJadxAttrType getAttrType() { return DATA; } } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java ================================================ package jadx.plugins.mappings; import java.util.Locale; import org.jetbrains.annotations.Nullable; import net.fabricmc.mappingio.format.MappingFormat; import jadx.api.plugins.options.OptionFlag; import jadx.api.plugins.options.impl.BasePluginOptionsBuilder; import jadx.core.utils.ListUtils; import static jadx.plugins.mappings.RenameMappingsPlugin.PLUGIN_ID; public class RenameMappingsOptions extends BasePluginOptionsBuilder { public static final String INVERT_OPT = PLUGIN_ID + ".invert"; public static final String FORMAT_OPT = PLUGIN_ID + ".format"; private boolean invert = false; /** * null value - used for 'auto' option */ private @Nullable MappingFormat format = null; @Override public void registerOptions() { option(FORMAT_OPT, MappingFormat.class) .description("mapping format") .parser(RenameMappingsOptions::parseMappingFormat) .formatter(v -> v == null ? "AUTO" : v.name()) .values(ListUtils.concat(null, MappingFormat.values())) .defaultValue(null) .flags(OptionFlag.PER_PROJECT, OptionFlag.DISABLE_IN_GUI) .setter(v -> format = v); boolOption(INVERT_OPT) .description("invert mapping on load") .defaultValue(false) .flags(OptionFlag.PER_PROJECT) .setter(v -> invert = v); } private static MappingFormat parseMappingFormat(String name) { String upName = name.toUpperCase(Locale.ROOT); if (upName.equals("AUTO")) { return null; } return MappingFormat.valueOf(upName); } public @Nullable MappingFormat getFormat() { return format; } public boolean isInvert() { return invert; } public String getOptionsHashString() { return format + ":" + invert; } } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsPlugin.java ================================================ package jadx.plugins.mappings; import java.nio.file.Files; import java.nio.file.Path; import jadx.api.JadxArgs; import jadx.api.args.UserRenamesMappingsMode; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.JadxPluginInfo; import jadx.core.utils.files.FileUtils; import jadx.plugins.mappings.load.ApplyMappingsPass; import jadx.plugins.mappings.load.CodeMappingsPass; import jadx.plugins.mappings.load.LoadMappingsPass; public class RenameMappingsPlugin implements JadxPlugin { public static final String PLUGIN_ID = "rename-mappings"; private final RenameMappingsOptions options = new RenameMappingsOptions(); @Override public JadxPluginInfo getPluginInfo() { return new JadxPluginInfo(PLUGIN_ID, "Rename Mappings", "various mappings support"); } @Override public void init(JadxPluginContext context) { context.registerOptions(options); JadxArgs args = context.getArgs(); if (args.getUserRenamesMappingsMode() == UserRenamesMappingsMode.IGNORE) { return; } Path mappingsPath = args.getUserRenamesMappingsPath(); if (mappingsPath == null || !Files.isReadable(mappingsPath)) { return; } context.addPass(new LoadMappingsPass(options)); context.addPass(new ApplyMappingsPass()); context.addPass(new CodeMappingsPass()); // use mapping file time modification to check for changes context.registerInputsHashSupplier(() -> FileUtils.md5Sum(getInputsHashString(mappingsPath))); } private String getInputsHashString(Path mappingsPath) { return getFileHashString(mappingsPath) + ':' + options.getOptionsHashString(); } private static String getFileHashString(Path mappingsPath) { try { return mappingsPath.toAbsolutePath().normalize() + ":" + Files.getLastModifiedTime(mappingsPath).toMillis(); } catch (Exception e) { return ""; } } } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/ApplyMappingsPass.java ================================================ package jadx.plugins.mappings.load; import net.fabricmc.mappingio.tree.MappingTreeView; import net.fabricmc.mappingio.tree.MappingTreeView.ClassMappingView; import net.fabricmc.mappingio.tree.MappingTreeView.FieldMappingView; import net.fabricmc.mappingio.tree.MappingTreeView.MethodMappingView; import jadx.api.plugins.pass.JadxPassInfo; import jadx.api.plugins.pass.impl.OrderedJadxPassInfo; import jadx.api.plugins.pass.types.JadxPreparePass; import jadx.core.codegen.TypeGen; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.plugins.mappings.RenameMappingsData; public class ApplyMappingsPass implements JadxPreparePass { @Override public JadxPassInfo getInfo() { return new OrderedJadxPassInfo( "ApplyMappings", "Apply mappings to classes, fields and methods") .after("LoadMappings") .before("RenameVisitor"); } @Override public void init(RootNode root) { RenameMappingsData data = RenameMappingsData.getData(root); if (data == null) { return; } MappingTreeView mappingTree = data.getMappings(); process(root, mappingTree); root.registerCodeDataUpdateListener(codeData -> process(root, mappingTree)); } private void process(RootNode root, MappingTreeView mappingTree) { for (ClassNode cls : root.getClasses()) { String clsRawName = cls.getClassInfo().getRawName().replace('.', '/'); ClassMappingView mapping = mappingTree.getClass(clsRawName); if (mapping != null) { processClass(cls, mapping); } } } private static void processClass(ClassNode cls, ClassMappingView classMapping) { String alias = classMapping.getDstName(0); if (alias != null) { cls.rename(alias.replace('/', '.')); } if (classMapping.getComment() != null) { cls.addCodeComment(classMapping.getComment()); } for (FieldNode field : cls.getFields()) { FieldInfo fieldInfo = field.getFieldInfo(); String signature = TypeGen.signature(fieldInfo.getType()); FieldMappingView fieldMapping = classMapping.getField(fieldInfo.getName(), signature); if (fieldMapping != null) { processField(field, fieldMapping); } } for (MethodNode method : cls.getMethods()) { MethodInfo methodInfo = method.getMethodInfo(); String methodName = methodInfo.getName(); String methodDesc = methodInfo.getShortId().substring(methodName.length()); MethodMappingView methodMapping = classMapping.getMethod(methodName, methodDesc); if (methodMapping != null) { processMethod(method, methodMapping); } } } private static void processField(FieldNode field, FieldMappingView fieldMapping) { String alias = fieldMapping.getDstName(0); if (alias != null) { field.rename(alias); } String comment = fieldMapping.getComment(); if (comment != null) { field.addCodeComment(comment); } } private static void processMethod(MethodNode method, MethodMappingView methodMapping) { String alias = methodMapping.getDstName(0); if (alias != null) { method.rename(alias); } String comment = methodMapping.getComment(); if (comment != null) { method.addCodeComment(comment); } // Method args & vars are handled in CodeMappingsPass } } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/CodeMappingsPass.java ================================================ package jadx.plugins.mappings.load; import java.util.HashMap; import java.util.List; import java.util.Map; import net.fabricmc.mappingio.tree.MappingTreeView; import net.fabricmc.mappingio.tree.MappingTreeView.ClassMappingView; import net.fabricmc.mappingio.tree.MappingTreeView.MethodArgMappingView; import net.fabricmc.mappingio.tree.MappingTreeView.MethodMappingView; import jadx.api.plugins.pass.JadxPassInfo; import jadx.api.plugins.pass.impl.OrderedJadxPassInfo; import jadx.api.plugins.pass.types.JadxDecompilePass; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.plugins.mappings.RenameMappingsData; import jadx.plugins.mappings.utils.DalvikToJavaBytecodeUtils; public class CodeMappingsPass implements JadxDecompilePass { private Map clsRenamesMap; @Override public JadxPassInfo getInfo() { return new OrderedJadxPassInfo( "CodeMappings", "Apply mappings to method args and vars") .before("CodeRenameVisitor"); } @Override public void init(RootNode root) { RenameMappingsData data = RenameMappingsData.getData(root); if (data == null) { return; } MappingTreeView mappingTree = data.getMappings(); updateMappingsMap(mappingTree); root.registerCodeDataUpdateListener(codeData -> updateMappingsMap(mappingTree)); } @Override public boolean visit(ClassNode cls) { ClassMappingView classMapping = getMapping(cls); if (classMapping != null) { applyRenames(cls, classMapping); } cls.getInnerClasses().forEach(this::visit); return false; } @Override public void visit(MethodNode mth) { } private static void applyRenames(ClassNode cls, ClassMappingView classMapping) { for (MethodNode mth : cls.getMethods()) { String methodName = mth.getMethodInfo().getName(); String methodDesc = mth.getMethodInfo().getShortId().substring(methodName.length()); List ssaVars = mth.getSVars(); if (ssaVars.isEmpty()) { continue; } MethodMappingView methodMapping = classMapping.getMethod(methodName, methodDesc); if (methodMapping == null) { continue; } // Method args for (MethodArgMappingView argMapping : methodMapping.getArgs()) { Integer mappingLvIndex = argMapping.getLvIndex(); for (SSAVar ssaVar : ssaVars) { Integer actualLvIndex = DalvikToJavaBytecodeUtils.getMethodArgLvIndex(ssaVar, mth); if (actualLvIndex.equals(mappingLvIndex)) { ssaVar.getCodeVar().setName(argMapping.getDstName(0)); break; } } } // TODO: Method vars (if ever feasible) } } private ClassMappingView getMapping(ClassNode cls) { if (clsRenamesMap == null || clsRenamesMap.isEmpty()) { return null; } String classPath = cls.getClassInfo().makeRawFullName().replace('.', '/'); return clsRenamesMap.get(classPath); } private void updateMappingsMap(MappingTreeView mappings) { clsRenamesMap = new HashMap<>(); for (ClassMappingView cls : mappings.getClasses()) { for (MethodMappingView mth : cls.getMethods()) { if (!mth.getArgs().isEmpty() || !mth.getVars().isEmpty()) { clsRenamesMap.put(cls.getSrcName(), cls); break; } } } } } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/load/LoadMappingsPass.java ================================================ package jadx.plugins.mappings.load; import java.nio.file.Path; import java.util.Collections; import net.fabricmc.mappingio.MappingReader; import net.fabricmc.mappingio.MappingUtil; import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; import net.fabricmc.mappingio.tree.MappingTreeView; import net.fabricmc.mappingio.tree.MemoryMappingTree; import net.fabricmc.mappingio.tree.VisitableMappingTree; import jadx.api.JadxArgs; import jadx.api.plugins.pass.JadxPassInfo; import jadx.api.plugins.pass.impl.SimpleJadxPassInfo; import jadx.api.plugins.pass.types.JadxPreparePass; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.plugins.mappings.RenameMappingsData; import jadx.plugins.mappings.RenameMappingsOptions; public class LoadMappingsPass implements JadxPreparePass { private final RenameMappingsOptions options; public LoadMappingsPass(RenameMappingsOptions options) { this.options = options; } @Override public JadxPassInfo getInfo() { return new SimpleJadxPassInfo("LoadMappings", "Load mappings file"); } @Override public void init(RootNode root) { MappingTreeView mappings = loadMapping(root.getArgs()); root.getAttributes().add(new RenameMappingsData(mappings)); } private MappingTreeView loadMapping(JadxArgs args) { try { Path mappingsPath = args.getUserRenamesMappingsPath(); VisitableMappingTree mappingTree = new MemoryMappingTree(); MappingReader.read(mappingsPath, options.getFormat(), mappingTree); if (mappingTree.getSrcNamespace() == null) { mappingTree.setSrcNamespace(MappingUtil.NS_SOURCE_FALLBACK); } if (mappingTree.getDstNamespaces() == null || mappingTree.getDstNamespaces().isEmpty()) { mappingTree.setDstNamespaces(Collections.singletonList(MappingUtil.NS_TARGET_FALLBACK)); } else if (mappingTree.getDstNamespaces().size() > 1) { throw new JadxRuntimeException( String.format("JADX only supports mappings with just one destination namespace! The provided ones have %s.", mappingTree.getDstNamespaces().size())); } if (options.isInvert()) { VisitableMappingTree invertedMappingTree = new MemoryMappingTree(); String dstNamespace = mappingTree.getDstNamespaces().get(0); mappingTree.accept(new MappingSourceNsSwitch(invertedMappingTree, dstNamespace)); return invertedMappingTree; } return mappingTree; } catch (Exception e) { throw new JadxRuntimeException("Failed to load mappings", e); } } } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/save/MappingExporter.java ================================================ package jadx.plugins.mappings.save; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.fabricmc.mappingio.MappedElementKind; import net.fabricmc.mappingio.MappingUtil; import net.fabricmc.mappingio.MappingWriter; import net.fabricmc.mappingio.format.MappingFormat; import net.fabricmc.mappingio.tree.MappingTreeView; import net.fabricmc.mappingio.tree.MemoryMappingTree; import net.fabricmc.mappingio.tree.VisitOrder; import net.fabricmc.mappingio.tree.VisitableMappingTree; import jadx.api.data.ICodeComment; import jadx.api.data.ICodeRename; import jadx.api.data.IJavaNodeRef.RefType; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxCodeRef; import jadx.api.metadata.annotations.VarNode; import jadx.core.Consts; import jadx.core.codegen.TypeGen; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.files.FileUtils; import jadx.plugins.mappings.RenameMappingsData; import jadx.plugins.mappings.utils.DalvikToJavaBytecodeUtils; import jadx.plugins.mappings.utils.VariablesUtils; public class MappingExporter { private static final Logger LOG = LoggerFactory.getLogger(MappingExporter.class); private final RootNode root; private final @Nullable MappingTreeView loadedMappingTree; public MappingExporter(RootNode root) { this.root = root; this.loadedMappingTree = RenameMappingsData.getTree(this.root); } public void exportMappings(Path path, JadxCodeData codeData, MappingFormat mappingFormat) { VisitableMappingTree mappingTree = new MemoryMappingTree(); // Map < SrcName > Set mappedClasses = new HashSet<>(); // Map < DeclClass + ShortId > Set mappedFields = new HashSet<>(); Set mappedMethods = new HashSet<>(); Set methodsWithMappedElements = new HashSet<>(); // Map < DeclClass + MethodShortId + CodeRef, NewName > Map mappedMethodArgsAndVars = new HashMap<>(); // Map < DeclClass [+ ShortId] [+ CodeRef], Comment > Map comments = new HashMap<>(); // We have to do this so we know for sure which elements are *manually* renamed for (ICodeRename codeRename : codeData.getRenames()) { if (codeRename.getNodeRef().getType().equals(RefType.CLASS)) { mappedClasses.add(codeRename.getNodeRef().getDeclaringClass()); } else if (codeRename.getNodeRef().getType().equals(RefType.FIELD)) { mappedFields.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId()); } else if (codeRename.getNodeRef().getType().equals(RefType.METHOD)) { if (codeRename.getCodeRef() == null) { mappedMethods.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId()); } else { methodsWithMappedElements.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId()); mappedMethodArgsAndVars.put(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId() + codeRename.getCodeRef(), codeRename.getNewName()); } } } for (ICodeComment codeComment : codeData.getComments()) { comments.put(codeComment.getNodeRef().getDeclaringClass() + (codeComment.getNodeRef().getShortId() == null ? "" : codeComment.getNodeRef().getShortId()) + (codeComment.getCodeRef() == null ? "" : codeComment.getCodeRef()), codeComment.getComment()); if (codeComment.getCodeRef() != null) { methodsWithMappedElements.add(codeComment.getNodeRef().getDeclaringClass() + codeComment.getNodeRef().getShortId()); } } try { String srcNamespace = MappingUtil.NS_SOURCE_FALLBACK; String dstNamespace = MappingUtil.NS_TARGET_FALLBACK; // Copy mappings from potentially imported mappings file if (loadedMappingTree != null && loadedMappingTree.getDstNamespaces() != null) { loadedMappingTree.accept(mappingTree); } mappingTree.visitHeader(); mappingTree.visitNamespaces(srcNamespace, Collections.singletonList(dstNamespace)); mappingTree.visitContent(); for (ClassNode cls : root.getClasses()) { ClassInfo classInfo = cls.getClassInfo(); String classPath = classInfo.makeRawFullName().replace('.', '/'); String rawClassName = classInfo.getRawName(); if (classInfo.hasAlias() && !classInfo.getAliasShortName().equals(classInfo.getShortName()) && mappedClasses.contains(rawClassName)) { mappingTree.visitClass(classPath); String alias = classInfo.makeAliasRawFullName().replace('.', '/'); if (alias.startsWith(Consts.DEFAULT_PACKAGE_NAME)) { alias = alias.substring(Consts.DEFAULT_PACKAGE_NAME.length() + 1); } mappingTree.visitDstName(MappedElementKind.CLASS, 0, alias); } if (comments.containsKey(rawClassName)) { mappingTree.visitClass(classPath); mappingTree.visitComment(MappedElementKind.CLASS, comments.get(rawClassName)); } for (FieldNode fld : cls.getFields()) { FieldInfo fieldInfo = fld.getFieldInfo(); if (fieldInfo.hasAlias() && mappedFields.contains(rawClassName + fieldInfo.getShortId())) { visitField(mappingTree, classPath, fieldInfo.getName(), TypeGen.signature(fieldInfo.getType())); mappingTree.visitDstName(MappedElementKind.FIELD, 0, fieldInfo.getAlias()); } if (comments.containsKey(rawClassName + fieldInfo.getShortId())) { visitField(mappingTree, classPath, fieldInfo.getName(), TypeGen.signature(fieldInfo.getType())); mappingTree.visitComment(MappedElementKind.FIELD, comments.get(rawClassName + fieldInfo.getShortId())); } } for (MethodNode mth : cls.getMethods()) { MethodInfo methodInfo = mth.getMethodInfo(); String methodName = methodInfo.getName(); String methodDesc = methodInfo.getShortId().substring(methodName.length()); if (methodInfo.hasAlias() && mappedMethods.contains(rawClassName + methodInfo.getShortId())) { visitMethod(mappingTree, classPath, methodName, methodDesc); mappingTree.visitDstName(MappedElementKind.METHOD, 0, methodInfo.getAlias()); } if (comments.containsKey(rawClassName + methodInfo.getShortId())) { visitMethod(mappingTree, classPath, methodName, methodDesc); mappingTree.visitComment(MappedElementKind.METHOD, comments.get(rawClassName + methodInfo.getShortId())); } if (!methodsWithMappedElements.contains(rawClassName + methodInfo.getShortId())) { continue; } // Method args int lvtIndex = mth.getAccessFlags().isStatic() ? 0 : 1; List args = mth.collectArgNodes(); for (VarNode arg : args) { Integer lvIndex = DalvikToJavaBytecodeUtils.getMethodArgLvIndex(arg); if (lvIndex == null) { lvIndex = -1; } String key = rawClassName + methodInfo.getShortId() + JadxCodeRef.forVar(arg.getReg(), arg.getSsa()); if (mappedMethodArgsAndVars.containsKey(key)) { visitMethodArg(mappingTree, classPath, methodName, methodDesc, args.indexOf(arg), lvIndex); mappingTree.visitDstName(MappedElementKind.METHOD_ARG, 0, mappedMethodArgsAndVars.get(key)); mappedMethodArgsAndVars.remove(key); } lvtIndex++; // Not checking for comments since method args can't have any } // Method vars for (VariablesUtils.VarInfo info : VariablesUtils.collect(mth)) { VarNode var = info.getVar(); int startOpIdx = info.getStartOpIdx(); int endOpIdx = info.getEndOpIdx(); int lvIndex = DalvikToJavaBytecodeUtils.getMethodVarLvIndex(var); String key = rawClassName + methodInfo.getShortId() + JadxCodeRef.forVar(var.getReg(), var.getSsa()); if (mappedMethodArgsAndVars.containsKey(key)) { visitMethodVar(mappingTree, classPath, methodName, methodDesc, lvtIndex, lvIndex, startOpIdx, endOpIdx); mappingTree.visitDstName(MappedElementKind.METHOD_VAR, 0, mappedMethodArgsAndVars.get(key)); } key = rawClassName + methodInfo.getShortId() + JadxCodeRef.forInsn(startOpIdx); if (comments.containsKey(key)) { visitMethodVar(mappingTree, classPath, methodName, methodDesc, lvtIndex, lvIndex, startOpIdx, endOpIdx); mappingTree.visitComment(MappedElementKind.METHOD_VAR, comments.get(key)); } lvtIndex++; } } } // write file as late as possible because a mapping collection can fail with exception if (mappingFormat.hasSingleFile()) { FileUtils.deleteFileIfExists(path); FileUtils.makeDirsForFile(path); Files.createFile(path); } else { FileUtils.makeDirs(path); } // Write file mappingTree.accept(MappingWriter.create(path, mappingFormat), VisitOrder.createByName()); mappingTree.visitEnd(); } catch (Exception e) { LOG.error("Failed to save deobfuscation map file '{}'", path.toAbsolutePath(), e); } } private void visitField(VisitableMappingTree tree, String classPath, String srcName, String srcDesc) throws IOException { tree.visitClass(classPath); tree.visitField(srcName, srcDesc); } private void visitMethod(VisitableMappingTree tree, String classPath, String srcName, String srcDesc) throws IOException { tree.visitClass(classPath); tree.visitMethod(srcName, srcDesc); } private void visitMethodArg(VisitableMappingTree tree, String classPath, String methodSrcName, String methodSrcDesc, int argPosition, int lvIndex) throws IOException { visitMethod(tree, classPath, methodSrcName, methodSrcDesc); tree.visitMethodArg(argPosition, lvIndex, null); } private void visitMethodVar(VisitableMappingTree tree, String classPath, String methodSrcName, String methodSrcDesc, int lvtIndex, int lvIndex, int startOpIdx, int endOpIdx) throws IOException { visitMethod(tree, classPath, methodSrcName, methodSrcDesc); tree.visitMethodVar(lvtIndex, lvIndex, startOpIdx, endOpIdx, null); } } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/utils/DalvikToJavaBytecodeUtils.java ================================================ package jadx.plugins.mappings.utils; import java.util.ArrayList; import java.util.List; import jadx.api.metadata.annotations.VarNode; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.MethodNode; public class DalvikToJavaBytecodeUtils { // **************************** // Local variable index // **************************** // Method args public static Integer getMethodArgLvIndex(VarNode methodArg) { MethodNode mth = methodArg.getMth(); Integer lvIndex = getMethodArgLvIndexViaSsaVars(methodArg.getReg(), mth); if (lvIndex != null) { return lvIndex; } List args = mth.collectArgNodes(); for (VarNode arg : args) { lvIndex = arg.getReg() - args.get(0).getReg() + (mth.getAccessFlags().isStatic() ? 0 : 1); if (arg.equals(methodArg)) { break; } } return lvIndex; } public static Integer getMethodArgLvIndex(SSAVar methodArgSsaVar, MethodNode mth) { return getMethodArgLvIndexViaSsaVars(methodArgSsaVar.getRegNum(), mth); } private static Integer getMethodArgLvIndexViaSsaVars(int regNum, MethodNode mth) { List ssaVars = mth.getSVars(); if (!ssaVars.isEmpty()) { return regNum - ssaVars.get(0).getRegNum(); } return null; } // Method vars public static int getMethodVarLvIndex(VarNode methodVar) { MethodNode mth = methodVar.getMth(); Integer lvIndex = getMethodVarLvIndexViaSsaVars(methodVar.getReg(), mth); if (lvIndex != null) { return lvIndex; } Integer lastArgLvIndex = mth.getAccessFlags().isStatic() ? -1 : 0; List args = mth.collectArgNodes(); if (!args.isEmpty()) { lastArgLvIndex = getMethodArgLvIndex(args.get(args.size() - 1)); } return lastArgLvIndex + methodVar.getReg() + (mth.getAccessFlags().isStatic() ? 0 : 1); } public static Integer getMethodVarLvIndex(SSAVar methodVarSsaVar, MethodNode mth) { return getMethodVarLvIndexViaSsaVars(methodVarSsaVar.getRegNum(), mth); } private static Integer getMethodVarLvIndexViaSsaVars(int regNum, MethodNode mth) { List ssaVars = mth.getSVars(); if (ssaVars.isEmpty()) { return null; } Integer lastArgLvIndex = mth.getAccessFlags().isStatic() ? -1 : 0; List args = mth.getArgRegs(); if (!args.isEmpty()) { lastArgLvIndex = getMethodArgLvIndexViaSsaVars(args.get(args.size() - 1).getSVar().getRegNum(), mth); } return lastArgLvIndex + regNum + (mth.getAccessFlags().isStatic() ? 0 : 1); } // **************************** // Local variable table index // **************************** // Method args public static Integer getMethodArgLvtIndex(VarNode methodArg) { MethodNode mth = methodArg.getMth(); int lvtIndex = mth.getAccessFlags().isStatic() ? 0 : 1; List args = mth.collectArgNodes(); for (VarNode arg : args) { if (arg.equals(methodArg)) { return lvtIndex; } lvtIndex++; } return null; } public static Integer getMethodArgLvtIndex(SSAVar methodArgSsaVar, MethodNode mth) { List ssaVars = mth.getSVars(); if (ssaVars.isEmpty()) { return null; } List args = mth.getArgRegs(); int lvtIndex = mth.getAccessFlags().isStatic() ? 0 : 1; for (RegisterArg arg : args) { if (arg.getSVar().equals(methodArgSsaVar)) { return lvtIndex; } lvtIndex++; } return null; } // Method vars // TODO: public static Integer getMethodVarLvtIndex(VarNode methodVar) {} public static Integer getMethodVarLvtIndex(SSAVar methodVarSsaVar, MethodNode mth) { List ssaVars = new ArrayList<>(mth.getSVars()); if (ssaVars.isEmpty()) { return null; } Integer lvtIndex = getMethodArgLvtIndex(methodVarSsaVar, mth); if (lvtIndex != null) { return lvtIndex; } lvtIndex = mth.getAccessFlags().isStatic() ? 0 : 1; lvtIndex += mth.getArgTypes().size(); lvtIndex = getMethodArgLvtIndex(methodVarSsaVar, mth) + 1; ssaVars.subList(0, ssaVars.indexOf(methodVarSsaVar) + 1).clear(); int lastRegNum = -1; for (SSAVar ssaVar : ssaVars) { if (ssaVar.getRegNum() == lastRegNum) { // Not present in bytecode // System.out.println("Duplicate RegNum: " + ssaVar.getRegNum()); continue; } lvtIndex++; if (ssaVar.equals(methodVarSsaVar)) { return lvtIndex; } lastRegNum = ssaVar.getRegNum(); } return null; } } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/utils/VariablesUtils.java ================================================ package jadx.plugins.mappings.utils; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.api.metadata.annotations.VarNode; import jadx.api.utils.CodeUtils; import jadx.core.dex.nodes.MethodNode; public class VariablesUtils { private static final Logger LOG = LoggerFactory.getLogger(VariablesUtils.class); public static class VarInfo { private final VarNode var; private final int startOpIdx; private int endOpIdx; public VarInfo(VarNode var, int startOpIdx) { this.var = var; this.startOpIdx = startOpIdx; this.endOpIdx = startOpIdx; } public VarNode getVar() { return var; } public int getStartOpIdx() { return startOpIdx; } public int getEndOpIdx() { return endOpIdx; } public void setEndOpIdx(int endOpIdx) { this.endOpIdx = endOpIdx; } } public static List collect(MethodNode mth) { ICodeInfo codeInfo = mth.getTopParentClass().getCode(); int mthDefPos = mth.getDefPosition(); int mthLineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos); CodeVisitor codeVisitor = new CodeVisitor(mth); codeInfo.getCodeMetadata().searchDown(mthLineEndPos, codeVisitor::process); return codeVisitor.getVars(); } private static class CodeVisitor { private final MethodNode mth; private final List vars = new ArrayList<>(); private int lastOffset = -1; public CodeVisitor(MethodNode mth) { this.mth = mth; } public @Nullable Boolean process(Integer pos, ICodeAnnotation ann) { if (ann instanceof InsnCodeOffset) { lastOffset = ((InsnCodeOffset) ann).getOffset(); } if (ann instanceof NodeDeclareRef) { ICodeNodeRef declRef = ((NodeDeclareRef) ann).getNode(); if (declRef instanceof VarNode) { VarNode varNode = (VarNode) declRef; if (!varNode.getMth().equals(mth)) { // Stop if we've gone too far and have entered a different method if (!vars.isEmpty()) { vars.get(vars.size() - 1).setEndOpIdx(declRef.getDefPosition() - 1); } return Boolean.TRUE; } if (lastOffset != -1) { if (!vars.isEmpty()) { vars.get(vars.size() - 1).setEndOpIdx(lastOffset - 1); } vars.add(new VarInfo(varNode, lastOffset)); } else { LOG.warn("Local variable not present in bytecode, skipping: {}#{}", mth.getMethodInfo().getRawFullId(), varNode.getName()); } lastOffset = -1; } } return null; } public List getVars() { return vars; } } } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.mappings.RenameMappingsPlugin ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/test/java/jadx/plugins/mappings/BaseRenameMappingsTest.java ================================================ package jadx.plugins.mappings; import java.io.File; import java.net.URL; import java.nio.file.Path; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.api.JavaClass; import jadx.api.plugins.loader.JadxBasePluginLoader; import jadx.core.plugins.files.SingleDirFilesGetter; import static org.assertj.core.api.Assertions.assertThat; public class BaseRenameMappingsTest { private static final Logger LOG = LoggerFactory.getLogger(BaseRenameMappingsTest.class); @TempDir Path testDir; Path outputDir; JadxArgs jadxArgs; String testResDir = ""; @BeforeEach public void setUp() { outputDir = testDir.resolve("output"); jadxArgs = new JadxArgs(); jadxArgs.setOutDir(outputDir.toFile()); jadxArgs.setFilesGetter(new SingleDirFilesGetter(testDir)); jadxArgs.setPluginLoader(new JadxBasePluginLoader()); } public File loadResourceFile(String fileName) { String path = testResDir + '/' + fileName; try { URL resource = getClass().getClassLoader().getResource(path); assertThat(resource).isNotNull(); return new File(resource.getFile()); } catch (Exception e) { throw new RuntimeException("Failed to load resource file: " + path, e); } } public void printClassesCode(List classes) { LOG.debug("Printing code for {} classes:", classes.size()); for (JavaClass jCls : classes) { LOG.debug("Class: {}\n{}\n---\n", jCls.getFullName(), jCls.getCode()); } } } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/test/java/jadx/plugins/mappings/TestInnerClassRename.java ================================================ package jadx.plugins.mappings; import java.util.List; import org.junit.jupiter.api.Test; import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import static org.assertj.core.api.Assertions.assertThat; class TestInnerClassRename extends BaseRenameMappingsTest { @Test public void test() { testResDir = "inner-cls-rename"; jadxArgs.getInputFiles().add(loadResourceFile("base.smali")); jadxArgs.getInputFiles().add(loadResourceFile("inner.smali")); jadxArgs.setUserRenamesMappingsPath(loadResourceFile("enigma.mapping").toPath()); try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) { jadx.load(); List classes = jadx.getClasses(); printClassesCode(classes); assertThat(classes).hasSize(1); JavaClass baseCls = classes.get(0); assertThat(baseCls.getName()).isEqualTo("BaseCls"); List innerClasses = baseCls.getInnerClasses(); assertThat(innerClasses).hasSize(1); assertThat(innerClasses.get(0).getName()).isEqualTo("RenamedInner"); assertThat(baseCls.getCode()).contains("class RenamedInner {"); } } } ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/test/resources/inner-cls-rename/base.smali ================================================ .class Ljadx/test/BaseCls; .super Ljava/lang/Object; ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/test/resources/inner-cls-rename/enigma.mapping ================================================ CLASS jadx/test/BaseCls CLASS Inner RenamedInner ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/test/resources/inner-cls-rename/inner.smali ================================================ .class Ljadx/test/BaseCls$Inner; .super Ljava/lang/Object; ================================================ FILE: jadx-plugins/jadx-rename-mappings/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss} %-5level - %msg%n ================================================ FILE: jadx-plugins/jadx-smali-input/build.gradle.kts ================================================ plugins { id("jadx-library") } dependencies { api(project(":jadx-core")) implementation(project(":jadx-plugins:jadx-dex-input")) implementation("com.android.tools.smali:smali:3.0.9") { exclude(group = "com.beust", module = "jcommander") // exclude old jcommander namespace } implementation("com.google.guava:guava:33.5.0-jre") // force the latest version for smali } ================================================ FILE: jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliConvert.java ================================================ package jadx.plugins.input.smali; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.android.tools.smali.smali.SmaliOptions; import jadx.plugins.input.dex.utils.IDexData; import jadx.plugins.input.dex.utils.SimpleDexData; public class SmaliConvert { private static final Logger LOG = LoggerFactory.getLogger(SmaliConvert.class); private final List dexData = new ArrayList<>(); public boolean execute(List input, SmaliInputOptions options) { List smaliFiles = filterSmaliFiles(input); if (smaliFiles.isEmpty()) { return false; } try { compile(smaliFiles, options); } catch (Exception e) { LOG.error("Smali process error", e); } return !dexData.isEmpty(); } @SuppressWarnings("ResultOfMethodCallIgnored") private void compile(List inputFiles, SmaliInputOptions options) { SmaliOptions smaliOptions = new SmaliOptions(); smaliOptions.apiLevel = options.getApiLevel(); smaliOptions.verboseErrors = true; smaliOptions.allowOdexOpcodes = false; smaliOptions.printTokens = false; int threads = options.getThreads(); LOG.debug("Compiling smali files: {}, threads: {}", inputFiles.size(), threads); long start = System.currentTimeMillis(); if (threads == 1 || inputFiles.size() == 1) { for (Path inputFile : inputFiles) { assemble(dexData, inputFile, smaliOptions); } } else { try { ExecutorService executor = Executors.newFixedThreadPool(threads); List syncList = Collections.synchronizedList(dexData); for (Path inputFile : inputFiles) { executor.execute(() -> assemble(syncList, inputFile, smaliOptions)); } executor.shutdown(); executor.awaitTermination(1, TimeUnit.HOURS); dexData.sort(Comparator.comparing(IDexData::getFileName)); } catch (InterruptedException e) { LOG.error("Smali compile interrupted", e); } } if (LOG.isDebugEnabled()) { LOG.debug("Smali compile done in: {}ms", System.currentTimeMillis() - start); } } private void assemble(List results, Path inputFile, SmaliOptions smaliOptions) { Path path = inputFile.toAbsolutePath(); try { byte[] dexContent = SmaliUtils.assemble(path.toFile(), smaliOptions); results.add(new SimpleDexData(path.toString(), dexContent)); } catch (Exception e) { LOG.error("Failed to assemble smali file: {}", path, e); } } private List filterSmaliFiles(List input) { PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**.smali"); return input.stream() .filter(matcher::matches) .collect(Collectors.toList()); } public List getDexData() { return dexData; } } ================================================ FILE: jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputOptions.java ================================================ package jadx.plugins.input.smali; import jadx.api.plugins.options.impl.BasePluginOptionsBuilder; public class SmaliInputOptions extends BasePluginOptionsBuilder { private int apiLevel; private int threads; // use jadx global threads count option @Override public void registerOptions() { intOption(SmaliInputPlugin.PLUGIN_ID + ".api-level") .description("Android API level") .defaultValue(27) .setter(v -> apiLevel = v); } public int getApiLevel() { return apiLevel; } public int getThreads() { return threads; } public void setThreads(int threads) { this.threads = threads; } } ================================================ FILE: jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java ================================================ package jadx.plugins.input.smali; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.input.data.impl.EmptyCodeLoader; import jadx.plugins.input.dex.DexInputPlugin; public class SmaliInputPlugin implements JadxPlugin { public static final String PLUGIN_ID = "smali-input"; private final SmaliInputOptions options = new SmaliInputOptions(); @Override public JadxPluginInfo getPluginInfo() { return new JadxPluginInfo(PLUGIN_ID, "Smali Input", "Load .smali files"); } @Override public void init(JadxPluginContext context) { context.registerOptions(options); options.setThreads(context.getArgs().getThreadsCount()); DexInputPlugin dexInput = context.plugins().getInstance(DexInputPlugin.class); context.addCodeInput(input -> { SmaliConvert convert = new SmaliConvert(); if (!convert.execute(input, options)) { return EmptyCodeLoader.INSTANCE; } return dexInput.loadDexData(convert.getDexData()); }); } } ================================================ FILE: jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliUtils.java ================================================ package jadx.plugins.input.smali; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.RecognitionException; import org.antlr.runtime.TokenStream; import org.antlr.runtime.tree.CommonTreeNodeStream; import org.antlr.runtime.tree.TreeNodeStream; import com.android.tools.smali.dexlib2.Opcodes; import com.android.tools.smali.dexlib2.writer.builder.DexBuilder; import com.android.tools.smali.dexlib2.writer.io.MemoryDataStore; import com.android.tools.smali.smali.SmaliOptions; import com.android.tools.smali.smali.smaliFlexLexer; import com.android.tools.smali.smali.smaliParser; import com.android.tools.smali.smali.smaliTreeWalker; /** * Utility methods to assemble smali to in-memory buffer. * This implementation uses smali library internal classes. */ public class SmaliUtils { @SuppressWarnings("ExtractMethodRecommender") public static byte[] assemble(File smaliFile, SmaliOptions options) throws IOException { StringBuilder errors = new StringBuilder(); try (FileInputStream fis = new FileInputStream(smaliFile); InputStreamReader reader = new InputStreamReader(fis, StandardCharsets.UTF_8)) { smaliFlexLexer lexer = new smaliFlexLexer(reader, options.apiLevel); lexer.setSourceFile(smaliFile); CommonTokenStream tokens = new CommonTokenStream(lexer); ParserWrapper parser = new ParserWrapper(tokens, errors); parser.setVerboseErrors(options.verboseErrors); parser.setAllowOdex(options.allowOdexOpcodes); parser.setApiLevel(options.apiLevel); ParserWrapper.smali_file_return parseResult = parser.smali_file(); if (parser.getNumberOfSyntaxErrors() > 0 || lexer.getNumberOfSyntaxErrors() > 0) { throw new RuntimeException("Smali parse error: " + errors); } CommonTreeNodeStream treeStream = new CommonTreeNodeStream(parseResult.getTree()); treeStream.setTokenStream(tokens); DexBuilder dexBuilder = new DexBuilder(Opcodes.forApi(options.apiLevel)); TreeWalkerWrapper dexGen = new TreeWalkerWrapper(treeStream, errors); dexGen.setApiLevel(options.apiLevel); dexGen.setVerboseErrors(options.verboseErrors); dexGen.setDexBuilder(dexBuilder); dexGen.smali_file(); if (dexGen.getNumberOfSyntaxErrors() > 0) { throw new RuntimeException("Smali compile error: " + errors); } MemoryDataStore dataStore = new MemoryDataStore(); dexBuilder.writeTo(dataStore); return dataStore.getData(); } catch (RecognitionException e) { throw new RuntimeException("Smali process error: " + errors, e); } } private static final class ParserWrapper extends smaliParser { private final StringBuilder errors; public ParserWrapper(TokenStream input, StringBuilder errors) { super(input); this.errors = errors; } @Override public void emitErrorMessage(String msg) { errors.append('\n').append(msg); } } private static final class TreeWalkerWrapper extends smaliTreeWalker { private final StringBuilder errors; public TreeWalkerWrapper(TreeNodeStream input, StringBuilder errors) { super(input); this.errors = errors; } @Override public void emitErrorMessage(String msg) { errors.append('\n').append(msg); } } } ================================================ FILE: jadx-plugins/jadx-smali-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.input.smali.SmaliInputPlugin ================================================ FILE: jadx-plugins/jadx-xapk-input/build.gradle.kts ================================================ plugins { id("jadx-library") } dependencies { api(project(":jadx-core")) implementation(project(":jadx-plugins:jadx-dex-input")) implementation("com.google.code.gson:gson:2.13.2") } ================================================ FILE: jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkCustomInput.java ================================================ package jadx.plugins.input.xapk; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import jadx.api.ResourceFile; import jadx.api.ResourcesLoader; import jadx.api.plugins.CustomResourcesLoader; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.input.data.impl.EmptyCodeLoader; import jadx.plugins.input.dex.DexInputPlugin; import jadx.plugins.input.xapk.data.XApkData; public class XApkCustomInput implements JadxCodeInput, CustomResourcesLoader { private final JadxPluginContext context; private final XApkLoader loader; public XApkCustomInput(JadxPluginContext context, XApkLoader loader) { this.context = context; this.loader = loader; } @Override public ICodeLoader loadFiles(List input) { List apks = new ArrayList<>(); for (Path inputPath : input) { XApkData data = loader.checkAndLoad(inputPath); if (data != null) { apks.addAll(data.getApks()); } } if (apks.isEmpty()) { return EmptyCodeLoader.INSTANCE; } DexInputPlugin dexInputPlugin = context.plugins().getInstance(DexInputPlugin.class); return dexInputPlugin.loadFiles(apks); } @Override public boolean load(ResourcesLoader resLoader, List list, File file) { XApkData xApkData = loader.checkAndLoad(file.toPath()); if (xApkData == null) { return false; } for (Path apkPath : xApkData.getApks()) { resLoader.defaultLoadFile(list, apkPath.toFile(), apkPath.getFileName() + "/"); } for (Path filePath : xApkData.getFiles()) { resLoader.defaultLoadFile(list, filePath.toFile(), ""); } return true; } @Override public void close() throws IOException { } } ================================================ FILE: jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkInputPlugin.java ================================================ package jadx.plugins.input.xapk; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.JadxPluginInfoBuilder; public class XApkInputPlugin implements JadxPlugin { private XApkLoader loader; @Override public JadxPluginInfo getPluginInfo() { return JadxPluginInfoBuilder.pluginId("xapk-input") .name("XApk Input") .description("Load .xapk files") .build(); } @Override public void init(JadxPluginContext context) { loader = new XApkLoader(context); XApkCustomInput customInput = new XApkCustomInput(context, loader); context.addCodeInput(customInput); context.getDecompiler().addCustomResourcesLoader(customInput); } @Override public void unload() { if (loader != null) { loader.unload(); } } } ================================================ FILE: jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkLoader.java ================================================ package jadx.plugins.input.xapk; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.JadxPluginContext; import jadx.core.utils.GsonUtils; import jadx.core.utils.files.FileUtils; import jadx.plugins.input.xapk.data.SplitApk; import jadx.plugins.input.xapk.data.XApkData; import jadx.plugins.input.xapk.data.XApkManifest; import jadx.zip.IZipEntry; import jadx.zip.ZipContent; public class XApkLoader { private static final Logger LOG = LoggerFactory.getLogger(XApkLoader.class); private final JadxPluginContext context; private final Map loaded = new HashMap<>(); public XApkLoader(JadxPluginContext context) { this.context = context; } public @Nullable XApkData checkAndLoad(Path inputPath) { String fileName = inputPath.getFileName().toString(); if (!fileName.toLowerCase(Locale.ROOT).endsWith(".xapk")) { return null; } try { XApkData loadedData = getLoaded(inputPath); if (loadedData != null) { return loadedData; } File xapkFile = inputPath.toFile(); if (!FileUtils.isZipFile(xapkFile)) { return null; } try (ZipContent content = context.getZipReader().open(xapkFile)) { IZipEntry manifestEntry = content.searchEntry("manifest.json"); if (manifestEntry == null) { return null; } String manifestStr = new String(manifestEntry.getBytes(), StandardCharsets.UTF_8); XApkManifest xApkManifest = GsonUtils.buildGson().fromJson(manifestStr, XApkManifest.class); if (xApkManifest.getVersion() != 2 || xApkManifest.getSplitApks().isEmpty()) { return null; } // checks complete // unpack all files into temp directory XApkData xApkData = unpackXApk(xapkFile, xApkManifest, content); saveLoaded(inputPath, xApkData); return xApkData; } } catch (Exception e) { LOG.warn("Failed to load XApk file: {}", inputPath.toAbsolutePath(), e); return null; } } private XApkData unpackXApk(File xapkFile, XApkManifest xApkManifest, ZipContent content) throws IOException { Set declaredApks = xApkManifest.getSplitApks().stream() .map(SplitApk::getFile).collect(Collectors.toSet()); List apks = new ArrayList<>(declaredApks.size()); List files = new ArrayList<>(); String dirName = xapkFile.getName() + '_' + System.currentTimeMillis(); Path tmpDir = context.files().getPluginTempDir().resolve(dirName); FileUtils.makeDirs(tmpDir); for (IZipEntry entry : content.getEntries()) { String fileName = entry.getName(); Path file = tmpDir.resolve(fileName); try (InputStream inputStream = entry.getInputStream()) { Files.copy(inputStream, file, StandardCopyOption.REPLACE_EXISTING); } if (declaredApks.contains(fileName)) { apks.add(file); } else { files.add(file); } } return new XApkData(xApkManifest, tmpDir, apks, files); } private XApkData getLoaded(Path inputPath) throws IOException { return loaded.get(pathToKey(inputPath)); } private void saveLoaded(Path inputPath, XApkData xApkData) throws IOException { loaded.put(pathToKey(inputPath), xApkData); } private static String pathToKey(Path path) throws IOException { return path.toRealPath(LinkOption.NOFOLLOW_LINKS).toString(); } public synchronized void unload() { for (XApkData data : loaded.values()) { FileUtils.deleteDirIfExists(data.getTmpDir()); } loaded.clear(); } } ================================================ FILE: jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/SplitApk.java ================================================ package jadx.plugins.input.xapk.data; public class SplitApk { private String file; private String id; public String getFile() { return file; } public void setFile(String file) { this.file = file; } public String getId() { return id; } public void setId(String id) { this.id = id; } } ================================================ FILE: jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/XApkData.java ================================================ package jadx.plugins.input.xapk.data; import java.nio.file.Path; import java.util.List; public class XApkData { private final XApkManifest manifest; private final Path tmpDir; private final List files; private final List apks; public XApkData(XApkManifest manifest, Path tmpDir, List apks, List files) { this.manifest = manifest; this.tmpDir = tmpDir; this.apks = apks; this.files = files; } public List getApks() { return apks; } public List getFiles() { return files; } public XApkManifest getManifest() { return manifest; } public Path getTmpDir() { return tmpDir; } } ================================================ FILE: jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/data/XApkManifest.java ================================================ package jadx.plugins.input.xapk.data; import java.util.List; import com.google.gson.annotations.SerializedName; public class XApkManifest { @SerializedName("xapk_version") int version; @SerializedName("split_apks") List splitApks; public List getSplitApks() { return splitApks; } public void setSplitApks(List splitApks) { this.splitApks = splitApks; } public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } } ================================================ FILE: jadx-plugins/jadx-xapk-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin ================================================ jadx.plugins.input.xapk.XApkInputPlugin ================================================ FILE: jadx-plugins-tools/build.gradle.kts ================================================ plugins { id("jadx-java") id("jadx-library") } dependencies { api(project(":jadx-core")) implementation(project(":jadx-commons:jadx-app-commons")) implementation("com.google.code.gson:gson:2.13.2") implementation("commons-io:commons-io:2.21.0") testImplementation("com.squareup.okhttp3:mockwebserver3:5.3.0") } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxExternalPluginsLoader.java ================================================ package jadx.plugins.tools; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ServiceLoader; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.loader.JadxPluginLoader; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; public class JadxExternalPluginsLoader implements JadxPluginLoader { private static final Logger LOG = LoggerFactory.getLogger(JadxExternalPluginsLoader.class); public static final String JADX_PLUGIN_CLASSLOADER_PREFIX = "jadx-plugin:"; private final List classLoaders = new ArrayList<>(); @Override public List load() { close(); long start = System.currentTimeMillis(); Map map = new HashMap<>(); loadFromClsLoader(map, thisClassLoader()); loadInstalledPlugins(map); List list = new ArrayList<>(map.size()); list.addAll(map.values()); list.sort(Comparator.comparing(p -> p.getClass().getSimpleName())); if (LOG.isDebugEnabled()) { LOG.debug("Collected {} plugins in {}ms", list.size(), System.currentTimeMillis() - start); } return list; } public JadxPlugin loadFromPath(Path pluginPath) { Map map = new HashMap<>(); loadFromPath(map, pluginPath); int loaded = map.size(); if (loaded == 0) { throw new JadxRuntimeException("No plugin found in jar: " + pluginPath); } if (loaded > 1) { String plugins = map.values().stream().map(p -> p.getPluginInfo().getPluginId()).collect(Collectors.joining(", ")); throw new JadxRuntimeException("Expect only one plugin per jar: " + pluginPath + ", but found: " + loaded + " - " + plugins); } return Utils.first(map.values()); } private void loadFromClsLoader(Map map, ClassLoader classLoader) { ServiceLoader serviceLoader = ServiceLoader.load(JadxPlugin.class, classLoader); for (ServiceLoader.Provider provider : serviceLoader.stream().collect(Collectors.toList())) { Class pluginClass = provider.type(); String clsName = pluginClass.getName(); if (!map.containsKey(clsName) && pluginClass.getClassLoader() == classLoader) { map.put(clsName, provider.get()); } } } private void loadInstalledPlugins(Map map) { List paths = JadxPluginsTools.getInstance().getEnabledPluginPaths(); for (Path pluginPath : paths) { loadFromPath(map, pluginPath); } } private void loadFromPath(Map map, Path pluginPath) { try { URL[] urls; if (Files.isDirectory(pluginPath)) { urls = FileUtils.listFiles(pluginPath, file -> FileUtils.hasExtension(file, ".jar")) .stream() .map(JadxExternalPluginsLoader::toURL) .toArray(URL[]::new); if (urls.length == 0) { throw new JadxRuntimeException("No jar files found in plugin directory"); } } else if (Files.isRegularFile(pluginPath)) { if (FileUtils.hasExtension(pluginPath, ".jar")) { urls = new URL[] { toURL(pluginPath) }; } else { throw new JadxRuntimeException("Unexpected plugin file format"); } } else { throw new JadxRuntimeException("Plugin file not found"); } String clsLoaderName = JADX_PLUGIN_CLASSLOADER_PREFIX + pluginPath.getFileName(); URLClassLoader pluginClsLoader = new URLClassLoader(clsLoaderName, urls, thisClassLoader()); classLoaders.add(pluginClsLoader); loadFromClsLoader(map, pluginClsLoader); } catch (Exception e) { throw new JadxRuntimeException("Failed to load plugins from: " + pluginPath, e); } } private static URL toURL(Path pluginPath) { try { return pluginPath.toUri().toURL(); } catch (MalformedURLException e) { throw new RuntimeException(e); } } private static ClassLoader thisClassLoader() { return JadxExternalPluginsLoader.class.getClassLoader(); } @Override public void close() { try { for (URLClassLoader classLoader : classLoaders) { try { classLoader.close(); } catch (Exception e) { // ignore } } } finally { classLoaders.clear(); } } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsList.java ================================================ package jadx.plugins.tools; import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import jadx.core.utils.files.FileUtils; import jadx.plugins.tools.data.JadxPluginListCache; import jadx.plugins.tools.data.JadxPluginListEntry; import jadx.plugins.tools.resolvers.github.GithubTools; import jadx.plugins.tools.resolvers.github.LocationInfo; import jadx.plugins.tools.resolvers.github.data.Asset; import jadx.plugins.tools.resolvers.github.data.Release; import jadx.plugins.tools.utils.PluginUtils; import jadx.zip.ZipReader; import static jadx.core.utils.GsonUtils.buildGson; import static jadx.plugins.tools.utils.PluginFiles.PLUGINS_LIST_CACHE; public class JadxPluginsList { private static final Logger LOG = LoggerFactory.getLogger(JadxPluginsList.class); private static final JadxPluginsList INSTANCE = new JadxPluginsList(); private static final Type LIST_TYPE = new TypeToken>() { }.getType(); private static final Type CACHE_TYPE = new TypeToken() { }.getType(); public static JadxPluginsList getInstance() { return INSTANCE; } private @Nullable JadxPluginListCache loadedList; private JadxPluginsList() { } /** * List provider with update callback. * Can be called one or two times: *
* - Apply cached data first *
* - If update is available, apply data after fetch *
* Method call is blocking. */ public synchronized void get(Consumer> consumer) { if (loadedList != null) { consumer.accept(loadedList.getList()); return; } JadxPluginListCache listCache = loadCache(); if (listCache != null) { consumer.accept(listCache.getList()); loadedList = listCache; } Release release = fetchLatestRelease(); if (listCache == null || !listCache.getVersion().equals(release.getName())) { JadxPluginListCache updatedList = fetchBundle(release); saveCache(updatedList); consumer.accept(updatedList.getList()); loadedList = updatedList; } } public List get() { AtomicReference> holder = new AtomicReference<>(); get(holder::set); return holder.get(); } private @Nullable JadxPluginListCache loadCache() { if (!Files.isRegularFile(PLUGINS_LIST_CACHE)) { return null; } try { String jsonStr = FileUtils.readFile(PLUGINS_LIST_CACHE); return buildGson().fromJson(jsonStr, CACHE_TYPE); } catch (Exception e) { return null; } } private void saveCache(JadxPluginListCache listCache) { try { String jsonStr = buildGson().toJson(listCache, CACHE_TYPE); FileUtils.writeFile(PLUGINS_LIST_CACHE, jsonStr); } catch (Exception e) { throw new RuntimeException("Error saving file: " + PLUGINS_LIST_CACHE, e); } } private Release fetchLatestRelease() { LOG.debug("Fetching latest plugins-list release info"); LocationInfo pluginsList = new LocationInfo("jadx-decompiler", "jadx-plugins-list", "list"); Release release = GithubTools.fetchRelease(pluginsList); List assets = release.getAssets(); if (assets.isEmpty()) { throw new RuntimeException("Release don't have assets"); } return release; } private JadxPluginListCache fetchBundle(Release release) { LOG.debug("Fetching plugins-list bundle: {}", release.getName()); try { Asset listAsset = release.getAssets().get(0); Path tmpListFile = Files.createTempFile("plugins-list", ".zip"); try { PluginUtils.downloadFile(listAsset.getDownloadUrl(), tmpListFile); JadxPluginListCache listCache = new JadxPluginListCache(); listCache.setVersion(release.getName()); listCache.setList(loadListBundle(tmpListFile)); return listCache; } finally { Files.deleteIfExists(tmpListFile); } } catch (Exception e) { throw new RuntimeException("Failed to load plugin-list bundle for release:" + release.getName(), e); } } private static List loadListBundle(Path tmpListFile) { Gson gson = buildGson(); List entries = new ArrayList<>(); new ZipReader().visitEntries(tmpListFile.toFile(), entry -> { if (entry.getName().endsWith(".json")) { try (Reader reader = new InputStreamReader(entry.getInputStream())) { entries.addAll(gson.fromJson(reader, LIST_TYPE)); } catch (Exception e) { throw new RuntimeException("Failed to read plugins list entry: " + entry.getName()); } } return null; }); return entries; } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsTools.java ================================================ package jadx.plugins.tools; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.utils.CommonFileUtils; import jadx.core.Jadx; import jadx.core.plugins.versions.VerifyRequiredVersion; import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; import jadx.plugins.tools.data.JadxInstalledPlugins; import jadx.plugins.tools.data.JadxPluginMetadata; import jadx.plugins.tools.data.JadxPluginUpdate; import jadx.plugins.tools.resolvers.IJadxPluginResolver; import jadx.plugins.tools.resolvers.ResolversRegistry; import jadx.plugins.tools.utils.PluginUtils; import jadx.zip.IZipEntry; import jadx.zip.ZipContent; import jadx.zip.ZipReader; import static jadx.core.utils.GsonUtils.buildGson; import static jadx.plugins.tools.utils.PluginFiles.DROPINS_DIR; import static jadx.plugins.tools.utils.PluginFiles.INSTALLED_DIR; import static jadx.plugins.tools.utils.PluginFiles.PLUGINS_JSON; public class JadxPluginsTools { private static final Logger LOG = LoggerFactory.getLogger(JadxPluginsTools.class); private static final JadxPluginsTools INSTANCE = new JadxPluginsTools(); public static JadxPluginsTools getInstance() { return INSTANCE; } private JadxPluginsTools() { } public JadxPluginMetadata install(String locationId) { IJadxPluginResolver resolver = ResolversRegistry.getResolver(locationId); Supplier> fetchVersions; if (resolver.hasVersion(locationId)) { fetchVersions = () -> { JadxPluginMetadata version = resolver.resolve(locationId) .orElseThrow(() -> new JadxRuntimeException("Failed to resolve plugin location: " + locationId)); return Collections.singletonList(version); }; } else { // load latest 10 version to search for compatible one fetchVersions = () -> resolver.resolveVersions(locationId, 1, 10); } List versionsMetadata; try { versionsMetadata = fetchVersions.get(); } catch (Exception e) { throw new JadxRuntimeException("Plugin info fetch failed, locationId: " + locationId, e); } if (versionsMetadata.isEmpty()) { throw new JadxRuntimeException("Plugin release not found, locationId: " + locationId); } VerifyRequiredVersion verifyRequiredVersion = new VerifyRequiredVersion(); List rejectedVersions = new ArrayList<>(); for (JadxPluginMetadata pluginMetadata : versionsMetadata) { // download plugin jar and fill metadata // any download or plugin instantiation errors will stop versions check fillMetadata(pluginMetadata); if (verifyRequiredVersion.isCompatible(pluginMetadata.getRequiredJadxVersion())) { install(pluginMetadata); return pluginMetadata; } String pluginVersion = Utils.getOrElse(pluginMetadata.getVersion(), "unknown"); rejectedVersions.add(" version '" + pluginVersion + "' not compatible, require: " + pluginMetadata.getRequiredJadxVersion()); } throw new JadxRuntimeException("Can't find compatible version to install" + ", current jadx version: " + verifyRequiredVersion.getJadxVersion() + "\nrejected plugin versions:\n" + String.join("\n", rejectedVersions)); } public JadxPluginMetadata resolveMetadata(String locationId) { IJadxPluginResolver resolver = ResolversRegistry.getResolver(locationId); JadxPluginMetadata pluginMetadata = resolver.resolve(locationId) .orElseThrow(() -> new RuntimeException("Failed to resolve locationId: " + locationId)); fillMetadata(pluginMetadata); return pluginMetadata; } public List getVersionsByLocation(String locationId, int page, int perPage) { IJadxPluginResolver resolver = ResolversRegistry.getResolver(locationId); List list = resolver.resolveVersions(locationId, page, perPage); for (JadxPluginMetadata pluginMetadata : list) { fillMetadata(pluginMetadata); } return list; } public List updateAll() { JadxInstalledPlugins plugins = loadPluginsJson(); int size = plugins.getInstalled().size(); List updates = new ArrayList<>(size); List newList = new ArrayList<>(size); for (JadxPluginMetadata plugin : plugins.getInstalled()) { JadxPluginMetadata newVersion = null; try { newVersion = update(plugin); } catch (Exception e) { LOG.warn("Failed to update plugin: {}", plugin.getPluginId(), e); } if (newVersion != null) { updates.add(new JadxPluginUpdate(plugin, newVersion)); newList.add(newVersion); } else { newList.add(plugin); } } if (!updates.isEmpty()) { plugins.setUpdated(System.currentTimeMillis()); plugins.setInstalled(newList); savePluginsJson(plugins); } return updates; } public Optional update(String pluginId) { JadxInstalledPlugins plugins = loadPluginsJson(); JadxPluginMetadata plugin = plugins.getInstalled().stream() .filter(p -> p.getPluginId().equals(pluginId)) .findFirst() .orElseThrow(() -> new RuntimeException("Plugin not found: " + pluginId)); JadxPluginMetadata newVersion = update(plugin); if (newVersion == null) { return Optional.empty(); } plugins.setUpdated(System.currentTimeMillis()); plugins.getInstalled().remove(plugin); plugins.getInstalled().add(newVersion); savePluginsJson(plugins); return Optional.of(new JadxPluginUpdate(plugin, newVersion)); } public boolean uninstall(String pluginId) { JadxInstalledPlugins plugins = loadPluginsJson(); Optional found = plugins.getInstalled().stream() .filter(p -> p.getPluginId().equals(pluginId)) .findFirst(); if (found.isEmpty()) { return false; } JadxPluginMetadata plugin = found.get(); deletePlugin(plugin); plugins.getInstalled().remove(plugin); savePluginsJson(plugins); return true; } public List getInstalled() { return loadPluginsJson().getInstalled(); } /** * Return all loadable plugins info (including installed, bundled and dropins). *
* For only installed plugins prefer {@link jadx.plugins.tools.JadxPluginsTools#getInstalled} * method. */ public List getAllPluginsInfo() { try (JadxExternalPluginsLoader pluginsLoader = new JadxExternalPluginsLoader()) { return pluginsLoader.load().stream() .map(JadxPlugin::getPluginInfo) .collect(Collectors.toList()); } } public List getEnabledPluginPaths() { List list = new ArrayList<>(); for (JadxPluginMetadata pluginMetadata : loadPluginsJson().getInstalled()) { if (pluginMetadata.isDisabled()) { continue; } list.add(INSTALLED_DIR.resolve(pluginMetadata.getPath())); } list.addAll(FileUtils.listFiles(DROPINS_DIR)); return list; } /** * Disable or enable plugin * * @return true if disabled status was changed */ public boolean changeDisabledStatus(String pluginId, boolean disabled) { JadxInstalledPlugins data = loadPluginsJson(); JadxPluginMetadata plugin = data.getInstalled().stream() .filter(p -> p.getPluginId().equals(pluginId)) .findFirst() .orElseThrow(() -> new RuntimeException("Plugin not found: " + pluginId)); if (plugin.isDisabled() == disabled) { return false; } plugin.setDisabled(disabled); data.setUpdated(System.currentTimeMillis()); savePluginsJson(data); return true; } private @Nullable JadxPluginMetadata update(JadxPluginMetadata plugin) { IJadxPluginResolver resolver = ResolversRegistry.getResolver(plugin.getLocationId()); if (!resolver.isUpdateSupported()) { return null; } Optional updateOpt = resolver.resolve(plugin.getLocationId()); if (updateOpt.isEmpty()) { return null; } JadxPluginMetadata update = updateOpt.get(); if (Objects.equals(update.getVersion(), plugin.getVersion())) { return null; } fillMetadata(update); install(update); return update; } private void install(JadxPluginMetadata metadata) { String reqVersionStr = metadata.getRequiredJadxVersion(); if (!VerifyRequiredVersion.isJadxCompatible(reqVersionStr)) { throw new JadxRuntimeException("Can't install plugin, required version: \"" + reqVersionStr + '\"' + " is not compatible with current jadx version: " + Jadx.getVersion()); } // remove previous version uninstall(metadata.getPluginId()); String version = metadata.getVersion(); String pluginBaseName = metadata.getPluginId() + (StringUtils.notBlank(version) ? '-' + version : ""); String pluginPathStr = metadata.getPath(); Path pluginPath = Paths.get(pluginPathStr); if (pluginPathStr.endsWith(".jar")) { Path pluginJar = INSTALLED_DIR.resolve(pluginBaseName + ".jar"); copyJar(pluginPath, pluginJar); metadata.setPath(INSTALLED_DIR.relativize(pluginJar).toString()); } else if (Files.isDirectory(pluginPath)) { Path pluginDir = INSTALLED_DIR.resolve(pluginBaseName); try { FileUtils.deleteDirIfExists(pluginDir); org.apache.commons.io.FileUtils.moveDirectory(pluginPath.toFile(), pluginDir.toFile()); } catch (IOException e) { throw new JadxRuntimeException("Failed to install plugin: " + pluginBaseName, e); } metadata.setPath(INSTALLED_DIR.relativize(pluginDir).toString()); } else { throw new JadxRuntimeException("Unexpected plugin path type: " + pluginPathStr); } // update plugins json JadxInstalledPlugins plugins = loadPluginsJson(); plugins.getInstalled().add(metadata); plugins.setUpdated(System.currentTimeMillis()); savePluginsJson(plugins); } private void fillMetadata(JadxPluginMetadata metadata) { try { String pluginPath = metadata.getPath(); if (needDownload(pluginPath)) { // download plugin String ext = CommonFileUtils.getFileExtension(pluginPath); Path tmpJar = Files.createTempFile(metadata.getName(), "plugin." + ext); PluginUtils.downloadFile(pluginPath, tmpJar); pluginPath = tmpJar.toAbsolutePath().toString(); } if (pluginPath.endsWith(".zip")) { // unpack plugin zip Path tmpDir = Files.createTempDirectory(metadata.getName()); unzip(Paths.get(pluginPath), tmpDir); pluginPath = tmpDir.toAbsolutePath().toString(); } metadata.setPath(pluginPath); fillMetadataFromPath(metadata, Paths.get(pluginPath)); } catch (Exception e) { throw new RuntimeException("Failed to fill plugin metadata, plugin: " + metadata.getPluginId(), e); } } private void fillMetadataFromPath(JadxPluginMetadata metadata, Path pluginPath) { try (JadxExternalPluginsLoader loader = new JadxExternalPluginsLoader()) { JadxPlugin jadxPlugin = loader.loadFromPath(pluginPath); JadxPluginInfo pluginInfo = jadxPlugin.getPluginInfo(); metadata.setPluginId(pluginInfo.getPluginId()); metadata.setName(pluginInfo.getName()); metadata.setDescription(pluginInfo.getDescription()); metadata.setHomepage(pluginInfo.getHomepage()); metadata.setRequiredJadxVersion(pluginInfo.getRequiredJadxVersion()); } catch (NoSuchMethodError e) { throw new RuntimeException("Looks like plugin uses unknown API, try to update jadx version", e); } } private static boolean needDownload(String jar) { return jar.startsWith("https://") || jar.startsWith("http://"); } private void copyJar(Path sourceJar, Path destJar) { try { Files.copy(sourceJar, destJar, StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { throw new RuntimeException("Failed to copy plugin jar: " + sourceJar + " to: " + destJar, e); } } private void deletePlugin(JadxPluginMetadata plugin) { try { Path pluginPath = INSTALLED_DIR.resolve(plugin.getPath()); if (Files.isDirectory(pluginPath)) { FileUtils.deleteDir(pluginPath); } else { Files.deleteIfExists(pluginPath); } } catch (IOException e) { // ignore } } private JadxInstalledPlugins loadPluginsJson() { if (!Files.isRegularFile(PLUGINS_JSON)) { JadxInstalledPlugins plugins = new JadxInstalledPlugins(); plugins.setVersion(1); return plugins; } try (Reader reader = Files.newBufferedReader(PLUGINS_JSON, StandardCharsets.UTF_8)) { JadxInstalledPlugins data = buildGson().fromJson(reader, JadxInstalledPlugins.class); upgradePluginsData(data); return data; } catch (Exception e) { throw new RuntimeException("Failed to read file: " + PLUGINS_JSON); } } private void savePluginsJson(JadxInstalledPlugins data) { if (data.getInstalled().isEmpty()) { try { Files.deleteIfExists(PLUGINS_JSON); } catch (Exception e) { throw new RuntimeException("Failed to remove file: " + PLUGINS_JSON, e); } return; } data.getInstalled().sort(null); try (Writer writer = Files.newBufferedWriter(PLUGINS_JSON, StandardCharsets.UTF_8)) { buildGson().toJson(data, writer); } catch (Exception e) { throw new RuntimeException("Error saving file: " + PLUGINS_JSON, e); } } private void upgradePluginsData(JadxInstalledPlugins data) { if (data.getVersion() == 0) { data.setVersion(1); } } private static void unzip(Path zipFile, Path outDir) { ZipReader zipReader = new ZipReader(); // TODO: pass zip options from jadx args try (ZipContent content = zipReader.open(zipFile.toFile())) { for (IZipEntry entry : content.getEntries()) { Path entryFile = outDir.resolve(entry.getName()); Files.copy(entry.getInputStream(), entryFile, StandardCopyOption.REPLACE_EXISTING); } } catch (IOException e) { throw new JadxRuntimeException("Failed to unzip file: " + zipFile, e); } } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxInstalledPlugins.java ================================================ package jadx.plugins.tools.data; import java.util.ArrayList; import java.util.List; public class JadxInstalledPlugins { private int version; private long updated; private List installed = new ArrayList<>(); public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } public long getUpdated() { return updated; } public void setUpdated(long updated) { this.updated = updated; } public List getInstalled() { return installed; } public void setInstalled(List installed) { this.installed = installed; } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginListCache.java ================================================ package jadx.plugins.tools.data; import java.util.List; public class JadxPluginListCache { private String version; private List list; public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public List getList() { return list; } public void setList(List list) { this.list = list; } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginListEntry.java ================================================ package jadx.plugins.tools.data; public class JadxPluginListEntry { private String pluginId; private String locationId; private String name; private String description; private String homepage; private int revision; public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getHomepage() { return homepage; } public void setHomepage(String homepage) { this.homepage = homepage; } public String getLocationId() { return locationId; } public void setLocationId(String locationId) { this.locationId = locationId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPluginId() { return pluginId; } public void setPluginId(String pluginId) { this.pluginId = pluginId; } public int getRevision() { return revision; } public void setRevision(int revision) { this.revision = revision; } @Override public String toString() { return "JadxPluginListEntry{" + "description='" + description + '\'' + ", pluginId='" + pluginId + '\'' + ", locationId='" + locationId + '\'' + ", name='" + name + '\'' + ", homepage='" + homepage + '\'' + ", revision=" + revision + '}'; } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginMetadata.java ================================================ package jadx.plugins.tools.data; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import com.google.gson.annotations.SerializedName; public class JadxPluginMetadata implements Comparable { private String pluginId; private String name; private String description; private String homepage; private @Nullable String requiredJadxVersion; private @Nullable String version; private String locationId; /** * Absolute path to '.jar' file or unpacked zip directory */ @SerializedName(value = "path", alternate = { "jar" }) private String path; private boolean disabled; public String getPluginId() { return pluginId; } public void setPluginId(String pluginId) { this.pluginId = pluginId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public @Nullable String getVersion() { return version; } public void setVersion(@Nullable String version) { this.version = version; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getHomepage() { return homepage; } public void setHomepage(String homepage) { this.homepage = homepage; } public @Nullable String getRequiredJadxVersion() { return requiredJadxVersion; } public void setRequiredJadxVersion(@Nullable String requiredJadxVersion) { this.requiredJadxVersion = requiredJadxVersion; } public String getLocationId() { return locationId; } public void setLocationId(String locationId) { this.locationId = locationId; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } @Deprecated public String getJar() { return path; } @Deprecated public void setJar(String jar) { this.path = jar; } public boolean isDisabled() { return disabled; } public void setDisabled(boolean disabled) { this.disabled = disabled; } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof JadxPluginMetadata)) { return false; } return pluginId.equals(((JadxPluginMetadata) other).pluginId); } @Override public int hashCode() { return pluginId.hashCode(); } @Override public int compareTo(@NotNull JadxPluginMetadata o) { return pluginId.compareTo(o.pluginId); } @Override public String toString() { return "JadxPluginMetadata{" + "id=" + pluginId + ", name=" + name + ", version=" + (version != null ? version : "?") + ", locationId=" + locationId + ", path=" + path + '}'; } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginUpdate.java ================================================ package jadx.plugins.tools.data; public class JadxPluginUpdate { private final JadxPluginMetadata oldVersion; private final JadxPluginMetadata newVersion; public JadxPluginUpdate(JadxPluginMetadata oldVersion, JadxPluginMetadata newVersion) { this.oldVersion = oldVersion; this.newVersion = newVersion; } public JadxPluginMetadata getOld() { return oldVersion; } public JadxPluginMetadata getNew() { return newVersion; } public String getPluginId() { return newVersion.getPluginId(); } public String getOldVersion() { return oldVersion.getVersion(); } public String getNewVersion() { return newVersion.getVersion(); } @Override public String toString() { return "PluginUpdate{" + oldVersion.getPluginId() + ": " + oldVersion.getVersion() + " -> " + newVersion.getVersion() + "}"; } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/IJadxPluginResolver.java ================================================ package jadx.plugins.tools.resolvers; import java.util.List; import java.util.Optional; import jadx.plugins.tools.data.JadxPluginMetadata; public interface IJadxPluginResolver { /** * Unique resolver identifier, should be same as locationId prefix */ String id(); /** * This resolver support updates and can fetch the latest version. */ boolean isUpdateSupported(); /** * Fetch the latest version plugin metadata by location */ Optional resolve(String locationId); /** * Fetch several latest versions (pageable) of plugin by locationId. * * @param page page number, starts with 1 * @param perPage result's count limit */ List resolveVersions(String locationId, int page, int perPage); /** * Check if locationId has a specified version number */ boolean hasVersion(String locationId); } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/README.md ================================================ ### Supported publish locations for Jadx plugins --- #### GitHub release artifact Pattern: `github::[:][:]` Examples: `github:skylot:jadx`, `github:skylot:jadx:sample-plugin` or `github:skylot:jadx:0.1.0` `` - exact version to install (optional), should be equal to release name Artifact name pattern: `[-].jar`. Default value for `` is a repo name, `-` is optional. --- #### Local file Install local jar file. Pattern: `file:.jar` Example: `file:/home/user/plugin.jar` As alternative to install, plugin jars can be copied to `plugins/dropins` folder. ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/ResolversRegistry.java ================================================ package jadx.plugins.tools.resolvers; import java.util.HashMap; import java.util.Map; import java.util.Objects; import jadx.plugins.tools.resolvers.file.LocalFileResolver; import jadx.plugins.tools.resolvers.github.GithubReleaseResolver; public class ResolversRegistry { private static final Map RESOLVERS_MAP = new HashMap<>(); static { register(new LocalFileResolver()); register(new GithubReleaseResolver()); } private static void register(IJadxPluginResolver resolver) { RESOLVERS_MAP.put(resolver.id(), resolver); } public static IJadxPluginResolver getResolver(String locationId) { Objects.requireNonNull(locationId); int sep = locationId.indexOf(':'); if (sep <= 0) { throw new IllegalArgumentException("Malformed locationId: " + locationId); } return getById(locationId.substring(0, sep)); } public static IJadxPluginResolver getById(String resolverId) { IJadxPluginResolver resolver = RESOLVERS_MAP.get(resolverId); if (resolver == null) { throw new IllegalArgumentException("Unknown resolverId: " + resolverId); } return resolver; } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/file/LocalFileResolver.java ================================================ package jadx.plugins.tools.resolvers.file; import java.io.File; import java.util.List; import java.util.Optional; import jadx.plugins.tools.data.JadxPluginMetadata; import jadx.plugins.tools.resolvers.IJadxPluginResolver; import static jadx.plugins.tools.utils.PluginUtils.removePrefix; public class LocalFileResolver implements IJadxPluginResolver { @Override public String id() { return "file"; } @Override public boolean isUpdateSupported() { return false; } private static boolean isValidFileLocation(String locationId) { return locationId.startsWith("file:") && (locationId.endsWith(".jar") || locationId.endsWith(".zip")); } @Override public Optional resolve(String locationId) { if (!isValidFileLocation(locationId)) { return Optional.empty(); } File pluginFile = new File(removePrefix(locationId, "file:")); if (!pluginFile.isFile()) { throw new RuntimeException("File not found: " + pluginFile.getAbsolutePath()); } JadxPluginMetadata metadata = new JadxPluginMetadata(); metadata.setLocationId(locationId); metadata.setPath(pluginFile.getAbsolutePath()); return Optional.of(metadata); } @Override public List resolveVersions(String locationId, int page, int perPage) { if (page > 1) { // no other versions return List.of(); } // return only the current file return resolve(locationId).map(List::of).orElseGet(List::of); } @Override public boolean hasVersion(String locationId) { // no supported return false; } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubReleaseResolver.java ================================================ package jadx.plugins.tools.resolvers.github; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import jadx.core.utils.ListUtils; import jadx.plugins.tools.data.JadxPluginMetadata; import jadx.plugins.tools.resolvers.IJadxPluginResolver; import jadx.plugins.tools.resolvers.github.data.Asset; import jadx.plugins.tools.resolvers.github.data.Release; import jadx.plugins.tools.utils.PluginUtils; public class GithubReleaseResolver implements IJadxPluginResolver { private static final Pattern VERSION_PATTERN = Pattern.compile("v?\\d+\\.\\d+(\\.\\d+)?"); @Override public Optional resolve(String locationId) { LocationInfo info = parseLocation(locationId); if (info == null) { return Optional.empty(); } Release release = GithubTools.fetchRelease(info); JadxPluginMetadata metadata = buildMetadata(release, info); return Optional.of(metadata); } @Override public List resolveVersions(String locationId, int page, int perPage) { LocationInfo info = parseLocation(locationId); if (info == null) { return List.of(); } return GithubTools.fetchReleases(info, page, perPage) .stream() .map(r -> buildMetadata(r, info)) .collect(Collectors.toList()); } @Override public boolean hasVersion(String locationId) { LocationInfo locationInfo = parseLocation(locationId); return locationInfo != null && locationInfo.getVersion() != null; } private JadxPluginMetadata buildMetadata(Release release, LocationInfo info) { List assets = release.getAssets(); String releaseVersion = PluginUtils.removePrefix(release.getName(), "v"); Asset asset = searchPluginAsset(assets, info.getArtifactPrefix(), releaseVersion); if (!asset.getName().contains(releaseVersion)) { String assetVersion = PluginUtils.extractVersion(asset.getName()); if (assetVersion != null) { releaseVersion = assetVersion; } } JadxPluginMetadata metadata = new JadxPluginMetadata(); metadata.setVersion(releaseVersion); metadata.setLocationId(buildLocationIdWithoutVersion(info)); // exclude version for later updates metadata.setPath(asset.getDownloadUrl()); return metadata; } private static LocationInfo parseLocation(String locationId) { if (!locationId.startsWith("github:")) { return null; } String[] parts = locationId.split(":"); if (parts.length < 3) { return null; } String owner = parts[1]; String project = parts[2]; String version = null; String artifactPrefix = project; if (parts.length >= 4) { String part = parts[3]; if (VERSION_PATTERN.matcher(part).matches()) { version = part; if (parts.length >= 5) { artifactPrefix = parts[4]; } } else { artifactPrefix = part; } } return new LocationInfo(owner, project, artifactPrefix, version); } private static Asset searchPluginAsset(List assets, String artifactPrefix, String releaseVersion) { Asset assetJar = searchAssetWithExt(assets, artifactPrefix, releaseVersion, ".jar"); if (assetJar != null) { return assetJar; } Asset assetZip = searchAssetWithExt(assets, artifactPrefix, releaseVersion, ".zip"); if (assetZip != null) { return assetZip; } throw new RuntimeException("Release artifact with prefix '" + artifactPrefix + "' not found"); } private static @Nullable Asset searchAssetWithExt(List assets, String artifactPrefix, String releaseVersion, String ext) { String artifactName = artifactPrefix + '-' + releaseVersion + ext; Asset exactAsset = ListUtils.filterOnlyOne(assets, a -> a.getName().equals(artifactName)); if (exactAsset != null) { return exactAsset; } // search without version filter return ListUtils.filterOnlyOne(assets, a -> { String assetFileName = a.getName(); return assetFileName.startsWith(artifactPrefix) && assetFileName.endsWith(ext); }); } private static String buildLocationIdWithoutVersion(LocationInfo info) { String baseLocation = "github:" + info.getOwner() + ':' + info.getProject(); if (info.getProject().equals(info.getArtifactPrefix())) { return baseLocation; } return baseLocation + ':' + info.getArtifactPrefix(); } @Override public String id() { return "github"; } @Override public boolean isUpdateSupported() { return true; } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubTools.java ================================================ package jadx.plugins.tools.resolvers.github; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.List; import java.util.stream.Collectors; import com.google.gson.reflect.TypeToken; import jadx.core.utils.files.FileUtils; import jadx.plugins.tools.resolvers.github.data.Release; import static jadx.core.utils.GsonUtils.buildGson; public class GithubTools { private static final GithubTools GITHUB_INSTANCE = new GithubTools("https://api.github.com"); private static final Type RELEASE_TYPE = new TypeToken() { }.getType(); private static final Type RELEASE_LIST_TYPE = new TypeToken>() { }.getType(); public static Release fetchRelease(LocationInfo info) { return GITHUB_INSTANCE.getRelease(info); } public static List fetchReleases(LocationInfo info, int page, int perPage) { return GITHUB_INSTANCE.getReleases(info, page, perPage); } private final String baseUrl; GithubTools(String baseUrl) { this.baseUrl = baseUrl; } Release getRelease(LocationInfo info) { String projectUrl = baseUrl + "/repos/" + info.getOwner() + "/" + info.getProject(); String version = info.getVersion(); if (version == null) { // get latest version return get(projectUrl + "/releases/latest", RELEASE_TYPE); } // search version in other releases (by name) List releases = fetchReleases(info, 1, 50); return releases.stream() .filter(r -> r.getName().equals(version)) .findFirst() .orElseThrow(() -> new RuntimeException("Release with version: " + version + " not found." + " Available versions: " + releases.stream().map(Release::getName).collect(Collectors.joining(", ")))); } List getReleases(LocationInfo info, int page, int perPage) { String projectUrl = baseUrl + "/repos/" + info.getOwner() + "/" + info.getProject(); String requestUrl = projectUrl + "/releases?page=" + page + "&per_page=" + perPage; return get(requestUrl, RELEASE_LIST_TYPE); } private static T get(String url, Type type) { HttpURLConnection con = null; try { try { con = (HttpURLConnection) URI.create(url).toURL().openConnection(); con.setRequestMethod("GET"); con.setInstanceFollowRedirects(true); int code = con.getResponseCode(); if (code != 200) { throw new RuntimeException(buildErrorDetails(con, url)); } } catch (IOException e) { throw new RuntimeException("Request failed, url: " + url, e); } try (Reader reader = new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)) { return buildGson().fromJson(reader, type); } catch (Exception e) { throw new RuntimeException("Failed to parse response, url: " + url, e); } } finally { if (con != null) { con.disconnect(); } } } private static String buildErrorDetails(HttpURLConnection con, String url) throws IOException { String shortMsg = con.getResponseMessage(); String remainRateLimit = con.getHeaderField("X-RateLimit-Remaining"); if ("0".equals(remainRateLimit)) { String resetTimeMs = con.getHeaderField("X-RateLimit-Reset"); String timeStr = resetTimeMs != null ? "after " + Instant.ofEpochSecond(Long.parseLong(resetTimeMs)) : "in one hour"; shortMsg += " (rate limit reached, try again " + timeStr + ')'; } StringBuilder headers = new StringBuilder(); for (int i = 0;; i++) { String value = con.getHeaderField(i); if (value == null) { break; } String key = con.getHeaderFieldKey(i); if (key != null) { headers.append('\n').append(key).append(": ").append(value); } } String responseStr = getResponseString(con); return "Request failed: " + con.getResponseCode() + ' ' + shortMsg + "\nURL: " + url + "\nHeaders:" + headers + (responseStr.isEmpty() ? "" : "\nresponse:\n" + responseStr); } private static String getResponseString(HttpURLConnection con) { try (InputStream in = con.getInputStream()) { return new String(FileUtils.streamToByteArray(in), StandardCharsets.UTF_8); } catch (Exception e) { // ignore return ""; } } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/LocationInfo.java ================================================ package jadx.plugins.tools.resolvers.github; import org.jetbrains.annotations.Nullable; public class LocationInfo { private final String owner; private final String project; private final String artifactPrefix; private final @Nullable String version; public LocationInfo(String owner, String project, String artifactPrefix) { this(owner, project, artifactPrefix, null); } public LocationInfo(String owner, String project, String artifactPrefix, @Nullable String version) { this.owner = owner; this.project = project; this.artifactPrefix = artifactPrefix; this.version = version; } public String getOwner() { return owner; } public String getProject() { return project; } public String getArtifactPrefix() { return artifactPrefix; } public @Nullable String getVersion() { return version; } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/data/Asset.java ================================================ package jadx.plugins.tools.resolvers.github.data; import com.google.gson.annotations.SerializedName; public class Asset { private int id; private String name; private long size; @SerializedName("browser_download_url") private String downloadUrl; @SerializedName("created_at") private String createdAt; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } public String getDownloadUrl() { return downloadUrl; } public void setDownloadUrl(String downloadUrl) { this.downloadUrl = downloadUrl; } public String getCreatedAt() { return createdAt; } public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } @Override public String toString() { return name + ", size: " + String.format("%.2fMB", size / 1024. / 1024.) + ", url: " + downloadUrl + ", date: " + createdAt; } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/data/Release.java ================================================ package jadx.plugins.tools.resolvers.github.data; import java.util.List; public class Release { private int id; private String name; private List assets; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public List getAssets() { return assets; } public void setAssets(List assets) { this.assets = assets; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(name); for (Asset asset : getAssets()) { sb.append("\n "); sb.append(asset); } return sb.toString(); } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginFiles.java ================================================ package jadx.plugins.tools.utils; import java.nio.file.Path; import jadx.commons.app.JadxCommonFiles; import static jadx.core.utils.files.FileUtils.makeDirs; public class PluginFiles { private static final Path PLUGINS_DIR = JadxCommonFiles.getConfigDir().resolve("plugins"); public static final Path PLUGINS_JSON = PLUGINS_DIR.resolve("plugins.json"); public static final Path INSTALLED_DIR = PLUGINS_DIR.resolve("installed"); public static final Path DROPINS_DIR = PLUGINS_DIR.resolve("dropins"); public static final Path PLUGINS_LIST_CACHE = JadxCommonFiles.getCacheDir().resolve("plugin-list.json"); static { makeDirs(INSTALLED_DIR); makeDirs(DROPINS_DIR); } } ================================================ FILE: jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginUtils.java ================================================ package jadx.plugins.tools.utils; import java.io.InputStream; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jetbrains.annotations.Nullable; public class PluginUtils { public static String removePrefix(String str, String prefix) { if (str.startsWith(prefix)) { return str.substring(prefix.length()); } return str; } public static void downloadFile(String fileUrl, Path destPath) { try (InputStream in = URI.create(fileUrl).toURL().openStream()) { Files.copy(in, destPath, StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { throw new RuntimeException("Failed to download file: " + fileUrl, e); } } private static final Pattern VERSION_LONG = Pattern.compile(".*v?(\\d+\\.\\d+\\.\\d+).*"); private static final Pattern VERSION_SHORT = Pattern.compile(".*v?(\\d+\\.\\d+).*"); public static @Nullable String extractVersion(String str) { Matcher longMatcher = VERSION_LONG.matcher(str); if (longMatcher.matches()) { return longMatcher.group(1); } Matcher shortMatcher = VERSION_SHORT.matcher(str); if (shortMatcher.matches()) { return shortMatcher.group(1); } return null; } } ================================================ FILE: jadx-plugins-tools/src/test/java/jadx/plugins/tools/resolvers/github/GithubToolsTest.java ================================================ package jadx.plugins.tools.resolvers.github; import java.io.IOException; import java.io.InputStream; import java.time.Instant; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import mockwebserver3.MockResponse; import mockwebserver3.MockWebServer; import jadx.core.utils.files.FileUtils; import jadx.plugins.tools.resolvers.github.data.Release; import static org.assertj.core.api.Assertions.assertThat; class GithubToolsTest { private static final Logger LOG = LoggerFactory.getLogger(GithubToolsTest.class); private MockWebServer server; private GithubTools githubTools; @BeforeEach public void setup() throws IOException { server = new MockWebServer(); server.start(); String baseUrl = server.url("/").toString(); githubTools = new GithubTools(baseUrl); } @AfterEach public void close() { server.close(); } @Test public void getReleaseGood() { server.enqueue(new MockResponse.Builder() .body(loadFromResource("plugins-list-good.json")) .build()); LocationInfo pluginsList = new LocationInfo("jadx-decompiler", "jadx-plugins-list", "list"); Release release = githubTools.getRelease(pluginsList); LOG.info("Result release: {}", release); assertThat(release.getName()).isEqualTo("v15"); assertThat(release.getAssets()).hasSize(1); } @Test public void getReleaseRateLimit() { server.enqueue(new MockResponse.Builder() .code(403) .addHeader("x-ratelimit-remaining", "0") .addHeader("x-ratelimit-reset", Instant.now().plusSeconds(60 * 60).getEpochSecond()) // 1 hour from now .body("{}") .build()); LocationInfo pluginsList = new LocationInfo("jadx-decompiler", "jadx-plugins-list", "list"); Assertions.assertThatThrownBy(() -> githubTools.getRelease(pluginsList)) .hasMessageContaining("403") .hasMessageContaining("Client Error") .hasMessageContaining("rate limit reached"); } private static String loadFromResource(String resName) { try (InputStream stream = GithubToolsTest.class.getResourceAsStream("/github/" + resName)) { return FileUtils.streamToString(stream); } catch (IOException e) { throw new RuntimeException("Failed to load resource: " + resName, e); } } } ================================================ FILE: jadx-plugins-tools/src/test/resources/github/plugins-list-good.json ================================================ { "assets": [ { "browser_download_url": "https://github.com/jadx-decompiler/jadx-plugins-list/releases/download/v15/jadx-plugins-list.zip", "content_type": "application/zip", "created_at": "2025-11-29T16:20:40Z", "digest": "sha256:a2a45c3a22be56b6f9c7e24d52c6411c4d546f386d7ea1e4ba124d4d28b4cf75", "download_count": 3412, "id": 322246364, "label": null, "name": "jadx-plugins-list.zip", "node_id": "RA_kwDOKEBYcM4TNRbc", "size": 1260, "state": "uploaded", "updated_at": "2025-11-29T16:20:42Z", "url": "https://api.github.com/repos/jadx-decompiler/jadx-plugins-list/releases/assets/322246364" } ], "assets_url": "https://api.github.com/repos/jadx-decompiler/jadx-plugins-list/releases/266146702/assets", "body": "What's Changed...", "created_at": "2025-11-29T16:19:33Z", "draft": false, "html_url": "https://github.com/jadx-decompiler/jadx-plugins-list/releases/tag/v15", "id": 266146702, "immutable": false, "mentions_count": 1, "name": "v15", "node_id": "RE_kwDOKEBYcM4P3ROO", "prerelease": false, "published_at": "2025-11-29T16:20:50Z", "tag_name": "v15", "tarball_url": "https://api.github.com/repos/jadx-decompiler/jadx-plugins-list/tarball/v15", "target_commitish": "main", "updated_at": "2025-11-29T16:20:50Z", "upload_url": "https://uploads.github.com/repos/jadx-decompiler/jadx-plugins-list/releases/266146702/assets{?name,label}", "url": "https://api.github.com/repos/jadx-decompiler/jadx-plugins-list/releases/266146702", "zipball_url": "https://api.github.com/repos/jadx-decompiler/jadx-plugins-list/zipball/v15" } ================================================ FILE: settings.gradle.kts ================================================ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version ("1.0.0") } if (!JavaVersion.current().isJava11Compatible) { throw GradleException("Jadx requires at least Java 11 for build (current version is '${JavaVersion.current()}')") } rootProject.name = "jadx" include("jadx-core") include("jadx-cli") include("jadx-gui") include("jadx-plugins-tools") include("jadx-commons:jadx-app-commons") include("jadx-commons:jadx-zip") include("jadx-plugins:jadx-input-api") include("jadx-plugins:jadx-dex-input") include("jadx-plugins:jadx-java-input") include("jadx-plugins:jadx-raung-input") include("jadx-plugins:jadx-smali-input") include("jadx-plugins:jadx-java-convert") include("jadx-plugins:jadx-rename-mappings") include("jadx-plugins:jadx-kotlin-metadata") include("jadx-plugins:jadx-kotlin-source-debug-extension") include("jadx-plugins:jadx-xapk-input") include("jadx-plugins:jadx-aab-input") include("jadx-plugins:jadx-apkm-input") include("jadx-plugins:jadx-apks-input")