From 628ac9fd336bf76a9e622aba03819e3c4135e217 Mon Sep 17 00:00:00 2001 From: solohsu Date: Sat, 19 Jan 2019 01:54:14 +0800 Subject: [PATCH] Migrate to GitHub --- .gitignore | 14 + Bridge/.gitignore | 1 + Bridge/build.gradle | 132 + Bridge/libs/framework-stub.jar | Bin 0 -> 15295 bytes Bridge/proguard-rules.pro | 30 + Bridge/src/main/AndroidManifest.xml | 2 + Bridge/src/main/apacheCommonsLang/LICENSE.txt | 202 + .../main/apacheCommonsLang/MODIFICATIONS.txt | 44 + Bridge/src/main/apacheCommonsLang/NOTICE.txt | 8 + .../main/apacheCommonsLang/RELEASE-NOTES.txt | 40 + .../org/apache/commons/lang3/ArrayUtils.java | 5797 +++++++++++++++ .../commons/lang3/CharSequenceUtils.java | 197 + .../org/apache/commons/lang3/CharUtils.java | 539 ++ .../org/apache/commons/lang3/ClassUtils.java | 1135 +++ .../org/apache/commons/lang3/JavaVersion.java | 168 + .../org/apache/commons/lang3/ObjectUtils.java | 609 ++ .../org/apache/commons/lang3/StringUtils.java | 6582 +++++++++++++++++ .../org/apache/commons/lang3/SystemUtils.java | 1443 ++++ .../org/apache/commons/lang3/Validate.java | 1070 +++ .../apache/commons/lang3/builder/Builder.java | 89 + .../lang3/builder/CompareToBuilder.java | 1020 +++ .../commons/lang3/builder/EqualsBuilder.java | 945 +++ .../lang3/builder/HashCodeBuilder.java | 961 +++ .../apache/commons/lang3/builder/IDKey.java | 74 + .../builder/ReflectionToStringBuilder.java | 691 ++ .../lang3/builder/ToStringBuilder.java | 1079 +++ .../commons/lang3/builder/ToStringStyle.java | 2271 ++++++ .../apache/commons/lang3/builder/package.html | 28 + .../lang3/exception/CloneFailedException.java | 62 + .../commons/lang3/exception/package.html | 27 + .../apache/commons/lang3/mutable/Mutable.java | 54 + .../commons/lang3/mutable/MutableInt.java | 273 + .../apache/commons/lang3/mutable/package.html | 29 + .../org/apache/commons/lang3/overview.html | 23 + .../org/apache/commons/lang3/package.html | 25 + .../commons/lang3/reflect/MemberUtils.java | 185 + .../commons/lang3/reflect/MethodUtils.java | 537 ++ .../apache/commons/lang3/reflect/package.html | 29 + .../commons/lang3/tuple/ImmutablePair.java | 103 + .../org/apache/commons/lang3/tuple/Pair.java | 177 + .../apache/commons/lang3/tuple/package.html | 22 + .../java/android/app/AndroidAppHelper.java | 223 + .../main/java/android/app/package-info.java | 4 + .../android/content/res/XModuleResources.java | 54 + .../android/content/res/XResForwarder.java | 34 + .../java/android/content/res/XResources.java | 1738 +++++ .../android/content/res/package-info.java | 4 + .../com/elderdrivers/riru/common/KeepAll.java | 4 + .../elderdrivers/riru/common/KeepMembers.java | 4 + .../com/elderdrivers/riru/xposed/Main.java | 83 + .../riru/xposed/core/HookMain.java | 167 + .../riru/xposed/core/HookMethodResolver.java | 156 + .../riru/xposed/dexmaker/DexLog.java | 37 + .../riru/xposed/dexmaker/DexMakerUtils.java | 186 + .../riru/xposed/dexmaker/DynamicBridge.java | 107 + .../riru/xposed/dexmaker/HookerDexMaker.java | 540 ++ .../riru/xposed/entry/Router.java | 49 + .../entry/bootstrap/AppBootstrapHookInfo.java | 16 + .../entry/bootstrap/SysBootstrapHookInfo.java | 18 + .../entry/bootstrap/SysInnerHookInfo.java | 10 + .../entry/hooker/HandleBindAppHooker.java | 74 + .../entry/hooker/InstrumentationHooker.java | 59 + .../hooker/LoadedApkConstructorHooker.java | 85 + .../xposed/entry/hooker/MakeAppHooker.java | 73 + .../hooker/StartBootstrapServicesHooker.java | 66 + .../xposed/entry/hooker/SystemMainHooker.java | 43 + .../entry/hooker/XposedInstallerHooker.java | 68 + .../riru/xposed/util/ClassLoaderUtils.java | 106 + .../riru/xposed/util/DexUtils.java | 67 + .../elderdrivers/riru/xposed/util/Utils.java | 45 + .../de/robv/android/xposed/DexCreator.java | 235 + .../android/xposed/GeneClass_Template.java | 74 + .../android/xposed/IXposedHookCmdInit.java | 28 + .../IXposedHookInitPackageResources.java | 36 + .../xposed/IXposedHookLoadPackage.java | 37 + .../android/xposed/IXposedHookZygoteInit.java | 35 + .../de/robv/android/xposed/IXposedMod.java | 4 + .../de/robv/android/xposed/SELinuxHelper.java | 83 + .../de/robv/android/xposed/XC_MethodHook.java | 168 + .../android/xposed/XC_MethodReplacement.java | 87 + .../android/xposed/XSharedPreferences.java | 295 + .../de/robv/android/xposed/XposedBridge.java | 529 ++ .../de/robv/android/xposed/XposedHelpers.java | 1725 +++++ .../de/robv/android/xposed/XposedInit.java | 234 + .../android/xposed/callbacks/IXUnhook.java | 25 + .../callbacks/XC_InitPackageResources.java | 57 + .../xposed/callbacks/XC_LayoutInflated.java | 99 + .../xposed/callbacks/XC_LoadPackage.java | 63 + .../android/xposed/callbacks/XCallback.java | 138 + .../xposed/callbacks/package-info.java | 9 + .../de/robv/android/xposed/package-info.java | 4 + .../android/xposed/services/BaseService.java | 179 + .../xposed/services/BinderService.java | 166 + .../xposed/services/DirectAccessService.java | 113 + .../android/xposed/services/FileResult.java | 60 + .../xposed/services/ZygoteService.java | 54 + .../android/xposed/services/package-info.java | 4 + Core/.gitignore | 7 + Core/build-module.sh | 16 + Core/build.gradle | 57 + Core/jni/.gitattributes | 1 + Core/jni/Android.mk | 3 + Core/jni/Application.mk | 6 + Core/jni/external/Android.mk | 16 + Core/jni/external/include/xhook/xhook.h | 50 + Core/jni/external/xhook/queue.h | 554 ++ Core/jni/external/xhook/tree.h | 768 ++ Core/jni/external/xhook/xh_core.c | 656 ++ Core/jni/external/xhook/xh_core.h | 48 + Core/jni/external/xhook/xh_elf.c | 1042 +++ Core/jni/external/xhook/xh_elf.h | 85 + Core/jni/external/xhook/xh_errno.h | 37 + Core/jni/external/xhook/xh_jni.c | 59 + Core/jni/external/xhook/xh_log.c | 27 + Core/jni/external/xhook/xh_log.h | 45 + Core/jni/external/xhook/xh_util.c | 121 + Core/jni/external/xhook/xh_util.h | 51 + Core/jni/external/xhook/xh_version.c | 66 + Core/jni/external/xhook/xh_version.h | 41 + Core/jni/external/xhook/xhook.c | 56 + Core/jni/external/xhook/xhook.h | 50 + Core/jni/main/Android.mk | 23 + Core/jni/main/include/JNIHelper.h | 73 + Core/jni/main/include/config.h | 20 + Core/jni/main/include/logging.h | 32 + Core/jni/main/include/misc.cpp | 91 + Core/jni/main/include/misc.h | 14 + Core/jni/main/include/riru.c | 76 + Core/jni/main/include/riru.h | 57 + Core/jni/main/inject/framework_hook.cpp | 91 + Core/jni/main/inject/framework_hook.h | 30 + Core/jni/main/java_hook/java_hook.cpp | 140 + Core/jni/main/java_hook/java_hook.h | 13 + Core/jni/main/main.cpp | 62 + Core/jni/main/native_hook/native_hook.cpp | 20 + Core/jni/main/native_hook/native_hook.h | 14 + Core/jni/main/yahfa/HookMain.c | 314 + Core/jni/main/yahfa/HookMain.h | 21 + Core/jni/main/yahfa/common.h | 29 + Core/jni/main/yahfa/env.h | 32 + Core/jni/main/yahfa/trampoline.c | 131 + Core/jni/main/yahfa/trampoline.h | 22 + Core/src/main/AndroidManifest.xml | 1 + Core/template_override/.gitattributes | 8 + .../META-INF/com/google/android/update-binary | 159 + .../com/google/android/updater-script | 1 + Core/template_override/common/post-fs-data.sh | 20 + Core/template_override/common/service.sh | 7 + Core/template_override/common/system.prop | 1 + Core/template_override/config.sh | 131 + Core/template_override/module.prop | 7 + Core/template_override/riru_module.prop | 5 + .../system/framework/edconfig.dex | 5 + .../system/framework/eddalvikdx.dex | Bin 0 -> 869496 bytes .../system/framework/eddexmaker.dex | Bin 0 -> 75744 bytes README.md | 28 +- build.gradle | 32 + build.sh | 72 + dalvikdx/.gitignore | 1 + dalvikdx/build.gradle | 8 + .../external/com/android/dex/Annotation.java | 65 + .../external/com/android/dex/CallSiteId.java | 55 + .../external/com/android/dex/ClassData.java | 104 + .../external/com/android/dex/ClassDef.java | 103 + .../java/external/com/android/dex/Code.java | 124 + .../java/external/com/android/dex/Dex.java | 819 ++ .../com/android/dex/DexException.java | 33 + .../external/com/android/dex/DexFormat.java | 170 + .../dex/DexIndexOverflowException.java | 30 + .../com/android/dex/EncodedValue.java | 59 + .../com/android/dex/EncodedValueCodec.java | 187 + .../com/android/dex/EncodedValueReader.java | 307 + .../external/com/android/dex/FieldId.java | 70 + .../java/external/com/android/dex/Leb128.java | 134 + .../com/android/dex/MethodHandle.java | 132 + .../external/com/android/dex/MethodId.java | 72 + .../java/external/com/android/dex/Mutf8.java | 115 + .../external/com/android/dex/ProtoId.java | 70 + .../java/external/com/android/dex/SizeOf.java | 123 + .../com/android/dex/TableOfContents.java | 246 + .../external/com/android/dex/TypeList.java | 57 + .../android/dex/util/ByteArrayByteInput.java | 32 + .../com/android/dex/util/ByteInput.java | 30 + .../com/android/dex/util/ByteOutput.java | 30 + .../dex/util/ExceptionWithContext.java | 148 + .../com/android/dex/util/FileUtils.java | 97 + .../com/android/dex/util/Unsigned.java | 42 + .../java/external/com/android/dx/Version.java | 25 + .../dx/cf/attrib/AttAnnotationDefault.java | 68 + .../dx/cf/attrib/AttBootstrapMethods.java | 59 + .../com/android/dx/cf/attrib/AttCode.java | 146 + .../dx/cf/attrib/AttConstantValue.java | 78 + .../android/dx/cf/attrib/AttDeprecated.java | 38 + .../dx/cf/attrib/AttEnclosingMethod.java | 79 + .../android/dx/cf/attrib/AttExceptions.java | 69 + .../android/dx/cf/attrib/AttInnerClasses.java | 65 + .../dx/cf/attrib/AttLineNumberTable.java | 66 + .../dx/cf/attrib/AttLocalVariableTable.java | 36 + .../cf/attrib/AttLocalVariableTypeTable.java | 36 + .../AttRuntimeInvisibleAnnotations.java | 40 + ...tRuntimeInvisibleParameterAnnotations.java | 42 + .../attrib/AttRuntimeVisibleAnnotations.java | 40 + ...AttRuntimeVisibleParameterAnnotations.java | 42 + .../android/dx/cf/attrib/AttSignature.java | 60 + .../dx/cf/attrib/AttSourceDebugExtension.java | 62 + .../android/dx/cf/attrib/AttSourceFile.java | 60 + .../android/dx/cf/attrib/AttSynthetic.java | 38 + .../android/dx/cf/attrib/BaseAnnotations.java | 73 + .../android/dx/cf/attrib/BaseAttribute.java | 47 + .../dx/cf/attrib/BaseLocalVariables.java | 66 + .../cf/attrib/BaseParameterAnnotations.java | 74 + .../android/dx/cf/attrib/InnerClassList.java | 137 + .../android/dx/cf/attrib/RawAttribute.java | 92 + .../com/android/dx/cf/attrib/package.html | 11 + .../com/android/dx/cf/code/BaseMachine.java | 595 ++ .../com/android/dx/cf/code/BasicBlocker.java | 465 ++ .../cf/code/BootstrapMethodArgumentsList.java | 75 + .../dx/cf/code/BootstrapMethodsList.java | 140 + .../com/android/dx/cf/code/ByteBlock.java | 146 + .../com/android/dx/cf/code/ByteBlockList.java | 74 + .../com/android/dx/cf/code/ByteCatchList.java | 317 + .../com/android/dx/cf/code/ByteOps.java | 650 ++ .../com/android/dx/cf/code/BytecodeArray.java | 1439 ++++ .../android/dx/cf/code/ConcreteMethod.java | 260 + .../android/dx/cf/code/ExecutionStack.java | 343 + .../com/android/dx/cf/code/Frame.java | 415 ++ .../android/dx/cf/code/LineNumberList.java | 184 + .../android/dx/cf/code/LocalVariableList.java | 373 + .../com/android/dx/cf/code/LocalsArray.java | 181 + .../android/dx/cf/code/LocalsArraySet.java | 461 ++ .../com/android/dx/cf/code/Machine.java | 216 + .../com/android/dx/cf/code/Merger.java | 305 + .../android/dx/cf/code/OneLocalsArray.java | 257 + .../com/android/dx/cf/code/ReturnAddress.java | 114 + .../com/android/dx/cf/code/Ropper.java | 1801 +++++ .../com/android/dx/cf/code/RopperMachine.java | 1032 +++ .../com/android/dx/cf/code/SimException.java | 37 + .../com/android/dx/cf/code/Simulator.java | 955 +++ .../com/android/dx/cf/code/SwitchList.java | 193 + .../android/dx/cf/code/ValueAwareMachine.java | 210 + .../com/android/dx/cf/code/package.html | 10 + .../android/dx/cf/cst/ConstantPoolParser.java | 450 ++ .../com/android/dx/cf/cst/ConstantTags.java | 64 + .../android/dx/cf/cst/MethodHandleKind.java | 49 + .../dx/cf/direct/AnnotationParser.java | 470 ++ .../dx/cf/direct/AttributeFactory.java | 134 + .../dx/cf/direct/AttributeListParser.java | 164 + .../android/dx/cf/direct/ClassPathOpener.java | 292 + .../android/dx/cf/direct/CodeObserver.java | 312 + .../android/dx/cf/direct/DirectClassFile.java | 688 ++ .../android/dx/cf/direct/FieldListParser.java | 86 + .../dx/cf/direct/MemberListParser.java | 240 + .../dx/cf/direct/MethodListParser.java | 86 + .../dx/cf/direct/StdAttributeFactory.java | 861 +++ .../com/android/dx/cf/direct/package.html | 12 + .../com/android/dx/cf/iface/Attribute.java | 38 + .../android/dx/cf/iface/AttributeList.java | 75 + .../com/android/dx/cf/iface/ClassFile.java | 131 + .../com/android/dx/cf/iface/Field.java | 35 + .../com/android/dx/cf/iface/FieldList.java | 48 + .../com/android/dx/cf/iface/HasAttribute.java | 32 + .../com/android/dx/cf/iface/Member.java | 75 + .../com/android/dx/cf/iface/Method.java | 34 + .../com/android/dx/cf/iface/MethodList.java | 47 + .../android/dx/cf/iface/ParseException.java | 37 + .../android/dx/cf/iface/ParseObserver.java | 68 + .../android/dx/cf/iface/StdAttributeList.java | 108 + .../com/android/dx/cf/iface/StdField.java | 55 + .../com/android/dx/cf/iface/StdFieldList.java | 50 + .../com/android/dx/cf/iface/StdMember.java | 116 + .../com/android/dx/cf/iface/StdMethod.java | 56 + .../android/dx/cf/iface/StdMethodList.java | 50 + .../com/android/dx/cf/iface/package.html | 10 + .../external/com/android/dx/command/Main.java | 178 + .../android/dx/command/UsageException.java | 25 + .../dx/command/annotool/AnnotationLister.java | 285 + .../com/android/dx/command/annotool/Main.java | 147 + .../android/dx/command/dexer/DxContext.java | 34 + .../com/android/dx/command/dexer/Main.java | 1974 +++++ .../com/android/dx/command/dump/Args.java | 56 + .../android/dx/command/dump/BaseDumper.java | 287 + .../android/dx/command/dump/BlockDumper.java | 344 + .../android/dx/command/dump/ClassDumper.java | 73 + .../android/dx/command/dump/DotDumper.java | 174 + .../com/android/dx/command/dump/Main.java | 134 + .../android/dx/command/dump/SsaDumper.java | 178 + .../dx/command/findusages/FindUsages.java | 211 + .../android/dx/command/findusages/Main.java | 35 + .../com/android/dx/command/grep/Grep.java | 131 + .../com/android/dx/command/grep/Main.java | 34 + .../com/android/dx/dex/DexOptions.java | 79 + .../dx/dex/cf/AttributeTranslator.java | 443 ++ .../com/android/dx/dex/cf/CfOptions.java | 49 + .../com/android/dx/dex/cf/CfTranslator.java | 442 ++ .../com/android/dx/dex/cf/CodeStatistics.java | 164 + .../android/dx/dex/cf/OptimizerOptions.java | 178 + .../com/android/dx/dex/cf/package.html | 15 + .../com/android/dx/dex/code/ArrayData.java | 201 + .../android/dx/dex/code/BlockAddresses.java | 143 + .../com/android/dx/dex/code/CatchBuilder.java | 47 + .../android/dx/dex/code/CatchHandlerList.java | 241 + .../com/android/dx/dex/code/CatchTable.java | 193 + .../com/android/dx/dex/code/CodeAddress.java | 91 + .../com/android/dx/dex/code/CstInsn.java | 236 + .../com/android/dx/dex/code/DalvCode.java | 231 + .../com/android/dx/dex/code/DalvInsn.java | 487 ++ .../com/android/dx/dex/code/DalvInsnList.java | 286 + .../external/com/android/dx/dex/code/Dop.java | 173 + .../com/android/dx/dex/code/Dops.java | 1261 ++++ .../android/dx/dex/code/FixedSizeInsn.java | 73 + .../dx/dex/code/HighRegisterPrefix.java | 146 + .../com/android/dx/dex/code/InsnFormat.java | 668 ++ .../com/android/dx/dex/code/LocalList.java | 947 +++ .../android/dx/dex/code/LocalSnapshot.java | 103 + .../com/android/dx/dex/code/LocalStart.java | 105 + .../com/android/dx/dex/code/MultiCstInsn.java | 254 + .../com/android/dx/dex/code/OddSpacer.java | 76 + .../android/dx/dex/code/OutputCollector.java | 121 + .../android/dx/dex/code/OutputFinisher.java | 938 +++ .../com/android/dx/dex/code/PositionList.java | 192 + .../com/android/dx/dex/code/RopToDop.java | 599 ++ .../android/dx/dex/code/RopTranslator.java | 913 +++ .../com/android/dx/dex/code/SimpleInsn.java | 59 + .../android/dx/dex/code/StdCatchBuilder.java | 318 + .../com/android/dx/dex/code/SwitchData.java | 258 + .../com/android/dx/dex/code/TargetInsn.java | 132 + .../android/dx/dex/code/VariableSizeInsn.java | 49 + .../com/android/dx/dex/code/ZeroSizeInsn.java | 62 + .../com/android/dx/dex/code/form/Form10t.java | 86 + .../com/android/dx/dex/code/form/Form10x.java | 72 + .../com/android/dx/dex/code/form/Form11n.java | 109 + .../com/android/dx/dex/code/form/Form11x.java | 87 + .../com/android/dx/dex/code/form/Form12x.java | 161 + .../com/android/dx/dex/code/form/Form20t.java | 86 + .../com/android/dx/dex/code/form/Form21c.java | 152 + .../com/android/dx/dex/code/form/Form21h.java | 125 + .../com/android/dx/dex/code/form/Form21s.java | 109 + .../com/android/dx/dex/code/form/Form21t.java | 105 + .../com/android/dx/dex/code/form/Form22b.java | 112 + .../com/android/dx/dex/code/form/Form22c.java | 114 + .../com/android/dx/dex/code/form/Form22s.java | 113 + .../com/android/dx/dex/code/form/Form22t.java | 109 + .../com/android/dx/dex/code/form/Form22x.java | 92 + .../com/android/dx/dex/code/form/Form23x.java | 95 + .../com/android/dx/dex/code/form/Form30t.java | 82 + .../com/android/dx/dex/code/form/Form31c.java | 141 + .../com/android/dx/dex/code/form/Form31i.java | 105 + .../com/android/dx/dex/code/form/Form31t.java | 99 + .../com/android/dx/dex/code/form/Form32x.java | 93 + .../com/android/dx/dex/code/form/Form35c.java | 212 + .../com/android/dx/dex/code/form/Form3rc.java | 108 + .../android/dx/dex/code/form/Form45cc.java | 222 + .../android/dx/dex/code/form/Form4rcc.java | 120 + .../com/android/dx/dex/code/form/Form51l.java | 102 + .../dx/dex/code/form/SpecialFormat.java | 72 + .../android/dx/dex/file/AnnotationItem.java | 222 + .../dx/dex/file/AnnotationSetItem.java | 159 + .../dx/dex/file/AnnotationSetRefItem.java | 81 + .../android/dx/dex/file/AnnotationUtils.java | 269 + .../dx/dex/file/AnnotationsDirectoryItem.java | 389 + .../android/dx/dex/file/CallSiteIdItem.java | 90 + .../dx/dex/file/CallSiteIdsSection.java | 130 + .../com/android/dx/dex/file/CallSiteItem.java | 105 + .../com/android/dx/dex/file/CatchStructs.java | 314 + .../android/dx/dex/file/ClassDataItem.java | 425 ++ .../com/android/dx/dex/file/ClassDefItem.java | 411 + .../android/dx/dex/file/ClassDefsSection.java | 186 + .../com/android/dx/dex/file/CodeItem.java | 327 + .../dx/dex/file/DebugInfoConstants.java | 154 + .../android/dx/dex/file/DebugInfoDecoder.java | 597 ++ .../android/dx/dex/file/DebugInfoEncoder.java | 932 +++ .../android/dx/dex/file/DebugInfoItem.java | 193 + .../com/android/dx/dex/file/DexFile.java | 805 ++ .../android/dx/dex/file/EncodedArrayItem.java | 123 + .../com/android/dx/dex/file/EncodedField.java | 157 + .../android/dx/dex/file/EncodedMember.java | 85 + .../android/dx/dex/file/EncodedMethod.java | 198 + .../dx/dex/file/FieldAnnotationStruct.java | 126 + .../com/android/dx/dex/file/FieldIdItem.java | 70 + .../android/dx/dex/file/FieldIdsSection.java | 136 + .../com/android/dx/dex/file/HeaderItem.java | 115 + .../android/dx/dex/file/HeaderSection.java | 62 + .../com/android/dx/dex/file/IdItem.java | 61 + .../com/android/dx/dex/file/IndexedItem.java | 81 + .../com/android/dx/dex/file/Item.java | 80 + .../com/android/dx/dex/file/ItemType.java | 100 + .../com/android/dx/dex/file/MapItem.java | 234 + .../com/android/dx/dex/file/MemberIdItem.java | 109 + .../android/dx/dex/file/MemberIdsSection.java | 86 + .../dx/dex/file/MethodAnnotationStruct.java | 126 + .../android/dx/dex/file/MethodHandleItem.java | 104 + .../dx/dex/file/MethodHandlesSection.java | 75 + .../com/android/dx/dex/file/MethodIdItem.java | 70 + .../android/dx/dex/file/MethodIdsSection.java | 136 + .../android/dx/dex/file/MixedItemSection.java | 362 + .../android/dx/dex/file/OffsettedItem.java | 315 + .../dex/file/ParameterAnnotationStruct.java | 165 + .../com/android/dx/dex/file/ProtoIdItem.java | 159 + .../android/dx/dex/file/ProtoIdsSection.java | 155 + .../com/android/dx/dex/file/Section.java | 286 + .../com/android/dx/dex/file/Statistics.java | 194 + .../android/dx/dex/file/StringDataItem.java | 99 + .../com/android/dx/dex/file/StringIdItem.java | 127 + .../android/dx/dex/file/StringIdsSection.java | 181 + .../com/android/dx/dex/file/TypeIdItem.java | 70 + .../android/dx/dex/file/TypeIdsSection.java | 197 + .../com/android/dx/dex/file/TypeListItem.java | 122 + .../dx/dex/file/UniformItemSection.java | 111 + .../android/dx/dex/file/UniformListItem.java | 214 + .../com/android/dx/dex/file/ValueEncoder.java | 447 ++ .../com/android/dx/io/CodeReader.java | 137 + .../com/android/dx/io/DexIndexPrinter.java | 129 + .../external/com/android/dx/io/IndexType.java | 64 + .../com/android/dx/io/OpcodeInfo.java | 1295 ++++ .../external/com/android/dx/io/Opcodes.java | 353 + .../dx/io/instructions/AddressMap.java | 51 + .../dx/io/instructions/BaseCodeCursor.java | 62 + .../dx/io/instructions/CodeCursor.java | 47 + .../android/dx/io/instructions/CodeInput.java | 45 + .../dx/io/instructions/CodeOutput.java | 77 + .../io/instructions/DecodedInstruction.java | 488 ++ ...illArrayDataPayloadDecodedInstruction.java | 102 + .../FiveRegisterDecodedInstruction.java | 98 + .../FourRegisterDecodedInstruction.java | 88 + .../dx/io/instructions/InstructionCodec.java | 1112 +++ .../InvokePolymorphicDecodedInstruction.java | 87 + ...okePolymorphicRangeDecodedInstruction.java | 78 + .../OneRegisterDecodedInstruction.java | 58 + ...PackedSwitchPayloadDecodedInstruction.java | 64 + .../RegisterRangeDecodedInstruction.java | 63 + .../io/instructions/ShortArrayCodeInput.java | 77 + .../io/instructions/ShortArrayCodeOutput.java | 157 + ...SparseSwitchPayloadDecodedInstruction.java | 68 + .../ThreeRegisterDecodedInstruction.java | 78 + .../TwoRegisterDecodedInstruction.java | 68 + .../ZeroRegisterDecodedInstruction.java | 46 + .../com/android/dx/merge/CollisionPolicy.java | 34 + .../com/android/dx/merge/DexMerger.java | 1201 +++ .../com/android/dx/merge/IndexMap.java | 389 + .../dx/merge/InstructionTransformer.java | 141 + .../com/android/dx/merge/SortableType.java | 117 + .../android/dx/rop/annotation/Annotation.java | 226 + .../rop/annotation/AnnotationVisibility.java | 47 + .../dx/rop/annotation/Annotations.java | 214 + .../dx/rop/annotation/AnnotationsList.java | 91 + .../dx/rop/annotation/NameValuePair.java | 110 + .../com/android/dx/rop/code/AccessFlags.java | 406 + .../com/android/dx/rop/code/BasicBlock.java | 283 + .../android/dx/rop/code/BasicBlockList.java | 408 + .../code/ConservativeTranslationAdvice.java | 55 + .../com/android/dx/rop/code/CstInsn.java | 74 + .../dx/rop/code/DexTranslationAdvice.java | 133 + .../com/android/dx/rop/code/Exceptions.java | 133 + .../dx/rop/code/FillArrayDataInsn.java | 115 + .../com/android/dx/rop/code/Insn.java | 476 ++ .../com/android/dx/rop/code/InsnList.java | 130 + .../dx/rop/code/InvokePolymorphicInsn.java | 239 + .../com/android/dx/rop/code/LocalItem.java | 144 + .../dx/rop/code/LocalVariableExtractor.java | 191 + .../dx/rop/code/LocalVariableInfo.java | 253 + .../com/android/dx/rop/code/PlainCstInsn.java | 87 + .../com/android/dx/rop/code/PlainInsn.java | 157 + .../com/android/dx/rop/code/RegOps.java | 417 ++ .../com/android/dx/rop/code/RegisterSpec.java | 686 ++ .../android/dx/rop/code/RegisterSpecList.java | 441 ++ .../android/dx/rop/code/RegisterSpecSet.java | 396 + .../external/com/android/dx/rop/code/Rop.java | 407 + .../com/android/dx/rop/code/RopMethod.java | 206 + .../com/android/dx/rop/code/Rops.java | 2131 ++++++ .../android/dx/rop/code/SourcePosition.java | 168 + .../com/android/dx/rop/code/SwitchInsn.java | 119 + .../android/dx/rop/code/ThrowingCstInsn.java | 110 + .../com/android/dx/rop/code/ThrowingInsn.java | 120 + .../dx/rop/code/TranslationAdvice.java | 62 + .../com/android/dx/rop/code/package.html | 8 + .../com/android/dx/rop/cst/Constant.java | 69 + .../com/android/dx/rop/cst/ConstantPool.java | 77 + .../com/android/dx/rop/cst/CstAnnotation.java | 97 + .../com/android/dx/rop/cst/CstArray.java | 160 + .../android/dx/rop/cst/CstBaseMethodRef.java | 216 + .../com/android/dx/rop/cst/CstBoolean.java | 101 + .../com/android/dx/rop/cst/CstByte.java | 101 + .../com/android/dx/rop/cst/CstCallSite.java | 110 + .../android/dx/rop/cst/CstCallSiteRef.java | 110 + .../com/android/dx/rop/cst/CstChar.java | 101 + .../com/android/dx/rop/cst/CstDouble.java | 92 + .../com/android/dx/rop/cst/CstEnumRef.java | 69 + .../com/android/dx/rop/cst/CstFieldRef.java | 80 + .../com/android/dx/rop/cst/CstFloat.java | 93 + .../com/android/dx/rop/cst/CstInteger.java | 118 + .../dx/rop/cst/CstInterfaceMethodRef.java | 60 + .../android/dx/rop/cst/CstInvokeDynamic.java | 224 + .../com/android/dx/rop/cst/CstKnownNull.java | 112 + .../com/android/dx/rop/cst/CstLiteral32.java | 87 + .../com/android/dx/rop/cst/CstLiteral64.java | 87 + .../android/dx/rop/cst/CstLiteralBits.java | 82 + .../com/android/dx/rop/cst/CstLong.java | 89 + .../com/android/dx/rop/cst/CstMemberRef.java | 123 + .../android/dx/rop/cst/CstMethodHandle.java | 205 + .../com/android/dx/rop/cst/CstMethodRef.java | 39 + .../com/android/dx/rop/cst/CstNat.java | 171 + .../com/android/dx/rop/cst/CstProtoRef.java | 99 + .../com/android/dx/rop/cst/CstShort.java | 102 + .../com/android/dx/rop/cst/CstString.java | 377 + .../com/android/dx/rop/cst/CstType.java | 299 + .../android/dx/rop/cst/StdConstantPool.java | 153 + .../com/android/dx/rop/cst/TypedConstant.java | 53 + .../com/android/dx/rop/cst/Zeroes.java | 55 + .../com/android/dx/rop/cst/package.html | 9 + .../com/android/dx/rop/package-info.java | 200 + .../com/android/dx/rop/type/Prototype.java | 421 ++ .../com/android/dx/rop/type/StdTypeList.java | 410 + .../com/android/dx/rop/type/Type.java | 913 +++ .../com/android/dx/rop/type/TypeBearer.java | 74 + .../com/android/dx/rop/type/TypeList.java | 69 + .../com/android/dx/rop/type/package.html | 8 + .../android/dx/ssa/BasicRegisterMapper.java | 128 + .../com/android/dx/ssa/ConstCollector.java | 400 + .../com/android/dx/ssa/DeadCodeRemover.java | 271 + .../external/com/android/dx/ssa/DomFront.java | 200 + .../com/android/dx/ssa/Dominators.java | 286 + .../com/android/dx/ssa/EscapeAnalysis.java | 846 +++ .../dx/ssa/InterferenceRegisterMapper.java | 162 + .../com/android/dx/ssa/LiteralOpUpgrader.java | 209 + .../dx/ssa/LocalVariableExtractor.java | 207 + .../com/android/dx/ssa/LocalVariableInfo.java | 250 + .../com/android/dx/ssa/MoveParamCombiner.java | 159 + .../com/android/dx/ssa/NormalSsaInsn.java | 242 + .../com/android/dx/ssa/Optimizer.java | 256 + .../external/com/android/dx/ssa/PhiInsn.java | 403 + .../com/android/dx/ssa/PhiTypeResolver.java | 199 + .../com/android/dx/ssa/RegisterMapper.java | 83 + .../external/com/android/dx/ssa/SCCP.java | 680 ++ .../com/android/dx/ssa/SetFactory.java | 95 + .../com/android/dx/ssa/SsaBasicBlock.java | 1011 +++ .../com/android/dx/ssa/SsaConverter.java | 403 + .../external/com/android/dx/ssa/SsaInsn.java | 293 + .../com/android/dx/ssa/SsaMethod.java | 857 +++ .../com/android/dx/ssa/SsaRenamer.java | 667 ++ .../dx/ssa/back/FirstFitAllocator.java | 149 + .../back/FirstFitLocalCombiningAllocator.java | 1259 ++++ .../dx/ssa/back/IdenticalBlockCombiner.java | 177 + .../dx/ssa/back/InterferenceGraph.java | 103 + .../android/dx/ssa/back/LivenessAnalyzer.java | 289 + .../dx/ssa/back/NullRegisterAllocator.java | 55 + .../dx/ssa/back/RegisterAllocator.java | 195 + .../com/android/dx/ssa/back/SsaToRop.java | 384 + .../com/android/dx/ssa/package-info.java | 103 + .../com/android/dx/util/AnnotatedOutput.java | 79 + .../com/android/dx/util/BitIntSet.java | 154 + .../external/com/android/dx/util/Bits.java | 236 + .../com/android/dx/util/ByteArray.java | 364 + .../dx/util/ByteArrayAnnotatedOutput.java | 655 ++ .../com/android/dx/util/FixedSizeList.java | 277 + .../external/com/android/dx/util/Hex.java | 303 + .../com/android/dx/util/HexParser.java | 145 + .../com/android/dx/util/IndentingWriter.java | 169 + .../com/android/dx/util/IntIterator.java | 38 + .../external/com/android/dx/util/IntList.java | 453 ++ .../external/com/android/dx/util/IntSet.java | 67 + .../com/android/dx/util/LabeledItem.java | 30 + .../com/android/dx/util/LabeledList.java | 187 + .../com/android/dx/util/ListIntSet.java | 141 + .../android/dx/util/MutabilityControl.java | 89 + .../android/dx/util/MutabilityException.java | 37 + .../external/com/android/dx/util/Output.java | 132 + .../external/com/android/dx/util/ToHuman.java | 31 + .../com/android/dx/util/TwoColumnOutput.java | 254 + .../external/com/android/dx/util/Warning.java | 31 + .../external/com/android/dx/util/Writers.java | 48 + .../external/com/android/dx/util/package.html | 3 + .../android/multidex/ArchivePathElement.java | 100 + .../android/multidex/ClassPathElement.java | 41 + .../multidex/ClassReferenceListBuilder.java | 167 + .../android/multidex/FolderPathElement.java | 63 + .../android/multidex/MainDexListBuilder.java | 194 + .../external/com/android/multidex/Path.java | 118 + dexmaker/.gitignore | 1 + dexmaker/build.gradle | 30 + .../external/com/android/dx/AnnotationId.java | 257 + .../com/android/dx/AppDataDirGuesser.java | 225 + .../external/com/android/dx/BinaryOp.java | 129 + .../java/external/com/android/dx/Code.java | 924 +++ .../external/com/android/dx/Comparison.java | 77 + .../external/com/android/dx/Constants.java | 72 + .../external/com/android/dx/DexMaker.java | 655 ++ .../java/external/com/android/dx/FieldId.java | 77 + .../java/external/com/android/dx/Label.java | 92 + .../java/external/com/android/dx/Local.java | 75 + .../external/com/android/dx/MethodId.java | 131 + .../java/external/com/android/dx/TypeId.java | 153 + .../external/com/android/dx/TypeList.java | 67 + .../java/external/com/android/dx/UnaryOp.java | 44 + .../com/android/dx/stock/ProxyBuilder.java | 994 +++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 + gradlew.bat | 84 + hiddenapistubs/.gitignore | 1 + hiddenapistubs/build.gradle | 23 + hiddenapistubs/src/main/AndroidManifest.xml | 3 + .../main/java/android/app/ActivityThread.java | 22 + .../src/main/java/android/app/LoadedApk.java | 21 + .../android/content/pm/PackageParser.java | 24 + .../android/content/res/AssetManager.java | 18 + .../content/res/CompatibilityInfo.java | 18 + .../java/android/content/res/Resources.java | 196 + .../java/android/content/res/TypedArray.java | 90 + .../src/main/java/android/os/SELinux.java | 15 + .../main/java/android/os/ServiceManager.java | 7 + .../com/android/internal/os/RuntimeInit.java | 7 + .../com/android/internal/os/ZygoteInit.java | 7 + .../com/android/internal/util/XmlUtils.java | 14 + .../dalvik/system/BaseDexClassLoader.java | 44 + .../xposed/dummy/XResourcesSuperClass.java | 19 + .../xposed/dummy/XTypedArraySuperClass.java | 20 + settings.gradle | 1 + 617 files changed, 134710 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 Bridge/.gitignore create mode 100644 Bridge/build.gradle create mode 100644 Bridge/libs/framework-stub.jar create mode 100644 Bridge/proguard-rules.pro create mode 100644 Bridge/src/main/AndroidManifest.xml create mode 100644 Bridge/src/main/apacheCommonsLang/LICENSE.txt create mode 100644 Bridge/src/main/apacheCommonsLang/MODIFICATIONS.txt create mode 100644 Bridge/src/main/apacheCommonsLang/NOTICE.txt create mode 100644 Bridge/src/main/apacheCommonsLang/RELEASE-NOTES.txt create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ArrayUtils.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/CharSequenceUtils.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/CharUtils.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ClassUtils.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/JavaVersion.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ObjectUtils.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/StringUtils.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/SystemUtils.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/Validate.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/Builder.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/CompareToBuilder.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/EqualsBuilder.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/HashCodeBuilder.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/IDKey.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ToStringBuilder.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ToStringStyle.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/package.html create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/exception/CloneFailedException.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/exception/package.html create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/Mutable.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/MutableInt.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/package.html create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/overview.html create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/package.html create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/MemberUtils.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/MethodUtils.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/package.html create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/ImmutablePair.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/Pair.java create mode 100644 Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/package.html create mode 100644 Bridge/src/main/java/android/app/AndroidAppHelper.java create mode 100644 Bridge/src/main/java/android/app/package-info.java create mode 100644 Bridge/src/main/java/android/content/res/XModuleResources.java create mode 100644 Bridge/src/main/java/android/content/res/XResForwarder.java create mode 100644 Bridge/src/main/java/android/content/res/XResources.java create mode 100644 Bridge/src/main/java/android/content/res/package-info.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/common/KeepAll.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/common/KeepMembers.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/Main.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/core/HookMain.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/core/HookMethodResolver.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DexLog.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DexMakerUtils.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DynamicBridge.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/HookerDexMaker.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/Router.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/AppBootstrapHookInfo.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/SysBootstrapHookInfo.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/SysInnerHookInfo.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/HandleBindAppHooker.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/InstrumentationHooker.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/LoadedApkConstructorHooker.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/MakeAppHooker.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/StartBootstrapServicesHooker.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/SystemMainHooker.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/XposedInstallerHooker.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/util/ClassLoaderUtils.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/util/DexUtils.java create mode 100644 Bridge/src/main/java/com/elderdrivers/riru/xposed/util/Utils.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/DexCreator.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/GeneClass_Template.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/IXposedMod.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/SELinuxHelper.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/XC_MethodHook.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/XSharedPreferences.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/XposedBridge.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/XposedHelpers.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/XposedInit.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/callbacks/XCallback.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/callbacks/package-info.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/package-info.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/services/BaseService.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/services/BinderService.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/services/DirectAccessService.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/services/FileResult.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/services/ZygoteService.java create mode 100644 Bridge/src/main/java/de/robv/android/xposed/services/package-info.java create mode 100644 Core/.gitignore create mode 100644 Core/build-module.sh create mode 100644 Core/build.gradle create mode 100644 Core/jni/.gitattributes create mode 100644 Core/jni/Android.mk create mode 100644 Core/jni/Application.mk create mode 100644 Core/jni/external/Android.mk create mode 100644 Core/jni/external/include/xhook/xhook.h create mode 100644 Core/jni/external/xhook/queue.h create mode 100644 Core/jni/external/xhook/tree.h create mode 100644 Core/jni/external/xhook/xh_core.c create mode 100644 Core/jni/external/xhook/xh_core.h create mode 100644 Core/jni/external/xhook/xh_elf.c create mode 100644 Core/jni/external/xhook/xh_elf.h create mode 100644 Core/jni/external/xhook/xh_errno.h create mode 100644 Core/jni/external/xhook/xh_jni.c create mode 100644 Core/jni/external/xhook/xh_log.c create mode 100644 Core/jni/external/xhook/xh_log.h create mode 100644 Core/jni/external/xhook/xh_util.c create mode 100644 Core/jni/external/xhook/xh_util.h create mode 100644 Core/jni/external/xhook/xh_version.c create mode 100644 Core/jni/external/xhook/xh_version.h create mode 100644 Core/jni/external/xhook/xhook.c create mode 100644 Core/jni/external/xhook/xhook.h create mode 100644 Core/jni/main/Android.mk create mode 100644 Core/jni/main/include/JNIHelper.h create mode 100644 Core/jni/main/include/config.h create mode 100644 Core/jni/main/include/logging.h create mode 100644 Core/jni/main/include/misc.cpp create mode 100644 Core/jni/main/include/misc.h create mode 100644 Core/jni/main/include/riru.c create mode 100644 Core/jni/main/include/riru.h create mode 100644 Core/jni/main/inject/framework_hook.cpp create mode 100644 Core/jni/main/inject/framework_hook.h create mode 100644 Core/jni/main/java_hook/java_hook.cpp create mode 100644 Core/jni/main/java_hook/java_hook.h create mode 100644 Core/jni/main/main.cpp create mode 100644 Core/jni/main/native_hook/native_hook.cpp create mode 100644 Core/jni/main/native_hook/native_hook.h create mode 100644 Core/jni/main/yahfa/HookMain.c create mode 100644 Core/jni/main/yahfa/HookMain.h create mode 100644 Core/jni/main/yahfa/common.h create mode 100644 Core/jni/main/yahfa/env.h create mode 100644 Core/jni/main/yahfa/trampoline.c create mode 100644 Core/jni/main/yahfa/trampoline.h create mode 100644 Core/src/main/AndroidManifest.xml create mode 100644 Core/template_override/.gitattributes create mode 100644 Core/template_override/META-INF/com/google/android/update-binary create mode 100644 Core/template_override/META-INF/com/google/android/updater-script create mode 100644 Core/template_override/common/post-fs-data.sh create mode 100644 Core/template_override/common/service.sh create mode 100644 Core/template_override/common/system.prop create mode 100644 Core/template_override/config.sh create mode 100644 Core/template_override/module.prop create mode 100644 Core/template_override/riru_module.prop create mode 100644 Core/template_override/system/framework/edconfig.dex create mode 100644 Core/template_override/system/framework/eddalvikdx.dex create mode 100644 Core/template_override/system/framework/eddexmaker.dex create mode 100644 build.gradle create mode 100644 build.sh create mode 100644 dalvikdx/.gitignore create mode 100644 dalvikdx/build.gradle create mode 100644 dalvikdx/src/main/java/external/com/android/dex/Annotation.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/CallSiteId.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/ClassData.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/ClassDef.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/Code.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/Dex.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/DexException.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/DexFormat.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/DexIndexOverflowException.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/EncodedValue.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/EncodedValueCodec.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/EncodedValueReader.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/FieldId.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/Leb128.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/MethodHandle.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/MethodId.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/Mutf8.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/ProtoId.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/SizeOf.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/TableOfContents.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/TypeList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/util/ByteArrayByteInput.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/util/ByteInput.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/util/ByteOutput.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/util/ExceptionWithContext.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/util/FileUtils.java create mode 100644 dalvikdx/src/main/java/external/com/android/dex/util/Unsigned.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/Version.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttAnnotationDefault.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttBootstrapMethods.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttCode.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttConstantValue.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttDeprecated.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttEnclosingMethod.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttExceptions.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttInnerClasses.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLineNumberTable.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLocalVariableTable.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLocalVariableTypeTable.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeInvisibleAnnotations.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeInvisibleParameterAnnotations.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeVisibleAnnotations.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeVisibleParameterAnnotations.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSignature.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSourceDebugExtension.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSourceFile.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSynthetic.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseAnnotations.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseAttribute.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseLocalVariables.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseParameterAnnotations.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/InnerClassList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/RawAttribute.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/attrib/package.html create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/BaseMachine.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/BasicBlocker.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/BootstrapMethodArgumentsList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/BootstrapMethodsList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteBlock.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteBlockList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteCatchList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteOps.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/BytecodeArray.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/ConcreteMethod.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/ExecutionStack.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/Frame.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/LineNumberList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalVariableList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalsArray.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalsArraySet.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/Machine.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/Merger.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/OneLocalsArray.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/ReturnAddress.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/Ropper.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/RopperMachine.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/SimException.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/Simulator.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/SwitchList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/ValueAwareMachine.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/code/package.html create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/cst/ConstantPoolParser.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/cst/ConstantTags.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/cst/MethodHandleKind.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/direct/AnnotationParser.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/direct/AttributeFactory.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/direct/AttributeListParser.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/direct/ClassPathOpener.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/direct/CodeObserver.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/direct/DirectClassFile.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/direct/FieldListParser.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/direct/MemberListParser.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/direct/MethodListParser.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/direct/StdAttributeFactory.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/direct/package.html create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/Attribute.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/AttributeList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/ClassFile.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/Field.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/FieldList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/HasAttribute.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/Member.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/Method.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/MethodList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/ParseException.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/ParseObserver.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdAttributeList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdField.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdFieldList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMember.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMethod.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMethodList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/cf/iface/package.html create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/Main.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/UsageException.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/annotool/AnnotationLister.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/annotool/Main.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/dexer/DxContext.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/dexer/Main.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/dump/Args.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/dump/BaseDumper.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/dump/BlockDumper.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/dump/ClassDumper.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/dump/DotDumper.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/dump/Main.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/dump/SsaDumper.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/findusages/FindUsages.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/findusages/Main.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/grep/Grep.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/command/grep/Main.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/DexOptions.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/cf/AttributeTranslator.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/cf/CfOptions.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/cf/CfTranslator.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/cf/CodeStatistics.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/cf/OptimizerOptions.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/cf/package.html create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/ArrayData.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/BlockAddresses.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/CatchBuilder.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/CatchHandlerList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/CatchTable.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/CodeAddress.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/CstInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/DalvCode.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/DalvInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/DalvInsnList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/Dop.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/Dops.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/FixedSizeInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/HighRegisterPrefix.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/InsnFormat.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/LocalList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/LocalSnapshot.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/LocalStart.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/MultiCstInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/OddSpacer.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/OutputCollector.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/OutputFinisher.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/PositionList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/RopToDop.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/RopTranslator.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/SimpleInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/StdCatchBuilder.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/SwitchData.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/TargetInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/VariableSizeInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/ZeroSizeInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form10t.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form10x.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form11n.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form11x.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form12x.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form20t.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form21c.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form21h.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form21s.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form21t.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form22b.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form22c.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form22s.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form22t.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form22x.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form23x.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form30t.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form31c.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form31i.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form31t.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form32x.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form35c.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form3rc.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form45cc.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form4rcc.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/Form51l.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/code/form/SpecialFormat.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/AnnotationItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/AnnotationSetItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/AnnotationSetRefItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/AnnotationUtils.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/AnnotationsDirectoryItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/CallSiteIdItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/CallSiteIdsSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/CallSiteItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/CatchStructs.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/ClassDataItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/ClassDefItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/ClassDefsSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/CodeItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/DebugInfoConstants.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/DebugInfoDecoder.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/DebugInfoEncoder.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/DebugInfoItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/DexFile.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/EncodedArrayItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/EncodedField.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/EncodedMember.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/EncodedMethod.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/FieldAnnotationStruct.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/FieldIdItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/FieldIdsSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/HeaderItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/HeaderSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/IdItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/IndexedItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/Item.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/ItemType.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/MapItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/MemberIdItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/MemberIdsSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/MethodAnnotationStruct.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/MethodHandleItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/MethodHandlesSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/MethodIdItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/MethodIdsSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/MixedItemSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/OffsettedItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/ParameterAnnotationStruct.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/ProtoIdItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/ProtoIdsSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/Section.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/Statistics.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/StringDataItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/StringIdItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/StringIdsSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/TypeIdItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/TypeIdsSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/TypeListItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/UniformItemSection.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/UniformListItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/dex/file/ValueEncoder.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/CodeReader.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/DexIndexPrinter.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/IndexType.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/OpcodeInfo.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/Opcodes.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/AddressMap.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/BaseCodeCursor.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/CodeCursor.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/CodeInput.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/CodeOutput.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/DecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/FillArrayDataPayloadDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/FiveRegisterDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/FourRegisterDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/InstructionCodec.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/InvokePolymorphicDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/InvokePolymorphicRangeDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/OneRegisterDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/PackedSwitchPayloadDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/RegisterRangeDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/ShortArrayCodeInput.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/ShortArrayCodeOutput.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/SparseSwitchPayloadDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/ThreeRegisterDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/TwoRegisterDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/io/instructions/ZeroRegisterDecodedInstruction.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/merge/CollisionPolicy.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/merge/DexMerger.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/merge/IndexMap.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/merge/InstructionTransformer.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/merge/SortableType.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/annotation/Annotation.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/annotation/AnnotationVisibility.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/annotation/Annotations.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/annotation/AnnotationsList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/annotation/NameValuePair.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/AccessFlags.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/BasicBlock.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/BasicBlockList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/ConservativeTranslationAdvice.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/CstInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/DexTranslationAdvice.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/Exceptions.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/FillArrayDataInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/Insn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/InsnList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/InvokePolymorphicInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/LocalItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/LocalVariableExtractor.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/LocalVariableInfo.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/PlainCstInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/PlainInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/RegOps.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/RegisterSpec.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/RegisterSpecList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/RegisterSpecSet.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/Rop.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/RopMethod.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/Rops.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/SourcePosition.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/SwitchInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/ThrowingCstInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/ThrowingInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/TranslationAdvice.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/code/package.html create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/Constant.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/ConstantPool.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstAnnotation.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstArray.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstBaseMethodRef.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstBoolean.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstByte.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstCallSite.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstCallSiteRef.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstChar.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstDouble.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstEnumRef.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstFieldRef.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstFloat.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstInteger.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstInterfaceMethodRef.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstInvokeDynamic.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstKnownNull.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstLiteral32.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstLiteral64.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstLiteralBits.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstLong.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstMemberRef.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstMethodHandle.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstMethodRef.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstNat.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstProtoRef.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstShort.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstString.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/CstType.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/StdConstantPool.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/TypedConstant.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/Zeroes.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/cst/package.html create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/package-info.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/type/Prototype.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/type/StdTypeList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/type/Type.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/type/TypeBearer.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/type/TypeList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/rop/type/package.html create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/BasicRegisterMapper.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/ConstCollector.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/DeadCodeRemover.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/DomFront.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/Dominators.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/EscapeAnalysis.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/InterferenceRegisterMapper.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/LiteralOpUpgrader.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/LocalVariableExtractor.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/LocalVariableInfo.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/MoveParamCombiner.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/NormalSsaInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/Optimizer.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/PhiInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/PhiTypeResolver.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/RegisterMapper.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/SCCP.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/SetFactory.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/SsaBasicBlock.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/SsaConverter.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/SsaInsn.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/SsaMethod.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/SsaRenamer.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/back/FirstFitAllocator.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/back/FirstFitLocalCombiningAllocator.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/back/IdenticalBlockCombiner.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/back/InterferenceGraph.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/back/LivenessAnalyzer.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/back/NullRegisterAllocator.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/back/RegisterAllocator.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/back/SsaToRop.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/ssa/package-info.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/AnnotatedOutput.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/BitIntSet.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/Bits.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/ByteArray.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/ByteArrayAnnotatedOutput.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/FixedSizeList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/Hex.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/HexParser.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/IndentingWriter.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/IntIterator.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/IntList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/IntSet.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/LabeledItem.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/LabeledList.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/ListIntSet.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/MutabilityControl.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/MutabilityException.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/Output.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/ToHuman.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/TwoColumnOutput.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/Warning.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/Writers.java create mode 100644 dalvikdx/src/main/java/external/com/android/dx/util/package.html create mode 100644 dalvikdx/src/main/java/external/com/android/multidex/ArchivePathElement.java create mode 100644 dalvikdx/src/main/java/external/com/android/multidex/ClassPathElement.java create mode 100644 dalvikdx/src/main/java/external/com/android/multidex/ClassReferenceListBuilder.java create mode 100644 dalvikdx/src/main/java/external/com/android/multidex/FolderPathElement.java create mode 100644 dalvikdx/src/main/java/external/com/android/multidex/MainDexListBuilder.java create mode 100644 dalvikdx/src/main/java/external/com/android/multidex/Path.java create mode 100644 dexmaker/.gitignore create mode 100644 dexmaker/build.gradle create mode 100644 dexmaker/src/main/java/external/com/android/dx/AnnotationId.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/AppDataDirGuesser.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/BinaryOp.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/Code.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/Comparison.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/Constants.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/DexMaker.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/FieldId.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/Label.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/Local.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/MethodId.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/TypeId.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/TypeList.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/UnaryOp.java create mode 100644 dexmaker/src/main/java/external/com/android/dx/stock/ProxyBuilder.java create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 hiddenapistubs/.gitignore create mode 100644 hiddenapistubs/build.gradle create mode 100644 hiddenapistubs/src/main/AndroidManifest.xml create mode 100644 hiddenapistubs/src/main/java/android/app/ActivityThread.java create mode 100644 hiddenapistubs/src/main/java/android/app/LoadedApk.java create mode 100644 hiddenapistubs/src/main/java/android/content/pm/PackageParser.java create mode 100644 hiddenapistubs/src/main/java/android/content/res/AssetManager.java create mode 100644 hiddenapistubs/src/main/java/android/content/res/CompatibilityInfo.java create mode 100644 hiddenapistubs/src/main/java/android/content/res/Resources.java create mode 100644 hiddenapistubs/src/main/java/android/content/res/TypedArray.java create mode 100644 hiddenapistubs/src/main/java/android/os/SELinux.java create mode 100644 hiddenapistubs/src/main/java/android/os/ServiceManager.java create mode 100644 hiddenapistubs/src/main/java/com/android/internal/os/RuntimeInit.java create mode 100644 hiddenapistubs/src/main/java/com/android/internal/os/ZygoteInit.java create mode 100644 hiddenapistubs/src/main/java/com/android/internal/util/XmlUtils.java create mode 100644 hiddenapistubs/src/main/java/dalvik/system/BaseDexClassLoader.java create mode 100644 hiddenapistubs/src/main/java/xposed/dummy/XResourcesSuperClass.java create mode 100644 hiddenapistubs/src/main/java/xposed/dummy/XTypedArraySuperClass.java create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8ed3597e --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +.idea +/.idea/caches/build_file_checksums.ser +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +/release +.externalNativeBuild +elf-cleaner.sh diff --git a/Bridge/.gitignore b/Bridge/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/Bridge/.gitignore @@ -0,0 +1 @@ +/build diff --git a/Bridge/build.gradle b/Bridge/build.gradle new file mode 100644 index 00000000..5099f5ed --- /dev/null +++ b/Bridge/build.gradle @@ -0,0 +1,132 @@ +import groovy.xml.XmlUtil + +apply plugin: 'com.android.application' + +gradle.projectsEvaluated { + tasks.withType(JavaCompile) { + options.compilerArgs.add('-Xbootclasspath/p:libs/framework-stub.jar') + } +} + +android { + compileSdkVersion 28 + buildToolsVersion '28.0.3' + + defaultConfig { + multiDexEnabled false + minSdkVersion 23 + } + + sourceSets { + main { + java.srcDirs += ['src/main/apacheCommonsLang'] + jniLibs.srcDirs = ['libs'] + } + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + // Only build the release variant +// variantFilter { variant -> +// if (variant.buildType.name != BuilderConstants.DEBUG) { +// variant.ignore = true +// } +// } +} + +task generateStubs(type: Javadoc, dependsOn: 'compileReleaseSources') { + source = file('src/main/java') + ext.stubsDir = "$buildDir/api/stub-sources" + outputs.dir ext.stubsDir + title = null + + options { + doclet = 'com.google.doclava.Doclava' + docletpath = fileTree(dir: 'doclib', include: '**/*.jar').asType(List) + jFlags '-Dignore.symbol.file' + addBooleanOption 'nodocs', true + addFileOption 'stubs', file(ext.stubsDir) + addFileOption 'api', file('doclib/api/current.txt') + addBooleanOption 'hide 111', true + addBooleanOption 'hide 113', true + addBooleanOption 'hidePackage xposed.dummy', true + } +} + +task compileStubs(type: JavaCompile, dependsOn: 'generateStubs') { + source = fileTree(generateStubs.ext.stubsDir) + destinationDir = file("$buildDir/api/stub-classes") + options.compilerArgs += '-XDsuppressNotes' +} + +task jarStubs(type: Jar) { + from compileStubs + destinationDir = file("$buildDir/api") + baseName = 'api' +} + +task jarStubsSource(type: Jar) { + from generateStubs.source + destinationDir = jarStubs.destinationDir + baseName = jarStubs.baseName + classifier = 'sources' +} + +task generateAPI(dependsOn: ['generateStubs', 'jarStubs', 'jarStubsSource']) + +// Make sure that hiddenapistubs are placed before the Android SDK in app.iml +// as there doesn't seem to be any way to configure this in Android Studio. +task fixIml { + ext.imlFile = projectDir.absolutePath + '/' + project.name + '.iml' + inputs.file imlFile + outputs.file imlFile + + println imlFile + doLast { + def imlFile = file(project.name + ".iml") + println 'Change ' + project.name + '.iml order' + try { + def parsedXml = (new XmlParser()).parse(imlFile) + def jdkNode = parsedXml.component[1].orderEntry.find { it.'@type' == 'jdk' } + parsedXml.component[1].remove(jdkNode) + def sdkString = "Android API " + android.compileSdkVersion.substring("android-".length()) + " Platform" + new Node(parsedXml.component[1], 'orderEntry', ['type': 'jdk', 'jdkName': sdkString, 'jdkType': 'Android SDK']) + XmlUtil.serialize(parsedXml, new FileOutputStream(imlFile)) + } catch (FileNotFoundException e) { + // nop, iml not found + } + } +} + +tasks.preBuild.dependsOn fixIml + +dependencies { + compileOnly files("libs/framework-stub.jar") + compileOnly project(':dexmaker') +} + +afterEvaluate { + + task javac + + tasks.withType(JavaCompile) { + options.compilerArgs.add('-Xbootclasspath/p:libs/framework-stub.jar') + } + + android.applicationVariants.all { variant -> + def nameCapped = variant.name.capitalize() + def nameLowered = variant.name.toLowerCase() + + def makeAndCopyTask = task("makeAndCopy${nameCapped}", type: Copy, dependsOn: "assemble${nameCapped}") { + from "build/intermediates/transforms/dexMerger/${nameLowered}/0/classes.dex" + into '../Core/template_override/system/framework' + rename("classes.dex", "edxposed.dex") + } + } + +} \ No newline at end of file diff --git a/Bridge/libs/framework-stub.jar b/Bridge/libs/framework-stub.jar new file mode 100644 index 0000000000000000000000000000000000000000..36cd86b3c4fd658ed66ff5195ae0cc54d6ebc485 GIT binary patch literal 15295 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 SP*C)sAAQg^FUtb{>;D0!&FVP- literal 0 HcmV?d00001 diff --git a/Bridge/proguard-rules.pro b/Bridge/proguard-rules.pro new file mode 100644 index 00000000..41509fdd --- /dev/null +++ b/Bridge/proguard-rules.pro @@ -0,0 +1,30 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-keep class de.robv.android.xposed.** {*;} +-keep class android.** { *; } + +-keep interface com.elderdrivers.riru.common.KeepAll +-keep interface com.elderdrivers.riru.common.KeepMembers + +-keep class * implements com.elderdrivers.riru.common.KeepAll { *; } +-keepclassmembers class * implements com.elderdrivers.riru.common.KeepMembers { *; } \ No newline at end of file diff --git a/Bridge/src/main/AndroidManifest.xml b/Bridge/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4208d992 --- /dev/null +++ b/Bridge/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/Bridge/src/main/apacheCommonsLang/LICENSE.txt b/Bridge/src/main/apacheCommonsLang/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/LICENSE.txt @@ -0,0 +1,202 @@ + + 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. diff --git a/Bridge/src/main/apacheCommonsLang/MODIFICATIONS.txt b/Bridge/src/main/apacheCommonsLang/MODIFICATIONS.txt new file mode 100644 index 00000000..d6f4e370 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/MODIFICATIONS.txt @@ -0,0 +1,44 @@ +This is the original source code of the Apache Commons Lang library version 3.1 +as downloaded from http://commons.apache.org/lang/download_lang.cgi, except for +these modifications: +- Class MemberUtils changed to public +- Method compareParameterTypes in MemberUtils changed to public +- Prefix "external." for packages to avoid conflicts with other apps +- Removed unused sub-packages for smaller file size: +concurrent/ +event/ +math/ +text/ +time/ +- Removed unused classes for smaller file size: +AnnotationUtils.java +BitField.java +BooleanUtils.java +CharEncoding.java +CharRange.java +CharSet.java +CharSetUtils.java +EnumUtils.java +LocaleUtils.java +RandomStringUtils.java +Range.java +SerializationException.java +SerializationUtils.java +StringEscapeUtils.java +builder/StandardToStringStyle.java +exception/ContextedException.java +exception/ContextedRuntimeException.java +exception/DefaultExceptionContext.java +exception/ExceptionContext.java +exception/ExceptionUtils.java +mutable/MutableBoolean.java +mutable/MutableByte.java +mutable/MutableDouble.java +mutable/MutableFloat.java +mutable/MutableLong.java +mutable/MutableObject.java +mutable/MutableShort.java +reflect/ConstructorUtils.java +reflect/FieldUtils.java +reflect/TypeUtils.java +tuple/MutablePair.java diff --git a/Bridge/src/main/apacheCommonsLang/NOTICE.txt b/Bridge/src/main/apacheCommonsLang/NOTICE.txt new file mode 100644 index 00000000..2f0ca384 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/NOTICE.txt @@ -0,0 +1,8 @@ +Apache Commons Lang +Copyright 2001-2011 The Apache Software Foundation + +This product includes software developed by +The Apache Software Foundation (http://www.apache.org/). + +This product includes software from the Spring Framework, +under the Apache License 2.0 (see: StringUtils.containsWhitespace()) diff --git a/Bridge/src/main/apacheCommonsLang/RELEASE-NOTES.txt b/Bridge/src/main/apacheCommonsLang/RELEASE-NOTES.txt new file mode 100644 index 00000000..e0ec1336 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/RELEASE-NOTES.txt @@ -0,0 +1,40 @@ +$Id: RELEASE-NOTES.txt 1199820 2011-11-09 16:14:52Z bayard $ + + Commons Lang Package + Version 3.1 + Release Notes + + +INTRODUCTION: + +This document contains the release notes for the 3.1 version of Apache Commons Lang. +Commons Lang is a set of utility functions and reusable components that should be of use in any +Java environment. + +Lang 3.0 and onwards now targets Java 5.0, making use of features that arrived with Java 5.0 such as generics, +variable arguments, autoboxing, concurrency and formatted output. + +For the advice on upgrading from 2.x to 3.x, see the following page: + + http://commons.apache.org/lang/article3_0.html + +CHANGES IN 3.1 +================ + + [LANG-760] Add API StringUtils.toString(byte[] intput, String charsetName) + [LANG-756] Add APIs ClassUtils.isPrimitiveWrapper(Class) and isPrimitiveOrWrapper(Class) + [LANG-758] Add an example with whitespace in StringUtils.defaultIfEmpty + [LANG-752] Fix createLong() so it behaves like createInteger() + [LANG-751] Include the actual type in the Validate.isInstance and isAssignableFrom exception messages + [LANG-748] Deprecating chomp(String, String) + [LANG-736] CharUtils static final array CHAR_STRING is not needed to compute CHAR_STRING_ARRAY + [LANG-695] SystemUtils.IS_OS_UNIX doesn't recognize FreeBSD as a Unix system + +BUG FIXES IN 3.1 +================== + + [LANG-749] Incorrect Bundle-SymbolicName in Manifest + [LANG-746] NumberUtils does not handle upper-case hex: 0X and -0X + [LANG-744] StringUtils throws java.security.AccessControlException on Google App Engine + [LANG-741] Ant build has wrong component.name + [LANG-698] Document that the Mutable numbers don't work as expected with String.format diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ArrayUtils.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ArrayUtils.java new file mode 100644 index 00000000..1db7dc97 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ArrayUtils.java @@ -0,0 +1,5797 @@ +/* + * 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 external.org.apache.commons.lang3; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + + +import external.org.apache.commons.lang3.builder.EqualsBuilder; +import external.org.apache.commons.lang3.builder.HashCodeBuilder; +import external.org.apache.commons.lang3.builder.ToStringBuilder; +import external.org.apache.commons.lang3.builder.ToStringStyle; +import external.org.apache.commons.lang3.mutable.MutableInt; + +/** + *

Operations on arrays, primitive arrays (like {@code int[]}) and + * primitive wrapper arrays (like {@code Integer[]}).

+ * + *

This class tries to handle {@code null} input gracefully. + * An exception will not be thrown for a {@code null} + * array input. However, an Object array that contains a {@code null} + * element may throw an exception. Each method documents its behaviour.

+ * + *

#ThreadSafe#

+ * @since 2.0 + * @version $Id: ArrayUtils.java 1154216 2011-08-05 13:57:16Z mbenson $ + */ +public class ArrayUtils { + + /** + * An empty immutable {@code Object} array. + */ + public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + /** + * An empty immutable {@code Class} array. + */ + public static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + /** + * An empty immutable {@code String} array. + */ + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + /** + * An empty immutable {@code long} array. + */ + public static final long[] EMPTY_LONG_ARRAY = new long[0]; + /** + * An empty immutable {@code Long} array. + */ + public static final Long[] EMPTY_LONG_OBJECT_ARRAY = new Long[0]; + /** + * An empty immutable {@code int} array. + */ + public static final int[] EMPTY_INT_ARRAY = new int[0]; + /** + * An empty immutable {@code Integer} array. + */ + public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0]; + /** + * An empty immutable {@code short} array. + */ + public static final short[] EMPTY_SHORT_ARRAY = new short[0]; + /** + * An empty immutable {@code Short} array. + */ + public static final Short[] EMPTY_SHORT_OBJECT_ARRAY = new Short[0]; + /** + * An empty immutable {@code byte} array. + */ + public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + /** + * An empty immutable {@code Byte} array. + */ + public static final Byte[] EMPTY_BYTE_OBJECT_ARRAY = new Byte[0]; + /** + * An empty immutable {@code double} array. + */ + public static final double[] EMPTY_DOUBLE_ARRAY = new double[0]; + /** + * An empty immutable {@code Double} array. + */ + public static final Double[] EMPTY_DOUBLE_OBJECT_ARRAY = new Double[0]; + /** + * An empty immutable {@code float} array. + */ + public static final float[] EMPTY_FLOAT_ARRAY = new float[0]; + /** + * An empty immutable {@code Float} array. + */ + public static final Float[] EMPTY_FLOAT_OBJECT_ARRAY = new Float[0]; + /** + * An empty immutable {@code boolean} array. + */ + public static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0]; + /** + * An empty immutable {@code Boolean} array. + */ + public static final Boolean[] EMPTY_BOOLEAN_OBJECT_ARRAY = new Boolean[0]; + /** + * An empty immutable {@code char} array. + */ + public static final char[] EMPTY_CHAR_ARRAY = new char[0]; + /** + * An empty immutable {@code Character} array. + */ + public static final Character[] EMPTY_CHARACTER_OBJECT_ARRAY = new Character[0]; + + /** + * The index value when an element is not found in a list or array: {@code -1}. + * This value is returned by methods in this class and can also be used in comparisons with values returned by + * various method from {@link java.util.List}. + */ + public static final int INDEX_NOT_FOUND = -1; + + /** + *

ArrayUtils instances should NOT be constructed in standard programming. + * Instead, the class should be used as ArrayUtils.clone(new int[] {2}).

+ * + *

This constructor is public to permit tools that require a JavaBean instance + * to operate.

+ */ + public ArrayUtils() { + super(); + } + + + // NOTE: Cannot use {@code} to enclose text which includes {}, but is OK + + + // Basic methods handling multi-dimensional arrays + //----------------------------------------------------------------------- + /** + *

Outputs an array as a String, treating {@code null} as an empty array.

+ * + *

Multi-dimensional arrays are handled correctly, including + * multi-dimensional primitive arrays.

+ * + *

The format is that of Java source code, for example {a,b}.

+ * + * @param array the array to get a toString for, may be {@code null} + * @return a String representation of the array, '{}' if null array input + */ + public static String toString(Object array) { + return toString(array, "{}"); + } + + /** + *

Outputs an array as a String handling {@code null}s.

+ * + *

Multi-dimensional arrays are handled correctly, including + * multi-dimensional primitive arrays.

+ * + *

The format is that of Java source code, for example {a,b}.

+ * + * @param array the array to get a toString for, may be {@code null} + * @param stringIfNull the String to return if the array is {@code null} + * @return a String representation of the array + */ + public static String toString(Object array, String stringIfNull) { + if (array == null) { + return stringIfNull; + } + return new ToStringBuilder(array, ToStringStyle.SIMPLE_STYLE).append(array).toString(); + } + + /** + *

Get a hash code for an array handling multi-dimensional arrays correctly.

+ * + *

Multi-dimensional primitive arrays are also handled correctly by this method.

+ * + * @param array the array to get a hash code for, {@code null} returns zero + * @return a hash code for the array + */ + public static int hashCode(Object array) { + return new HashCodeBuilder().append(array).toHashCode(); + } + + /** + *

Compares two arrays, using equals(), handling multi-dimensional arrays + * correctly.

+ * + *

Multi-dimensional primitive arrays are also handled correctly by this method.

+ * + * @param array1 the left hand array to compare, may be {@code null} + * @param array2 the right hand array to compare, may be {@code null} + * @return {@code true} if the arrays are equal + */ + public static boolean isEquals(Object array1, Object array2) { + return new EqualsBuilder().append(array1, array2).isEquals(); + } + + // To map + //----------------------------------------------------------------------- + /** + *

Converts the given array into a {@link java.util.Map}. Each element of the array + * must be either a {@link java.util.Map.Entry} or an Array, containing at least two + * elements, where the first element is used as key and the second as + * value.

+ * + *

This method can be used to initialize:

+ *
+     * // Create a Map mapping colors.
+     * Map colorMap = MapUtils.toMap(new String[][] {{
+     *     {"RED", "#FF0000"},
+     *     {"GREEN", "#00FF00"},
+     *     {"BLUE", "#0000FF"}});
+     * 
+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array an array whose elements are either a {@link java.util.Map.Entry} or + * an Array containing at least two elements, may be {@code null} + * @return a {@code Map} that was created from the array + * @throws IllegalArgumentException if one element of this Array is + * itself an Array containing less then two elements + * @throws IllegalArgumentException if the array contains elements other + * than {@link java.util.Map.Entry} and an Array + */ + public static Map toMap(Object[] array) { + if (array == null) { + return null; + } + final Map map = new HashMap((int) (array.length * 1.5)); + for (int i = 0; i < array.length; i++) { + Object object = array[i]; + if (object instanceof Map.Entry) { + Map.Entry entry = (Map.Entry) object; + map.put(entry.getKey(), entry.getValue()); + } else if (object instanceof Object[]) { + Object[] entry = (Object[]) object; + if (entry.length < 2) { + throw new IllegalArgumentException("Array element " + i + ", '" + + object + + "', has a length less than 2"); + } + map.put(entry[0], entry[1]); + } else { + throw new IllegalArgumentException("Array element " + i + ", '" + + object + + "', is neither of type Map.Entry nor an Array"); + } + } + return map; + } + + // Generic array + //----------------------------------------------------------------------- + /** + *

Create a type-safe generic array.

+ * + *

The Java language does not allow an array to be created from a generic type:

+ * + *
+    public static <T> T[] createAnArray(int size) {
+        return new T[size]; // compiler error here
+    }
+    public static <T> T[] createAnArray(int size) {
+        return (T[])new Object[size]; // ClassCastException at runtime
+    }
+     * 
+ * + *

Therefore new arrays of generic types can be created with this method. + * For example, an array of Strings can be created:

+ * + *
+    String[] array = ArrayUtils.toArray("1", "2");
+    String[] emptyArray = ArrayUtils.<String>toArray();
+     * 
+ * + *

The method is typically used in scenarios, where the caller itself uses generic types + * that have to be combined into an array.

+ * + *

Note, this method makes only sense to provide arguments of the same type so that the + * compiler can deduce the type of the array itself. While it is possible to select the + * type explicitly like in + * Number[] array = ArrayUtils.<Number>toArray(Integer.valueOf(42), Double.valueOf(Math.PI)), + * there is no real advantage when compared to + * new Number[] {Integer.valueOf(42), Double.valueOf(Math.PI)}.

+ * + * @param the array's element type + * @param items the varargs array items, null allowed + * @return the array, not null unless a null array is passed in + * @since 3.0 + */ + public static T[] toArray(final T... items) { + return items; + } + + // Clone + //----------------------------------------------------------------------- + /** + *

Shallow clones an array returning a typecast result and handling + * {@code null}.

+ * + *

The objects in the array are not cloned, thus there is no special + * handling for multi-dimensional arrays.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param the component type of the array + * @param array the array to shallow clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static T[] clone(T[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static long[] clone(long[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static int[] clone(int[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static short[] clone(short[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static char[] clone(char[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static byte[] clone(byte[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static double[] clone(double[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static float[] clone(float[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + /** + *

Clones an array returning a typecast result and handling + * {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array the array to clone, may be {@code null} + * @return the cloned array, {@code null} if {@code null} input + */ + public static boolean[] clone(boolean[] array) { + if (array == null) { + return null; + } + return array.clone(); + } + + // nullToEmpty + //----------------------------------------------------------------------- + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Object[] nullToEmpty(Object[] array) { + if (array == null || array.length == 0) { + return EMPTY_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static String[] nullToEmpty(String[] array) { + if (array == null || array.length == 0) { + return EMPTY_STRING_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static long[] nullToEmpty(long[] array) { + if (array == null || array.length == 0) { + return EMPTY_LONG_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static int[] nullToEmpty(int[] array) { + if (array == null || array.length == 0) { + return EMPTY_INT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static short[] nullToEmpty(short[] array) { + if (array == null || array.length == 0) { + return EMPTY_SHORT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static char[] nullToEmpty(char[] array) { + if (array == null || array.length == 0) { + return EMPTY_CHAR_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static byte[] nullToEmpty(byte[] array) { + if (array == null || array.length == 0) { + return EMPTY_BYTE_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static double[] nullToEmpty(double[] array) { + if (array == null || array.length == 0) { + return EMPTY_DOUBLE_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static float[] nullToEmpty(float[] array) { + if (array == null || array.length == 0) { + return EMPTY_FLOAT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static boolean[] nullToEmpty(boolean[] array) { + if (array == null || array.length == 0) { + return EMPTY_BOOLEAN_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Long[] nullToEmpty(Long[] array) { + if (array == null || array.length == 0) { + return EMPTY_LONG_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Integer[] nullToEmpty(Integer[] array) { + if (array == null || array.length == 0) { + return EMPTY_INTEGER_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Short[] nullToEmpty(Short[] array) { + if (array == null || array.length == 0) { + return EMPTY_SHORT_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Character[] nullToEmpty(Character[] array) { + if (array == null || array.length == 0) { + return EMPTY_CHARACTER_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Byte[] nullToEmpty(Byte[] array) { + if (array == null || array.length == 0) { + return EMPTY_BYTE_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Double[] nullToEmpty(Double[] array) { + if (array == null || array.length == 0) { + return EMPTY_DOUBLE_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Float[] nullToEmpty(Float[] array) { + if (array == null || array.length == 0) { + return EMPTY_FLOAT_OBJECT_ARRAY; + } + return array; + } + + /** + *

Defensive programming technique to change a {@code null} + * reference to an empty one.

+ * + *

This method returns an empty array for a {@code null} input array.

+ * + *

As a memory optimizing technique an empty array passed in will be overridden with + * the empty {@code public static} references in this class.

+ * + * @param array the array to check for {@code null} or empty + * @return the same array, {@code public static} empty array if {@code null} or empty input + * @since 2.5 + */ + public static Boolean[] nullToEmpty(Boolean[] array) { + if (array == null || array.length == 0) { + return EMPTY_BOOLEAN_OBJECT_ARRAY; + } + return array; + } + + // Subarrays + //----------------------------------------------------------------------- + /** + *

Produces a new array containing the elements between + * the start and end indices.

+ * + *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + *

The component type of the subarray is always the same as + * that of the input array. Thus, if the input is an array of type + * {@code Date}, the following usage is envisaged:

+ * + *
+     * Date[] someDates = (Date[])ArrayUtils.subarray(allDates, 2, 5);
+     * 
+ * + * @param the component type of the array + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @since 2.1 + */ + public static T[] subarray(T[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + int newSize = endIndexExclusive - startIndexInclusive; + Class type = array.getClass().getComponentType(); + if (newSize <= 0) { + @SuppressWarnings("unchecked") // OK, because array is of type T + final T[] emptyArray = (T[]) Array.newInstance(type, 0); + return emptyArray; + } + @SuppressWarnings("unchecked") // OK, because array is of type T + T[] subarray = (T[]) Array.newInstance(type, newSize); + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code long} array containing the elements + * between the start and end indices.

+ * + *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @since 2.1 + */ + public static long[] subarray(long[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_LONG_ARRAY; + } + + long[] subarray = new long[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code int} array containing the elements + * between the start and end indices.

+ * + *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @since 2.1 + */ + public static int[] subarray(int[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_INT_ARRAY; + } + + int[] subarray = new int[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code short} array containing the elements + * between the start and end indices.

+ * + *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @since 2.1 + */ + public static short[] subarray(short[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_SHORT_ARRAY; + } + + short[] subarray = new short[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code char} array containing the elements + * between the start and end indices.

+ * + *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @since 2.1 + */ + public static char[] subarray(char[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_CHAR_ARRAY; + } + + char[] subarray = new char[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code byte} array containing the elements + * between the start and end indices.

+ * + *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @since 2.1 + */ + public static byte[] subarray(byte[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_BYTE_ARRAY; + } + + byte[] subarray = new byte[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code double} array containing the elements + * between the start and end indices.

+ * + *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @since 2.1 + */ + public static double[] subarray(double[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_DOUBLE_ARRAY; + } + + double[] subarray = new double[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code float} array containing the elements + * between the start and end indices.

+ * + *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @since 2.1 + */ + public static float[] subarray(float[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_FLOAT_ARRAY; + } + + float[] subarray = new float[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + /** + *

Produces a new {@code boolean} array containing the elements + * between the start and end indices.

+ * + *

The start index is inclusive, the end index exclusive. + * Null array input produces null output.

+ * + * @param array the array + * @param startIndexInclusive the starting index. Undervalue (<0) + * is promoted to 0, overvalue (>array.length) results + * in an empty array. + * @param endIndexExclusive elements up to endIndex-1 are present in the + * returned subarray. Undervalue (< startIndex) produces + * empty array, overvalue (>array.length) is demoted to + * array length. + * @return a new array containing the elements between + * the start and end indices. + * @since 2.1 + */ + public static boolean[] subarray(boolean[] array, int startIndexInclusive, int endIndexExclusive) { + if (array == null) { + return null; + } + if (startIndexInclusive < 0) { + startIndexInclusive = 0; + } + if (endIndexExclusive > array.length) { + endIndexExclusive = array.length; + } + int newSize = endIndexExclusive - startIndexInclusive; + if (newSize <= 0) { + return EMPTY_BOOLEAN_ARRAY; + } + + boolean[] subarray = new boolean[newSize]; + System.arraycopy(array, startIndexInclusive, subarray, 0, newSize); + return subarray; + } + + // Is same length + //----------------------------------------------------------------------- + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}. + * + *

Any multi-dimensional aspects of the arrays are ignored.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(Object[] array1, Object[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(long[] array1, long[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(int[] array1, int[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(short[] array1, short[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(char[] array1, char[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(byte[] array1, byte[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(double[] array1, double[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(float[] array1, float[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + /** + *

Checks whether two arrays are the same length, treating + * {@code null} arrays as length {@code 0}.

+ * + * @param array1 the first array, may be {@code null} + * @param array2 the second array, may be {@code null} + * @return {@code true} if length of arrays matches, treating + * {@code null} as an empty array + */ + public static boolean isSameLength(boolean[] array1, boolean[] array2) { + if ((array1 == null && array2 != null && array2.length > 0) || + (array2 == null && array1 != null && array1.length > 0) || + (array1 != null && array2 != null && array1.length != array2.length)) { + return false; + } + return true; + } + + //----------------------------------------------------------------------- + /** + *

Returns the length of the specified array. + * This method can deal with {@code Object} arrays and with primitive arrays.

+ * + *

If the input array is {@code null}, {@code 0} is returned.

+ * + *
+     * ArrayUtils.getLength(null)            = 0
+     * ArrayUtils.getLength([])              = 0
+     * ArrayUtils.getLength([null])          = 1
+     * ArrayUtils.getLength([true, false])   = 2
+     * ArrayUtils.getLength([1, 2, 3])       = 3
+     * ArrayUtils.getLength(["a", "b", "c"]) = 3
+     * 
+ * + * @param array the array to retrieve the length from, may be null + * @return The length of the array, or {@code 0} if the array is {@code null} + * @throws IllegalArgumentException if the object arguement is not an array. + * @since 2.1 + */ + public static int getLength(Object array) { + if (array == null) { + return 0; + } + return Array.getLength(array); + } + + /** + *

Checks whether two arrays are the same type taking into account + * multi-dimensional arrays.

+ * + * @param array1 the first array, must not be {@code null} + * @param array2 the second array, must not be {@code null} + * @return {@code true} if type of arrays matches + * @throws IllegalArgumentException if either array is {@code null} + */ + public static boolean isSameType(Object array1, Object array2) { + if (array1 == null || array2 == null) { + throw new IllegalArgumentException("The Array must not be null"); + } + return array1.getClass().getName().equals(array2.getClass().getName()); + } + + // Reverse + //----------------------------------------------------------------------- + /** + *

Reverses the order of the given array.

+ * + *

There is no special handling for multi-dimensional arrays.

+ * + *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(Object[] array) { + if (array == null) { + return; + } + int i = 0; + int j = array.length - 1; + Object tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

Reverses the order of the given array.

+ * + *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(long[] array) { + if (array == null) { + return; + } + int i = 0; + int j = array.length - 1; + long tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

Reverses the order of the given array.

+ * + *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(int[] array) { + if (array == null) { + return; + } + int i = 0; + int j = array.length - 1; + int tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

Reverses the order of the given array.

+ * + *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(short[] array) { + if (array == null) { + return; + } + int i = 0; + int j = array.length - 1; + short tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

Reverses the order of the given array.

+ * + *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(char[] array) { + if (array == null) { + return; + } + int i = 0; + int j = array.length - 1; + char tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

Reverses the order of the given array.

+ * + *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(byte[] array) { + if (array == null) { + return; + } + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

Reverses the order of the given array.

+ * + *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(double[] array) { + if (array == null) { + return; + } + int i = 0; + int j = array.length - 1; + double tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

Reverses the order of the given array.

+ * + *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(float[] array) { + if (array == null) { + return; + } + int i = 0; + int j = array.length - 1; + float tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + *

Reverses the order of the given array.

+ * + *

This method does nothing for a {@code null} input array.

+ * + * @param array the array to reverse, may be {@code null} + */ + public static void reverse(boolean[] array) { + if (array == null) { + return; + } + int i = 0; + int j = array.length - 1; + boolean tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + // IndexOf search + // ---------------------------------------------------------------------- + + // Object IndexOf + //----------------------------------------------------------------------- + /** + *

Finds the index of the given object in the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param objectToFind the object to find, may be {@code null} + * @return the index of the object within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(Object[] array, Object objectToFind) { + return indexOf(array, objectToFind, 0); + } + + /** + *

Finds the index of the given object in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param objectToFind the object to find, may be {@code null} + * @param startIndex the index to start searching at + * @return the index of the object within the array starting at the index, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(Object[] array, Object objectToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + if (objectToFind == null) { + for (int i = startIndex; i < array.length; i++) { + if (array[i] == null) { + return i; + } + } + } else if (array.getClass().getComponentType().isInstance(objectToFind)) { + for (int i = startIndex; i < array.length; i++) { + if (objectToFind.equals(array[i])) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given object within the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param objectToFind the object to find, may be {@code null} + * @return the last index of the object within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(Object[] array, Object objectToFind) { + return lastIndexOf(array, objectToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given object in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than + * the array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param objectToFind the object to find, may be {@code null} + * @param startIndex the start index to travers backwards from + * @return the last index of the object within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(Object[] array, Object objectToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + if (objectToFind == null) { + for (int i = startIndex; i >= 0; i--) { + if (array[i] == null) { + return i; + } + } + } else if (array.getClass().getComponentType().isInstance(objectToFind)) { + for (int i = startIndex; i >= 0; i--) { + if (objectToFind.equals(array[i])) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the object is in the given array.

+ * + *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param objectToFind the object to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(Object[] array, Object objectToFind) { + return indexOf(array, objectToFind) != INDEX_NOT_FOUND; + } + + // long IndexOf + //----------------------------------------------------------------------- + /** + *

Finds the index of the given value in the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(long[] array, long valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(long[] array, long valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(long[] array, long valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(long[] array, long valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ * + *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(long[] array, long valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // int IndexOf + //----------------------------------------------------------------------- + /** + *

Finds the index of the given value in the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(int[] array, int valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(int[] array, int valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(int[] array, int valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(int[] array, int valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ * + *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(int[] array, int valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // short IndexOf + //----------------------------------------------------------------------- + /** + *

Finds the index of the given value in the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(short[] array, short valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(short[] array, short valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(short[] array, short valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(short[] array, short valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ * + *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(short[] array, short valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // char IndexOf + //----------------------------------------------------------------------- + /** + *

Finds the index of the given value in the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + * @since 2.1 + */ + public static int indexOf(char[] array, char valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + * @since 2.1 + */ + public static int indexOf(char[] array, char valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + * @since 2.1 + */ + public static int lastIndexOf(char[] array, char valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + * @since 2.1 + */ + public static int lastIndexOf(char[] array, char valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ * + *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + * @since 2.1 + */ + public static boolean contains(char[] array, char valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // byte IndexOf + //----------------------------------------------------------------------- + /** + *

Finds the index of the given value in the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(byte[] array, byte valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(byte[] array, byte valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(byte[] array, byte valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(byte[] array, byte valueToFind, int startIndex) { + if (array == null) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ * + *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(byte[] array, byte valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // double IndexOf + //----------------------------------------------------------------------- + /** + *

Finds the index of the given value in the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(double[] array, double valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value within a given tolerance in the array. + * This method will return the index of the first value which falls between the region + * defined by valueToFind - tolerance and valueToFind + tolerance.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param tolerance tolerance of the search + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(double[] array, double valueToFind, double tolerance) { + return indexOf(array, valueToFind, 0, tolerance); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(double[] array, double valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the index of the given value in the array starting at the given index. + * This method will return the index of the first value which falls between the region + * defined by valueToFind - tolerance and valueToFind + tolerance.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @param tolerance tolerance of the search + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(double[] array, double valueToFind, int startIndex, double tolerance) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + double min = valueToFind - tolerance; + double max = valueToFind + tolerance; + for (int i = startIndex; i < array.length; i++) { + if (array[i] >= min && array[i] <= max) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(double[] array, double valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value within a given tolerance in the array. + * This method will return the index of the last value which falls between the region + * defined by valueToFind - tolerance and valueToFind + tolerance.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param tolerance tolerance of the search + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(double[] array, double valueToFind, double tolerance) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE, tolerance); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(double[] array, double valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value in the array starting at the given index. + * This method will return the index of the last value which falls between the region + * defined by valueToFind - tolerance and valueToFind + tolerance.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @param tolerance search for value within plus/minus this amount + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(double[] array, double valueToFind, int startIndex, double tolerance) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + double min = valueToFind - tolerance; + double max = valueToFind + tolerance; + for (int i = startIndex; i >= 0; i--) { + if (array[i] >= min && array[i] <= max) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ * + *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(double[] array, double valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + /** + *

Checks if a value falling within the given tolerance is in the + * given array. If the array contains a value within the inclusive range + * defined by (value - tolerance) to (value + tolerance).

+ * + *

The method returns {@code false} if a {@code null} array + * is passed in.

+ * + * @param array the array to search + * @param valueToFind the value to find + * @param tolerance the array contains the tolerance of the search + * @return true if value falling within tolerance is in array + */ + public static boolean contains(double[] array, double valueToFind, double tolerance) { + return indexOf(array, valueToFind, 0, tolerance) != INDEX_NOT_FOUND; + } + + // float IndexOf + //----------------------------------------------------------------------- + /** + *

Finds the index of the given value in the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(float[] array, float valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(float[] array, float valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(float[] array, float valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than the + * array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(float[] array, float valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ * + *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(float[] array, float valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // boolean IndexOf + //----------------------------------------------------------------------- + /** + *

Finds the index of the given value in the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int indexOf(boolean[] array, boolean valueToFind) { + return indexOf(array, valueToFind, 0); + } + + /** + *

Finds the index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex is treated as zero. A startIndex larger than the array + * length will return {@link #INDEX_NOT_FOUND} ({@code -1}).

+ * + * @param array the array to search through for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the index to start searching at + * @return the index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} + * array input + */ + public static int indexOf(boolean[] array, boolean valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex; i < array.length; i++) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Finds the last index of the given value within the array.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) if + * {@code null} array input.

+ * + * @param array the array to travers backwords looking for the object, may be {@code null} + * @param valueToFind the object to find + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(boolean[] array, boolean valueToFind) { + return lastIndexOf(array, valueToFind, Integer.MAX_VALUE); + } + + /** + *

Finds the last index of the given value in the array starting at the given index.

+ * + *

This method returns {@link #INDEX_NOT_FOUND} ({@code -1}) for a {@code null} input array.

+ * + *

A negative startIndex will return {@link #INDEX_NOT_FOUND} ({@code -1}). A startIndex larger than + * the array length will search from the end of the array.

+ * + * @param array the array to traverse for looking for the object, may be {@code null} + * @param valueToFind the value to find + * @param startIndex the start index to travers backwards from + * @return the last index of the value within the array, + * {@link #INDEX_NOT_FOUND} ({@code -1}) if not found or {@code null} array input + */ + public static int lastIndexOf(boolean[] array, boolean valueToFind, int startIndex) { + if (ArrayUtils.isEmpty(array)) { + return INDEX_NOT_FOUND; + } + if (startIndex < 0) { + return INDEX_NOT_FOUND; + } else if (startIndex >= array.length) { + startIndex = array.length - 1; + } + for (int i = startIndex; i >= 0; i--) { + if (valueToFind == array[i]) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Checks if the value is in the given array.

+ * + *

The method returns {@code false} if a {@code null} array is passed in.

+ * + * @param array the array to search through + * @param valueToFind the value to find + * @return {@code true} if the array contains the object + */ + public static boolean contains(boolean[] array, boolean valueToFind) { + return indexOf(array, valueToFind) != INDEX_NOT_FOUND; + } + + // Primitive/Object array converters + // ---------------------------------------------------------------------- + + // Character array converters + // ---------------------------------------------------------------------- + /** + *

Converts an array of object Characters to primitives.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Character} array, may be {@code null} + * @return a {@code char} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static char[] toPrimitive(Character[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_CHAR_ARRAY; + } + final char[] result = new char[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].charValue(); + } + return result; + } + + /** + *

Converts an array of object Character to primitives handling {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Character} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code char} array, {@code null} if null array input + */ + public static char[] toPrimitive(Character[] array, char valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_CHAR_ARRAY; + } + final char[] result = new char[array.length]; + for (int i = 0; i < array.length; i++) { + Character b = array[i]; + result[i] = (b == null ? valueForNull : b.charValue()); + } + return result; + } + + /** + *

Converts an array of primitive chars to objects.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code char} array + * @return a {@code Character} array, {@code null} if null array input + */ + public static Character[] toObject(char[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_CHARACTER_OBJECT_ARRAY; + } + final Character[] result = new Character[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Character.valueOf(array[i]); + } + return result; + } + + // Long array converters + // ---------------------------------------------------------------------- + /** + *

Converts an array of object Longs to primitives.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Long} array, may be {@code null} + * @return a {@code long} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static long[] toPrimitive(Long[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_LONG_ARRAY; + } + final long[] result = new long[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].longValue(); + } + return result; + } + + /** + *

Converts an array of object Long to primitives handling {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Long} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code long} array, {@code null} if null array input + */ + public static long[] toPrimitive(Long[] array, long valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_LONG_ARRAY; + } + final long[] result = new long[array.length]; + for (int i = 0; i < array.length; i++) { + Long b = array[i]; + result[i] = (b == null ? valueForNull : b.longValue()); + } + return result; + } + + /** + *

Converts an array of primitive longs to objects.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code long} array + * @return a {@code Long} array, {@code null} if null array input + */ + public static Long[] toObject(long[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_LONG_OBJECT_ARRAY; + } + final Long[] result = new Long[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Long.valueOf(array[i]); + } + return result; + } + + // Int array converters + // ---------------------------------------------------------------------- + /** + *

Converts an array of object Integers to primitives.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Integer} array, may be {@code null} + * @return an {@code int} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static int[] toPrimitive(Integer[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_INT_ARRAY; + } + final int[] result = new int[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].intValue(); + } + return result; + } + + /** + *

Converts an array of object Integer to primitives handling {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Integer} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return an {@code int} array, {@code null} if null array input + */ + public static int[] toPrimitive(Integer[] array, int valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_INT_ARRAY; + } + final int[] result = new int[array.length]; + for (int i = 0; i < array.length; i++) { + Integer b = array[i]; + result[i] = (b == null ? valueForNull : b.intValue()); + } + return result; + } + + /** + *

Converts an array of primitive ints to objects.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array an {@code int} array + * @return an {@code Integer} array, {@code null} if null array input + */ + public static Integer[] toObject(int[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_INTEGER_OBJECT_ARRAY; + } + final Integer[] result = new Integer[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Integer.valueOf(array[i]); + } + return result; + } + + // Short array converters + // ---------------------------------------------------------------------- + /** + *

Converts an array of object Shorts to primitives.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Short} array, may be {@code null} + * @return a {@code byte} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static short[] toPrimitive(Short[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_SHORT_ARRAY; + } + final short[] result = new short[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].shortValue(); + } + return result; + } + + /** + *

Converts an array of object Short to primitives handling {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Short} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code byte} array, {@code null} if null array input + */ + public static short[] toPrimitive(Short[] array, short valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_SHORT_ARRAY; + } + final short[] result = new short[array.length]; + for (int i = 0; i < array.length; i++) { + Short b = array[i]; + result[i] = (b == null ? valueForNull : b.shortValue()); + } + return result; + } + + /** + *

Converts an array of primitive shorts to objects.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code short} array + * @return a {@code Short} array, {@code null} if null array input + */ + public static Short[] toObject(short[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_SHORT_OBJECT_ARRAY; + } + final Short[] result = new Short[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Short.valueOf(array[i]); + } + return result; + } + + // Byte array converters + // ---------------------------------------------------------------------- + /** + *

Converts an array of object Bytes to primitives.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Byte} array, may be {@code null} + * @return a {@code byte} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static byte[] toPrimitive(Byte[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BYTE_ARRAY; + } + final byte[] result = new byte[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].byteValue(); + } + return result; + } + + /** + *

Converts an array of object Bytes to primitives handling {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Byte} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code byte} array, {@code null} if null array input + */ + public static byte[] toPrimitive(Byte[] array, byte valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BYTE_ARRAY; + } + final byte[] result = new byte[array.length]; + for (int i = 0; i < array.length; i++) { + Byte b = array[i]; + result[i] = (b == null ? valueForNull : b.byteValue()); + } + return result; + } + + /** + *

Converts an array of primitive bytes to objects.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code byte} array + * @return a {@code Byte} array, {@code null} if null array input + */ + public static Byte[] toObject(byte[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BYTE_OBJECT_ARRAY; + } + final Byte[] result = new Byte[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Byte.valueOf(array[i]); + } + return result; + } + + // Double array converters + // ---------------------------------------------------------------------- + /** + *

Converts an array of object Doubles to primitives.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Double} array, may be {@code null} + * @return a {@code double} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static double[] toPrimitive(Double[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_DOUBLE_ARRAY; + } + final double[] result = new double[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].doubleValue(); + } + return result; + } + + /** + *

Converts an array of object Doubles to primitives handling {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Double} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code double} array, {@code null} if null array input + */ + public static double[] toPrimitive(Double[] array, double valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_DOUBLE_ARRAY; + } + final double[] result = new double[array.length]; + for (int i = 0; i < array.length; i++) { + Double b = array[i]; + result[i] = (b == null ? valueForNull : b.doubleValue()); + } + return result; + } + + /** + *

Converts an array of primitive doubles to objects.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code double} array + * @return a {@code Double} array, {@code null} if null array input + */ + public static Double[] toObject(double[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_DOUBLE_OBJECT_ARRAY; + } + final Double[] result = new Double[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Double.valueOf(array[i]); + } + return result; + } + + // Float array converters + // ---------------------------------------------------------------------- + /** + *

Converts an array of object Floats to primitives.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Float} array, may be {@code null} + * @return a {@code float} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static float[] toPrimitive(Float[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_FLOAT_ARRAY; + } + final float[] result = new float[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].floatValue(); + } + return result; + } + + /** + *

Converts an array of object Floats to primitives handling {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Float} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code float} array, {@code null} if null array input + */ + public static float[] toPrimitive(Float[] array, float valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_FLOAT_ARRAY; + } + final float[] result = new float[array.length]; + for (int i = 0; i < array.length; i++) { + Float b = array[i]; + result[i] = (b == null ? valueForNull : b.floatValue()); + } + return result; + } + + /** + *

Converts an array of primitive floats to objects.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code float} array + * @return a {@code Float} array, {@code null} if null array input + */ + public static Float[] toObject(float[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_FLOAT_OBJECT_ARRAY; + } + final Float[] result = new Float[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = Float.valueOf(array[i]); + } + return result; + } + + // Boolean array converters + // ---------------------------------------------------------------------- + /** + *

Converts an array of object Booleans to primitives.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Boolean} array, may be {@code null} + * @return a {@code boolean} array, {@code null} if null array input + * @throws NullPointerException if array content is {@code null} + */ + public static boolean[] toPrimitive(Boolean[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BOOLEAN_ARRAY; + } + final boolean[] result = new boolean[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i].booleanValue(); + } + return result; + } + + /** + *

Converts an array of object Booleans to primitives handling {@code null}.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code Boolean} array, may be {@code null} + * @param valueForNull the value to insert if {@code null} found + * @return a {@code boolean} array, {@code null} if null array input + */ + public static boolean[] toPrimitive(Boolean[] array, boolean valueForNull) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BOOLEAN_ARRAY; + } + final boolean[] result = new boolean[array.length]; + for (int i = 0; i < array.length; i++) { + Boolean b = array[i]; + result[i] = (b == null ? valueForNull : b.booleanValue()); + } + return result; + } + + /** + *

Converts an array of primitive booleans to objects.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array a {@code boolean} array + * @return a {@code Boolean} array, {@code null} if null array input + */ + public static Boolean[] toObject(boolean[] array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return EMPTY_BOOLEAN_OBJECT_ARRAY; + } + final Boolean[] result = new Boolean[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = (array[i] ? Boolean.TRUE : Boolean.FALSE); + } + return result; + } + + // ---------------------------------------------------------------------- + /** + *

Checks if an array of Objects is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(Object[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive longs is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(long[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive ints is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(int[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive shorts is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(short[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive chars is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(char[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive bytes is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(byte[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive doubles is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(double[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive floats is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(float[] array) { + return array == null || array.length == 0; + } + + /** + *

Checks if an array of primitive booleans is empty or {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is empty or {@code null} + * @since 2.1 + */ + public static boolean isEmpty(boolean[] array) { + return array == null || array.length == 0; + } + + // ---------------------------------------------------------------------- + /** + *

Checks if an array of Objects is not empty or not {@code null}.

+ * + * @param the component type of the array + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(T[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive longs is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(long[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive ints is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(int[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive shorts is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(short[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive chars is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(char[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive bytes is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(byte[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive doubles is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(double[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive floats is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(float[] array) { + return (array != null && array.length != 0); + } + + /** + *

Checks if an array of primitive booleans is not empty or not {@code null}.

+ * + * @param array the array to test + * @return {@code true} if the array is not empty or not {@code null} + * @since 2.5 + */ + public static boolean isNotEmpty(boolean[] array) { + return (array != null && array.length != 0); + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ * + *
+     * ArrayUtils.addAll(null, null)     = null
+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * ArrayUtils.addAll([null], [null]) = [null, null]
+     * ArrayUtils.addAll(["a", "b", "c"], ["1", "2", "3"]) = ["a", "b", "c", "1", "2", "3"]
+     * 
+ * + * @param the component type of the array + * @param array1 the first array whose elements are added to the new array, may be {@code null} + * @param array2 the second array whose elements are added to the new array, may be {@code null} + * @return The new array, {@code null} if both arrays are {@code null}. + * The type of the new array is the type of the first array, + * unless the first array is null, in which case the type is the same as the second array. + * @since 2.1 + * @throws IllegalArgumentException if the array types are incompatible + */ + public static T[] addAll(T[] array1, T... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + final Class type1 = array1.getClass().getComponentType(); + @SuppressWarnings("unchecked") // OK, because array is of type T + T[] joinedArray = (T[]) Array.newInstance(type1, array1.length + array2.length); + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + try { + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + } catch (ArrayStoreException ase) { + // Check if problem was due to incompatible types + /* + * We do this here, rather than before the copy because: + * - it would be a wasted check most of the time + * - safer, in case check turns out to be too strict + */ + final Class type2 = array2.getClass().getComponentType(); + if (!type1.isAssignableFrom(type2)){ + throw new IllegalArgumentException("Cannot store "+type2.getName()+" in an array of " + +type1.getName(), ase); + } + throw ase; // No, so rethrow original + } + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ * + *
+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new boolean[] array. + * @since 2.1 + */ + public static boolean[] addAll(boolean[] array1, boolean... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + boolean[] joinedArray = new boolean[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ * + *
+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new char[] array. + * @since 2.1 + */ + public static char[] addAll(char[] array1, char... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + char[] joinedArray = new char[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ * + *
+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new byte[] array. + * @since 2.1 + */ + public static byte[] addAll(byte[] array1, byte... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + byte[] joinedArray = new byte[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ * + *
+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new short[] array. + * @since 2.1 + */ + public static short[] addAll(short[] array1, short... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + short[] joinedArray = new short[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ * + *
+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new int[] array. + * @since 2.1 + */ + public static int[] addAll(int[] array1, int... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + int[] joinedArray = new int[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ * + *
+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new long[] array. + * @since 2.1 + */ + public static long[] addAll(long[] array1, long... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + long[] joinedArray = new long[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ * + *
+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new float[] array. + * @since 2.1 + */ + public static float[] addAll(float[] array1, float... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + float[] joinedArray = new float[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Adds all the elements of the given arrays into a new array.

+ *

The new array contains all of the element of {@code array1} followed + * by all of the elements {@code array2}. When an array is returned, it is always + * a new array.

+ * + *
+     * ArrayUtils.addAll(array1, null)   = cloned copy of array1
+     * ArrayUtils.addAll(null, array2)   = cloned copy of array2
+     * ArrayUtils.addAll([], [])         = []
+     * 
+ * + * @param array1 the first array whose elements are added to the new array. + * @param array2 the second array whose elements are added to the new array. + * @return The new double[] array. + * @since 2.1 + */ + public static double[] addAll(double[] array1, double... array2) { + if (array1 == null) { + return clone(array2); + } else if (array2 == null) { + return clone(array1); + } + double[] joinedArray = new double[array1.length + array2.length]; + System.arraycopy(array1, 0, joinedArray, 0, array1.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ * + *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element, unless the element itself is null, + * in which case the return type is Object[]

+ * + *
+     * ArrayUtils.add(null, null)      = [null]
+     * ArrayUtils.add(null, "a")       = ["a"]
+     * ArrayUtils.add(["a"], null)     = ["a", null]
+     * ArrayUtils.add(["a"], "b")      = ["a", "b"]
+     * ArrayUtils.add(["a", "b"], "c") = ["a", "b", "c"]
+     * 
+ * + * @param the component type of the array + * @param array the array to "add" the element to, may be {@code null} + * @param element the object to add, may be {@code null} + * @return A new array containing the existing elements plus the new element + * The returned array type will be that of the input array (unless null), + * in which case it will have the same type as the element. + * If both are null, an IllegalArgumentException is thrown + * @since 2.1 + * @throws IllegalArgumentException if both arguments are null + */ + public static T[] add(T[] array, T element) { + Class type; + if (array != null){ + type = array.getClass(); + } else if (element != null) { + type = element.getClass(); + } else { + throw new IllegalArgumentException("Arguments cannot both be null"); + } + @SuppressWarnings("unchecked") // type must be T + T[] newArray = (T[]) copyArrayGrow1(array, type); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ * + *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add(null, true)          = [true]
+     * ArrayUtils.add([true], false)       = [true, false]
+     * ArrayUtils.add([true, false], true) = [true, false, true]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static boolean[] add(boolean[] array, boolean element) { + boolean[] newArray = (boolean[])copyArrayGrow1(array, Boolean.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ * + *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static byte[] add(byte[] array, byte element) { + byte[] newArray = (byte[])copyArrayGrow1(array, Byte.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ * + *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add(null, '0')       = ['0']
+     * ArrayUtils.add(['1'], '0')      = ['1', '0']
+     * ArrayUtils.add(['1', '0'], '1') = ['1', '0', '1']
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static char[] add(char[] array, char element) { + char[] newArray = (char[])copyArrayGrow1(array, Character.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ * + *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static double[] add(double[] array, double element) { + double[] newArray = (double[])copyArrayGrow1(array, Double.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ * + *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static float[] add(float[] array, float element) { + float[] newArray = (float[])copyArrayGrow1(array, Float.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ * + *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static int[] add(int[] array, int element) { + int[] newArray = (int[])copyArrayGrow1(array, Integer.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ * + *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static long[] add(long[] array, long element) { + long[] newArray = (long[])copyArrayGrow1(array, Long.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + *

Copies the given array and adds the given element at the end of the new array.

+ * + *

The new array contains the same elements of the input + * array plus the given element in the last position. The component type of + * the new array is the same as that of the input array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add(null, 0)   = [0]
+     * ArrayUtils.add([1], 0)    = [1, 0]
+     * ArrayUtils.add([1, 0], 1) = [1, 0, 1]
+     * 
+ * + * @param array the array to copy and add the element to, may be {@code null} + * @param element the object to add at the last index of the new array + * @return A new array containing the existing elements plus the new element + * @since 2.1 + */ + public static short[] add(short[] array, short element) { + short[] newArray = (short[])copyArrayGrow1(array, Short.TYPE); + newArray[newArray.length - 1] = element; + return newArray; + } + + /** + * Returns a copy of the given array of size 1 greater than the argument. + * The last value of the array is left to the default value. + * + * @param array The array to copy, must not be {@code null}. + * @param newArrayComponentType If {@code array} is {@code null}, create a + * size 1 array of this type. + * @return A new copy of the array of size 1 greater than the input. + */ + private static Object copyArrayGrow1(Object array, Class newArrayComponentType) { + if (array != null) { + int arrayLength = Array.getLength(array); + Object newArray = Array.newInstance(array.getClass().getComponentType(), arrayLength + 1); + System.arraycopy(array, 0, newArray, 0, arrayLength); + return newArray; + } + return Array.newInstance(newArrayComponentType, 1); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ * + *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add(null, 0, null)      = [null]
+     * ArrayUtils.add(null, 0, "a")       = ["a"]
+     * ArrayUtils.add(["a"], 1, null)     = ["a", null]
+     * ArrayUtils.add(["a"], 1, "b")      = ["a", "b"]
+     * ArrayUtils.add(["a", "b"], 3, "c") = ["a", "b", "c"]
+     * 
+ * + * @param the component type of the array + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + * @throws IllegalArgumentException if both array and element are null + */ + public static T[] add(T[] array, int index, T element) { + Class clss = null; + if (array != null) { + clss = array.getClass().getComponentType(); + } else if (element != null) { + clss = element.getClass(); + } else { + throw new IllegalArgumentException("Array and element cannot both be null"); + } + @SuppressWarnings("unchecked") // the add method creates an array of type clss, which is type T + final T[] newArray = (T[]) add(array, index, element, clss); + return newArray; + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ * + *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add(null, 0, true)          = [true]
+     * ArrayUtils.add([true], 0, false)       = [false, true]
+     * ArrayUtils.add([false], 1, true)       = [false, true]
+     * ArrayUtils.add([true, false], 1, true) = [true, true, false]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static boolean[] add(boolean[] array, int index, boolean element) { + return (boolean[]) add(array, index, Boolean.valueOf(element), Boolean.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ * + *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add(null, 0, 'a')            = ['a']
+     * ArrayUtils.add(['a'], 0, 'b')           = ['b', 'a']
+     * ArrayUtils.add(['a', 'b'], 0, 'c')      = ['c', 'a', 'b']
+     * ArrayUtils.add(['a', 'b'], 1, 'k')      = ['a', 'k', 'b']
+     * ArrayUtils.add(['a', 'b', 'c'], 1, 't') = ['a', 't', 'b', 'c']
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static char[] add(char[] array, int index, char element) { + return (char[]) add(array, index, Character.valueOf(element), Character.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ * + *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add([1], 0, 2)         = [2, 1]
+     * ArrayUtils.add([2, 6], 2, 3)      = [2, 6, 3]
+     * ArrayUtils.add([2, 6], 0, 1)      = [1, 2, 6]
+     * ArrayUtils.add([2, 6, 3], 2, 1)   = [2, 6, 1, 3]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static byte[] add(byte[] array, int index, byte element) { + return (byte[]) add(array, index, Byte.valueOf(element), Byte.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ * + *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add([1], 0, 2)         = [2, 1]
+     * ArrayUtils.add([2, 6], 2, 10)     = [2, 6, 10]
+     * ArrayUtils.add([2, 6], 0, -4)     = [-4, 2, 6]
+     * ArrayUtils.add([2, 6, 3], 2, 1)   = [2, 6, 1, 3]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static short[] add(short[] array, int index, short element) { + return (short[]) add(array, index, Short.valueOf(element), Short.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ * + *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add([1], 0, 2)         = [2, 1]
+     * ArrayUtils.add([2, 6], 2, 10)     = [2, 6, 10]
+     * ArrayUtils.add([2, 6], 0, -4)     = [-4, 2, 6]
+     * ArrayUtils.add([2, 6, 3], 2, 1)   = [2, 6, 1, 3]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static int[] add(int[] array, int index, int element) { + return (int[]) add(array, index, Integer.valueOf(element), Integer.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ * + *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add([1L], 0, 2L)           = [2L, 1L]
+     * ArrayUtils.add([2L, 6L], 2, 10L)      = [2L, 6L, 10L]
+     * ArrayUtils.add([2L, 6L], 0, -4L)      = [-4L, 2L, 6L]
+     * ArrayUtils.add([2L, 6L, 3L], 2, 1L)   = [2L, 6L, 1L, 3L]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static long[] add(long[] array, int index, long element) { + return (long[]) add(array, index, Long.valueOf(element), Long.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ * + *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add([1.1f], 0, 2.2f)               = [2.2f, 1.1f]
+     * ArrayUtils.add([2.3f, 6.4f], 2, 10.5f)        = [2.3f, 6.4f, 10.5f]
+     * ArrayUtils.add([2.6f, 6.7f], 0, -4.8f)        = [-4.8f, 2.6f, 6.7f]
+     * ArrayUtils.add([2.9f, 6.0f, 0.3f], 2, 1.0f)   = [2.9f, 6.0f, 1.0f, 0.3f]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static float[] add(float[] array, int index, float element) { + return (float[]) add(array, index, Float.valueOf(element), Float.TYPE); + } + + /** + *

Inserts the specified element at the specified position in the array. + * Shifts the element currently at that position (if any) and any subsequent + * elements to the right (adds one to their indices).

+ * + *

This method returns a new array with the same elements of the input + * array plus the given element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, a new one element array is returned + * whose component type is the same as the element.

+ * + *
+     * ArrayUtils.add([1.1], 0, 2.2)              = [2.2, 1.1]
+     * ArrayUtils.add([2.3, 6.4], 2, 10.5)        = [2.3, 6.4, 10.5]
+     * ArrayUtils.add([2.6, 6.7], 0, -4.8)        = [-4.8, 2.6, 6.7]
+     * ArrayUtils.add([2.9, 6.0, 0.3], 2, 1.0)    = [2.9, 6.0, 1.0, 0.3]
+     * 
+ * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @return A new array containing the existing elements and the new element + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index > array.length). + */ + public static double[] add(double[] array, int index, double element) { + return (double[]) add(array, index, Double.valueOf(element), Double.TYPE); + } + + /** + * Underlying implementation of add(array, index, element) methods. + * The last parameter is the class, which may not equal element.getClass + * for primitives. + * + * @param array the array to add the element to, may be {@code null} + * @param index the position of the new object + * @param element the object to add + * @param clss the type of the element being added + * @return A new array containing the existing elements and the new element + */ + private static Object add(Object array, int index, Object element, Class clss) { + if (array == null) { + if (index != 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Length: 0"); + } + Object joinedArray = Array.newInstance(clss, 1); + Array.set(joinedArray, 0, element); + return joinedArray; + } + int length = Array.getLength(array); + if (index > length || index < 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Length: " + length); + } + Object result = Array.newInstance(clss, length + 1); + System.arraycopy(array, 0, result, 0, index); + Array.set(result, index, element); + if (index < length) { + System.arraycopy(array, index, result, index + 1, length - index); + } + return result; + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ * + *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.remove(["a"], 0)           = []
+     * ArrayUtils.remove(["a", "b"], 0)      = ["b"]
+     * ArrayUtils.remove(["a", "b"], 1)      = ["a"]
+     * ArrayUtils.remove(["a", "b", "c"], 1) = ["a", "c"]
+     * 
+ * + * @param the component type of the array + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + @SuppressWarnings("unchecked") // remove() always creates an array of the same type as its input + public static T[] remove(T[] array, int index) { + return (T[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ * + *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *
+     * ArrayUtils.removeElement(null, "a")            = null
+     * ArrayUtils.removeElement([], "a")              = []
+     * ArrayUtils.removeElement(["a"], "b")           = ["a"]
+     * ArrayUtils.removeElement(["a", "b"], "a")      = ["b"]
+     * ArrayUtils.removeElement(["a", "b", "a"], "a") = ["b", "a"]
+     * 
+ * + * @param the component type of the array + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static T[] removeElement(T[] array, Object element) { + int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ * + *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.remove([true], 0)              = []
+     * ArrayUtils.remove([true, false], 0)       = [false]
+     * ArrayUtils.remove([true, false], 1)       = [true]
+     * ArrayUtils.remove([true, true, false], 1) = [true, false]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static boolean[] remove(boolean[] array, int index) { + return (boolean[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ * + *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *
+     * ArrayUtils.removeElement(null, true)                = null
+     * ArrayUtils.removeElement([], true)                  = []
+     * ArrayUtils.removeElement([true], false)             = [true]
+     * ArrayUtils.removeElement([true, false], false)      = [true]
+     * ArrayUtils.removeElement([true, false, true], true) = [false, true]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static boolean[] removeElement(boolean[] array, boolean element) { + int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ * + *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.remove([1], 0)          = []
+     * ArrayUtils.remove([1, 0], 0)       = [0]
+     * ArrayUtils.remove([1, 0], 1)       = [1]
+     * ArrayUtils.remove([1, 0, 1], 1)    = [1, 1]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static byte[] remove(byte[] array, int index) { + return (byte[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ * + *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *
+     * ArrayUtils.removeElement(null, 1)        = null
+     * ArrayUtils.removeElement([], 1)          = []
+     * ArrayUtils.removeElement([1], 0)         = [1]
+     * ArrayUtils.removeElement([1, 0], 0)      = [1]
+     * ArrayUtils.removeElement([1, 0, 1], 1)   = [0, 1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static byte[] removeElement(byte[] array, byte element) { + int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ * + *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.remove(['a'], 0)           = []
+     * ArrayUtils.remove(['a', 'b'], 0)      = ['b']
+     * ArrayUtils.remove(['a', 'b'], 1)      = ['a']
+     * ArrayUtils.remove(['a', 'b', 'c'], 1) = ['a', 'c']
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static char[] remove(char[] array, int index) { + return (char[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ * + *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *
+     * ArrayUtils.removeElement(null, 'a')            = null
+     * ArrayUtils.removeElement([], 'a')              = []
+     * ArrayUtils.removeElement(['a'], 'b')           = ['a']
+     * ArrayUtils.removeElement(['a', 'b'], 'a')      = ['b']
+     * ArrayUtils.removeElement(['a', 'b', 'a'], 'a') = ['b', 'a']
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static char[] removeElement(char[] array, char element) { + int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ * + *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.remove([1.1], 0)           = []
+     * ArrayUtils.remove([2.5, 6.0], 0)      = [6.0]
+     * ArrayUtils.remove([2.5, 6.0], 1)      = [2.5]
+     * ArrayUtils.remove([2.5, 6.0, 3.8], 1) = [2.5, 3.8]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static double[] remove(double[] array, int index) { + return (double[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ * + *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *
+     * ArrayUtils.removeElement(null, 1.1)            = null
+     * ArrayUtils.removeElement([], 1.1)              = []
+     * ArrayUtils.removeElement([1.1], 1.2)           = [1.1]
+     * ArrayUtils.removeElement([1.1, 2.3], 1.1)      = [2.3]
+     * ArrayUtils.removeElement([1.1, 2.3, 1.1], 1.1) = [2.3, 1.1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static double[] removeElement(double[] array, double element) { + int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ * + *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.remove([1.1], 0)           = []
+     * ArrayUtils.remove([2.5, 6.0], 0)      = [6.0]
+     * ArrayUtils.remove([2.5, 6.0], 1)      = [2.5]
+     * ArrayUtils.remove([2.5, 6.0, 3.8], 1) = [2.5, 3.8]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static float[] remove(float[] array, int index) { + return (float[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ * + *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *
+     * ArrayUtils.removeElement(null, 1.1)            = null
+     * ArrayUtils.removeElement([], 1.1)              = []
+     * ArrayUtils.removeElement([1.1], 1.2)           = [1.1]
+     * ArrayUtils.removeElement([1.1, 2.3], 1.1)      = [2.3]
+     * ArrayUtils.removeElement([1.1, 2.3, 1.1], 1.1) = [2.3, 1.1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static float[] removeElement(float[] array, float element) { + int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ * + *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.remove([1], 0)         = []
+     * ArrayUtils.remove([2, 6], 0)      = [6]
+     * ArrayUtils.remove([2, 6], 1)      = [2]
+     * ArrayUtils.remove([2, 6, 3], 1)   = [2, 3]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static int[] remove(int[] array, int index) { + return (int[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ * + *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *
+     * ArrayUtils.removeElement(null, 1)      = null
+     * ArrayUtils.removeElement([], 1)        = []
+     * ArrayUtils.removeElement([1], 2)       = [1]
+     * ArrayUtils.removeElement([1, 3], 1)    = [3]
+     * ArrayUtils.removeElement([1, 3, 1], 1) = [3, 1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static int[] removeElement(int[] array, int element) { + int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ * + *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.remove([1], 0)         = []
+     * ArrayUtils.remove([2, 6], 0)      = [6]
+     * ArrayUtils.remove([2, 6], 1)      = [2]
+     * ArrayUtils.remove([2, 6, 3], 1)   = [2, 3]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static long[] remove(long[] array, int index) { + return (long[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ * + *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *
+     * ArrayUtils.removeElement(null, 1)      = null
+     * ArrayUtils.removeElement([], 1)        = []
+     * ArrayUtils.removeElement([1], 2)       = [1]
+     * ArrayUtils.removeElement([1, 3], 1)    = [3]
+     * ArrayUtils.removeElement([1, 3, 1], 1) = [3, 1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static long[] removeElement(long[] array, long element) { + int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ * + *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.remove([1], 0)         = []
+     * ArrayUtils.remove([2, 6], 0)      = [6]
+     * ArrayUtils.remove([2, 6], 1)      = [2]
+     * ArrayUtils.remove([2, 6, 3], 1)   = [2, 3]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + public static short[] remove(short[] array, int index) { + return (short[]) remove((Object) array, index); + } + + /** + *

Removes the first occurrence of the specified element from the + * specified array. All subsequent elements are shifted to the left + * (subtracts one from their indices). If the array doesn't contains + * such an element, no elements are removed from the array.

+ * + *

This method returns a new array with the same elements of the input + * array except the first occurrence of the specified element. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *
+     * ArrayUtils.removeElement(null, 1)      = null
+     * ArrayUtils.removeElement([], 1)        = []
+     * ArrayUtils.removeElement([1], 2)       = [1]
+     * ArrayUtils.removeElement([1, 3], 1)    = [3]
+     * ArrayUtils.removeElement([1, 3, 1], 1) = [3, 1]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param element the element to be removed + * @return A new array containing the existing elements except the first + * occurrence of the specified element. + * @since 2.1 + */ + public static short[] removeElement(short[] array, short element) { + int index = indexOf(array, element); + if (index == INDEX_NOT_FOUND) { + return clone(array); + } + return remove(array, index); + } + + /** + *

Removes the element at the specified position from the specified array. + * All subsequent elements are shifted to the left (subtracts one from + * their indices).

+ * + *

This method returns a new array with the same elements of the input + * array except the element on the specified position. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + * @param array the array to remove the element from, may not be {@code null} + * @param index the position of the element to be removed + * @return A new array containing the existing elements except the element + * at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 2.1 + */ + private static Object remove(Object array, int index) { + int length = getLength(array); + if (index < 0 || index >= length) { + throw new IndexOutOfBoundsException("Index: " + index + ", Length: " + length); + } + + Object result = Array.newInstance(array.getClass().getComponentType(), length - 1); + System.arraycopy(array, 0, result, 0, index); + if (index < length - 1) { + System.arraycopy(array, index + 1, result, index, length - index - 1); + } + + return result; + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ * + *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.removeAll(["a", "b", "c"], 0, 2) = ["b"]
+     * ArrayUtils.removeAll(["a", "b", "c"], 1, 2) = ["a"]
+     * 
+ * + * @param the component type of the array + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + @SuppressWarnings("unchecked") + // removeAll() always creates an array of the same type as its input + public static T[] removeAll(T[] array, int... indices) { + return (T[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes occurrences of specified elements, in specified quantities, + * from the specified array. All subsequent elements are shifted left. + * For any element-to-be-removed specified in greater quantities than + * contained in the original array, no change occurs beyond the + * removal of the existing matching items.

+ * + *

This method returns a new array with the same elements of the input + * array except for the earliest-encountered occurrences of the specified + * elements. The component type of the returned array is always the same + * as that of the input array.

+ * + *
+     * ArrayUtils.removeElements(null, "a", "b")            = null
+     * ArrayUtils.removeElements([], "a", "b")              = []
+     * ArrayUtils.removeElements(["a"], "b", "c")           = ["a"]
+     * ArrayUtils.removeElements(["a", "b"], "a", "c")      = ["b"]
+     * ArrayUtils.removeElements(["a", "b", "a"], "a")      = ["b", "a"]
+     * ArrayUtils.removeElements(["a", "b", "a"], "a", "a") = ["b"]
+     * 
+ * + * @param the component type of the array + * @param array the array to remove the element from, may be {@code null} + * @param values the elements to be removed + * @return A new array containing the existing elements except the + * earliest-encountered occurrences of the specified elements. + * @since 3.0.1 + */ + public static T[] removeElements(T[] array, T... values) { + if (isEmpty(array) || isEmpty(values)) { + return clone(array); + } + HashMap occurrences = new HashMap(values.length); + for (T v : values) { + MutableInt count = occurrences.get(v); + if (count == null) { + occurrences.put(v, new MutableInt(1)); + } else { + count.increment(); + } + } + HashSet toRemove = new HashSet(); + for (Map.Entry e : occurrences.entrySet()) { + T v = e.getKey(); + int found = 0; + for (int i = 0, ct = e.getValue().intValue(); i < ct; i++) { + found = indexOf(array, v, found); + if (found < 0) { + break; + } + toRemove.add(found++); + } + } + return removeAll(array, extractIndices(toRemove)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ * + *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static byte[] removeAll(byte[] array, int... indices) { + return (byte[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes occurrences of specified elements, in specified quantities, + * from the specified array. All subsequent elements are shifted left. + * For any element-to-be-removed specified in greater quantities than + * contained in the original array, no change occurs beyond the + * removal of the existing matching items.

+ * + *

This method returns a new array with the same elements of the input + * array except for the earliest-encountered occurrences of the specified + * elements. The component type of the returned array is always the same + * as that of the input array.

+ * + *
+     * ArrayUtils.removeElements(null, 1, 2)      = null
+     * ArrayUtils.removeElements([], 1, 2)        = []
+     * ArrayUtils.removeElements([1], 2, 3)       = [1]
+     * ArrayUtils.removeElements([1, 3], 1, 2)    = [3]
+     * ArrayUtils.removeElements([1, 3, 1], 1)    = [3, 1]
+     * ArrayUtils.removeElements([1, 3, 1], 1, 1) = [3]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param values the elements to be removed + * @return A new array containing the existing elements except the + * earliest-encountered occurrences of the specified elements. + * @since 3.0.1 + */ + public static byte[] removeElements(byte[] array, byte... values) { + if (isEmpty(array) || isEmpty(values)) { + return clone(array); + } + HashMap occurrences = new HashMap(values.length); + for (byte v : values) { + Byte boxed = Byte.valueOf(v); + MutableInt count = occurrences.get(boxed); + if (count == null) { + occurrences.put(boxed, new MutableInt(1)); + } else { + count.increment(); + } + } + HashSet toRemove = new HashSet(); + for (Map.Entry e : occurrences.entrySet()) { + Byte v = e.getKey(); + int found = 0; + for (int i = 0, ct = e.getValue().intValue(); i < ct; i++) { + found = indexOf(array, v.byteValue(), found); + if (found < 0) { + break; + } + toRemove.add(found++); + } + } + return removeAll(array, extractIndices(toRemove)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ * + *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static short[] removeAll(short[] array, int... indices) { + return (short[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes occurrences of specified elements, in specified quantities, + * from the specified array. All subsequent elements are shifted left. + * For any element-to-be-removed specified in greater quantities than + * contained in the original array, no change occurs beyond the + * removal of the existing matching items.

+ * + *

This method returns a new array with the same elements of the input + * array except for the earliest-encountered occurrences of the specified + * elements. The component type of the returned array is always the same + * as that of the input array.

+ * + *
+     * ArrayUtils.removeElements(null, 1, 2)      = null
+     * ArrayUtils.removeElements([], 1, 2)        = []
+     * ArrayUtils.removeElements([1], 2, 3)       = [1]
+     * ArrayUtils.removeElements([1, 3], 1, 2)    = [3]
+     * ArrayUtils.removeElements([1, 3, 1], 1)    = [3, 1]
+     * ArrayUtils.removeElements([1, 3, 1], 1, 1) = [3]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param values the elements to be removed + * @return A new array containing the existing elements except the + * earliest-encountered occurrences of the specified elements. + * @since 3.0.1 + */ + public static short[] removeElements(short[] array, short... values) { + if (isEmpty(array) || isEmpty(values)) { + return clone(array); + } + HashMap occurrences = new HashMap(values.length); + for (short v : values) { + Short boxed = Short.valueOf(v); + MutableInt count = occurrences.get(boxed); + if (count == null) { + occurrences.put(boxed, new MutableInt(1)); + } else { + count.increment(); + } + } + HashSet toRemove = new HashSet(); + for (Map.Entry e : occurrences.entrySet()) { + Short v = e.getKey(); + int found = 0; + for (int i = 0, ct = e.getValue().intValue(); i < ct; i++) { + found = indexOf(array, v.shortValue(), found); + if (found < 0) { + break; + } + toRemove.add(found++); + } + } + return removeAll(array, extractIndices(toRemove)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ * + *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static int[] removeAll(int[] array, int... indices) { + return (int[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes occurrences of specified elements, in specified quantities, + * from the specified array. All subsequent elements are shifted left. + * For any element-to-be-removed specified in greater quantities than + * contained in the original array, no change occurs beyond the + * removal of the existing matching items.

+ * + *

This method returns a new array with the same elements of the input + * array except for the earliest-encountered occurrences of the specified + * elements. The component type of the returned array is always the same + * as that of the input array.

+ * + *
+     * ArrayUtils.removeElements(null, 1, 2)      = null
+     * ArrayUtils.removeElements([], 1, 2)        = []
+     * ArrayUtils.removeElements([1], 2, 3)       = [1]
+     * ArrayUtils.removeElements([1, 3], 1, 2)    = [3]
+     * ArrayUtils.removeElements([1, 3, 1], 1)    = [3, 1]
+     * ArrayUtils.removeElements([1, 3, 1], 1, 1) = [3]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param values the elements to be removed + * @return A new array containing the existing elements except the + * earliest-encountered occurrences of the specified elements. + * @since 3.0.1 + */ + public static int[] removeElements(int[] array, int... values) { + if (isEmpty(array) || isEmpty(values)) { + return clone(array); + } + HashMap occurrences = new HashMap(values.length); + for (int v : values) { + Integer boxed = Integer.valueOf(v); + MutableInt count = occurrences.get(boxed); + if (count == null) { + occurrences.put(boxed, new MutableInt(1)); + } else { + count.increment(); + } + } + HashSet toRemove = new HashSet(); + for (Map.Entry e : occurrences.entrySet()) { + Integer v = e.getKey(); + int found = 0; + for (int i = 0, ct = e.getValue().intValue(); i < ct; i++) { + found = indexOf(array, v.intValue(), found); + if (found < 0) { + break; + } + toRemove.add(found++); + } + } + return removeAll(array, extractIndices(toRemove)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ * + *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static char[] removeAll(char[] array, int... indices) { + return (char[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes occurrences of specified elements, in specified quantities, + * from the specified array. All subsequent elements are shifted left. + * For any element-to-be-removed specified in greater quantities than + * contained in the original array, no change occurs beyond the + * removal of the existing matching items.

+ * + *

This method returns a new array with the same elements of the input + * array except for the earliest-encountered occurrences of the specified + * elements. The component type of the returned array is always the same + * as that of the input array.

+ * + *
+     * ArrayUtils.removeElements(null, 1, 2)      = null
+     * ArrayUtils.removeElements([], 1, 2)        = []
+     * ArrayUtils.removeElements([1], 2, 3)       = [1]
+     * ArrayUtils.removeElements([1, 3], 1, 2)    = [3]
+     * ArrayUtils.removeElements([1, 3, 1], 1)    = [3, 1]
+     * ArrayUtils.removeElements([1, 3, 1], 1, 1) = [3]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param values the elements to be removed + * @return A new array containing the existing elements except the + * earliest-encountered occurrences of the specified elements. + * @since 3.0.1 + */ + public static char[] removeElements(char[] array, char... values) { + if (isEmpty(array) || isEmpty(values)) { + return clone(array); + } + HashMap occurrences = new HashMap(values.length); + for (char v : values) { + Character boxed = Character.valueOf(v); + MutableInt count = occurrences.get(boxed); + if (count == null) { + occurrences.put(boxed, new MutableInt(1)); + } else { + count.increment(); + } + } + HashSet toRemove = new HashSet(); + for (Map.Entry e : occurrences.entrySet()) { + Character v = e.getKey(); + int found = 0; + for (int i = 0, ct = e.getValue().intValue(); i < ct; i++) { + found = indexOf(array, v.charValue(), found); + if (found < 0) { + break; + } + toRemove.add(found++); + } + } + return removeAll(array, extractIndices(toRemove)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ * + *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static long[] removeAll(long[] array, int... indices) { + return (long[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes occurrences of specified elements, in specified quantities, + * from the specified array. All subsequent elements are shifted left. + * For any element-to-be-removed specified in greater quantities than + * contained in the original array, no change occurs beyond the + * removal of the existing matching items.

+ * + *

This method returns a new array with the same elements of the input + * array except for the earliest-encountered occurrences of the specified + * elements. The component type of the returned array is always the same + * as that of the input array.

+ * + *
+     * ArrayUtils.removeElements(null, 1, 2)      = null
+     * ArrayUtils.removeElements([], 1, 2)        = []
+     * ArrayUtils.removeElements([1], 2, 3)       = [1]
+     * ArrayUtils.removeElements([1, 3], 1, 2)    = [3]
+     * ArrayUtils.removeElements([1, 3, 1], 1)    = [3, 1]
+     * ArrayUtils.removeElements([1, 3, 1], 1, 1) = [3]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param values the elements to be removed + * @return A new array containing the existing elements except the + * earliest-encountered occurrences of the specified elements. + * @since 3.0.1 + */ + public static long[] removeElements(long[] array, long... values) { + if (isEmpty(array) || isEmpty(values)) { + return clone(array); + } + HashMap occurrences = new HashMap(values.length); + for (long v : values) { + Long boxed = Long.valueOf(v); + MutableInt count = occurrences.get(boxed); + if (count == null) { + occurrences.put(boxed, new MutableInt(1)); + } else { + count.increment(); + } + } + HashSet toRemove = new HashSet(); + for (Map.Entry e : occurrences.entrySet()) { + Long v = e.getKey(); + int found = 0; + for (int i = 0, ct = e.getValue().intValue(); i < ct; i++) { + found = indexOf(array, v.longValue(), found); + if (found < 0) { + break; + } + toRemove.add(found++); + } + } + return removeAll(array, extractIndices(toRemove)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ * + *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static float[] removeAll(float[] array, int... indices) { + return (float[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes occurrences of specified elements, in specified quantities, + * from the specified array. All subsequent elements are shifted left. + * For any element-to-be-removed specified in greater quantities than + * contained in the original array, no change occurs beyond the + * removal of the existing matching items.

+ * + *

This method returns a new array with the same elements of the input + * array except for the earliest-encountered occurrences of the specified + * elements. The component type of the returned array is always the same + * as that of the input array.

+ * + *
+     * ArrayUtils.removeElements(null, 1, 2)      = null
+     * ArrayUtils.removeElements([], 1, 2)        = []
+     * ArrayUtils.removeElements([1], 2, 3)       = [1]
+     * ArrayUtils.removeElements([1, 3], 1, 2)    = [3]
+     * ArrayUtils.removeElements([1, 3, 1], 1)    = [3, 1]
+     * ArrayUtils.removeElements([1, 3, 1], 1, 1) = [3]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param values the elements to be removed + * @return A new array containing the existing elements except the + * earliest-encountered occurrences of the specified elements. + * @since 3.0.1 + */ + public static float[] removeElements(float[] array, float... values) { + if (isEmpty(array) || isEmpty(values)) { + return clone(array); + } + HashMap occurrences = new HashMap(values.length); + for (float v : values) { + Float boxed = Float.valueOf(v); + MutableInt count = occurrences.get(boxed); + if (count == null) { + occurrences.put(boxed, new MutableInt(1)); + } else { + count.increment(); + } + } + HashSet toRemove = new HashSet(); + for (Map.Entry e : occurrences.entrySet()) { + Float v = e.getKey(); + int found = 0; + for (int i = 0, ct = e.getValue().intValue(); i < ct; i++) { + found = indexOf(array, v.floatValue(), found); + if (found < 0) { + break; + } + toRemove.add(found++); + } + } + return removeAll(array, extractIndices(toRemove)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ * + *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.removeAll([1], 0)             = []
+     * ArrayUtils.removeAll([2, 6], 0)          = [6]
+     * ArrayUtils.removeAll([2, 6], 0, 1)       = []
+     * ArrayUtils.removeAll([2, 6, 3], 1, 2)    = [2]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 2)    = [6]
+     * ArrayUtils.removeAll([2, 6, 3], 0, 1, 2) = []
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static double[] removeAll(double[] array, int... indices) { + return (double[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes occurrences of specified elements, in specified quantities, + * from the specified array. All subsequent elements are shifted left. + * For any element-to-be-removed specified in greater quantities than + * contained in the original array, no change occurs beyond the + * removal of the existing matching items.

+ * + *

This method returns a new array with the same elements of the input + * array except for the earliest-encountered occurrences of the specified + * elements. The component type of the returned array is always the same + * as that of the input array.

+ * + *
+     * ArrayUtils.removeElements(null, 1, 2)      = null
+     * ArrayUtils.removeElements([], 1, 2)        = []
+     * ArrayUtils.removeElements([1], 2, 3)       = [1]
+     * ArrayUtils.removeElements([1, 3], 1, 2)    = [3]
+     * ArrayUtils.removeElements([1, 3, 1], 1)    = [3, 1]
+     * ArrayUtils.removeElements([1, 3, 1], 1, 1) = [3]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param values the elements to be removed + * @return A new array containing the existing elements except the + * earliest-encountered occurrences of the specified elements. + * @since 3.0.1 + */ + public static double[] removeElements(double[] array, double... values) { + if (isEmpty(array) || isEmpty(values)) { + return clone(array); + } + HashMap occurrences = new HashMap(values.length); + for (double v : values) { + Double boxed = Double.valueOf(v); + MutableInt count = occurrences.get(boxed); + if (count == null) { + occurrences.put(boxed, new MutableInt(1)); + } else { + count.increment(); + } + } + HashSet toRemove = new HashSet(); + for (Map.Entry e : occurrences.entrySet()) { + Double v = e.getKey(); + int found = 0; + for (int i = 0, ct = e.getValue().intValue(); i < ct; i++) { + found = indexOf(array, v.doubleValue(), found); + if (found < 0) { + break; + } + toRemove.add(found++); + } + } + return removeAll(array, extractIndices(toRemove)); + } + + /** + *

Removes the elements at the specified positions from the specified array. + * All remaining elements are shifted to the left.

+ * + *

This method returns a new array with the same elements of the input + * array except those at the specified positions. The component + * type of the returned array is always the same as that of the input + * array.

+ * + *

If the input array is {@code null}, an IndexOutOfBoundsException + * will be thrown, because in that case no valid index can be specified.

+ * + *
+     * ArrayUtils.removeAll([true, false, true], 0, 2) = [false]
+     * ArrayUtils.removeAll([true, false, true], 1, 2) = [true]
+     * 
+ * + * @param array the array to remove the element from, may not be {@code null} + * @param indices the positions of the elements to be removed + * @return A new array containing the existing elements except those + * at the specified positions. + * @throws IndexOutOfBoundsException if any index is out of range + * (index < 0 || index >= array.length), or if the array is {@code null}. + * @since 3.0.1 + */ + public static boolean[] removeAll(boolean[] array, int... indices) { + return (boolean[]) removeAll((Object) array, clone(indices)); + } + + /** + *

Removes occurrences of specified elements, in specified quantities, + * from the specified array. All subsequent elements are shifted left. + * For any element-to-be-removed specified in greater quantities than + * contained in the original array, no change occurs beyond the + * removal of the existing matching items.

+ * + *

This method returns a new array with the same elements of the input + * array except for the earliest-encountered occurrences of the specified + * elements. The component type of the returned array is always the same + * as that of the input array.

+ * + *
+     * ArrayUtils.removeElements(null, true, false)               = null
+     * ArrayUtils.removeElements([], true, false)                 = []
+     * ArrayUtils.removeElements([true], false, false)            = [true]
+     * ArrayUtils.removeElements([true, false], true, true)       = [false]
+     * ArrayUtils.removeElements([true, false, true], true)       = [false, true]
+     * ArrayUtils.removeElements([true, false, true], true, true) = [false]
+     * 
+ * + * @param array the array to remove the element from, may be {@code null} + * @param values the elements to be removed + * @return A new array containing the existing elements except the + * earliest-encountered occurrences of the specified elements. + * @since 3.0.1 + */ + public static boolean[] removeElements(boolean[] array, boolean... values) { + if (isEmpty(array) || isEmpty(values)) { + return clone(array); + } + HashMap occurrences = new HashMap(values.length); + for (boolean v : values) { + Boolean boxed = Boolean.valueOf(v); + MutableInt count = occurrences.get(boxed); + if (count == null) { + occurrences.put(boxed, new MutableInt(1)); + } else { + count.increment(); + } + } + HashSet toRemove = new HashSet(); + for (Map.Entry e : occurrences.entrySet()) { + Boolean v = e.getKey(); + int found = 0; + for (int i = 0, ct = e.getValue().intValue(); i < ct; i++) { + found = indexOf(array, v.booleanValue(), found); + if (found < 0) { + break; + } + toRemove.add(found++); + } + } + return removeAll(array, extractIndices(toRemove)); + } + + /** + * Removes multiple array elements specified by index. + * @param array source + * @param indices to remove, WILL BE SORTED--so only clones of user-owned arrays! + * @return new array of same type minus elements specified by unique values of {@code indices} + * @since 3.0.1 + */ + private static Object removeAll(Object array, int... indices) { + int length = getLength(array); + int diff = 0; + + if (isNotEmpty(indices)) { + Arrays.sort(indices); + + int i = indices.length; + int prevIndex = length; + while (--i >= 0) { + int index = indices[i]; + if (index < 0 || index >= length) { + throw new IndexOutOfBoundsException("Index: " + index + ", Length: " + length); + } + if (index >= prevIndex) { + continue; + } + diff++; + prevIndex = index; + } + } + Object result = Array.newInstance(array.getClass().getComponentType(), length - diff); + if (diff < length) { + int end = length; + int dest = length - diff; + for (int i = indices.length - 1; i >= 0; i--) { + int index = indices[i]; + if (end - index > 1) { + int cp = end - index - 1; + dest -= cp; + System.arraycopy(array, index + 1, result, dest, cp); + } + end = index; + } + if (end > 0) { + System.arraycopy(array, 0, result, 0, end); + } + } + return result; + } + + /** + * Extract a set of Integer indices into an int[]. + * @param coll {@code HashSet} of {@code Integer} + * @return int[] + * @since 3.0.1 + */ + private static int[] extractIndices(HashSet coll) { + int[] result = new int[coll.size()]; + int i = 0; + for (Integer index : coll) { + result[i++] = index.intValue(); + } + return result; + } +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/CharSequenceUtils.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/CharSequenceUtils.java new file mode 100644 index 00000000..8e090f57 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/CharSequenceUtils.java @@ -0,0 +1,197 @@ +/* + * 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 external.org.apache.commons.lang3; + +/** + *

Operations on {@link java.lang.CharSequence} that are + * {@code null} safe.

+ * + * @see java.lang.CharSequence + * @since 3.0 + * @version $Id: CharSequenceUtils.java 1199894 2011-11-09 17:53:59Z ggregory $ + */ +public class CharSequenceUtils { + + /** + *

{@code CharSequenceUtils} instances should NOT be constructed in + * standard programming.

+ * + *

This constructor is public to permit tools that require a JavaBean + * instance to operate.

+ */ + public CharSequenceUtils() { + super(); + } + + //----------------------------------------------------------------------- + /** + *

Returns a new {@code CharSequence} that is a subsequence of this + * sequence starting with the {@code char} value at the specified index.

+ * + *

This provides the {@code CharSequence} equivalent to {@link String#substring(int)}. + * The length (in {@code char}) of the returned sequence is {@code length() - start}, + * so if {@code start == end} then an empty sequence is returned.

+ * + * @param cs the specified subsequence, null returns null + * @param start the start index, inclusive, valid + * @return a new subsequence, may be null + * @throws IndexOutOfBoundsException if {@code start} is negative or if + * {@code start} is greater than {@code length()} + */ + public static CharSequence subSequence(CharSequence cs, int start) { + return cs == null ? null : cs.subSequence(start, cs.length()); + } + + //----------------------------------------------------------------------- + /** + *

Finds the first index in the {@code CharSequence} that matches the + * specified character.

+ * + * @param cs the {@code CharSequence} to be processed, not null + * @param searchChar the char to be searched for + * @param start the start index, negative starts at the string start + * @return the index where the search char was found, -1 if not found + */ + static int indexOf(CharSequence cs, int searchChar, int start) { + if (cs instanceof String) { + return ((String) cs).indexOf(searchChar, start); + } else { + int sz = cs.length(); + if (start < 0) { + start = 0; + } + for (int i = start; i < sz; i++) { + if (cs.charAt(i) == searchChar) { + return i; + } + } + return -1; + } + } + + /** + * Used by the indexOf(CharSequence methods) as a green implementation of indexOf. + * + * @param cs the {@code CharSequence} to be processed + * @param searchChar the {@code CharSequence} to be searched for + * @param start the start index + * @return the index where the search sequence was found + */ + static int indexOf(CharSequence cs, CharSequence searchChar, int start) { + return cs.toString().indexOf(searchChar.toString(), start); +// if (cs instanceof String && searchChar instanceof String) { +// // TODO: Do we assume searchChar is usually relatively small; +// // If so then calling toString() on it is better than reverting to +// // the green implementation in the else block +// return ((String) cs).indexOf((String) searchChar, start); +// } else { +// // TODO: Implement rather than convert to String +// return cs.toString().indexOf(searchChar.toString(), start); +// } + } + + /** + *

Finds the last index in the {@code CharSequence} that matches the + * specified character.

+ * + * @param cs the {@code CharSequence} to be processed + * @param searchChar the char to be searched for + * @param start the start index, negative returns -1, beyond length starts at end + * @return the index where the search char was found, -1 if not found + */ + static int lastIndexOf(CharSequence cs, int searchChar, int start) { + if (cs instanceof String) { + return ((String) cs).lastIndexOf(searchChar, start); + } else { + int sz = cs.length(); + if (start < 0) { + return -1; + } + if (start >= sz) { + start = sz - 1; + } + for (int i = start; i >= 0; --i) { + if (cs.charAt(i) == searchChar) { + return i; + } + } + return -1; + } + } + + /** + * Used by the lastIndexOf(CharSequence methods) as a green implementation of lastIndexOf + * + * @param cs the {@code CharSequence} to be processed + * @param searchChar the {@code CharSequence} to be searched for + * @param start the start index + * @return the index where the search sequence was found + */ + static int lastIndexOf(CharSequence cs, CharSequence searchChar, int start) { + return cs.toString().lastIndexOf(searchChar.toString(), start); +// if (cs instanceof String && searchChar instanceof String) { +// // TODO: Do we assume searchChar is usually relatively small; +// // If so then calling toString() on it is better than reverting to +// // the green implementation in the else block +// return ((String) cs).lastIndexOf((String) searchChar, start); +// } else { +// // TODO: Implement rather than convert to String +// return cs.toString().lastIndexOf(searchChar.toString(), start); +// } + } + + /** + * Green implementation of toCharArray. + * + * @param cs the {@code CharSequence} to be processed + * @return the resulting char array + */ + static char[] toCharArray(CharSequence cs) { + if (cs instanceof String) { + return ((String) cs).toCharArray(); + } else { + int sz = cs.length(); + char[] array = new char[cs.length()]; + for (int i = 0; i < sz; i++) { + array[i] = cs.charAt(i); + } + return array; + } + } + + /** + * Green implementation of regionMatches. + * + * @param cs the {@code CharSequence} to be processed + * @param ignoreCase whether or not to be case insensitive + * @param thisStart the index to start on the {@code cs} CharSequence + * @param substring the {@code CharSequence} to be looked for + * @param start the index to start on the {@code substring} CharSequence + * @param length character length of the region + * @return whether the region matched + */ + static boolean regionMatches(CharSequence cs, boolean ignoreCase, int thisStart, + CharSequence substring, int start, int length) { + if (cs instanceof String && substring instanceof String) { + return ((String) cs).regionMatches(ignoreCase, thisStart, (String) substring, start, length); + } else { + // TODO: Implement rather than convert to String + return cs.toString().regionMatches(ignoreCase, thisStart, substring.toString(), start, length); + } + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/CharUtils.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/CharUtils.java new file mode 100644 index 00000000..d176a824 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/CharUtils.java @@ -0,0 +1,539 @@ +/* + * 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 external.org.apache.commons.lang3; + +/** + *

Operations on char primitives and Character objects.

+ * + *

This class tries to handle {@code null} input gracefully. + * An exception will not be thrown for a {@code null} input. + * Each method documents its behaviour in more detail.

+ * + *

#ThreadSafe#

+ * @since 2.1 + * @version $Id: CharUtils.java 1158279 2011-08-16 14:06:45Z ggregory $ + */ +public class CharUtils { + + private static final String[] CHAR_STRING_ARRAY = new String[128]; + + /** + * {@code \u000a} linefeed LF ('\n'). + * + * @see JLF: Escape Sequences + * for Character and String Literals + * @since 2.2 + */ + public static final char LF = '\n'; + + /** + * {@code \u000d} carriage return CR ('\r'). + * + * @see JLF: Escape Sequences + * for Character and String Literals + * @since 2.2 + */ + public static final char CR = '\r'; + + + static { + for (char c = 0; c < CHAR_STRING_ARRAY.length; c++) { + CHAR_STRING_ARRAY[c] = String.valueOf(c); + } + } + + /** + *

{@code CharUtils} instances should NOT be constructed in standard programming. + * Instead, the class should be used as {@code CharUtils.toString('c');}.

+ * + *

This constructor is public to permit tools that require a JavaBean instance + * to operate.

+ */ + public CharUtils() { + super(); + } + + //----------------------------------------------------------------------- + /** + *

Converts the character to a Character.

+ * + *

For ASCII 7 bit characters, this uses a cache that will return the + * same Character object each time.

+ * + *
+     *   CharUtils.toCharacterObject(' ')  = ' '
+     *   CharUtils.toCharacterObject('A')  = 'A'
+     * 
+ * + * @deprecated Java 5 introduced {@link Character#valueOf(char)} which caches chars 0 through 127. + * @param ch the character to convert + * @return a Character of the specified character + */ + @Deprecated + public static Character toCharacterObject(char ch) { + return Character.valueOf(ch); + } + + /** + *

Converts the String to a Character using the first character, returning + * null for empty Strings.

+ * + *

For ASCII 7 bit characters, this uses a cache that will return the + * same Character object each time.

+ * + *
+     *   CharUtils.toCharacterObject(null) = null
+     *   CharUtils.toCharacterObject("")   = null
+     *   CharUtils.toCharacterObject("A")  = 'A'
+     *   CharUtils.toCharacterObject("BA") = 'B'
+     * 
+ * + * @param str the character to convert + * @return the Character value of the first letter of the String + */ + public static Character toCharacterObject(String str) { + if (StringUtils.isEmpty(str)) { + return null; + } + return Character.valueOf(str.charAt(0)); + } + + //----------------------------------------------------------------------- + /** + *

Converts the Character to a char throwing an exception for {@code null}.

+ * + *
+     *   CharUtils.toChar(' ')  = ' '
+     *   CharUtils.toChar('A')  = 'A'
+     *   CharUtils.toChar(null) throws IllegalArgumentException
+     * 
+ * + * @param ch the character to convert + * @return the char value of the Character + * @throws IllegalArgumentException if the Character is null + */ + public static char toChar(Character ch) { + if (ch == null) { + throw new IllegalArgumentException("The Character must not be null"); + } + return ch.charValue(); + } + + /** + *

Converts the Character to a char handling {@code null}.

+ * + *
+     *   CharUtils.toChar(null, 'X') = 'X'
+     *   CharUtils.toChar(' ', 'X')  = ' '
+     *   CharUtils.toChar('A', 'X')  = 'A'
+     * 
+ * + * @param ch the character to convert + * @param defaultValue the value to use if the Character is null + * @return the char value of the Character or the default if null + */ + public static char toChar(Character ch, char defaultValue) { + if (ch == null) { + return defaultValue; + } + return ch.charValue(); + } + + //----------------------------------------------------------------------- + /** + *

Converts the String to a char using the first character, throwing + * an exception on empty Strings.

+ * + *
+     *   CharUtils.toChar("A")  = 'A'
+     *   CharUtils.toChar("BA") = 'B'
+     *   CharUtils.toChar(null) throws IllegalArgumentException
+     *   CharUtils.toChar("")   throws IllegalArgumentException
+     * 
+ * + * @param str the character to convert + * @return the char value of the first letter of the String + * @throws IllegalArgumentException if the String is empty + */ + public static char toChar(String str) { + if (StringUtils.isEmpty(str)) { + throw new IllegalArgumentException("The String must not be empty"); + } + return str.charAt(0); + } + + /** + *

Converts the String to a char using the first character, defaulting + * the value on empty Strings.

+ * + *
+     *   CharUtils.toChar(null, 'X') = 'X'
+     *   CharUtils.toChar("", 'X')   = 'X'
+     *   CharUtils.toChar("A", 'X')  = 'A'
+     *   CharUtils.toChar("BA", 'X') = 'B'
+     * 
+ * + * @param str the character to convert + * @param defaultValue the value to use if the Character is null + * @return the char value of the first letter of the String or the default if null + */ + public static char toChar(String str, char defaultValue) { + if (StringUtils.isEmpty(str)) { + return defaultValue; + } + return str.charAt(0); + } + + //----------------------------------------------------------------------- + /** + *

Converts the character to the Integer it represents, throwing an + * exception if the character is not numeric.

+ * + *

This method coverts the char '1' to the int 1 and so on.

+ * + *
+     *   CharUtils.toIntValue('3')  = 3
+     *   CharUtils.toIntValue('A')  throws IllegalArgumentException
+     * 
+ * + * @param ch the character to convert + * @return the int value of the character + * @throws IllegalArgumentException if the character is not ASCII numeric + */ + public static int toIntValue(char ch) { + if (isAsciiNumeric(ch) == false) { + throw new IllegalArgumentException("The character " + ch + " is not in the range '0' - '9'"); + } + return ch - 48; + } + + /** + *

Converts the character to the Integer it represents, throwing an + * exception if the character is not numeric.

+ * + *

This method coverts the char '1' to the int 1 and so on.

+ * + *
+     *   CharUtils.toIntValue('3', -1)  = 3
+     *   CharUtils.toIntValue('A', -1)  = -1
+     * 
+ * + * @param ch the character to convert + * @param defaultValue the default value to use if the character is not numeric + * @return the int value of the character + */ + public static int toIntValue(char ch, int defaultValue) { + if (isAsciiNumeric(ch) == false) { + return defaultValue; + } + return ch - 48; + } + + /** + *

Converts the character to the Integer it represents, throwing an + * exception if the character is not numeric.

+ * + *

This method coverts the char '1' to the int 1 and so on.

+ * + *
+     *   CharUtils.toIntValue('3')  = 3
+     *   CharUtils.toIntValue(null) throws IllegalArgumentException
+     *   CharUtils.toIntValue('A')  throws IllegalArgumentException
+     * 
+ * + * @param ch the character to convert, not null + * @return the int value of the character + * @throws IllegalArgumentException if the Character is not ASCII numeric or is null + */ + public static int toIntValue(Character ch) { + if (ch == null) { + throw new IllegalArgumentException("The character must not be null"); + } + return toIntValue(ch.charValue()); + } + + /** + *

Converts the character to the Integer it represents, throwing an + * exception if the character is not numeric.

+ * + *

This method coverts the char '1' to the int 1 and so on.

+ * + *
+     *   CharUtils.toIntValue(null, -1) = -1
+     *   CharUtils.toIntValue('3', -1)  = 3
+     *   CharUtils.toIntValue('A', -1)  = -1
+     * 
+ * + * @param ch the character to convert + * @param defaultValue the default value to use if the character is not numeric + * @return the int value of the character + */ + public static int toIntValue(Character ch, int defaultValue) { + if (ch == null) { + return defaultValue; + } + return toIntValue(ch.charValue(), defaultValue); + } + + //----------------------------------------------------------------------- + /** + *

Converts the character to a String that contains the one character.

+ * + *

For ASCII 7 bit characters, this uses a cache that will return the + * same String object each time.

+ * + *
+     *   CharUtils.toString(' ')  = " "
+     *   CharUtils.toString('A')  = "A"
+     * 
+ * + * @param ch the character to convert + * @return a String containing the one specified character + */ + public static String toString(char ch) { + if (ch < 128) { + return CHAR_STRING_ARRAY[ch]; + } + return new String(new char[] {ch}); + } + + /** + *

Converts the character to a String that contains the one character.

+ * + *

For ASCII 7 bit characters, this uses a cache that will return the + * same String object each time.

+ * + *

If {@code null} is passed in, {@code null} will be returned.

+ * + *
+     *   CharUtils.toString(null) = null
+     *   CharUtils.toString(' ')  = " "
+     *   CharUtils.toString('A')  = "A"
+     * 
+ * + * @param ch the character to convert + * @return a String containing the one specified character + */ + public static String toString(Character ch) { + if (ch == null) { + return null; + } + return toString(ch.charValue()); + } + + //-------------------------------------------------------------------------- + /** + *

Converts the string to the Unicode format '\u0020'.

+ * + *

This format is the Java source code format.

+ * + *
+     *   CharUtils.unicodeEscaped(' ') = "\u0020"
+     *   CharUtils.unicodeEscaped('A') = "\u0041"
+     * 
+ * + * @param ch the character to convert + * @return the escaped Unicode string + */ + public static String unicodeEscaped(char ch) { + if (ch < 0x10) { + return "\\u000" + Integer.toHexString(ch); + } else if (ch < 0x100) { + return "\\u00" + Integer.toHexString(ch); + } else if (ch < 0x1000) { + return "\\u0" + Integer.toHexString(ch); + } + return "\\u" + Integer.toHexString(ch); + } + + /** + *

Converts the string to the Unicode format '\u0020'.

+ * + *

This format is the Java source code format.

+ * + *

If {@code null} is passed in, {@code null} will be returned.

+ * + *
+     *   CharUtils.unicodeEscaped(null) = null
+     *   CharUtils.unicodeEscaped(' ')  = "\u0020"
+     *   CharUtils.unicodeEscaped('A')  = "\u0041"
+     * 
+ * + * @param ch the character to convert, may be null + * @return the escaped Unicode string, null if null input + */ + public static String unicodeEscaped(Character ch) { + if (ch == null) { + return null; + } + return unicodeEscaped(ch.charValue()); + } + + //-------------------------------------------------------------------------- + /** + *

Checks whether the character is ASCII 7 bit.

+ * + *
+     *   CharUtils.isAscii('a')  = true
+     *   CharUtils.isAscii('A')  = true
+     *   CharUtils.isAscii('3')  = true
+     *   CharUtils.isAscii('-')  = true
+     *   CharUtils.isAscii('\n') = true
+     *   CharUtils.isAscii('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if less than 128 + */ + public static boolean isAscii(char ch) { + return ch < 128; + } + + /** + *

Checks whether the character is ASCII 7 bit printable.

+ * + *
+     *   CharUtils.isAsciiPrintable('a')  = true
+     *   CharUtils.isAsciiPrintable('A')  = true
+     *   CharUtils.isAsciiPrintable('3')  = true
+     *   CharUtils.isAsciiPrintable('-')  = true
+     *   CharUtils.isAsciiPrintable('\n') = false
+     *   CharUtils.isAsciiPrintable('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 32 and 126 inclusive + */ + public static boolean isAsciiPrintable(char ch) { + return ch >= 32 && ch < 127; + } + + /** + *

Checks whether the character is ASCII 7 bit control.

+ * + *
+     *   CharUtils.isAsciiControl('a')  = false
+     *   CharUtils.isAsciiControl('A')  = false
+     *   CharUtils.isAsciiControl('3')  = false
+     *   CharUtils.isAsciiControl('-')  = false
+     *   CharUtils.isAsciiControl('\n') = true
+     *   CharUtils.isAsciiControl('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if less than 32 or equals 127 + */ + public static boolean isAsciiControl(char ch) { + return ch < 32 || ch == 127; + } + + /** + *

Checks whether the character is ASCII 7 bit alphabetic.

+ * + *
+     *   CharUtils.isAsciiAlpha('a')  = true
+     *   CharUtils.isAsciiAlpha('A')  = true
+     *   CharUtils.isAsciiAlpha('3')  = false
+     *   CharUtils.isAsciiAlpha('-')  = false
+     *   CharUtils.isAsciiAlpha('\n') = false
+     *   CharUtils.isAsciiAlpha('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 65 and 90 or 97 and 122 inclusive + */ + public static boolean isAsciiAlpha(char ch) { + return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'); + } + + /** + *

Checks whether the character is ASCII 7 bit alphabetic upper case.

+ * + *
+     *   CharUtils.isAsciiAlphaUpper('a')  = false
+     *   CharUtils.isAsciiAlphaUpper('A')  = true
+     *   CharUtils.isAsciiAlphaUpper('3')  = false
+     *   CharUtils.isAsciiAlphaUpper('-')  = false
+     *   CharUtils.isAsciiAlphaUpper('\n') = false
+     *   CharUtils.isAsciiAlphaUpper('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 65 and 90 inclusive + */ + public static boolean isAsciiAlphaUpper(char ch) { + return ch >= 'A' && ch <= 'Z'; + } + + /** + *

Checks whether the character is ASCII 7 bit alphabetic lower case.

+ * + *
+     *   CharUtils.isAsciiAlphaLower('a')  = true
+     *   CharUtils.isAsciiAlphaLower('A')  = false
+     *   CharUtils.isAsciiAlphaLower('3')  = false
+     *   CharUtils.isAsciiAlphaLower('-')  = false
+     *   CharUtils.isAsciiAlphaLower('\n') = false
+     *   CharUtils.isAsciiAlphaLower('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 97 and 122 inclusive + */ + public static boolean isAsciiAlphaLower(char ch) { + return ch >= 'a' && ch <= 'z'; + } + + /** + *

Checks whether the character is ASCII 7 bit numeric.

+ * + *
+     *   CharUtils.isAsciiNumeric('a')  = false
+     *   CharUtils.isAsciiNumeric('A')  = false
+     *   CharUtils.isAsciiNumeric('3')  = true
+     *   CharUtils.isAsciiNumeric('-')  = false
+     *   CharUtils.isAsciiNumeric('\n') = false
+     *   CharUtils.isAsciiNumeric('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 48 and 57 inclusive + */ + public static boolean isAsciiNumeric(char ch) { + return ch >= '0' && ch <= '9'; + } + + /** + *

Checks whether the character is ASCII 7 bit numeric.

+ * + *
+     *   CharUtils.isAsciiAlphanumeric('a')  = true
+     *   CharUtils.isAsciiAlphanumeric('A')  = true
+     *   CharUtils.isAsciiAlphanumeric('3')  = true
+     *   CharUtils.isAsciiAlphanumeric('-')  = false
+     *   CharUtils.isAsciiAlphanumeric('\n') = false
+     *   CharUtils.isAsciiAlphanumeric('©') = false
+     * 
+ * + * @param ch the character to check + * @return true if between 48 and 57 or 65 and 90 or 97 and 122 inclusive + */ + public static boolean isAsciiAlphanumeric(char ch) { + return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9'); + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ClassUtils.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ClassUtils.java new file mode 100644 index 00000000..183d1d11 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ClassUtils.java @@ -0,0 +1,1135 @@ +/* + * 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 external.org.apache.commons.lang3; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + + +/** + *

Operates on classes without using reflection.

+ * + *

This class handles invalid {@code null} inputs as best it can. + * Each method documents its behaviour in more detail.

+ * + *

The notion of a {@code canonical name} includes the human + * readable name for the type, for example {@code int[]}. The + * non-canonical method variants work with the JVM names, such as + * {@code [I}.

+ * + * @since 2.0 + * @version $Id: ClassUtils.java 1199894 2011-11-09 17:53:59Z ggregory $ + */ +public class ClassUtils { + + /** + *

The package separator character: '.' == {@value}.

+ */ + public static final char PACKAGE_SEPARATOR_CHAR = '.'; + + /** + *

The package separator String: {@code "."}.

+ */ + public static final String PACKAGE_SEPARATOR = String.valueOf(PACKAGE_SEPARATOR_CHAR); + + /** + *

The inner class separator character: '$' == {@value}.

+ */ + public static final char INNER_CLASS_SEPARATOR_CHAR = '$'; + + /** + *

The inner class separator String: {@code "$"}.

+ */ + public static final String INNER_CLASS_SEPARATOR = String.valueOf(INNER_CLASS_SEPARATOR_CHAR); + + /** + * Maps primitive {@code Class}es to their corresponding wrapper {@code Class}. + */ + private static final Map, Class> primitiveWrapperMap = new HashMap, Class>(); + static { + primitiveWrapperMap.put(Boolean.TYPE, Boolean.class); + primitiveWrapperMap.put(Byte.TYPE, Byte.class); + primitiveWrapperMap.put(Character.TYPE, Character.class); + primitiveWrapperMap.put(Short.TYPE, Short.class); + primitiveWrapperMap.put(Integer.TYPE, Integer.class); + primitiveWrapperMap.put(Long.TYPE, Long.class); + primitiveWrapperMap.put(Double.TYPE, Double.class); + primitiveWrapperMap.put(Float.TYPE, Float.class); + primitiveWrapperMap.put(Void.TYPE, Void.TYPE); + } + + /** + * Maps wrapper {@code Class}es to their corresponding primitive types. + */ + private static final Map, Class> wrapperPrimitiveMap = new HashMap, Class>(); + static { + for (Class primitiveClass : primitiveWrapperMap.keySet()) { + Class wrapperClass = primitiveWrapperMap.get(primitiveClass); + if (!primitiveClass.equals(wrapperClass)) { + wrapperPrimitiveMap.put(wrapperClass, primitiveClass); + } + } + } + + /** + * Maps a primitive class name to its corresponding abbreviation used in array class names. + */ + private static final Map abbreviationMap = new HashMap(); + + /** + * Maps an abbreviation used in array class names to corresponding primitive class name. + */ + private static final Map reverseAbbreviationMap = new HashMap(); + + /** + * Add primitive type abbreviation to maps of abbreviations. + * + * @param primitive Canonical name of primitive type + * @param abbreviation Corresponding abbreviation of primitive type + */ + private static void addAbbreviation(String primitive, String abbreviation) { + abbreviationMap.put(primitive, abbreviation); + reverseAbbreviationMap.put(abbreviation, primitive); + } + + /** + * Feed abbreviation maps + */ + static { + addAbbreviation("int", "I"); + addAbbreviation("boolean", "Z"); + addAbbreviation("float", "F"); + addAbbreviation("long", "J"); + addAbbreviation("short", "S"); + addAbbreviation("byte", "B"); + addAbbreviation("double", "D"); + addAbbreviation("char", "C"); + } + + /** + *

ClassUtils instances should NOT be constructed in standard programming. + * Instead, the class should be used as + * {@code ClassUtils.getShortClassName(cls)}.

+ * + *

This constructor is public to permit tools that require a JavaBean + * instance to operate.

+ */ + public ClassUtils() { + super(); + } + + // Short class name + // ---------------------------------------------------------------------- + /** + *

Gets the class name minus the package name for an {@code Object}.

+ * + * @param object the class to get the short name for, may be null + * @param valueIfNull the value to return if null + * @return the class name of the object without the package name, or the null value + */ + public static String getShortClassName(Object object, String valueIfNull) { + if (object == null) { + return valueIfNull; + } + return getShortClassName(object.getClass()); + } + + /** + *

Gets the class name minus the package name from a {@code Class}.

+ * + *

Consider using the Java 5 API {@link Class#getSimpleName()} instead. + * The one known difference is that this code will return {@code "Map.Entry"} while + * the {@code java.lang.Class} variant will simply return {@code "Entry"}.

+ * + * @param cls the class to get the short name for. + * @return the class name without the package name or an empty string + */ + public static String getShortClassName(Class cls) { + if (cls == null) { + return StringUtils.EMPTY; + } + return getShortClassName(cls.getName()); + } + + /** + *

Gets the class name minus the package name from a String.

+ * + *

The string passed in is assumed to be a class name - it is not checked.

+ + *

Note that this method differs from Class.getSimpleName() in that this will + * return {@code "Map.Entry"} whilst the {@code java.lang.Class} variant will simply + * return {@code "Entry"}.

+ * + * @param className the className to get the short name for + * @return the class name of the class without the package name or an empty string + */ + public static String getShortClassName(String className) { + if (className == null) { + return StringUtils.EMPTY; + } + if (className.length() == 0) { + return StringUtils.EMPTY; + } + + StringBuilder arrayPrefix = new StringBuilder(); + + // Handle array encoding + if (className.startsWith("[")) { + while (className.charAt(0) == '[') { + className = className.substring(1); + arrayPrefix.append("[]"); + } + // Strip Object type encoding + if (className.charAt(0) == 'L' && className.charAt(className.length() - 1) == ';') { + className = className.substring(1, className.length() - 1); + } + } + + if (reverseAbbreviationMap.containsKey(className)) { + className = reverseAbbreviationMap.get(className); + } + + int lastDotIdx = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR); + int innerIdx = className.indexOf( + INNER_CLASS_SEPARATOR_CHAR, lastDotIdx == -1 ? 0 : lastDotIdx + 1); + String out = className.substring(lastDotIdx + 1); + if (innerIdx != -1) { + out = out.replace(INNER_CLASS_SEPARATOR_CHAR, PACKAGE_SEPARATOR_CHAR); + } + return out + arrayPrefix; + } + + /** + *

Null-safe version of aClass.getSimpleName()

+ * + * @param cls the class for which to get the simple name. + * @return the simple class name. + * @since 3.0 + * @see Class#getSimpleName() + */ + public static String getSimpleName(Class cls) { + if (cls == null) { + return StringUtils.EMPTY; + } + return cls.getSimpleName(); + } + + /** + *

Null-safe version of aClass.getSimpleName()

+ * + * @param object the object for which to get the simple class name. + * @param valueIfNull the value to return if object is null + * @return the simple class name. + * @since 3.0 + * @see Class#getSimpleName() + */ + public static String getSimpleName(Object object, String valueIfNull) { + if (object == null) { + return valueIfNull; + } + return getSimpleName(object.getClass()); + } + + // Package name + // ---------------------------------------------------------------------- + /** + *

Gets the package name of an {@code Object}.

+ * + * @param object the class to get the package name for, may be null + * @param valueIfNull the value to return if null + * @return the package name of the object, or the null value + */ + public static String getPackageName(Object object, String valueIfNull) { + if (object == null) { + return valueIfNull; + } + return getPackageName(object.getClass()); + } + + /** + *

Gets the package name of a {@code Class}.

+ * + * @param cls the class to get the package name for, may be {@code null}. + * @return the package name or an empty string + */ + public static String getPackageName(Class cls) { + if (cls == null) { + return StringUtils.EMPTY; + } + return getPackageName(cls.getName()); + } + + /** + *

Gets the package name from a {@code String}.

+ * + *

The string passed in is assumed to be a class name - it is not checked.

+ *

If the class is unpackaged, return an empty string.

+ * + * @param className the className to get the package name for, may be {@code null} + * @return the package name or an empty string + */ + public static String getPackageName(String className) { + if (className == null || className.length() == 0) { + return StringUtils.EMPTY; + } + + // Strip array encoding + while (className.charAt(0) == '[') { + className = className.substring(1); + } + // Strip Object type encoding + if (className.charAt(0) == 'L' && className.charAt(className.length() - 1) == ';') { + className = className.substring(1); + } + + int i = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR); + if (i == -1) { + return StringUtils.EMPTY; + } + return className.substring(0, i); + } + + // Superclasses/Superinterfaces + // ---------------------------------------------------------------------- + /** + *

Gets a {@code List} of superclasses for the given class.

+ * + * @param cls the class to look up, may be {@code null} + * @return the {@code List} of superclasses in order going up from this one + * {@code null} if null input + */ + public static List> getAllSuperclasses(Class cls) { + if (cls == null) { + return null; + } + List> classes = new ArrayList>(); + Class superclass = cls.getSuperclass(); + while (superclass != null) { + classes.add(superclass); + superclass = superclass.getSuperclass(); + } + return classes; + } + + /** + *

Gets a {@code List} of all interfaces implemented by the given + * class and its superclasses.

+ * + *

The order is determined by looking through each interface in turn as + * declared in the source file and following its hierarchy up. Then each + * superclass is considered in the same way. Later duplicates are ignored, + * so the order is maintained.

+ * + * @param cls the class to look up, may be {@code null} + * @return the {@code List} of interfaces in order, + * {@code null} if null input + */ + public static List> getAllInterfaces(Class cls) { + if (cls == null) { + return null; + } + + LinkedHashSet> interfacesFound = new LinkedHashSet>(); + getAllInterfaces(cls, interfacesFound); + + return new ArrayList>(interfacesFound); + } + + /** + * Get the interfaces for the specified class. + * + * @param cls the class to look up, may be {@code null} + * @param interfacesFound the {@code Set} of interfaces for the class + */ + private static void getAllInterfaces(Class cls, HashSet> interfacesFound) { + while (cls != null) { + Class[] interfaces = cls.getInterfaces(); + + for (Class i : interfaces) { + if (interfacesFound.add(i)) { + getAllInterfaces(i, interfacesFound); + } + } + + cls = cls.getSuperclass(); + } + } + + // Convert list + // ---------------------------------------------------------------------- + /** + *

Given a {@code List} of class names, this method converts them into classes.

+ * + *

A new {@code List} is returned. If the class name cannot be found, {@code null} + * is stored in the {@code List}. If the class name in the {@code List} is + * {@code null}, {@code null} is stored in the output {@code List}.

+ * + * @param classNames the classNames to change + * @return a {@code List} of Class objects corresponding to the class names, + * {@code null} if null input + * @throws ClassCastException if classNames contains a non String entry + */ + public static List> convertClassNamesToClasses(List classNames) { + if (classNames == null) { + return null; + } + List> classes = new ArrayList>(classNames.size()); + for (String className : classNames) { + try { + classes.add(Class.forName(className)); + } catch (Exception ex) { + classes.add(null); + } + } + return classes; + } + + /** + *

Given a {@code List} of {@code Class} objects, this method converts + * them into class names.

+ * + *

A new {@code List} is returned. {@code null} objects will be copied into + * the returned list as {@code null}.

+ * + * @param classes the classes to change + * @return a {@code List} of class names corresponding to the Class objects, + * {@code null} if null input + * @throws ClassCastException if {@code classes} contains a non-{@code Class} entry + */ + public static List convertClassesToClassNames(List> classes) { + if (classes == null) { + return null; + } + List classNames = new ArrayList(classes.size()); + for (Class cls : classes) { + if (cls == null) { + classNames.add(null); + } else { + classNames.add(cls.getName()); + } + } + return classNames; + } + + // Is assignable + // ---------------------------------------------------------------------- + /** + *

Checks if an array of Classes can be assigned to another array of Classes.

+ * + *

This method calls {@link #isAssignable(Class, Class) isAssignable} for each + * Class pair in the input arrays. It can be used to check if a set of arguments + * (the first parameter) are suitably compatible with a set of method parameter types + * (the second parameter).

+ * + *

Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method, this + * method takes into account widenings of primitive classes and + * {@code null}s.

+ * + *

Primitive widenings allow an int to be assigned to a {@code long}, + * {@code float} or {@code double}. This method returns the correct + * result for these cases.

+ * + *

{@code Null} may be assigned to any reference type. This method will + * return {@code true} if {@code null} is passed in and the toClass is + * non-primitive.

+ * + *

Specifically, this method tests whether the type represented by the + * specified {@code Class} parameter can be converted to the type + * represented by this {@code Class} object via an identity conversion + * widening primitive or widening reference conversion. See + * The Java Language Specification, + * sections 5.1.1, 5.1.2 and 5.1.4 for details.

+ * + *

Since Lang 3.0, this method will default behavior for + * calculating assignability between primitive and wrapper types corresponding + * to the running Java version; i.e. autoboxing will be the default + * behavior in VMs running Java versions >= 1.5.

+ * + * @param classArray the array of Classes to check, may be {@code null} + * @param toClassArray the array of Classes to try to assign into, may be {@code null} + * @return {@code true} if assignment possible + */ + public static boolean isAssignable(Class[] classArray, Class... toClassArray) { + return isAssignable(classArray, toClassArray, SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_1_5)); + } + + /** + *

Checks if an array of Classes can be assigned to another array of Classes.

+ * + *

This method calls {@link #isAssignable(Class, Class) isAssignable} for each + * Class pair in the input arrays. It can be used to check if a set of arguments + * (the first parameter) are suitably compatible with a set of method parameter types + * (the second parameter).

+ * + *

Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method, this + * method takes into account widenings of primitive classes and + * {@code null}s.

+ * + *

Primitive widenings allow an int to be assigned to a {@code long}, + * {@code float} or {@code double}. This method returns the correct + * result for these cases.

+ * + *

{@code Null} may be assigned to any reference type. This method will + * return {@code true} if {@code null} is passed in and the toClass is + * non-primitive.

+ * + *

Specifically, this method tests whether the type represented by the + * specified {@code Class} parameter can be converted to the type + * represented by this {@code Class} object via an identity conversion + * widening primitive or widening reference conversion. See + * The Java Language Specification, + * sections 5.1.1, 5.1.2 and 5.1.4 for details.

+ * + * @param classArray the array of Classes to check, may be {@code null} + * @param toClassArray the array of Classes to try to assign into, may be {@code null} + * @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers + * @return {@code true} if assignment possible + */ + public static boolean isAssignable(Class[] classArray, Class[] toClassArray, boolean autoboxing) { + if (ArrayUtils.isSameLength(classArray, toClassArray) == false) { + return false; + } + if (classArray == null) { + classArray = ArrayUtils.EMPTY_CLASS_ARRAY; + } + if (toClassArray == null) { + toClassArray = ArrayUtils.EMPTY_CLASS_ARRAY; + } + for (int i = 0; i < classArray.length; i++) { + if (isAssignable(classArray[i], toClassArray[i], autoboxing) == false) { + return false; + } + } + return true; + } + + /** + * Returns whether the given {@code type} is a primitive or primitive wrapper ({@link Boolean}, {@link Byte}, {@link Character}, + * {@link Short}, {@link Integer}, {@link Long}, {@link Double}, {@link Float}). + * + * @param type + * The class to query or null. + * @return true if the given {@code type} is a primitive or primitive wrapper ({@link Boolean}, {@link Byte}, {@link Character}, + * {@link Short}, {@link Integer}, {@link Long}, {@link Double}, {@link Float}). + * @since 3.1 + */ + public static boolean isPrimitiveOrWrapper(Class type) { + if (type == null) { + return false; + } + return type.isPrimitive() || isPrimitiveWrapper(type); + } + + /** + * Returns whether the given {@code type} is a primitive wrapper ({@link Boolean}, {@link Byte}, {@link Character}, {@link Short}, + * {@link Integer}, {@link Long}, {@link Double}, {@link Float}). + * + * @param type + * The class to query or null. + * @return true if the given {@code type} is a primitive wrapper ({@link Boolean}, {@link Byte}, {@link Character}, {@link Short}, + * {@link Integer}, {@link Long}, {@link Double}, {@link Float}). + * @since 3.1 + */ + public static boolean isPrimitiveWrapper(Class type) { + return wrapperPrimitiveMap.containsKey(type); + } + + /** + *

Checks if one {@code Class} can be assigned to a variable of + * another {@code Class}.

+ * + *

Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method, + * this method takes into account widenings of primitive classes and + * {@code null}s.

+ * + *

Primitive widenings allow an int to be assigned to a long, float or + * double. This method returns the correct result for these cases.

+ * + *

{@code Null} may be assigned to any reference type. This method + * will return {@code true} if {@code null} is passed in and the + * toClass is non-primitive.

+ * + *

Specifically, this method tests whether the type represented by the + * specified {@code Class} parameter can be converted to the type + * represented by this {@code Class} object via an identity conversion + * widening primitive or widening reference conversion. See + * The Java Language Specification, + * sections 5.1.1, 5.1.2 and 5.1.4 for details.

+ * + *

Since Lang 3.0, this method will default behavior for + * calculating assignability between primitive and wrapper types corresponding + * to the running Java version; i.e. autoboxing will be the default + * behavior in VMs running Java versions >= 1.5.

+ * + * @param cls the Class to check, may be null + * @param toClass the Class to try to assign into, returns false if null + * @return {@code true} if assignment possible + */ + public static boolean isAssignable(Class cls, Class toClass) { + return isAssignable(cls, toClass, SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_1_5)); + } + + /** + *

Checks if one {@code Class} can be assigned to a variable of + * another {@code Class}.

+ * + *

Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method, + * this method takes into account widenings of primitive classes and + * {@code null}s.

+ * + *

Primitive widenings allow an int to be assigned to a long, float or + * double. This method returns the correct result for these cases.

+ * + *

{@code Null} may be assigned to any reference type. This method + * will return {@code true} if {@code null} is passed in and the + * toClass is non-primitive.

+ * + *

Specifically, this method tests whether the type represented by the + * specified {@code Class} parameter can be converted to the type + * represented by this {@code Class} object via an identity conversion + * widening primitive or widening reference conversion. See + * The Java Language Specification, + * sections 5.1.1, 5.1.2 and 5.1.4 for details.

+ * + * @param cls the Class to check, may be null + * @param toClass the Class to try to assign into, returns false if null + * @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers + * @return {@code true} if assignment possible + */ + public static boolean isAssignable(Class cls, Class toClass, boolean autoboxing) { + if (toClass == null) { + return false; + } + // have to check for null, as isAssignableFrom doesn't + if (cls == null) { + return !toClass.isPrimitive(); + } + //autoboxing: + if (autoboxing) { + if (cls.isPrimitive() && !toClass.isPrimitive()) { + cls = primitiveToWrapper(cls); + if (cls == null) { + return false; + } + } + if (toClass.isPrimitive() && !cls.isPrimitive()) { + cls = wrapperToPrimitive(cls); + if (cls == null) { + return false; + } + } + } + if (cls.equals(toClass)) { + return true; + } + if (cls.isPrimitive()) { + if (toClass.isPrimitive() == false) { + return false; + } + if (Integer.TYPE.equals(cls)) { + return Long.TYPE.equals(toClass) + || Float.TYPE.equals(toClass) + || Double.TYPE.equals(toClass); + } + if (Long.TYPE.equals(cls)) { + return Float.TYPE.equals(toClass) + || Double.TYPE.equals(toClass); + } + if (Boolean.TYPE.equals(cls)) { + return false; + } + if (Double.TYPE.equals(cls)) { + return false; + } + if (Float.TYPE.equals(cls)) { + return Double.TYPE.equals(toClass); + } + if (Character.TYPE.equals(cls)) { + return Integer.TYPE.equals(toClass) + || Long.TYPE.equals(toClass) + || Float.TYPE.equals(toClass) + || Double.TYPE.equals(toClass); + } + if (Short.TYPE.equals(cls)) { + return Integer.TYPE.equals(toClass) + || Long.TYPE.equals(toClass) + || Float.TYPE.equals(toClass) + || Double.TYPE.equals(toClass); + } + if (Byte.TYPE.equals(cls)) { + return Short.TYPE.equals(toClass) + || Integer.TYPE.equals(toClass) + || Long.TYPE.equals(toClass) + || Float.TYPE.equals(toClass) + || Double.TYPE.equals(toClass); + } + // should never get here + return false; + } + return toClass.isAssignableFrom(cls); + } + + /** + *

Converts the specified primitive Class object to its corresponding + * wrapper Class object.

+ * + *

NOTE: From v2.2, this method handles {@code Void.TYPE}, + * returning {@code Void.TYPE}.

+ * + * @param cls the class to convert, may be null + * @return the wrapper class for {@code cls} or {@code cls} if + * {@code cls} is not a primitive. {@code null} if null input. + * @since 2.1 + */ + public static Class primitiveToWrapper(Class cls) { + Class convertedClass = cls; + if (cls != null && cls.isPrimitive()) { + convertedClass = primitiveWrapperMap.get(cls); + } + return convertedClass; + } + + /** + *

Converts the specified array of primitive Class objects to an array of + * its corresponding wrapper Class objects.

+ * + * @param classes the class array to convert, may be null or empty + * @return an array which contains for each given class, the wrapper class or + * the original class if class is not a primitive. {@code null} if null input. + * Empty array if an empty array passed in. + * @since 2.1 + */ + public static Class[] primitivesToWrappers(Class... classes) { + if (classes == null) { + return null; + } + + if (classes.length == 0) { + return classes; + } + + Class[] convertedClasses = new Class[classes.length]; + for (int i = 0; i < classes.length; i++) { + convertedClasses[i] = primitiveToWrapper(classes[i]); + } + return convertedClasses; + } + + /** + *

Converts the specified wrapper class to its corresponding primitive + * class.

+ * + *

This method is the counter part of {@code primitiveToWrapper()}. + * If the passed in class is a wrapper class for a primitive type, this + * primitive type will be returned (e.g. {@code Integer.TYPE} for + * {@code Integer.class}). For other classes, or if the parameter is + * null, the return value is null.

+ * + * @param cls the class to convert, may be null + * @return the corresponding primitive type if {@code cls} is a + * wrapper class, null otherwise + * @see #primitiveToWrapper(Class) + * @since 2.4 + */ + public static Class wrapperToPrimitive(Class cls) { + return wrapperPrimitiveMap.get(cls); + } + + /** + *

Converts the specified array of wrapper Class objects to an array of + * its corresponding primitive Class objects.

+ * + *

This method invokes {@code wrapperToPrimitive()} for each element + * of the passed in array.

+ * + * @param classes the class array to convert, may be null or empty + * @return an array which contains for each given class, the primitive class or + * null if the original class is not a wrapper class. {@code null} if null input. + * Empty array if an empty array passed in. + * @see #wrapperToPrimitive(Class) + * @since 2.4 + */ + public static Class[] wrappersToPrimitives(Class... classes) { + if (classes == null) { + return null; + } + + if (classes.length == 0) { + return classes; + } + + Class[] convertedClasses = new Class[classes.length]; + for (int i = 0; i < classes.length; i++) { + convertedClasses[i] = wrapperToPrimitive(classes[i]); + } + return convertedClasses; + } + + // Inner class + // ---------------------------------------------------------------------- + /** + *

Is the specified class an inner class or static nested class.

+ * + * @param cls the class to check, may be null + * @return {@code true} if the class is an inner or static nested class, + * false if not or {@code null} + */ + public static boolean isInnerClass(Class cls) { + return cls != null && cls.getEnclosingClass() != null; + } + + // Class loading + // ---------------------------------------------------------------------- + /** + * Returns the class represented by {@code className} using the + * {@code classLoader}. This implementation supports the syntaxes + * "{@code java.util.Map.Entry[]}", "{@code java.util.Map$Entry[]}", + * "{@code [Ljava.util.Map.Entry;}", and "{@code [Ljava.util.Map$Entry;}". + * + * @param classLoader the class loader to use to load the class + * @param className the class name + * @param initialize whether the class must be initialized + * @return the class represented by {@code className} using the {@code classLoader} + * @throws ClassNotFoundException if the class is not found + */ + public static Class getClass( + ClassLoader classLoader, String className, boolean initialize) throws ClassNotFoundException { + try { + Class clazz; + if (abbreviationMap.containsKey(className)) { + String clsName = "[" + abbreviationMap.get(className); + clazz = Class.forName(clsName, initialize, classLoader).getComponentType(); + } else { + clazz = Class.forName(toCanonicalName(className), initialize, classLoader); + } + return clazz; + } catch (ClassNotFoundException ex) { + // allow path separators (.) as inner class name separators + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR); + + if (lastDotIndex != -1) { + try { + return getClass(classLoader, className.substring(0, lastDotIndex) + + INNER_CLASS_SEPARATOR_CHAR + className.substring(lastDotIndex + 1), + initialize); + } catch (ClassNotFoundException ex2) { // NOPMD + // ignore exception + } + } + + throw ex; + } + } + + /** + * Returns the (initialized) class represented by {@code className} + * using the {@code classLoader}. This implementation supports + * the syntaxes "{@code java.util.Map.Entry[]}", + * "{@code java.util.Map$Entry[]}", "{@code [Ljava.util.Map.Entry;}", + * and "{@code [Ljava.util.Map$Entry;}". + * + * @param classLoader the class loader to use to load the class + * @param className the class name + * @return the class represented by {@code className} using the {@code classLoader} + * @throws ClassNotFoundException if the class is not found + */ + public static Class getClass(ClassLoader classLoader, String className) throws ClassNotFoundException { + return getClass(classLoader, className, true); + } + + /** + * Returns the (initialized) class represented by {@code className} + * using the current thread's context class loader. This implementation + * supports the syntaxes "{@code java.util.Map.Entry[]}", + * "{@code java.util.Map$Entry[]}", "{@code [Ljava.util.Map.Entry;}", + * and "{@code [Ljava.util.Map$Entry;}". + * + * @param className the class name + * @return the class represented by {@code className} using the current thread's context class loader + * @throws ClassNotFoundException if the class is not found + */ + public static Class getClass(String className) throws ClassNotFoundException { + return getClass(className, true); + } + + /** + * Returns the class represented by {@code className} using the + * current thread's context class loader. This implementation supports the + * syntaxes "{@code java.util.Map.Entry[]}", "{@code java.util.Map$Entry[]}", + * "{@code [Ljava.util.Map.Entry;}", and "{@code [Ljava.util.Map$Entry;}". + * + * @param className the class name + * @param initialize whether the class must be initialized + * @return the class represented by {@code className} using the current thread's context class loader + * @throws ClassNotFoundException if the class is not found + */ + public static Class getClass(String className, boolean initialize) throws ClassNotFoundException { + ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); + ClassLoader loader = contextCL == null ? ClassUtils.class.getClassLoader() : contextCL; + return getClass(loader, className, initialize); + } + + // Public method + // ---------------------------------------------------------------------- + /** + *

Returns the desired Method much like {@code Class.getMethod}, however + * it ensures that the returned Method is from a public class or interface and not + * from an anonymous inner class. This means that the Method is invokable and + * doesn't fall foul of Java bug + * 4071957). + * + *

Set set = Collections.unmodifiableSet(...);
+     *  Method method = ClassUtils.getPublicMethod(set.getClass(), "isEmpty",  new Class[0]);
+     *  Object result = method.invoke(set, new Object[]);
+ *

+ * + * @param cls the class to check, not null + * @param methodName the name of the method + * @param parameterTypes the list of parameters + * @return the method + * @throws NullPointerException if the class is null + * @throws SecurityException if a a security violation occured + * @throws NoSuchMethodException if the method is not found in the given class + * or if the metothod doen't conform with the requirements + */ + public static Method getPublicMethod(Class cls, String methodName, Class... parameterTypes) + throws SecurityException, NoSuchMethodException { + + Method declaredMethod = cls.getMethod(methodName, parameterTypes); + if (Modifier.isPublic(declaredMethod.getDeclaringClass().getModifiers())) { + return declaredMethod; + } + + List> candidateClasses = new ArrayList>(); + candidateClasses.addAll(getAllInterfaces(cls)); + candidateClasses.addAll(getAllSuperclasses(cls)); + + for (Class candidateClass : candidateClasses) { + if (!Modifier.isPublic(candidateClass.getModifiers())) { + continue; + } + Method candidateMethod; + try { + candidateMethod = candidateClass.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException ex) { + continue; + } + if (Modifier.isPublic(candidateMethod.getDeclaringClass().getModifiers())) { + return candidateMethod; + } + } + + throw new NoSuchMethodException("Can't find a public method for " + + methodName + " " + ArrayUtils.toString(parameterTypes)); + } + + // ---------------------------------------------------------------------- + /** + * Converts a class name to a JLS style class name. + * + * @param className the class name + * @return the converted name + */ + private static String toCanonicalName(String className) { + className = StringUtils.deleteWhitespace(className); + if (className == null) { + throw new NullPointerException("className must not be null."); + } else if (className.endsWith("[]")) { + StringBuilder classNameBuffer = new StringBuilder(); + while (className.endsWith("[]")) { + className = className.substring(0, className.length() - 2); + classNameBuffer.append("["); + } + String abbreviation = abbreviationMap.get(className); + if (abbreviation != null) { + classNameBuffer.append(abbreviation); + } else { + classNameBuffer.append("L").append(className).append(";"); + } + className = classNameBuffer.toString(); + } + return className; + } + + /** + *

Converts an array of {@code Object} in to an array of {@code Class} objects. + * If any of these objects is null, a null element will be inserted into the array.

+ * + *

This method returns {@code null} for a {@code null} input array.

+ * + * @param array an {@code Object} array + * @return a {@code Class} array, {@code null} if null array input + * @since 2.4 + */ + public static Class[] toClass(Object... array) { + if (array == null) { + return null; + } else if (array.length == 0) { + return ArrayUtils.EMPTY_CLASS_ARRAY; + } + Class[] classes = new Class[array.length]; + for (int i = 0; i < array.length; i++) { + classes[i] = array[i] == null ? null : array[i].getClass(); + } + return classes; + } + + // Short canonical name + // ---------------------------------------------------------------------- + /** + *

Gets the canonical name minus the package name for an {@code Object}.

+ * + * @param object the class to get the short name for, may be null + * @param valueIfNull the value to return if null + * @return the canonical name of the object without the package name, or the null value + * @since 2.4 + */ + public static String getShortCanonicalName(Object object, String valueIfNull) { + if (object == null) { + return valueIfNull; + } + return getShortCanonicalName(object.getClass().getName()); + } + + /** + *

Gets the canonical name minus the package name from a {@code Class}.

+ * + * @param cls the class to get the short name for. + * @return the canonical name without the package name or an empty string + * @since 2.4 + */ + public static String getShortCanonicalName(Class cls) { + if (cls == null) { + return StringUtils.EMPTY; + } + return getShortCanonicalName(cls.getName()); + } + + /** + *

Gets the canonical name minus the package name from a String.

+ * + *

The string passed in is assumed to be a canonical name - it is not checked.

+ * + * @param canonicalName the class name to get the short name for + * @return the canonical name of the class without the package name or an empty string + * @since 2.4 + */ + public static String getShortCanonicalName(String canonicalName) { + return ClassUtils.getShortClassName(getCanonicalName(canonicalName)); + } + + // Package name + // ---------------------------------------------------------------------- + /** + *

Gets the package name from the canonical name of an {@code Object}.

+ * + * @param object the class to get the package name for, may be null + * @param valueIfNull the value to return if null + * @return the package name of the object, or the null value + * @since 2.4 + */ + public static String getPackageCanonicalName(Object object, String valueIfNull) { + if (object == null) { + return valueIfNull; + } + return getPackageCanonicalName(object.getClass().getName()); + } + + /** + *

Gets the package name from the canonical name of a {@code Class}.

+ * + * @param cls the class to get the package name for, may be {@code null}. + * @return the package name or an empty string + * @since 2.4 + */ + public static String getPackageCanonicalName(Class cls) { + if (cls == null) { + return StringUtils.EMPTY; + } + return getPackageCanonicalName(cls.getName()); + } + + /** + *

Gets the package name from the canonical name.

+ * + *

The string passed in is assumed to be a canonical name - it is not checked.

+ *

If the class is unpackaged, return an empty string.

+ * + * @param canonicalName the canonical name to get the package name for, may be {@code null} + * @return the package name or an empty string + * @since 2.4 + */ + public static String getPackageCanonicalName(String canonicalName) { + return ClassUtils.getPackageName(getCanonicalName(canonicalName)); + } + + /** + *

Converts a given name of class into canonical format. + * If name of class is not a name of array class it returns + * unchanged name.

+ *

Example: + *

    + *
  • {@code getCanonicalName("[I") = "int[]"}
  • + *
  • {@code getCanonicalName("[Ljava.lang.String;") = "java.lang.String[]"}
  • + *
  • {@code getCanonicalName("java.lang.String") = "java.lang.String"}
  • + *
+ *

+ * + * @param className the name of class + * @return canonical form of class name + * @since 2.4 + */ + private static String getCanonicalName(String className) { + className = StringUtils.deleteWhitespace(className); + if (className == null) { + return null; + } else { + int dim = 0; + while (className.startsWith("[")) { + dim++; + className = className.substring(1); + } + if (dim < 1) { + return className; + } else { + if (className.startsWith("L")) { + className = className.substring( + 1, + className.endsWith(";") + ? className.length() - 1 + : className.length()); + } else { + if (className.length() > 0) { + className = reverseAbbreviationMap.get(className.substring(0, 1)); + } + } + StringBuilder canonicalClassNameBuffer = new StringBuilder(className); + for (int i = 0; i < dim; i++) { + canonicalClassNameBuffer.append("[]"); + } + return canonicalClassNameBuffer.toString(); + } + } + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/JavaVersion.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/JavaVersion.java new file mode 100644 index 00000000..6e2b6fea --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/JavaVersion.java @@ -0,0 +1,168 @@ +/* + * 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 external.org.apache.commons.lang3; + +/** + *

An enum representing all the versions of the Java specification. + * This is intended to mirror available values from the + * java.specification.version System property.

+ * + * @since 3.0 + * @version $Id: $ + */ +public enum JavaVersion { + + /** + * The Java version reported by Android. This is not an official Java version number. + */ + JAVA_0_9(1.5f, "0.9"), + + /** + * Java 1.1. + */ + JAVA_1_1(1.1f, "1.1"), + + /** + * Java 1.2. + */ + JAVA_1_2(1.2f, "1.2"), + + /** + * Java 1.3. + */ + JAVA_1_3(1.3f, "1.3"), + + /** + * Java 1.4. + */ + JAVA_1_4(1.4f, "1.4"), + + /** + * Java 1.5. + */ + JAVA_1_5(1.5f, "1.5"), + + /** + * Java 1.6. + */ + JAVA_1_6(1.6f, "1.6"), + + /** + * Java 1.7. + */ + JAVA_1_7(1.7f, "1.7"), + + /** + * Java 1.8. + */ + JAVA_1_8(1.8f, "1.8"); + + /** + * The float value. + */ + private float value; + /** + * The standard name. + */ + private String name; + + /** + * Constructor. + * + * @param value the float value + * @param name the standard name, not null + */ + JavaVersion(final float value, final String name) { + this.value = value; + this.name = name; + } + + //----------------------------------------------------------------------- + /** + *

Whether this version of Java is at least the version of Java passed in.

+ * + *

For example:
+ * {@code myVersion.atLeast(JavaVersion.JAVA_1_4)}

+ * + * @param requiredVersion the version to check against, not null + * @return true if this version is equal to or greater than the specified version + */ + public boolean atLeast(JavaVersion requiredVersion) { + return this.value >= requiredVersion.value; + } + + /** + * Transforms the given string with a Java version number to the + * corresponding constant of this enumeration class. This method is used + * internally. + * + * @param nom the Java version as string + * @return the corresponding enumeration constant or null if the + * version is unknown + */ + // helper for static importing + static JavaVersion getJavaVersion(final String nom) { + return get(nom); + } + + /** + * Transforms the given string with a Java version number to the + * corresponding constant of this enumeration class. This method is used + * internally. + * + * @param nom the Java version as string + * @return the corresponding enumeration constant or null if the + * version is unknown + */ + static JavaVersion get(final String nom) { + if ("0.9".equals(nom)) { + return JAVA_0_9; + } else if ("1.1".equals(nom)) { + return JAVA_1_1; + } else if ("1.2".equals(nom)) { + return JAVA_1_2; + } else if ("1.3".equals(nom)) { + return JAVA_1_3; + } else if ("1.4".equals(nom)) { + return JAVA_1_4; + } else if ("1.5".equals(nom)) { + return JAVA_1_5; + } else if ("1.6".equals(nom)) { + return JAVA_1_6; + } else if ("1.7".equals(nom)) { + return JAVA_1_7; + } else if ("1.8".equals(nom)) { + return JAVA_1_8; + } else { + return null; + } + } + + //----------------------------------------------------------------------- + /** + *

The string value is overridden to return the standard name.

+ * + *

For example, "1.5".

+ * + * @return the name, not null + */ + @Override + public String toString() { + return name; + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ObjectUtils.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ObjectUtils.java new file mode 100644 index 00000000..d8e85a8c --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/ObjectUtils.java @@ -0,0 +1,609 @@ +/* + * 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 external.org.apache.commons.lang3; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeSet; + + +import external.org.apache.commons.lang3.exception.CloneFailedException; +import external.org.apache.commons.lang3.mutable.MutableInt; + +/** + *

Operations on {@code Object}.

+ * + *

This class tries to handle {@code null} input gracefully. + * An exception will generally not be thrown for a {@code null} input. + * Each method documents its behaviour in more detail.

+ * + *

#ThreadSafe#

+ * @since 1.0 + * @version $Id: ObjectUtils.java 1199894 2011-11-09 17:53:59Z ggregory $ + */ +//@Immutable +public class ObjectUtils { + + /** + *

Singleton used as a {@code null} placeholder where + * {@code null} has another meaning.

+ * + *

For example, in a {@code HashMap} the + * {@link java.util.HashMap#get(java.lang.Object)} method returns + * {@code null} if the {@code Map} contains {@code null} or if there + * is no matching key. The {@code Null} placeholder can be used to + * distinguish between these two cases.

+ * + *

Another example is {@code Hashtable}, where {@code null} + * cannot be stored.

+ * + *

This instance is Serializable.

+ */ + public static final Null NULL = new Null(); + + /** + *

{@code ObjectUtils} instances should NOT be constructed in + * standard programming. Instead, the static methods on the class should + * be used, such as {@code ObjectUtils.defaultIfNull("a","b");}.

+ * + *

This constructor is public to permit tools that require a JavaBean + * instance to operate.

+ */ + public ObjectUtils() { + super(); + } + + // Defaulting + //----------------------------------------------------------------------- + /** + *

Returns a default value if the object passed is {@code null}.

+ * + *
+     * ObjectUtils.defaultIfNull(null, null)      = null
+     * ObjectUtils.defaultIfNull(null, "")        = ""
+     * ObjectUtils.defaultIfNull(null, "zz")      = "zz"
+     * ObjectUtils.defaultIfNull("abc", *)        = "abc"
+     * ObjectUtils.defaultIfNull(Boolean.TRUE, *) = Boolean.TRUE
+     * 
+ * + * @param the type of the object + * @param object the {@code Object} to test, may be {@code null} + * @param defaultValue the default value to return, may be {@code null} + * @return {@code object} if it is not {@code null}, defaultValue otherwise + */ + public static T defaultIfNull(T object, T defaultValue) { + return object != null ? object : defaultValue; + } + + /** + *

Returns the first value in the array which is not {@code null}. + * If all the values are {@code null} or the array is {@code null} + * or empty then {@code null} is returned.

+ * + *
+     * ObjectUtils.firstNonNull(null, null)      = null
+     * ObjectUtils.firstNonNull(null, "")        = ""
+     * ObjectUtils.firstNonNull(null, null, "")  = ""
+     * ObjectUtils.firstNonNull(null, "zz")      = "zz"
+     * ObjectUtils.firstNonNull("abc", *)        = "abc"
+     * ObjectUtils.firstNonNull(null, "xyz", *)  = "xyz"
+     * ObjectUtils.firstNonNull(Boolean.TRUE, *) = Boolean.TRUE
+     * ObjectUtils.firstNonNull()                = null
+     * 
+ * + * @param the component type of the array + * @param values the values to test, may be {@code null} or empty + * @return the first value from {@code values} which is not {@code null}, + * or {@code null} if there are no non-null values + * @since 3.0 + */ + public static T firstNonNull(T... values) { + if (values != null) { + for (T val : values) { + if (val != null) { + return val; + } + } + } + return null; + } + + // Null-safe equals/hashCode + //----------------------------------------------------------------------- + /** + *

Compares two objects for equality, where either one or both + * objects may be {@code null}.

+ * + *
+     * ObjectUtils.equals(null, null)                  = true
+     * ObjectUtils.equals(null, "")                    = false
+     * ObjectUtils.equals("", null)                    = false
+     * ObjectUtils.equals("", "")                      = true
+     * ObjectUtils.equals(Boolean.TRUE, null)          = false
+     * ObjectUtils.equals(Boolean.TRUE, "true")        = false
+     * ObjectUtils.equals(Boolean.TRUE, Boolean.TRUE)  = true
+     * ObjectUtils.equals(Boolean.TRUE, Boolean.FALSE) = false
+     * 
+ * + * @param object1 the first object, may be {@code null} + * @param object2 the second object, may be {@code null} + * @return {@code true} if the values of both objects are the same + */ + public static boolean equals(Object object1, Object object2) { + if (object1 == object2) { + return true; + } + if (object1 == null || object2 == null) { + return false; + } + return object1.equals(object2); + } + + /** + *

Compares two objects for inequality, where either one or both + * objects may be {@code null}.

+ * + *
+     * ObjectUtils.notEqual(null, null)                  = false
+     * ObjectUtils.notEqual(null, "")                    = true
+     * ObjectUtils.notEqual("", null)                    = true
+     * ObjectUtils.notEqual("", "")                      = false
+     * ObjectUtils.notEqual(Boolean.TRUE, null)          = true
+     * ObjectUtils.notEqual(Boolean.TRUE, "true")        = true
+     * ObjectUtils.notEqual(Boolean.TRUE, Boolean.TRUE)  = false
+     * ObjectUtils.notEqual(Boolean.TRUE, Boolean.FALSE) = true
+     * 
+ * + * @param object1 the first object, may be {@code null} + * @param object2 the second object, may be {@code null} + * @return {@code false} if the values of both objects are the same + */ + public static boolean notEqual(Object object1, Object object2) { + return ObjectUtils.equals(object1, object2) == false; + } + + /** + *

Gets the hash code of an object returning zero when the + * object is {@code null}.

+ * + *
+     * ObjectUtils.hashCode(null)   = 0
+     * ObjectUtils.hashCode(obj)    = obj.hashCode()
+     * 
+ * + * @param obj the object to obtain the hash code of, may be {@code null} + * @return the hash code of the object, or zero if null + * @since 2.1 + */ + public static int hashCode(Object obj) { + // hashCode(Object) retained for performance, as hash code is often critical + return obj == null ? 0 : obj.hashCode(); + } + + /** + *

Gets the hash code for multiple objects.

+ * + *

This allows a hash code to be rapidly calculated for a number of objects. + * The hash code for a single object is the not same as {@link #hashCode(Object)}. + * The hash code for multiple objects is the same as that calculated by an + * {@code ArrayList} containing the specified objects.

+ * + *
+     * ObjectUtils.hashCodeMulti()                 = 1
+     * ObjectUtils.hashCodeMulti((Object[]) null)  = 1
+     * ObjectUtils.hashCodeMulti(a)                = 31 + a.hashCode()
+     * ObjectUtils.hashCodeMulti(a,b)              = (31 + a.hashCode()) * 31 + b.hashCode()
+     * ObjectUtils.hashCodeMulti(a,b,c)            = ((31 + a.hashCode()) * 31 + b.hashCode()) * 31 + c.hashCode()
+     * 
+ * + * @param objects the objects to obtain the hash code of, may be {@code null} + * @return the hash code of the objects, or zero if null + * @since 3.0 + */ + public static int hashCodeMulti(Object... objects) { + int hash = 1; + if (objects != null) { + for (Object object : objects) { + hash = hash * 31 + ObjectUtils.hashCode(object); + } + } + return hash; + } + + // Identity ToString + //----------------------------------------------------------------------- + /** + *

Gets the toString that would be produced by {@code Object} + * if a class did not override toString itself. {@code null} + * will return {@code null}.

+ * + *
+     * ObjectUtils.identityToString(null)         = null
+     * ObjectUtils.identityToString("")           = "java.lang.String@1e23"
+     * ObjectUtils.identityToString(Boolean.TRUE) = "java.lang.Boolean@7fa"
+     * 
+ * + * @param object the object to create a toString for, may be + * {@code null} + * @return the default toString text, or {@code null} if + * {@code null} passed in + */ + public static String identityToString(Object object) { + if (object == null) { + return null; + } + StringBuffer buffer = new StringBuffer(); + identityToString(buffer, object); + return buffer.toString(); + } + + /** + *

Appends the toString that would be produced by {@code Object} + * if a class did not override toString itself. {@code null} + * will throw a NullPointerException for either of the two parameters.

+ * + *
+     * ObjectUtils.identityToString(buf, "")            = buf.append("java.lang.String@1e23"
+     * ObjectUtils.identityToString(buf, Boolean.TRUE)  = buf.append("java.lang.Boolean@7fa"
+     * ObjectUtils.identityToString(buf, Boolean.TRUE)  = buf.append("java.lang.Boolean@7fa")
+     * 
+ * + * @param buffer the buffer to append to + * @param object the object to create a toString for + * @since 2.4 + */ + public static void identityToString(StringBuffer buffer, Object object) { + if (object == null) { + throw new NullPointerException("Cannot get the toString of a null identity"); + } + buffer.append(object.getClass().getName()) + .append('@') + .append(Integer.toHexString(System.identityHashCode(object))); + } + + // ToString + //----------------------------------------------------------------------- + /** + *

Gets the {@code toString} of an {@code Object} returning + * an empty string ("") if {@code null} input.

+ * + *
+     * ObjectUtils.toString(null)         = ""
+     * ObjectUtils.toString("")           = ""
+     * ObjectUtils.toString("bat")        = "bat"
+     * ObjectUtils.toString(Boolean.TRUE) = "true"
+     * 
+ * + * @see StringUtils#defaultString(String) + * @see String#valueOf(Object) + * @param obj the Object to {@code toString}, may be null + * @return the passed in Object's toString, or nullStr if {@code null} input + * @since 2.0 + */ + public static String toString(Object obj) { + return obj == null ? "" : obj.toString(); + } + + /** + *

Gets the {@code toString} of an {@code Object} returning + * a specified text if {@code null} input.

+ * + *
+     * ObjectUtils.toString(null, null)           = null
+     * ObjectUtils.toString(null, "null")         = "null"
+     * ObjectUtils.toString("", "null")           = ""
+     * ObjectUtils.toString("bat", "null")        = "bat"
+     * ObjectUtils.toString(Boolean.TRUE, "null") = "true"
+     * 
+ * + * @see StringUtils#defaultString(String,String) + * @see String#valueOf(Object) + * @param obj the Object to {@code toString}, may be null + * @param nullStr the String to return if {@code null} input, may be null + * @return the passed in Object's toString, or nullStr if {@code null} input + * @since 2.0 + */ + public static String toString(Object obj, String nullStr) { + return obj == null ? nullStr : obj.toString(); + } + + // Comparable + //----------------------------------------------------------------------- + /** + *

Null safe comparison of Comparables.

+ * + * @param type of the values processed by this method + * @param values the set of comparable values, may be null + * @return + *
    + *
  • If any objects are non-null and unequal, the lesser object. + *
  • If all objects are non-null and equal, the first. + *
  • If any of the comparables are null, the lesser of the non-null objects. + *
  • If all the comparables are null, null is returned. + *
+ */ + public static > T min(T... values) { + T result = null; + if (values != null) { + for (T value : values) { + if (compare(value, result, true) < 0) { + result = value; + } + } + } + return result; + } + + /** + *

Null safe comparison of Comparables.

+ * + * @param type of the values processed by this method + * @param values the set of comparable values, may be null + * @return + *
    + *
  • If any objects are non-null and unequal, the greater object. + *
  • If all objects are non-null and equal, the first. + *
  • If any of the comparables are null, the greater of the non-null objects. + *
  • If all the comparables are null, null is returned. + *
+ */ + public static > T max(T... values) { + T result = null; + if (values != null) { + for (T value : values) { + if (compare(value, result, false) > 0) { + result = value; + } + } + } + return result; + } + + /** + *

Null safe comparison of Comparables. + * {@code null} is assumed to be less than a non-{@code null} value.

+ * + * @param type of the values processed by this method + * @param c1 the first comparable, may be null + * @param c2 the second comparable, may be null + * @return a negative value if c1 < c2, zero if c1 = c2 + * and a positive value if c1 > c2 + */ + public static > int compare(T c1, T c2) { + return compare(c1, c2, false); + } + + /** + *

Null safe comparison of Comparables.

+ * + * @param type of the values processed by this method + * @param c1 the first comparable, may be null + * @param c2 the second comparable, may be null + * @param nullGreater if true {@code null} is considered greater + * than a non-{@code null} value or if false {@code null} is + * considered less than a Non-{@code null} value + * @return a negative value if c1 < c2, zero if c1 = c2 + * and a positive value if c1 > c2 + * @see java.util.Comparator#compare(Object, Object) + */ + public static > int compare(T c1, T c2, boolean nullGreater) { + if (c1 == c2) { + return 0; + } else if (c1 == null) { + return nullGreater ? 1 : -1; + } else if (c2 == null) { + return nullGreater ? -1 : 1; + } + return c1.compareTo(c2); + } + + /** + * Find the "best guess" middle value among comparables. If there is an even + * number of total values, the lower of the two middle values will be returned. + * @param type of values processed by this method + * @param items to compare + * @return T at middle position + * @throws NullPointerException if items is {@code null} + * @throws IllegalArgumentException if items is empty or contains {@code null} values + * @since 3.0.1 + */ + public static > T median(T... items) { + Validate.notEmpty(items); + Validate.noNullElements(items); + TreeSet sort = new TreeSet(); + Collections.addAll(sort, items); + @SuppressWarnings("unchecked") //we know all items added were T instances + T result = (T) sort.toArray()[(sort.size() - 1) / 2]; + return result; + } + + /** + * Find the "best guess" middle value among comparables. If there is an even + * number of total values, the lower of the two middle values will be returned. + * @param type of values processed by this method + * @param comparator to use for comparisons + * @param items to compare + * @return T at middle position + * @throws NullPointerException if items or comparator is {@code null} + * @throws IllegalArgumentException if items is empty or contains {@code null} values + * @since 3.0.1 + */ + public static T median(Comparator comparator, T... items) { + Validate.notEmpty(items, "null/empty items"); + Validate.noNullElements(items); + Validate.notNull(comparator, "null comparator"); + TreeSet sort = new TreeSet(comparator); + Collections.addAll(sort, items); + @SuppressWarnings("unchecked") //we know all items added were T instances + T result = (T) sort.toArray()[(sort.size() - 1) / 2]; + return result; + } + + // Mode + //----------------------------------------------------------------------- + /** + * Find the most frequently occurring item. + * + * @param type of values processed by this method + * @param items to check + * @return most populous T, {@code null} if non-unique or no items supplied + * @since 3.0.1 + */ + public static T mode(T... items) { + if (ArrayUtils.isNotEmpty(items)) { + HashMap occurrences = new HashMap(items.length); + for (T t : items) { + MutableInt count = occurrences.get(t); + if (count == null) { + occurrences.put(t, new MutableInt(1)); + } else { + count.increment(); + } + } + T result = null; + int max = 0; + for (Map.Entry e : occurrences.entrySet()) { + int cmp = e.getValue().intValue(); + if (cmp == max) { + result = null; + } else if (cmp > max) { + max = cmp; + result = e.getKey(); + } + } + return result; + } + return null; + } + + // cloning + //----------------------------------------------------------------------- + /** + *

Clone an object.

+ * + * @param the type of the object + * @param obj the object to clone, null returns null + * @return the clone if the object implements {@link Cloneable} otherwise {@code null} + * @throws CloneFailedException if the object is cloneable and the clone operation fails + * @since 3.0 + */ + public static T clone(final T obj) { + if (obj instanceof Cloneable) { + final Object result; + if (obj.getClass().isArray()) { + final Class componentType = obj.getClass().getComponentType(); + if (!componentType.isPrimitive()) { + result = ((Object[]) obj).clone(); + } else { + int length = Array.getLength(obj); + result = Array.newInstance(componentType, length); + while (length-- > 0) { + Array.set(result, length, Array.get(obj, length)); + } + } + } else { + try { + final Method clone = obj.getClass().getMethod("clone"); + result = clone.invoke(obj); + } catch (final NoSuchMethodException e) { + throw new CloneFailedException("Cloneable type " + + obj.getClass().getName() + + " has no clone method", e); + } catch (final IllegalAccessException e) { + throw new CloneFailedException("Cannot clone Cloneable type " + + obj.getClass().getName(), e); + } catch (final InvocationTargetException e) { + throw new CloneFailedException("Exception cloning Cloneable type " + + obj.getClass().getName(), e.getCause()); + } + } + @SuppressWarnings("unchecked") + final T checked = (T) result; + return checked; + } + + return null; + } + + /** + *

Clone an object if possible.

+ * + *

This method is similar to {@link #clone(Object)}, but will return the provided + * instance as the return value instead of {@code null} if the instance + * is not cloneable. This is more convenient if the caller uses different + * implementations (e.g. of a service) and some of the implementations do not allow concurrent + * processing or have state. In such cases the implementation can simply provide a proper + * clone implementation and the caller's code does not have to change.

+ * + * @param the type of the object + * @param obj the object to clone, null returns null + * @return the clone if the object implements {@link Cloneable} otherwise the object itself + * @throws CloneFailedException if the object is cloneable and the clone operation fails + * @since 3.0 + */ + public static T cloneIfPossible(final T obj) { + final T clone = clone(obj); + return clone == null ? obj : clone; + } + + // Null + //----------------------------------------------------------------------- + /** + *

Class used as a null placeholder where {@code null} + * has another meaning.

+ * + *

For example, in a {@code HashMap} the + * {@link java.util.HashMap#get(java.lang.Object)} method returns + * {@code null} if the {@code Map} contains {@code null} or if there is + * no matching key. The {@code Null} placeholder can be used to distinguish + * between these two cases.

+ * + *

Another example is {@code Hashtable}, where {@code null} + * cannot be stored.

+ */ + public static class Null implements Serializable { + /** + * Required for serialization support. Declare serialization compatibility with Commons Lang 1.0 + * + * @see java.io.Serializable + */ + private static final long serialVersionUID = 7092611880189329093L; + + /** + * Restricted constructor - singleton. + */ + Null() { + super(); + } + + /** + *

Ensure singleton.

+ * + * @return the singleton value + */ + private Object readResolve() { + return ObjectUtils.NULL; + } + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/StringUtils.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/StringUtils.java new file mode 100644 index 00000000..68032566 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/StringUtils.java @@ -0,0 +1,6582 @@ +/* + * 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 external.org.apache.commons.lang3; + +import java.io.UnsupportedEncodingException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +/** + *

Operations on {@link java.lang.String} that are + * {@code null} safe.

+ * + *
    + *
  • IsEmpty/IsBlank + * - checks if a String contains text
  • + *
  • Trim/Strip + * - removes leading and trailing whitespace
  • + *
  • Equals + * - compares two strings null-safe
  • + *
  • startsWith + * - check if a String starts with a prefix null-safe
  • + *
  • endsWith + * - check if a String ends with a suffix null-safe
  • + *
  • IndexOf/LastIndexOf/Contains + * - null-safe index-of checks + *
  • IndexOfAny/LastIndexOfAny/IndexOfAnyBut/LastIndexOfAnyBut + * - index-of any of a set of Strings
  • + *
  • ContainsOnly/ContainsNone/ContainsAny + * - does String contains only/none/any of these characters
  • + *
  • Substring/Left/Right/Mid + * - null-safe substring extractions
  • + *
  • SubstringBefore/SubstringAfter/SubstringBetween + * - substring extraction relative to other strings
  • + *
  • Split/Join + * - splits a String into an array of substrings and vice versa
  • + *
  • Remove/Delete + * - removes part of a String
  • + *
  • Replace/Overlay + * - Searches a String and replaces one String with another
  • + *
  • Chomp/Chop + * - removes the last part of a String
  • + *
  • LeftPad/RightPad/Center/Repeat + * - pads a String
  • + *
  • UpperCase/LowerCase/SwapCase/Capitalize/Uncapitalize + * - changes the case of a String
  • + *
  • CountMatches + * - counts the number of occurrences of one String in another
  • + *
  • IsAlpha/IsNumeric/IsWhitespace/IsAsciiPrintable + * - checks the characters in a String
  • + *
  • DefaultString + * - protects against a null input String
  • + *
  • Reverse/ReverseDelimited + * - reverses a String
  • + *
  • Abbreviate + * - abbreviates a string using ellipsis
  • + *
  • Difference + * - compares Strings and reports on their differences
  • + *
  • LevenshteinDistance + * - the number of changes needed to change one String into another
  • + *
+ * + *

The {@code StringUtils} class defines certain words related to + * String handling.

+ * + *
    + *
  • null - {@code null}
  • + *
  • empty - a zero-length string ({@code ""})
  • + *
  • space - the space character ({@code ' '}, char 32)
  • + *
  • whitespace - the characters defined by {@link Character#isWhitespace(char)}
  • + *
  • trim - the characters <= 32 as in {@link String#trim()}
  • + *
+ * + *

{@code StringUtils} handles {@code null} input Strings quietly. + * That is to say that a {@code null} input will return {@code null}. + * Where a {@code boolean} or {@code int} is being returned + * details vary by method.

+ * + *

A side effect of the {@code null} handling is that a + * {@code NullPointerException} should be considered a bug in + * {@code StringUtils}.

+ * + *

Methods in this class give sample code to explain their operation. + * The symbol {@code *} is used to indicate any input including {@code null}.

+ * + *

#ThreadSafe#

+ * @see java.lang.String + * @since 1.0 + * @version $Id: StringUtils.java 1199894 2011-11-09 17:53:59Z ggregory $ + */ +//@Immutable +public class StringUtils { + // Performance testing notes (JDK 1.4, Jul03, scolebourne) + // Whitespace: + // Character.isWhitespace() is faster than WHITESPACE.indexOf() + // where WHITESPACE is a string of all whitespace characters + // + // Character access: + // String.charAt(n) versus toCharArray(), then array[n] + // String.charAt(n) is about 15% worse for a 10K string + // They are about equal for a length 50 string + // String.charAt(n) is about 4 times better for a length 3 string + // String.charAt(n) is best bet overall + // + // Append: + // String.concat about twice as fast as StringBuffer.append + // (not sure who tested this) + + /** + * The empty String {@code ""}. + * @since 2.0 + */ + public static final String EMPTY = ""; + + /** + * Represents a failed index search. + * @since 2.1 + */ + public static final int INDEX_NOT_FOUND = -1; + + /** + *

The maximum size to which the padding constant(s) can expand.

+ */ + private static final int PAD_LIMIT = 8192; + + /** + * A regex pattern for recognizing blocks of whitespace characters. + */ + private static final Pattern WHITESPACE_BLOCK = Pattern.compile("\\s+"); + + /** + *

{@code StringUtils} instances should NOT be constructed in + * standard programming. Instead, the class should be used as + * {@code StringUtils.trim(" foo ");}.

+ * + *

This constructor is public to permit tools that require a JavaBean + * instance to operate.

+ */ + public StringUtils() { + super(); + } + + // Empty checks + //----------------------------------------------------------------------- + /** + *

Checks if a CharSequence is empty ("") or null.

+ * + *
+     * StringUtils.isEmpty(null)      = true
+     * StringUtils.isEmpty("")        = true
+     * StringUtils.isEmpty(" ")       = false
+     * StringUtils.isEmpty("bob")     = false
+     * StringUtils.isEmpty("  bob  ") = false
+     * 
+ * + *

NOTE: This method changed in Lang version 2.0. + * It no longer trims the CharSequence. + * That functionality is available in isBlank().

+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is empty or null + * @since 3.0 Changed signature from isEmpty(String) to isEmpty(CharSequence) + */ + public static boolean isEmpty(CharSequence cs) { + return cs == null || cs.length() == 0; + } + + /** + *

Checks if a CharSequence is not empty ("") and not null.

+ * + *
+     * StringUtils.isNotEmpty(null)      = false
+     * StringUtils.isNotEmpty("")        = false
+     * StringUtils.isNotEmpty(" ")       = true
+     * StringUtils.isNotEmpty("bob")     = true
+     * StringUtils.isNotEmpty("  bob  ") = true
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is not empty and not null + * @since 3.0 Changed signature from isNotEmpty(String) to isNotEmpty(CharSequence) + */ + public static boolean isNotEmpty(CharSequence cs) { + return !StringUtils.isEmpty(cs); + } + + /** + *

Checks if a CharSequence is whitespace, empty ("") or null.

+ * + *
+     * StringUtils.isBlank(null)      = true
+     * StringUtils.isBlank("")        = true
+     * StringUtils.isBlank(" ")       = true
+     * StringUtils.isBlank("bob")     = false
+     * StringUtils.isBlank("  bob  ") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is null, empty or whitespace + * @since 2.0 + * @since 3.0 Changed signature from isBlank(String) to isBlank(CharSequence) + */ + public static boolean isBlank(CharSequence cs) { + int strLen; + if (cs == null || (strLen = cs.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if (Character.isWhitespace(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if a CharSequence is not empty (""), not null and not whitespace only.

+ * + *
+     * StringUtils.isNotBlank(null)      = false
+     * StringUtils.isNotBlank("")        = false
+     * StringUtils.isNotBlank(" ")       = false
+     * StringUtils.isNotBlank("bob")     = true
+     * StringUtils.isNotBlank("  bob  ") = true
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is + * not empty and not null and not whitespace + * @since 2.0 + * @since 3.0 Changed signature from isNotBlank(String) to isNotBlank(CharSequence) + */ + public static boolean isNotBlank(CharSequence cs) { + return !StringUtils.isBlank(cs); + } + + // Trim + //----------------------------------------------------------------------- + /** + *

Removes control characters (char <= 32) from both + * ends of this String, handling {@code null} by returning + * {@code null}.

+ * + *

The String is trimmed using {@link String#trim()}. + * Trim removes start and end characters <= 32. + * To strip whitespace use {@link #strip(String)}.

+ * + *

To trim your choice of characters, use the + * {@link #strip(String, String)} methods.

+ * + *
+     * StringUtils.trim(null)          = null
+     * StringUtils.trim("")            = ""
+     * StringUtils.trim("     ")       = ""
+     * StringUtils.trim("abc")         = "abc"
+     * StringUtils.trim("    abc    ") = "abc"
+     * 
+ * + * @param str the String to be trimmed, may be null + * @return the trimmed string, {@code null} if null String input + */ + public static String trim(String str) { + return str == null ? null : str.trim(); + } + + /** + *

Removes control characters (char <= 32) from both + * ends of this String returning {@code null} if the String is + * empty ("") after the trim or if it is {@code null}. + * + *

The String is trimmed using {@link String#trim()}. + * Trim removes start and end characters <= 32. + * To strip whitespace use {@link #stripToNull(String)}.

+ * + *
+     * StringUtils.trimToNull(null)          = null
+     * StringUtils.trimToNull("")            = null
+     * StringUtils.trimToNull("     ")       = null
+     * StringUtils.trimToNull("abc")         = "abc"
+     * StringUtils.trimToNull("    abc    ") = "abc"
+     * 
+ * + * @param str the String to be trimmed, may be null + * @return the trimmed String, + * {@code null} if only chars <= 32, empty or null String input + * @since 2.0 + */ + public static String trimToNull(String str) { + String ts = trim(str); + return isEmpty(ts) ? null : ts; + } + + /** + *

Removes control characters (char <= 32) from both + * ends of this String returning an empty String ("") if the String + * is empty ("") after the trim or if it is {@code null}. + * + *

The String is trimmed using {@link String#trim()}. + * Trim removes start and end characters <= 32. + * To strip whitespace use {@link #stripToEmpty(String)}.

+ * + *
+     * StringUtils.trimToEmpty(null)          = ""
+     * StringUtils.trimToEmpty("")            = ""
+     * StringUtils.trimToEmpty("     ")       = ""
+     * StringUtils.trimToEmpty("abc")         = "abc"
+     * StringUtils.trimToEmpty("    abc    ") = "abc"
+     * 
+ * + * @param str the String to be trimmed, may be null + * @return the trimmed String, or an empty String if {@code null} input + * @since 2.0 + */ + public static String trimToEmpty(String str) { + return str == null ? EMPTY : str.trim(); + } + + // Stripping + //----------------------------------------------------------------------- + /** + *

Strips whitespace from the start and end of a String.

+ * + *

This is similar to {@link #trim(String)} but removes whitespace. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *

A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.strip(null)     = null
+     * StringUtils.strip("")       = ""
+     * StringUtils.strip("   ")    = ""
+     * StringUtils.strip("abc")    = "abc"
+     * StringUtils.strip("  abc")  = "abc"
+     * StringUtils.strip("abc  ")  = "abc"
+     * StringUtils.strip(" abc ")  = "abc"
+     * StringUtils.strip(" ab c ") = "ab c"
+     * 
+ * + * @param str the String to remove whitespace from, may be null + * @return the stripped String, {@code null} if null String input + */ + public static String strip(String str) { + return strip(str, null); + } + + /** + *

Strips whitespace from the start and end of a String returning + * {@code null} if the String is empty ("") after the strip.

+ * + *

This is similar to {@link #trimToNull(String)} but removes whitespace. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripToNull(null)     = null
+     * StringUtils.stripToNull("")       = null
+     * StringUtils.stripToNull("   ")    = null
+     * StringUtils.stripToNull("abc")    = "abc"
+     * StringUtils.stripToNull("  abc")  = "abc"
+     * StringUtils.stripToNull("abc  ")  = "abc"
+     * StringUtils.stripToNull(" abc ")  = "abc"
+     * StringUtils.stripToNull(" ab c ") = "ab c"
+     * 
+ * + * @param str the String to be stripped, may be null + * @return the stripped String, + * {@code null} if whitespace, empty or null String input + * @since 2.0 + */ + public static String stripToNull(String str) { + if (str == null) { + return null; + } + str = strip(str, null); + return str.length() == 0 ? null : str; + } + + /** + *

Strips whitespace from the start and end of a String returning + * an empty String if {@code null} input.

+ * + *

This is similar to {@link #trimToEmpty(String)} but removes whitespace. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripToEmpty(null)     = ""
+     * StringUtils.stripToEmpty("")       = ""
+     * StringUtils.stripToEmpty("   ")    = ""
+     * StringUtils.stripToEmpty("abc")    = "abc"
+     * StringUtils.stripToEmpty("  abc")  = "abc"
+     * StringUtils.stripToEmpty("abc  ")  = "abc"
+     * StringUtils.stripToEmpty(" abc ")  = "abc"
+     * StringUtils.stripToEmpty(" ab c ") = "ab c"
+     * 
+ * + * @param str the String to be stripped, may be null + * @return the trimmed String, or an empty String if {@code null} input + * @since 2.0 + */ + public static String stripToEmpty(String str) { + return str == null ? EMPTY : strip(str, null); + } + + /** + *

Strips any of a set of characters from the start and end of a String. + * This is similar to {@link String#trim()} but allows the characters + * to be stripped to be controlled.

+ * + *

A {@code null} input String returns {@code null}. + * An empty string ("") input returns the empty string.

+ * + *

If the stripChars String is {@code null}, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}. + * Alternatively use {@link #strip(String)}.

+ * + *
+     * StringUtils.strip(null, *)          = null
+     * StringUtils.strip("", *)            = ""
+     * StringUtils.strip("abc", null)      = "abc"
+     * StringUtils.strip("  abc", null)    = "abc"
+     * StringUtils.strip("abc  ", null)    = "abc"
+     * StringUtils.strip(" abc ", null)    = "abc"
+     * StringUtils.strip("  abcyx", "xyz") = "  abc"
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the characters to remove, null treated as whitespace + * @return the stripped String, {@code null} if null String input + */ + public static String strip(String str, String stripChars) { + if (isEmpty(str)) { + return str; + } + str = stripStart(str, stripChars); + return stripEnd(str, stripChars); + } + + /** + *

Strips any of a set of characters from the start of a String.

+ * + *

A {@code null} input String returns {@code null}. + * An empty string ("") input returns the empty string.

+ * + *

If the stripChars String is {@code null}, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripStart(null, *)          = null
+     * StringUtils.stripStart("", *)            = ""
+     * StringUtils.stripStart("abc", "")        = "abc"
+     * StringUtils.stripStart("abc", null)      = "abc"
+     * StringUtils.stripStart("  abc", null)    = "abc"
+     * StringUtils.stripStart("abc  ", null)    = "abc  "
+     * StringUtils.stripStart(" abc ", null)    = "abc "
+     * StringUtils.stripStart("yxabc  ", "xyz") = "abc  "
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the characters to remove, null treated as whitespace + * @return the stripped String, {@code null} if null String input + */ + public static String stripStart(String str, String stripChars) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return str; + } + int start = 0; + if (stripChars == null) { + while (start != strLen && Character.isWhitespace(str.charAt(start))) { + start++; + } + } else if (stripChars.length() == 0) { + return str; + } else { + while (start != strLen && stripChars.indexOf(str.charAt(start)) != INDEX_NOT_FOUND) { + start++; + } + } + return str.substring(start); + } + + /** + *

Strips any of a set of characters from the end of a String.

+ * + *

A {@code null} input String returns {@code null}. + * An empty string ("") input returns the empty string.

+ * + *

If the stripChars String is {@code null}, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripEnd(null, *)          = null
+     * StringUtils.stripEnd("", *)            = ""
+     * StringUtils.stripEnd("abc", "")        = "abc"
+     * StringUtils.stripEnd("abc", null)      = "abc"
+     * StringUtils.stripEnd("  abc", null)    = "  abc"
+     * StringUtils.stripEnd("abc  ", null)    = "abc"
+     * StringUtils.stripEnd(" abc ", null)    = " abc"
+     * StringUtils.stripEnd("  abcyx", "xyz") = "  abc"
+     * StringUtils.stripEnd("120.00", ".0")   = "12"
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the set of characters to remove, null treated as whitespace + * @return the stripped String, {@code null} if null String input + */ + public static String stripEnd(String str, String stripChars) { + int end; + if (str == null || (end = str.length()) == 0) { + return str; + } + + if (stripChars == null) { + while (end != 0 && Character.isWhitespace(str.charAt(end - 1))) { + end--; + } + } else if (stripChars.length() == 0) { + return str; + } else { + while (end != 0 && stripChars.indexOf(str.charAt(end - 1)) != INDEX_NOT_FOUND) { + end--; + } + } + return str.substring(0, end); + } + + // StripAll + //----------------------------------------------------------------------- + /** + *

Strips whitespace from the start and end of every String in an array. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *

A new array is returned each time, except for length zero. + * A {@code null} array will return {@code null}. + * An empty array will return itself. + * A {@code null} array entry will be ignored.

+ * + *
+     * StringUtils.stripAll(null)             = null
+     * StringUtils.stripAll([])               = []
+     * StringUtils.stripAll(["abc", "  abc"]) = ["abc", "abc"]
+     * StringUtils.stripAll(["abc  ", null])  = ["abc", null]
+     * 
+ * + * @param strs the array to remove whitespace from, may be null + * @return the stripped Strings, {@code null} if null array input + */ + public static String[] stripAll(String... strs) { + return stripAll(strs, null); + } + + /** + *

Strips any of a set of characters from the start and end of every + * String in an array.

+ * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *

A new array is returned each time, except for length zero. + * A {@code null} array will return {@code null}. + * An empty array will return itself. + * A {@code null} array entry will be ignored. + * A {@code null} stripChars will strip whitespace as defined by + * {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripAll(null, *)                = null
+     * StringUtils.stripAll([], *)                  = []
+     * StringUtils.stripAll(["abc", "  abc"], null) = ["abc", "abc"]
+     * StringUtils.stripAll(["abc  ", null], null)  = ["abc", null]
+     * StringUtils.stripAll(["abc  ", null], "yz")  = ["abc  ", null]
+     * StringUtils.stripAll(["yabcz", null], "yz")  = ["abc", null]
+     * 
+ * + * @param strs the array to remove characters from, may be null + * @param stripChars the characters to remove, null treated as whitespace + * @return the stripped Strings, {@code null} if null array input + */ + public static String[] stripAll(String[] strs, String stripChars) { + int strsLen; + if (strs == null || (strsLen = strs.length) == 0) { + return strs; + } + String[] newArr = new String[strsLen]; + for (int i = 0; i < strsLen; i++) { + newArr[i] = strip(strs[i], stripChars); + } + return newArr; + } + + /** + *

Removes diacritics (~= accents) from a string. The case will not be altered.

+ *

For instance, 'à' will be replaced by 'a'.

+ *

Note that ligatures will be left as is.

+ * + *

This method will use the first available implementation of: + * Java 6's {@link java.text.Normalizer}, Java 1.3–1.5's {@code sun.text.Normalizer}

+ * + *
+     * StringUtils.stripAccents(null)                = null
+     * StringUtils.stripAccents("")                  = ""
+     * StringUtils.stripAccents("control")           = "control"
+     * StringUtils.stripAccents("éclair")     = "eclair"
+     * 
+ * + * @param input String to be stripped + * @return input text with diacritics removed + * + * @since 3.0 + */ + // See also Lucene's ASCIIFoldingFilter (Lucene 2.9) that replaces accented characters by their unaccented equivalent (and uncommitted bug fix: https://issues.apache.org/jira/browse/LUCENE-1343?focusedCommentId=12858907&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#action_12858907). + public static String stripAccents(String input) { + if(input == null) { + return null; + } + try { + String result = null; + if (InitStripAccents.java6NormalizeMethod != null) { + result = removeAccentsJava6(input); + } else if (InitStripAccents.sunDecomposeMethod != null) { + result = removeAccentsSUN(input); + } else { + throw new UnsupportedOperationException( + "The stripAccents(CharSequence) method requires at least" + +" Java6, but got: "+InitStripAccents.java6Exception + +"; or a Sun JVM: "+InitStripAccents.sunException); + } + // Note that none of the above methods correctly remove ligatures... + return result; + } catch(IllegalArgumentException iae) { + throw new RuntimeException("IllegalArgumentException occurred", iae); + } catch(IllegalAccessException iae) { + throw new RuntimeException("IllegalAccessException occurred", iae); + } catch(InvocationTargetException ite) { + throw new RuntimeException("InvocationTargetException occurred", ite); + } catch(SecurityException se) { + throw new RuntimeException("SecurityException occurred", se); + } + } + + /** + * Use {@code java.text.Normalizer#normalize(CharSequence, Normalizer.Form)} + * (but be careful, this class exists in Java 1.3, with an entirely different meaning!) + * + * @param text the text to be processed + * @return the processed string + * @throws IllegalAccessException may be thrown by a reflection call + * @throws InvocationTargetException if a reflection call throws an exception + * @throws IllegalStateException if the {@code Normalizer} class is not available + */ + private static String removeAccentsJava6(CharSequence text) + throws IllegalAccessException, InvocationTargetException { + /* + String decomposed = java.text.Normalizer.normalize(CharSequence, Normalizer.Form.NFD); + return java6Pattern.matcher(decomposed).replaceAll("");//$NON-NLS-1$ + */ + if (InitStripAccents.java6NormalizeMethod == null || InitStripAccents.java6NormalizerFormNFD == null) { + throw new IllegalStateException("java.text.Normalizer is not available", InitStripAccents.java6Exception); + } + String result; + result = (String) InitStripAccents.java6NormalizeMethod.invoke(null, new Object[] {text, InitStripAccents.java6NormalizerFormNFD}); + result = InitStripAccents.java6Pattern.matcher(result).replaceAll("");//$NON-NLS-1$ + return result; + } + + /** + * Use {@code sun.text.Normalizer#decompose(String, boolean, int)} + * + * @param text the text to be processed + * @return the processed string + * @throws IllegalAccessException may be thrown by a reflection call + * @throws InvocationTargetException if a reflection call throws an exception + * @throws IllegalStateException if the {@code Normalizer} class is not available + */ + private static String removeAccentsSUN(CharSequence text) + throws IllegalAccessException, InvocationTargetException { + /* + String decomposed = sun.text.Normalizer.decompose(text, false, 0); + return sunPattern.matcher(decomposed).replaceAll("");//$NON-NLS-1$ + */ + if (InitStripAccents.sunDecomposeMethod == null) { + throw new IllegalStateException("sun.text.Normalizer is not available", InitStripAccents.sunException); + } + String result; + result = (String) InitStripAccents.sunDecomposeMethod.invoke(null, new Object[] {text, Boolean.FALSE, Integer.valueOf(0)}); + result = InitStripAccents.sunPattern.matcher(result).replaceAll("");//$NON-NLS-1$ + return result; + } + + // IOD container for stripAccent() initialisation + private static class InitStripAccents { + // SUN internal, Java 1.3 -> Java 5 + private static final Throwable sunException; + private static final Method sunDecomposeMethod; + private static final Pattern sunPattern = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");//$NON-NLS-1$ + // Java 6+ + private static final Throwable java6Exception; + private static final Method java6NormalizeMethod; + private static final Object java6NormalizerFormNFD; + private static final Pattern java6Pattern = sunPattern; + + static { + // Set up defaults for final static fields + Object _java6NormalizerFormNFD = null; + Method _java6NormalizeMethod = null; + Method _sunDecomposeMethod = null; + Throwable _java6Exception = null; + Throwable _sunException = null; + try { + // java.text.Normalizer.normalize(CharSequence, Normalizer.Form.NFD); + // Be careful not to get Java 1.3 java.text.Normalizer! + Class normalizerFormClass = Thread.currentThread().getContextClassLoader() + .loadClass("java.text.Normalizer$Form");//$NON-NLS-1$ + _java6NormalizerFormNFD = normalizerFormClass.getField("NFD").get(null);//$NON-NLS-1$ + Class normalizerClass = Thread.currentThread().getContextClassLoader() + .loadClass("java.text.Normalizer");//$NON-NLS-1$ + _java6NormalizeMethod = normalizerClass.getMethod("normalize",//$NON-NLS-1$ + new Class[] {CharSequence.class, normalizerFormClass});//$NON-NLS-1$ + } catch (Exception e1) { + // Only check for Sun method if Java 6 method is not available + _java6Exception = e1; + try { + // sun.text.Normalizer.decompose(text, false, 0); + Class normalizerClass = Thread.currentThread().getContextClassLoader() + .loadClass("sun.text.Normalizer");//$NON-NLS-1$ + _sunDecomposeMethod = normalizerClass.getMethod("decompose",//$NON-NLS-1$ + new Class[] {String.class, Boolean.TYPE, Integer.TYPE});//$NON-NLS-1$ + } catch (Exception e2) { + _sunException = e2; + } + } + + // Set up final static fields + java6Exception = _java6Exception; + java6NormalizerFormNFD = _java6NormalizerFormNFD; + java6NormalizeMethod = _java6NormalizeMethod; + sunException = _sunException; + sunDecomposeMethod = _sunDecomposeMethod; + } + } + + // Equals + //----------------------------------------------------------------------- + /** + *

Compares two CharSequences, returning {@code true} if they are equal.

+ * + *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered to be equal. The comparison is case sensitive.

+ * + *
+     * StringUtils.equals(null, null)   = true
+     * StringUtils.equals(null, "abc")  = false
+     * StringUtils.equals("abc", null)  = false
+     * StringUtils.equals("abc", "abc") = true
+     * StringUtils.equals("abc", "ABC") = false
+     * 
+ * + * @see java.lang.String#equals(Object) + * @param cs1 the first CharSequence, may be null + * @param cs2 the second CharSequence, may be null + * @return {@code true} if the CharSequences are equal, case sensitive, or + * both {@code null} + * @since 3.0 Changed signature from equals(String, String) to equals(CharSequence, CharSequence) + */ + public static boolean equals(CharSequence cs1, CharSequence cs2) { + return cs1 == null ? cs2 == null : cs1.equals(cs2); + } + + /** + *

Compares two CharSequences, returning {@code true} if they are equal ignoring + * the case.

+ * + *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered equal. Comparison is case insensitive.

+ * + *
+     * StringUtils.equalsIgnoreCase(null, null)   = true
+     * StringUtils.equalsIgnoreCase(null, "abc")  = false
+     * StringUtils.equalsIgnoreCase("abc", null)  = false
+     * StringUtils.equalsIgnoreCase("abc", "abc") = true
+     * StringUtils.equalsIgnoreCase("abc", "ABC") = true
+     * 
+ * + * @param str1 the first CharSequence, may be null + * @param str2 the second CharSequence, may be null + * @return {@code true} if the CharSequence are equal, case insensitive, or + * both {@code null} + * @since 3.0 Changed signature from equalsIgnoreCase(String, String) to equalsIgnoreCase(CharSequence, CharSequence) + */ + public static boolean equalsIgnoreCase(CharSequence str1, CharSequence str2) { + if (str1 == null || str2 == null) { + return str1 == str2; + } else { + return CharSequenceUtils.regionMatches(str1, true, 0, str2, 0, Math.max(str1.length(), str2.length())); + } + } + + // IndexOf + //----------------------------------------------------------------------- + /** + *

Finds the first index within a CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(int, int)} if possible.

+ * + *

A {@code null} or empty ("") CharSequence will return {@code INDEX_NOT_FOUND (-1)}.

+ * + *
+     * StringUtils.indexOf(null, *)         = -1
+     * StringUtils.indexOf("", *)           = -1
+     * StringUtils.indexOf("aabaabaa", 'a') = 0
+     * StringUtils.indexOf("aabaabaa", 'b') = 2
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChar the character to find + * @return the first index of the search character, + * -1 if no match or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from indexOf(String, int) to indexOf(CharSequence, int) + */ + public static int indexOf(CharSequence seq, int searchChar) { + if (isEmpty(seq)) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.indexOf(seq, searchChar, 0); + } + + /** + *

Finds the first index within a CharSequence from a start position, + * handling {@code null}. + * This method uses {@link String#indexOf(int, int)} if possible.

+ * + *

A {@code null} or empty ("") CharSequence will return {@code (INDEX_NOT_FOUND) -1}. + * A negative start position is treated as zero. + * A start position greater than the string length returns {@code -1}.

+ * + *
+     * StringUtils.indexOf(null, *, *)          = -1
+     * StringUtils.indexOf("", *, *)            = -1
+     * StringUtils.indexOf("aabaabaa", 'b', 0)  = 2
+     * StringUtils.indexOf("aabaabaa", 'b', 3)  = 5
+     * StringUtils.indexOf("aabaabaa", 'b', 9)  = -1
+     * StringUtils.indexOf("aabaabaa", 'b', -1) = 2
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChar the character to find + * @param startPos the start position, negative treated as zero + * @return the first index of the search character, + * -1 if no match or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from indexOf(String, int, int) to indexOf(CharSequence, int, int) + */ + public static int indexOf(CharSequence seq, int searchChar, int startPos) { + if (isEmpty(seq)) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.indexOf(seq, searchChar, startPos); + } + + /** + *

Finds the first index within a CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(String, int)} if possible.

+ * + *

A {@code null} CharSequence will return {@code -1}.

+ * + *
+     * StringUtils.indexOf(null, *)          = -1
+     * StringUtils.indexOf(*, null)          = -1
+     * StringUtils.indexOf("", "")           = 0
+     * StringUtils.indexOf("", *)            = -1 (except when * = "")
+     * StringUtils.indexOf("aabaabaa", "a")  = 0
+     * StringUtils.indexOf("aabaabaa", "b")  = 2
+     * StringUtils.indexOf("aabaabaa", "ab") = 1
+     * StringUtils.indexOf("aabaabaa", "")   = 0
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @return the first index of the search CharSequence, + * -1 if no match or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from indexOf(String, String) to indexOf(CharSequence, CharSequence) + */ + public static int indexOf(CharSequence seq, CharSequence searchSeq) { + if (seq == null || searchSeq == null) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.indexOf(seq, searchSeq, 0); + } + + /** + *

Finds the first index within a CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(String, int)} if possible.

+ * + *

A {@code null} CharSequence will return {@code -1}. + * A negative start position is treated as zero. + * An empty ("") search CharSequence always matches. + * A start position greater than the string length only matches + * an empty search CharSequence.

+ * + *
+     * StringUtils.indexOf(null, *, *)          = -1
+     * StringUtils.indexOf(*, null, *)          = -1
+     * StringUtils.indexOf("", "", 0)           = 0
+     * StringUtils.indexOf("", *, 0)            = -1 (except when * = "")
+     * StringUtils.indexOf("aabaabaa", "a", 0)  = 0
+     * StringUtils.indexOf("aabaabaa", "b", 0)  = 2
+     * StringUtils.indexOf("aabaabaa", "ab", 0) = 1
+     * StringUtils.indexOf("aabaabaa", "b", 3)  = 5
+     * StringUtils.indexOf("aabaabaa", "b", 9)  = -1
+     * StringUtils.indexOf("aabaabaa", "b", -1) = 2
+     * StringUtils.indexOf("aabaabaa", "", 2)   = 2
+     * StringUtils.indexOf("abc", "", 9)        = 3
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @param startPos the start position, negative treated as zero + * @return the first index of the search CharSequence, + * -1 if no match or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from indexOf(String, String, int) to indexOf(CharSequence, CharSequence, int) + */ + public static int indexOf(CharSequence seq, CharSequence searchSeq, int startPos) { + if (seq == null || searchSeq == null) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.indexOf(seq, searchSeq, startPos); + } + + /** + *

Finds the n-th index within a CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(String)} if possible.

+ * + *

A {@code null} CharSequence will return {@code -1}.

+ * + *
+     * StringUtils.ordinalIndexOf(null, *, *)          = -1
+     * StringUtils.ordinalIndexOf(*, null, *)          = -1
+     * StringUtils.ordinalIndexOf("", "", *)           = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "a", 1)  = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "a", 2)  = 1
+     * StringUtils.ordinalIndexOf("aabaabaa", "b", 1)  = 2
+     * StringUtils.ordinalIndexOf("aabaabaa", "b", 2)  = 5
+     * StringUtils.ordinalIndexOf("aabaabaa", "ab", 1) = 1
+     * StringUtils.ordinalIndexOf("aabaabaa", "ab", 2) = 4
+     * StringUtils.ordinalIndexOf("aabaabaa", "", 1)   = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "", 2)   = 0
+     * 
+ * + *

Note that 'head(CharSequence str, int n)' may be implemented as:

+ * + *
+     *   str.substring(0, lastOrdinalIndexOf(str, "\n", n))
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @param ordinal the n-th {@code searchStr} to find + * @return the n-th index of the search CharSequence, + * {@code -1} ({@code INDEX_NOT_FOUND}) if no match or {@code null} string input + * @since 2.1 + * @since 3.0 Changed signature from ordinalIndexOf(String, String, int) to ordinalIndexOf(CharSequence, CharSequence, int) + */ + public static int ordinalIndexOf(CharSequence str, CharSequence searchStr, int ordinal) { + return ordinalIndexOf(str, searchStr, ordinal, false); + } + + /** + *

Finds the n-th index within a String, handling {@code null}. + * This method uses {@link String#indexOf(String)} if possible.

+ * + *

A {@code null} CharSequence will return {@code -1}.

+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @param ordinal the n-th {@code searchStr} to find + * @param lastIndex true if lastOrdinalIndexOf() otherwise false if ordinalIndexOf() + * @return the n-th index of the search CharSequence, + * {@code -1} ({@code INDEX_NOT_FOUND}) if no match or {@code null} string input + */ + // Shared code between ordinalIndexOf(String,String,int) and lastOrdinalIndexOf(String,String,int) + private static int ordinalIndexOf(CharSequence str, CharSequence searchStr, int ordinal, boolean lastIndex) { + if (str == null || searchStr == null || ordinal <= 0) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return lastIndex ? str.length() : 0; + } + int found = 0; + int index = lastIndex ? str.length() : INDEX_NOT_FOUND; + do { + if (lastIndex) { + index = CharSequenceUtils.lastIndexOf(str, searchStr, index - 1); + } else { + index = CharSequenceUtils.indexOf(str, searchStr, index + 1); + } + if (index < 0) { + return index; + } + found++; + } while (found < ordinal); + return index; + } + + /** + *

Case in-sensitive find of the first index within a CharSequence.

+ * + *

A {@code null} CharSequence will return {@code -1}. + * A negative start position is treated as zero. + * An empty ("") search CharSequence always matches. + * A start position greater than the string length only matches + * an empty search CharSequence.

+ * + *
+     * StringUtils.indexOfIgnoreCase(null, *)          = -1
+     * StringUtils.indexOfIgnoreCase(*, null)          = -1
+     * StringUtils.indexOfIgnoreCase("", "")           = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "a")  = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "b")  = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "ab") = 1
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @return the first index of the search CharSequence, + * -1 if no match or {@code null} string input + * @since 2.5 + * @since 3.0 Changed signature from indexOfIgnoreCase(String, String) to indexOfIgnoreCase(CharSequence, CharSequence) + */ + public static int indexOfIgnoreCase(CharSequence str, CharSequence searchStr) { + return indexOfIgnoreCase(str, searchStr, 0); + } + + /** + *

Case in-sensitive find of the first index within a CharSequence + * from the specified position.

+ * + *

A {@code null} CharSequence will return {@code -1}. + * A negative start position is treated as zero. + * An empty ("") search CharSequence always matches. + * A start position greater than the string length only matches + * an empty search CharSequence.

+ * + *
+     * StringUtils.indexOfIgnoreCase(null, *, *)          = -1
+     * StringUtils.indexOfIgnoreCase(*, null, *)          = -1
+     * StringUtils.indexOfIgnoreCase("", "", 0)           = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+     * StringUtils.indexOfIgnoreCase("abc", "", 9)        = 3
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @param startPos the start position, negative treated as zero + * @return the first index of the search CharSequence, + * -1 if no match or {@code null} string input + * @since 2.5 + * @since 3.0 Changed signature from indexOfIgnoreCase(String, String, int) to indexOfIgnoreCase(CharSequence, CharSequence, int) + */ + public static int indexOfIgnoreCase(CharSequence str, CharSequence searchStr, int startPos) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (startPos < 0) { + startPos = 0; + } + int endLimit = str.length() - searchStr.length() + 1; + if (startPos > endLimit) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return startPos; + } + for (int i = startPos; i < endLimit; i++) { + if (CharSequenceUtils.regionMatches(str, true, i, searchStr, 0, searchStr.length())) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + // LastIndexOf + //----------------------------------------------------------------------- + /** + *

Finds the last index within a CharSequence, handling {@code null}. + * This method uses {@link String#lastIndexOf(int)} if possible.

+ * + *

A {@code null} or empty ("") CharSequence will return {@code -1}.

+ * + *
+     * StringUtils.lastIndexOf(null, *)         = -1
+     * StringUtils.lastIndexOf("", *)           = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'a') = 7
+     * StringUtils.lastIndexOf("aabaabaa", 'b') = 5
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChar the character to find + * @return the last index of the search character, + * -1 if no match or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from lastIndexOf(String, int) to lastIndexOf(CharSequence, int) + */ + public static int lastIndexOf(CharSequence seq, int searchChar) { + if (isEmpty(seq)) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.lastIndexOf(seq, searchChar, seq.length()); + } + + /** + *

Finds the last index within a CharSequence from a start position, + * handling {@code null}. + * This method uses {@link String#lastIndexOf(int, int)} if possible.

+ * + *

A {@code null} or empty ("") CharSequence will return {@code -1}. + * A negative start position returns {@code -1}. + * A start position greater than the string length searches the whole string.

+ * + *
+     * StringUtils.lastIndexOf(null, *, *)          = -1
+     * StringUtils.lastIndexOf("", *,  *)           = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 8)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 4)  = 2
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 0)  = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 9)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", 'b', -1) = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'a', 0)  = 0
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChar the character to find + * @param startPos the start position + * @return the last index of the search character, + * -1 if no match or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from lastIndexOf(String, int, int) to lastIndexOf(CharSequence, int, int) + */ + public static int lastIndexOf(CharSequence seq, int searchChar, int startPos) { + if (isEmpty(seq)) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.lastIndexOf(seq, searchChar, startPos); + } + + /** + *

Finds the last index within a CharSequence, handling {@code null}. + * This method uses {@link String#lastIndexOf(String)} if possible.

+ * + *

A {@code null} CharSequence will return {@code -1}.

+ * + *
+     * StringUtils.lastIndexOf(null, *)          = -1
+     * StringUtils.lastIndexOf(*, null)          = -1
+     * StringUtils.lastIndexOf("", "")           = 0
+     * StringUtils.lastIndexOf("aabaabaa", "a")  = 7
+     * StringUtils.lastIndexOf("aabaabaa", "b")  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "ab") = 4
+     * StringUtils.lastIndexOf("aabaabaa", "")   = 8
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @return the last index of the search String, + * -1 if no match or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from lastIndexOf(String, String) to lastIndexOf(CharSequence, CharSequence) + */ + public static int lastIndexOf(CharSequence seq, CharSequence searchSeq) { + if (seq == null || searchSeq == null) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.lastIndexOf(seq, searchSeq, seq.length()); + } + + /** + *

Finds the n-th last index within a String, handling {@code null}. + * This method uses {@link String#lastIndexOf(String)}.

+ * + *

A {@code null} String will return {@code -1}.

+ * + *
+     * StringUtils.lastOrdinalIndexOf(null, *, *)          = -1
+     * StringUtils.lastOrdinalIndexOf(*, null, *)          = -1
+     * StringUtils.lastOrdinalIndexOf("", "", *)           = 0
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "a", 1)  = 7
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "a", 2)  = 6
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "b", 1)  = 5
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "b", 2)  = 2
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "ab", 1) = 4
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "ab", 2) = 1
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "", 1)   = 8
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "", 2)   = 8
+     * 
+ * + *

Note that 'tail(CharSequence str, int n)' may be implemented as:

+ * + *
+     *   str.substring(lastOrdinalIndexOf(str, "\n", n) + 1)
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @param ordinal the n-th last {@code searchStr} to find + * @return the n-th last index of the search CharSequence, + * {@code -1} ({@code INDEX_NOT_FOUND}) if no match or {@code null} string input + * @since 2.5 + * @since 3.0 Changed signature from lastOrdinalIndexOf(String, String, int) to lastOrdinalIndexOf(CharSequence, CharSequence, int) + */ + public static int lastOrdinalIndexOf(CharSequence str, CharSequence searchStr, int ordinal) { + return ordinalIndexOf(str, searchStr, ordinal, true); + } + + /** + *

Finds the first index within a CharSequence, handling {@code null}. + * This method uses {@link String#lastIndexOf(String, int)} if possible.

+ * + *

A {@code null} CharSequence will return {@code -1}. + * A negative start position returns {@code -1}. + * An empty ("") search CharSequence always matches unless the start position is negative. + * A start position greater than the string length searches the whole string.

+ * + *
+     * StringUtils.lastIndexOf(null, *, *)          = -1
+     * StringUtils.lastIndexOf(*, null, *)          = -1
+     * StringUtils.lastIndexOf("aabaabaa", "a", 8)  = 7
+     * StringUtils.lastIndexOf("aabaabaa", "b", 8)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "ab", 8) = 4
+     * StringUtils.lastIndexOf("aabaabaa", "b", 9)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "b", -1) = -1
+     * StringUtils.lastIndexOf("aabaabaa", "a", 0)  = 0
+     * StringUtils.lastIndexOf("aabaabaa", "b", 0)  = -1
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @param startPos the start position, negative treated as zero + * @return the first index of the search CharSequence, + * -1 if no match or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from lastIndexOf(String, String, int) to lastIndexOf(CharSequence, CharSequence, int) + */ + public static int lastIndexOf(CharSequence seq, CharSequence searchSeq, int startPos) { + if (seq == null || searchSeq == null) { + return INDEX_NOT_FOUND; + } + return CharSequenceUtils.lastIndexOf(seq, searchSeq, startPos); + } + + /** + *

Case in-sensitive find of the last index within a CharSequence.

+ * + *

A {@code null} CharSequence will return {@code -1}. + * A negative start position returns {@code -1}. + * An empty ("") search CharSequence always matches unless the start position is negative. + * A start position greater than the string length searches the whole string.

+ * + *
+     * StringUtils.lastIndexOfIgnoreCase(null, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase(*, null)          = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A")  = 7
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B")  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "AB") = 4
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @return the first index of the search CharSequence, + * -1 if no match or {@code null} string input + * @since 2.5 + * @since 3.0 Changed signature from lastIndexOfIgnoreCase(String, String) to lastIndexOfIgnoreCase(CharSequence, CharSequence) + */ + public static int lastIndexOfIgnoreCase(CharSequence str, CharSequence searchStr) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + return lastIndexOfIgnoreCase(str, searchStr, str.length()); + } + + /** + *

Case in-sensitive find of the last index within a CharSequence + * from the specified position.

+ * + *

A {@code null} CharSequence will return {@code -1}. + * A negative start position returns {@code -1}. + * An empty ("") search CharSequence always matches unless the start position is negative. + * A start position greater than the string length searches the whole string.

+ * + *
+     * StringUtils.lastIndexOfIgnoreCase(null, *, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase(*, null, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 8)  = 7
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 8)  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "AB", 8) = 4
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 9)  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", -1) = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 0)  = -1
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @param startPos the start position + * @return the first index of the search CharSequence, + * -1 if no match or {@code null} input + * @since 2.5 + * @since 3.0 Changed signature from lastIndexOfIgnoreCase(String, String, int) to lastIndexOfIgnoreCase(CharSequence, CharSequence, int) + */ + public static int lastIndexOfIgnoreCase(CharSequence str, CharSequence searchStr, int startPos) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (startPos > str.length() - searchStr.length()) { + startPos = str.length() - searchStr.length(); + } + if (startPos < 0) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return startPos; + } + + for (int i = startPos; i >= 0; i--) { + if (CharSequenceUtils.regionMatches(str, true, i, searchStr, 0, searchStr.length())) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + // Contains + //----------------------------------------------------------------------- + /** + *

Checks if CharSequence contains a search character, handling {@code null}. + * This method uses {@link String#indexOf(int)} if possible.

+ * + *

A {@code null} or empty ("") CharSequence will return {@code false}.

+ * + *
+     * StringUtils.contains(null, *)    = false
+     * StringUtils.contains("", *)      = false
+     * StringUtils.contains("abc", 'a') = true
+     * StringUtils.contains("abc", 'z') = false
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChar the character to find + * @return true if the CharSequence contains the search character, + * false if not or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from contains(String, int) to contains(CharSequence, int) + */ + public static boolean contains(CharSequence seq, int searchChar) { + if (isEmpty(seq)) { + return false; + } + return CharSequenceUtils.indexOf(seq, searchChar, 0) >= 0; + } + + /** + *

Checks if CharSequence contains a search CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(String)} if possible.

+ * + *

A {@code null} CharSequence will return {@code false}.

+ * + *
+     * StringUtils.contains(null, *)     = false
+     * StringUtils.contains(*, null)     = false
+     * StringUtils.contains("", "")      = true
+     * StringUtils.contains("abc", "")   = true
+     * StringUtils.contains("abc", "a")  = true
+     * StringUtils.contains("abc", "z")  = false
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @return true if the CharSequence contains the search CharSequence, + * false if not or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from contains(String, String) to contains(CharSequence, CharSequence) + */ + public static boolean contains(CharSequence seq, CharSequence searchSeq) { + if (seq == null || searchSeq == null) { + return false; + } + return CharSequenceUtils.indexOf(seq, searchSeq, 0) >= 0; + } + + /** + *

Checks if CharSequence contains a search CharSequence irrespective of case, + * handling {@code null}. Case-insensitivity is defined as by + * {@link String#equalsIgnoreCase(String)}. + * + *

A {@code null} CharSequence will return {@code false}.

+ * + *
+     * StringUtils.contains(null, *) = false
+     * StringUtils.contains(*, null) = false
+     * StringUtils.contains("", "") = true
+     * StringUtils.contains("abc", "") = true
+     * StringUtils.contains("abc", "a") = true
+     * StringUtils.contains("abc", "z") = false
+     * StringUtils.contains("abc", "A") = true
+     * StringUtils.contains("abc", "Z") = false
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @return true if the CharSequence contains the search CharSequence irrespective of + * case or false if not or {@code null} string input + * @since 3.0 Changed signature from containsIgnoreCase(String, String) to containsIgnoreCase(CharSequence, CharSequence) + */ + public static boolean containsIgnoreCase(CharSequence str, CharSequence searchStr) { + if (str == null || searchStr == null) { + return false; + } + int len = searchStr.length(); + int max = str.length() - len; + for (int i = 0; i <= max; i++) { + if (CharSequenceUtils.regionMatches(str, true, i, searchStr, 0, len)) { + return true; + } + } + return false; + } + + /** + * Check whether the given CharSequence contains any whitespace characters. + * @param seq the CharSequence to check (may be {@code null}) + * @return {@code true} if the CharSequence is not empty and + * contains at least 1 whitespace character + * @see java.lang.Character#isWhitespace + * @since 3.0 + */ + // From org.springframework.util.StringUtils, under Apache License 2.0 + public static boolean containsWhitespace(CharSequence seq) { + if (isEmpty(seq)) { + return false; + } + int strLen = seq.length(); + for (int i = 0; i < strLen; i++) { + if (Character.isWhitespace(seq.charAt(i))) { + return true; + } + } + return false; + } + + // IndexOfAny chars + //----------------------------------------------------------------------- + /** + *

Search a CharSequence to find the first index of any + * character in the given set of characters.

+ * + *

A {@code null} String will return {@code -1}. + * A {@code null} or zero length search array will return {@code -1}.

+ * + *
+     * StringUtils.indexOfAny(null, *)                = -1
+     * StringUtils.indexOfAny("", *)                  = -1
+     * StringUtils.indexOfAny(*, null)                = -1
+     * StringUtils.indexOfAny(*, [])                  = -1
+     * StringUtils.indexOfAny("zzabyycdxx",['z','a']) = 0
+     * StringUtils.indexOfAny("zzabyycdxx",['b','y']) = 3
+     * StringUtils.indexOfAny("aba", ['z'])           = -1
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param searchChars the chars to search for, may be null + * @return the index of any of the chars, -1 if no match or null input + * @since 2.0 + * @since 3.0 Changed signature from indexOfAny(String, char[]) to indexOfAny(CharSequence, char...) + */ + public static int indexOfAny(CharSequence cs, char... searchChars) { + if (isEmpty(cs) || ArrayUtils.isEmpty(searchChars)) { + return INDEX_NOT_FOUND; + } + int csLen = cs.length(); + int csLast = csLen - 1; + int searchLen = searchChars.length; + int searchLast = searchLen - 1; + for (int i = 0; i < csLen; i++) { + char ch = cs.charAt(i); + for (int j = 0; j < searchLen; j++) { + if (searchChars[j] == ch) { + if (i < csLast && j < searchLast && Character.isHighSurrogate(ch)) { + // ch is a supplementary character + if (searchChars[j + 1] == cs.charAt(i + 1)) { + return i; + } + } else { + return i; + } + } + } + } + return INDEX_NOT_FOUND; + } + + /** + *

Search a CharSequence to find the first index of any + * character in the given set of characters.

+ * + *

A {@code null} String will return {@code -1}. + * A {@code null} search string will return {@code -1}.

+ * + *
+     * StringUtils.indexOfAny(null, *)            = -1
+     * StringUtils.indexOfAny("", *)              = -1
+     * StringUtils.indexOfAny(*, null)            = -1
+     * StringUtils.indexOfAny(*, "")              = -1
+     * StringUtils.indexOfAny("zzabyycdxx", "za") = 0
+     * StringUtils.indexOfAny("zzabyycdxx", "by") = 3
+     * StringUtils.indexOfAny("aba","z")          = -1
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param searchChars the chars to search for, may be null + * @return the index of any of the chars, -1 if no match or null input + * @since 2.0 + * @since 3.0 Changed signature from indexOfAny(String, String) to indexOfAny(CharSequence, String) + */ + public static int indexOfAny(CharSequence cs, String searchChars) { + if (isEmpty(cs) || isEmpty(searchChars)) { + return INDEX_NOT_FOUND; + } + return indexOfAny(cs, searchChars.toCharArray()); + } + + // ContainsAny + //----------------------------------------------------------------------- + /** + *

Checks if the CharSequence contains any character in the given + * set of characters.

+ * + *

A {@code null} CharSequence will return {@code false}. + * A {@code null} or zero length search array will return {@code false}.

+ * + *
+     * StringUtils.containsAny(null, *)                = false
+     * StringUtils.containsAny("", *)                  = false
+     * StringUtils.containsAny(*, null)                = false
+     * StringUtils.containsAny(*, [])                  = false
+     * StringUtils.containsAny("zzabyycdxx",['z','a']) = true
+     * StringUtils.containsAny("zzabyycdxx",['b','y']) = true
+     * StringUtils.containsAny("aba", ['z'])           = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param searchChars the chars to search for, may be null + * @return the {@code true} if any of the chars are found, + * {@code false} if no match or null input + * @since 2.4 + * @since 3.0 Changed signature from containsAny(String, char[]) to containsAny(CharSequence, char...) + */ + public static boolean containsAny(CharSequence cs, char... searchChars) { + if (isEmpty(cs) || ArrayUtils.isEmpty(searchChars)) { + return false; + } + int csLength = cs.length(); + int searchLength = searchChars.length; + int csLast = csLength - 1; + int searchLast = searchLength - 1; + for (int i = 0; i < csLength; i++) { + char ch = cs.charAt(i); + for (int j = 0; j < searchLength; j++) { + if (searchChars[j] == ch) { + if (Character.isHighSurrogate(ch)) { + if (j == searchLast) { + // missing low surrogate, fine, like String.indexOf(String) + return true; + } + if (i < csLast && searchChars[j + 1] == cs.charAt(i + 1)) { + return true; + } + } else { + // ch is in the Basic Multilingual Plane + return true; + } + } + } + } + return false; + } + + /** + *

+ * Checks if the CharSequence contains any character in the given set of characters. + *

+ * + *

+ * A {@code null} CharSequence will return {@code false}. A {@code null} search CharSequence will return + * {@code false}. + *

+ * + *
+     * StringUtils.containsAny(null, *)            = false
+     * StringUtils.containsAny("", *)              = false
+     * StringUtils.containsAny(*, null)            = false
+     * StringUtils.containsAny(*, "")              = false
+     * StringUtils.containsAny("zzabyycdxx", "za") = true
+     * StringUtils.containsAny("zzabyycdxx", "by") = true
+     * StringUtils.containsAny("aba","z")          = false
+     * 
+ * + * @param cs + * the CharSequence to check, may be null + * @param searchChars + * the chars to search for, may be null + * @return the {@code true} if any of the chars are found, {@code false} if no match or null input + * @since 2.4 + * @since 3.0 Changed signature from containsAny(String, String) to containsAny(CharSequence, CharSequence) + */ + public static boolean containsAny(CharSequence cs, CharSequence searchChars) { + if (searchChars == null) { + return false; + } + return containsAny(cs, CharSequenceUtils.toCharArray(searchChars)); + } + + // IndexOfAnyBut chars + //----------------------------------------------------------------------- + /** + *

Searches a CharSequence to find the first index of any + * character not in the given set of characters.

+ * + *

A {@code null} CharSequence will return {@code -1}. + * A {@code null} or zero length search array will return {@code -1}.

+ * + *
+     * StringUtils.indexOfAnyBut(null, *)                              = -1
+     * StringUtils.indexOfAnyBut("", *)                                = -1
+     * StringUtils.indexOfAnyBut(*, null)                              = -1
+     * StringUtils.indexOfAnyBut(*, [])                                = -1
+     * StringUtils.indexOfAnyBut("zzabyycdxx", new char[] {'z', 'a'} ) = 3
+     * StringUtils.indexOfAnyBut("aba", new char[] {'z'} )             = 0
+     * StringUtils.indexOfAnyBut("aba", new char[] {'a', 'b'} )        = -1
+
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param searchChars the chars to search for, may be null + * @return the index of any of the chars, -1 if no match or null input + * @since 2.0 + * @since 3.0 Changed signature from indexOfAnyBut(String, char[]) to indexOfAnyBut(CharSequence, char...) + */ + public static int indexOfAnyBut(CharSequence cs, char... searchChars) { + if (isEmpty(cs) || ArrayUtils.isEmpty(searchChars)) { + return INDEX_NOT_FOUND; + } + int csLen = cs.length(); + int csLast = csLen - 1; + int searchLen = searchChars.length; + int searchLast = searchLen - 1; + outer: + for (int i = 0; i < csLen; i++) { + char ch = cs.charAt(i); + for (int j = 0; j < searchLen; j++) { + if (searchChars[j] == ch) { + if (i < csLast && j < searchLast && Character.isHighSurrogate(ch)) { + if (searchChars[j + 1] == cs.charAt(i + 1)) { + continue outer; + } + } else { + continue outer; + } + } + } + return i; + } + return INDEX_NOT_FOUND; + } + + /** + *

Search a CharSequence to find the first index of any + * character not in the given set of characters.

+ * + *

A {@code null} CharSequence will return {@code -1}. + * A {@code null} or empty search string will return {@code -1}.

+ * + *
+     * StringUtils.indexOfAnyBut(null, *)            = -1
+     * StringUtils.indexOfAnyBut("", *)              = -1
+     * StringUtils.indexOfAnyBut(*, null)            = -1
+     * StringUtils.indexOfAnyBut(*, "")              = -1
+     * StringUtils.indexOfAnyBut("zzabyycdxx", "za") = 3
+     * StringUtils.indexOfAnyBut("zzabyycdxx", "")   = -1
+     * StringUtils.indexOfAnyBut("aba","ab")         = -1
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchChars the chars to search for, may be null + * @return the index of any of the chars, -1 if no match or null input + * @since 2.0 + * @since 3.0 Changed signature from indexOfAnyBut(String, String) to indexOfAnyBut(CharSequence, CharSequence) + */ + public static int indexOfAnyBut(CharSequence seq, CharSequence searchChars) { + if (isEmpty(seq) || isEmpty(searchChars)) { + return INDEX_NOT_FOUND; + } + int strLen = seq.length(); + for (int i = 0; i < strLen; i++) { + char ch = seq.charAt(i); + boolean chFound = CharSequenceUtils.indexOf(searchChars, ch, 0) >= 0; + if (i + 1 < strLen && Character.isHighSurrogate(ch)) { + char ch2 = seq.charAt(i + 1); + if (chFound && CharSequenceUtils.indexOf(searchChars, ch2, 0) < 0) { + return i; + } + } else { + if (!chFound) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + // ContainsOnly + //----------------------------------------------------------------------- + /** + *

Checks if the CharSequence contains only certain characters.

+ * + *

A {@code null} CharSequence will return {@code false}. + * A {@code null} valid character array will return {@code false}. + * An empty CharSequence (length()=0) always returns {@code true}.

+ * + *
+     * StringUtils.containsOnly(null, *)       = false
+     * StringUtils.containsOnly(*, null)       = false
+     * StringUtils.containsOnly("", *)         = true
+     * StringUtils.containsOnly("ab", '')      = false
+     * StringUtils.containsOnly("abab", 'abc') = true
+     * StringUtils.containsOnly("ab1", 'abc')  = false
+     * StringUtils.containsOnly("abz", 'abc')  = false
+     * 
+ * + * @param cs the String to check, may be null + * @param valid an array of valid chars, may be null + * @return true if it only contains valid chars and is non-null + * @since 3.0 Changed signature from containsOnly(String, char[]) to containsOnly(CharSequence, char...) + */ + public static boolean containsOnly(CharSequence cs, char... valid) { + // All these pre-checks are to maintain API with an older version + if (valid == null || cs == null) { + return false; + } + if (cs.length() == 0) { + return true; + } + if (valid.length == 0) { + return false; + } + return indexOfAnyBut(cs, valid) == INDEX_NOT_FOUND; + } + + /** + *

Checks if the CharSequence contains only certain characters.

+ * + *

A {@code null} CharSequence will return {@code false}. + * A {@code null} valid character String will return {@code false}. + * An empty String (length()=0) always returns {@code true}.

+ * + *
+     * StringUtils.containsOnly(null, *)       = false
+     * StringUtils.containsOnly(*, null)       = false
+     * StringUtils.containsOnly("", *)         = true
+     * StringUtils.containsOnly("ab", "")      = false
+     * StringUtils.containsOnly("abab", "abc") = true
+     * StringUtils.containsOnly("ab1", "abc")  = false
+     * StringUtils.containsOnly("abz", "abc")  = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param validChars a String of valid chars, may be null + * @return true if it only contains valid chars and is non-null + * @since 2.0 + * @since 3.0 Changed signature from containsOnly(String, String) to containsOnly(CharSequence, String) + */ + public static boolean containsOnly(CharSequence cs, String validChars) { + if (cs == null || validChars == null) { + return false; + } + return containsOnly(cs, validChars.toCharArray()); + } + + // ContainsNone + //----------------------------------------------------------------------- + /** + *

Checks that the CharSequence does not contain certain characters.

+ * + *

A {@code null} CharSequence will return {@code true}. + * A {@code null} invalid character array will return {@code true}. + * An empty CharSequence (length()=0) always returns true.

+ * + *
+     * StringUtils.containsNone(null, *)       = true
+     * StringUtils.containsNone(*, null)       = true
+     * StringUtils.containsNone("", *)         = true
+     * StringUtils.containsNone("ab", '')      = true
+     * StringUtils.containsNone("abab", 'xyz') = true
+     * StringUtils.containsNone("ab1", 'xyz')  = true
+     * StringUtils.containsNone("abz", 'xyz')  = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param searchChars an array of invalid chars, may be null + * @return true if it contains none of the invalid chars, or is null + * @since 2.0 + * @since 3.0 Changed signature from containsNone(String, char[]) to containsNone(CharSequence, char...) + */ + public static boolean containsNone(CharSequence cs, char... searchChars) { + if (cs == null || searchChars == null) { + return true; + } + int csLen = cs.length(); + int csLast = csLen - 1; + int searchLen = searchChars.length; + int searchLast = searchLen - 1; + for (int i = 0; i < csLen; i++) { + char ch = cs.charAt(i); + for (int j = 0; j < searchLen; j++) { + if (searchChars[j] == ch) { + if (Character.isHighSurrogate(ch)) { + if (j == searchLast) { + // missing low surrogate, fine, like String.indexOf(String) + return false; + } + if (i < csLast && searchChars[j + 1] == cs.charAt(i + 1)) { + return false; + } + } else { + // ch is in the Basic Multilingual Plane + return false; + } + } + } + } + return true; + } + + /** + *

Checks that the CharSequence does not contain certain characters.

+ * + *

A {@code null} CharSequence will return {@code true}. + * A {@code null} invalid character array will return {@code true}. + * An empty String ("") always returns true.

+ * + *
+     * StringUtils.containsNone(null, *)       = true
+     * StringUtils.containsNone(*, null)       = true
+     * StringUtils.containsNone("", *)         = true
+     * StringUtils.containsNone("ab", "")      = true
+     * StringUtils.containsNone("abab", "xyz") = true
+     * StringUtils.containsNone("ab1", "xyz")  = true
+     * StringUtils.containsNone("abz", "xyz")  = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @param invalidChars a String of invalid chars, may be null + * @return true if it contains none of the invalid chars, or is null + * @since 2.0 + * @since 3.0 Changed signature from containsNone(String, String) to containsNone(CharSequence, String) + */ + public static boolean containsNone(CharSequence cs, String invalidChars) { + if (cs == null || invalidChars == null) { + return true; + } + return containsNone(cs, invalidChars.toCharArray()); + } + + // IndexOfAny strings + //----------------------------------------------------------------------- + /** + *

Find the first index of any of a set of potential substrings.

+ * + *

A {@code null} CharSequence will return {@code -1}. + * A {@code null} or zero length search array will return {@code -1}. + * A {@code null} search array entry will be ignored, but a search + * array containing "" will return {@code 0} if {@code str} is not + * null. This method uses {@link String#indexOf(String)} if possible.

+ * + *
+     * StringUtils.indexOfAny(null, *)                     = -1
+     * StringUtils.indexOfAny(*, null)                     = -1
+     * StringUtils.indexOfAny(*, [])                       = -1
+     * StringUtils.indexOfAny("zzabyycdxx", ["ab","cd"])   = 2
+     * StringUtils.indexOfAny("zzabyycdxx", ["cd","ab"])   = 2
+     * StringUtils.indexOfAny("zzabyycdxx", ["mn","op"])   = -1
+     * StringUtils.indexOfAny("zzabyycdxx", ["zab","aby"]) = 1
+     * StringUtils.indexOfAny("zzabyycdxx", [""])          = 0
+     * StringUtils.indexOfAny("", [""])                    = 0
+     * StringUtils.indexOfAny("", ["a"])                   = -1
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStrs the CharSequences to search for, may be null + * @return the first index of any of the searchStrs in str, -1 if no match + * @since 3.0 Changed signature from indexOfAny(String, String[]) to indexOfAny(CharSequence, CharSequence...) + */ + public static int indexOfAny(CharSequence str, CharSequence... searchStrs) { + if (str == null || searchStrs == null) { + return INDEX_NOT_FOUND; + } + int sz = searchStrs.length; + + // String's can't have a MAX_VALUEth index. + int ret = Integer.MAX_VALUE; + + int tmp = 0; + for (int i = 0; i < sz; i++) { + CharSequence search = searchStrs[i]; + if (search == null) { + continue; + } + tmp = CharSequenceUtils.indexOf(str, search, 0); + if (tmp == INDEX_NOT_FOUND) { + continue; + } + + if (tmp < ret) { + ret = tmp; + } + } + + return ret == Integer.MAX_VALUE ? INDEX_NOT_FOUND : ret; + } + + /** + *

Find the latest index of any of a set of potential substrings.

+ * + *

A {@code null} CharSequence will return {@code -1}. + * A {@code null} search array will return {@code -1}. + * A {@code null} or zero length search array entry will be ignored, + * but a search array containing "" will return the length of {@code str} + * if {@code str} is not null. This method uses {@link String#indexOf(String)} if possible

+ * + *
+     * StringUtils.lastIndexOfAny(null, *)                   = -1
+     * StringUtils.lastIndexOfAny(*, null)                   = -1
+     * StringUtils.lastIndexOfAny(*, [])                     = -1
+     * StringUtils.lastIndexOfAny(*, [null])                 = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["ab","cd"]) = 6
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["cd","ab"]) = 6
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn","op"]) = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn","op"]) = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn",""])   = 10
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStrs the CharSequences to search for, may be null + * @return the last index of any of the CharSequences, -1 if no match + * @since 3.0 Changed signature from lastIndexOfAny(String, String[]) to lastIndexOfAny(CharSequence, CharSequence) + */ + public static int lastIndexOfAny(CharSequence str, CharSequence... searchStrs) { + if (str == null || searchStrs == null) { + return INDEX_NOT_FOUND; + } + int sz = searchStrs.length; + int ret = INDEX_NOT_FOUND; + int tmp = 0; + for (int i = 0; i < sz; i++) { + CharSequence search = searchStrs[i]; + if (search == null) { + continue; + } + tmp = CharSequenceUtils.lastIndexOf(str, search, str.length()); + if (tmp > ret) { + ret = tmp; + } + } + return ret; + } + + // Substring + //----------------------------------------------------------------------- + /** + *

Gets a substring from the specified String avoiding exceptions.

+ * + *

A negative start position can be used to start {@code n} + * characters from the end of the String.

+ * + *

A {@code null} String will return {@code null}. + * An empty ("") String will return "".

+ * + *
+     * StringUtils.substring(null, *)   = null
+     * StringUtils.substring("", *)     = ""
+     * StringUtils.substring("abc", 0)  = "abc"
+     * StringUtils.substring("abc", 2)  = "c"
+     * StringUtils.substring("abc", 4)  = ""
+     * StringUtils.substring("abc", -2) = "bc"
+     * StringUtils.substring("abc", -4) = "abc"
+     * 
+ * + * @param str the String to get the substring from, may be null + * @param start the position to start from, negative means + * count back from the end of the String by this many characters + * @return substring from start position, {@code null} if null String input + */ + public static String substring(String str, int start) { + if (str == null) { + return null; + } + + // handle negatives, which means last n characters + if (start < 0) { + start = str.length() + start; // remember start is negative + } + + if (start < 0) { + start = 0; + } + if (start > str.length()) { + return EMPTY; + } + + return str.substring(start); + } + + /** + *

Gets a substring from the specified String avoiding exceptions.

+ * + *

A negative start position can be used to start/end {@code n} + * characters from the end of the String.

+ * + *

The returned substring starts with the character in the {@code start} + * position and ends before the {@code end} position. All position counting is + * zero-based -- i.e., to start at the beginning of the string use + * {@code start = 0}. Negative start and end positions can be used to + * specify offsets relative to the end of the String.

+ * + *

If {@code start} is not strictly to the left of {@code end}, "" + * is returned.

+ * + *
+     * StringUtils.substring(null, *, *)    = null
+     * StringUtils.substring("", * ,  *)    = "";
+     * StringUtils.substring("abc", 0, 2)   = "ab"
+     * StringUtils.substring("abc", 2, 0)   = ""
+     * StringUtils.substring("abc", 2, 4)   = "c"
+     * StringUtils.substring("abc", 4, 6)   = ""
+     * StringUtils.substring("abc", 2, 2)   = ""
+     * StringUtils.substring("abc", -2, -1) = "b"
+     * StringUtils.substring("abc", -4, 2)  = "ab"
+     * 
+ * + * @param str the String to get the substring from, may be null + * @param start the position to start from, negative means + * count back from the end of the String by this many characters + * @param end the position to end at (exclusive), negative means + * count back from the end of the String by this many characters + * @return substring from start position to end position, + * {@code null} if null String input + */ + public static String substring(String str, int start, int end) { + if (str == null) { + return null; + } + + // handle negatives + if (end < 0) { + end = str.length() + end; // remember end is negative + } + if (start < 0) { + start = str.length() + start; // remember start is negative + } + + // check length next + if (end > str.length()) { + end = str.length(); + } + + // if start is greater than end, return "" + if (start > end) { + return EMPTY; + } + + if (start < 0) { + start = 0; + } + if (end < 0) { + end = 0; + } + + return str.substring(start, end); + } + + // Left/Right/Mid + //----------------------------------------------------------------------- + /** + *

Gets the leftmost {@code len} characters of a String.

+ * + *

If {@code len} characters are not available, or the + * String is {@code null}, the String will be returned without + * an exception. An empty String is returned if len is negative.

+ * + *
+     * StringUtils.left(null, *)    = null
+     * StringUtils.left(*, -ve)     = ""
+     * StringUtils.left("", *)      = ""
+     * StringUtils.left("abc", 0)   = ""
+     * StringUtils.left("abc", 2)   = "ab"
+     * StringUtils.left("abc", 4)   = "abc"
+     * 
+ * + * @param str the String to get the leftmost characters from, may be null + * @param len the length of the required String + * @return the leftmost characters, {@code null} if null String input + */ + public static String left(String str, int len) { + if (str == null) { + return null; + } + if (len < 0) { + return EMPTY; + } + if (str.length() <= len) { + return str; + } + return str.substring(0, len); + } + + /** + *

Gets the rightmost {@code len} characters of a String.

+ * + *

If {@code len} characters are not available, or the String + * is {@code null}, the String will be returned without an + * an exception. An empty String is returned if len is negative.

+ * + *
+     * StringUtils.right(null, *)    = null
+     * StringUtils.right(*, -ve)     = ""
+     * StringUtils.right("", *)      = ""
+     * StringUtils.right("abc", 0)   = ""
+     * StringUtils.right("abc", 2)   = "bc"
+     * StringUtils.right("abc", 4)   = "abc"
+     * 
+ * + * @param str the String to get the rightmost characters from, may be null + * @param len the length of the required String + * @return the rightmost characters, {@code null} if null String input + */ + public static String right(String str, int len) { + if (str == null) { + return null; + } + if (len < 0) { + return EMPTY; + } + if (str.length() <= len) { + return str; + } + return str.substring(str.length() - len); + } + + /** + *

Gets {@code len} characters from the middle of a String.

+ * + *

If {@code len} characters are not available, the remainder + * of the String will be returned without an exception. If the + * String is {@code null}, {@code null} will be returned. + * An empty String is returned if len is negative or exceeds the + * length of {@code str}.

+ * + *
+     * StringUtils.mid(null, *, *)    = null
+     * StringUtils.mid(*, *, -ve)     = ""
+     * StringUtils.mid("", 0, *)      = ""
+     * StringUtils.mid("abc", 0, 2)   = "ab"
+     * StringUtils.mid("abc", 0, 4)   = "abc"
+     * StringUtils.mid("abc", 2, 4)   = "c"
+     * StringUtils.mid("abc", 4, 2)   = ""
+     * StringUtils.mid("abc", -2, 2)  = "ab"
+     * 
+ * + * @param str the String to get the characters from, may be null + * @param pos the position to start from, negative treated as zero + * @param len the length of the required String + * @return the middle characters, {@code null} if null String input + */ + public static String mid(String str, int pos, int len) { + if (str == null) { + return null; + } + if (len < 0 || pos > str.length()) { + return EMPTY; + } + if (pos < 0) { + pos = 0; + } + if (str.length() <= pos + len) { + return str.substring(pos); + } + return str.substring(pos, pos + len); + } + + // SubStringAfter/SubStringBefore + //----------------------------------------------------------------------- + /** + *

Gets the substring before the first occurrence of a separator. + * The separator is not returned.

+ * + *

A {@code null} string input will return {@code null}. + * An empty ("") string input will return the empty string. + * A {@code null} separator will return the input string.

+ * + *

If nothing is found, the string input is returned.

+ * + *
+     * StringUtils.substringBefore(null, *)      = null
+     * StringUtils.substringBefore("", *)        = ""
+     * StringUtils.substringBefore("abc", "a")   = ""
+     * StringUtils.substringBefore("abcba", "b") = "a"
+     * StringUtils.substringBefore("abc", "c")   = "ab"
+     * StringUtils.substringBefore("abc", "d")   = "abc"
+     * StringUtils.substringBefore("abc", "")    = ""
+     * StringUtils.substringBefore("abc", null)  = "abc"
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring before the first occurrence of the separator, + * {@code null} if null String input + * @since 2.0 + */ + public static String substringBefore(String str, String separator) { + if (isEmpty(str) || separator == null) { + return str; + } + if (separator.length() == 0) { + return EMPTY; + } + int pos = str.indexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return str; + } + return str.substring(0, pos); + } + + /** + *

Gets the substring after the first occurrence of a separator. + * The separator is not returned.

+ * + *

A {@code null} string input will return {@code null}. + * An empty ("") string input will return the empty string. + * A {@code null} separator will return the empty string if the + * input string is not {@code null}.

+ * + *

If nothing is found, the empty string is returned.

+ * + *
+     * StringUtils.substringAfter(null, *)      = null
+     * StringUtils.substringAfter("", *)        = ""
+     * StringUtils.substringAfter(*, null)      = ""
+     * StringUtils.substringAfter("abc", "a")   = "bc"
+     * StringUtils.substringAfter("abcba", "b") = "cba"
+     * StringUtils.substringAfter("abc", "c")   = ""
+     * StringUtils.substringAfter("abc", "d")   = ""
+     * StringUtils.substringAfter("abc", "")    = "abc"
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring after the first occurrence of the separator, + * {@code null} if null String input + * @since 2.0 + */ + public static String substringAfter(String str, String separator) { + if (isEmpty(str)) { + return str; + } + if (separator == null) { + return EMPTY; + } + int pos = str.indexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return EMPTY; + } + return str.substring(pos + separator.length()); + } + + /** + *

Gets the substring before the last occurrence of a separator. + * The separator is not returned.

+ * + *

A {@code null} string input will return {@code null}. + * An empty ("") string input will return the empty string. + * An empty or {@code null} separator will return the input string.

+ * + *

If nothing is found, the string input is returned.

+ * + *
+     * StringUtils.substringBeforeLast(null, *)      = null
+     * StringUtils.substringBeforeLast("", *)        = ""
+     * StringUtils.substringBeforeLast("abcba", "b") = "abc"
+     * StringUtils.substringBeforeLast("abc", "c")   = "ab"
+     * StringUtils.substringBeforeLast("a", "a")     = ""
+     * StringUtils.substringBeforeLast("a", "z")     = "a"
+     * StringUtils.substringBeforeLast("a", null)    = "a"
+     * StringUtils.substringBeforeLast("a", "")      = "a"
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring before the last occurrence of the separator, + * {@code null} if null String input + * @since 2.0 + */ + public static String substringBeforeLast(String str, String separator) { + if (isEmpty(str) || isEmpty(separator)) { + return str; + } + int pos = str.lastIndexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return str; + } + return str.substring(0, pos); + } + + /** + *

Gets the substring after the last occurrence of a separator. + * The separator is not returned.

+ * + *

A {@code null} string input will return {@code null}. + * An empty ("") string input will return the empty string. + * An empty or {@code null} separator will return the empty string if + * the input string is not {@code null}.

+ * + *

If nothing is found, the empty string is returned.

+ * + *
+     * StringUtils.substringAfterLast(null, *)      = null
+     * StringUtils.substringAfterLast("", *)        = ""
+     * StringUtils.substringAfterLast(*, "")        = ""
+     * StringUtils.substringAfterLast(*, null)      = ""
+     * StringUtils.substringAfterLast("abc", "a")   = "bc"
+     * StringUtils.substringAfterLast("abcba", "b") = "a"
+     * StringUtils.substringAfterLast("abc", "c")   = ""
+     * StringUtils.substringAfterLast("a", "a")     = ""
+     * StringUtils.substringAfterLast("a", "z")     = ""
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring after the last occurrence of the separator, + * {@code null} if null String input + * @since 2.0 + */ + public static String substringAfterLast(String str, String separator) { + if (isEmpty(str)) { + return str; + } + if (isEmpty(separator)) { + return EMPTY; + } + int pos = str.lastIndexOf(separator); + if (pos == INDEX_NOT_FOUND || pos == str.length() - separator.length()) { + return EMPTY; + } + return str.substring(pos + separator.length()); + } + + // Substring between + //----------------------------------------------------------------------- + /** + *

Gets the String that is nested in between two instances of the + * same String.

+ * + *

A {@code null} input String returns {@code null}. + * A {@code null} tag returns {@code null}.

+ * + *
+     * StringUtils.substringBetween(null, *)            = null
+     * StringUtils.substringBetween("", "")             = ""
+     * StringUtils.substringBetween("", "tag")          = null
+     * StringUtils.substringBetween("tagabctag", null)  = null
+     * StringUtils.substringBetween("tagabctag", "")    = ""
+     * StringUtils.substringBetween("tagabctag", "tag") = "abc"
+     * 
+ * + * @param str the String containing the substring, may be null + * @param tag the String before and after the substring, may be null + * @return the substring, {@code null} if no match + * @since 2.0 + */ + public static String substringBetween(String str, String tag) { + return substringBetween(str, tag, tag); + } + + /** + *

Gets the String that is nested in between two Strings. + * Only the first match is returned.

+ * + *

A {@code null} input String returns {@code null}. + * A {@code null} open/close returns {@code null} (no match). + * An empty ("") open and close returns an empty string.

+ * + *
+     * StringUtils.substringBetween("wx[b]yz", "[", "]") = "b"
+     * StringUtils.substringBetween(null, *, *)          = null
+     * StringUtils.substringBetween(*, null, *)          = null
+     * StringUtils.substringBetween(*, *, null)          = null
+     * StringUtils.substringBetween("", "", "")          = ""
+     * StringUtils.substringBetween("", "", "]")         = null
+     * StringUtils.substringBetween("", "[", "]")        = null
+     * StringUtils.substringBetween("yabcz", "", "")     = ""
+     * StringUtils.substringBetween("yabcz", "y", "z")   = "abc"
+     * StringUtils.substringBetween("yabczyabcz", "y", "z")   = "abc"
+     * 
+ * + * @param str the String containing the substring, may be null + * @param open the String before the substring, may be null + * @param close the String after the substring, may be null + * @return the substring, {@code null} if no match + * @since 2.0 + */ + public static String substringBetween(String str, String open, String close) { + if (str == null || open == null || close == null) { + return null; + } + int start = str.indexOf(open); + if (start != INDEX_NOT_FOUND) { + int end = str.indexOf(close, start + open.length()); + if (end != INDEX_NOT_FOUND) { + return str.substring(start + open.length(), end); + } + } + return null; + } + + /** + *

Searches a String for substrings delimited by a start and end tag, + * returning all matching substrings in an array.

+ * + *

A {@code null} input String returns {@code null}. + * A {@code null} open/close returns {@code null} (no match). + * An empty ("") open/close returns {@code null} (no match).

+ * + *
+     * StringUtils.substringsBetween("[a][b][c]", "[", "]") = ["a","b","c"]
+     * StringUtils.substringsBetween(null, *, *)            = null
+     * StringUtils.substringsBetween(*, null, *)            = null
+     * StringUtils.substringsBetween(*, *, null)            = null
+     * StringUtils.substringsBetween("", "[", "]")          = []
+     * 
+ * + * @param str the String containing the substrings, null returns null, empty returns empty + * @param open the String identifying the start of the substring, empty returns null + * @param close the String identifying the end of the substring, empty returns null + * @return a String Array of substrings, or {@code null} if no match + * @since 2.3 + */ + public static String[] substringsBetween(String str, String open, String close) { + if (str == null || isEmpty(open) || isEmpty(close)) { + return null; + } + int strLen = str.length(); + if (strLen == 0) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + int closeLen = close.length(); + int openLen = open.length(); + List list = new ArrayList(); + int pos = 0; + while (pos < strLen - closeLen) { + int start = str.indexOf(open, pos); + if (start < 0) { + break; + } + start += openLen; + int end = str.indexOf(close, start); + if (end < 0) { + break; + } + list.add(str.substring(start, end)); + pos = end + closeLen; + } + if (list.isEmpty()) { + return null; + } + return list.toArray(new String [list.size()]); + } + + // Nested extraction + //----------------------------------------------------------------------- + + // Splitting + //----------------------------------------------------------------------- + /** + *

Splits the provided text into an array, using whitespace as the + * separator. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *

The separator is not included in the returned String array. + * Adjacent separators are treated as one separator. + * For more control over the split use the StrTokenizer class.

+ * + *

A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.split(null)       = null
+     * StringUtils.split("")         = []
+     * StringUtils.split("abc def")  = ["abc", "def"]
+     * StringUtils.split("abc  def") = ["abc", "def"]
+     * StringUtils.split(" abc ")    = ["abc"]
+     * 
+ * + * @param str the String to parse, may be null + * @return an array of parsed Strings, {@code null} if null String input + */ + public static String[] split(String str) { + return split(str, null, -1); + } + + /** + *

Splits the provided text into an array, separator specified. + * This is an alternative to using StringTokenizer.

+ * + *

The separator is not included in the returned String array. + * Adjacent separators are treated as one separator. + * For more control over the split use the StrTokenizer class.

+ * + *

A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.split(null, *)         = null
+     * StringUtils.split("", *)           = []
+     * StringUtils.split("a.b.c", '.')    = ["a", "b", "c"]
+     * StringUtils.split("a..b.c", '.')   = ["a", "b", "c"]
+     * StringUtils.split("a:b:c", '.')    = ["a:b:c"]
+     * StringUtils.split("a b c", ' ')    = ["a", "b", "c"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChar the character used as the delimiter + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.0 + */ + public static String[] split(String str, char separatorChar) { + return splitWorker(str, separatorChar, false); + } + + /** + *

Splits the provided text into an array, separators specified. + * This is an alternative to using StringTokenizer.

+ * + *

The separator is not included in the returned String array. + * Adjacent separators are treated as one separator. + * For more control over the split use the StrTokenizer class.

+ * + *

A {@code null} input String returns {@code null}. + * A {@code null} separatorChars splits on whitespace.

+ * + *
+     * StringUtils.split(null, *)         = null
+     * StringUtils.split("", *)           = []
+     * StringUtils.split("abc def", null) = ["abc", "def"]
+     * StringUtils.split("abc def", " ")  = ["abc", "def"]
+     * StringUtils.split("abc  def", " ") = ["abc", "def"]
+     * StringUtils.split("ab:cd:ef", ":") = ["ab", "cd", "ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChars the characters used as the delimiters, + * {@code null} splits on whitespace + * @return an array of parsed Strings, {@code null} if null String input + */ + public static String[] split(String str, String separatorChars) { + return splitWorker(str, separatorChars, -1, false); + } + + /** + *

Splits the provided text into an array with a maximum length, + * separators specified.

+ * + *

The separator is not included in the returned String array. + * Adjacent separators are treated as one separator.

+ * + *

A {@code null} input String returns {@code null}. + * A {@code null} separatorChars splits on whitespace.

+ * + *

If more than {@code max} delimited substrings are found, the last + * returned string includes all characters after the first {@code max - 1} + * returned strings (including separator characters).

+ * + *
+     * StringUtils.split(null, *, *)            = null
+     * StringUtils.split("", *, *)              = []
+     * StringUtils.split("ab de fg", null, 0)   = ["ab", "cd", "ef"]
+     * StringUtils.split("ab   de fg", null, 0) = ["ab", "cd", "ef"]
+     * StringUtils.split("ab:cd:ef", ":", 0)    = ["ab", "cd", "ef"]
+     * StringUtils.split("ab:cd:ef", ":", 2)    = ["ab", "cd:ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separatorChars the characters used as the delimiters, + * {@code null} splits on whitespace + * @param max the maximum number of elements to include in the + * array. A zero or negative value implies no limit + * @return an array of parsed Strings, {@code null} if null String input + */ + public static String[] split(String str, String separatorChars, int max) { + return splitWorker(str, separatorChars, max, false); + } + + /** + *

Splits the provided text into an array, separator string specified.

+ * + *

The separator(s) will not be included in the returned String array. + * Adjacent separators are treated as one separator.

+ * + *

A {@code null} input String returns {@code null}. + * A {@code null} separator splits on whitespace.

+ * + *
+     * StringUtils.splitByWholeSeparator(null, *)               = null
+     * StringUtils.splitByWholeSeparator("", *)                 = []
+     * StringUtils.splitByWholeSeparator("ab de fg", null)      = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab   de fg", null)    = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab:cd:ef", ":")       = ["ab", "cd", "ef"]
+     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-") = ["ab", "cd", "ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separator String containing the String to be used as a delimiter, + * {@code null} splits on whitespace + * @return an array of parsed Strings, {@code null} if null String was input + */ + public static String[] splitByWholeSeparator(String str, String separator) { + return splitByWholeSeparatorWorker( str, separator, -1, false ) ; + } + + /** + *

Splits the provided text into an array, separator string specified. + * Returns a maximum of {@code max} substrings.

+ * + *

The separator(s) will not be included in the returned String array. + * Adjacent separators are treated as one separator.

+ * + *

A {@code null} input String returns {@code null}. + * A {@code null} separator splits on whitespace.

+ * + *
+     * StringUtils.splitByWholeSeparator(null, *, *)               = null
+     * StringUtils.splitByWholeSeparator("", *, *)                 = []
+     * StringUtils.splitByWholeSeparator("ab de fg", null, 0)      = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab   de fg", null, 0)    = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparator("ab:cd:ef", ":", 2)       = ["ab", "cd:ef"]
+     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-", 5) = ["ab", "cd", "ef"]
+     * StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-", 2) = ["ab", "cd-!-ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separator String containing the String to be used as a delimiter, + * {@code null} splits on whitespace + * @param max the maximum number of elements to include in the returned + * array. A zero or negative value implies no limit. + * @return an array of parsed Strings, {@code null} if null String was input + */ + public static String[] splitByWholeSeparator( String str, String separator, int max ) { + return splitByWholeSeparatorWorker(str, separator, max, false); + } + + /** + *

Splits the provided text into an array, separator string specified.

+ * + *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * For more control over the split use the StrTokenizer class.

+ * + *

A {@code null} input String returns {@code null}. + * A {@code null} separator splits on whitespace.

+ * + *
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens(null, *)               = null
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("", *)                 = []
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab de fg", null)      = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab   de fg", null)    = ["ab", "", "", "de", "fg"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab:cd:ef", ":")       = ["ab", "cd", "ef"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab-!-cd-!-ef", "-!-") = ["ab", "cd", "ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separator String containing the String to be used as a delimiter, + * {@code null} splits on whitespace + * @return an array of parsed Strings, {@code null} if null String was input + * @since 2.4 + */ + public static String[] splitByWholeSeparatorPreserveAllTokens(String str, String separator) { + return splitByWholeSeparatorWorker(str, separator, -1, true); + } + + /** + *

Splits the provided text into an array, separator string specified. + * Returns a maximum of {@code max} substrings.

+ * + *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * For more control over the split use the StrTokenizer class.

+ * + *

A {@code null} input String returns {@code null}. + * A {@code null} separator splits on whitespace.

+ * + *
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens(null, *, *)               = null
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("", *, *)                 = []
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab de fg", null, 0)      = ["ab", "de", "fg"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab   de fg", null, 0)    = ["ab", "", "", "de", "fg"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab:cd:ef", ":", 2)       = ["ab", "cd:ef"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab-!-cd-!-ef", "-!-", 5) = ["ab", "cd", "ef"]
+     * StringUtils.splitByWholeSeparatorPreserveAllTokens("ab-!-cd-!-ef", "-!-", 2) = ["ab", "cd-!-ef"]
+     * 
+ * + * @param str the String to parse, may be null + * @param separator String containing the String to be used as a delimiter, + * {@code null} splits on whitespace + * @param max the maximum number of elements to include in the returned + * array. A zero or negative value implies no limit. + * @return an array of parsed Strings, {@code null} if null String was input + * @since 2.4 + */ + public static String[] splitByWholeSeparatorPreserveAllTokens(String str, String separator, int max) { + return splitByWholeSeparatorWorker(str, separator, max, true); + } + + /** + * Performs the logic for the {@code splitByWholeSeparatorPreserveAllTokens} methods. + * + * @param str the String to parse, may be {@code null} + * @param separator String containing the String to be used as a delimiter, + * {@code null} splits on whitespace + * @param max the maximum number of elements to include in the returned + * array. A zero or negative value implies no limit. + * @param preserveAllTokens if {@code true}, adjacent separators are + * treated as empty token separators; if {@code false}, adjacent + * separators are treated as one separator. + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.4 + */ + private static String[] splitByWholeSeparatorWorker( + String str, String separator, int max, boolean preserveAllTokens) { + if (str == null) { + return null; + } + + int len = str.length(); + + if (len == 0) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + + if (separator == null || EMPTY.equals(separator)) { + // Split on whitespace. + return splitWorker(str, null, max, preserveAllTokens); + } + + int separatorLength = separator.length(); + + ArrayList substrings = new ArrayList(); + int numberOfSubstrings = 0; + int beg = 0; + int end = 0; + while (end < len) { + end = str.indexOf(separator, beg); + + if (end > -1) { + if (end > beg) { + numberOfSubstrings += 1; + + if (numberOfSubstrings == max) { + end = len; + substrings.add(str.substring(beg)); + } else { + // The following is OK, because String.substring( beg, end ) excludes + // the character at the position 'end'. + substrings.add(str.substring(beg, end)); + + // Set the starting point for the next search. + // The following is equivalent to beg = end + (separatorLength - 1) + 1, + // which is the right calculation: + beg = end + separatorLength; + } + } else { + // We found a consecutive occurrence of the separator, so skip it. + if (preserveAllTokens) { + numberOfSubstrings += 1; + if (numberOfSubstrings == max) { + end = len; + substrings.add(str.substring(beg)); + } else { + substrings.add(EMPTY); + } + } + beg = end + separatorLength; + } + } else { + // String.substring( beg ) goes from 'beg' to the end of the String. + substrings.add(str.substring(beg)); + end = len; + } + } + + return substrings.toArray(new String[substrings.size()]); + } + + // ----------------------------------------------------------------------- + /** + *

Splits the provided text into an array, using whitespace as the + * separator, preserving all tokens, including empty tokens created by + * adjacent separators. This is an alternative to using StringTokenizer. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * For more control over the split use the StrTokenizer class.

+ * + *

A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.splitPreserveAllTokens(null)       = null
+     * StringUtils.splitPreserveAllTokens("")         = []
+     * StringUtils.splitPreserveAllTokens("abc def")  = ["abc", "def"]
+     * StringUtils.splitPreserveAllTokens("abc  def") = ["abc", "", "def"]
+     * StringUtils.splitPreserveAllTokens(" abc ")    = ["", "abc", ""]
+     * 
+ * + * @param str the String to parse, may be {@code null} + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.1 + */ + public static String[] splitPreserveAllTokens(String str) { + return splitWorker(str, null, -1, true); + } + + /** + *

Splits the provided text into an array, separator specified, + * preserving all tokens, including empty tokens created by adjacent + * separators. This is an alternative to using StringTokenizer.

+ * + *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * For more control over the split use the StrTokenizer class.

+ * + *

A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.splitPreserveAllTokens(null, *)         = null
+     * StringUtils.splitPreserveAllTokens("", *)           = []
+     * StringUtils.splitPreserveAllTokens("a.b.c", '.')    = ["a", "b", "c"]
+     * StringUtils.splitPreserveAllTokens("a..b.c", '.')   = ["a", "", "b", "c"]
+     * StringUtils.splitPreserveAllTokens("a:b:c", '.')    = ["a:b:c"]
+     * StringUtils.splitPreserveAllTokens("a\tb\nc", null) = ["a", "b", "c"]
+     * StringUtils.splitPreserveAllTokens("a b c", ' ')    = ["a", "b", "c"]
+     * StringUtils.splitPreserveAllTokens("a b c ", ' ')   = ["a", "b", "c", ""]
+     * StringUtils.splitPreserveAllTokens("a b c  ", ' ')   = ["a", "b", "c", "", ""]
+     * StringUtils.splitPreserveAllTokens(" a b c", ' ')   = ["", a", "b", "c"]
+     * StringUtils.splitPreserveAllTokens("  a b c", ' ')  = ["", "", a", "b", "c"]
+     * StringUtils.splitPreserveAllTokens(" a b c ", ' ')  = ["", a", "b", "c", ""]
+     * 
+ * + * @param str the String to parse, may be {@code null} + * @param separatorChar the character used as the delimiter, + * {@code null} splits on whitespace + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.1 + */ + public static String[] splitPreserveAllTokens(String str, char separatorChar) { + return splitWorker(str, separatorChar, true); + } + + /** + * Performs the logic for the {@code split} and + * {@code splitPreserveAllTokens} methods that do not return a + * maximum array length. + * + * @param str the String to parse, may be {@code null} + * @param separatorChar the separate character + * @param preserveAllTokens if {@code true}, adjacent separators are + * treated as empty token separators; if {@code false}, adjacent + * separators are treated as one separator. + * @return an array of parsed Strings, {@code null} if null String input + */ + private static String[] splitWorker(String str, char separatorChar, boolean preserveAllTokens) { + // Performance tuned for 2.0 (JDK1.4) + + if (str == null) { + return null; + } + int len = str.length(); + if (len == 0) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + List list = new ArrayList(); + int i = 0, start = 0; + boolean match = false; + boolean lastMatch = false; + while (i < len) { + if (str.charAt(i) == separatorChar) { + if (match || preserveAllTokens) { + list.add(str.substring(start, i)); + match = false; + lastMatch = true; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + if (match || preserveAllTokens && lastMatch) { + list.add(str.substring(start, i)); + } + return list.toArray(new String[list.size()]); + } + + /** + *

Splits the provided text into an array, separators specified, + * preserving all tokens, including empty tokens created by adjacent + * separators. This is an alternative to using StringTokenizer.

+ * + *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * For more control over the split use the StrTokenizer class.

+ * + *

A {@code null} input String returns {@code null}. + * A {@code null} separatorChars splits on whitespace.

+ * + *
+     * StringUtils.splitPreserveAllTokens(null, *)           = null
+     * StringUtils.splitPreserveAllTokens("", *)             = []
+     * StringUtils.splitPreserveAllTokens("abc def", null)   = ["abc", "def"]
+     * StringUtils.splitPreserveAllTokens("abc def", " ")    = ["abc", "def"]
+     * StringUtils.splitPreserveAllTokens("abc  def", " ")   = ["abc", "", def"]
+     * StringUtils.splitPreserveAllTokens("ab:cd:ef", ":")   = ["ab", "cd", "ef"]
+     * StringUtils.splitPreserveAllTokens("ab:cd:ef:", ":")  = ["ab", "cd", "ef", ""]
+     * StringUtils.splitPreserveAllTokens("ab:cd:ef::", ":") = ["ab", "cd", "ef", "", ""]
+     * StringUtils.splitPreserveAllTokens("ab::cd:ef", ":")  = ["ab", "", cd", "ef"]
+     * StringUtils.splitPreserveAllTokens(":cd:ef", ":")     = ["", cd", "ef"]
+     * StringUtils.splitPreserveAllTokens("::cd:ef", ":")    = ["", "", cd", "ef"]
+     * StringUtils.splitPreserveAllTokens(":cd:ef:", ":")    = ["", cd", "ef", ""]
+     * 
+ * + * @param str the String to parse, may be {@code null} + * @param separatorChars the characters used as the delimiters, + * {@code null} splits on whitespace + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.1 + */ + public static String[] splitPreserveAllTokens(String str, String separatorChars) { + return splitWorker(str, separatorChars, -1, true); + } + + /** + *

Splits the provided text into an array with a maximum length, + * separators specified, preserving all tokens, including empty tokens + * created by adjacent separators.

+ * + *

The separator is not included in the returned String array. + * Adjacent separators are treated as separators for empty tokens. + * Adjacent separators are treated as one separator.

+ * + *

A {@code null} input String returns {@code null}. + * A {@code null} separatorChars splits on whitespace.

+ * + *

If more than {@code max} delimited substrings are found, the last + * returned string includes all characters after the first {@code max - 1} + * returned strings (including separator characters).

+ * + *
+     * StringUtils.splitPreserveAllTokens(null, *, *)            = null
+     * StringUtils.splitPreserveAllTokens("", *, *)              = []
+     * StringUtils.splitPreserveAllTokens("ab de fg", null, 0)   = ["ab", "cd", "ef"]
+     * StringUtils.splitPreserveAllTokens("ab   de fg", null, 0) = ["ab", "cd", "ef"]
+     * StringUtils.splitPreserveAllTokens("ab:cd:ef", ":", 0)    = ["ab", "cd", "ef"]
+     * StringUtils.splitPreserveAllTokens("ab:cd:ef", ":", 2)    = ["ab", "cd:ef"]
+     * StringUtils.splitPreserveAllTokens("ab   de fg", null, 2) = ["ab", "  de fg"]
+     * StringUtils.splitPreserveAllTokens("ab   de fg", null, 3) = ["ab", "", " de fg"]
+     * StringUtils.splitPreserveAllTokens("ab   de fg", null, 4) = ["ab", "", "", "de fg"]
+     * 
+ * + * @param str the String to parse, may be {@code null} + * @param separatorChars the characters used as the delimiters, + * {@code null} splits on whitespace + * @param max the maximum number of elements to include in the + * array. A zero or negative value implies no limit + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.1 + */ + public static String[] splitPreserveAllTokens(String str, String separatorChars, int max) { + return splitWorker(str, separatorChars, max, true); + } + + /** + * Performs the logic for the {@code split} and + * {@code splitPreserveAllTokens} methods that return a maximum array + * length. + * + * @param str the String to parse, may be {@code null} + * @param separatorChars the separate character + * @param max the maximum number of elements to include in the + * array. A zero or negative value implies no limit. + * @param preserveAllTokens if {@code true}, adjacent separators are + * treated as empty token separators; if {@code false}, adjacent + * separators are treated as one separator. + * @return an array of parsed Strings, {@code null} if null String input + */ + private static String[] splitWorker(String str, String separatorChars, int max, boolean preserveAllTokens) { + // Performance tuned for 2.0 (JDK1.4) + // Direct code is quicker than StringTokenizer. + // Also, StringTokenizer uses isSpace() not isWhitespace() + + if (str == null) { + return null; + } + int len = str.length(); + if (len == 0) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + List list = new ArrayList(); + int sizePlus1 = 1; + int i = 0, start = 0; + boolean match = false; + boolean lastMatch = false; + if (separatorChars == null) { + // Null separator means use whitespace + while (i < len) { + if (Character.isWhitespace(str.charAt(i))) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } else if (separatorChars.length() == 1) { + // Optimise 1 character case + char sep = separatorChars.charAt(0); + while (i < len) { + if (str.charAt(i) == sep) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } else { + // standard case + while (i < len) { + if (separatorChars.indexOf(str.charAt(i)) >= 0) { + if (match || preserveAllTokens) { + lastMatch = true; + if (sizePlus1++ == max) { + i = len; + lastMatch = false; + } + list.add(str.substring(start, i)); + match = false; + } + start = ++i; + continue; + } + lastMatch = false; + match = true; + i++; + } + } + if (match || preserveAllTokens && lastMatch) { + list.add(str.substring(start, i)); + } + return list.toArray(new String[list.size()]); + } + + /** + *

Splits a String by Character type as returned by + * {@code java.lang.Character.getType(char)}. Groups of contiguous + * characters of the same type are returned as complete tokens. + *

+     * StringUtils.splitByCharacterType(null)         = null
+     * StringUtils.splitByCharacterType("")           = []
+     * StringUtils.splitByCharacterType("ab de fg")   = ["ab", " ", "de", " ", "fg"]
+     * StringUtils.splitByCharacterType("ab   de fg") = ["ab", "   ", "de", " ", "fg"]
+     * StringUtils.splitByCharacterType("ab:cd:ef")   = ["ab", ":", "cd", ":", "ef"]
+     * StringUtils.splitByCharacterType("number5")    = ["number", "5"]
+     * StringUtils.splitByCharacterType("fooBar")     = ["foo", "B", "ar"]
+     * StringUtils.splitByCharacterType("foo200Bar")  = ["foo", "200", "B", "ar"]
+     * StringUtils.splitByCharacterType("ASFRules")   = ["ASFR", "ules"]
+     * 
+ * @param str the String to split, may be {@code null} + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.4 + */ + public static String[] splitByCharacterType(String str) { + return splitByCharacterType(str, false); + } + + /** + *

Splits a String by Character type as returned by + * {@code java.lang.Character.getType(char)}. Groups of contiguous + * characters of the same type are returned as complete tokens, with the + * following exception: the character of type + * {@code Character.UPPERCASE_LETTER}, if any, immediately + * preceding a token of type {@code Character.LOWERCASE_LETTER} + * will belong to the following token rather than to the preceding, if any, + * {@code Character.UPPERCASE_LETTER} token. + *

+     * StringUtils.splitByCharacterTypeCamelCase(null)         = null
+     * StringUtils.splitByCharacterTypeCamelCase("")           = []
+     * StringUtils.splitByCharacterTypeCamelCase("ab de fg")   = ["ab", " ", "de", " ", "fg"]
+     * StringUtils.splitByCharacterTypeCamelCase("ab   de fg") = ["ab", "   ", "de", " ", "fg"]
+     * StringUtils.splitByCharacterTypeCamelCase("ab:cd:ef")   = ["ab", ":", "cd", ":", "ef"]
+     * StringUtils.splitByCharacterTypeCamelCase("number5")    = ["number", "5"]
+     * StringUtils.splitByCharacterTypeCamelCase("fooBar")     = ["foo", "Bar"]
+     * StringUtils.splitByCharacterTypeCamelCase("foo200Bar")  = ["foo", "200", "Bar"]
+     * StringUtils.splitByCharacterTypeCamelCase("ASFRules")   = ["ASF", "Rules"]
+     * 
+ * @param str the String to split, may be {@code null} + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.4 + */ + public static String[] splitByCharacterTypeCamelCase(String str) { + return splitByCharacterType(str, true); + } + + /** + *

Splits a String by Character type as returned by + * {@code java.lang.Character.getType(char)}. Groups of contiguous + * characters of the same type are returned as complete tokens, with the + * following exception: if {@code camelCase} is {@code true}, + * the character of type {@code Character.UPPERCASE_LETTER}, if any, + * immediately preceding a token of type {@code Character.LOWERCASE_LETTER} + * will belong to the following token rather than to the preceding, if any, + * {@code Character.UPPERCASE_LETTER} token. + * @param str the String to split, may be {@code null} + * @param camelCase whether to use so-called "camel-case" for letter types + * @return an array of parsed Strings, {@code null} if null String input + * @since 2.4 + */ + private static String[] splitByCharacterType(String str, boolean camelCase) { + if (str == null) { + return null; + } + if (str.length() == 0) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + char[] c = str.toCharArray(); + List list = new ArrayList(); + int tokenStart = 0; + int currentType = Character.getType(c[tokenStart]); + for (int pos = tokenStart + 1; pos < c.length; pos++) { + int type = Character.getType(c[pos]); + if (type == currentType) { + continue; + } + if (camelCase && type == Character.LOWERCASE_LETTER && currentType == Character.UPPERCASE_LETTER) { + int newTokenStart = pos - 1; + if (newTokenStart != tokenStart) { + list.add(new String(c, tokenStart, newTokenStart - tokenStart)); + tokenStart = newTokenStart; + } + } else { + list.add(new String(c, tokenStart, pos - tokenStart)); + tokenStart = pos; + } + currentType = type; + } + list.add(new String(c, tokenStart, c.length - tokenStart)); + return list.toArray(new String[list.size()]); + } + + // Joining + //----------------------------------------------------------------------- + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ * + *

No separator is added to the joined String. + * Null objects or empty strings within the array are represented by + * empty strings.

+ * + *
+     * StringUtils.join(null)            = null
+     * StringUtils.join([])              = ""
+     * StringUtils.join([null])          = ""
+     * StringUtils.join(["a", "b", "c"]) = "abc"
+     * StringUtils.join([null, "", "a"]) = "a"
+     * 
+ * + * @param the specific type of values to join together + * @param elements the values to join together, may be null + * @return the joined String, {@code null} if null array input + * @since 2.0 + * @since 3.0 Changed signature to use varargs + */ + public static String join(T... elements) { + return join(elements, null); + } + + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ * + *

No delimiter is added before or after the list. + * Null objects or empty strings within the array are represented by + * empty strings.

+ * + *
+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join(["a", "b", "c"], ';')  = "a;b;c"
+     * StringUtils.join(["a", "b", "c"], null) = "abc"
+     * StringUtils.join([null, "", "a"], ';')  = ";;a"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null array input + * @since 2.0 + */ + public static String join(Object[] array, char separator) { + if (array == null) { + return null; + } + + return join(array, separator, 0, array.length); + } + + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ * + *

No delimiter is added before or after the list. + * Null objects or empty strings within the array are represented by + * empty strings.

+ * + *
+     * StringUtils.join(null, *)               = null
+     * StringUtils.join([], *)                 = ""
+     * StringUtils.join([null], *)             = ""
+     * StringUtils.join(["a", "b", "c"], ';')  = "a;b;c"
+     * StringUtils.join(["a", "b", "c"], null) = "abc"
+     * StringUtils.join([null, "", "a"], ';')  = ";;a"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use + * @param startIndex the first index to start joining from. It is + * an error to pass in an end index past the end of the array + * @param endIndex the index to stop joining from (exclusive). It is + * an error to pass in an end index past the end of the array + * @return the joined String, {@code null} if null array input + * @since 2.0 + */ + public static String join(Object[] array, char separator, int startIndex, int endIndex) { + if (array == null) { + return null; + } + int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + + StringBuilder buf = new StringBuilder(noOfItems * 16); + + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + if (array[i] != null) { + buf.append(array[i]); + } + } + return buf.toString(); + } + + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ * + *

No delimiter is added before or after the list. + * A {@code null} separator is the same as an empty String (""). + * Null objects or empty strings within the array are represented by + * empty strings.

+ * + *
+     * StringUtils.join(null, *)                = null
+     * StringUtils.join([], *)                  = ""
+     * StringUtils.join([null], *)              = ""
+     * StringUtils.join(["a", "b", "c"], "--")  = "a--b--c"
+     * StringUtils.join(["a", "b", "c"], null)  = "abc"
+     * StringUtils.join(["a", "b", "c"], "")    = "abc"
+     * StringUtils.join([null, "", "a"], ',')   = ",,a"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use, null treated as "" + * @return the joined String, {@code null} if null array input + */ + public static String join(Object[] array, String separator) { + if (array == null) { + return null; + } + return join(array, separator, 0, array.length); + } + + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ * + *

No delimiter is added before or after the list. + * A {@code null} separator is the same as an empty String (""). + * Null objects or empty strings within the array are represented by + * empty strings.

+ * + *
+     * StringUtils.join(null, *)                = null
+     * StringUtils.join([], *)                  = ""
+     * StringUtils.join([null], *)              = ""
+     * StringUtils.join(["a", "b", "c"], "--")  = "a--b--c"
+     * StringUtils.join(["a", "b", "c"], null)  = "abc"
+     * StringUtils.join(["a", "b", "c"], "")    = "abc"
+     * StringUtils.join([null, "", "a"], ',')   = ",,a"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param separator the separator character to use, null treated as "" + * @param startIndex the first index to start joining from. It is + * an error to pass in an end index past the end of the array + * @param endIndex the index to stop joining from (exclusive). It is + * an error to pass in an end index past the end of the array + * @return the joined String, {@code null} if null array input + */ + public static String join(Object[] array, String separator, int startIndex, int endIndex) { + if (array == null) { + return null; + } + if (separator == null) { + separator = EMPTY; + } + + // endIndex - startIndex > 0: Len = NofStrings *(len(firstString) + len(separator)) + // (Assuming that all Strings are roughly equally long) + int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + + StringBuilder buf = new StringBuilder(noOfItems * 16); + + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + if (array[i] != null) { + buf.append(array[i]); + } + } + return buf.toString(); + } + + /** + *

Joins the elements of the provided {@code Iterator} into + * a single String containing the provided elements.

+ * + *

No delimiter is added before or after the list. Null objects or empty + * strings within the iteration are represented by empty strings.

+ * + *

See the examples here: {@link #join(Object[],char)}.

+ * + * @param iterator the {@code Iterator} of values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null iterator input + * @since 2.0 + */ + public static String join(Iterator iterator, char separator) { + + // handle null, zero and one elements before building a buffer + if (iterator == null) { + return null; + } + if (!iterator.hasNext()) { + return EMPTY; + } + Object first = iterator.next(); + if (!iterator.hasNext()) { + return ObjectUtils.toString(first); + } + + // two or more elements + StringBuilder buf = new StringBuilder(256); // Java default is 16, probably too small + if (first != null) { + buf.append(first); + } + + while (iterator.hasNext()) { + buf.append(separator); + Object obj = iterator.next(); + if (obj != null) { + buf.append(obj); + } + } + + return buf.toString(); + } + + /** + *

Joins the elements of the provided {@code Iterator} into + * a single String containing the provided elements.

+ * + *

No delimiter is added before or after the list. + * A {@code null} separator is the same as an empty String ("").

+ * + *

See the examples here: {@link #join(Object[],String)}.

+ * + * @param iterator the {@code Iterator} of values to join together, may be null + * @param separator the separator character to use, null treated as "" + * @return the joined String, {@code null} if null iterator input + */ + public static String join(Iterator iterator, String separator) { + + // handle null, zero and one elements before building a buffer + if (iterator == null) { + return null; + } + if (!iterator.hasNext()) { + return EMPTY; + } + Object first = iterator.next(); + if (!iterator.hasNext()) { + return ObjectUtils.toString(first); + } + + // two or more elements + StringBuilder buf = new StringBuilder(256); // Java default is 16, probably too small + if (first != null) { + buf.append(first); + } + + while (iterator.hasNext()) { + if (separator != null) { + buf.append(separator); + } + Object obj = iterator.next(); + if (obj != null) { + buf.append(obj); + } + } + return buf.toString(); + } + + /** + *

Joins the elements of the provided {@code Iterable} into + * a single String containing the provided elements.

+ * + *

No delimiter is added before or after the list. Null objects or empty + * strings within the iteration are represented by empty strings.

+ * + *

See the examples here: {@link #join(Object[],char)}.

+ * + * @param iterable the {@code Iterable} providing the values to join together, may be null + * @param separator the separator character to use + * @return the joined String, {@code null} if null iterator input + * @since 2.3 + */ + public static String join(Iterable iterable, char separator) { + if (iterable == null) { + return null; + } + return join(iterable.iterator(), separator); + } + + /** + *

Joins the elements of the provided {@code Iterable} into + * a single String containing the provided elements.

+ * + *

No delimiter is added before or after the list. + * A {@code null} separator is the same as an empty String ("").

+ * + *

See the examples here: {@link #join(Object[],String)}.

+ * + * @param iterable the {@code Iterable} providing the values to join together, may be null + * @param separator the separator character to use, null treated as "" + * @return the joined String, {@code null} if null iterator input + * @since 2.3 + */ + public static String join(Iterable iterable, String separator) { + if (iterable == null) { + return null; + } + return join(iterable.iterator(), separator); + } + + // Delete + //----------------------------------------------------------------------- + /** + *

Deletes all whitespaces from a String as defined by + * {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.deleteWhitespace(null)         = null
+     * StringUtils.deleteWhitespace("")           = ""
+     * StringUtils.deleteWhitespace("abc")        = "abc"
+     * StringUtils.deleteWhitespace("   ab  c  ") = "abc"
+     * 
+ * + * @param str the String to delete whitespace from, may be null + * @return the String without whitespaces, {@code null} if null String input + */ + public static String deleteWhitespace(String str) { + if (isEmpty(str)) { + return str; + } + int sz = str.length(); + char[] chs = new char[sz]; + int count = 0; + for (int i = 0; i < sz; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + chs[count++] = str.charAt(i); + } + } + if (count == sz) { + return str; + } + return new String(chs, 0, count); + } + + // Remove + //----------------------------------------------------------------------- + /** + *

Removes a substring only if it is at the beginning of a source string, + * otherwise returns the source string.

+ * + *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string. + * A {@code null} search string will return the source string.

+ * + *
+     * StringUtils.removeStart(null, *)      = null
+     * StringUtils.removeStart("", *)        = ""
+     * StringUtils.removeStart(*, null)      = *
+     * StringUtils.removeStart("www.domain.com", "www.")   = "domain.com"
+     * StringUtils.removeStart("domain.com", "www.")       = "domain.com"
+     * StringUtils.removeStart("www.domain.com", "domain") = "www.domain.com"
+     * StringUtils.removeStart("abc", "")    = "abc"
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the String to search for and remove, may be null + * @return the substring with the string removed if found, + * {@code null} if null String input + * @since 2.1 + */ + public static String removeStart(String str, String remove) { + if (isEmpty(str) || isEmpty(remove)) { + return str; + } + if (str.startsWith(remove)){ + return str.substring(remove.length()); + } + return str; + } + + /** + *

Case insensitive removal of a substring if it is at the beginning of a source string, + * otherwise returns the source string.

+ * + *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string. + * A {@code null} search string will return the source string.

+ * + *
+     * StringUtils.removeStartIgnoreCase(null, *)      = null
+     * StringUtils.removeStartIgnoreCase("", *)        = ""
+     * StringUtils.removeStartIgnoreCase(*, null)      = *
+     * StringUtils.removeStartIgnoreCase("www.domain.com", "www.")   = "domain.com"
+     * StringUtils.removeStartIgnoreCase("www.domain.com", "WWW.")   = "domain.com"
+     * StringUtils.removeStartIgnoreCase("domain.com", "www.")       = "domain.com"
+     * StringUtils.removeStartIgnoreCase("www.domain.com", "domain") = "www.domain.com"
+     * StringUtils.removeStartIgnoreCase("abc", "")    = "abc"
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the String to search for (case insensitive) and remove, may be null + * @return the substring with the string removed if found, + * {@code null} if null String input + * @since 2.4 + */ + public static String removeStartIgnoreCase(String str, String remove) { + if (isEmpty(str) || isEmpty(remove)) { + return str; + } + if (startsWithIgnoreCase(str, remove)) { + return str.substring(remove.length()); + } + return str; + } + + /** + *

Removes a substring only if it is at the end of a source string, + * otherwise returns the source string.

+ * + *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string. + * A {@code null} search string will return the source string.

+ * + *
+     * StringUtils.removeEnd(null, *)      = null
+     * StringUtils.removeEnd("", *)        = ""
+     * StringUtils.removeEnd(*, null)      = *
+     * StringUtils.removeEnd("www.domain.com", ".com.")  = "www.domain.com"
+     * StringUtils.removeEnd("www.domain.com", ".com")   = "www.domain"
+     * StringUtils.removeEnd("www.domain.com", "domain") = "www.domain.com"
+     * StringUtils.removeEnd("abc", "")    = "abc"
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the String to search for and remove, may be null + * @return the substring with the string removed if found, + * {@code null} if null String input + * @since 2.1 + */ + public static String removeEnd(String str, String remove) { + if (isEmpty(str) || isEmpty(remove)) { + return str; + } + if (str.endsWith(remove)) { + return str.substring(0, str.length() - remove.length()); + } + return str; + } + + /** + *

Case insensitive removal of a substring if it is at the end of a source string, + * otherwise returns the source string.

+ * + *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string. + * A {@code null} search string will return the source string.

+ * + *
+     * StringUtils.removeEndIgnoreCase(null, *)      = null
+     * StringUtils.removeEndIgnoreCase("", *)        = ""
+     * StringUtils.removeEndIgnoreCase(*, null)      = *
+     * StringUtils.removeEndIgnoreCase("www.domain.com", ".com.")  = "www.domain.com"
+     * StringUtils.removeEndIgnoreCase("www.domain.com", ".com")   = "www.domain"
+     * StringUtils.removeEndIgnoreCase("www.domain.com", "domain") = "www.domain.com"
+     * StringUtils.removeEndIgnoreCase("abc", "")    = "abc"
+     * StringUtils.removeEndIgnoreCase("www.domain.com", ".COM") = "www.domain")
+     * StringUtils.removeEndIgnoreCase("www.domain.COM", ".com") = "www.domain")
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the String to search for (case insensitive) and remove, may be null + * @return the substring with the string removed if found, + * {@code null} if null String input + * @since 2.4 + */ + public static String removeEndIgnoreCase(String str, String remove) { + if (isEmpty(str) || isEmpty(remove)) { + return str; + } + if (endsWithIgnoreCase(str, remove)) { + return str.substring(0, str.length() - remove.length()); + } + return str; + } + + /** + *

Removes all occurrences of a substring from within the source string.

+ * + *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string. + * A {@code null} remove string will return the source string. + * An empty ("") remove string will return the source string.

+ * + *
+     * StringUtils.remove(null, *)        = null
+     * StringUtils.remove("", *)          = ""
+     * StringUtils.remove(*, null)        = *
+     * StringUtils.remove(*, "")          = *
+     * StringUtils.remove("queued", "ue") = "qd"
+     * StringUtils.remove("queued", "zz") = "queued"
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the String to search for and remove, may be null + * @return the substring with the string removed if found, + * {@code null} if null String input + * @since 2.1 + */ + public static String remove(String str, String remove) { + if (isEmpty(str) || isEmpty(remove)) { + return str; + } + return replace(str, remove, EMPTY, -1); + } + + /** + *

Removes all occurrences of a character from within the source string.

+ * + *

A {@code null} source string will return {@code null}. + * An empty ("") source string will return the empty string.

+ * + *
+     * StringUtils.remove(null, *)       = null
+     * StringUtils.remove("", *)         = ""
+     * StringUtils.remove("queued", 'u') = "qeed"
+     * StringUtils.remove("queued", 'z') = "queued"
+     * 
+ * + * @param str the source String to search, may be null + * @param remove the char to search for and remove, may be null + * @return the substring with the char removed if found, + * {@code null} if null String input + * @since 2.1 + */ + public static String remove(String str, char remove) { + if (isEmpty(str) || str.indexOf(remove) == INDEX_NOT_FOUND) { + return str; + } + char[] chars = str.toCharArray(); + int pos = 0; + for (int i = 0; i < chars.length; i++) { + if (chars[i] != remove) { + chars[pos++] = chars[i]; + } + } + return new String(chars, 0, pos); + } + + // Replacing + //----------------------------------------------------------------------- + /** + *

Replaces a String with another String inside a larger String, once.

+ * + *

A {@code null} reference passed to this method is a no-op.

+ * + *
+     * StringUtils.replaceOnce(null, *, *)        = null
+     * StringUtils.replaceOnce("", *, *)          = ""
+     * StringUtils.replaceOnce("any", null, *)    = "any"
+     * StringUtils.replaceOnce("any", *, null)    = "any"
+     * StringUtils.replaceOnce("any", "", *)      = "any"
+     * StringUtils.replaceOnce("aba", "a", null)  = "aba"
+     * StringUtils.replaceOnce("aba", "a", "")    = "ba"
+     * StringUtils.replaceOnce("aba", "a", "z")   = "zba"
+     * 
+ * + * @see #replace(String text, String searchString, String replacement, int max) + * @param text text to search and replace in, may be null + * @param searchString the String to search for, may be null + * @param replacement the String to replace with, may be null + * @return the text with any replacements processed, + * {@code null} if null String input + */ + public static String replaceOnce(String text, String searchString, String replacement) { + return replace(text, searchString, replacement, 1); + } + + /** + *

Replaces all occurrences of a String within another String.

+ * + *

A {@code null} reference passed to this method is a no-op.

+ * + *
+     * StringUtils.replace(null, *, *)        = null
+     * StringUtils.replace("", *, *)          = ""
+     * StringUtils.replace("any", null, *)    = "any"
+     * StringUtils.replace("any", *, null)    = "any"
+     * StringUtils.replace("any", "", *)      = "any"
+     * StringUtils.replace("aba", "a", null)  = "aba"
+     * StringUtils.replace("aba", "a", "")    = "b"
+     * StringUtils.replace("aba", "a", "z")   = "zbz"
+     * 
+ * + * @see #replace(String text, String searchString, String replacement, int max) + * @param text text to search and replace in, may be null + * @param searchString the String to search for, may be null + * @param replacement the String to replace it with, may be null + * @return the text with any replacements processed, + * {@code null} if null String input + */ + public static String replace(String text, String searchString, String replacement) { + return replace(text, searchString, replacement, -1); + } + + /** + *

Replaces a String with another String inside a larger String, + * for the first {@code max} values of the search String.

+ * + *

A {@code null} reference passed to this method is a no-op.

+ * + *
+     * StringUtils.replace(null, *, *, *)         = null
+     * StringUtils.replace("", *, *, *)           = ""
+     * StringUtils.replace("any", null, *, *)     = "any"
+     * StringUtils.replace("any", *, null, *)     = "any"
+     * StringUtils.replace("any", "", *, *)       = "any"
+     * StringUtils.replace("any", *, *, 0)        = "any"
+     * StringUtils.replace("abaa", "a", null, -1) = "abaa"
+     * StringUtils.replace("abaa", "a", "", -1)   = "b"
+     * StringUtils.replace("abaa", "a", "z", 0)   = "abaa"
+     * StringUtils.replace("abaa", "a", "z", 1)   = "zbaa"
+     * StringUtils.replace("abaa", "a", "z", 2)   = "zbza"
+     * StringUtils.replace("abaa", "a", "z", -1)  = "zbzz"
+     * 
+ * + * @param text text to search and replace in, may be null + * @param searchString the String to search for, may be null + * @param replacement the String to replace it with, may be null + * @param max maximum number of values to replace, or {@code -1} if no maximum + * @return the text with any replacements processed, + * {@code null} if null String input + */ + public static String replace(String text, String searchString, String replacement, int max) { + if (isEmpty(text) || isEmpty(searchString) || replacement == null || max == 0) { + return text; + } + int start = 0; + int end = text.indexOf(searchString, start); + if (end == INDEX_NOT_FOUND) { + return text; + } + int replLength = searchString.length(); + int increase = replacement.length() - replLength; + increase = increase < 0 ? 0 : increase; + increase *= max < 0 ? 16 : max > 64 ? 64 : max; + StringBuilder buf = new StringBuilder(text.length() + increase); + while (end != INDEX_NOT_FOUND) { + buf.append(text.substring(start, end)).append(replacement); + start = end + replLength; + if (--max == 0) { + break; + } + end = text.indexOf(searchString, start); + } + buf.append(text.substring(start)); + return buf.toString(); + } + + /** + *

+ * Replaces all occurrences of Strings within another String. + *

+ * + *

+ * A {@code null} reference passed to this method is a no-op, or if + * any "search string" or "string to replace" is null, that replace will be + * ignored. This will not repeat. For repeating replaces, call the + * overloaded method. + *

+ * + *
+     *  StringUtils.replaceEach(null, *, *)        = null
+     *  StringUtils.replaceEach("", *, *)          = ""
+     *  StringUtils.replaceEach("aba", null, null) = "aba"
+     *  StringUtils.replaceEach("aba", new String[0], null) = "aba"
+     *  StringUtils.replaceEach("aba", null, new String[0]) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, null)  = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""})  = "b"
+     *  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"})  = "aba"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})  = "wcte"
+     *  (example of how it does not repeat)
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"})  = "dcte"
+     * 
+ * + * @param text + * text to search and replace in, no-op if null + * @param searchList + * the Strings to search for, no-op if null + * @param replacementList + * the Strings to replace them with, no-op if null + * @return the text with any replacements processed, {@code null} if + * null String input + * @throws IllegalArgumentException + * if the lengths of the arrays are not the same (null is ok, + * and/or size 0) + * @since 2.4 + */ + public static String replaceEach(String text, String[] searchList, String[] replacementList) { + return replaceEach(text, searchList, replacementList, false, 0); + } + + /** + *

+ * Replaces all occurrences of Strings within another String. + *

+ * + *

+ * A {@code null} reference passed to this method is a no-op, or if + * any "search string" or "string to replace" is null, that replace will be + * ignored. + *

+ * + *
+     *  StringUtils.replaceEach(null, *, *, *) = null
+     *  StringUtils.replaceEach("", *, *, *) = ""
+     *  StringUtils.replaceEach("aba", null, null, *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[0], null, *) = "aba"
+     *  StringUtils.replaceEach("aba", null, new String[0], *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, null, *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""}, *) = "b"
+     *  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"}, *) = "aba"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}, *) = "wcte"
+     *  (example of how it repeats)
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, false) = "dcte"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, true) = "tcte"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, true) = IllegalStateException
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, false) = "dcabe"
+     * 
+ * + * @param text + * text to search and replace in, no-op if null + * @param searchList + * the Strings to search for, no-op if null + * @param replacementList + * the Strings to replace them with, no-op if null + * @return the text with any replacements processed, {@code null} if + * null String input + * @throws IllegalStateException + * if the search is repeating and there is an endless loop due + * to outputs of one being inputs to another + * @throws IllegalArgumentException + * if the lengths of the arrays are not the same (null is ok, + * and/or size 0) + * @since 2.4 + */ + public static String replaceEachRepeatedly(String text, String[] searchList, String[] replacementList) { + // timeToLive should be 0 if not used or nothing to replace, else it's + // the length of the replace array + int timeToLive = searchList == null ? 0 : searchList.length; + return replaceEach(text, searchList, replacementList, true, timeToLive); + } + + /** + *

+ * Replaces all occurrences of Strings within another String. + *

+ * + *

+ * A {@code null} reference passed to this method is a no-op, or if + * any "search string" or "string to replace" is null, that replace will be + * ignored. + *

+ * + *
+     *  StringUtils.replaceEach(null, *, *, *) = null
+     *  StringUtils.replaceEach("", *, *, *) = ""
+     *  StringUtils.replaceEach("aba", null, null, *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[0], null, *) = "aba"
+     *  StringUtils.replaceEach("aba", null, new String[0], *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, null, *) = "aba"
+     *  StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""}, *) = "b"
+     *  StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"}, *) = "aba"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}, *) = "wcte"
+     *  (example of how it repeats)
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, false) = "dcte"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, true) = "tcte"
+     *  StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, *) = IllegalStateException
+     * 
+ * + * @param text + * text to search and replace in, no-op if null + * @param searchList + * the Strings to search for, no-op if null + * @param replacementList + * the Strings to replace them with, no-op if null + * @param repeat if true, then replace repeatedly + * until there are no more possible replacements or timeToLive < 0 + * @param timeToLive + * if less than 0 then there is a circular reference and endless + * loop + * @return the text with any replacements processed, {@code null} if + * null String input + * @throws IllegalStateException + * if the search is repeating and there is an endless loop due + * to outputs of one being inputs to another + * @throws IllegalArgumentException + * if the lengths of the arrays are not the same (null is ok, + * and/or size 0) + * @since 2.4 + */ + private static String replaceEach( + String text, String[] searchList, String[] replacementList, boolean repeat, int timeToLive) { + + // mchyzer Performance note: This creates very few new objects (one major goal) + // let me know if there are performance requests, we can create a harness to measure + + if (text == null || text.length() == 0 || searchList == null || + searchList.length == 0 || replacementList == null || replacementList.length == 0) { + return text; + } + + // if recursing, this shouldn't be less than 0 + if (timeToLive < 0) { + throw new IllegalStateException("Aborting to protect against StackOverflowError - " + + "output of one loop is the input of another"); + } + + int searchLength = searchList.length; + int replacementLength = replacementList.length; + + // make sure lengths are ok, these need to be equal + if (searchLength != replacementLength) { + throw new IllegalArgumentException("Search and Replace array lengths don't match: " + + searchLength + + " vs " + + replacementLength); + } + + // keep track of which still have matches + boolean[] noMoreMatchesForReplIndex = new boolean[searchLength]; + + // index on index that the match was found + int textIndex = -1; + int replaceIndex = -1; + int tempIndex = -1; + + // index of replace array that will replace the search string found + // NOTE: logic duplicated below START + for (int i = 0; i < searchLength; i++) { + if (noMoreMatchesForReplIndex[i] || searchList[i] == null || + searchList[i].length() == 0 || replacementList[i] == null) { + continue; + } + tempIndex = text.indexOf(searchList[i]); + + // see if we need to keep searching for this + if (tempIndex == -1) { + noMoreMatchesForReplIndex[i] = true; + } else { + if (textIndex == -1 || tempIndex < textIndex) { + textIndex = tempIndex; + replaceIndex = i; + } + } + } + // NOTE: logic mostly below END + + // no search strings found, we are done + if (textIndex == -1) { + return text; + } + + int start = 0; + + // get a good guess on the size of the result buffer so it doesn't have to double if it goes over a bit + int increase = 0; + + // count the replacement text elements that are larger than their corresponding text being replaced + for (int i = 0; i < searchList.length; i++) { + if (searchList[i] == null || replacementList[i] == null) { + continue; + } + int greater = replacementList[i].length() - searchList[i].length(); + if (greater > 0) { + increase += 3 * greater; // assume 3 matches + } + } + // have upper-bound at 20% increase, then let Java take over + increase = Math.min(increase, text.length() / 5); + + StringBuilder buf = new StringBuilder(text.length() + increase); + + while (textIndex != -1) { + + for (int i = start; i < textIndex; i++) { + buf.append(text.charAt(i)); + } + buf.append(replacementList[replaceIndex]); + + start = textIndex + searchList[replaceIndex].length(); + + textIndex = -1; + replaceIndex = -1; + tempIndex = -1; + // find the next earliest match + // NOTE: logic mostly duplicated above START + for (int i = 0; i < searchLength; i++) { + if (noMoreMatchesForReplIndex[i] || searchList[i] == null || + searchList[i].length() == 0 || replacementList[i] == null) { + continue; + } + tempIndex = text.indexOf(searchList[i], start); + + // see if we need to keep searching for this + if (tempIndex == -1) { + noMoreMatchesForReplIndex[i] = true; + } else { + if (textIndex == -1 || tempIndex < textIndex) { + textIndex = tempIndex; + replaceIndex = i; + } + } + } + // NOTE: logic duplicated above END + + } + int textLength = text.length(); + for (int i = start; i < textLength; i++) { + buf.append(text.charAt(i)); + } + String result = buf.toString(); + if (!repeat) { + return result; + } + + return replaceEach(result, searchList, replacementList, repeat, timeToLive - 1); + } + + // Replace, character based + //----------------------------------------------------------------------- + /** + *

Replaces all occurrences of a character in a String with another. + * This is a null-safe version of {@link String#replace(char, char)}.

+ * + *

A {@code null} string input returns {@code null}. + * An empty ("") string input returns an empty string.

+ * + *
+     * StringUtils.replaceChars(null, *, *)        = null
+     * StringUtils.replaceChars("", *, *)          = ""
+     * StringUtils.replaceChars("abcba", 'b', 'y') = "aycya"
+     * StringUtils.replaceChars("abcba", 'z', 'y') = "abcba"
+     * 
+ * + * @param str String to replace characters in, may be null + * @param searchChar the character to search for, may be null + * @param replaceChar the character to replace, may be null + * @return modified String, {@code null} if null string input + * @since 2.0 + */ + public static String replaceChars(String str, char searchChar, char replaceChar) { + if (str == null) { + return null; + } + return str.replace(searchChar, replaceChar); + } + + /** + *

Replaces multiple characters in a String in one go. + * This method can also be used to delete characters.

+ * + *

For example:
+ * replaceChars("hello", "ho", "jy") = jelly.

+ * + *

A {@code null} string input returns {@code null}. + * An empty ("") string input returns an empty string. + * A null or empty set of search characters returns the input string.

+ * + *

The length of the search characters should normally equal the length + * of the replace characters. + * If the search characters is longer, then the extra search characters + * are deleted. + * If the search characters is shorter, then the extra replace characters + * are ignored.

+ * + *
+     * StringUtils.replaceChars(null, *, *)           = null
+     * StringUtils.replaceChars("", *, *)             = ""
+     * StringUtils.replaceChars("abc", null, *)       = "abc"
+     * StringUtils.replaceChars("abc", "", *)         = "abc"
+     * StringUtils.replaceChars("abc", "b", null)     = "ac"
+     * StringUtils.replaceChars("abc", "b", "")       = "ac"
+     * StringUtils.replaceChars("abcba", "bc", "yz")  = "ayzya"
+     * StringUtils.replaceChars("abcba", "bc", "y")   = "ayya"
+     * StringUtils.replaceChars("abcba", "bc", "yzx") = "ayzya"
+     * 
+ * + * @param str String to replace characters in, may be null + * @param searchChars a set of characters to search for, may be null + * @param replaceChars a set of characters to replace, may be null + * @return modified String, {@code null} if null string input + * @since 2.0 + */ + public static String replaceChars(String str, String searchChars, String replaceChars) { + if (isEmpty(str) || isEmpty(searchChars)) { + return str; + } + if (replaceChars == null) { + replaceChars = EMPTY; + } + boolean modified = false; + int replaceCharsLength = replaceChars.length(); + int strLength = str.length(); + StringBuilder buf = new StringBuilder(strLength); + for (int i = 0; i < strLength; i++) { + char ch = str.charAt(i); + int index = searchChars.indexOf(ch); + if (index >= 0) { + modified = true; + if (index < replaceCharsLength) { + buf.append(replaceChars.charAt(index)); + } + } else { + buf.append(ch); + } + } + if (modified) { + return buf.toString(); + } + return str; + } + + // Overlay + //----------------------------------------------------------------------- + /** + *

Overlays part of a String with another String.

+ * + *

A {@code null} string input returns {@code null}. + * A negative index is treated as zero. + * An index greater than the string length is treated as the string length. + * The start index is always the smaller of the two indices.

+ * + *
+     * StringUtils.overlay(null, *, *, *)            = null
+     * StringUtils.overlay("", "abc", 0, 0)          = "abc"
+     * StringUtils.overlay("abcdef", null, 2, 4)     = "abef"
+     * StringUtils.overlay("abcdef", "", 2, 4)       = "abef"
+     * StringUtils.overlay("abcdef", "", 4, 2)       = "abef"
+     * StringUtils.overlay("abcdef", "zzzz", 2, 4)   = "abzzzzef"
+     * StringUtils.overlay("abcdef", "zzzz", 4, 2)   = "abzzzzef"
+     * StringUtils.overlay("abcdef", "zzzz", -1, 4)  = "zzzzef"
+     * StringUtils.overlay("abcdef", "zzzz", 2, 8)   = "abzzzz"
+     * StringUtils.overlay("abcdef", "zzzz", -2, -3) = "zzzzabcdef"
+     * StringUtils.overlay("abcdef", "zzzz", 8, 10)  = "abcdefzzzz"
+     * 
+ * + * @param str the String to do overlaying in, may be null + * @param overlay the String to overlay, may be null + * @param start the position to start overlaying at + * @param end the position to stop overlaying before + * @return overlayed String, {@code null} if null String input + * @since 2.0 + */ + public static String overlay(String str, String overlay, int start, int end) { + if (str == null) { + return null; + } + if (overlay == null) { + overlay = EMPTY; + } + int len = str.length(); + if (start < 0) { + start = 0; + } + if (start > len) { + start = len; + } + if (end < 0) { + end = 0; + } + if (end > len) { + end = len; + } + if (start > end) { + int temp = start; + start = end; + end = temp; + } + return new StringBuilder(len + start - end + overlay.length() + 1) + .append(str.substring(0, start)) + .append(overlay) + .append(str.substring(end)) + .toString(); + } + + // Chomping + //----------------------------------------------------------------------- + /** + *

Removes one newline from end of a String if it's there, + * otherwise leave it alone. A newline is "{@code \n}", + * "{@code \r}", or "{@code \r\n}".

+ * + *

NOTE: This method changed in 2.0. + * It now more closely matches Perl chomp.

+ * + *
+     * StringUtils.chomp(null)          = null
+     * StringUtils.chomp("")            = ""
+     * StringUtils.chomp("abc \r")      = "abc "
+     * StringUtils.chomp("abc\n")       = "abc"
+     * StringUtils.chomp("abc\r\n")     = "abc"
+     * StringUtils.chomp("abc\r\n\r\n") = "abc\r\n"
+     * StringUtils.chomp("abc\n\r")     = "abc\n"
+     * StringUtils.chomp("abc\n\rabc")  = "abc\n\rabc"
+     * StringUtils.chomp("\r")          = ""
+     * StringUtils.chomp("\n")          = ""
+     * StringUtils.chomp("\r\n")        = ""
+     * 
+ * + * @param str the String to chomp a newline from, may be null + * @return String without newline, {@code null} if null String input + */ + public static String chomp(String str) { + if (isEmpty(str)) { + return str; + } + + if (str.length() == 1) { + char ch = str.charAt(0); + if (ch == CharUtils.CR || ch == CharUtils.LF) { + return EMPTY; + } + return str; + } + + int lastIdx = str.length() - 1; + char last = str.charAt(lastIdx); + + if (last == CharUtils.LF) { + if (str.charAt(lastIdx - 1) == CharUtils.CR) { + lastIdx--; + } + } else if (last != CharUtils.CR) { + lastIdx++; + } + return str.substring(0, lastIdx); + } + + /** + *

Removes {@code separator} from the end of + * {@code str} if it's there, otherwise leave it alone.

+ * + *

NOTE: This method changed in version 2.0. + * It now more closely matches Perl chomp. + * For the previous behavior, use {@link #substringBeforeLast(String, String)}. + * This method uses {@link String#endsWith(String)}.

+ * + *
+     * StringUtils.chomp(null, *)         = null
+     * StringUtils.chomp("", *)           = ""
+     * StringUtils.chomp("foobar", "bar") = "foo"
+     * StringUtils.chomp("foobar", "baz") = "foobar"
+     * StringUtils.chomp("foo", "foo")    = ""
+     * StringUtils.chomp("foo ", "foo")   = "foo "
+     * StringUtils.chomp(" foo", "foo")   = " "
+     * StringUtils.chomp("foo", "foooo")  = "foo"
+     * StringUtils.chomp("foo", "")       = "foo"
+     * StringUtils.chomp("foo", null)     = "foo"
+     * 
+ * + * @param str the String to chomp from, may be null + * @param separator separator String, may be null + * @return String without trailing separator, {@code null} if null String input + * @deprecated This feature will be removed in Lang 4.0, use {@link StringUtils#removeEnd(String, String)} instead + */ + @Deprecated + public static String chomp(String str, String separator) { + return removeEnd(str,separator); + } + + // Chopping + //----------------------------------------------------------------------- + /** + *

Remove the last character from a String.

+ * + *

If the String ends in {@code \r\n}, then remove both + * of them.

+ * + *
+     * StringUtils.chop(null)          = null
+     * StringUtils.chop("")            = ""
+     * StringUtils.chop("abc \r")      = "abc "
+     * StringUtils.chop("abc\n")       = "abc"
+     * StringUtils.chop("abc\r\n")     = "abc"
+     * StringUtils.chop("abc")         = "ab"
+     * StringUtils.chop("abc\nabc")    = "abc\nab"
+     * StringUtils.chop("a")           = ""
+     * StringUtils.chop("\r")          = ""
+     * StringUtils.chop("\n")          = ""
+     * StringUtils.chop("\r\n")        = ""
+     * 
+ * + * @param str the String to chop last character from, may be null + * @return String without last character, {@code null} if null String input + */ + public static String chop(String str) { + if (str == null) { + return null; + } + int strLen = str.length(); + if (strLen < 2) { + return EMPTY; + } + int lastIdx = strLen - 1; + String ret = str.substring(0, lastIdx); + char last = str.charAt(lastIdx); + if (last == CharUtils.LF && ret.charAt(lastIdx - 1) == CharUtils.CR) { + return ret.substring(0, lastIdx - 1); + } + return ret; + } + + // Conversion + //----------------------------------------------------------------------- + + // Padding + //----------------------------------------------------------------------- + /** + *

Repeat a String {@code repeat} times to form a + * new String.

+ * + *
+     * StringUtils.repeat(null, 2) = null
+     * StringUtils.repeat("", 0)   = ""
+     * StringUtils.repeat("", 2)   = ""
+     * StringUtils.repeat("a", 3)  = "aaa"
+     * StringUtils.repeat("ab", 2) = "abab"
+     * StringUtils.repeat("a", -2) = ""
+     * 
+ * + * @param str the String to repeat, may be null + * @param repeat number of times to repeat str, negative treated as zero + * @return a new String consisting of the original String repeated, + * {@code null} if null String input + */ + public static String repeat(String str, int repeat) { + // Performance tuned for 2.0 (JDK1.4) + + if (str == null) { + return null; + } + if (repeat <= 0) { + return EMPTY; + } + int inputLength = str.length(); + if (repeat == 1 || inputLength == 0) { + return str; + } + if (inputLength == 1 && repeat <= PAD_LIMIT) { + return repeat(str.charAt(0), repeat); + } + + int outputLength = inputLength * repeat; + switch (inputLength) { + case 1 : + return repeat(str.charAt(0), repeat); + case 2 : + char ch0 = str.charAt(0); + char ch1 = str.charAt(1); + char[] output2 = new char[outputLength]; + for (int i = repeat * 2 - 2; i >= 0; i--, i--) { + output2[i] = ch0; + output2[i + 1] = ch1; + } + return new String(output2); + default : + StringBuilder buf = new StringBuilder(outputLength); + for (int i = 0; i < repeat; i++) { + buf.append(str); + } + return buf.toString(); + } + } + + /** + *

Repeat a String {@code repeat} times to form a + * new String, with a String separator injected each time.

+ * + *
+     * StringUtils.repeat(null, null, 2) = null
+     * StringUtils.repeat(null, "x", 2)  = null
+     * StringUtils.repeat("", null, 0)   = ""
+     * StringUtils.repeat("", "", 2)     = ""
+     * StringUtils.repeat("", "x", 3)    = "xxx"
+     * StringUtils.repeat("?", ", ", 3)  = "?, ?, ?"
+     * 
+ * + * @param str the String to repeat, may be null + * @param separator the String to inject, may be null + * @param repeat number of times to repeat str, negative treated as zero + * @return a new String consisting of the original String repeated, + * {@code null} if null String input + * @since 2.5 + */ + public static String repeat(String str, String separator, int repeat) { + if(str == null || separator == null) { + return repeat(str, repeat); + } else { + // given that repeat(String, int) is quite optimized, better to rely on it than try and splice this into it + String result = repeat(str + separator, repeat); + return removeEnd(result, separator); + } + } + + /** + *

Returns padding using the specified delimiter repeated + * to a given length.

+ * + *
+     * StringUtils.repeat(0, 'e')  = ""
+     * StringUtils.repeat(3, 'e')  = "eee"
+     * StringUtils.repeat(-2, 'e') = ""
+     * 
+ * + *

Note: this method doesn't not support padding with + * Unicode Supplementary Characters + * as they require a pair of {@code char}s to be represented. + * If you are needing to support full I18N of your applications + * consider using {@link #repeat(String, int)} instead. + *

+ * + * @param ch character to repeat + * @param repeat number of times to repeat char, negative treated as zero + * @return String with repeated character + * @see #repeat(String, int) + */ + public static String repeat(char ch, int repeat) { + char[] buf = new char[repeat]; + for (int i = repeat - 1; i >= 0; i--) { + buf[i] = ch; + } + return new String(buf); + } + + /** + *

Right pad a String with spaces (' ').

+ * + *

The String is padded to the size of {@code size}.

+ * + *
+     * StringUtils.rightPad(null, *)   = null
+     * StringUtils.rightPad("", 3)     = "   "
+     * StringUtils.rightPad("bat", 3)  = "bat"
+     * StringUtils.rightPad("bat", 5)  = "bat  "
+     * StringUtils.rightPad("bat", 1)  = "bat"
+     * StringUtils.rightPad("bat", -1) = "bat"
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @return right padded String or original String if no padding is necessary, + * {@code null} if null String input + */ + public static String rightPad(String str, int size) { + return rightPad(str, size, ' '); + } + + /** + *

Right pad a String with a specified character.

+ * + *

The String is padded to the size of {@code size}.

+ * + *
+     * StringUtils.rightPad(null, *, *)     = null
+     * StringUtils.rightPad("", 3, 'z')     = "zzz"
+     * StringUtils.rightPad("bat", 3, 'z')  = "bat"
+     * StringUtils.rightPad("bat", 5, 'z')  = "batzz"
+     * StringUtils.rightPad("bat", 1, 'z')  = "bat"
+     * StringUtils.rightPad("bat", -1, 'z') = "bat"
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @param padChar the character to pad with + * @return right padded String or original String if no padding is necessary, + * {@code null} if null String input + * @since 2.0 + */ + public static String rightPad(String str, int size, char padChar) { + if (str == null) { + return null; + } + int pads = size - str.length(); + if (pads <= 0) { + return str; // returns original String when possible + } + if (pads > PAD_LIMIT) { + return rightPad(str, size, String.valueOf(padChar)); + } + return str.concat(repeat(padChar, pads)); + } + + /** + *

Right pad a String with a specified String.

+ * + *

The String is padded to the size of {@code size}.

+ * + *
+     * StringUtils.rightPad(null, *, *)      = null
+     * StringUtils.rightPad("", 3, "z")      = "zzz"
+     * StringUtils.rightPad("bat", 3, "yz")  = "bat"
+     * StringUtils.rightPad("bat", 5, "yz")  = "batyz"
+     * StringUtils.rightPad("bat", 8, "yz")  = "batyzyzy"
+     * StringUtils.rightPad("bat", 1, "yz")  = "bat"
+     * StringUtils.rightPad("bat", -1, "yz") = "bat"
+     * StringUtils.rightPad("bat", 5, null)  = "bat  "
+     * StringUtils.rightPad("bat", 5, "")    = "bat  "
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @param padStr the String to pad with, null or empty treated as single space + * @return right padded String or original String if no padding is necessary, + * {@code null} if null String input + */ + public static String rightPad(String str, int size, String padStr) { + if (str == null) { + return null; + } + if (isEmpty(padStr)) { + padStr = " "; + } + int padLen = padStr.length(); + int strLen = str.length(); + int pads = size - strLen; + if (pads <= 0) { + return str; // returns original String when possible + } + if (padLen == 1 && pads <= PAD_LIMIT) { + return rightPad(str, size, padStr.charAt(0)); + } + + if (pads == padLen) { + return str.concat(padStr); + } else if (pads < padLen) { + return str.concat(padStr.substring(0, pads)); + } else { + char[] padding = new char[pads]; + char[] padChars = padStr.toCharArray(); + for (int i = 0; i < pads; i++) { + padding[i] = padChars[i % padLen]; + } + return str.concat(new String(padding)); + } + } + + /** + *

Left pad a String with spaces (' ').

+ * + *

The String is padded to the size of {@code size}.

+ * + *
+     * StringUtils.leftPad(null, *)   = null
+     * StringUtils.leftPad("", 3)     = "   "
+     * StringUtils.leftPad("bat", 3)  = "bat"
+     * StringUtils.leftPad("bat", 5)  = "  bat"
+     * StringUtils.leftPad("bat", 1)  = "bat"
+     * StringUtils.leftPad("bat", -1) = "bat"
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @return left padded String or original String if no padding is necessary, + * {@code null} if null String input + */ + public static String leftPad(String str, int size) { + return leftPad(str, size, ' '); + } + + /** + *

Left pad a String with a specified character.

+ * + *

Pad to a size of {@code size}.

+ * + *
+     * StringUtils.leftPad(null, *, *)     = null
+     * StringUtils.leftPad("", 3, 'z')     = "zzz"
+     * StringUtils.leftPad("bat", 3, 'z')  = "bat"
+     * StringUtils.leftPad("bat", 5, 'z')  = "zzbat"
+     * StringUtils.leftPad("bat", 1, 'z')  = "bat"
+     * StringUtils.leftPad("bat", -1, 'z') = "bat"
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @param padChar the character to pad with + * @return left padded String or original String if no padding is necessary, + * {@code null} if null String input + * @since 2.0 + */ + public static String leftPad(String str, int size, char padChar) { + if (str == null) { + return null; + } + int pads = size - str.length(); + if (pads <= 0) { + return str; // returns original String when possible + } + if (pads > PAD_LIMIT) { + return leftPad(str, size, String.valueOf(padChar)); + } + return repeat(padChar, pads).concat(str); + } + + /** + *

Left pad a String with a specified String.

+ * + *

Pad to a size of {@code size}.

+ * + *
+     * StringUtils.leftPad(null, *, *)      = null
+     * StringUtils.leftPad("", 3, "z")      = "zzz"
+     * StringUtils.leftPad("bat", 3, "yz")  = "bat"
+     * StringUtils.leftPad("bat", 5, "yz")  = "yzbat"
+     * StringUtils.leftPad("bat", 8, "yz")  = "yzyzybat"
+     * StringUtils.leftPad("bat", 1, "yz")  = "bat"
+     * StringUtils.leftPad("bat", -1, "yz") = "bat"
+     * StringUtils.leftPad("bat", 5, null)  = "  bat"
+     * StringUtils.leftPad("bat", 5, "")    = "  bat"
+     * 
+ * + * @param str the String to pad out, may be null + * @param size the size to pad to + * @param padStr the String to pad with, null or empty treated as single space + * @return left padded String or original String if no padding is necessary, + * {@code null} if null String input + */ + public static String leftPad(String str, int size, String padStr) { + if (str == null) { + return null; + } + if (isEmpty(padStr)) { + padStr = " "; + } + int padLen = padStr.length(); + int strLen = str.length(); + int pads = size - strLen; + if (pads <= 0) { + return str; // returns original String when possible + } + if (padLen == 1 && pads <= PAD_LIMIT) { + return leftPad(str, size, padStr.charAt(0)); + } + + if (pads == padLen) { + return padStr.concat(str); + } else if (pads < padLen) { + return padStr.substring(0, pads).concat(str); + } else { + char[] padding = new char[pads]; + char[] padChars = padStr.toCharArray(); + for (int i = 0; i < pads; i++) { + padding[i] = padChars[i % padLen]; + } + return new String(padding).concat(str); + } + } + + /** + * Gets a CharSequence length or {@code 0} if the CharSequence is + * {@code null}. + * + * @param cs + * a CharSequence or {@code null} + * @return CharSequence length or {@code 0} if the CharSequence is + * {@code null}. + * @since 2.4 + * @since 3.0 Changed signature from length(String) to length(CharSequence) + */ + public static int length(CharSequence cs) { + return cs == null ? 0 : cs.length(); + } + + // Centering + //----------------------------------------------------------------------- + /** + *

Centers a String in a larger String of size {@code size} + * using the space character (' ').

+ * + *

If the size is less than the String length, the String is returned. + * A {@code null} String returns {@code null}. + * A negative size is treated as zero.

+ * + *

Equivalent to {@code center(str, size, " ")}.

+ * + *
+     * StringUtils.center(null, *)   = null
+     * StringUtils.center("", 4)     = "    "
+     * StringUtils.center("ab", -1)  = "ab"
+     * StringUtils.center("ab", 4)   = " ab "
+     * StringUtils.center("abcd", 2) = "abcd"
+     * StringUtils.center("a", 4)    = " a  "
+     * 
+ * + * @param str the String to center, may be null + * @param size the int size of new String, negative treated as zero + * @return centered String, {@code null} if null String input + */ + public static String center(String str, int size) { + return center(str, size, ' '); + } + + /** + *

Centers a String in a larger String of size {@code size}. + * Uses a supplied character as the value to pad the String with.

+ * + *

If the size is less than the String length, the String is returned. + * A {@code null} String returns {@code null}. + * A negative size is treated as zero.

+ * + *
+     * StringUtils.center(null, *, *)     = null
+     * StringUtils.center("", 4, ' ')     = "    "
+     * StringUtils.center("ab", -1, ' ')  = "ab"
+     * StringUtils.center("ab", 4, ' ')   = " ab"
+     * StringUtils.center("abcd", 2, ' ') = "abcd"
+     * StringUtils.center("a", 4, ' ')    = " a  "
+     * StringUtils.center("a", 4, 'y')    = "yayy"
+     * 
+ * + * @param str the String to center, may be null + * @param size the int size of new String, negative treated as zero + * @param padChar the character to pad the new String with + * @return centered String, {@code null} if null String input + * @since 2.0 + */ + public static String center(String str, int size, char padChar) { + if (str == null || size <= 0) { + return str; + } + int strLen = str.length(); + int pads = size - strLen; + if (pads <= 0) { + return str; + } + str = leftPad(str, strLen + pads / 2, padChar); + str = rightPad(str, size, padChar); + return str; + } + + /** + *

Centers a String in a larger String of size {@code size}. + * Uses a supplied String as the value to pad the String with.

+ * + *

If the size is less than the String length, the String is returned. + * A {@code null} String returns {@code null}. + * A negative size is treated as zero.

+ * + *
+     * StringUtils.center(null, *, *)     = null
+     * StringUtils.center("", 4, " ")     = "    "
+     * StringUtils.center("ab", -1, " ")  = "ab"
+     * StringUtils.center("ab", 4, " ")   = " ab"
+     * StringUtils.center("abcd", 2, " ") = "abcd"
+     * StringUtils.center("a", 4, " ")    = " a  "
+     * StringUtils.center("a", 4, "yz")   = "yayz"
+     * StringUtils.center("abc", 7, null) = "  abc  "
+     * StringUtils.center("abc", 7, "")   = "  abc  "
+     * 
+ * + * @param str the String to center, may be null + * @param size the int size of new String, negative treated as zero + * @param padStr the String to pad the new String with, must not be null or empty + * @return centered String, {@code null} if null String input + * @throws IllegalArgumentException if padStr is {@code null} or empty + */ + public static String center(String str, int size, String padStr) { + if (str == null || size <= 0) { + return str; + } + if (isEmpty(padStr)) { + padStr = " "; + } + int strLen = str.length(); + int pads = size - strLen; + if (pads <= 0) { + return str; + } + str = leftPad(str, strLen + pads / 2, padStr); + str = rightPad(str, size, padStr); + return str; + } + + // Case conversion + //----------------------------------------------------------------------- + /** + *

Converts a String to upper case as per {@link String#toUpperCase()}.

+ * + *

A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.upperCase(null)  = null
+     * StringUtils.upperCase("")    = ""
+     * StringUtils.upperCase("aBc") = "ABC"
+     * 
+ * + *

Note: As described in the documentation for {@link String#toUpperCase()}, + * the result of this method is affected by the current locale. + * For platform-independent case transformations, the method {@link #lowerCase(String, Locale)} + * should be used with a specific locale (e.g. {@link Locale#ENGLISH}).

+ * + * @param str the String to upper case, may be null + * @return the upper cased String, {@code null} if null String input + */ + public static String upperCase(String str) { + if (str == null) { + return null; + } + return str.toUpperCase(); + } + + /** + *

Converts a String to upper case as per {@link String#toUpperCase(Locale)}.

+ * + *

A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.upperCase(null, Locale.ENGLISH)  = null
+     * StringUtils.upperCase("", Locale.ENGLISH)    = ""
+     * StringUtils.upperCase("aBc", Locale.ENGLISH) = "ABC"
+     * 
+ * + * @param str the String to upper case, may be null + * @param locale the locale that defines the case transformation rules, must not be null + * @return the upper cased String, {@code null} if null String input + * @since 2.5 + */ + public static String upperCase(String str, Locale locale) { + if (str == null) { + return null; + } + return str.toUpperCase(locale); + } + + /** + *

Converts a String to lower case as per {@link String#toLowerCase()}.

+ * + *

A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.lowerCase(null)  = null
+     * StringUtils.lowerCase("")    = ""
+     * StringUtils.lowerCase("aBc") = "abc"
+     * 
+ * + *

Note: As described in the documentation for {@link String#toLowerCase()}, + * the result of this method is affected by the current locale. + * For platform-independent case transformations, the method {@link #lowerCase(String, Locale)} + * should be used with a specific locale (e.g. {@link Locale#ENGLISH}).

+ * + * @param str the String to lower case, may be null + * @return the lower cased String, {@code null} if null String input + */ + public static String lowerCase(String str) { + if (str == null) { + return null; + } + return str.toLowerCase(); + } + + /** + *

Converts a String to lower case as per {@link String#toLowerCase(Locale)}.

+ * + *

A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.lowerCase(null, Locale.ENGLISH)  = null
+     * StringUtils.lowerCase("", Locale.ENGLISH)    = ""
+     * StringUtils.lowerCase("aBc", Locale.ENGLISH) = "abc"
+     * 
+ * + * @param str the String to lower case, may be null + * @param locale the locale that defines the case transformation rules, must not be null + * @return the lower cased String, {@code null} if null String input + * @since 2.5 + */ + public static String lowerCase(String str, Locale locale) { + if (str == null) { + return null; + } + return str.toLowerCase(locale); + } + + /** + *

Capitalizes a String changing the first letter to title case as + * per {@link Character#toTitleCase(char)}. No other letters are changed.

+ * + *

For a word based algorithm, see {@link external.org.apache.commons.lang3.text.WordUtils#capitalize(String)}. + * A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.capitalize(null)  = null
+     * StringUtils.capitalize("")    = ""
+     * StringUtils.capitalize("cat") = "Cat"
+     * StringUtils.capitalize("cAt") = "CAt"
+     * 
+ * + * @param str the String to capitalize, may be null + * @return the capitalized String, {@code null} if null String input + * @see external.org.apache.commons.lang3.text.WordUtils#capitalize(String) + * @see #uncapitalize(String) + * @since 2.0 + */ + public static String capitalize(String str) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return str; + } + return new StringBuilder(strLen) + .append(Character.toTitleCase(str.charAt(0))) + .append(str.substring(1)) + .toString(); + } + + /** + *

Uncapitalizes a String changing the first letter to title case as + * per {@link Character#toLowerCase(char)}. No other letters are changed.

+ * + *

For a word based algorithm, see {@link external.org.apache.commons.lang3.text.WordUtils#uncapitalize(String)}. + * A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.uncapitalize(null)  = null
+     * StringUtils.uncapitalize("")    = ""
+     * StringUtils.uncapitalize("Cat") = "cat"
+     * StringUtils.uncapitalize("CAT") = "cAT"
+     * 
+ * + * @param str the String to uncapitalize, may be null + * @return the uncapitalized String, {@code null} if null String input + * @see external.org.apache.commons.lang3.text.WordUtils#uncapitalize(String) + * @see #capitalize(String) + * @since 2.0 + */ + public static String uncapitalize(String str) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return str; + } + return new StringBuilder(strLen) + .append(Character.toLowerCase(str.charAt(0))) + .append(str.substring(1)) + .toString(); + } + + /** + *

Swaps the case of a String changing upper and title case to + * lower case, and lower case to upper case.

+ * + *
    + *
  • Upper case character converts to Lower case
  • + *
  • Title case character converts to Lower case
  • + *
  • Lower case character converts to Upper case
  • + *
+ * + *

For a word based algorithm, see {@link external.org.apache.commons.lang3.text.WordUtils#swapCase(String)}. + * A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.swapCase(null)                 = null
+     * StringUtils.swapCase("")                   = ""
+     * StringUtils.swapCase("The dog has a BONE") = "tHE DOG HAS A bone"
+     * 
+ * + *

NOTE: This method changed in Lang version 2.0. + * It no longer performs a word based algorithm. + * If you only use ASCII, you will notice no change. + * That functionality is available in org.apache.commons.lang3.text.WordUtils.

+ * + * @param str the String to swap case, may be null + * @return the changed String, {@code null} if null String input + */ + public static String swapCase(String str) { + if (StringUtils.isEmpty(str)) { + return str; + } + + char[] buffer = str.toCharArray(); + + for (int i = 0; i < buffer.length; i++) { + char ch = buffer[i]; + if (Character.isUpperCase(ch)) { + buffer[i] = Character.toLowerCase(ch); + } else if (Character.isTitleCase(ch)) { + buffer[i] = Character.toLowerCase(ch); + } else if (Character.isLowerCase(ch)) { + buffer[i] = Character.toUpperCase(ch); + } + } + return new String(buffer); + } + + // Count matches + //----------------------------------------------------------------------- + /** + *

Counts how many times the substring appears in the larger string.

+ * + *

A {@code null} or empty ("") String input returns {@code 0}.

+ * + *
+     * StringUtils.countMatches(null, *)       = 0
+     * StringUtils.countMatches("", *)         = 0
+     * StringUtils.countMatches("abba", null)  = 0
+     * StringUtils.countMatches("abba", "")    = 0
+     * StringUtils.countMatches("abba", "a")   = 2
+     * StringUtils.countMatches("abba", "ab")  = 1
+     * StringUtils.countMatches("abba", "xxx") = 0
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param sub the substring to count, may be null + * @return the number of occurrences, 0 if either CharSequence is {@code null} + * @since 3.0 Changed signature from countMatches(String, String) to countMatches(CharSequence, CharSequence) + */ + public static int countMatches(CharSequence str, CharSequence sub) { + if (isEmpty(str) || isEmpty(sub)) { + return 0; + } + int count = 0; + int idx = 0; + while ((idx = CharSequenceUtils.indexOf(str, sub, idx)) != INDEX_NOT_FOUND) { + count++; + idx += sub.length(); + } + return count; + } + + // Character Tests + //----------------------------------------------------------------------- + /** + *

Checks if the CharSequence contains only Unicode letters.

+ * + *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code false}.

+ * + *
+     * StringUtils.isAlpha(null)   = false
+     * StringUtils.isAlpha("")     = false
+     * StringUtils.isAlpha("  ")   = false
+     * StringUtils.isAlpha("abc")  = true
+     * StringUtils.isAlpha("ab2c") = false
+     * StringUtils.isAlpha("ab-c") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains letters, and is non-null + * @since 3.0 Changed signature from isAlpha(String) to isAlpha(CharSequence) + * @since 3.0 Changed "" to return false and not true + */ + public static boolean isAlpha(CharSequence cs) { + if (cs == null || cs.length() == 0) { + return false; + } + int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isLetter(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only Unicode letters and + * space (' ').

+ * + *

{@code null} will return {@code false} + * An empty CharSequence (length()=0) will return {@code true}.

+ * + *
+     * StringUtils.isAlphaSpace(null)   = false
+     * StringUtils.isAlphaSpace("")     = true
+     * StringUtils.isAlphaSpace("  ")   = true
+     * StringUtils.isAlphaSpace("abc")  = true
+     * StringUtils.isAlphaSpace("ab c") = true
+     * StringUtils.isAlphaSpace("ab2c") = false
+     * StringUtils.isAlphaSpace("ab-c") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains letters and space, + * and is non-null + * @since 3.0 Changed signature from isAlphaSpace(String) to isAlphaSpace(CharSequence) + */ + public static boolean isAlphaSpace(CharSequence cs) { + if (cs == null) { + return false; + } + int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isLetter(cs.charAt(i)) == false && cs.charAt(i) != ' ') { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only Unicode letters or digits.

+ * + *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code false}.

+ * + *
+     * StringUtils.isAlphanumeric(null)   = false
+     * StringUtils.isAlphanumeric("")     = false
+     * StringUtils.isAlphanumeric("  ")   = false
+     * StringUtils.isAlphanumeric("abc")  = true
+     * StringUtils.isAlphanumeric("ab c") = false
+     * StringUtils.isAlphanumeric("ab2c") = true
+     * StringUtils.isAlphanumeric("ab-c") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains letters or digits, + * and is non-null + * @since 3.0 Changed signature from isAlphanumeric(String) to isAlphanumeric(CharSequence) + * @since 3.0 Changed "" to return false and not true + */ + public static boolean isAlphanumeric(CharSequence cs) { + if (cs == null || cs.length() == 0) { + return false; + } + int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isLetterOrDigit(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only Unicode letters, digits + * or space ({@code ' '}).

+ * + *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code true}.

+ * + *
+     * StringUtils.isAlphanumericSpace(null)   = false
+     * StringUtils.isAlphanumericSpace("")     = true
+     * StringUtils.isAlphanumericSpace("  ")   = true
+     * StringUtils.isAlphanumericSpace("abc")  = true
+     * StringUtils.isAlphanumericSpace("ab c") = true
+     * StringUtils.isAlphanumericSpace("ab2c") = true
+     * StringUtils.isAlphanumericSpace("ab-c") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains letters, digits or space, + * and is non-null + * @since 3.0 Changed signature from isAlphanumericSpace(String) to isAlphanumericSpace(CharSequence) + */ + public static boolean isAlphanumericSpace(CharSequence cs) { + if (cs == null) { + return false; + } + int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isLetterOrDigit(cs.charAt(i)) == false && cs.charAt(i) != ' ') { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only ASCII printable characters.

+ * + *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code true}.

+ * + *
+     * StringUtils.isAsciiPrintable(null)     = false
+     * StringUtils.isAsciiPrintable("")       = true
+     * StringUtils.isAsciiPrintable(" ")      = true
+     * StringUtils.isAsciiPrintable("Ceki")   = true
+     * StringUtils.isAsciiPrintable("ab2c")   = true
+     * StringUtils.isAsciiPrintable("!ab-c~") = true
+     * StringUtils.isAsciiPrintable("\u0020") = true
+     * StringUtils.isAsciiPrintable("\u0021") = true
+     * StringUtils.isAsciiPrintable("\u007e") = true
+     * StringUtils.isAsciiPrintable("\u007f") = false
+     * StringUtils.isAsciiPrintable("Ceki G\u00fclc\u00fc") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if every character is in the range + * 32 thru 126 + * @since 2.1 + * @since 3.0 Changed signature from isAsciiPrintable(String) to isAsciiPrintable(CharSequence) + */ + public static boolean isAsciiPrintable(CharSequence cs) { + if (cs == null) { + return false; + } + int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (CharUtils.isAsciiPrintable(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only Unicode digits. + * A decimal point is not a Unicode digit and returns false.

+ * + *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code false}.

+ * + *
+     * StringUtils.isNumeric(null)   = false
+     * StringUtils.isNumeric("")     = false
+     * StringUtils.isNumeric("  ")   = false
+     * StringUtils.isNumeric("123")  = true
+     * StringUtils.isNumeric("12 3") = false
+     * StringUtils.isNumeric("ab2c") = false
+     * StringUtils.isNumeric("12-3") = false
+     * StringUtils.isNumeric("12.3") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains digits, and is non-null + * @since 3.0 Changed signature from isNumeric(String) to isNumeric(CharSequence) + * @since 3.0 Changed "" to return false and not true + */ + public static boolean isNumeric(CharSequence cs) { + if (cs == null || cs.length() == 0) { + return false; + } + int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isDigit(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only Unicode digits or space + * ({@code ' '}). + * A decimal point is not a Unicode digit and returns false.

+ * + *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code true}.

+ * + *
+     * StringUtils.isNumericSpace(null)   = false
+     * StringUtils.isNumericSpace("")     = true
+     * StringUtils.isNumericSpace("  ")   = true
+     * StringUtils.isNumericSpace("123")  = true
+     * StringUtils.isNumericSpace("12 3") = true
+     * StringUtils.isNumericSpace("ab2c") = false
+     * StringUtils.isNumericSpace("12-3") = false
+     * StringUtils.isNumericSpace("12.3") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains digits or space, + * and is non-null + * @since 3.0 Changed signature from isNumericSpace(String) to isNumericSpace(CharSequence) + */ + public static boolean isNumericSpace(CharSequence cs) { + if (cs == null) { + return false; + } + int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isDigit(cs.charAt(i)) == false && cs.charAt(i) != ' ') { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only whitespace.

+ * + *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code true}.

+ * + *
+     * StringUtils.isWhitespace(null)   = false
+     * StringUtils.isWhitespace("")     = true
+     * StringUtils.isWhitespace("  ")   = true
+     * StringUtils.isWhitespace("abc")  = false
+     * StringUtils.isWhitespace("ab2c") = false
+     * StringUtils.isWhitespace("ab-c") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains whitespace, and is non-null + * @since 2.0 + * @since 3.0 Changed signature from isWhitespace(String) to isWhitespace(CharSequence) + */ + public static boolean isWhitespace(CharSequence cs) { + if (cs == null) { + return false; + } + int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isWhitespace(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only lowercase characters.

+ * + *

{@code null} will return {@code false}. + * An empty CharSequence (length()=0) will return {@code false}.

+ * + *
+     * StringUtils.isAllLowerCase(null)   = false
+     * StringUtils.isAllLowerCase("")     = false
+     * StringUtils.isAllLowerCase("  ")   = false
+     * StringUtils.isAllLowerCase("abc")  = true
+     * StringUtils.isAllLowerCase("abC") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains lowercase characters, and is non-null + * @since 2.5 + * @since 3.0 Changed signature from isAllLowerCase(String) to isAllLowerCase(CharSequence) + */ + public static boolean isAllLowerCase(CharSequence cs) { + if (cs == null || isEmpty(cs)) { + return false; + } + int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isLowerCase(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + /** + *

Checks if the CharSequence contains only uppercase characters.

+ * + *

{@code null} will return {@code false}. + * An empty String (length()=0) will return {@code false}.

+ * + *
+     * StringUtils.isAllUpperCase(null)   = false
+     * StringUtils.isAllUpperCase("")     = false
+     * StringUtils.isAllUpperCase("  ")   = false
+     * StringUtils.isAllUpperCase("ABC")  = true
+     * StringUtils.isAllUpperCase("aBC") = false
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if only contains uppercase characters, and is non-null + * @since 2.5 + * @since 3.0 Changed signature from isAllUpperCase(String) to isAllUpperCase(CharSequence) + */ + public static boolean isAllUpperCase(CharSequence cs) { + if (cs == null || isEmpty(cs)) { + return false; + } + int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (Character.isUpperCase(cs.charAt(i)) == false) { + return false; + } + } + return true; + } + + // Defaults + //----------------------------------------------------------------------- + /** + *

Returns either the passed in String, + * or if the String is {@code null}, an empty String ("").

+ * + *
+     * StringUtils.defaultString(null)  = ""
+     * StringUtils.defaultString("")    = ""
+     * StringUtils.defaultString("bat") = "bat"
+     * 
+ * + * @see ObjectUtils#toString(Object) + * @see String#valueOf(Object) + * @param str the String to check, may be null + * @return the passed in String, or the empty String if it + * was {@code null} + */ + public static String defaultString(String str) { + return str == null ? EMPTY : str; + } + + /** + *

Returns either the passed in String, or if the String is + * {@code null}, the value of {@code defaultStr}.

+ * + *
+     * StringUtils.defaultString(null, "NULL")  = "NULL"
+     * StringUtils.defaultString("", "NULL")    = ""
+     * StringUtils.defaultString("bat", "NULL") = "bat"
+     * 
+ * + * @see ObjectUtils#toString(Object,String) + * @see String#valueOf(Object) + * @param str the String to check, may be null + * @param defaultStr the default String to return + * if the input is {@code null}, may be null + * @return the passed in String, or the default if it was {@code null} + */ + public static String defaultString(String str, String defaultStr) { + return str == null ? defaultStr : str; + } + + /** + *

Returns either the passed in CharSequence, or if the CharSequence is + * whitespace, empty ("") or {@code null}, the value of {@code defaultStr}.

+ * + *
+     * StringUtils.defaultIfBlank(null, "NULL")  = "NULL"
+     * StringUtils.defaultIfBlank("", "NULL")    = "NULL"
+     * StringUtils.defaultIfBlank(" ", "NULL")   = "NULL"
+     * StringUtils.defaultIfBlank("bat", "NULL") = "bat"
+     * StringUtils.defaultIfBlank("", null)      = null
+     * 
+ * @param the specific kind of CharSequence + * @param str the CharSequence to check, may be null + * @param defaultStr the default CharSequence to return + * if the input is whitespace, empty ("") or {@code null}, may be null + * @return the passed in CharSequence, or the default + * @see StringUtils#defaultString(String, String) + */ + public static T defaultIfBlank(T str, T defaultStr) { + return StringUtils.isBlank(str) ? defaultStr : str; + } + + /** + *

Returns either the passed in CharSequence, or if the CharSequence is + * empty or {@code null}, the value of {@code defaultStr}.

+ * + *
+     * StringUtils.defaultIfEmpty(null, "NULL")  = "NULL"
+     * StringUtils.defaultIfEmpty("", "NULL")    = "NULL"
+     * StringUtils.defaultIfEmpty(" ", "NULL")   = " "
+     * StringUtils.defaultIfEmpty("bat", "NULL") = "bat"
+     * StringUtils.defaultIfEmpty("", null)      = null
+     * 
+ * @param the specific kind of CharSequence + * @param str the CharSequence to check, may be null + * @param defaultStr the default CharSequence to return + * if the input is empty ("") or {@code null}, may be null + * @return the passed in CharSequence, or the default + * @see StringUtils#defaultString(String, String) + */ + public static T defaultIfEmpty(T str, T defaultStr) { + return StringUtils.isEmpty(str) ? defaultStr : str; + } + + // Reversing + //----------------------------------------------------------------------- + /** + *

Reverses a String as per {@link StringBuilder#reverse()}.

+ * + *

A {@code null} String returns {@code null}.

+ * + *
+     * StringUtils.reverse(null)  = null
+     * StringUtils.reverse("")    = ""
+     * StringUtils.reverse("bat") = "tab"
+     * 
+ * + * @param str the String to reverse, may be null + * @return the reversed String, {@code null} if null String input + */ + public static String reverse(String str) { + if (str == null) { + return null; + } + return new StringBuilder(str).reverse().toString(); + } + + /** + *

Reverses a String that is delimited by a specific character.

+ * + *

The Strings between the delimiters are not reversed. + * Thus java.lang.String becomes String.lang.java (if the delimiter + * is {@code '.'}).

+ * + *
+     * StringUtils.reverseDelimited(null, *)      = null
+     * StringUtils.reverseDelimited("", *)        = ""
+     * StringUtils.reverseDelimited("a.b.c", 'x') = "a.b.c"
+     * StringUtils.reverseDelimited("a.b.c", ".") = "c.b.a"
+     * 
+ * + * @param str the String to reverse, may be null + * @param separatorChar the separator character to use + * @return the reversed String, {@code null} if null String input + * @since 2.0 + */ + public static String reverseDelimited(String str, char separatorChar) { + if (str == null) { + return null; + } + // could implement manually, but simple way is to reuse other, + // probably slower, methods. + String[] strs = split(str, separatorChar); + ArrayUtils.reverse(strs); + return join(strs, separatorChar); + } + + // Abbreviating + //----------------------------------------------------------------------- + /** + *

Abbreviates a String using ellipses. This will turn + * "Now is the time for all good men" into "Now is the time for..."

+ * + *

Specifically: + *

    + *
  • If {@code str} is less than {@code maxWidth} characters + * long, return it.
  • + *
  • Else abbreviate it to {@code (substring(str, 0, max-3) + "...")}.
  • + *
  • If {@code maxWidth} is less than {@code 4}, throw an + * {@code IllegalArgumentException}.
  • + *
  • In no case will it return a String of length greater than + * {@code maxWidth}.
  • + *
+ *

+ * + *
+     * StringUtils.abbreviate(null, *)      = null
+     * StringUtils.abbreviate("", 4)        = ""
+     * StringUtils.abbreviate("abcdefg", 6) = "abc..."
+     * StringUtils.abbreviate("abcdefg", 7) = "abcdefg"
+     * StringUtils.abbreviate("abcdefg", 8) = "abcdefg"
+     * StringUtils.abbreviate("abcdefg", 4) = "a..."
+     * StringUtils.abbreviate("abcdefg", 3) = IllegalArgumentException
+     * 
+ * + * @param str the String to check, may be null + * @param maxWidth maximum length of result String, must be at least 4 + * @return abbreviated String, {@code null} if null String input + * @throws IllegalArgumentException if the width is too small + * @since 2.0 + */ + public static String abbreviate(String str, int maxWidth) { + return abbreviate(str, 0, maxWidth); + } + + /** + *

Abbreviates a String using ellipses. This will turn + * "Now is the time for all good men" into "...is the time for..."

+ * + *

Works like {@code abbreviate(String, int)}, but allows you to specify + * a "left edge" offset. Note that this left edge is not necessarily going to + * be the leftmost character in the result, or the first character following the + * ellipses, but it will appear somewhere in the result. + * + *

In no case will it return a String of length greater than + * {@code maxWidth}.

+ * + *
+     * StringUtils.abbreviate(null, *, *)                = null
+     * StringUtils.abbreviate("", 0, 4)                  = ""
+     * StringUtils.abbreviate("abcdefghijklmno", -1, 10) = "abcdefg..."
+     * StringUtils.abbreviate("abcdefghijklmno", 0, 10)  = "abcdefg..."
+     * StringUtils.abbreviate("abcdefghijklmno", 1, 10)  = "abcdefg..."
+     * StringUtils.abbreviate("abcdefghijklmno", 4, 10)  = "abcdefg..."
+     * StringUtils.abbreviate("abcdefghijklmno", 5, 10)  = "...fghi..."
+     * StringUtils.abbreviate("abcdefghijklmno", 6, 10)  = "...ghij..."
+     * StringUtils.abbreviate("abcdefghijklmno", 8, 10)  = "...ijklmno"
+     * StringUtils.abbreviate("abcdefghijklmno", 10, 10) = "...ijklmno"
+     * StringUtils.abbreviate("abcdefghijklmno", 12, 10) = "...ijklmno"
+     * StringUtils.abbreviate("abcdefghij", 0, 3)        = IllegalArgumentException
+     * StringUtils.abbreviate("abcdefghij", 5, 6)        = IllegalArgumentException
+     * 
+ * + * @param str the String to check, may be null + * @param offset left edge of source String + * @param maxWidth maximum length of result String, must be at least 4 + * @return abbreviated String, {@code null} if null String input + * @throws IllegalArgumentException if the width is too small + * @since 2.0 + */ + public static String abbreviate(String str, int offset, int maxWidth) { + if (str == null) { + return null; + } + if (maxWidth < 4) { + throw new IllegalArgumentException("Minimum abbreviation width is 4"); + } + if (str.length() <= maxWidth) { + return str; + } + if (offset > str.length()) { + offset = str.length(); + } + if (str.length() - offset < maxWidth - 3) { + offset = str.length() - (maxWidth - 3); + } + final String abrevMarker = "..."; + if (offset <= 4) { + return str.substring(0, maxWidth - 3) + abrevMarker; + } + if (maxWidth < 7) { + throw new IllegalArgumentException("Minimum abbreviation width with offset is 7"); + } + if (offset + maxWidth - 3 < str.length()) { + return abrevMarker + abbreviate(str.substring(offset), maxWidth - 3); + } + return abrevMarker + str.substring(str.length() - (maxWidth - 3)); + } + + /** + *

Abbreviates a String to the length passed, replacing the middle characters with the supplied + * replacement String.

+ * + *

This abbreviation only occurs if the following criteria is met: + *

    + *
  • Neither the String for abbreviation nor the replacement String are null or empty
  • + *
  • The length to truncate to is less than the length of the supplied String
  • + *
  • The length to truncate to is greater than 0
  • + *
  • The abbreviated String will have enough room for the length supplied replacement String + * and the first and last characters of the supplied String for abbreviation
  • + *
+ * Otherwise, the returned String will be the same as the supplied String for abbreviation. + *

+ * + *
+     * StringUtils.abbreviateMiddle(null, null, 0)      = null
+     * StringUtils.abbreviateMiddle("abc", null, 0)      = "abc"
+     * StringUtils.abbreviateMiddle("abc", ".", 0)      = "abc"
+     * StringUtils.abbreviateMiddle("abc", ".", 3)      = "abc"
+     * StringUtils.abbreviateMiddle("abcdef", ".", 4)     = "ab.f"
+     * 
+ * + * @param str the String to abbreviate, may be null + * @param middle the String to replace the middle characters with, may be null + * @param length the length to abbreviate {@code str} to. + * @return the abbreviated String if the above criteria is met, or the original String supplied for abbreviation. + * @since 2.5 + */ + public static String abbreviateMiddle(String str, String middle, int length) { + if (isEmpty(str) || isEmpty(middle)) { + return str; + } + + if (length >= str.length() || length < middle.length()+2) { + return str; + } + + int targetSting = length-middle.length(); + int startOffset = targetSting/2+targetSting%2; + int endOffset = str.length()-targetSting/2; + + StringBuilder builder = new StringBuilder(length); + builder.append(str.substring(0,startOffset)); + builder.append(middle); + builder.append(str.substring(endOffset)); + + return builder.toString(); + } + + // Difference + //----------------------------------------------------------------------- + /** + *

Compares two Strings, and returns the portion where they differ. + * (More precisely, return the remainder of the second String, + * starting from where it's different from the first.)

+ * + *

For example, + * {@code difference("i am a machine", "i am a robot") -> "robot"}.

+ * + *
+     * StringUtils.difference(null, null) = null
+     * StringUtils.difference("", "") = ""
+     * StringUtils.difference("", "abc") = "abc"
+     * StringUtils.difference("abc", "") = ""
+     * StringUtils.difference("abc", "abc") = ""
+     * StringUtils.difference("ab", "abxyz") = "xyz"
+     * StringUtils.difference("abcde", "abxyz") = "xyz"
+     * StringUtils.difference("abcde", "xyz") = "xyz"
+     * 
+ * + * @param str1 the first String, may be null + * @param str2 the second String, may be null + * @return the portion of str2 where it differs from str1; returns the + * empty String if they are equal + * @since 2.0 + */ + public static String difference(String str1, String str2) { + if (str1 == null) { + return str2; + } + if (str2 == null) { + return str1; + } + int at = indexOfDifference(str1, str2); + if (at == INDEX_NOT_FOUND) { + return EMPTY; + } + return str2.substring(at); + } + + /** + *

Compares two CharSequences, and returns the index at which the + * CharSequences begin to differ.

+ * + *

For example, + * {@code indexOfDifference("i am a machine", "i am a robot") -> 7}

+ * + *
+     * StringUtils.indexOfDifference(null, null) = -1
+     * StringUtils.indexOfDifference("", "") = -1
+     * StringUtils.indexOfDifference("", "abc") = 0
+     * StringUtils.indexOfDifference("abc", "") = 0
+     * StringUtils.indexOfDifference("abc", "abc") = -1
+     * StringUtils.indexOfDifference("ab", "abxyz") = 2
+     * StringUtils.indexOfDifference("abcde", "abxyz") = 2
+     * StringUtils.indexOfDifference("abcde", "xyz") = 0
+     * 
+ * + * @param cs1 the first CharSequence, may be null + * @param cs2 the second CharSequence, may be null + * @return the index where cs1 and cs2 begin to differ; -1 if they are equal + * @since 2.0 + * @since 3.0 Changed signature from indexOfDifference(String, String) to + * indexOfDifference(CharSequence, CharSequence) + */ + public static int indexOfDifference(CharSequence cs1, CharSequence cs2) { + if (cs1 == cs2) { + return INDEX_NOT_FOUND; + } + if (cs1 == null || cs2 == null) { + return 0; + } + int i; + for (i = 0; i < cs1.length() && i < cs2.length(); ++i) { + if (cs1.charAt(i) != cs2.charAt(i)) { + break; + } + } + if (i < cs2.length() || i < cs1.length()) { + return i; + } + return INDEX_NOT_FOUND; + } + + /** + *

Compares all CharSequences in an array and returns the index at which the + * CharSequences begin to differ.

+ * + *

For example, + * indexOfDifference(new String[] {"i am a machine", "i am a robot"}) -> 7

+ * + *
+     * StringUtils.indexOfDifference(null) = -1
+     * StringUtils.indexOfDifference(new String[] {}) = -1
+     * StringUtils.indexOfDifference(new String[] {"abc"}) = -1
+     * StringUtils.indexOfDifference(new String[] {null, null}) = -1
+     * StringUtils.indexOfDifference(new String[] {"", ""}) = -1
+     * StringUtils.indexOfDifference(new String[] {"", null}) = 0
+     * StringUtils.indexOfDifference(new String[] {"abc", null, null}) = 0
+     * StringUtils.indexOfDifference(new String[] {null, null, "abc"}) = 0
+     * StringUtils.indexOfDifference(new String[] {"", "abc"}) = 0
+     * StringUtils.indexOfDifference(new String[] {"abc", ""}) = 0
+     * StringUtils.indexOfDifference(new String[] {"abc", "abc"}) = -1
+     * StringUtils.indexOfDifference(new String[] {"abc", "a"}) = 1
+     * StringUtils.indexOfDifference(new String[] {"ab", "abxyz"}) = 2
+     * StringUtils.indexOfDifference(new String[] {"abcde", "abxyz"}) = 2
+     * StringUtils.indexOfDifference(new String[] {"abcde", "xyz"}) = 0
+     * StringUtils.indexOfDifference(new String[] {"xyz", "abcde"}) = 0
+     * StringUtils.indexOfDifference(new String[] {"i am a machine", "i am a robot"}) = 7
+     * 
+ * + * @param css array of CharSequences, entries may be null + * @return the index where the strings begin to differ; -1 if they are all equal + * @since 2.4 + * @since 3.0 Changed signature from indexOfDifference(String...) to indexOfDifference(CharSequence...) + */ + public static int indexOfDifference(CharSequence... css) { + if (css == null || css.length <= 1) { + return INDEX_NOT_FOUND; + } + boolean anyStringNull = false; + boolean allStringsNull = true; + int arrayLen = css.length; + int shortestStrLen = Integer.MAX_VALUE; + int longestStrLen = 0; + + // find the min and max string lengths; this avoids checking to make + // sure we are not exceeding the length of the string each time through + // the bottom loop. + for (int i = 0; i < arrayLen; i++) { + if (css[i] == null) { + anyStringNull = true; + shortestStrLen = 0; + } else { + allStringsNull = false; + shortestStrLen = Math.min(css[i].length(), shortestStrLen); + longestStrLen = Math.max(css[i].length(), longestStrLen); + } + } + + // handle lists containing all nulls or all empty strings + if (allStringsNull || longestStrLen == 0 && !anyStringNull) { + return INDEX_NOT_FOUND; + } + + // handle lists containing some nulls or some empty strings + if (shortestStrLen == 0) { + return 0; + } + + // find the position with the first difference across all strings + int firstDiff = -1; + for (int stringPos = 0; stringPos < shortestStrLen; stringPos++) { + char comparisonChar = css[0].charAt(stringPos); + for (int arrayPos = 1; arrayPos < arrayLen; arrayPos++) { + if (css[arrayPos].charAt(stringPos) != comparisonChar) { + firstDiff = stringPos; + break; + } + } + if (firstDiff != -1) { + break; + } + } + + if (firstDiff == -1 && shortestStrLen != longestStrLen) { + // we compared all of the characters up to the length of the + // shortest string and didn't find a match, but the string lengths + // vary, so return the length of the shortest string. + return shortestStrLen; + } + return firstDiff; + } + + /** + *

Compares all Strings in an array and returns the initial sequence of + * characters that is common to all of them.

+ * + *

For example, + * getCommonPrefix(new String[] {"i am a machine", "i am a robot"}) -> "i am a "

+ * + *
+     * StringUtils.getCommonPrefix(null) = ""
+     * StringUtils.getCommonPrefix(new String[] {}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"abc"}) = "abc"
+     * StringUtils.getCommonPrefix(new String[] {null, null}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"", ""}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"", null}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"abc", null, null}) = ""
+     * StringUtils.getCommonPrefix(new String[] {null, null, "abc"}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"", "abc"}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"abc", ""}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"abc", "abc"}) = "abc"
+     * StringUtils.getCommonPrefix(new String[] {"abc", "a"}) = "a"
+     * StringUtils.getCommonPrefix(new String[] {"ab", "abxyz"}) = "ab"
+     * StringUtils.getCommonPrefix(new String[] {"abcde", "abxyz"}) = "ab"
+     * StringUtils.getCommonPrefix(new String[] {"abcde", "xyz"}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"xyz", "abcde"}) = ""
+     * StringUtils.getCommonPrefix(new String[] {"i am a machine", "i am a robot"}) = "i am a "
+     * 
+ * + * @param strs array of String objects, entries may be null + * @return the initial sequence of characters that are common to all Strings + * in the array; empty String if the array is null, the elements are all null + * or if there is no common prefix. + * @since 2.4 + */ + public static String getCommonPrefix(String... strs) { + if (strs == null || strs.length == 0) { + return EMPTY; + } + int smallestIndexOfDiff = indexOfDifference(strs); + if (smallestIndexOfDiff == INDEX_NOT_FOUND) { + // all strings were identical + if (strs[0] == null) { + return EMPTY; + } + return strs[0]; + } else if (smallestIndexOfDiff == 0) { + // there were no common initial characters + return EMPTY; + } else { + // we found a common initial character sequence + return strs[0].substring(0, smallestIndexOfDiff); + } + } + + // Misc + //----------------------------------------------------------------------- + /** + *

Find the Levenshtein distance between two Strings.

+ * + *

This is the number of changes needed to change one String into + * another, where each change is a single character modification (deletion, + * insertion or substitution).

+ * + *

The previous implementation of the Levenshtein distance algorithm + * was from http://www.merriampark.com/ld.htm

+ * + *

Chas Emerick has written an implementation in Java, which avoids an OutOfMemoryError + * which can occur when my Java implementation is used with very large strings.
+ * This implementation of the Levenshtein distance algorithm + * is from http://www.merriampark.com/ldjava.htm

+ * + *
+     * StringUtils.getLevenshteinDistance(null, *)             = IllegalArgumentException
+     * StringUtils.getLevenshteinDistance(*, null)             = IllegalArgumentException
+     * StringUtils.getLevenshteinDistance("","")               = 0
+     * StringUtils.getLevenshteinDistance("","a")              = 1
+     * StringUtils.getLevenshteinDistance("aaapppp", "")       = 7
+     * StringUtils.getLevenshteinDistance("frog", "fog")       = 1
+     * StringUtils.getLevenshteinDistance("fly", "ant")        = 3
+     * StringUtils.getLevenshteinDistance("elephant", "hippo") = 7
+     * StringUtils.getLevenshteinDistance("hippo", "elephant") = 7
+     * StringUtils.getLevenshteinDistance("hippo", "zzzzzzzz") = 8
+     * StringUtils.getLevenshteinDistance("hello", "hallo")    = 1
+     * 
+ * + * @param s the first String, must not be null + * @param t the second String, must not be null + * @return result distance + * @throws IllegalArgumentException if either String input {@code null} + * @since 3.0 Changed signature from getLevenshteinDistance(String, String) to + * getLevenshteinDistance(CharSequence, CharSequence) + */ + public static int getLevenshteinDistance(CharSequence s, CharSequence t) { + if (s == null || t == null) { + throw new IllegalArgumentException("Strings must not be null"); + } + + /* + The difference between this impl. and the previous is that, rather + than creating and retaining a matrix of size s.length() + 1 by t.length() + 1, + we maintain two single-dimensional arrays of length s.length() + 1. The first, d, + is the 'current working' distance array that maintains the newest distance cost + counts as we iterate through the characters of String s. Each time we increment + the index of String t we are comparing, d is copied to p, the second int[]. Doing so + allows us to retain the previous cost counts as required by the algorithm (taking + the minimum of the cost count to the left, up one, and diagonally up and to the left + of the current cost count being calculated). (Note that the arrays aren't really + copied anymore, just switched...this is clearly much better than cloning an array + or doing a System.arraycopy() each time through the outer loop.) + + Effectively, the difference between the two implementations is this one does not + cause an out of memory condition when calculating the LD over two very large strings. + */ + + int n = s.length(); // length of s + int m = t.length(); // length of t + + if (n == 0) { + return m; + } else if (m == 0) { + return n; + } + + if (n > m) { + // swap the input strings to consume less memory + CharSequence tmp = s; + s = t; + t = tmp; + n = m; + m = t.length(); + } + + int p[] = new int[n + 1]; //'previous' cost array, horizontally + int d[] = new int[n + 1]; // cost array, horizontally + int _d[]; //placeholder to assist in swapping p and d + + // indexes into strings s and t + int i; // iterates through s + int j; // iterates through t + + char t_j; // jth character of t + + int cost; // cost + + for (i = 0; i <= n; i++) { + p[i] = i; + } + + for (j = 1; j <= m; j++) { + t_j = t.charAt(j - 1); + d[0] = j; + + for (i = 1; i <= n; i++) { + cost = s.charAt(i - 1) == t_j ? 0 : 1; + // minimum of cell to the left+1, to the top+1, diagonally left and up +cost + d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost); + } + + // copy current distance counts to 'previous row' distance counts + _d = p; + p = d; + d = _d; + } + + // our last action in the above loop was to switch d and p, so p now + // actually has the most recent cost counts + return p[n]; + } + + /** + *

Find the Levenshtein distance between two Strings if it's less than or equal to a given + * threshold.

+ * + *

This is the number of changes needed to change one String into + * another, where each change is a single character modification (deletion, + * insertion or substitution).

+ * + *

This implementation follows from Algorithms on Strings, Trees and Sequences by Dan Gusfield + * and Chas Emerick's implementation of the Levenshtein distance algorithm from + * http://www.merriampark.com/ld.htm

+ * + *
+     * StringUtils.getLevenshteinDistance(null, *, *)             = IllegalArgumentException
+     * StringUtils.getLevenshteinDistance(*, null, *)             = IllegalArgumentException
+     * StringUtils.getLevenshteinDistance(*, *, -1)               = IllegalArgumentException
+     * StringUtils.getLevenshteinDistance("","", 0)               = 0
+     * StringUtils.getLevenshteinDistance("aaapppp", "", 8)       = 7
+     * StringUtils.getLevenshteinDistance("aaapppp", "", 7)       = 7
+     * StringUtils.getLevenshteinDistance("aaapppp", "", 6))      = -1
+     * StringUtils.getLevenshteinDistance("elephant", "hippo", 7) = 7
+     * StringUtils.getLevenshteinDistance("elephant", "hippo", 6) = -1
+     * StringUtils.getLevenshteinDistance("hippo", "elephant", 7) = 7
+     * StringUtils.getLevenshteinDistance("hippo", "elephant", 6) = -1
+     * 
+ * + * @param s the first String, must not be null + * @param t the second String, must not be null + * @param threshold the target threshold, must not be negative + * @return result distance, or {@code -1} if the distance would be greater than the threshold + * @throws IllegalArgumentException if either String input {@code null} or negative threshold + */ + public static int getLevenshteinDistance(CharSequence s, CharSequence t, int threshold) { + if (s == null || t == null) { + throw new IllegalArgumentException("Strings must not be null"); + } + if (threshold < 0) { + throw new IllegalArgumentException("Threshold must not be negative"); + } + + /* + This implementation only computes the distance if it's less than or equal to the + threshold value, returning -1 if it's greater. The advantage is performance: unbounded + distance is O(nm), but a bound of k allows us to reduce it to O(km) time by only + computing a diagonal stripe of width 2k + 1 of the cost table. + It is also possible to use this to compute the unbounded Levenshtein distance by starting + the threshold at 1 and doubling each time until the distance is found; this is O(dm), where + d is the distance. + + One subtlety comes from needing to ignore entries on the border of our stripe + eg. + p[] = |#|#|#|* + d[] = *|#|#|#| + We must ignore the entry to the left of the leftmost member + We must ignore the entry above the rightmost member + + Another subtlety comes from our stripe running off the matrix if the strings aren't + of the same size. Since string s is always swapped to be the shorter of the two, + the stripe will always run off to the upper right instead of the lower left of the matrix. + + As a concrete example, suppose s is of length 5, t is of length 7, and our threshold is 1. + In this case we're going to walk a stripe of length 3. The matrix would look like so: + + 1 2 3 4 5 + 1 |#|#| | | | + 2 |#|#|#| | | + 3 | |#|#|#| | + 4 | | |#|#|#| + 5 | | | |#|#| + 6 | | | | |#| + 7 | | | | | | + + Note how the stripe leads off the table as there is no possible way to turn a string of length 5 + into one of length 7 in edit distance of 1. + + Additionally, this implementation decreases memory usage by using two + single-dimensional arrays and swapping them back and forth instead of allocating + an entire n by m matrix. This requires a few minor changes, such as immediately returning + when it's detected that the stripe has run off the matrix and initially filling the arrays with + large values so that entries we don't compute are ignored. + + See Algorithms on Strings, Trees and Sequences by Dan Gusfield for some discussion. + */ + + int n = s.length(); // length of s + int m = t.length(); // length of t + + // if one string is empty, the edit distance is necessarily the length of the other + if (n == 0) { + return m <= threshold ? m : -1; + } else if (m == 0) { + return n <= threshold ? n : -1; + } + + if (n > m) { + // swap the two strings to consume less memory + CharSequence tmp = s; + s = t; + t = tmp; + n = m; + m = t.length(); + } + + int p[] = new int[n + 1]; // 'previous' cost array, horizontally + int d[] = new int[n + 1]; // cost array, horizontally + int _d[]; // placeholder to assist in swapping p and d + + // fill in starting table values + int boundary = Math.min(n, threshold) + 1; + for (int i = 0; i < boundary; i++) { + p[i] = i; + } + // these fills ensure that the value above the rightmost entry of our + // stripe will be ignored in following loop iterations + Arrays.fill(p, boundary, p.length, Integer.MAX_VALUE); + Arrays.fill(d, Integer.MAX_VALUE); + + // iterates through t + for (int j = 1; j <= m; j++) { + char t_j = t.charAt(j - 1); // jth character of t + d[0] = j; + + // compute stripe indices, constrain to array size + int min = Math.max(1, j - threshold); + int max = Math.min(n, j + threshold); + + // the stripe may lead off of the table if s and t are of different sizes + if (min > max) { + return -1; + } + + // ignore entry left of leftmost + if (min > 1) { + d[min - 1] = Integer.MAX_VALUE; + } + + // iterates through [min, max] in s + for (int i = min; i <= max; i++) { + if (s.charAt(i - 1) == t_j) { + // diagonally left and up + d[i] = p[i - 1]; + } else { + // 1 + minimum of cell to the left, to the top, diagonally left and up + d[i] = 1 + Math.min(Math.min(d[i - 1], p[i]), p[i - 1]); + } + } + + // copy current distance counts to 'previous row' distance counts + _d = p; + p = d; + d = _d; + } + + // if p[n] is greater than the threshold, there's no guarantee on it being the correct + // distance + if (p[n] <= threshold) { + return p[n]; + } else { + return -1; + } + } + + // startsWith + //----------------------------------------------------------------------- + + /** + *

Check if a CharSequence starts with a specified prefix.

+ * + *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered to be equal. The comparison is case sensitive.

+ * + *
+     * StringUtils.startsWith(null, null)      = true
+     * StringUtils.startsWith(null, "abc")     = false
+     * StringUtils.startsWith("abcdef", null)  = false
+     * StringUtils.startsWith("abcdef", "abc") = true
+     * StringUtils.startsWith("ABCDEF", "abc") = false
+     * 
+ * + * @see java.lang.String#startsWith(String) + * @param str the CharSequence to check, may be null + * @param prefix the prefix to find, may be null + * @return {@code true} if the CharSequence starts with the prefix, case sensitive, or + * both {@code null} + * @since 2.4 + * @since 3.0 Changed signature from startsWith(String, String) to startsWith(CharSequence, CharSequence) + */ + public static boolean startsWith(CharSequence str, CharSequence prefix) { + return startsWith(str, prefix, false); + } + + /** + *

Case insensitive check if a CharSequence starts with a specified prefix.

+ * + *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered to be equal. The comparison is case insensitive.

+ * + *
+     * StringUtils.startsWithIgnoreCase(null, null)      = true
+     * StringUtils.startsWithIgnoreCase(null, "abc")     = false
+     * StringUtils.startsWithIgnoreCase("abcdef", null)  = false
+     * StringUtils.startsWithIgnoreCase("abcdef", "abc") = true
+     * StringUtils.startsWithIgnoreCase("ABCDEF", "abc") = true
+     * 
+ * + * @see java.lang.String#startsWith(String) + * @param str the CharSequence to check, may be null + * @param prefix the prefix to find, may be null + * @return {@code true} if the CharSequence starts with the prefix, case insensitive, or + * both {@code null} + * @since 2.4 + * @since 3.0 Changed signature from startsWithIgnoreCase(String, String) to startsWithIgnoreCase(CharSequence, CharSequence) + */ + public static boolean startsWithIgnoreCase(CharSequence str, CharSequence prefix) { + return startsWith(str, prefix, true); + } + + /** + *

Check if a CharSequence starts with a specified prefix (optionally case insensitive).

+ * + * @see java.lang.String#startsWith(String) + * @param str the CharSequence to check, may be null + * @param prefix the prefix to find, may be null + * @param ignoreCase indicates whether the compare should ignore case + * (case insensitive) or not. + * @return {@code true} if the CharSequence starts with the prefix or + * both {@code null} + */ + private static boolean startsWith(CharSequence str, CharSequence prefix, boolean ignoreCase) { + if (str == null || prefix == null) { + return str == null && prefix == null; + } + if (prefix.length() > str.length()) { + return false; + } + return CharSequenceUtils.regionMatches(str, ignoreCase, 0, prefix, 0, prefix.length()); + } + + /** + *

Check if a CharSequence starts with any of an array of specified strings.

+ * + *
+     * StringUtils.startsWithAny(null, null)      = false
+     * StringUtils.startsWithAny(null, new String[] {"abc"})  = false
+     * StringUtils.startsWithAny("abcxyz", null)     = false
+     * StringUtils.startsWithAny("abcxyz", new String[] {""}) = false
+     * StringUtils.startsWithAny("abcxyz", new String[] {"abc"}) = true
+     * StringUtils.startsWithAny("abcxyz", new String[] {null, "xyz", "abc"}) = true
+     * 
+ * + * @param string the CharSequence to check, may be null + * @param searchStrings the CharSequences to find, may be null or empty + * @return {@code true} if the CharSequence starts with any of the the prefixes, case insensitive, or + * both {@code null} + * @since 2.5 + * @since 3.0 Changed signature from startsWithAny(String, String[]) to startsWithAny(CharSequence, CharSequence...) + */ + public static boolean startsWithAny(CharSequence string, CharSequence... searchStrings) { + if (isEmpty(string) || ArrayUtils.isEmpty(searchStrings)) { + return false; + } + for (CharSequence searchString : searchStrings) { + if (StringUtils.startsWith(string, searchString)) { + return true; + } + } + return false; + } + + // endsWith + //----------------------------------------------------------------------- + + /** + *

Check if a CharSequence ends with a specified suffix.

+ * + *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered to be equal. The comparison is case sensitive.

+ * + *
+     * StringUtils.endsWith(null, null)      = true
+     * StringUtils.endsWith(null, "def")     = false
+     * StringUtils.endsWith("abcdef", null)  = false
+     * StringUtils.endsWith("abcdef", "def") = true
+     * StringUtils.endsWith("ABCDEF", "def") = false
+     * StringUtils.endsWith("ABCDEF", "cde") = false
+     * 
+ * + * @see java.lang.String#endsWith(String) + * @param str the CharSequence to check, may be null + * @param suffix the suffix to find, may be null + * @return {@code true} if the CharSequence ends with the suffix, case sensitive, or + * both {@code null} + * @since 2.4 + * @since 3.0 Changed signature from endsWith(String, String) to endsWith(CharSequence, CharSequence) + */ + public static boolean endsWith(CharSequence str, CharSequence suffix) { + return endsWith(str, suffix, false); + } + + /** + *

Case insensitive check if a CharSequence ends with a specified suffix.

+ * + *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered to be equal. The comparison is case insensitive.

+ * + *
+     * StringUtils.endsWithIgnoreCase(null, null)      = true
+     * StringUtils.endsWithIgnoreCase(null, "def")     = false
+     * StringUtils.endsWithIgnoreCase("abcdef", null)  = false
+     * StringUtils.endsWithIgnoreCase("abcdef", "def") = true
+     * StringUtils.endsWithIgnoreCase("ABCDEF", "def") = true
+     * StringUtils.endsWithIgnoreCase("ABCDEF", "cde") = false
+     * 
+ * + * @see java.lang.String#endsWith(String) + * @param str the CharSequence to check, may be null + * @param suffix the suffix to find, may be null + * @return {@code true} if the CharSequence ends with the suffix, case insensitive, or + * both {@code null} + * @since 2.4 + * @since 3.0 Changed signature from endsWithIgnoreCase(String, String) to endsWithIgnoreCase(CharSequence, CharSequence) + */ + public static boolean endsWithIgnoreCase(CharSequence str, CharSequence suffix) { + return endsWith(str, suffix, true); + } + + /** + *

Check if a CharSequence ends with a specified suffix (optionally case insensitive).

+ * + * @see java.lang.String#endsWith(String) + * @param str the CharSequence to check, may be null + * @param suffix the suffix to find, may be null + * @param ignoreCase indicates whether the compare should ignore case + * (case insensitive) or not. + * @return {@code true} if the CharSequence starts with the prefix or + * both {@code null} + */ + private static boolean endsWith(CharSequence str, CharSequence suffix, boolean ignoreCase) { + if (str == null || suffix == null) { + return str == null && suffix == null; + } + if (suffix.length() > str.length()) { + return false; + } + int strOffset = str.length() - suffix.length(); + return CharSequenceUtils.regionMatches(str, ignoreCase, strOffset, suffix, 0, suffix.length()); + } + + /** + *

+ * Similar to http://www.w3.org/TR/xpath/#function-normalize + * -space + *

+ *

+ * The function returns the argument string with whitespace normalized by using + * {@link #trim(String)} to remove leading and trailing whitespace + * and then replacing sequences of whitespace characters by a single space. + *

+ * In XML Whitespace characters are the same as those allowed by the S production, which is S ::= (#x20 | #x9 | #xD | #xA)+ + *

+ * Java's regexp pattern \s defines whitespace as [ \t\n\x0B\f\r] + *

+ * For reference: + *

    + *
  • \x0B = vertical tab
  • + *
  • \f = #xC = form feed
  • + *
  • #x20 = space
  • + *
  • #x9 = \t
  • + *
  • #xA = \n
  • + *
  • #xD = \r
  • + *
+ *

+ *

+ * The difference is that Java's whitespace includes vertical tab and form feed, which this functional will also + * normalize. Additionally {@link #trim(String)} removes control characters (char <= 32) from both + * ends of this String. + *

+ * + * @see Pattern + * @see #trim(String) + * @see http://www.w3.org/TR/xpath/#function-normalize-space + * @param str the source String to normalize whitespaces from, may be null + * @return the modified string with whitespace normalized, {@code null} if null String input + * + * @since 3.0 + */ + public static String normalizeSpace(String str) { + if (str == null) { + return null; + } + return WHITESPACE_BLOCK.matcher(trim(str)).replaceAll(" "); + } + + /** + *

Check if a CharSequence ends with any of an array of specified strings.

+ * + *
+     * StringUtils.endsWithAny(null, null)      = false
+     * StringUtils.endsWithAny(null, new String[] {"abc"})  = false
+     * StringUtils.endsWithAny("abcxyz", null)     = false
+     * StringUtils.endsWithAny("abcxyz", new String[] {""}) = true
+     * StringUtils.endsWithAny("abcxyz", new String[] {"xyz"}) = true
+     * StringUtils.endsWithAny("abcxyz", new String[] {null, "xyz", "abc"}) = true
+     * 
+ * + * @param string the CharSequence to check, may be null + * @param searchStrings the CharSequences to find, may be null or empty + * @return {@code true} if the CharSequence ends with any of the the prefixes, case insensitive, or + * both {@code null} + * @since 3.0 + */ + public static boolean endsWithAny(CharSequence string, CharSequence... searchStrings) { + if (isEmpty(string) || ArrayUtils.isEmpty(searchStrings)) { + return false; + } + for (CharSequence searchString : searchStrings) { + if (StringUtils.endsWith(string, searchString)) { + return true; + } + } + return false; + } + + /** + * Converts a byte[] to a String using the specified character encoding. + * + * @param bytes + * the byte array to read from + * @param charsetName + * the encoding to use, if null then use the platform default + * @return a new String + * @throws UnsupportedEncodingException + * If the named charset is not supported + * @throws NullPointerException + * if the input is null + * @since 3.1 + */ + public static String toString(byte[] bytes, String charsetName) throws UnsupportedEncodingException { + return charsetName == null ? new String(bytes) : new String(bytes, charsetName); + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/SystemUtils.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/SystemUtils.java new file mode 100644 index 00000000..e6a1c10f --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/SystemUtils.java @@ -0,0 +1,1443 @@ +/* + * 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 external.org.apache.commons.lang3; + +import java.io.File; + +/** + *

+ * Helpers for {@code java.lang.System}. + *

+ *

+ * If a system property cannot be read due to security restrictions, the corresponding field in this class will be set + * to {@code null} and a message will be written to {@code System.err}. + *

+ *

+ * #ThreadSafe# + *

+ * + * @since 1.0 + * @version $Id: SystemUtils.java 1199816 2011-11-09 16:11:34Z bayard $ + */ +public class SystemUtils { + + /** + * The prefix String for all Windows OS. + */ + private static final String OS_NAME_WINDOWS_PREFIX = "Windows"; + + // System property constants + // ----------------------------------------------------------------------- + // These MUST be declared first. Other constants depend on this. + + /** + * The System property key for the user home directory. + */ + private static final String USER_HOME_KEY = "user.home"; + + /** + * The System property key for the user directory. + */ + private static final String USER_DIR_KEY = "user.dir"; + + /** + * The System property key for the Java IO temporary directory. + */ + private static final String JAVA_IO_TMPDIR_KEY = "java.io.tmpdir"; + + /** + * The System property key for the Java home directory. + */ + private static final String JAVA_HOME_KEY = "java.home"; + + /** + *

+ * The {@code awt.toolkit} System Property. + *

+ *

+ * Holds a class name, on Windows XP this is {@code sun.awt.windows.WToolkit}. + *

+ *

+ * On platforms without a GUI, this value is {@code null}. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.1 + */ + public static final String AWT_TOOLKIT = getSystemProperty("awt.toolkit"); + + /** + *

+ * The {@code file.encoding} System Property. + *

+ *

+ * File encoding, such as {@code Cp1252}. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.0 + * @since Java 1.2 + */ + public static final String FILE_ENCODING = getSystemProperty("file.encoding"); + + /** + *

+ * The {@code file.separator} System Property. File separator ("/" on UNIX). + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String FILE_SEPARATOR = getSystemProperty("file.separator"); + + /** + *

+ * The {@code java.awt.fonts} System Property. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.1 + */ + public static final String JAVA_AWT_FONTS = getSystemProperty("java.awt.fonts"); + + /** + *

+ * The {@code java.awt.graphicsenv} System Property. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.1 + */ + public static final String JAVA_AWT_GRAPHICSENV = getSystemProperty("java.awt.graphicsenv"); + + /** + *

+ * The {@code java.awt.headless} System Property. The value of this property is the String {@code "true"} or + * {@code "false"}. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @see #isJavaAwtHeadless() + * @since 2.1 + * @since Java 1.4 + */ + public static final String JAVA_AWT_HEADLESS = getSystemProperty("java.awt.headless"); + + /** + *

+ * The {@code java.awt.printerjob} System Property. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.1 + */ + public static final String JAVA_AWT_PRINTERJOB = getSystemProperty("java.awt.printerjob"); + + /** + *

+ * The {@code java.class.path} System Property. Java class path. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String JAVA_CLASS_PATH = getSystemProperty("java.class.path"); + + /** + *

+ * The {@code java.class.version} System Property. Java class format version number. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String JAVA_CLASS_VERSION = getSystemProperty("java.class.version"); + + /** + *

+ * The {@code java.compiler} System Property. Name of JIT compiler to use. First in JDK version 1.2. Not used in Sun + * JDKs after 1.2. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.2. Not used in Sun versions after 1.2. + */ + public static final String JAVA_COMPILER = getSystemProperty("java.compiler"); + + /** + *

+ * The {@code java.endorsed.dirs} System Property. Path of endorsed directory or directories. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.4 + */ + public static final String JAVA_ENDORSED_DIRS = getSystemProperty("java.endorsed.dirs"); + + /** + *

+ * The {@code java.ext.dirs} System Property. Path of extension directory or directories. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.3 + */ + public static final String JAVA_EXT_DIRS = getSystemProperty("java.ext.dirs"); + + /** + *

+ * The {@code java.home} System Property. Java installation directory. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String JAVA_HOME = getSystemProperty(JAVA_HOME_KEY); + + /** + *

+ * The {@code java.io.tmpdir} System Property. Default temp file path. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.2 + */ + public static final String JAVA_IO_TMPDIR = getSystemProperty(JAVA_IO_TMPDIR_KEY); + + /** + *

+ * The {@code java.library.path} System Property. List of paths to search when loading libraries. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.2 + */ + public static final String JAVA_LIBRARY_PATH = getSystemProperty("java.library.path"); + + /** + *

+ * The {@code java.runtime.name} System Property. Java Runtime Environment name. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.0 + * @since Java 1.3 + */ + public static final String JAVA_RUNTIME_NAME = getSystemProperty("java.runtime.name"); + + /** + *

+ * The {@code java.runtime.version} System Property. Java Runtime Environment version. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.0 + * @since Java 1.3 + */ + public static final String JAVA_RUNTIME_VERSION = getSystemProperty("java.runtime.version"); + + /** + *

+ * The {@code java.specification.name} System Property. Java Runtime Environment specification name. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.2 + */ + public static final String JAVA_SPECIFICATION_NAME = getSystemProperty("java.specification.name"); + + /** + *

+ * The {@code java.specification.vendor} System Property. Java Runtime Environment specification vendor. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.2 + */ + public static final String JAVA_SPECIFICATION_VENDOR = getSystemProperty("java.specification.vendor"); + + /** + *

+ * The {@code java.specification.version} System Property. Java Runtime Environment specification version. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.3 + */ + public static final String JAVA_SPECIFICATION_VERSION = getSystemProperty("java.specification.version"); + private static final JavaVersion JAVA_SPECIFICATION_VERSION_AS_ENUM = JavaVersion.get(JAVA_SPECIFICATION_VERSION); + + /** + *

+ * The {@code java.util.prefs.PreferencesFactory} System Property. A class name. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.1 + * @since Java 1.4 + */ + public static final String JAVA_UTIL_PREFS_PREFERENCES_FACTORY = + getSystemProperty("java.util.prefs.PreferencesFactory"); + + /** + *

+ * The {@code java.vendor} System Property. Java vendor-specific string. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String JAVA_VENDOR = getSystemProperty("java.vendor"); + + /** + *

+ * The {@code java.vendor.url} System Property. Java vendor URL. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String JAVA_VENDOR_URL = getSystemProperty("java.vendor.url"); + + /** + *

+ * The {@code java.version} System Property. Java version number. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String JAVA_VERSION = getSystemProperty("java.version"); + + /** + *

+ * The {@code java.vm.info} System Property. Java Virtual Machine implementation info. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.0 + * @since Java 1.2 + */ + public static final String JAVA_VM_INFO = getSystemProperty("java.vm.info"); + + /** + *

+ * The {@code java.vm.name} System Property. Java Virtual Machine implementation name. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.2 + */ + public static final String JAVA_VM_NAME = getSystemProperty("java.vm.name"); + + /** + *

+ * The {@code java.vm.specification.name} System Property. Java Virtual Machine specification name. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.2 + */ + public static final String JAVA_VM_SPECIFICATION_NAME = getSystemProperty("java.vm.specification.name"); + + /** + *

+ * The {@code java.vm.specification.vendor} System Property. Java Virtual Machine specification vendor. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.2 + */ + public static final String JAVA_VM_SPECIFICATION_VENDOR = getSystemProperty("java.vm.specification.vendor"); + + /** + *

+ * The {@code java.vm.specification.version} System Property. Java Virtual Machine specification version. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.2 + */ + public static final String JAVA_VM_SPECIFICATION_VERSION = getSystemProperty("java.vm.specification.version"); + + /** + *

+ * The {@code java.vm.vendor} System Property. Java Virtual Machine implementation vendor. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.2 + */ + public static final String JAVA_VM_VENDOR = getSystemProperty("java.vm.vendor"); + + /** + *

+ * The {@code java.vm.version} System Property. Java Virtual Machine implementation version. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.2 + */ + public static final String JAVA_VM_VERSION = getSystemProperty("java.vm.version"); + + /** + *

+ * The {@code line.separator} System Property. Line separator ("\n" on UNIX). + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String LINE_SEPARATOR = getSystemProperty("line.separator"); + + /** + *

+ * The {@code os.arch} System Property. Operating system architecture. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String OS_ARCH = getSystemProperty("os.arch"); + + /** + *

+ * The {@code os.name} System Property. Operating system name. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String OS_NAME = getSystemProperty("os.name"); + + /** + *

+ * The {@code os.version} System Property. Operating system version. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String OS_VERSION = getSystemProperty("os.version"); + + /** + *

+ * The {@code path.separator} System Property. Path separator (":" on UNIX). + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String PATH_SEPARATOR = getSystemProperty("path.separator"); + + /** + *

+ * The {@code user.country} or {@code user.region} System Property. User's country code, such as {@code GB}. First + * in Java version 1.2 as {@code user.region}. Renamed to {@code user.country} in 1.4 + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.0 + * @since Java 1.2 + */ + public static final String USER_COUNTRY = getSystemProperty("user.country") == null ? + getSystemProperty("user.region") : getSystemProperty("user.country"); + + /** + *

+ * The {@code user.dir} System Property. User's current working directory. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String USER_DIR = getSystemProperty(USER_DIR_KEY); + + /** + *

+ * The {@code user.home} System Property. User's home directory. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String USER_HOME = getSystemProperty(USER_HOME_KEY); + + /** + *

+ * The {@code user.language} System Property. User's language code, such as {@code "en"}. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.0 + * @since Java 1.2 + */ + public static final String USER_LANGUAGE = getSystemProperty("user.language"); + + /** + *

+ * The {@code user.name} System Property. User's account name. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since Java 1.1 + */ + public static final String USER_NAME = getSystemProperty("user.name"); + + /** + *

+ * The {@code user.timezone} System Property. For example: {@code "America/Los_Angeles"}. + *

+ *

+ * Defaults to {@code null} if the runtime does not have security access to read this property or the property does + * not exist. + *

+ *

+ * This value is initialized when the class is loaded. If {@link System#setProperty(String,String)} or + * {@link System#setProperties(java.util.Properties)} is called after this class is loaded, the value will be out of + * sync with that System property. + *

+ * + * @since 2.1 + */ + public static final String USER_TIMEZONE = getSystemProperty("user.timezone"); + + // Java version checks + // ----------------------------------------------------------------------- + // These MUST be declared after those above as they depend on the + // values being set up + + /** + *

+ * Is {@code true} if this is Java version 1.1 (also 1.1.x versions). + *

+ *

+ * The field will return {@code false} if {@link #JAVA_VERSION} is {@code null}. + *

+ */ + public static final boolean IS_JAVA_1_1 = getJavaVersionMatches("1.1"); + + /** + *

+ * Is {@code true} if this is Java version 1.2 (also 1.2.x versions). + *

+ *

+ * The field will return {@code false} if {@link #JAVA_VERSION} is {@code null}. + *

+ */ + public static final boolean IS_JAVA_1_2 = getJavaVersionMatches("1.2"); + + /** + *

+ * Is {@code true} if this is Java version 1.3 (also 1.3.x versions). + *

+ *

+ * The field will return {@code false} if {@link #JAVA_VERSION} is {@code null}. + *

+ */ + public static final boolean IS_JAVA_1_3 = getJavaVersionMatches("1.3"); + + /** + *

+ * Is {@code true} if this is Java version 1.4 (also 1.4.x versions). + *

+ *

+ * The field will return {@code false} if {@link #JAVA_VERSION} is {@code null}. + *

+ */ + public static final boolean IS_JAVA_1_4 = getJavaVersionMatches("1.4"); + + /** + *

+ * Is {@code true} if this is Java version 1.5 (also 1.5.x versions). + *

+ *

+ * The field will return {@code false} if {@link #JAVA_VERSION} is {@code null}. + *

+ */ + public static final boolean IS_JAVA_1_5 = getJavaVersionMatches("1.5"); + + /** + *

+ * Is {@code true} if this is Java version 1.6 (also 1.6.x versions). + *

+ *

+ * The field will return {@code false} if {@link #JAVA_VERSION} is {@code null}. + *

+ */ + public static final boolean IS_JAVA_1_6 = getJavaVersionMatches("1.6"); + + /** + *

+ * Is {@code true} if this is Java version 1.7 (also 1.7.x versions). + *

+ *

+ * The field will return {@code false} if {@link #JAVA_VERSION} is {@code null}. + *

+ * + * @since 3.0 + */ + public static final boolean IS_JAVA_1_7 = getJavaVersionMatches("1.7"); + + // Operating system checks + // ----------------------------------------------------------------------- + // These MUST be declared after those above as they depend on the + // values being set up + // OS names from http://www.vamphq.com/os.html + // Selected ones included - please advise dev@commons.apache.org + // if you want another added or a mistake corrected + + /** + *

+ * Is {@code true} if this is AIX. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_AIX = getOSMatchesName("AIX"); + + /** + *

+ * Is {@code true} if this is HP-UX. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_HP_UX = getOSMatchesName("HP-UX"); + + /** + *

+ * Is {@code true} if this is Irix. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_IRIX = getOSMatchesName("Irix"); + + /** + *

+ * Is {@code true} if this is Linux. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_LINUX = getOSMatchesName("Linux") || getOSMatchesName("LINUX"); + + /** + *

+ * Is {@code true} if this is Mac. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_MAC = getOSMatchesName("Mac"); + + /** + *

+ * Is {@code true} if this is Mac. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_MAC_OSX = getOSMatchesName("Mac OS X"); + + /** + *

+ * Is {@code true} if this is FreeBSD. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 3.1 + */ + public static final boolean IS_OS_FREE_BSD = getOSMatchesName("FreeBSD"); + + /** + *

+ * Is {@code true} if this is OpenBSD. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 3.1 + */ + public static final boolean IS_OS_OPEN_BSD = getOSMatchesName("OpenBSD"); + + /** + *

+ * Is {@code true} if this is NetBSD. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 3.1 + */ + public static final boolean IS_OS_NET_BSD = getOSMatchesName("NetBSD"); + + /** + *

+ * Is {@code true} if this is OS/2. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_OS2 = getOSMatchesName("OS/2"); + + /** + *

+ * Is {@code true} if this is Solaris. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_SOLARIS = getOSMatchesName("Solaris"); + + /** + *

+ * Is {@code true} if this is SunOS. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_SUN_OS = getOSMatchesName("SunOS"); + + /** + *

+ * Is {@code true} if this is a UNIX like system, as in any of AIX, HP-UX, Irix, Linux, MacOSX, Solaris or SUN OS. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.1 + */ + public static final boolean IS_OS_UNIX = IS_OS_AIX || IS_OS_HP_UX || IS_OS_IRIX || IS_OS_LINUX || IS_OS_MAC_OSX + || IS_OS_SOLARIS || IS_OS_SUN_OS || IS_OS_FREE_BSD || IS_OS_OPEN_BSD || IS_OS_NET_BSD; + + /** + *

+ * Is {@code true} if this is Windows. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_WINDOWS = getOSMatchesName(OS_NAME_WINDOWS_PREFIX); + + /** + *

+ * Is {@code true} if this is Windows 2000. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_WINDOWS_2000 = getOSMatches(OS_NAME_WINDOWS_PREFIX, "5.0"); + + /** + *

+ * Is {@code true} if this is Windows 2003. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 3.1 + */ + public static final boolean IS_OS_WINDOWS_2003 = getOSMatches(OS_NAME_WINDOWS_PREFIX, "5.2"); + + /** + *

+ * Is {@code true} if this is Windows 2008. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 3.1 + */ + public static final boolean IS_OS_WINDOWS_2008 = getOSMatches(OS_NAME_WINDOWS_PREFIX + " Server 2008", "6.1"); + + /** + *

+ * Is {@code true} if this is Windows 95. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_WINDOWS_95 = getOSMatches(OS_NAME_WINDOWS_PREFIX + " 9", "4.0"); + // Java 1.2 running on Windows98 returns 'Windows 95', hence the above + + /** + *

+ * Is {@code true} if this is Windows 98. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_WINDOWS_98 = getOSMatches(OS_NAME_WINDOWS_PREFIX + " 9", "4.1"); + // Java 1.2 running on Windows98 returns 'Windows 95', hence the above + + /** + *

+ * Is {@code true} if this is Windows ME. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_WINDOWS_ME = getOSMatches(OS_NAME_WINDOWS_PREFIX, "4.9"); + // Java 1.2 running on WindowsME may return 'Windows 95', hence the above + + /** + *

+ * Is {@code true} if this is Windows NT. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_WINDOWS_NT = getOSMatchesName(OS_NAME_WINDOWS_PREFIX + " NT"); + // Windows 2000 returns 'Windows 2000' but may suffer from same Java1.2 problem + + /** + *

+ * Is {@code true} if this is Windows XP. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.0 + */ + public static final boolean IS_OS_WINDOWS_XP = getOSMatches(OS_NAME_WINDOWS_PREFIX, "5.1"); + + // ----------------------------------------------------------------------- + /** + *

+ * Is {@code true} if this is Windows Vista. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 2.4 + */ + public static final boolean IS_OS_WINDOWS_VISTA = getOSMatches(OS_NAME_WINDOWS_PREFIX, "6.0"); + + /** + *

+ * Is {@code true} if this is Windows 7. + *

+ *

+ * The field will return {@code false} if {@code OS_NAME} is {@code null}. + *

+ * + * @since 3.0 + */ + public static final boolean IS_OS_WINDOWS_7 = getOSMatches(OS_NAME_WINDOWS_PREFIX, "6.1"); + + /** + *

+ * Gets the Java home directory as a {@code File}. + *

+ * + * @return a directory + * @throws SecurityException if a security manager exists and its {@code checkPropertyAccess} method doesn't allow + * access to the specified system property. + * @see System#getProperty(String) + * @since 2.1 + */ + public static File getJavaHome() { + return new File(System.getProperty(JAVA_HOME_KEY)); + } + + /** + *

+ * Gets the Java IO temporary directory as a {@code File}. + *

+ * + * @return a directory + * @throws SecurityException if a security manager exists and its {@code checkPropertyAccess} method doesn't allow + * access to the specified system property. + * @see System#getProperty(String) + * @since 2.1 + */ + public static File getJavaIoTmpDir() { + return new File(System.getProperty(JAVA_IO_TMPDIR_KEY)); + } + + /** + *

+ * Decides if the Java version matches. + *

+ * + * @param versionPrefix the prefix for the java version + * @return true if matches, or false if not or can't determine + */ + private static boolean getJavaVersionMatches(String versionPrefix) { + return isJavaVersionMatch(JAVA_SPECIFICATION_VERSION, versionPrefix); + } + + /** + * Decides if the operating system matches. + * + * @param osNamePrefix the prefix for the os name + * @param osVersionPrefix the prefix for the version + * @return true if matches, or false if not or can't determine + */ + private static boolean getOSMatches(String osNamePrefix, String osVersionPrefix) { + return isOSMatch(OS_NAME, OS_VERSION, osNamePrefix, osVersionPrefix); + } + + /** + * Decides if the operating system matches. + * + * @param osNamePrefix the prefix for the os name + * @return true if matches, or false if not or can't determine + */ + private static boolean getOSMatchesName(String osNamePrefix) { + return isOSNameMatch(OS_NAME, osNamePrefix); + } + + // ----------------------------------------------------------------------- + /** + *

+ * Gets a System property, defaulting to {@code null} if the property cannot be read. + *

+ *

+ * If a {@code SecurityException} is caught, the return value is {@code null} and a message is written to + * {@code System.err}. + *

+ * + * @param property the system property name + * @return the system property value or {@code null} if a security problem occurs + */ + private static String getSystemProperty(String property) { + try { + return System.getProperty(property); + } catch (SecurityException ex) { + // we are not allowed to look at this property + System.err.println("Caught a SecurityException reading the system property '" + property + + "'; the SystemUtils property value will default to null."); + return null; + } + } + + /** + *

+ * Gets the user directory as a {@code File}. + *

+ * + * @return a directory + * @throws SecurityException if a security manager exists and its {@code checkPropertyAccess} method doesn't allow + * access to the specified system property. + * @see System#getProperty(String) + * @since 2.1 + */ + public static File getUserDir() { + return new File(System.getProperty(USER_DIR_KEY)); + } + + /** + *

+ * Gets the user home directory as a {@code File}. + *

+ * + * @return a directory + * @throws SecurityException if a security manager exists and its {@code checkPropertyAccess} method doesn't allow + * access to the specified system property. + * @see System#getProperty(String) + * @since 2.1 + */ + public static File getUserHome() { + return new File(System.getProperty(USER_HOME_KEY)); + } + + /** + * Returns whether the {@link #JAVA_AWT_HEADLESS} value is {@code true}. + * + * @return {@code true} if {@code JAVA_AWT_HEADLESS} is {@code "true"}, {@code false} otherwise. + * @see #JAVA_AWT_HEADLESS + * @since 2.1 + * @since Java 1.4 + */ + public static boolean isJavaAwtHeadless() { + return JAVA_AWT_HEADLESS != null ? JAVA_AWT_HEADLESS.equals(Boolean.TRUE.toString()) : false; + } + + /** + *

+ * Is the Java version at least the requested version. + *

+ *

+ * Example input: + *

+ *
    + *
  • {@code 1.2f} to test for Java 1.2
  • + *
  • {@code 1.31f} to test for Java 1.3.1
  • + *
+ * + * @param requiredVersion the required version, for example 1.31f + * @return {@code true} if the actual version is equal or greater than the required version + */ + public static boolean isJavaVersionAtLeast(JavaVersion requiredVersion) { + return JAVA_SPECIFICATION_VERSION_AS_ENUM.atLeast(requiredVersion); + } + + /** + *

+ * Decides if the Java version matches. + *

+ *

+ * This method is package private instead of private to support unit test invocation. + *

+ * + * @param version the actual Java version + * @param versionPrefix the prefix for the expected Java version + * @return true if matches, or false if not or can't determine + */ + static boolean isJavaVersionMatch(String version, String versionPrefix) { + if (version == null) { + return false; + } + return version.startsWith(versionPrefix); + } + + /** + * Decides if the operating system matches. + *

+ * This method is package private instead of private to support unit test invocation. + *

+ * + * @param osName the actual OS name + * @param osVersion the actual OS version + * @param osNamePrefix the prefix for the expected OS name + * @param osVersionPrefix the prefix for the expected OS version + * @return true if matches, or false if not or can't determine + */ + static boolean isOSMatch(String osName, String osVersion, String osNamePrefix, String osVersionPrefix) { + if (osName == null || osVersion == null) { + return false; + } + return osName.startsWith(osNamePrefix) && osVersion.startsWith(osVersionPrefix); + } + + /** + * Decides if the operating system matches. + *

+ * This method is package private instead of private to support unit test invocation. + *

+ * + * @param osName the actual OS name + * @param osNamePrefix the prefix for the expected OS name + * @return true if matches, or false if not or can't determine + */ + static boolean isOSNameMatch(String osName, String osNamePrefix) { + if (osName == null) { + return false; + } + return osName.startsWith(osNamePrefix); + } + + // ----------------------------------------------------------------------- + /** + *

+ * SystemUtils instances should NOT be constructed in standard programming. Instead, the class should be used as + * {@code SystemUtils.FILE_SEPARATOR}. + *

+ *

+ * This constructor is public to permit tools that require a JavaBean instance to operate. + *

+ */ + public SystemUtils() { + super(); + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/Validate.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/Validate.java new file mode 100644 index 00000000..72213854 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/Validate.java @@ -0,0 +1,1070 @@ +/* + * 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 external.org.apache.commons.lang3; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.regex.Pattern; + +/** + *

This class assists in validating arguments. The validation methods are + * based along the following principles: + *

    + *
  • An invalid {@code null} argument causes a {@link NullPointerException}.
  • + *
  • A non-{@code null} argument causes an {@link IllegalArgumentException}.
  • + *
  • An invalid index into an array/collection/map/string causes an {@link IndexOutOfBoundsException}.
  • + *
+ * + *

All exceptions messages are + * format strings + * as defined by the Java platform. For example:

+ * + *
+ * Validate.isTrue(i > 0, "The value must be greater than zero: %d", i);
+ * Validate.notNull(surname, "The surname must not be %s", null);
+ * 
+ * + *

#ThreadSafe#

+ * @version $Id: Validate.java 1199983 2011-11-09 21:41:24Z ggregory $ + * @see java.lang.String#format(String, Object...) + * @since 2.0 + */ +public class Validate { + + private static final String DEFAULT_EXCLUSIVE_BETWEEN_EX_MESSAGE = + "The value %s is not in the specified exclusive range of %s to %s"; + private static final String DEFAULT_INCLUSIVE_BETWEEN_EX_MESSAGE = + "The value %s is not in the specified inclusive range of %s to %s"; + private static final String DEFAULT_MATCHES_PATTERN_EX = "The string %s does not match the pattern %s"; + private static final String DEFAULT_IS_NULL_EX_MESSAGE = "The validated object is null"; + private static final String DEFAULT_IS_TRUE_EX_MESSAGE = "The validated expression is false"; + private static final String DEFAULT_NO_NULL_ELEMENTS_ARRAY_EX_MESSAGE = + "The validated array contains null element at index: %d"; + private static final String DEFAULT_NO_NULL_ELEMENTS_COLLECTION_EX_MESSAGE = + "The validated collection contains null element at index: %d"; + private static final String DEFAULT_NOT_BLANK_EX_MESSAGE = "The validated character sequence is blank"; + private static final String DEFAULT_NOT_EMPTY_ARRAY_EX_MESSAGE = "The validated array is empty"; + private static final String DEFAULT_NOT_EMPTY_CHAR_SEQUENCE_EX_MESSAGE = + "The validated character sequence is empty"; + private static final String DEFAULT_NOT_EMPTY_COLLECTION_EX_MESSAGE = "The validated collection is empty"; + private static final String DEFAULT_NOT_EMPTY_MAP_EX_MESSAGE = "The validated map is empty"; + private static final String DEFAULT_VALID_INDEX_ARRAY_EX_MESSAGE = "The validated array index is invalid: %d"; + private static final String DEFAULT_VALID_INDEX_CHAR_SEQUENCE_EX_MESSAGE = + "The validated character sequence index is invalid: %d"; + private static final String DEFAULT_VALID_INDEX_COLLECTION_EX_MESSAGE = + "The validated collection index is invalid: %d"; + private static final String DEFAULT_VALID_STATE_EX_MESSAGE = "The validated state is false"; + private static final String DEFAULT_IS_ASSIGNABLE_EX_MESSAGE = "Cannot assign a %s to a %s"; + private static final String DEFAULT_IS_INSTANCE_OF_EX_MESSAGE = "Expected type: %s, actual: %s"; + + /** + * Constructor. This class should not normally be instantiated. + */ + public Validate() { + super(); + } + + // isTrue + //--------------------------------------------------------------------------------- + + /** + *

Validate that the argument condition is {@code true}; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ * + *
Validate.isTrue(i > 0.0, "The value must be greater than zero: %d", i);
+ * + *

For performance reasons, the long value is passed as a separate parameter and + * appended to the exception message only in the case of an error.

+ * + * @param expression the boolean expression to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param value the value to append to the message when invalid + * @throws IllegalArgumentException if expression is {@code false} + * @see #isTrue(boolean) + * @see #isTrue(boolean, String, double) + * @see #isTrue(boolean, String, Object...) + */ + public static void isTrue(boolean expression, String message, long value) { + if (expression == false) { + throw new IllegalArgumentException(String.format(message, Long.valueOf(value))); + } + } + + /** + *

Validate that the argument condition is {@code true}; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ * + *
Validate.isTrue(d > 0.0, "The value must be greater than zero: %s", d);
+ * + *

For performance reasons, the double value is passed as a separate parameter and + * appended to the exception message only in the case of an error.

+ * + * @param expression the boolean expression to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param value the value to append to the message when invalid + * @throws IllegalArgumentException if expression is {@code false} + * @see #isTrue(boolean) + * @see #isTrue(boolean, String, long) + * @see #isTrue(boolean, String, Object...) + */ + public static void isTrue(boolean expression, String message, double value) { + if (expression == false) { + throw new IllegalArgumentException(String.format(message, Double.valueOf(value))); + } + } + + /** + *

Validate that the argument condition is {@code true}; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ * + *
+     * Validate.isTrue(i >= min && i <= max, "The value must be between %d and %d", min, max);
+     * Validate.isTrue(myObject.isOk(), "The object is not okay");
+ * + * @param expression the boolean expression to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if expression is {@code false} + * @see #isTrue(boolean) + * @see #isTrue(boolean, String, long) + * @see #isTrue(boolean, String, double) + */ + public static void isTrue(boolean expression, String message, Object... values) { + if (expression == false) { + throw new IllegalArgumentException(String.format(message, values)); + } + } + + /** + *

Validate that the argument condition is {@code true}; otherwise + * throwing an exception. This method is useful when validating according + * to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ * + *
+     * Validate.isTrue(i > 0);
+     * Validate.isTrue(myObject.isOk());
+ * + *

The message of the exception is "The validated expression is + * false".

+ * + * @param expression the boolean expression to check + * @throws IllegalArgumentException if expression is {@code false} + * @see #isTrue(boolean, String, long) + * @see #isTrue(boolean, String, double) + * @see #isTrue(boolean, String, Object...) + */ + public static void isTrue(boolean expression) { + if (expression == false) { + throw new IllegalArgumentException(DEFAULT_IS_TRUE_EX_MESSAGE); + } + } + + // notNull + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument is not {@code null}; + * otherwise throwing an exception. + * + *

Validate.notNull(myObject, "The object must not be null");
+ * + *

The message of the exception is "The validated object is + * null".

+ * + * @param the object type + * @param object the object to check + * @return the validated object (never {@code null} for method chaining) + * @throws NullPointerException if the object is {@code null} + * @see #notNull(Object, String, Object...) + */ + public static T notNull(T object) { + return notNull(object, DEFAULT_IS_NULL_EX_MESSAGE); + } + + /** + *

Validate that the specified argument is not {@code null}; + * otherwise throwing an exception with the specified message. + * + *

Validate.notNull(myObject, "The object must not be null");
+ * + * @param the object type + * @param object the object to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message + * @return the validated object (never {@code null} for method chaining) + * @throws NullPointerException if the object is {@code null} + * @see #notNull(Object) + */ + public static T notNull(T object, String message, Object... values) { + if (object == null) { + throw new NullPointerException(String.format(message, values)); + } + return object; + } + + // notEmpty array + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument array is neither {@code null} + * nor a length of zero (no elements); otherwise throwing an exception + * with the specified message. + * + *

Validate.notEmpty(myArray, "The array must not be empty");
+ * + * @param the array type + * @param array the array to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated array (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if the array is empty + * @see #notEmpty(Object[]) + */ + public static T[] notEmpty(T[] array, String message, Object... values) { + if (array == null) { + throw new NullPointerException(String.format(message, values)); + } + if (array.length == 0) { + throw new IllegalArgumentException(String.format(message, values)); + } + return array; + } + + /** + *

Validate that the specified argument array is neither {@code null} + * nor a length of zero (no elements); otherwise throwing an exception. + * + *

Validate.notEmpty(myArray);
+ * + *

The message in the exception is "The validated array is + * empty". + * + * @param the array type + * @param array the array to check, validated not null by this method + * @return the validated array (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if the array is empty + * @see #notEmpty(Object[], String, Object...) + */ + public static T[] notEmpty(T[] array) { + return notEmpty(array, DEFAULT_NOT_EMPTY_ARRAY_EX_MESSAGE); + } + + // notEmpty collection + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument collection is neither {@code null} + * nor a size of zero (no elements); otherwise throwing an exception + * with the specified message. + * + *

Validate.notEmpty(myCollection, "The collection must not be empty");
+ * + * @param the collection type + * @param collection the collection to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated collection (never {@code null} method for chaining) + * @throws NullPointerException if the collection is {@code null} + * @throws IllegalArgumentException if the collection is empty + * @see #notEmpty(Object[]) + */ + public static > T notEmpty(T collection, String message, Object... values) { + if (collection == null) { + throw new NullPointerException(String.format(message, values)); + } + if (collection.isEmpty()) { + throw new IllegalArgumentException(String.format(message, values)); + } + return collection; + } + + /** + *

Validate that the specified argument collection is neither {@code null} + * nor a size of zero (no elements); otherwise throwing an exception. + * + *

Validate.notEmpty(myCollection);
+ * + *

The message in the exception is "The validated collection is + * empty".

+ * + * @param the collection type + * @param collection the collection to check, validated not null by this method + * @return the validated collection (never {@code null} method for chaining) + * @throws NullPointerException if the collection is {@code null} + * @throws IllegalArgumentException if the collection is empty + * @see #notEmpty(Collection, String, Object...) + */ + public static > T notEmpty(T collection) { + return notEmpty(collection, DEFAULT_NOT_EMPTY_COLLECTION_EX_MESSAGE); + } + + // notEmpty map + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument map is neither {@code null} + * nor a size of zero (no elements); otherwise throwing an exception + * with the specified message. + * + *

Validate.notEmpty(myMap, "The map must not be empty");
+ * + * @param the map type + * @param map the map to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated map (never {@code null} method for chaining) + * @throws NullPointerException if the map is {@code null} + * @throws IllegalArgumentException if the map is empty + * @see #notEmpty(Object[]) + */ + public static > T notEmpty(T map, String message, Object... values) { + if (map == null) { + throw new NullPointerException(String.format(message, values)); + } + if (map.isEmpty()) { + throw new IllegalArgumentException(String.format(message, values)); + } + return map; + } + + /** + *

Validate that the specified argument map is neither {@code null} + * nor a size of zero (no elements); otherwise throwing an exception. + * + *

Validate.notEmpty(myMap);
+ * + *

The message in the exception is "The validated map is + * empty".

+ * + * @param the map type + * @param map the map to check, validated not null by this method + * @return the validated map (never {@code null} method for chaining) + * @throws NullPointerException if the map is {@code null} + * @throws IllegalArgumentException if the map is empty + * @see #notEmpty(Map, String, Object...) + */ + public static > T notEmpty(T map) { + return notEmpty(map, DEFAULT_NOT_EMPTY_MAP_EX_MESSAGE); + } + + // notEmpty string + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument character sequence is + * neither {@code null} nor a length of zero (no characters); + * otherwise throwing an exception with the specified message. + * + *

Validate.notEmpty(myString, "The string must not be empty");
+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated character sequence (never {@code null} method for chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IllegalArgumentException if the character sequence is empty + * @see #notEmpty(CharSequence) + */ + public static T notEmpty(T chars, String message, Object... values) { + if (chars == null) { + throw new NullPointerException(String.format(message, values)); + } + if (chars.length() == 0) { + throw new IllegalArgumentException(String.format(message, values)); + } + return chars; + } + + /** + *

Validate that the specified argument character sequence is + * neither {@code null} nor a length of zero (no characters); + * otherwise throwing an exception with the specified message. + * + *

Validate.notEmpty(myString);
+ * + *

The message in the exception is "The validated + * character sequence is empty".

+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @return the validated character sequence (never {@code null} method for chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IllegalArgumentException if the character sequence is empty + * @see #notEmpty(CharSequence, String, Object...) + */ + public static T notEmpty(T chars) { + return notEmpty(chars, DEFAULT_NOT_EMPTY_CHAR_SEQUENCE_EX_MESSAGE); + } + + // notBlank string + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument character sequence is + * neither {@code null}, a length of zero (no characters), empty + * nor whitespace; otherwise throwing an exception with the specified + * message. + * + *

Validate.notBlank(myString, "The string must not be blank");
+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated character sequence (never {@code null} method for chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IllegalArgumentException if the character sequence is blank + * @see #notBlank(CharSequence) + * + * @since 3.0 + */ + public static T notBlank(T chars, String message, Object... values) { + if (chars == null) { + throw new NullPointerException(String.format(message, values)); + } + if (StringUtils.isBlank(chars)) { + throw new IllegalArgumentException(String.format(message, values)); + } + return chars; + } + + /** + *

Validate that the specified argument character sequence is + * neither {@code null}, a length of zero (no characters), empty + * nor whitespace; otherwise throwing an exception. + * + *

Validate.notBlank(myString);
+ * + *

The message in the exception is "The validated character + * sequence is blank".

+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @return the validated character sequence (never {@code null} method for chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IllegalArgumentException if the character sequence is blank + * @see #notBlank(CharSequence, String, Object...) + * + * @since 3.0 + */ + public static T notBlank(T chars) { + return notBlank(chars, DEFAULT_NOT_BLANK_EX_MESSAGE); + } + + // noNullElements array + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument array is neither + * {@code null} nor contains any elements that are {@code null}; + * otherwise throwing an exception with the specified message. + * + *

Validate.noNullElements(myArray, "The array contain null at position %d");
+ * + *

If the array is {@code null}, then the message in the exception + * is "The validated object is null".

+ * + *

If the array has a {@code null} element, then the iteration + * index of the invalid element is appended to the {@code values} + * argument.

+ * + * @param the array type + * @param array the array to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated array (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if an element is {@code null} + * @see #noNullElements(Object[]) + */ + public static T[] noNullElements(T[] array, String message, Object... values) { + Validate.notNull(array); + for (int i = 0; i < array.length; i++) { + if (array[i] == null) { + Object[] values2 = ArrayUtils.add(values, Integer.valueOf(i)); + throw new IllegalArgumentException(String.format(message, values2)); + } + } + return array; + } + + /** + *

Validate that the specified argument array is neither + * {@code null} nor contains any elements that are {@code null}; + * otherwise throwing an exception. + * + *

Validate.noNullElements(myArray);
+ * + *

If the array is {@code null}, then the message in the exception + * is "The validated object is null".

+ * + *

If the array has a {@code null} element, then the message in the + * exception is "The validated array contains null element at index: + * " followed by the index.

+ * + * @param the array type + * @param array the array to check, validated not null by this method + * @return the validated array (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if an element is {@code null} + * @see #noNullElements(Object[], String, Object...) + */ + public static T[] noNullElements(T[] array) { + return noNullElements(array, DEFAULT_NO_NULL_ELEMENTS_ARRAY_EX_MESSAGE); + } + + // noNullElements iterable + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument iterable is neither + * {@code null} nor contains any elements that are {@code null}; + * otherwise throwing an exception with the specified message. + * + *

Validate.noNullElements(myCollection, "The collection contains null at position %d");
+ * + *

If the iterable is {@code null}, then the message in the exception + * is "The validated object is null".

+ * + *

If the iterable has a {@code null} element, then the iteration + * index of the invalid element is appended to the {@code values} + * argument.

+ * + * @param the iterable type + * @param iterable the iterable to check, validated not null by this method + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated iterable (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if an element is {@code null} + * @see #noNullElements(Iterable) + */ + public static > T noNullElements(T iterable, String message, Object... values) { + Validate.notNull(iterable); + int i = 0; + for (Iterator it = iterable.iterator(); it.hasNext(); i++) { + if (it.next() == null) { + Object[] values2 = ArrayUtils.addAll(values, Integer.valueOf(i)); + throw new IllegalArgumentException(String.format(message, values2)); + } + } + return iterable; + } + + /** + *

Validate that the specified argument iterable is neither + * {@code null} nor contains any elements that are {@code null}; + * otherwise throwing an exception. + * + *

Validate.noNullElements(myCollection);
+ * + *

If the iterable is {@code null}, then the message in the exception + * is "The validated object is null".

+ * + *

If the array has a {@code null} element, then the message in the + * exception is "The validated iterable contains null element at index: + * " followed by the index.

+ * + * @param the iterable type + * @param iterable the iterable to check, validated not null by this method + * @return the validated iterable (never {@code null} method for chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IllegalArgumentException if an element is {@code null} + * @see #noNullElements(Iterable, String, Object...) + */ + public static > T noNullElements(T iterable) { + return noNullElements(iterable, DEFAULT_NO_NULL_ELEMENTS_COLLECTION_EX_MESSAGE); + } + + // validIndex array + //--------------------------------------------------------------------------------- + + /** + *

Validates that the index is within the bounds of the argument + * array; otherwise throwing an exception with the specified message.

+ * + *
Validate.validIndex(myArray, 2, "The array index is invalid: ");
+ * + *

If the array is {@code null}, then the message of the exception + * is "The validated object is null".

+ * + * @param the array type + * @param array the array to check, validated not null by this method + * @param index the index to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated array (never {@code null} for method chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(Object[], int) + * + * @since 3.0 + */ + public static T[] validIndex(T[] array, int index, String message, Object... values) { + Validate.notNull(array); + if (index < 0 || index >= array.length) { + throw new IndexOutOfBoundsException(String.format(message, values)); + } + return array; + } + + /** + *

Validates that the index is within the bounds of the argument + * array; otherwise throwing an exception.

+ * + *
Validate.validIndex(myArray, 2);
+ * + *

If the array is {@code null}, then the message of the exception + * is "The validated object is null".

+ * + *

If the index is invalid, then the message of the exception is + * "The validated array index is invalid: " followed by the + * index.

+ * + * @param the array type + * @param array the array to check, validated not null by this method + * @param index the index to check + * @return the validated array (never {@code null} for method chaining) + * @throws NullPointerException if the array is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(Object[], int, String, Object...) + * + * @since 3.0 + */ + public static T[] validIndex(T[] array, int index) { + return validIndex(array, index, DEFAULT_VALID_INDEX_ARRAY_EX_MESSAGE, Integer.valueOf(index)); + } + + // validIndex collection + //--------------------------------------------------------------------------------- + + /** + *

Validates that the index is within the bounds of the argument + * collection; otherwise throwing an exception with the specified message.

+ * + *
Validate.validIndex(myCollection, 2, "The collection index is invalid: ");
+ * + *

If the collection is {@code null}, then the message of the + * exception is "The validated object is null".

+ * + * @param the collection type + * @param collection the collection to check, validated not null by this method + * @param index the index to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated collection (never {@code null} for chaining) + * @throws NullPointerException if the collection is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(Collection, int) + * + * @since 3.0 + */ + public static > T validIndex(T collection, int index, String message, Object... values) { + Validate.notNull(collection); + if (index < 0 || index >= collection.size()) { + throw new IndexOutOfBoundsException(String.format(message, values)); + } + return collection; + } + + /** + *

Validates that the index is within the bounds of the argument + * collection; otherwise throwing an exception.

+ * + *
Validate.validIndex(myCollection, 2);
+ * + *

If the index is invalid, then the message of the exception + * is "The validated collection index is invalid: " + * followed by the index.

+ * + * @param the collection type + * @param collection the collection to check, validated not null by this method + * @param index the index to check + * @return the validated collection (never {@code null} for method chaining) + * @throws NullPointerException if the collection is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(Collection, int, String, Object...) + * + * @since 3.0 + */ + public static > T validIndex(T collection, int index) { + return validIndex(collection, index, DEFAULT_VALID_INDEX_COLLECTION_EX_MESSAGE, Integer.valueOf(index)); + } + + // validIndex string + //--------------------------------------------------------------------------------- + + /** + *

Validates that the index is within the bounds of the argument + * character sequence; otherwise throwing an exception with the + * specified message.

+ * + *
Validate.validIndex(myStr, 2, "The string index is invalid: ");
+ * + *

If the character sequence is {@code null}, then the message + * of the exception is "The validated object is null".

+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @param index the index to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @return the validated character sequence (never {@code null} for method chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(CharSequence, int) + * + * @since 3.0 + */ + public static T validIndex(T chars, int index, String message, Object... values) { + Validate.notNull(chars); + if (index < 0 || index >= chars.length()) { + throw new IndexOutOfBoundsException(String.format(message, values)); + } + return chars; + } + + /** + *

Validates that the index is within the bounds of the argument + * character sequence; otherwise throwing an exception.

+ * + *
Validate.validIndex(myStr, 2);
+ * + *

If the character sequence is {@code null}, then the message + * of the exception is "The validated object is + * null".

+ * + *

If the index is invalid, then the message of the exception + * is "The validated character sequence index is invalid: " + * followed by the index.

+ * + * @param the character sequence type + * @param chars the character sequence to check, validated not null by this method + * @param index the index to check + * @return the validated character sequence (never {@code null} for method chaining) + * @throws NullPointerException if the character sequence is {@code null} + * @throws IndexOutOfBoundsException if the index is invalid + * @see #validIndex(CharSequence, int, String, Object...) + * + * @since 3.0 + */ + public static T validIndex(T chars, int index) { + return validIndex(chars, index, DEFAULT_VALID_INDEX_CHAR_SEQUENCE_EX_MESSAGE, Integer.valueOf(index)); + } + + // validState + //--------------------------------------------------------------------------------- + + /** + *

Validate that the stateful condition is {@code true}; otherwise + * throwing an exception. This method is useful when validating according + * to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ * + *
+     * Validate.validState(field > 0);
+     * Validate.validState(this.isOk());
+ * + *

The message of the exception is "The validated state is + * false".

+ * + * @param expression the boolean expression to check + * @throws IllegalStateException if expression is {@code false} + * @see #validState(boolean, String, Object...) + * + * @since 3.0 + */ + public static void validState(boolean expression) { + if (expression == false) { + throw new IllegalStateException(DEFAULT_VALID_STATE_EX_MESSAGE); + } + } + + /** + *

Validate that the stateful condition is {@code true}; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary boolean expression, such as validating a + * primitive number or using your own custom validation expression.

+ * + *
Validate.validState(this.isOk(), "The state is not OK: %s", myObject);
+ * + * @param expression the boolean expression to check + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalStateException if expression is {@code false} + * @see #validState(boolean) + * + * @since 3.0 + */ + public static void validState(boolean expression, String message, Object... values) { + if (expression == false) { + throw new IllegalStateException(String.format(message, values)); + } + } + + // matchesPattern + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument character sequence matches the specified regular + * expression pattern; otherwise throwing an exception.

+ * + *
Validate.matchesPattern("hi", "[a-z]*");
+ * + *

The syntax of the pattern is the one used in the {@link Pattern} class.

+ * + * @param input the character sequence to validate, not null + * @param pattern the regular expression pattern, not null + * @throws IllegalArgumentException if the character sequence does not match the pattern + * @see #matchesPattern(CharSequence, String, String, Object...) + * + * @since 3.0 + */ + public static void matchesPattern(CharSequence input, String pattern) { + if (Pattern.matches(pattern, input) == false) { + throw new IllegalArgumentException(String.format(DEFAULT_MATCHES_PATTERN_EX, input, pattern)); + } + } + + /** + *

Validate that the specified argument character sequence matches the specified regular + * expression pattern; otherwise throwing an exception with the specified message.

+ * + *
Validate.matchesPattern("hi", "[a-z]*", "%s does not match %s", "hi" "[a-z]*");
+ * + *

The syntax of the pattern is the one used in the {@link Pattern} class.

+ * + * @param input the character sequence to validate, not null + * @param pattern the regular expression pattern, not null + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if the character sequence does not match the pattern + * @see #matchesPattern(CharSequence, String) + * + * @since 3.0 + */ + public static void matchesPattern(CharSequence input, String pattern, String message, Object... values) { + if (Pattern.matches(pattern, input) == false) { + throw new IllegalArgumentException(String.format(message, values)); + } + } + + // inclusiveBetween + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument object fall between the two + * inclusive values specified; otherwise, throws an exception.

+ * + *
Validate.inclusiveBetween(0, 2, 1);
+ * + * @param the type of the argument object + * @param start the inclusive start value, not null + * @param end the inclusive end value, not null + * @param value the object to validate, not null + * @throws IllegalArgumentException if the value falls out of the boundaries + * @see #inclusiveBetween(Object, Object, Comparable, String, Object...) + * + * @since 3.0 + */ + public static void inclusiveBetween(T start, T end, Comparable value) { + if (value.compareTo(start) < 0 || value.compareTo(end) > 0) { + throw new IllegalArgumentException(String.format(DEFAULT_INCLUSIVE_BETWEEN_EX_MESSAGE, value, start, end)); + } + } + + /** + *

Validate that the specified argument object fall between the two + * inclusive values specified; otherwise, throws an exception with the + * specified message.

+ * + *
Validate.inclusiveBetween(0, 2, 1, "Not in boundaries");
+ * + * @param the type of the argument object + * @param start the inclusive start value, not null + * @param end the inclusive end value, not null + * @param value the object to validate, not null + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if the value falls out of the boundaries + * @see #inclusiveBetween(Object, Object, Comparable) + * + * @since 3.0 + */ + public static void inclusiveBetween(T start, T end, Comparable value, String message, Object... values) { + if (value.compareTo(start) < 0 || value.compareTo(end) > 0) { + throw new IllegalArgumentException(String.format(message, values)); + } + } + + // exclusiveBetween + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument object fall between the two + * exclusive values specified; otherwise, throws an exception.

+ * + *
Validate.inclusiveBetween(0, 2, 1);
+ * + * @param the type of the argument object + * @param start the exclusive start value, not null + * @param end the exclusive end value, not null + * @param value the object to validate, not null + * @throws IllegalArgumentException if the value falls out of the boundaries + * @see #exclusiveBetween(Object, Object, Comparable, String, Object...) + * + * @since 3.0 + */ + public static void exclusiveBetween(T start, T end, Comparable value) { + if (value.compareTo(start) <= 0 || value.compareTo(end) >= 0) { + throw new IllegalArgumentException(String.format(DEFAULT_EXCLUSIVE_BETWEEN_EX_MESSAGE, value, start, end)); + } + } + + /** + *

Validate that the specified argument object fall between the two + * exclusive values specified; otherwise, throws an exception with the + * specified message.

+ * + *
Validate.inclusiveBetween(0, 2, 1, "Not in boundaries");
+ * + * @param the type of the argument object + * @param start the exclusive start value, not null + * @param end the exclusive end value, not null + * @param value the object to validate, not null + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if the value falls out of the boundaries + * @see #exclusiveBetween(Object, Object, Comparable) + * + * @since 3.0 + */ + public static void exclusiveBetween(T start, T end, Comparable value, String message, Object... values) { + if (value.compareTo(start) <= 0 || value.compareTo(end) >= 0) { + throw new IllegalArgumentException(String.format(message, values)); + } + } + + // isInstanceOf + //--------------------------------------------------------------------------------- + + /** + * Validates that the argument is an instance of the specified class, if not throws an exception. + * + *

This method is useful when validating according to an arbitrary class

+ * + *
Validate.isInstanceOf(OkClass.class, object);
+ * + *

The message of the exception is "Expected type: {type}, actual: {obj_type}"

+ * + * @param type the class the object must be validated against, not null + * @param obj the object to check, null throws an exception + * @throws IllegalArgumentException if argument is not of specified class + * @see #isInstanceOf(Class, Object, String, Object...) + * + * @since 3.0 + */ + public static void isInstanceOf(Class type, Object obj) { + if (type.isInstance(obj) == false) { + throw new IllegalArgumentException(String.format(DEFAULT_IS_INSTANCE_OF_EX_MESSAGE, type.getName(), + obj == null ? "null" : obj.getClass().getName())); + } + } + + /** + *

Validate that the argument is an instance of the specified class; otherwise + * throwing an exception with the specified message. This method is useful when + * validating according to an arbitrary class

+ * + *
Validate.isInstanceOf(OkClass.classs, object, "Wrong class, object is of class %s",
+     *   object.getClass().getName());
+ * + * @param type the class the object must be validated against, not null + * @param obj the object to check, null throws an exception + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if argument is not of specified class + * @see #isInstanceOf(Class, Object) + * + * @since 3.0 + */ + public static void isInstanceOf(Class type, Object obj, String message, Object... values) { + if (type.isInstance(obj) == false) { + throw new IllegalArgumentException(String.format(message, values)); + } + } + + // isAssignableFrom + //--------------------------------------------------------------------------------- + + /** + * Validates that the argument can be converted to the specified class, if not, throws an exception. + * + *

This method is useful when validating that there will be no casting errors.

+ * + *
Validate.isAssignableFrom(SuperClass.class, object.getClass());
+ * + *

The message format of the exception is "Cannot assign {type} to {superType}"

+ * + * @param superType the class the class must be validated against, not null + * @param type the class to check, not null + * @throws IllegalArgumentException if type argument is not assignable to the specified superType + * @see #isAssignableFrom(Class, Class, String, Object...) + * + * @since 3.0 + */ + public static void isAssignableFrom(Class superType, Class type) { + if (superType.isAssignableFrom(type) == false) { + throw new IllegalArgumentException(String.format(DEFAULT_IS_ASSIGNABLE_EX_MESSAGE, type == null ? "null" : type.getName(), + superType.getName())); + } + } + + /** + * Validates that the argument can be converted to the specified class, if not throws an exception. + * + *

This method is useful when validating if there will be no casting errors.

+ * + *
Validate.isAssignableFrom(SuperClass.class, object.getClass());
+ * + *

The message of the exception is "The validated object can not be converted to the" + * followed by the name of the class and "class"

+ * + * @param superType the class the class must be validated against, not null + * @param type the class to check, not null + * @param message the {@link String#format(String, Object...)} exception message if invalid, not null + * @param values the optional values for the formatted exception message, null array not recommended + * @throws IllegalArgumentException if argument can not be converted to the specified class + * @see #isAssignableFrom(Class, Class) + */ + public static void isAssignableFrom(Class superType, Class type, String message, Object... values) { + if (superType.isAssignableFrom(type) == false) { + throw new IllegalArgumentException(String.format(message, values)); + } + } +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/Builder.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/Builder.java new file mode 100644 index 00000000..a3b840cf --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/Builder.java @@ -0,0 +1,89 @@ +/* + * 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 external.org.apache.commons.lang3.builder; + +/** + *

+ * The Builder interface is designed to designate a class as a builder + * object in the Builder design pattern. Builders are capable of creating and + * configuring objects or results that normally take multiple steps to construct + * or are very complex to derive. + *

+ * + *

+ * The builder interface defines a single method, {@link #build()}, that + * classes must implement. The result of this method should be the final + * configured object or result after all building operations are performed. + *

+ * + *

+ * It is a recommended practice that the methods supplied to configure the + * object or result being built return a reference to {@code this} so that + * method calls can be chained together. + *

+ * + *

+ * Example Builder: + *

+ * class FontBuilder implements Builder<Font> {
+ *     private Font font;
+ *     
+ *     public FontBuilder(String fontName) {
+ *         this.font = new Font(fontName, Font.PLAIN, 12);
+ *     }
+ * 
+ *     public FontBuilder bold() {
+ *         this.font = this.font.deriveFont(Font.BOLD);
+ *         return this; // Reference returned so calls can be chained
+ *     }
+ *     
+ *     public FontBuilder size(float pointSize) {
+ *         this.font = this.font.deriveFont(pointSize);
+ *         return this; // Reference returned so calls can be chained
+ *     }
+ * 
+ *     // Other Font construction methods
+ * 
+ *     public Font build() {
+ *         return this.font;
+ *     }
+ * }
+ * 
+ * + * Example Builder Usage: + *
+ * Font bold14ptSansSerifFont = new FontBuilder(Font.SANS_SERIF).bold()
+ *                                                              .size(14.0f)
+ *                                                              .build();
+ * 
+ *

+ * + * @param the type of object that the builder will construct or compute. + * + * @since 3.0 + * @version $Id: Builder.java 1088899 2011-04-05 05:31:27Z bayard $ + */ +public interface Builder { + + /** + * Returns a reference to the object being constructed or result being + * calculated by the builder. + * + * @return the object constructed or result calculated by the builder. + */ + public T build(); +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/CompareToBuilder.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/CompareToBuilder.java new file mode 100644 index 00000000..9d06f589 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/CompareToBuilder.java @@ -0,0 +1,1020 @@ +/* + * 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 external.org.apache.commons.lang3.builder; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Comparator; + +import external.org.apache.commons.lang3.ArrayUtils; + +/** + * Assists in implementing {@link java.lang.Comparable#compareTo(Object)} methods. + * + * It is consistent with equals(Object) and + * hashcode() built with {@link EqualsBuilder} and + * {@link HashCodeBuilder}.

+ * + *

Two Objects that compare equal using equals(Object) should normally + * also compare equal using compareTo(Object).

+ * + *

All relevant fields should be included in the calculation of the + * comparison. Derived fields may be ignored. The same fields, in the same + * order, should be used in both compareTo(Object) and + * equals(Object).

+ * + *

To use this class write code as follows:

+ * + *
+ * public class MyClass {
+ *   String field1;
+ *   int field2;
+ *   boolean field3;
+ *
+ *   ...
+ *
+ *   public int compareTo(Object o) {
+ *     MyClass myClass = (MyClass) o;
+ *     return new CompareToBuilder()
+ *       .appendSuper(super.compareTo(o)
+ *       .append(this.field1, myClass.field1)
+ *       .append(this.field2, myClass.field2)
+ *       .append(this.field3, myClass.field3)
+ *       .toComparison();
+ *   }
+ * }
+ * 
+ * + *

Alternatively, there are {@link #reflectionCompare(Object, Object) reflectionCompare} methods that use + * reflection to determine the fields to append. Because fields can be private, + * reflectionCompare uses {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)} to + * bypass normal access control checks. This will fail under a security manager, + * unless the appropriate permissions are set up correctly. It is also + * slower than appending explicitly.

+ * + *

A typical implementation of compareTo(Object) using + * reflectionCompare looks like:

+ + *
+ * public int compareTo(Object o) {
+ *   return CompareToBuilder.reflectionCompare(this, o);
+ * }
+ * 
+ * + * @see java.lang.Comparable + * @see java.lang.Object#equals(Object) + * @see java.lang.Object#hashCode() + * @see EqualsBuilder + * @see HashCodeBuilder + * @since 1.0 + * @version $Id: CompareToBuilder.java 1199735 2011-11-09 13:11:07Z sebb $ + */ +public class CompareToBuilder implements Builder { + + /** + * Current state of the comparison as appended fields are checked. + */ + private int comparison; + + /** + *

Constructor for CompareToBuilder.

+ * + *

Starts off assuming that the objects are equal. Multiple calls are + * then made to the various append methods, followed by a call to + * {@link #toComparison} to get the result.

+ */ + public CompareToBuilder() { + super(); + comparison = 0; + } + + //----------------------------------------------------------------------- + /** + *

Compares two Objects via reflection.

+ * + *

Fields can be private, thus AccessibleObject.setAccessible + * is used to bypass normal access control checks. This will fail under a + * security manager unless the appropriate permissions are set.

+ * + *
    + *
  • Static fields will not be compared
  • + *
  • Transient members will be not be compared, as they are likely derived + * fields
  • + *
  • Superclass fields will be compared
  • + *
+ * + *

If both lhs and rhs are null, + * they are considered equal.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @return a negative integer, zero, or a positive integer as lhs + * is less than, equal to, or greater than rhs + * @throws NullPointerException if either (but not both) parameters are + * null + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + */ + public static int reflectionCompare(Object lhs, Object rhs) { + return reflectionCompare(lhs, rhs, false, null); + } + + /** + *

Compares two Objects via reflection.

+ * + *

Fields can be private, thus AccessibleObject.setAccessible + * is used to bypass normal access control checks. This will fail under a + * security manager unless the appropriate permissions are set.

+ * + *
    + *
  • Static fields will not be compared
  • + *
  • If compareTransients is true, + * compares transient members. Otherwise ignores them, as they + * are likely derived fields.
  • + *
  • Superclass fields will be compared
  • + *
+ * + *

If both lhs and rhs are null, + * they are considered equal.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param compareTransients whether to compare transient fields + * @return a negative integer, zero, or a positive integer as lhs + * is less than, equal to, or greater than rhs + * @throws NullPointerException if either lhs or rhs + * (but not both) is null + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + */ + public static int reflectionCompare(Object lhs, Object rhs, boolean compareTransients) { + return reflectionCompare(lhs, rhs, compareTransients, null); + } + + /** + *

Compares two Objects via reflection.

+ * + *

Fields can be private, thus AccessibleObject.setAccessible + * is used to bypass normal access control checks. This will fail under a + * security manager unless the appropriate permissions are set.

+ * + *
    + *
  • Static fields will not be compared
  • + *
  • If compareTransients is true, + * compares transient members. Otherwise ignores them, as they + * are likely derived fields.
  • + *
  • Superclass fields will be compared
  • + *
+ * + *

If both lhs and rhs are null, + * they are considered equal.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param excludeFields Collection of String fields to exclude + * @return a negative integer, zero, or a positive integer as lhs + * is less than, equal to, or greater than rhs + * @throws NullPointerException if either lhs or rhs + * (but not both) is null + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + * @since 2.2 + */ + public static int reflectionCompare(Object lhs, Object rhs, Collection excludeFields) { + return reflectionCompare(lhs, rhs, ReflectionToStringBuilder.toNoNullStringArray(excludeFields)); + } + + /** + *

Compares two Objects via reflection.

+ * + *

Fields can be private, thus AccessibleObject.setAccessible + * is used to bypass normal access control checks. This will fail under a + * security manager unless the appropriate permissions are set.

+ * + *
    + *
  • Static fields will not be compared
  • + *
  • If compareTransients is true, + * compares transient members. Otherwise ignores them, as they + * are likely derived fields.
  • + *
  • Superclass fields will be compared
  • + *
+ * + *

If both lhs and rhs are null, + * they are considered equal.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param excludeFields array of fields to exclude + * @return a negative integer, zero, or a positive integer as lhs + * is less than, equal to, or greater than rhs + * @throws NullPointerException if either lhs or rhs + * (but not both) is null + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + * @since 2.2 + */ + public static int reflectionCompare(Object lhs, Object rhs, String... excludeFields) { + return reflectionCompare(lhs, rhs, false, null, excludeFields); + } + + /** + *

Compares two Objects via reflection.

+ * + *

Fields can be private, thus AccessibleObject.setAccessible + * is used to bypass normal access control checks. This will fail under a + * security manager unless the appropriate permissions are set.

+ * + *
    + *
  • Static fields will not be compared
  • + *
  • If the compareTransients is true, + * compares transient members. Otherwise ignores them, as they + * are likely derived fields.
  • + *
  • Compares superclass fields up to and including reflectUpToClass. + * If reflectUpToClass is null, compares all superclass fields.
  • + *
+ * + *

If both lhs and rhs are null, + * they are considered equal.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param compareTransients whether to compare transient fields + * @param reflectUpToClass last superclass for which fields are compared + * @param excludeFields fields to exclude + * @return a negative integer, zero, or a positive integer as lhs + * is less than, equal to, or greater than rhs + * @throws NullPointerException if either lhs or rhs + * (but not both) is null + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + * @since 2.2 (2.0 as reflectionCompare(Object, Object, boolean, Class)) + */ + public static int reflectionCompare( + Object lhs, + Object rhs, + boolean compareTransients, + Class reflectUpToClass, + String... excludeFields) { + + if (lhs == rhs) { + return 0; + } + if (lhs == null || rhs == null) { + throw new NullPointerException(); + } + Class lhsClazz = lhs.getClass(); + if (!lhsClazz.isInstance(rhs)) { + throw new ClassCastException(); + } + CompareToBuilder compareToBuilder = new CompareToBuilder(); + reflectionAppend(lhs, rhs, lhsClazz, compareToBuilder, compareTransients, excludeFields); + while (lhsClazz.getSuperclass() != null && lhsClazz != reflectUpToClass) { + lhsClazz = lhsClazz.getSuperclass(); + reflectionAppend(lhs, rhs, lhsClazz, compareToBuilder, compareTransients, excludeFields); + } + return compareToBuilder.toComparison(); + } + + /** + *

Appends to builder the comparison of lhs + * to rhs using the fields defined in clazz.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param clazz Class that defines fields to be compared + * @param builder CompareToBuilder to append to + * @param useTransients whether to compare transient fields + * @param excludeFields fields to exclude + */ + private static void reflectionAppend( + Object lhs, + Object rhs, + Class clazz, + CompareToBuilder builder, + boolean useTransients, + String[] excludeFields) { + + Field[] fields = clazz.getDeclaredFields(); + AccessibleObject.setAccessible(fields, true); + for (int i = 0; i < fields.length && builder.comparison == 0; i++) { + Field f = fields[i]; + if (!ArrayUtils.contains(excludeFields, f.getName()) + && (f.getName().indexOf('$') == -1) + && (useTransients || !Modifier.isTransient(f.getModifiers())) + && (!Modifier.isStatic(f.getModifiers()))) { + try { + builder.append(f.get(lhs), f.get(rhs)); + } catch (IllegalAccessException e) { + // This can't happen. Would get a Security exception instead. + // Throw a runtime exception in case the impossible happens. + throw new InternalError("Unexpected IllegalAccessException"); + } + } + } + } + + //----------------------------------------------------------------------- + /** + *

Appends to the builder the compareTo(Object) + * result of the superclass.

+ * + * @param superCompareTo result of calling super.compareTo(Object) + * @return this - used to chain append calls + * @since 2.0 + */ + public CompareToBuilder appendSuper(int superCompareTo) { + if (comparison != 0) { + return this; + } + comparison = superCompareTo; + return this; + } + + //----------------------------------------------------------------------- + /** + *

Appends to the builder the comparison of + * two Objects.

+ * + *
    + *
  1. Check if lhs == rhs
  2. + *
  3. Check if either lhs or rhs is null, + * a null object is less than a non-null object
  4. + *
  5. Check the object contents
  6. + *
+ * + *

lhs must either be an array or implement {@link Comparable}.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @return this - used to chain append calls + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + */ + public CompareToBuilder append(Object lhs, Object rhs) { + return append(lhs, rhs, null); + } + + /** + *

Appends to the builder the comparison of + * two Objects.

+ * + *
    + *
  1. Check if lhs == rhs
  2. + *
  3. Check if either lhs or rhs is null, + * a null object is less than a non-null object
  4. + *
  5. Check the object contents
  6. + *
+ * + *

If lhs is an array, array comparison methods will be used. + * Otherwise comparator will be used to compare the objects. + * If comparator is null, lhs must + * implement {@link Comparable} instead.

+ * + * @param lhs left-hand object + * @param rhs right-hand object + * @param comparator Comparator used to compare the objects, + * null means treat lhs as Comparable + * @return this - used to chain append calls + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + * @since 2.0 + */ + public CompareToBuilder append(Object lhs, Object rhs, Comparator comparator) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.getClass().isArray()) { + // switch on type of array, to dispatch to the correct handler + // handles multi dimensional arrays + // throws a ClassCastException if rhs is not the correct array type + if (lhs instanceof long[]) { + append((long[]) lhs, (long[]) rhs); + } else if (lhs instanceof int[]) { + append((int[]) lhs, (int[]) rhs); + } else if (lhs instanceof short[]) { + append((short[]) lhs, (short[]) rhs); + } else if (lhs instanceof char[]) { + append((char[]) lhs, (char[]) rhs); + } else if (lhs instanceof byte[]) { + append((byte[]) lhs, (byte[]) rhs); + } else if (lhs instanceof double[]) { + append((double[]) lhs, (double[]) rhs); + } else if (lhs instanceof float[]) { + append((float[]) lhs, (float[]) rhs); + } else if (lhs instanceof boolean[]) { + append((boolean[]) lhs, (boolean[]) rhs); + } else { + // not an array of primitives + // throws a ClassCastException if rhs is not an array + append((Object[]) lhs, (Object[]) rhs, comparator); + } + } else { + // the simple case, not an array, just test the element + if (comparator == null) { + @SuppressWarnings("unchecked") // assume this can be done; if not throw CCE as per Javadoc + final Comparable comparable = (Comparable) lhs; + comparison = comparable.compareTo(rhs); + } else { + @SuppressWarnings("unchecked") // assume this can be done; if not throw CCE as per Javadoc + final Comparator comparator2 = (Comparator) comparator; + comparison = comparator2.compare(lhs, rhs); + } + } + return this; + } + + //------------------------------------------------------------------------- + /** + * Appends to the builder the comparison of + * two longs. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(long lhs, long rhs) { + if (comparison != 0) { + return this; + } + comparison = ((lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0)); + return this; + } + + /** + * Appends to the builder the comparison of + * two ints. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(int lhs, int rhs) { + if (comparison != 0) { + return this; + } + comparison = ((lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0)); + return this; + } + + /** + * Appends to the builder the comparison of + * two shorts. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(short lhs, short rhs) { + if (comparison != 0) { + return this; + } + comparison = ((lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0)); + return this; + } + + /** + * Appends to the builder the comparison of + * two chars. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(char lhs, char rhs) { + if (comparison != 0) { + return this; + } + comparison = ((lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0)); + return this; + } + + /** + * Appends to the builder the comparison of + * two bytes. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(byte lhs, byte rhs) { + if (comparison != 0) { + return this; + } + comparison = ((lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0)); + return this; + } + + /** + *

Appends to the builder the comparison of + * two doubles.

+ * + *

This handles NaNs, Infinities, and -0.0.

+ * + *

It is compatible with the hash code generated by + * HashCodeBuilder.

+ * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(double lhs, double rhs) { + if (comparison != 0) { + return this; + } + comparison = Double.compare(lhs, rhs); + return this; + } + + /** + *

Appends to the builder the comparison of + * two floats.

+ * + *

This handles NaNs, Infinities, and -0.0.

+ * + *

It is compatible with the hash code generated by + * HashCodeBuilder.

+ * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(float lhs, float rhs) { + if (comparison != 0) { + return this; + } + comparison = Float.compare(lhs, rhs); + return this; + } + + /** + * Appends to the builder the comparison of + * two booleanss. + * + * @param lhs left-hand value + * @param rhs right-hand value + * @return this - used to chain append calls + */ + public CompareToBuilder append(boolean lhs, boolean rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == false) { + comparison = -1; + } else { + comparison = +1; + } + return this; + } + + //----------------------------------------------------------------------- + /** + *

Appends to the builder the deep comparison of + * two Object arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a short length array is less than a long length array
  6. + *
  7. Check array contents element by element using {@link #append(Object, Object, Comparator)}
  8. + *
+ * + *

This method will also will be called for the top level of multi-dimensional, + * ragged, and multi-typed arrays.

+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + */ + public CompareToBuilder append(Object[] lhs, Object[] rhs) { + return append(lhs, rhs, null); + } + + /** + *

Appends to the builder the deep comparison of + * two Object arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a short length array is less than a long length array
  6. + *
  7. Check array contents element by element using {@link #append(Object, Object, Comparator)}
  8. + *
+ * + *

This method will also will be called for the top level of multi-dimensional, + * ragged, and multi-typed arrays.

+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @param comparator Comparator to use to compare the array elements, + * null means to treat lhs elements as Comparable. + * @return this - used to chain append calls + * @throws ClassCastException if rhs is not assignment-compatible + * with lhs + * @since 2.0 + */ + public CompareToBuilder append(Object[] lhs, Object[] rhs, Comparator comparator) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i], comparator); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two long arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(long, long)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(long[] lhs, long[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two int arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(int, int)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(int[] lhs, int[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two short arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(short, short)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(short[] lhs, short[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two char arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(char, char)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(char[] lhs, char[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two byte arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(byte, byte)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(byte[] lhs, byte[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two double arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(double, double)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(double[] lhs, double[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two float arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(float, float)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(float[] lhs, float[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Appends to the builder the deep comparison of + * two boolean arrays.

+ * + *
    + *
  1. Check if arrays are the same using ==
  2. + *
  3. Check if for null, null is less than non-null
  4. + *
  5. Check array length, a shorter length array is less than a longer length array
  6. + *
  7. Check array contents element by element using {@link #append(boolean, boolean)}
  8. + *
+ * + * @param lhs left-hand array + * @param rhs right-hand array + * @return this - used to chain append calls + */ + public CompareToBuilder append(boolean[] lhs, boolean[] rhs) { + if (comparison != 0) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null) { + comparison = -1; + return this; + } + if (rhs == null) { + comparison = +1; + return this; + } + if (lhs.length != rhs.length) { + comparison = (lhs.length < rhs.length) ? -1 : +1; + return this; + } + for (int i = 0; i < lhs.length && comparison == 0; i++) { + append(lhs[i], rhs[i]); + } + return this; + } + + //----------------------------------------------------------------------- + /** + * Returns a negative integer, a positive integer, or zero as + * the builder has judged the "left-hand" side + * as less than, greater than, or equal to the "right-hand" + * side. + * + * @return final comparison result + * @see #build() + */ + public int toComparison() { + return comparison; + } + + /** + * Returns a negative Integer, a positive Integer, or zero as + * the builder has judged the "left-hand" side + * as less than, greater than, or equal to the "right-hand" + * side. + * + * @return final comparison result as an Integer + * @see #toComparison() + * @since 3.0 + */ + public Integer build() { + return Integer.valueOf(toComparison()); + } +} + diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/EqualsBuilder.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/EqualsBuilder.java new file mode 100644 index 00000000..c8a459c5 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/EqualsBuilder.java @@ -0,0 +1,945 @@ +/* + * 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 external.org.apache.commons.lang3.builder; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + + +import external.org.apache.commons.lang3.ArrayUtils; +import external.org.apache.commons.lang3.tuple.Pair; + +/** + *

Assists in implementing {@link Object#equals(Object)} methods.

+ * + *

This class provides methods to build a good equals method for any + * class. It follows rules laid out in + * Effective Java + * , by Joshua Bloch. In particular the rule for comparing doubles, + * floats, and arrays can be tricky. Also, making sure that + * equals() and hashCode() are consistent can be + * difficult.

+ * + *

Two Objects that compare as equals must generate the same hash code, + * but two Objects with the same hash code do not have to be equal.

+ * + *

All relevant fields should be included in the calculation of equals. + * Derived fields may be ignored. In particular, any field used in + * generating a hash code must be used in the equals method, and vice + * versa.

+ * + *

Typical use for the code is as follows:

+ *
+ * public boolean equals(Object obj) {
+ *   if (obj == null) { return false; }
+ *   if (obj == this) { return true; }
+ *   if (obj.getClass() != getClass()) {
+ *     return false;
+ *   }
+ *   MyClass rhs = (MyClass) obj;
+ *   return new EqualsBuilder()
+ *                 .appendSuper(super.equals(obj))
+ *                 .append(field1, rhs.field1)
+ *                 .append(field2, rhs.field2)
+ *                 .append(field3, rhs.field3)
+ *                 .isEquals();
+ *  }
+ * 
+ * + *

Alternatively, there is a method that uses reflection to determine + * the fields to test. Because these fields are usually private, the method, + * reflectionEquals, uses AccessibleObject.setAccessible to + * change the visibility of the fields. This will fail under a security + * manager, unless the appropriate permissions are set up correctly. It is + * also slower than testing explicitly.

+ * + *

A typical invocation for this method would look like:

+ *
+ * public boolean equals(Object obj) {
+ *   return EqualsBuilder.reflectionEquals(this, obj);
+ * }
+ * 
+ * + * @since 1.0 + * @version $Id: EqualsBuilder.java 1091531 2011-04-12 18:29:49Z ggregory $ + */ +public class EqualsBuilder implements Builder { + + /** + *

+ * A registry of objects used by reflection methods to detect cyclical object references and avoid infinite loops. + *

+ * + * @since 3.0 + */ + private static final ThreadLocal>> REGISTRY = new ThreadLocal>>(); + + /* + * NOTE: we cannot store the actual objects in a HashSet, as that would use the very hashCode() + * we are in the process of calculating. + * + * So we generate a one-to-one mapping from the original object to a new object. + * + * Now HashSet uses equals() to determine if two elements with the same hashcode really + * are equal, so we also need to ensure that the replacement objects are only equal + * if the original objects are identical. + * + * The original implementation (2.4 and before) used the System.indentityHashCode() + * method - however this is not guaranteed to generate unique ids (e.g. LANG-459) + * + * We now use the IDKey helper class (adapted from org.apache.axis.utils.IDKey) + * to disambiguate the duplicate ids. + */ + + /** + *

+ * Returns the registry of object pairs being traversed by the reflection + * methods in the current thread. + *

+ * + * @return Set the registry of objects being traversed + * @since 3.0 + */ + static Set> getRegistry() { + return REGISTRY.get(); + } + + /** + *

+ * Converters value pair into a register pair. + *

+ * + * @param lhs this object + * @param rhs the other object + * + * @return the pair + */ + static Pair getRegisterPair(Object lhs, Object rhs) { + IDKey left = new IDKey(lhs); + IDKey right = new IDKey(rhs); + return Pair.of(left, right); + } + + /** + *

+ * Returns true if the registry contains the given object pair. + * Used by the reflection methods to avoid infinite loops. + * Objects might be swapped therefore a check is needed if the object pair + * is registered in given or swapped order. + *

+ * + * @param lhs this object to lookup in registry + * @param rhs the other object to lookup on registry + * @return boolean true if the registry contains the given object. + * @since 3.0 + */ + static boolean isRegistered(Object lhs, Object rhs) { + Set> registry = getRegistry(); + Pair pair = getRegisterPair(lhs, rhs); + Pair swappedPair = Pair.of(pair.getLeft(), pair.getRight()); + + return registry != null + && (registry.contains(pair) || registry.contains(swappedPair)); + } + + /** + *

+ * Registers the given object pair. + * Used by the reflection methods to avoid infinite loops. + *

+ * + * @param lhs this object to register + * @param rhs the other object to register + */ + static void register(Object lhs, Object rhs) { + synchronized (EqualsBuilder.class) { + if (getRegistry() == null) { + REGISTRY.set(new HashSet>()); + } + } + + Set> registry = getRegistry(); + Pair pair = getRegisterPair(lhs, rhs); + registry.add(pair); + } + + /** + *

+ * Unregisters the given object pair. + *

+ * + *

+ * Used by the reflection methods to avoid infinite loops. + * + * @param lhs this object to unregister + * @param rhs the other object to unregister + * @since 3.0 + */ + static void unregister(Object lhs, Object rhs) { + Set> registry = getRegistry(); + if (registry != null) { + Pair pair = getRegisterPair(lhs, rhs); + registry.remove(pair); + synchronized (EqualsBuilder.class) { + //read again + registry = getRegistry(); + if (registry != null && registry.isEmpty()) { + REGISTRY.remove(); + } + } + } + } + + /** + * If the fields tested are equals. + * The default value is true. + */ + private boolean isEquals = true; + + /** + *

Constructor for EqualsBuilder.

+ * + *

Starts off assuming that equals is true.

+ * @see Object#equals(Object) + */ + public EqualsBuilder() { + // do nothing for now. + } + + //------------------------------------------------------------------------- + + /** + *

This method uses reflection to determine if the two Objects + * are equal.

+ * + *

It uses AccessibleObject.setAccessible to gain access to private + * fields. This means that it will throw a security exception if run under + * a security manager, if the permissions are not set up correctly. It is also + * not as efficient as testing explicitly.

+ * + *

Transient members will be not be tested, as they are likely derived + * fields, and not part of the value of the Object.

+ * + *

Static fields will not be tested. Superclass fields will be included.

+ * + * @param lhs this object + * @param rhs the other object + * @param excludeFields Collection of String field names to exclude from testing + * @return true if the two Objects have tested equals. + */ + public static boolean reflectionEquals(Object lhs, Object rhs, Collection excludeFields) { + return reflectionEquals(lhs, rhs, ReflectionToStringBuilder.toNoNullStringArray(excludeFields)); + } + + /** + *

This method uses reflection to determine if the two Objects + * are equal.

+ * + *

It uses AccessibleObject.setAccessible to gain access to private + * fields. This means that it will throw a security exception if run under + * a security manager, if the permissions are not set up correctly. It is also + * not as efficient as testing explicitly.

+ * + *

Transient members will be not be tested, as they are likely derived + * fields, and not part of the value of the Object.

+ * + *

Static fields will not be tested. Superclass fields will be included.

+ * + * @param lhs this object + * @param rhs the other object + * @param excludeFields array of field names to exclude from testing + * @return true if the two Objects have tested equals. + */ + public static boolean reflectionEquals(Object lhs, Object rhs, String... excludeFields) { + return reflectionEquals(lhs, rhs, false, null, excludeFields); + } + + /** + *

This method uses reflection to determine if the two Objects + * are equal.

+ * + *

It uses AccessibleObject.setAccessible to gain access to private + * fields. This means that it will throw a security exception if run under + * a security manager, if the permissions are not set up correctly. It is also + * not as efficient as testing explicitly.

+ * + *

If the TestTransients parameter is set to true, transient + * members will be tested, otherwise they are ignored, as they are likely + * derived fields, and not part of the value of the Object.

+ * + *

Static fields will not be tested. Superclass fields will be included.

+ * + * @param lhs this object + * @param rhs the other object + * @param testTransients whether to include transient fields + * @return true if the two Objects have tested equals. + */ + public static boolean reflectionEquals(Object lhs, Object rhs, boolean testTransients) { + return reflectionEquals(lhs, rhs, testTransients, null); + } + + /** + *

This method uses reflection to determine if the two Objects + * are equal.

+ * + *

It uses AccessibleObject.setAccessible to gain access to private + * fields. This means that it will throw a security exception if run under + * a security manager, if the permissions are not set up correctly. It is also + * not as efficient as testing explicitly.

+ * + *

If the testTransients parameter is set to true, transient + * members will be tested, otherwise they are ignored, as they are likely + * derived fields, and not part of the value of the Object.

+ * + *

Static fields will not be included. Superclass fields will be appended + * up to and including the specified superclass. A null superclass is treated + * as java.lang.Object.

+ * + * @param lhs this object + * @param rhs the other object + * @param testTransients whether to include transient fields + * @param reflectUpToClass the superclass to reflect up to (inclusive), + * may be null + * @param excludeFields array of field names to exclude from testing + * @return true if the two Objects have tested equals. + * @since 2.0 + */ + public static boolean reflectionEquals(Object lhs, Object rhs, boolean testTransients, Class reflectUpToClass, + String... excludeFields) { + if (lhs == rhs) { + return true; + } + if (lhs == null || rhs == null) { + return false; + } + // Find the leaf class since there may be transients in the leaf + // class or in classes between the leaf and root. + // If we are not testing transients or a subclass has no ivars, + // then a subclass can test equals to a superclass. + Class lhsClass = lhs.getClass(); + Class rhsClass = rhs.getClass(); + Class testClass; + if (lhsClass.isInstance(rhs)) { + testClass = lhsClass; + if (!rhsClass.isInstance(lhs)) { + // rhsClass is a subclass of lhsClass + testClass = rhsClass; + } + } else if (rhsClass.isInstance(lhs)) { + testClass = rhsClass; + if (!lhsClass.isInstance(rhs)) { + // lhsClass is a subclass of rhsClass + testClass = lhsClass; + } + } else { + // The two classes are not related. + return false; + } + EqualsBuilder equalsBuilder = new EqualsBuilder(); + try { + reflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields); + while (testClass.getSuperclass() != null && testClass != reflectUpToClass) { + testClass = testClass.getSuperclass(); + reflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields); + } + } catch (IllegalArgumentException e) { + // In this case, we tried to test a subclass vs. a superclass and + // the subclass has ivars or the ivars are transient and + // we are testing transients. + // If a subclass has ivars that we are trying to test them, we get an + // exception and we know that the objects are not equal. + return false; + } + return equalsBuilder.isEquals(); + } + + /** + *

Appends the fields and values defined by the given object of the + * given Class.

+ * + * @param lhs the left hand object + * @param rhs the right hand object + * @param clazz the class to append details of + * @param builder the builder to append to + * @param useTransients whether to test transient fields + * @param excludeFields array of field names to exclude from testing + */ + private static void reflectionAppend( + Object lhs, + Object rhs, + Class clazz, + EqualsBuilder builder, + boolean useTransients, + String[] excludeFields) { + + if (isRegistered(lhs, rhs)) { + return; + } + + try { + register(lhs, rhs); + Field[] fields = clazz.getDeclaredFields(); + AccessibleObject.setAccessible(fields, true); + for (int i = 0; i < fields.length && builder.isEquals; i++) { + Field f = fields[i]; + if (!ArrayUtils.contains(excludeFields, f.getName()) + && (f.getName().indexOf('$') == -1) + && (useTransients || !Modifier.isTransient(f.getModifiers())) + && (!Modifier.isStatic(f.getModifiers()))) { + try { + builder.append(f.get(lhs), f.get(rhs)); + } catch (IllegalAccessException e) { + //this can't happen. Would get a Security exception instead + //throw a runtime exception in case the impossible happens. + throw new InternalError("Unexpected IllegalAccessException"); + } + } + } + } finally { + unregister(lhs, rhs); + } + } + + //------------------------------------------------------------------------- + + /** + *

Adds the result of super.equals() to this builder.

+ * + * @param superEquals the result of calling super.equals() + * @return EqualsBuilder - used to chain calls. + * @since 2.0 + */ + public EqualsBuilder appendSuper(boolean superEquals) { + if (isEquals == false) { + return this; + } + isEquals = superEquals; + return this; + } + + //------------------------------------------------------------------------- + + /** + *

Test if two Objects are equal using their + * equals method.

+ * + * @param lhs the left hand object + * @param rhs the right hand object + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(Object lhs, Object rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + Class lhsClass = lhs.getClass(); + if (!lhsClass.isArray()) { + // The simple case, not an array, just test the element + isEquals = lhs.equals(rhs); + } else if (lhs.getClass() != rhs.getClass()) { + // Here when we compare different dimensions, for example: a boolean[][] to a boolean[] + this.setEquals(false); + } + // 'Switch' on type of array, to dispatch to the correct handler + // This handles multi dimensional arrays of the same depth + else if (lhs instanceof long[]) { + append((long[]) lhs, (long[]) rhs); + } else if (lhs instanceof int[]) { + append((int[]) lhs, (int[]) rhs); + } else if (lhs instanceof short[]) { + append((short[]) lhs, (short[]) rhs); + } else if (lhs instanceof char[]) { + append((char[]) lhs, (char[]) rhs); + } else if (lhs instanceof byte[]) { + append((byte[]) lhs, (byte[]) rhs); + } else if (lhs instanceof double[]) { + append((double[]) lhs, (double[]) rhs); + } else if (lhs instanceof float[]) { + append((float[]) lhs, (float[]) rhs); + } else if (lhs instanceof boolean[]) { + append((boolean[]) lhs, (boolean[]) rhs); + } else { + // Not an array of primitives + append((Object[]) lhs, (Object[]) rhs); + } + return this; + } + + /** + *

+ * Test if two long s are equal. + *

+ * + * @param lhs + * the left hand long + * @param rhs + * the right hand long + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(long lhs, long rhs) { + if (isEquals == false) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Test if two ints are equal.

+ * + * @param lhs the left hand int + * @param rhs the right hand int + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(int lhs, int rhs) { + if (isEquals == false) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Test if two shorts are equal.

+ * + * @param lhs the left hand short + * @param rhs the right hand short + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(short lhs, short rhs) { + if (isEquals == false) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Test if two chars are equal.

+ * + * @param lhs the left hand char + * @param rhs the right hand char + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(char lhs, char rhs) { + if (isEquals == false) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Test if two bytes are equal.

+ * + * @param lhs the left hand byte + * @param rhs the right hand byte + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(byte lhs, byte rhs) { + if (isEquals == false) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Test if two doubles are equal by testing that the + * pattern of bits returned by doubleToLong are equal.

+ * + *

This handles NaNs, Infinities, and -0.0.

+ * + *

It is compatible with the hash code generated by + * HashCodeBuilder.

+ * + * @param lhs the left hand double + * @param rhs the right hand double + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(double lhs, double rhs) { + if (isEquals == false) { + return this; + } + return append(Double.doubleToLongBits(lhs), Double.doubleToLongBits(rhs)); + } + + /** + *

Test if two floats are equal byt testing that the + * pattern of bits returned by doubleToLong are equal.

+ * + *

This handles NaNs, Infinities, and -0.0.

+ * + *

It is compatible with the hash code generated by + * HashCodeBuilder.

+ * + * @param lhs the left hand float + * @param rhs the right hand float + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(float lhs, float rhs) { + if (isEquals == false) { + return this; + } + return append(Float.floatToIntBits(lhs), Float.floatToIntBits(rhs)); + } + + /** + *

Test if two booleanss are equal.

+ * + * @param lhs the left hand boolean + * @param rhs the right hand boolean + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(boolean lhs, boolean rhs) { + if (isEquals == false) { + return this; + } + isEquals = (lhs == rhs); + return this; + } + + /** + *

Performs a deep comparison of two Object arrays.

+ * + *

This also will be called for the top level of + * multi-dimensional, ragged, and multi-typed arrays.

+ * + * @param lhs the left hand Object[] + * @param rhs the right hand Object[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(Object[] lhs, Object[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of long. Length and all + * values are compared.

+ * + *

The method {@link #append(long, long)} is used.

+ * + * @param lhs the left hand long[] + * @param rhs the right hand long[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(long[] lhs, long[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of int. Length and all + * values are compared.

+ * + *

The method {@link #append(int, int)} is used.

+ * + * @param lhs the left hand int[] + * @param rhs the right hand int[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(int[] lhs, int[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of short. Length and all + * values are compared.

+ * + *

The method {@link #append(short, short)} is used.

+ * + * @param lhs the left hand short[] + * @param rhs the right hand short[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(short[] lhs, short[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of char. Length and all + * values are compared.

+ * + *

The method {@link #append(char, char)} is used.

+ * + * @param lhs the left hand char[] + * @param rhs the right hand char[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(char[] lhs, char[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of byte. Length and all + * values are compared.

+ * + *

The method {@link #append(byte, byte)} is used.

+ * + * @param lhs the left hand byte[] + * @param rhs the right hand byte[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(byte[] lhs, byte[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of double. Length and all + * values are compared.

+ * + *

The method {@link #append(double, double)} is used.

+ * + * @param lhs the left hand double[] + * @param rhs the right hand double[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(double[] lhs, double[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of float. Length and all + * values are compared.

+ * + *

The method {@link #append(float, float)} is used.

+ * + * @param lhs the left hand float[] + * @param rhs the right hand float[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(float[] lhs, float[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of boolean. Length and all + * values are compared.

+ * + *

The method {@link #append(boolean, boolean)} is used.

+ * + * @param lhs the left hand boolean[] + * @param rhs the right hand boolean[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(boolean[] lhs, boolean[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Returns true if the fields that have been checked + * are all equal.

+ * + * @return boolean + */ + public boolean isEquals() { + return this.isEquals; + } + + /** + *

Returns true if the fields that have been checked + * are all equal.

+ * + * @return true if all of the fields that have been checked + * are equal, false otherwise. + * + * @since 3.0 + */ + public Boolean build() { + return Boolean.valueOf(isEquals()); + } + + /** + * Sets the isEquals value. + * + * @param isEquals The value to set. + * @since 2.1 + */ + protected void setEquals(boolean isEquals) { + this.isEquals = isEquals; + } + + /** + * Reset the EqualsBuilder so you can use the same object again + * @since 2.5 + */ + public void reset() { + this.isEquals = true; + } +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/HashCodeBuilder.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/HashCodeBuilder.java new file mode 100644 index 00000000..093a9661 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/HashCodeBuilder.java @@ -0,0 +1,961 @@ +/* + * 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 external.org.apache.commons.lang3.builder; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import external.org.apache.commons.lang3.ArrayUtils; + +/** + *

+ * Assists in implementing {@link Object#hashCode()} methods. + *

+ * + *

+ * This class enables a good hashCode method to be built for any class. It follows the rules laid out in + * the book Effective Java by Joshua Bloch. Writing a + * good hashCode method is actually quite difficult. This class aims to simplify the process. + *

+ * + *

+ * The following is the approach taken. When appending a data field, the current total is multiplied by the + * multiplier then a relevant value + * for that data type is added. For example, if the current hashCode is 17, and the multiplier is 37, then + * appending the integer 45 will create a hashcode of 674, namely 17 * 37 + 45. + *

+ * + *

+ * All relevant fields from the object should be included in the hashCode method. Derived fields may be + * excluded. In general, any field used in the equals method must be used in the hashCode + * method. + *

+ * + *

+ * To use this class write code as follows: + *

+ * + *
+ * public class Person {
+ *   String name;
+ *   int age;
+ *   boolean smoker;
+ *   ...
+ *
+ *   public int hashCode() {
+ *     // you pick a hard-coded, randomly chosen, non-zero, odd number
+ *     // ideally different for each class
+ *     return new HashCodeBuilder(17, 37).
+ *       append(name).
+ *       append(age).
+ *       append(smoker).
+ *       toHashCode();
+ *   }
+ * }
+ * 
+ * + *

+ * If required, the superclass hashCode() can be added using {@link #appendSuper}. + *

+ * + *

+ * Alternatively, there is a method that uses reflection to determine the fields to test. Because these fields are + * usually private, the method, reflectionHashCode, uses AccessibleObject.setAccessible + * to change the visibility of the fields. This will fail under a security manager, unless the appropriate permissions + * are set up correctly. It is also slower than testing explicitly. + *

+ * + *

+ * A typical invocation for this method would look like: + *

+ * + *
+ * public int hashCode() {
+ *   return HashCodeBuilder.reflectionHashCode(this);
+ * }
+ * 
+ * + * @since 1.0 + * @version $Id: HashCodeBuilder.java 1144929 2011-07-10 18:26:16Z ggregory $ + */ +public class HashCodeBuilder implements Builder { + /** + *

+ * A registry of objects used by reflection methods to detect cyclical object references and avoid infinite loops. + *

+ * + * @since 2.3 + */ + private static final ThreadLocal> REGISTRY = new ThreadLocal>(); + + /* + * NOTE: we cannot store the actual objects in a HashSet, as that would use the very hashCode() + * we are in the process of calculating. + * + * So we generate a one-to-one mapping from the original object to a new object. + * + * Now HashSet uses equals() to determine if two elements with the same hashcode really + * are equal, so we also need to ensure that the replacement objects are only equal + * if the original objects are identical. + * + * The original implementation (2.4 and before) used the System.indentityHashCode() + * method - however this is not guaranteed to generate unique ids (e.g. LANG-459) + * + * We now use the IDKey helper class (adapted from org.apache.axis.utils.IDKey) + * to disambiguate the duplicate ids. + */ + + /** + *

+ * Returns the registry of objects being traversed by the reflection methods in the current thread. + *

+ * + * @return Set the registry of objects being traversed + * @since 2.3 + */ + static Set getRegistry() { + return REGISTRY.get(); + } + + /** + *

+ * Returns true if the registry contains the given object. Used by the reflection methods to avoid + * infinite loops. + *

+ * + * @param value + * The object to lookup in the registry. + * @return boolean true if the registry contains the given object. + * @since 2.3 + */ + static boolean isRegistered(Object value) { + Set registry = getRegistry(); + return registry != null && registry.contains(new IDKey(value)); + } + + /** + *

+ * Appends the fields and values defined by the given object of the given Class. + *

+ * + * @param object + * the object to append details of + * @param clazz + * the class to append details of + * @param builder + * the builder to append to + * @param useTransients + * whether to use transient fields + * @param excludeFields + * Collection of String field names to exclude from use in calculation of hash code + */ + private static void reflectionAppend(Object object, Class clazz, HashCodeBuilder builder, boolean useTransients, + String[] excludeFields) { + if (isRegistered(object)) { + return; + } + try { + register(object); + Field[] fields = clazz.getDeclaredFields(); + AccessibleObject.setAccessible(fields, true); + for (Field field : fields) { + if (!ArrayUtils.contains(excludeFields, field.getName()) + && (field.getName().indexOf('$') == -1) + && (useTransients || !Modifier.isTransient(field.getModifiers())) + && (!Modifier.isStatic(field.getModifiers()))) { + try { + Object fieldValue = field.get(object); + builder.append(fieldValue); + } catch (IllegalAccessException e) { + // this can't happen. Would get a Security exception instead + // throw a runtime exception in case the impossible happens. + throw new InternalError("Unexpected IllegalAccessException"); + } + } + } + } finally { + unregister(object); + } + } + + /** + *

+ * This method uses reflection to build a valid hash code. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * Transient members will be not be used, as they are likely derived fields, and not part of the value of the + * Object. + *

+ * + *

+ * Static fields will not be tested. Superclass fields will be included. + *

+ * + *

+ * Two randomly chosen, non-zero, odd numbers must be passed in. Ideally these should be different for each class, + * however this is not vital. Prime numbers are preferred, especially for the multiplier. + *

+ * + * @param initialNonZeroOddNumber + * a non-zero, odd number used as the initial value + * @param multiplierNonZeroOddNumber + * a non-zero, odd number used as the multiplier + * @param object + * the Object to create a hashCode for + * @return int hash code + * @throws IllegalArgumentException + * if the Object is null + * @throws IllegalArgumentException + * if the number is zero or even + */ + public static int reflectionHashCode(int initialNonZeroOddNumber, int multiplierNonZeroOddNumber, Object object) { + return reflectionHashCode(initialNonZeroOddNumber, multiplierNonZeroOddNumber, object, false, null); + } + + /** + *

+ * This method uses reflection to build a valid hash code. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * If the TestTransients parameter is set to true, transient members will be tested, otherwise they + * are ignored, as they are likely derived fields, and not part of the value of the Object. + *

+ * + *

+ * Static fields will not be tested. Superclass fields will be included. + *

+ * + *

+ * Two randomly chosen, non-zero, odd numbers must be passed in. Ideally these should be different for each class, + * however this is not vital. Prime numbers are preferred, especially for the multiplier. + *

+ * + * @param initialNonZeroOddNumber + * a non-zero, odd number used as the initial value + * @param multiplierNonZeroOddNumber + * a non-zero, odd number used as the multiplier + * @param object + * the Object to create a hashCode for + * @param testTransients + * whether to include transient fields + * @return int hash code + * @throws IllegalArgumentException + * if the Object is null + * @throws IllegalArgumentException + * if the number is zero or even + */ + public static int reflectionHashCode(int initialNonZeroOddNumber, int multiplierNonZeroOddNumber, Object object, + boolean testTransients) { + return reflectionHashCode(initialNonZeroOddNumber, multiplierNonZeroOddNumber, object, testTransients, null); + } + + /** + *

+ * This method uses reflection to build a valid hash code. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * If the TestTransients parameter is set to true, transient members will be tested, otherwise they + * are ignored, as they are likely derived fields, and not part of the value of the Object. + *

+ * + *

+ * Static fields will not be included. Superclass fields will be included up to and including the specified + * superclass. A null superclass is treated as java.lang.Object. + *

+ * + *

+ * Two randomly chosen, non-zero, odd numbers must be passed in. Ideally these should be different for each class, + * however this is not vital. Prime numbers are preferred, especially for the multiplier. + *

+ * + * @param + * the type of the object involved + * @param initialNonZeroOddNumber + * a non-zero, odd number used as the initial value + * @param multiplierNonZeroOddNumber + * a non-zero, odd number used as the multiplier + * @param object + * the Object to create a hashCode for + * @param testTransients + * whether to include transient fields + * @param reflectUpToClass + * the superclass to reflect up to (inclusive), may be null + * @param excludeFields + * array of field names to exclude from use in calculation of hash code + * @return int hash code + * @throws IllegalArgumentException + * if the Object is null + * @throws IllegalArgumentException + * if the number is zero or even + * @since 2.0 + */ + public static int reflectionHashCode(int initialNonZeroOddNumber, int multiplierNonZeroOddNumber, T object, + boolean testTransients, Class reflectUpToClass, String... excludeFields) { + + if (object == null) { + throw new IllegalArgumentException("The object to build a hash code for must not be null"); + } + HashCodeBuilder builder = new HashCodeBuilder(initialNonZeroOddNumber, multiplierNonZeroOddNumber); + Class clazz = object.getClass(); + reflectionAppend(object, clazz, builder, testTransients, excludeFields); + while (clazz.getSuperclass() != null && clazz != reflectUpToClass) { + clazz = clazz.getSuperclass(); + reflectionAppend(object, clazz, builder, testTransients, excludeFields); + } + return builder.toHashCode(); + } + + /** + *

+ * This method uses reflection to build a valid hash code. + *

+ * + *

+ * This constructor uses two hard coded choices for the constants needed to build a hash code. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * If the TestTransients parameter is set to true, transient members will be tested, otherwise they + * are ignored, as they are likely derived fields, and not part of the value of the Object. + *

+ * + *

+ * Static fields will not be tested. Superclass fields will be included. + *

+ * + * @param object + * the Object to create a hashCode for + * @param testTransients + * whether to include transient fields + * @return int hash code + * @throws IllegalArgumentException + * if the object is null + */ + public static int reflectionHashCode(Object object, boolean testTransients) { + return reflectionHashCode(17, 37, object, testTransients, null); + } + + /** + *

+ * This method uses reflection to build a valid hash code. + *

+ * + *

+ * This constructor uses two hard coded choices for the constants needed to build a hash code. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * Transient members will be not be used, as they are likely derived fields, and not part of the value of the + * Object. + *

+ * + *

+ * Static fields will not be tested. Superclass fields will be included. + *

+ * + * @param object + * the Object to create a hashCode for + * @param excludeFields + * Collection of String field names to exclude from use in calculation of hash code + * @return int hash code + * @throws IllegalArgumentException + * if the object is null + */ + public static int reflectionHashCode(Object object, Collection excludeFields) { + return reflectionHashCode(object, ReflectionToStringBuilder.toNoNullStringArray(excludeFields)); + } + + // ------------------------------------------------------------------------- + + /** + *

+ * This method uses reflection to build a valid hash code. + *

+ * + *

+ * This constructor uses two hard coded choices for the constants needed to build a hash code. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * Transient members will be not be used, as they are likely derived fields, and not part of the value of the + * Object. + *

+ * + *

+ * Static fields will not be tested. Superclass fields will be included. + *

+ * + * @param object + * the Object to create a hashCode for + * @param excludeFields + * array of field names to exclude from use in calculation of hash code + * @return int hash code + * @throws IllegalArgumentException + * if the object is null + */ + public static int reflectionHashCode(Object object, String... excludeFields) { + return reflectionHashCode(17, 37, object, false, null, excludeFields); + } + + /** + *

+ * Registers the given object. Used by the reflection methods to avoid infinite loops. + *

+ * + * @param value + * The object to register. + */ + static void register(Object value) { + synchronized (HashCodeBuilder.class) { + if (getRegistry() == null) { + REGISTRY.set(new HashSet()); + } + } + getRegistry().add(new IDKey(value)); + } + + /** + *

+ * Unregisters the given object. + *

+ * + *

+ * Used by the reflection methods to avoid infinite loops. + * + * @param value + * The object to unregister. + * @since 2.3 + */ + static void unregister(Object value) { + Set registry = getRegistry(); + if (registry != null) { + registry.remove(new IDKey(value)); + synchronized (HashCodeBuilder.class) { + //read again + registry = getRegistry(); + if (registry != null && registry.isEmpty()) { + REGISTRY.remove(); + } + } + } + } + + /** + * Constant to use in building the hashCode. + */ + private final int iConstant; + + /** + * Running total of the hashCode. + */ + private int iTotal = 0; + + /** + *

+ * Uses two hard coded choices for the constants needed to build a hashCode. + *

+ */ + public HashCodeBuilder() { + iConstant = 37; + iTotal = 17; + } + + /** + *

+ * Two randomly chosen, non-zero, odd numbers must be passed in. Ideally these should be different for each class, + * however this is not vital. + *

+ * + *

+ * Prime numbers are preferred, especially for the multiplier. + *

+ * + * @param initialNonZeroOddNumber + * a non-zero, odd number used as the initial value + * @param multiplierNonZeroOddNumber + * a non-zero, odd number used as the multiplier + * @throws IllegalArgumentException + * if the number is zero or even + */ + public HashCodeBuilder(int initialNonZeroOddNumber, int multiplierNonZeroOddNumber) { + if (initialNonZeroOddNumber == 0) { + throw new IllegalArgumentException("HashCodeBuilder requires a non zero initial value"); + } + if (initialNonZeroOddNumber % 2 == 0) { + throw new IllegalArgumentException("HashCodeBuilder requires an odd initial value"); + } + if (multiplierNonZeroOddNumber == 0) { + throw new IllegalArgumentException("HashCodeBuilder requires a non zero multiplier"); + } + if (multiplierNonZeroOddNumber % 2 == 0) { + throw new IllegalArgumentException("HashCodeBuilder requires an odd multiplier"); + } + iConstant = multiplierNonZeroOddNumber; + iTotal = initialNonZeroOddNumber; + } + + /** + *

+ * Append a hashCode for a boolean. + *

+ *

+ * This adds 1 when true, and 0 when false to the hashCode. + *

+ *

+ * This is in contrast to the standard java.lang.Boolean.hashCode handling, which computes + * a hashCode value of 1231 for java.lang.Boolean instances + * that represent true or 1237 for java.lang.Boolean instances + * that represent false. + *

+ *

+ * This is in accordance with the Effective Java design. + *

+ * + * @param value + * the boolean to add to the hashCode + * @return this + */ + public HashCodeBuilder append(boolean value) { + iTotal = iTotal * iConstant + (value ? 0 : 1); + return this; + } + + /** + *

+ * Append a hashCode for a boolean array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(boolean[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (boolean element : array) { + append(element); + } + } + return this; + } + + // ------------------------------------------------------------------------- + + /** + *

+ * Append a hashCode for a byte. + *

+ * + * @param value + * the byte to add to the hashCode + * @return this + */ + public HashCodeBuilder append(byte value) { + iTotal = iTotal * iConstant + value; + return this; + } + + // ------------------------------------------------------------------------- + + /** + *

+ * Append a hashCode for a byte array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(byte[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (byte element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for a char. + *

+ * + * @param value + * the char to add to the hashCode + * @return this + */ + public HashCodeBuilder append(char value) { + iTotal = iTotal * iConstant + value; + return this; + } + + /** + *

+ * Append a hashCode for a char array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(char[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (char element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for a double. + *

+ * + * @param value + * the double to add to the hashCode + * @return this + */ + public HashCodeBuilder append(double value) { + return append(Double.doubleToLongBits(value)); + } + + /** + *

+ * Append a hashCode for a double array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(double[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (double element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for a float. + *

+ * + * @param value + * the float to add to the hashCode + * @return this + */ + public HashCodeBuilder append(float value) { + iTotal = iTotal * iConstant + Float.floatToIntBits(value); + return this; + } + + /** + *

+ * Append a hashCode for a float array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(float[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (float element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for an int. + *

+ * + * @param value + * the int to add to the hashCode + * @return this + */ + public HashCodeBuilder append(int value) { + iTotal = iTotal * iConstant + value; + return this; + } + + /** + *

+ * Append a hashCode for an int array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(int[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (int element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for a long. + *

+ * + * @param value + * the long to add to the hashCode + * @return this + */ + // NOTE: This method uses >> and not >>> as Effective Java and + // Long.hashCode do. Ideally we should switch to >>> at + // some stage. There are backwards compat issues, so + // that will have to wait for the time being. cf LANG-342. + public HashCodeBuilder append(long value) { + iTotal = iTotal * iConstant + ((int) (value ^ (value >> 32))); + return this; + } + + /** + *

+ * Append a hashCode for a long array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(long[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (long element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for an Object. + *

+ * + * @param object + * the Object to add to the hashCode + * @return this + */ + public HashCodeBuilder append(Object object) { + if (object == null) { + iTotal = iTotal * iConstant; + + } else { + if(object.getClass().isArray()) { + // 'Switch' on type of array, to dispatch to the correct handler + // This handles multi dimensional arrays + if (object instanceof long[]) { + append((long[]) object); + } else if (object instanceof int[]) { + append((int[]) object); + } else if (object instanceof short[]) { + append((short[]) object); + } else if (object instanceof char[]) { + append((char[]) object); + } else if (object instanceof byte[]) { + append((byte[]) object); + } else if (object instanceof double[]) { + append((double[]) object); + } else if (object instanceof float[]) { + append((float[]) object); + } else if (object instanceof boolean[]) { + append((boolean[]) object); + } else { + // Not an array of primitives + append((Object[]) object); + } + } else { + iTotal = iTotal * iConstant + object.hashCode(); + } + } + return this; + } + + /** + *

+ * Append a hashCode for an Object array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(Object[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (Object element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Append a hashCode for a short. + *

+ * + * @param value + * the short to add to the hashCode + * @return this + */ + public HashCodeBuilder append(short value) { + iTotal = iTotal * iConstant + value; + return this; + } + + /** + *

+ * Append a hashCode for a short array. + *

+ * + * @param array + * the array to add to the hashCode + * @return this + */ + public HashCodeBuilder append(short[] array) { + if (array == null) { + iTotal = iTotal * iConstant; + } else { + for (short element : array) { + append(element); + } + } + return this; + } + + /** + *

+ * Adds the result of super.hashCode() to this builder. + *

+ * + * @param superHashCode + * the result of calling super.hashCode() + * @return this HashCodeBuilder, used to chain calls. + * @since 2.0 + */ + public HashCodeBuilder appendSuper(int superHashCode) { + iTotal = iTotal * iConstant + superHashCode; + return this; + } + + /** + *

+ * Return the computed hashCode. + *

+ * + * @return hashCode based on the fields appended + */ + public int toHashCode() { + return iTotal; + } + + /** + * Returns the computed hashCode. + * + * @return hashCode based on the fields appended + * + * @since 3.0 + */ + public Integer build() { + return Integer.valueOf(toHashCode()); + } + + /** + *

+ * The computed hashCode from toHashCode() is returned due to the likelihood + * of bugs in mis-calling toHashCode() and the unlikeliness of it mattering what the hashCode for + * HashCodeBuilder itself is.

+ * + * @return hashCode based on the fields appended + * @since 2.5 + */ + @Override + public int hashCode() { + return toHashCode(); + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/IDKey.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/IDKey.java new file mode 100644 index 00000000..68d93885 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/IDKey.java @@ -0,0 +1,74 @@ +/* + * 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 external.org.apache.commons.lang3.builder; + +// adapted from org.apache.axis.utils.IDKey + +/** + * Wrap an identity key (System.identityHashCode()) + * so that an object can only be equal() to itself. + * + * This is necessary to disambiguate the occasional duplicate + * identityHashCodes that can occur. + * + */ +final class IDKey { + private final Object value; + private final int id; + + /** + * Constructor for IDKey + * @param _value The value + */ + public IDKey(Object _value) { + // This is the Object hashcode + id = System.identityHashCode(_value); + // There have been some cases (LANG-459) that return the + // same identity hash code for different objects. So + // the value is also added to disambiguate these cases. + value = _value; + } + + /** + * returns hashcode - i.e. the system identity hashcode. + * @return the hashcode + */ + @Override + public int hashCode() { + return id; + } + + /** + * checks if instances are equal + * @param other The other object to compare to + * @return if the instances are for the same object + */ + @Override + public boolean equals(Object other) { + if (!(other instanceof IDKey)) { + return false; + } + IDKey idKey = (IDKey) other; + if (id != idKey.id) { + return false; + } + // Note that identity equals is used. + return value == idKey.value; + } +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java new file mode 100644 index 00000000..a6f41ec6 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java @@ -0,0 +1,691 @@ +/* + * 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 external.org.apache.commons.lang3.builder; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import external.org.apache.commons.lang3.ArrayUtils; +import external.org.apache.commons.lang3.ClassUtils; + +/** + *

+ * Assists in implementing {@link Object#toString()} methods using reflection. + *

+ *

+ * This class uses reflection to determine the fields to append. Because these fields are usually private, the class + * uses {@link java.lang.reflect.AccessibleObject#setAccessible(java.lang.reflect.AccessibleObject[], boolean)} to + * change the visibility of the fields. This will fail under a security manager, unless the appropriate permissions are + * set up correctly. + *

+ *

+ * Using reflection to access (private) fields circumvents any synchronization protection guarding access to these + * fields. If a toString method cannot safely read a field, you should exclude it from the toString method, or use + * synchronization consistent with the class' lock management around the invocation of the method. Take special care to + * exclude non-thread-safe collection classes, because these classes may throw ConcurrentModificationException if + * modified while the toString method is executing. + *

+ *

+ * A typical invocation for this method would look like: + *

+ *
+ * public String toString() {
+ *     return ReflectionToStringBuilder.toString(this);
+ * }
+ * 
+ *

+ * You can also use the builder to debug 3rd party objects: + *

+ *
+ * System.out.println("An object: " + ReflectionToStringBuilder.toString(anObject));
+ * 
+ *

+ * A subclass can control field output by overriding the methods: + *

    + *
  • {@link #accept(java.lang.reflect.Field)}
  • + *
  • {@link #getValue(java.lang.reflect.Field)}
  • + *
+ *

+ *

+ * For example, this method does not include the password field in the returned String: + *

+ *
+ * public String toString() {
+ *     return (new ReflectionToStringBuilder(this) {
+ *         protected boolean accept(Field f) {
+ *             return super.accept(f) && !f.getName().equals("password");
+ *         }
+ *     }).toString();
+ * }
+ * 
+ *

+ * The exact format of the toString is determined by the {@link ToStringStyle} passed into the constructor. + *

+ * + * @since 2.0 + * @version $Id: ReflectionToStringBuilder.java 1200177 2011-11-10 06:14:33Z ggregory $ + */ +public class ReflectionToStringBuilder extends ToStringBuilder { + + /** + *

+ * Builds a toString value using the default ToStringStyle through reflection. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * Transient members will be not be included, as they are likely derived. Static fields will not be included. + * Superclass fields will be appended. + *

+ * + * @param object + * the Object to be output + * @return the String result + * @throws IllegalArgumentException + * if the Object is null + */ + public static String toString(Object object) { + return toString(object, null, false, false, null); + } + + /** + *

+ * Builds a toString value through reflection. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * Transient members will be not be included, as they are likely derived. Static fields will not be included. + * Superclass fields will be appended. + *

+ * + *

+ * If the style is null, the default ToStringStyle is used. + *

+ * + * @param object + * the Object to be output + * @param style + * the style of the toString to create, may be null + * @return the String result + * @throws IllegalArgumentException + * if the Object or ToStringStyle is null + */ + public static String toString(Object object, ToStringStyle style) { + return toString(object, style, false, false, null); + } + + /** + *

+ * Builds a toString value through reflection. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * If the outputTransients is true, transient members will be output, otherwise they + * are ignored, as they are likely derived fields, and not part of the value of the Object. + *

+ * + *

+ * Static fields will not be included. Superclass fields will be appended. + *

+ * + *

+ * If the style is null, the default ToStringStyle is used. + *

+ * + * @param object + * the Object to be output + * @param style + * the style of the toString to create, may be null + * @param outputTransients + * whether to include transient fields + * @return the String result + * @throws IllegalArgumentException + * if the Object is null + */ + public static String toString(Object object, ToStringStyle style, boolean outputTransients) { + return toString(object, style, outputTransients, false, null); + } + + /** + *

+ * Builds a toString value through reflection. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * If the outputTransients is true, transient fields will be output, otherwise they + * are ignored, as they are likely derived fields, and not part of the value of the Object. + *

+ * + *

+ * If the outputStatics is true, static fields will be output, otherwise they are + * ignored. + *

+ * + *

+ * Static fields will not be included. Superclass fields will be appended. + *

+ * + *

+ * If the style is null, the default ToStringStyle is used. + *

+ * + * @param object + * the Object to be output + * @param style + * the style of the toString to create, may be null + * @param outputTransients + * whether to include transient fields + * @param outputStatics + * whether to include transient fields + * @return the String result + * @throws IllegalArgumentException + * if the Object is null + * @since 2.1 + */ + public static String toString(Object object, ToStringStyle style, boolean outputTransients, boolean outputStatics) { + return toString(object, style, outputTransients, outputStatics, null); + } + + /** + *

+ * Builds a toString value through reflection. + *

+ * + *

+ * It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will + * throw a security exception if run under a security manager, if the permissions are not set up correctly. It is + * also not as efficient as testing explicitly. + *

+ * + *

+ * If the outputTransients is true, transient fields will be output, otherwise they + * are ignored, as they are likely derived fields, and not part of the value of the Object. + *

+ * + *

+ * If the outputStatics is true, static fields will be output, otherwise they are + * ignored. + *

+ * + *

+ * Superclass fields will be appended up to and including the specified superclass. A null superclass is treated as + * java.lang.Object. + *

+ * + *

+ * If the style is null, the default ToStringStyle is used. + *

+ * + * @param + * the type of the object + * @param object + * the Object to be output + * @param style + * the style of the toString to create, may be null + * @param outputTransients + * whether to include transient fields + * @param outputStatics + * whether to include static fields + * @param reflectUpToClass + * the superclass to reflect up to (inclusive), may be null + * @return the String result + * @throws IllegalArgumentException + * if the Object is null + * @since 2.1 + */ + public static String toString( + T object, ToStringStyle style, boolean outputTransients, + boolean outputStatics, Class reflectUpToClass) { + return new ReflectionToStringBuilder(object, style, null, reflectUpToClass, outputTransients, outputStatics) + .toString(); + } + + /** + * Builds a String for a toString method excluding the given field names. + * + * @param object + * The object to "toString". + * @param excludeFieldNames + * The field names to exclude. Null excludes nothing. + * @return The toString value. + */ + public static String toStringExclude(Object object, Collection excludeFieldNames) { + return toStringExclude(object, toNoNullStringArray(excludeFieldNames)); + } + + /** + * Converts the given Collection into an array of Strings. The returned array does not contain null + * entries. Note that {@link Arrays#sort(Object[])} will throw an {@link NullPointerException} if an array element + * is null. + * + * @param collection + * The collection to convert + * @return A new array of Strings. + */ + static String[] toNoNullStringArray(Collection collection) { + if (collection == null) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + return toNoNullStringArray(collection.toArray()); + } + + /** + * Returns a new array of Strings without null elements. Internal method used to normalize exclude lists + * (arrays and collections). Note that {@link Arrays#sort(Object[])} will throw an {@link NullPointerException} + * if an array element is null. + * + * @param array + * The array to check + * @return The given array or a new array without null. + */ + static String[] toNoNullStringArray(Object[] array) { + List list = new ArrayList(array.length); + for (Object e : array) { + if (e != null) { + list.add(e.toString()); + } + } + return list.toArray(ArrayUtils.EMPTY_STRING_ARRAY); + } + + + /** + * Builds a String for a toString method excluding the given field names. + * + * @param object + * The object to "toString". + * @param excludeFieldNames + * The field names to exclude + * @return The toString value. + */ + public static String toStringExclude(Object object, String... excludeFieldNames) { + return new ReflectionToStringBuilder(object).setExcludeFieldNames(excludeFieldNames).toString(); + } + + /** + * Whether or not to append static fields. + */ + private boolean appendStatics = false; + + /** + * Whether or not to append transient fields. + */ + private boolean appendTransients = false; + + /** + * Which field names to exclude from output. Intended for fields like "password". + * + * @since 3.0 this is protected instead of private + */ + protected String[] excludeFieldNames; + + /** + * The last super class to stop appending fields for. + */ + private Class upToClass = null; + + /** + *

+ * Constructor. + *

+ * + *

+ * This constructor outputs using the default style set with setDefaultStyle. + *

+ * + * @param object + * the Object to build a toString for, must not be null + * @throws IllegalArgumentException + * if the Object passed in is null + */ + public ReflectionToStringBuilder(Object object) { + super(object); + } + + /** + *

+ * Constructor. + *

+ * + *

+ * If the style is null, the default style is used. + *

+ * + * @param object + * the Object to build a toString for, must not be null + * @param style + * the style of the toString to create, may be null + * @throws IllegalArgumentException + * if the Object passed in is null + */ + public ReflectionToStringBuilder(Object object, ToStringStyle style) { + super(object, style); + } + + /** + *

+ * Constructor. + *

+ * + *

+ * If the style is null, the default style is used. + *

+ * + *

+ * If the buffer is null, a new one is created. + *

+ * + * @param object + * the Object to build a toString for + * @param style + * the style of the toString to create, may be null + * @param buffer + * the StringBuffer to populate, may be null + * @throws IllegalArgumentException + * if the Object passed in is null + */ + public ReflectionToStringBuilder(Object object, ToStringStyle style, StringBuffer buffer) { + super(object, style, buffer); + } + + /** + * Constructor. + * + * @param + * the type of the object + * @param object + * the Object to build a toString for + * @param style + * the style of the toString to create, may be null + * @param buffer + * the StringBuffer to populate, may be null + * @param reflectUpToClass + * the superclass to reflect up to (inclusive), may be null + * @param outputTransients + * whether to include transient fields + * @param outputStatics + * whether to include static fields + * @since 2.1 + */ + public ReflectionToStringBuilder( + T object, ToStringStyle style, StringBuffer buffer, + Class reflectUpToClass, boolean outputTransients, boolean outputStatics) { + super(object, style, buffer); + this.setUpToClass(reflectUpToClass); + this.setAppendTransients(outputTransients); + this.setAppendStatics(outputStatics); + } + + /** + * Returns whether or not to append the given Field. + *
    + *
  • Transient fields are appended only if {@link #isAppendTransients()} returns true. + *
  • Static fields are appended only if {@link #isAppendStatics()} returns true. + *
  • Inner class fields are not appened.
  • + *
+ * + * @param field + * The Field to test. + * @return Whether or not to append the given Field. + */ + protected boolean accept(Field field) { + if (field.getName().indexOf(ClassUtils.INNER_CLASS_SEPARATOR_CHAR) != -1) { + // Reject field from inner class. + return false; + } + if (Modifier.isTransient(field.getModifiers()) && !this.isAppendTransients()) { + // Reject transient fields. + return false; + } + if (Modifier.isStatic(field.getModifiers()) && !this.isAppendStatics()) { + // Reject static fields. + return false; + } + if (this.excludeFieldNames != null + && Arrays.binarySearch(this.excludeFieldNames, field.getName()) >= 0) { + // Reject fields from the getExcludeFieldNames list. + return false; + } + return true; + } + + /** + *

+ * Appends the fields and values defined by the given object of the given Class. + *

+ * + *

+ * If a cycle is detected as an object is "toString()'ed", such an object is rendered as if + * Object.toString() had been called and not implemented by the object. + *

+ * + * @param clazz + * The class of object parameter + */ + protected void appendFieldsIn(Class clazz) { + if (clazz.isArray()) { + this.reflectionAppendArray(this.getObject()); + return; + } + Field[] fields = clazz.getDeclaredFields(); + AccessibleObject.setAccessible(fields, true); + for (Field field : fields) { + String fieldName = field.getName(); + if (this.accept(field)) { + try { + // Warning: Field.get(Object) creates wrappers objects + // for primitive types. + Object fieldValue = this.getValue(field); + this.append(fieldName, fieldValue); + } catch (IllegalAccessException ex) { + //this can't happen. Would get a Security exception + // instead + //throw a runtime exception in case the impossible + // happens. + throw new InternalError("Unexpected IllegalAccessException: " + ex.getMessage()); + } + } + } + } + + /** + * @return Returns the excludeFieldNames. + */ + public String[] getExcludeFieldNames() { + return this.excludeFieldNames.clone(); + } + + /** + *

+ * Gets the last super class to stop appending fields for. + *

+ * + * @return The last super class to stop appending fields for. + */ + public Class getUpToClass() { + return this.upToClass; + } + + /** + *

+ * Calls java.lang.reflect.Field.get(Object). + *

+ * + * @param field + * The Field to query. + * @return The Object from the given Field. + * + * @throws IllegalArgumentException + * see {@link java.lang.reflect.Field#get(Object)} + * @throws IllegalAccessException + * see {@link java.lang.reflect.Field#get(Object)} + * + * @see java.lang.reflect.Field#get(Object) + */ + protected Object getValue(Field field) throws IllegalArgumentException, IllegalAccessException { + return field.get(this.getObject()); + } + + /** + *

+ * Gets whether or not to append static fields. + *

+ * + * @return Whether or not to append static fields. + * @since 2.1 + */ + public boolean isAppendStatics() { + return this.appendStatics; + } + + /** + *

+ * Gets whether or not to append transient fields. + *

+ * + * @return Whether or not to append transient fields. + */ + public boolean isAppendTransients() { + return this.appendTransients; + } + + /** + *

+ * Append to the toString an Object array. + *

+ * + * @param array + * the array to add to the toString + * @return this + */ + public ReflectionToStringBuilder reflectionAppendArray(Object array) { + this.getStyle().reflectionAppendArrayDetail(this.getStringBuffer(), null, array); + return this; + } + + /** + *

+ * Sets whether or not to append static fields. + *

+ * + * @param appendStatics + * Whether or not to append static fields. + * @since 2.1 + */ + public void setAppendStatics(boolean appendStatics) { + this.appendStatics = appendStatics; + } + + /** + *

+ * Sets whether or not to append transient fields. + *

+ * + * @param appendTransients + * Whether or not to append transient fields. + */ + public void setAppendTransients(boolean appendTransients) { + this.appendTransients = appendTransients; + } + + /** + * Sets the field names to exclude. + * + * @param excludeFieldNamesParam + * The excludeFieldNames to excluding from toString or null. + * @return this + */ + public ReflectionToStringBuilder setExcludeFieldNames(String... excludeFieldNamesParam) { + if (excludeFieldNamesParam == null) { + this.excludeFieldNames = null; + } else { + //clone and remove nulls + this.excludeFieldNames = toNoNullStringArray(excludeFieldNamesParam); + Arrays.sort(this.excludeFieldNames); + } + return this; + } + + /** + *

+ * Sets the last super class to stop appending fields for. + *

+ * + * @param clazz + * The last super class to stop appending fields for. + */ + public void setUpToClass(Class clazz) { + if (clazz != null) { + Object object = getObject(); + if (object != null && clazz.isInstance(object) == false) { + throw new IllegalArgumentException("Specified class is not a superclass of the object"); + } + } + this.upToClass = clazz; + } + + /** + *

+ * Gets the String built by this builder. + *

+ * + * @return the built string + */ + @Override + public String toString() { + if (this.getObject() == null) { + return this.getStyle().getNullText(); + } + Class clazz = this.getObject().getClass(); + this.appendFieldsIn(clazz); + while (clazz.getSuperclass() != null && clazz != this.getUpToClass()) { + clazz = clazz.getSuperclass(); + this.appendFieldsIn(clazz); + } + return super.toString(); + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ToStringBuilder.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ToStringBuilder.java new file mode 100644 index 00000000..1cb42b5d --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ToStringBuilder.java @@ -0,0 +1,1079 @@ +/* + * 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 external.org.apache.commons.lang3.builder; + +import external.org.apache.commons.lang3.ObjectUtils; + +/** + *

Assists in implementing {@link Object#toString()} methods.

+ * + *

This class enables a good and consistent toString() to be built for any + * class or object. This class aims to simplify the process by:

+ *
    + *
  • allowing field names
  • + *
  • handling all types consistently
  • + *
  • handling nulls consistently
  • + *
  • outputting arrays and multi-dimensional arrays
  • + *
  • enabling the detail level to be controlled for Objects and Collections
  • + *
  • handling class hierarchies
  • + *
+ * + *

To use this class write code as follows:

+ * + *
+ * public class Person {
+ *   String name;
+ *   int age;
+ *   boolean smoker;
+ *
+ *   ...
+ *
+ *   public String toString() {
+ *     return new ToStringBuilder(this).
+ *       append("name", name).
+ *       append("age", age).
+ *       append("smoker", smoker).
+ *       toString();
+ *   }
+ * }
+ * 
+ * + *

This will produce a toString of the format: + * Person@7f54[name=Stephen,age=29,smoker=false]

+ * + *

To add the superclass toString, use {@link #appendSuper}. + * To append the toString from an object that is delegated + * to (or any other object), use {@link #appendToString}.

+ * + *

Alternatively, there is a method that uses reflection to determine + * the fields to test. Because these fields are usually private, the method, + * reflectionToString, uses AccessibleObject.setAccessible to + * change the visibility of the fields. This will fail under a security manager, + * unless the appropriate permissions are set up correctly. It is also + * slower than testing explicitly.

+ * + *

A typical invocation for this method would look like:

+ * + *
+ * public String toString() {
+ *   return ToStringBuilder.reflectionToString(this);
+ * }
+ * 
+ * + *

You can also use the builder to debug 3rd party objects:

+ * + *
+ * System.out.println("An object: " + ToStringBuilder.reflectionToString(anObject));
+ * 
+ * + *

The exact format of the toString is determined by + * the {@link ToStringStyle} passed into the constructor.

+ * + * @since 1.0 + * @version $Id: ToStringBuilder.java 1088899 2011-04-05 05:31:27Z bayard $ + */ +public class ToStringBuilder implements Builder { + + /** + * The default style of output to use, not null. + */ + private static volatile ToStringStyle defaultStyle = ToStringStyle.DEFAULT_STYLE; + + //---------------------------------------------------------------------------- + + /** + *

Gets the default ToStringStyle to use.

+ * + *

This method gets a singleton default value, typically for the whole JVM. + * Changing this default should generally only be done during application startup. + * It is recommended to pass a ToStringStyle to the constructor instead + * of using this global default.

+ * + *

This method can be used from multiple threads. + * Internally, a volatile variable is used to provide the guarantee + * that the latest value set using {@link #setDefaultStyle} is the value returned. + * It is strongly recommended that the default style is only changed during application startup.

+ * + *

One reason for changing the default could be to have a verbose style during + * development and a compact style in production.

+ * + * @return the default ToStringStyle, never null + */ + public static ToStringStyle getDefaultStyle() { + return defaultStyle; + } + + /** + *

Sets the default ToStringStyle to use.

+ * + *

This method sets a singleton default value, typically for the whole JVM. + * Changing this default should generally only be done during application startup. + * It is recommended to pass a ToStringStyle to the constructor instead + * of changing this global default.

+ * + *

This method is not intended for use from multiple threads. + * Internally, a volatile variable is used to provide the guarantee + * that the latest value set is the value returned from {@link #getDefaultStyle}.

+ * + * @param style the default ToStringStyle + * @throws IllegalArgumentException if the style is null + */ + public static void setDefaultStyle(ToStringStyle style) { + if (style == null) { + throw new IllegalArgumentException("The style must not be null"); + } + defaultStyle = style; + } + + //---------------------------------------------------------------------------- + /** + *

Uses ReflectionToStringBuilder to generate a + * toString for the specified object.

+ * + * @param object the Object to be output + * @return the String result + * @see ReflectionToStringBuilder#toString(Object) + */ + public static String reflectionToString(Object object) { + return ReflectionToStringBuilder.toString(object); + } + + /** + *

Uses ReflectionToStringBuilder to generate a + * toString for the specified object.

+ * + * @param object the Object to be output + * @param style the style of the toString to create, may be null + * @return the String result + * @see ReflectionToStringBuilder#toString(Object,ToStringStyle) + */ + public static String reflectionToString(Object object, ToStringStyle style) { + return ReflectionToStringBuilder.toString(object, style); + } + + /** + *

Uses ReflectionToStringBuilder to generate a + * toString for the specified object.

+ * + * @param object the Object to be output + * @param style the style of the toString to create, may be null + * @param outputTransients whether to include transient fields + * @return the String result + * @see ReflectionToStringBuilder#toString(Object,ToStringStyle,boolean) + */ + public static String reflectionToString(Object object, ToStringStyle style, boolean outputTransients) { + return ReflectionToStringBuilder.toString(object, style, outputTransients, false, null); + } + + /** + *

Uses ReflectionToStringBuilder to generate a + * toString for the specified object.

+ * + * @param the type of the object + * @param object the Object to be output + * @param style the style of the toString to create, may be null + * @param outputTransients whether to include transient fields + * @param reflectUpToClass the superclass to reflect up to (inclusive), may be null + * @return the String result + * @see ReflectionToStringBuilder#toString(Object,ToStringStyle,boolean,boolean,Class) + * @since 2.0 + */ + public static String reflectionToString( + T object, + ToStringStyle style, + boolean outputTransients, + Class reflectUpToClass) { + return ReflectionToStringBuilder.toString(object, style, outputTransients, false, reflectUpToClass); + } + + //---------------------------------------------------------------------------- + + /** + * Current toString buffer, not null. + */ + private final StringBuffer buffer; + /** + * The object being output, may be null. + */ + private final Object object; + /** + * The style of output to use, not null. + */ + private final ToStringStyle style; + + /** + *

Constructs a builder for the specified object using the default output style.

+ * + *

This default style is obtained from {@link #getDefaultStyle()}.

+ * + * @param object the Object to build a toString for, not recommended to be null + */ + public ToStringBuilder(Object object) { + this(object, null, null); + } + + /** + *

Constructs a builder for the specified object using the a defined output style.

+ * + *

If the style is null, the default style is used.

+ * + * @param object the Object to build a toString for, not recommended to be null + * @param style the style of the toString to create, null uses the default style + */ + public ToStringBuilder(Object object, ToStringStyle style) { + this(object, style, null); + } + + /** + *

Constructs a builder for the specified object.

+ * + *

If the style is null, the default style is used.

+ * + *

If the buffer is null, a new one is created.

+ * + * @param object the Object to build a toString for, not recommended to be null + * @param style the style of the toString to create, null uses the default style + * @param buffer the StringBuffer to populate, may be null + */ + public ToStringBuilder(Object object, ToStringStyle style, StringBuffer buffer) { + if (style == null) { + style = getDefaultStyle(); + } + if (buffer == null) { + buffer = new StringBuffer(512); + } + this.buffer = buffer; + this.style = style; + this.object = object; + + style.appendStart(buffer, object); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a boolean + * value.

+ * + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(boolean value) { + style.append(buffer, null, value); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a boolean + * array.

+ * + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(boolean[] array) { + style.append(buffer, null, array, null); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a byte + * value.

+ * + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(byte value) { + style.append(buffer, null, value); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a byte + * array.

+ * + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(byte[] array) { + style.append(buffer, null, array, null); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a char + * value.

+ * + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(char value) { + style.append(buffer, null, value); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a char + * array.

+ * + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(char[] array) { + style.append(buffer, null, array, null); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a double + * value.

+ * + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(double value) { + style.append(buffer, null, value); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a double + * array.

+ * + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(double[] array) { + style.append(buffer, null, array, null); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a float + * value.

+ * + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(float value) { + style.append(buffer, null, value); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a float + * array.

+ * + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(float[] array) { + style.append(buffer, null, array, null); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString an int + * value.

+ * + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(int value) { + style.append(buffer, null, value); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString an int + * array.

+ * + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(int[] array) { + style.append(buffer, null, array, null); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a long + * value.

+ * + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(long value) { + style.append(buffer, null, value); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a long + * array.

+ * + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(long[] array) { + style.append(buffer, null, array, null); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString an Object + * value.

+ * + * @param obj the value to add to the toString + * @return this + */ + public ToStringBuilder append(Object obj) { + style.append(buffer, null, obj, null); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString an Object + * array.

+ * + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(Object[] array) { + style.append(buffer, null, array, null); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a short + * value.

+ * + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(short value) { + style.append(buffer, null, value); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a short + * array.

+ * + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(short[] array) { + style.append(buffer, null, array, null); + return this; + } + + /** + *

Append to the toString a boolean + * value.

+ * + * @param fieldName the field name + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, boolean value) { + style.append(buffer, fieldName, value); + return this; + } + + /** + *

Append to the toString a boolean + * array.

+ * + * @param fieldName the field name + * @param array the array to add to the hashCode + * @return this + */ + public ToStringBuilder append(String fieldName, boolean[] array) { + style.append(buffer, fieldName, array, null); + return this; + } + + /** + *

Append to the toString a boolean + * array.

+ * + *

A boolean parameter controls the level of detail to show. + * Setting true will output the array in full. Setting + * false will output a summary, typically the size of + * the array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info + * @return this + */ + public ToStringBuilder append(String fieldName, boolean[] array, boolean fullDetail) { + style.append(buffer, fieldName, array, Boolean.valueOf(fullDetail)); + return this; + } + + /** + *

Append to the toString an byte + * value.

+ * + * @param fieldName the field name + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, byte value) { + style.append(buffer, fieldName, value); + return this; + } + + /** + *

Append to the toString a byte array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, byte[] array) { + style.append(buffer, fieldName, array, null); + return this; + } + + /** + *

Append to the toString a byte + * array.

+ * + *

A boolean parameter controls the level of detail to show. + * Setting true will output the array in full. Setting + * false will output a summary, typically the size of + * the array. + * + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info + * @return this + */ + public ToStringBuilder append(String fieldName, byte[] array, boolean fullDetail) { + style.append(buffer, fieldName, array, Boolean.valueOf(fullDetail)); + return this; + } + + /** + *

Append to the toString a char + * value.

+ * + * @param fieldName the field name + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, char value) { + style.append(buffer, fieldName, value); + return this; + } + + /** + *

Append to the toString a char + * array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, char[] array) { + style.append(buffer, fieldName, array, null); + return this; + } + + /** + *

Append to the toString a char + * array.

+ * + *

A boolean parameter controls the level of detail to show. + * Setting true will output the array in full. Setting + * false will output a summary, typically the size of + * the array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info + * @return this + */ + public ToStringBuilder append(String fieldName, char[] array, boolean fullDetail) { + style.append(buffer, fieldName, array, Boolean.valueOf(fullDetail)); + return this; + } + + /** + *

Append to the toString a double + * value.

+ * + * @param fieldName the field name + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, double value) { + style.append(buffer, fieldName, value); + return this; + } + + /** + *

Append to the toString a double + * array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, double[] array) { + style.append(buffer, fieldName, array, null); + return this; + } + + /** + *

Append to the toString a double + * array.

+ * + *

A boolean parameter controls the level of detail to show. + * Setting true will output the array in full. Setting + * false will output a summary, typically the size of + * the array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info + * @return this + */ + public ToStringBuilder append(String fieldName, double[] array, boolean fullDetail) { + style.append(buffer, fieldName, array, Boolean.valueOf(fullDetail)); + return this; + } + + /** + *

Append to the toString an float + * value.

+ * + * @param fieldName the field name + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, float value) { + style.append(buffer, fieldName, value); + return this; + } + + /** + *

Append to the toString a float + * array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, float[] array) { + style.append(buffer, fieldName, array, null); + return this; + } + + /** + *

Append to the toString a float + * array.

+ * + *

A boolean parameter controls the level of detail to show. + * Setting true will output the array in full. Setting + * false will output a summary, typically the size of + * the array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info + * @return this + */ + public ToStringBuilder append(String fieldName, float[] array, boolean fullDetail) { + style.append(buffer, fieldName, array, Boolean.valueOf(fullDetail)); + return this; + } + + /** + *

Append to the toString an int + * value.

+ * + * @param fieldName the field name + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, int value) { + style.append(buffer, fieldName, value); + return this; + } + + /** + *

Append to the toString an int + * array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, int[] array) { + style.append(buffer, fieldName, array, null); + return this; + } + + /** + *

Append to the toString an int + * array.

+ * + *

A boolean parameter controls the level of detail to show. + * Setting true will output the array in full. Setting + * false will output a summary, typically the size of + * the array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info + * @return this + */ + public ToStringBuilder append(String fieldName, int[] array, boolean fullDetail) { + style.append(buffer, fieldName, array, Boolean.valueOf(fullDetail)); + return this; + } + + /** + *

Append to the toString a long + * value.

+ * + * @param fieldName the field name + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, long value) { + style.append(buffer, fieldName, value); + return this; + } + + /** + *

Append to the toString a long + * array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, long[] array) { + style.append(buffer, fieldName, array, null); + return this; + } + + /** + *

Append to the toString a long + * array.

+ * + *

A boolean parameter controls the level of detail to show. + * Setting true will output the array in full. Setting + * false will output a summary, typically the size of + * the array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info + * @return this + */ + public ToStringBuilder append(String fieldName, long[] array, boolean fullDetail) { + style.append(buffer, fieldName, array, Boolean.valueOf(fullDetail)); + return this; + } + + /** + *

Append to the toString an Object + * value.

+ * + * @param fieldName the field name + * @param obj the value to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, Object obj) { + style.append(buffer, fieldName, obj, null); + return this; + } + + /** + *

Append to the toString an Object + * value.

+ * + * @param fieldName the field name + * @param obj the value to add to the toString + * @param fullDetail true for detail, + * false for summary info + * @return this + */ + public ToStringBuilder append(String fieldName, Object obj, boolean fullDetail) { + style.append(buffer, fieldName, obj, Boolean.valueOf(fullDetail)); + return this; + } + + /** + *

Append to the toString an Object + * array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, Object[] array) { + style.append(buffer, fieldName, array, null); + return this; + } + + /** + *

Append to the toString an Object + * array.

+ * + *

A boolean parameter controls the level of detail to show. + * Setting true will output the array in full. Setting + * false will output a summary, typically the size of + * the array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info + * @return this + */ + public ToStringBuilder append(String fieldName, Object[] array, boolean fullDetail) { + style.append(buffer, fieldName, array, Boolean.valueOf(fullDetail)); + return this; + } + + /** + *

Append to the toString an short + * value.

+ * + * @param fieldName the field name + * @param value the value to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, short value) { + style.append(buffer, fieldName, value); + return this; + } + + /** + *

Append to the toString a short + * array.

+ * + * @param fieldName the field name + * @param array the array to add to the toString + * @return this + */ + public ToStringBuilder append(String fieldName, short[] array) { + style.append(buffer, fieldName, array, null); + return this; + } + + /** + *

Append to the toString a short + * array.

+ * + *

A boolean parameter controls the level of detail to show. + * Setting true will output the array in full. Setting + * false will output a summary, typically the size of + * the array. + * + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info + * @return this + */ + public ToStringBuilder append(String fieldName, short[] array, boolean fullDetail) { + style.append(buffer, fieldName, array, Boolean.valueOf(fullDetail)); + return this; + } + + /** + *

Appends with the same format as the default Object toString() + * method. Appends the class name followed by + * {@link System#identityHashCode(java.lang.Object)}.

+ * + * @param object the Object whose class name and id to output + * @return this + * @since 2.0 + */ + public ToStringBuilder appendAsObjectToString(Object object) { + ObjectUtils.identityToString(this.getStringBuffer(), object); + return this; + } + + //---------------------------------------------------------------------------- + + /** + *

Append the toString from the superclass.

+ * + *

This method assumes that the superclass uses the same ToStringStyle + * as this one.

+ * + *

If superToString is null, no change is made.

+ * + * @param superToString the result of super.toString() + * @return this + * @since 2.0 + */ + public ToStringBuilder appendSuper(String superToString) { + if (superToString != null) { + style.appendSuper(buffer, superToString); + } + return this; + } + + /** + *

Append the toString from another object.

+ * + *

This method is useful where a class delegates most of the implementation of + * its properties to another class. You can then call toString() on + * the other class and pass the result into this method.

+ * + *
+     *   private AnotherObject delegate;
+     *   private String fieldInThisClass;
+     *
+     *   public String toString() {
+     *     return new ToStringBuilder(this).
+     *       appendToString(delegate.toString()).
+     *       append(fieldInThisClass).
+     *       toString();
+     *   }
+ * + *

This method assumes that the other object uses the same ToStringStyle + * as this one.

+ * + *

If the toString is null, no change is made.

+ * + * @param toString the result of toString() on another object + * @return this + * @since 2.0 + */ + public ToStringBuilder appendToString(String toString) { + if (toString != null) { + style.appendToString(buffer, toString); + } + return this; + } + + /** + *

Returns the Object being output.

+ * + * @return The object being output. + * @since 2.0 + */ + public Object getObject() { + return object; + } + + /** + *

Gets the StringBuffer being populated.

+ * + * @return the StringBuffer being populated + */ + public StringBuffer getStringBuffer() { + return buffer; + } + + //---------------------------------------------------------------------------- + + /** + *

Gets the ToStringStyle being used.

+ * + * @return the ToStringStyle being used + * @since 2.0 + */ + public ToStringStyle getStyle() { + return style; + } + + /** + *

Returns the built toString.

+ * + *

This method appends the end of data indicator, and can only be called once. + * Use {@link #getStringBuffer} to get the current string state.

+ * + *

If the object is null, return the style's nullText

+ * + * @return the String toString + */ + @Override + public String toString() { + if (this.getObject() == null) { + this.getStringBuffer().append(this.getStyle().getNullText()); + } else { + style.appendEnd(this.getStringBuffer(), this.getObject()); + } + return this.getStringBuffer().toString(); + } + + /** + * Returns the String that was build as an object representation. The + * default implementation utilizes the {@link #toString()} implementation. + * + * @return the String toString + * + * @see #toString() + * + * @since 3.0 + */ + public String build() { + return toString(); + } +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ToStringStyle.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ToStringStyle.java new file mode 100644 index 00000000..783ae6f6 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/ToStringStyle.java @@ -0,0 +1,2271 @@ +/* + * 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 external.org.apache.commons.lang3.builder; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Map; +import java.util.WeakHashMap; + +import external.org.apache.commons.lang3.ClassUtils; +import external.org.apache.commons.lang3.ObjectUtils; +import external.org.apache.commons.lang3.SystemUtils; + +/** + *

Controls String formatting for {@link ToStringBuilder}. + * The main public interface is always via ToStringBuilder.

+ * + *

These classes are intended to be used as Singletons. + * There is no need to instantiate a new style each time. A program + * will generally use one of the predefined constants on this class. + * Alternatively, the {@link StandardToStringStyle} class can be used + * to set the individual settings. Thus most styles can be achieved + * without subclassing.

+ * + *

If required, a subclass can override as many or as few of the + * methods as it requires. Each object type (from boolean + * to long to Object to int[]) has + * its own methods to output it. Most have two versions, detail and summary. + * + *

For example, the detail version of the array based methods will + * output the whole array, whereas the summary method will just output + * the array length.

+ * + *

If you want to format the output of certain objects, such as dates, you + * must create a subclass and override a method. + *

+ * public class MyStyle extends ToStringStyle {
+ *   protected void appendDetail(StringBuffer buffer, String fieldName, Object value) {
+ *     if (value instanceof Date) {
+ *       value = new SimpleDateFormat("yyyy-MM-dd").format(value);
+ *     }
+ *     buffer.append(value);
+ *   }
+ * }
+ * 
+ *

+ * + * @since 1.0 + * @version $Id: ToStringStyle.java 1091066 2011-04-11 13:30:11Z mbenson $ + */ +public abstract class ToStringStyle implements Serializable { + + /** + * Serialization version ID. + */ + private static final long serialVersionUID = -2587890625525655916L; + + /** + * The default toString style. Using the Using the Person + * example from {@link ToStringBuilder}, the output would look like this: + * + *
+     * Person@182f0db[name=John Doe,age=33,smoker=false]
+     * 
+ */ + public static final ToStringStyle DEFAULT_STYLE = new DefaultToStringStyle(); + + /** + * The multi line toString style. Using the Using the Person + * example from {@link ToStringBuilder}, the output would look like this: + * + *
+     * Person@182f0db[
+     *   name=John Doe
+     *   age=33
+     *   smoker=false
+     * ]
+     * 
+ */ + public static final ToStringStyle MULTI_LINE_STYLE = new MultiLineToStringStyle(); + + /** + * The no field names toString style. Using the Using the + * Person example from {@link ToStringBuilder}, the output + * would look like this: + * + *
+     * Person@182f0db[John Doe,33,false]
+     * 
+ */ + public static final ToStringStyle NO_FIELD_NAMES_STYLE = new NoFieldNameToStringStyle(); + + /** + * The short prefix toString style. Using the Person example + * from {@link ToStringBuilder}, the output would look like this: + * + *
+     * Person[name=John Doe,age=33,smoker=false]
+     * 
+ * + * @since 2.1 + */ + public static final ToStringStyle SHORT_PREFIX_STYLE = new ShortPrefixToStringStyle(); + + /** + * The simple toString style. Using the Using the Person + * example from {@link ToStringBuilder}, the output would look like this: + * + *
+     * John Doe,33,false
+     * 
+ */ + public static final ToStringStyle SIMPLE_STYLE = new SimpleToStringStyle(); + + /** + *

+ * A registry of objects used by reflectionToString methods + * to detect cyclical object references and avoid infinite loops. + *

+ */ + private static final ThreadLocal> REGISTRY = + new ThreadLocal>(); + + /** + *

+ * Returns the registry of objects being traversed by the reflectionToString + * methods in the current thread. + *

+ * + * @return Set the registry of objects being traversed + */ + static Map getRegistry() { + return REGISTRY.get(); + } + + /** + *

+ * Returns true if the registry contains the given object. + * Used by the reflection methods to avoid infinite loops. + *

+ * + * @param value + * The object to lookup in the registry. + * @return boolean true if the registry contains the given + * object. + */ + static boolean isRegistered(Object value) { + Map m = getRegistry(); + return m != null && m.containsKey(value); + } + + /** + *

+ * Registers the given object. Used by the reflection methods to avoid + * infinite loops. + *

+ * + * @param value + * The object to register. + */ + static void register(Object value) { + if (value != null) { + Map m = getRegistry(); + if (m == null) { + REGISTRY.set(new WeakHashMap()); + } + getRegistry().put(value, null); + } + } + + /** + *

+ * Unregisters the given object. + *

+ * + *

+ * Used by the reflection methods to avoid infinite loops. + *

+ * + * @param value + * The object to unregister. + */ + static void unregister(Object value) { + if (value != null) { + Map m = getRegistry(); + if (m != null) { + m.remove(value); + if (m.isEmpty()) { + REGISTRY.remove(); + } + } + } + } + + /** + * Whether to use the field names, the default is true. + */ + private boolean useFieldNames = true; + + /** + * Whether to use the class name, the default is true. + */ + private boolean useClassName = true; + + /** + * Whether to use short class names, the default is false. + */ + private boolean useShortClassName = false; + + /** + * Whether to use the identity hash code, the default is true. + */ + private boolean useIdentityHashCode = true; + + /** + * The content start '['. + */ + private String contentStart = "["; + + /** + * The content end ']'. + */ + private String contentEnd = "]"; + + /** + * The field name value separator '='. + */ + private String fieldNameValueSeparator = "="; + + /** + * Whether the field separator should be added before any other fields. + */ + private boolean fieldSeparatorAtStart = false; + + /** + * Whether the field separator should be added after any other fields. + */ + private boolean fieldSeparatorAtEnd = false; + + /** + * The field separator ','. + */ + private String fieldSeparator = ","; + + /** + * The array start '{'. + */ + private String arrayStart = "{"; + + /** + * The array separator ','. + */ + private String arraySeparator = ","; + + /** + * The detail for array content. + */ + private boolean arrayContentDetail = true; + + /** + * The array end '}'. + */ + private String arrayEnd = "}"; + + /** + * The value to use when fullDetail is null, + * the default value is true. + */ + private boolean defaultFullDetail = true; + + /** + * The null text '<null>'. + */ + private String nullText = ""; + + /** + * The summary size text start '. + */ + private String sizeStartText = "'>'. + */ + private String sizeEndText = ">"; + + /** + * The summary object text start '<'. + */ + private String summaryObjectStartText = "<"; + + /** + * The summary object text start '>'. + */ + private String summaryObjectEndText = ">"; + + //---------------------------------------------------------------------------- + + /** + *

Constructor.

+ */ + protected ToStringStyle() { + super(); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString the superclass toString.

+ *

NOTE: It assumes that the toString has been created from the same ToStringStyle.

+ * + *

A null superToString is ignored.

+ * + * @param buffer the StringBuffer to populate + * @param superToString the super.toString() + * @since 2.0 + */ + public void appendSuper(StringBuffer buffer, String superToString) { + appendToString(buffer, superToString); + } + + /** + *

Append to the toString another toString.

+ *

NOTE: It assumes that the toString has been created from the same ToStringStyle.

+ * + *

A null toString is ignored.

+ * + * @param buffer the StringBuffer to populate + * @param toString the additional toString + * @since 2.0 + */ + public void appendToString(StringBuffer buffer, String toString) { + if (toString != null) { + int pos1 = toString.indexOf(contentStart) + contentStart.length(); + int pos2 = toString.lastIndexOf(contentEnd); + if (pos1 != pos2 && pos1 >= 0 && pos2 >= 0) { + String data = toString.substring(pos1, pos2); + if (fieldSeparatorAtStart) { + removeLastFieldSeparator(buffer); + } + buffer.append(data); + appendFieldSeparator(buffer); + } + } + } + + /** + *

Append to the toString the start of data indicator.

+ * + * @param buffer the StringBuffer to populate + * @param object the Object to build a toString for + */ + public void appendStart(StringBuffer buffer, Object object) { + if (object != null) { + appendClassName(buffer, object); + appendIdentityHashCode(buffer, object); + appendContentStart(buffer); + if (fieldSeparatorAtStart) { + appendFieldSeparator(buffer); + } + } + } + + /** + *

Append to the toString the end of data indicator.

+ * + * @param buffer the StringBuffer to populate + * @param object the Object to build a + * toString for. + */ + public void appendEnd(StringBuffer buffer, Object object) { + if (this.fieldSeparatorAtEnd == false) { + removeLastFieldSeparator(buffer); + } + appendContentEnd(buffer); + unregister(object); + } + + /** + *

Remove the last field separator from the buffer.

+ * + * @param buffer the StringBuffer to populate + * @since 2.0 + */ + protected void removeLastFieldSeparator(StringBuffer buffer) { + int len = buffer.length(); + int sepLen = fieldSeparator.length(); + if (len > 0 && sepLen > 0 && len >= sepLen) { + boolean match = true; + for (int i = 0; i < sepLen; i++) { + if (buffer.charAt(len - 1 - i) != fieldSeparator.charAt(sepLen - 1 - i)) { + match = false; + break; + } + } + if (match) { + buffer.setLength(len - sepLen); + } + } + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString an Object + * value, printing the full toString of the + * Object passed in.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param value the value to add to the toString + * @param fullDetail true for detail, false + * for summary info, null for style decides + */ + public void append(StringBuffer buffer, String fieldName, Object value, Boolean fullDetail) { + appendFieldStart(buffer, fieldName); + + if (value == null) { + appendNullText(buffer, fieldName); + + } else { + appendInternal(buffer, fieldName, value, isFullDetail(fullDetail)); + } + + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString an Object, + * correctly interpreting its type.

+ * + *

This method performs the main lookup by Class type to correctly + * route arrays, Collections, Maps and + * Objects to the appropriate method.

+ * + *

Either detail or summary views can be specified.

+ * + *

If a cycle is detected, an object will be appended with the + * Object.toString() format.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString, + * not null + * @param detail output detail or not + */ + protected void appendInternal(StringBuffer buffer, String fieldName, Object value, boolean detail) { + if (isRegistered(value) + && !(value instanceof Number || value instanceof Boolean || value instanceof Character)) { + appendCyclicObject(buffer, fieldName, value); + return; + } + + register(value); + + try { + if (value instanceof Collection) { + if (detail) { + appendDetail(buffer, fieldName, (Collection) value); + } else { + appendSummarySize(buffer, fieldName, ((Collection) value).size()); + } + + } else if (value instanceof Map) { + if (detail) { + appendDetail(buffer, fieldName, (Map) value); + } else { + appendSummarySize(buffer, fieldName, ((Map) value).size()); + } + + } else if (value instanceof long[]) { + if (detail) { + appendDetail(buffer, fieldName, (long[]) value); + } else { + appendSummary(buffer, fieldName, (long[]) value); + } + + } else if (value instanceof int[]) { + if (detail) { + appendDetail(buffer, fieldName, (int[]) value); + } else { + appendSummary(buffer, fieldName, (int[]) value); + } + + } else if (value instanceof short[]) { + if (detail) { + appendDetail(buffer, fieldName, (short[]) value); + } else { + appendSummary(buffer, fieldName, (short[]) value); + } + + } else if (value instanceof byte[]) { + if (detail) { + appendDetail(buffer, fieldName, (byte[]) value); + } else { + appendSummary(buffer, fieldName, (byte[]) value); + } + + } else if (value instanceof char[]) { + if (detail) { + appendDetail(buffer, fieldName, (char[]) value); + } else { + appendSummary(buffer, fieldName, (char[]) value); + } + + } else if (value instanceof double[]) { + if (detail) { + appendDetail(buffer, fieldName, (double[]) value); + } else { + appendSummary(buffer, fieldName, (double[]) value); + } + + } else if (value instanceof float[]) { + if (detail) { + appendDetail(buffer, fieldName, (float[]) value); + } else { + appendSummary(buffer, fieldName, (float[]) value); + } + + } else if (value instanceof boolean[]) { + if (detail) { + appendDetail(buffer, fieldName, (boolean[]) value); + } else { + appendSummary(buffer, fieldName, (boolean[]) value); + } + + } else if (value.getClass().isArray()) { + if (detail) { + appendDetail(buffer, fieldName, (Object[]) value); + } else { + appendSummary(buffer, fieldName, (Object[]) value); + } + + } else { + if (detail) { + appendDetail(buffer, fieldName, value); + } else { + appendSummary(buffer, fieldName, value); + } + } + } finally { + unregister(value); + } + } + + /** + *

Append to the toString an Object + * value that has been detected to participate in a cycle. This + * implementation will print the standard string value of the value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString, + * not null + * + * @since 2.2 + */ + protected void appendCyclicObject(StringBuffer buffer, String fieldName, Object value) { + ObjectUtils.identityToString(buffer, value); + } + + /** + *

Append to the toString an Object + * value, printing the full detail of the Object.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString, + * not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, Object value) { + buffer.append(value); + } + + /** + *

Append to the toString a Collection.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param coll the Collection to add to the + * toString, not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, Collection coll) { + buffer.append(coll); + } + + /** + *

Append to the toString a Map.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param map the Map to add to the toString, + * not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, Map map) { + buffer.append(map); + } + + /** + *

Append to the toString an Object + * value, printing a summary of the Object.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString, + * not null + */ + protected void appendSummary(StringBuffer buffer, String fieldName, Object value) { + buffer.append(summaryObjectStartText); + buffer.append(getShortClassName(value.getClass())); + buffer.append(summaryObjectEndText); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a long + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param value the value to add to the toString + */ + public void append(StringBuffer buffer, String fieldName, long value) { + appendFieldStart(buffer, fieldName); + appendDetail(buffer, fieldName, value); + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString a long + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString + */ + protected void appendDetail(StringBuffer buffer, String fieldName, long value) { + buffer.append(value); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString an int + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param value the value to add to the toString + */ + public void append(StringBuffer buffer, String fieldName, int value) { + appendFieldStart(buffer, fieldName); + appendDetail(buffer, fieldName, value); + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString an int + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString + */ + protected void appendDetail(StringBuffer buffer, String fieldName, int value) { + buffer.append(value); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a short + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param value the value to add to the toString + */ + public void append(StringBuffer buffer, String fieldName, short value) { + appendFieldStart(buffer, fieldName); + appendDetail(buffer, fieldName, value); + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString a short + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString + */ + protected void appendDetail(StringBuffer buffer, String fieldName, short value) { + buffer.append(value); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a byte + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param value the value to add to the toString + */ + public void append(StringBuffer buffer, String fieldName, byte value) { + appendFieldStart(buffer, fieldName); + appendDetail(buffer, fieldName, value); + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString a byte + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString + */ + protected void appendDetail(StringBuffer buffer, String fieldName, byte value) { + buffer.append(value); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a char + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param value the value to add to the toString + */ + public void append(StringBuffer buffer, String fieldName, char value) { + appendFieldStart(buffer, fieldName); + appendDetail(buffer, fieldName, value); + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString a char + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString + */ + protected void appendDetail(StringBuffer buffer, String fieldName, char value) { + buffer.append(value); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a double + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param value the value to add to the toString + */ + public void append(StringBuffer buffer, String fieldName, double value) { + appendFieldStart(buffer, fieldName); + appendDetail(buffer, fieldName, value); + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString a double + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString + */ + protected void appendDetail(StringBuffer buffer, String fieldName, double value) { + buffer.append(value); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a float + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param value the value to add to the toString + */ + public void append(StringBuffer buffer, String fieldName, float value) { + appendFieldStart(buffer, fieldName); + appendDetail(buffer, fieldName, value); + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString a float + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString + */ + protected void appendDetail(StringBuffer buffer, String fieldName, float value) { + buffer.append(value); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a boolean + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param value the value to add to the toString + */ + public void append(StringBuffer buffer, String fieldName, boolean value) { + appendFieldStart(buffer, fieldName); + appendDetail(buffer, fieldName, value); + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString a boolean + * value.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param value the value to add to the toString + */ + protected void appendDetail(StringBuffer buffer, String fieldName, boolean value) { + buffer.append(value); + } + + /** + *

Append to the toString an Object + * array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info, null for style decides + */ + public void append(StringBuffer buffer, String fieldName, Object[] array, Boolean fullDetail) { + appendFieldStart(buffer, fieldName); + + if (array == null) { + appendNullText(buffer, fieldName); + + } else if (isFullDetail(fullDetail)) { + appendDetail(buffer, fieldName, array); + + } else { + appendSummary(buffer, fieldName, array); + } + + appendFieldEnd(buffer, fieldName); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString the detail of an + * Object array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, Object[] array) { + buffer.append(arrayStart); + for (int i = 0; i < array.length; i++) { + Object item = array[i]; + if (i > 0) { + buffer.append(arraySeparator); + } + if (item == null) { + appendNullText(buffer, fieldName); + + } else { + appendInternal(buffer, fieldName, item, arrayContentDetail); + } + } + buffer.append(arrayEnd); + } + + /** + *

Append to the toString the detail of an array type.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + * @since 2.0 + */ + protected void reflectionAppendArrayDetail(StringBuffer buffer, String fieldName, Object array) { + buffer.append(arrayStart); + int length = Array.getLength(array); + for (int i = 0; i < length; i++) { + Object item = Array.get(array, i); + if (i > 0) { + buffer.append(arraySeparator); + } + if (item == null) { + appendNullText(buffer, fieldName); + + } else { + appendInternal(buffer, fieldName, item, arrayContentDetail); + } + } + buffer.append(arrayEnd); + } + + /** + *

Append to the toString a summary of an + * Object array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendSummary(StringBuffer buffer, String fieldName, Object[] array) { + appendSummarySize(buffer, fieldName, array.length); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a long + * array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info, null for style decides + */ + public void append(StringBuffer buffer, String fieldName, long[] array, Boolean fullDetail) { + appendFieldStart(buffer, fieldName); + + if (array == null) { + appendNullText(buffer, fieldName); + + } else if (isFullDetail(fullDetail)) { + appendDetail(buffer, fieldName, array); + + } else { + appendSummary(buffer, fieldName, array); + } + + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString the detail of a + * long array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, long[] array) { + buffer.append(arrayStart); + for (int i = 0; i < array.length; i++) { + if (i > 0) { + buffer.append(arraySeparator); + } + appendDetail(buffer, fieldName, array[i]); + } + buffer.append(arrayEnd); + } + + /** + *

Append to the toString a summary of a + * long array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendSummary(StringBuffer buffer, String fieldName, long[] array) { + appendSummarySize(buffer, fieldName, array.length); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString an int + * array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info, null for style decides + */ + public void append(StringBuffer buffer, String fieldName, int[] array, Boolean fullDetail) { + appendFieldStart(buffer, fieldName); + + if (array == null) { + appendNullText(buffer, fieldName); + + } else if (isFullDetail(fullDetail)) { + appendDetail(buffer, fieldName, array); + + } else { + appendSummary(buffer, fieldName, array); + } + + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString the detail of an + * int array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, int[] array) { + buffer.append(arrayStart); + for (int i = 0; i < array.length; i++) { + if (i > 0) { + buffer.append(arraySeparator); + } + appendDetail(buffer, fieldName, array[i]); + } + buffer.append(arrayEnd); + } + + /** + *

Append to the toString a summary of an + * int array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendSummary(StringBuffer buffer, String fieldName, int[] array) { + appendSummarySize(buffer, fieldName, array.length); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a short + * array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info, null for style decides + */ + public void append(StringBuffer buffer, String fieldName, short[] array, Boolean fullDetail) { + appendFieldStart(buffer, fieldName); + + if (array == null) { + appendNullText(buffer, fieldName); + + } else if (isFullDetail(fullDetail)) { + appendDetail(buffer, fieldName, array); + + } else { + appendSummary(buffer, fieldName, array); + } + + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString the detail of a + * short array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, short[] array) { + buffer.append(arrayStart); + for (int i = 0; i < array.length; i++) { + if (i > 0) { + buffer.append(arraySeparator); + } + appendDetail(buffer, fieldName, array[i]); + } + buffer.append(arrayEnd); + } + + /** + *

Append to the toString a summary of a + * short array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendSummary(StringBuffer buffer, String fieldName, short[] array) { + appendSummarySize(buffer, fieldName, array.length); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a byte + * array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info, null for style decides + */ + public void append(StringBuffer buffer, String fieldName, byte[] array, Boolean fullDetail) { + appendFieldStart(buffer, fieldName); + + if (array == null) { + appendNullText(buffer, fieldName); + + } else if (isFullDetail(fullDetail)) { + appendDetail(buffer, fieldName, array); + + } else { + appendSummary(buffer, fieldName, array); + } + + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString the detail of a + * byte array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, byte[] array) { + buffer.append(arrayStart); + for (int i = 0; i < array.length; i++) { + if (i > 0) { + buffer.append(arraySeparator); + } + appendDetail(buffer, fieldName, array[i]); + } + buffer.append(arrayEnd); + } + + /** + *

Append to the toString a summary of a + * byte array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendSummary(StringBuffer buffer, String fieldName, byte[] array) { + appendSummarySize(buffer, fieldName, array.length); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a char + * array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info, null for style decides + */ + public void append(StringBuffer buffer, String fieldName, char[] array, Boolean fullDetail) { + appendFieldStart(buffer, fieldName); + + if (array == null) { + appendNullText(buffer, fieldName); + + } else if (isFullDetail(fullDetail)) { + appendDetail(buffer, fieldName, array); + + } else { + appendSummary(buffer, fieldName, array); + } + + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString the detail of a + * char array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, char[] array) { + buffer.append(arrayStart); + for (int i = 0; i < array.length; i++) { + if (i > 0) { + buffer.append(arraySeparator); + } + appendDetail(buffer, fieldName, array[i]); + } + buffer.append(arrayEnd); + } + + /** + *

Append to the toString a summary of a + * char array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendSummary(StringBuffer buffer, String fieldName, char[] array) { + appendSummarySize(buffer, fieldName, array.length); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a double + * array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info, null for style decides + */ + public void append(StringBuffer buffer, String fieldName, double[] array, Boolean fullDetail) { + appendFieldStart(buffer, fieldName); + + if (array == null) { + appendNullText(buffer, fieldName); + + } else if (isFullDetail(fullDetail)) { + appendDetail(buffer, fieldName, array); + + } else { + appendSummary(buffer, fieldName, array); + } + + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString the detail of a + * double array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, double[] array) { + buffer.append(arrayStart); + for (int i = 0; i < array.length; i++) { + if (i > 0) { + buffer.append(arraySeparator); + } + appendDetail(buffer, fieldName, array[i]); + } + buffer.append(arrayEnd); + } + + /** + *

Append to the toString a summary of a + * double array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendSummary(StringBuffer buffer, String fieldName, double[] array) { + appendSummarySize(buffer, fieldName, array.length); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a float + * array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info, null for style decides + */ + public void append(StringBuffer buffer, String fieldName, float[] array, Boolean fullDetail) { + appendFieldStart(buffer, fieldName); + + if (array == null) { + appendNullText(buffer, fieldName); + + } else if (isFullDetail(fullDetail)) { + appendDetail(buffer, fieldName, array); + + } else { + appendSummary(buffer, fieldName, array); + } + + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString the detail of a + * float array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, float[] array) { + buffer.append(arrayStart); + for (int i = 0; i < array.length; i++) { + if (i > 0) { + buffer.append(arraySeparator); + } + appendDetail(buffer, fieldName, array[i]); + } + buffer.append(arrayEnd); + } + + /** + *

Append to the toString a summary of a + * float array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendSummary(StringBuffer buffer, String fieldName, float[] array) { + appendSummarySize(buffer, fieldName, array.length); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString a boolean + * array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + * @param array the array to add to the toString + * @param fullDetail true for detail, false + * for summary info, null for style decides + */ + public void append(StringBuffer buffer, String fieldName, boolean[] array, Boolean fullDetail) { + appendFieldStart(buffer, fieldName); + + if (array == null) { + appendNullText(buffer, fieldName); + + } else if (isFullDetail(fullDetail)) { + appendDetail(buffer, fieldName, array); + + } else { + appendSummary(buffer, fieldName, array); + } + + appendFieldEnd(buffer, fieldName); + } + + /** + *

Append to the toString the detail of a + * boolean array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendDetail(StringBuffer buffer, String fieldName, boolean[] array) { + buffer.append(arrayStart); + for (int i = 0; i < array.length; i++) { + if (i > 0) { + buffer.append(arraySeparator); + } + appendDetail(buffer, fieldName, array[i]); + } + buffer.append(arrayEnd); + } + + /** + *

Append to the toString a summary of a + * boolean array.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param array the array to add to the toString, + * not null + */ + protected void appendSummary(StringBuffer buffer, String fieldName, boolean[] array) { + appendSummarySize(buffer, fieldName, array.length); + } + + //---------------------------------------------------------------------------- + + /** + *

Append to the toString the class name.

+ * + * @param buffer the StringBuffer to populate + * @param object the Object whose name to output + */ + protected void appendClassName(StringBuffer buffer, Object object) { + if (useClassName && object != null) { + register(object); + if (useShortClassName) { + buffer.append(getShortClassName(object.getClass())); + } else { + buffer.append(object.getClass().getName()); + } + } + } + + /** + *

Append the {@link System#identityHashCode(java.lang.Object)}.

+ * + * @param buffer the StringBuffer to populate + * @param object the Object whose id to output + */ + protected void appendIdentityHashCode(StringBuffer buffer, Object object) { + if (this.isUseIdentityHashCode() && object!=null) { + register(object); + buffer.append('@'); + buffer.append(Integer.toHexString(System.identityHashCode(object))); + } + } + + /** + *

Append to the toString the content start.

+ * + * @param buffer the StringBuffer to populate + */ + protected void appendContentStart(StringBuffer buffer) { + buffer.append(contentStart); + } + + /** + *

Append to the toString the content end.

+ * + * @param buffer the StringBuffer to populate + */ + protected void appendContentEnd(StringBuffer buffer) { + buffer.append(contentEnd); + } + + /** + *

Append to the toString an indicator for null.

+ * + *

The default indicator is '<null>'.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + */ + protected void appendNullText(StringBuffer buffer, String fieldName) { + buffer.append(nullText); + } + + /** + *

Append to the toString the field separator.

+ * + * @param buffer the StringBuffer to populate + */ + protected void appendFieldSeparator(StringBuffer buffer) { + buffer.append(fieldSeparator); + } + + /** + *

Append to the toString the field start.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name + */ + protected void appendFieldStart(StringBuffer buffer, String fieldName) { + if (useFieldNames && fieldName != null) { + buffer.append(fieldName); + buffer.append(fieldNameValueSeparator); + } + } + + /** + *

Append to the toString the field end.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + */ + protected void appendFieldEnd(StringBuffer buffer, String fieldName) { + appendFieldSeparator(buffer); + } + + /** + *

Append to the toString a size summary.

+ * + *

The size summary is used to summarize the contents of + * Collections, Maps and arrays.

+ * + *

The output consists of a prefix, the passed in size + * and a suffix.

+ * + *

The default format is '<size=n>'.

+ * + * @param buffer the StringBuffer to populate + * @param fieldName the field name, typically not used as already appended + * @param size the size to append + */ + protected void appendSummarySize(StringBuffer buffer, String fieldName, int size) { + buffer.append(sizeStartText); + buffer.append(size); + buffer.append(sizeEndText); + } + + /** + *

Is this field to be output in full detail.

+ * + *

This method converts a detail request into a detail level. + * The calling code may request full detail (true), + * but a subclass might ignore that and always return + * false. The calling code may pass in + * null indicating that it doesn't care about + * the detail level. In this case the default detail level is + * used.

+ * + * @param fullDetailRequest the detail level requested + * @return whether full detail is to be shown + */ + protected boolean isFullDetail(Boolean fullDetailRequest) { + if (fullDetailRequest == null) { + return defaultFullDetail; + } + return fullDetailRequest.booleanValue(); + } + + /** + *

Gets the short class name for a class.

+ * + *

The short class name is the classname excluding + * the package name.

+ * + * @param cls the Class to get the short name of + * @return the short name + */ + protected String getShortClassName(Class cls) { + return ClassUtils.getShortClassName(cls); + } + + // Setters and getters for the customizable parts of the style + // These methods are not expected to be overridden, except to make public + // (They are not public so that immutable subclasses can be written) + //--------------------------------------------------------------------- + + /** + *

Gets whether to use the class name.

+ * + * @return the current useClassName flag + */ + protected boolean isUseClassName() { + return useClassName; + } + + /** + *

Sets whether to use the class name.

+ * + * @param useClassName the new useClassName flag + */ + protected void setUseClassName(boolean useClassName) { + this.useClassName = useClassName; + } + + //--------------------------------------------------------------------- + + /** + *

Gets whether to output short or long class names.

+ * + * @return the current useShortClassName flag + * @since 2.0 + */ + protected boolean isUseShortClassName() { + return useShortClassName; + } + + /** + *

Sets whether to output short or long class names.

+ * + * @param useShortClassName the new useShortClassName flag + * @since 2.0 + */ + protected void setUseShortClassName(boolean useShortClassName) { + this.useShortClassName = useShortClassName; + } + + //--------------------------------------------------------------------- + + /** + *

Gets whether to use the identity hash code.

+ * + * @return the current useIdentityHashCode flag + */ + protected boolean isUseIdentityHashCode() { + return useIdentityHashCode; + } + + /** + *

Sets whether to use the identity hash code.

+ * + * @param useIdentityHashCode the new useIdentityHashCode flag + */ + protected void setUseIdentityHashCode(boolean useIdentityHashCode) { + this.useIdentityHashCode = useIdentityHashCode; + } + + //--------------------------------------------------------------------- + + /** + *

Gets whether to use the field names passed in.

+ * + * @return the current useFieldNames flag + */ + protected boolean isUseFieldNames() { + return useFieldNames; + } + + /** + *

Sets whether to use the field names passed in.

+ * + * @param useFieldNames the new useFieldNames flag + */ + protected void setUseFieldNames(boolean useFieldNames) { + this.useFieldNames = useFieldNames; + } + + //--------------------------------------------------------------------- + + /** + *

Gets whether to use full detail when the caller doesn't + * specify.

+ * + * @return the current defaultFullDetail flag + */ + protected boolean isDefaultFullDetail() { + return defaultFullDetail; + } + + /** + *

Sets whether to use full detail when the caller doesn't + * specify.

+ * + * @param defaultFullDetail the new defaultFullDetail flag + */ + protected void setDefaultFullDetail(boolean defaultFullDetail) { + this.defaultFullDetail = defaultFullDetail; + } + + //--------------------------------------------------------------------- + + /** + *

Gets whether to output array content detail.

+ * + * @return the current array content detail setting + */ + protected boolean isArrayContentDetail() { + return arrayContentDetail; + } + + /** + *

Sets whether to output array content detail.

+ * + * @param arrayContentDetail the new arrayContentDetail flag + */ + protected void setArrayContentDetail(boolean arrayContentDetail) { + this.arrayContentDetail = arrayContentDetail; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the array start text.

+ * + * @return the current array start text + */ + protected String getArrayStart() { + return arrayStart; + } + + /** + *

Sets the array start text.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param arrayStart the new array start text + */ + protected void setArrayStart(String arrayStart) { + if (arrayStart == null) { + arrayStart = ""; + } + this.arrayStart = arrayStart; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the array end text.

+ * + * @return the current array end text + */ + protected String getArrayEnd() { + return arrayEnd; + } + + /** + *

Sets the array end text.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param arrayEnd the new array end text + */ + protected void setArrayEnd(String arrayEnd) { + if (arrayEnd == null) { + arrayEnd = ""; + } + this.arrayEnd = arrayEnd; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the array separator text.

+ * + * @return the current array separator text + */ + protected String getArraySeparator() { + return arraySeparator; + } + + /** + *

Sets the array separator text.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param arraySeparator the new array separator text + */ + protected void setArraySeparator(String arraySeparator) { + if (arraySeparator == null) { + arraySeparator = ""; + } + this.arraySeparator = arraySeparator; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the content start text.

+ * + * @return the current content start text + */ + protected String getContentStart() { + return contentStart; + } + + /** + *

Sets the content start text.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param contentStart the new content start text + */ + protected void setContentStart(String contentStart) { + if (contentStart == null) { + contentStart = ""; + } + this.contentStart = contentStart; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the content end text.

+ * + * @return the current content end text + */ + protected String getContentEnd() { + return contentEnd; + } + + /** + *

Sets the content end text.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param contentEnd the new content end text + */ + protected void setContentEnd(String contentEnd) { + if (contentEnd == null) { + contentEnd = ""; + } + this.contentEnd = contentEnd; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the field name value separator text.

+ * + * @return the current field name value separator text + */ + protected String getFieldNameValueSeparator() { + return fieldNameValueSeparator; + } + + /** + *

Sets the field name value separator text.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param fieldNameValueSeparator the new field name value separator text + */ + protected void setFieldNameValueSeparator(String fieldNameValueSeparator) { + if (fieldNameValueSeparator == null) { + fieldNameValueSeparator = ""; + } + this.fieldNameValueSeparator = fieldNameValueSeparator; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the field separator text.

+ * + * @return the current field separator text + */ + protected String getFieldSeparator() { + return fieldSeparator; + } + + /** + *

Sets the field separator text.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param fieldSeparator the new field separator text + */ + protected void setFieldSeparator(String fieldSeparator) { + if (fieldSeparator == null) { + fieldSeparator = ""; + } + this.fieldSeparator = fieldSeparator; + } + + //--------------------------------------------------------------------- + + /** + *

Gets whether the field separator should be added at the start + * of each buffer.

+ * + * @return the fieldSeparatorAtStart flag + * @since 2.0 + */ + protected boolean isFieldSeparatorAtStart() { + return fieldSeparatorAtStart; + } + + /** + *

Sets whether the field separator should be added at the start + * of each buffer.

+ * + * @param fieldSeparatorAtStart the fieldSeparatorAtStart flag + * @since 2.0 + */ + protected void setFieldSeparatorAtStart(boolean fieldSeparatorAtStart) { + this.fieldSeparatorAtStart = fieldSeparatorAtStart; + } + + //--------------------------------------------------------------------- + + /** + *

Gets whether the field separator should be added at the end + * of each buffer.

+ * + * @return fieldSeparatorAtEnd flag + * @since 2.0 + */ + protected boolean isFieldSeparatorAtEnd() { + return fieldSeparatorAtEnd; + } + + /** + *

Sets whether the field separator should be added at the end + * of each buffer.

+ * + * @param fieldSeparatorAtEnd the fieldSeparatorAtEnd flag + * @since 2.0 + */ + protected void setFieldSeparatorAtEnd(boolean fieldSeparatorAtEnd) { + this.fieldSeparatorAtEnd = fieldSeparatorAtEnd; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the text to output when null found.

+ * + * @return the current text to output when null found + */ + protected String getNullText() { + return nullText; + } + + /** + *

Sets the text to output when null found.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param nullText the new text to output when null found + */ + protected void setNullText(String nullText) { + if (nullText == null) { + nullText = ""; + } + this.nullText = nullText; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the start text to output when a Collection, + * Map or array size is output.

+ * + *

This is output before the size value.

+ * + * @return the current start of size text + */ + protected String getSizeStartText() { + return sizeStartText; + } + + /** + *

Sets the start text to output when a Collection, + * Map or array size is output.

+ * + *

This is output before the size value.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param sizeStartText the new start of size text + */ + protected void setSizeStartText(String sizeStartText) { + if (sizeStartText == null) { + sizeStartText = ""; + } + this.sizeStartText = sizeStartText; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the end text to output when a Collection, + * Map or array size is output.

+ * + *

This is output after the size value.

+ * + * @return the current end of size text + */ + protected String getSizeEndText() { + return sizeEndText; + } + + /** + *

Sets the end text to output when a Collection, + * Map or array size is output.

+ * + *

This is output after the size value.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param sizeEndText the new end of size text + */ + protected void setSizeEndText(String sizeEndText) { + if (sizeEndText == null) { + sizeEndText = ""; + } + this.sizeEndText = sizeEndText; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the start text to output when an Object is + * output in summary mode.

+ * + *

This is output before the size value.

+ * + * @return the current start of summary text + */ + protected String getSummaryObjectStartText() { + return summaryObjectStartText; + } + + /** + *

Sets the start text to output when an Object is + * output in summary mode.

+ * + *

This is output before the size value.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param summaryObjectStartText the new start of summary text + */ + protected void setSummaryObjectStartText(String summaryObjectStartText) { + if (summaryObjectStartText == null) { + summaryObjectStartText = ""; + } + this.summaryObjectStartText = summaryObjectStartText; + } + + //--------------------------------------------------------------------- + + /** + *

Gets the end text to output when an Object is + * output in summary mode.

+ * + *

This is output after the size value.

+ * + * @return the current end of summary text + */ + protected String getSummaryObjectEndText() { + return summaryObjectEndText; + } + + /** + *

Sets the end text to output when an Object is + * output in summary mode.

+ * + *

This is output after the size value.

+ * + *

null is accepted, but will be converted to + * an empty String.

+ * + * @param summaryObjectEndText the new end of summary text + */ + protected void setSummaryObjectEndText(String summaryObjectEndText) { + if (summaryObjectEndText == null) { + summaryObjectEndText = ""; + } + this.summaryObjectEndText = summaryObjectEndText; + } + + //---------------------------------------------------------------------------- + + /** + *

Default ToStringStyle.

+ * + *

This is an inner class rather than using + * StandardToStringStyle to ensure its immutability.

+ */ + private static final class DefaultToStringStyle extends ToStringStyle { + + /** + * Required for serialization support. + * + * @see java.io.Serializable + */ + private static final long serialVersionUID = 1L; + + /** + *

Constructor.

+ * + *

Use the static constant rather than instantiating.

+ */ + DefaultToStringStyle() { + super(); + } + + /** + *

Ensure Singleton after serialization.

+ * + * @return the singleton + */ + private Object readResolve() { + return ToStringStyle.DEFAULT_STYLE; + } + + } + + //---------------------------------------------------------------------------- + + /** + *

ToStringStyle that does not print out + * the field names.

+ * + *

This is an inner class rather than using + * StandardToStringStyle to ensure its immutability. + */ + private static final class NoFieldNameToStringStyle extends ToStringStyle { + + private static final long serialVersionUID = 1L; + + /** + *

Constructor.

+ * + *

Use the static constant rather than instantiating.

+ */ + NoFieldNameToStringStyle() { + super(); + this.setUseFieldNames(false); + } + + /** + *

Ensure Singleton after serialization.

+ * + * @return the singleton + */ + private Object readResolve() { + return ToStringStyle.NO_FIELD_NAMES_STYLE; + } + + } + + //---------------------------------------------------------------------------- + + /** + *

ToStringStyle that prints out the short + * class name and no identity hashcode.

+ * + *

This is an inner class rather than using + * StandardToStringStyle to ensure its immutability.

+ */ + private static final class ShortPrefixToStringStyle extends ToStringStyle { + + private static final long serialVersionUID = 1L; + + /** + *

Constructor.

+ * + *

Use the static constant rather than instantiating.

+ */ + ShortPrefixToStringStyle() { + super(); + this.setUseShortClassName(true); + this.setUseIdentityHashCode(false); + } + + /** + *

Ensure Singleton after serialization.

+ * @return the singleton + */ + private Object readResolve() { + return ToStringStyle.SHORT_PREFIX_STYLE; + } + + } + + /** + *

ToStringStyle that does not print out the + * classname, identity hashcode, content start or field name.

+ * + *

This is an inner class rather than using + * StandardToStringStyle to ensure its immutability.

+ */ + private static final class SimpleToStringStyle extends ToStringStyle { + + private static final long serialVersionUID = 1L; + + /** + *

Constructor.

+ * + *

Use the static constant rather than instantiating.

+ */ + SimpleToStringStyle() { + super(); + this.setUseClassName(false); + this.setUseIdentityHashCode(false); + this.setUseFieldNames(false); + this.setContentStart(""); + this.setContentEnd(""); + } + + /** + *

Ensure Singleton after serialization.

+ * @return the singleton + */ + private Object readResolve() { + return ToStringStyle.SIMPLE_STYLE; + } + + } + + //---------------------------------------------------------------------------- + + /** + *

ToStringStyle that outputs on multiple lines.

+ * + *

This is an inner class rather than using + * StandardToStringStyle to ensure its immutability.

+ */ + private static final class MultiLineToStringStyle extends ToStringStyle { + + private static final long serialVersionUID = 1L; + + /** + *

Constructor.

+ * + *

Use the static constant rather than instantiating.

+ */ + MultiLineToStringStyle() { + super(); + this.setContentStart("["); + this.setFieldSeparator(SystemUtils.LINE_SEPARATOR + " "); + this.setFieldSeparatorAtStart(true); + this.setContentEnd(SystemUtils.LINE_SEPARATOR + "]"); + } + + /** + *

Ensure Singleton after serialization.

+ * + * @return the singleton + */ + private Object readResolve() { + return ToStringStyle.MULTI_LINE_STYLE; + } + + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/package.html b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/package.html new file mode 100644 index 00000000..dd40682d --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/builder/package.html @@ -0,0 +1,28 @@ + + + +Assists in creating consistent equals(Object), toString(), +hashCode(), and compareTo(Object) methods. +@see java.lang.Object#equals(Object) +@see java.lang.Object#toString() +@see java.lang.Object#hashCode() +@see java.lang.Comparable#compareTo(Object) +@since 1.0 +

These classes are not thread-safe.

+ + diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/exception/CloneFailedException.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/exception/CloneFailedException.java new file mode 100644 index 00000000..edd2d775 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/exception/CloneFailedException.java @@ -0,0 +1,62 @@ +/* + * 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 external.org.apache.commons.lang3.exception; + +/** + * Exception thrown when a clone cannot be created. In contrast to + * {@link CloneNotSupportedException} this is a {@link RuntimeException}. + * + * @since 3.0 + */ +public class CloneFailedException extends RuntimeException { + // ~ Static fields/initializers --------------------------------------------- + + private static final long serialVersionUID = 20091223L; + + // ~ Constructors ----------------------------------------------------------- + + /** + * Constructs a CloneFailedException. + * + * @param message description of the exception + * @since upcoming + */ + public CloneFailedException(final String message) { + super(message); + } + + /** + * Constructs a CloneFailedException. + * + * @param cause cause of the exception + * @since upcoming + */ + public CloneFailedException(final Throwable cause) { + super(cause); + } + + /** + * Constructs a CloneFailedException. + * + * @param message description of the exception + * @param cause cause of the exception + * @since upcoming + */ + public CloneFailedException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/exception/package.html b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/exception/package.html new file mode 100644 index 00000000..b9d94036 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/exception/package.html @@ -0,0 +1,27 @@ + + + +Provides functionality for Exceptions. +

Contains the concept of an exception with context i.e. such an exception +will contain a map with keys and values. This provides an easy way to pass valuable +state information at exception time in useful form to a calling process.

+

Lastly, {@link org.apache.commons.lang3.exception.ExceptionUtils} +also contains Throwable manipulation and examination routines.

+@since 1.0 + + diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/Mutable.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/Mutable.java new file mode 100644 index 00000000..aa733e50 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/Mutable.java @@ -0,0 +1,54 @@ +/* + * 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 external.org.apache.commons.lang3.mutable; + +/** + * Provides mutable access to a value. + *

+ * Mutable is used as a generic interface to the implementations in this package. + *

+ * A typical use case would be to enable a primitive or string to be passed to a method and allow that method to + * effectively change the value of the primitive/string. Another use case is to store a frequently changing primitive in + * a collection (for example a total in a map) without needing to create new Integer/Long wrapper objects. + * + * @since 2.1 + * @param the type to set and get + * @version $Id: Mutable.java 1153213 2011-08-02 17:35:39Z ggregory $ + */ +public interface Mutable { + + /** + * Gets the value of this mutable. + * + * @return the stored value + */ + T getValue(); + + /** + * Sets the value of this mutable. + * + * @param value + * the value to store + * @throws NullPointerException + * if the object is null and null is invalid + * @throws ClassCastException + * if the type is invalid + */ + void setValue(T value); + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/MutableInt.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/MutableInt.java new file mode 100644 index 00000000..d7e83d9b --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/MutableInt.java @@ -0,0 +1,273 @@ +/* + * 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 external.org.apache.commons.lang3.mutable; + +/** + * A mutable int wrapper. + *

+ * Note that as MutableInt does not extend Integer, it is not treated by String.format as an Integer parameter. + * + * @see Integer + * @since 2.1 + * @version $Id: MutableInt.java 1160571 2011-08-23 07:36:08Z bayard $ + */ +public class MutableInt extends Number implements Comparable, Mutable { + + /** + * Required for serialization support. + * + * @see java.io.Serializable + */ + private static final long serialVersionUID = 512176391864L; + + /** The mutable value. */ + private int value; + + /** + * Constructs a new MutableInt with the default value of zero. + */ + public MutableInt() { + super(); + } + + /** + * Constructs a new MutableInt with the specified value. + * + * @param value the initial value to store + */ + public MutableInt(int value) { + super(); + this.value = value; + } + + /** + * Constructs a new MutableInt with the specified value. + * + * @param value the initial value to store, not null + * @throws NullPointerException if the object is null + */ + public MutableInt(Number value) { + super(); + this.value = value.intValue(); + } + + /** + * Constructs a new MutableInt parsing the given string. + * + * @param value the string to parse, not null + * @throws NumberFormatException if the string cannot be parsed into an int + * @since 2.5 + */ + public MutableInt(String value) throws NumberFormatException { + super(); + this.value = Integer.parseInt(value); + } + + //----------------------------------------------------------------------- + /** + * Gets the value as a Integer instance. + * + * @return the value as a Integer, never null + */ + public Integer getValue() { + return Integer.valueOf(this.value); + } + + /** + * Sets the value. + * + * @param value the value to set + */ + public void setValue(int value) { + this.value = value; + } + + /** + * Sets the value from any Number instance. + * + * @param value the value to set, not null + * @throws NullPointerException if the object is null + */ + public void setValue(Number value) { + this.value = value.intValue(); + } + + //----------------------------------------------------------------------- + /** + * Increments the value. + * + * @since Commons Lang 2.2 + */ + public void increment() { + value++; + } + + /** + * Decrements the value. + * + * @since Commons Lang 2.2 + */ + public void decrement() { + value--; + } + + //----------------------------------------------------------------------- + /** + * Adds a value to the value of this instance. + * + * @param operand the value to add, not null + * @since Commons Lang 2.2 + */ + public void add(int operand) { + this.value += operand; + } + + /** + * Adds a value to the value of this instance. + * + * @param operand the value to add, not null + * @throws NullPointerException if the object is null + * @since Commons Lang 2.2 + */ + public void add(Number operand) { + this.value += operand.intValue(); + } + + /** + * Subtracts a value from the value of this instance. + * + * @param operand the value to subtract, not null + * @since Commons Lang 2.2 + */ + public void subtract(int operand) { + this.value -= operand; + } + + /** + * Subtracts a value from the value of this instance. + * + * @param operand the value to subtract, not null + * @throws NullPointerException if the object is null + * @since Commons Lang 2.2 + */ + public void subtract(Number operand) { + this.value -= operand.intValue(); + } + + //----------------------------------------------------------------------- + // shortValue and byteValue rely on Number implementation + /** + * Returns the value of this MutableInt as an int. + * + * @return the numeric value represented by this object after conversion to type int. + */ + @Override + public int intValue() { + return value; + } + + /** + * Returns the value of this MutableInt as a long. + * + * @return the numeric value represented by this object after conversion to type long. + */ + @Override + public long longValue() { + return value; + } + + /** + * Returns the value of this MutableInt as a float. + * + * @return the numeric value represented by this object after conversion to type float. + */ + @Override + public float floatValue() { + return value; + } + + /** + * Returns the value of this MutableInt as a double. + * + * @return the numeric value represented by this object after conversion to type double. + */ + @Override + public double doubleValue() { + return value; + } + + //----------------------------------------------------------------------- + /** + * Gets this mutable as an instance of Integer. + * + * @return a Integer instance containing the value from this mutable, never null + */ + public Integer toInteger() { + return Integer.valueOf(intValue()); + } + + //----------------------------------------------------------------------- + /** + * Compares this object to the specified object. The result is true if and only if the argument is + * not null and is a MutableInt object that contains the same int value + * as this object. + * + * @param obj the object to compare with, null returns false + * @return true if the objects are the same; false otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof MutableInt) { + return value == ((MutableInt) obj).intValue(); + } + return false; + } + + /** + * Returns a suitable hash code for this mutable. + * + * @return a suitable hash code + */ + @Override + public int hashCode() { + return value; + } + + //----------------------------------------------------------------------- + /** + * Compares this mutable to another in ascending order. + * + * @param other the other mutable to compare to, not null + * @return negative if this is less, zero if equal, positive if greater + */ + public int compareTo(MutableInt other) { + int anotherVal = other.value; + return value < anotherVal ? -1 : (value == anotherVal ? 0 : 1); + } + + //----------------------------------------------------------------------- + /** + * Returns the String value of this mutable. + * + * @return the mutable value as a string + */ + @Override + public String toString() { + return String.valueOf(value); + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/package.html b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/package.html new file mode 100644 index 00000000..2f7436af --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/mutable/package.html @@ -0,0 +1,29 @@ + + + + + + + +Provides typed mutable wrappers to primitive values and Object. +@since 2.1 +

These classes are not thread-safe.

+ + diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/overview.html b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/overview.html new file mode 100644 index 00000000..f8433aae --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/overview.html @@ -0,0 +1,23 @@ + + + +

+This document is the API specification for the Apache Commons Lang library. +

+ + diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/package.html b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/package.html new file mode 100644 index 00000000..1625c62d --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/package.html @@ -0,0 +1,25 @@ + + + +Provides highly reusable static utility methods, chiefly concerned +with adding value to the {@link java.lang} classes. +@since 1.0 +

Most of these classes are immutable and thus thread-safe. +However Charset is not currently guaranteed thread-safe under all circumstances.

+ + diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/MemberUtils.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/MemberUtils.java new file mode 100644 index 00000000..c268fd76 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/MemberUtils.java @@ -0,0 +1,185 @@ +/* + * 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 external.org.apache.commons.lang3.reflect; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Member; +import java.lang.reflect.Modifier; + +import external.org.apache.commons.lang3.ClassUtils; + +/** + * Contains common code for working with Methods/Constructors, extracted and + * refactored from MethodUtils when it was imported from Commons + * BeanUtils. + * + * @since 2.5 + * @version $Id: MemberUtils.java 1143537 2011-07-06 19:30:22Z joehni $ + */ +public abstract class MemberUtils { + // TODO extract an interface to implement compareParameterSets(...)? + + private static final int ACCESS_TEST = Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE; + + /** Array of primitive number types ordered by "promotability" */ + private static final Class[] ORDERED_PRIMITIVE_TYPES = { Byte.TYPE, Short.TYPE, + Character.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE }; + + /** + * XXX Default access superclass workaround + * + * When a public class has a default access superclass with public members, + * these members are accessible. Calling them from compiled code works fine. + * Unfortunately, on some JVMs, using reflection to invoke these members + * seems to (wrongly) prevent access even when the modifier is public. + * Calling setAccessible(true) solves the problem but will only work from + * sufficiently privileged code. Better workarounds would be gratefully + * accepted. + * @param o the AccessibleObject to set as accessible + */ + static void setAccessibleWorkaround(AccessibleObject o) { + if (o == null || o.isAccessible()) { + return; + } + Member m = (Member) o; + if (Modifier.isPublic(m.getModifiers()) + && isPackageAccess(m.getDeclaringClass().getModifiers())) { + try { + o.setAccessible(true); + } catch (SecurityException e) { // NOPMD + // ignore in favor of subsequent IllegalAccessException + } + } + } + + /** + * Returns whether a given set of modifiers implies package access. + * @param modifiers to test + * @return true unless package/protected/private modifier detected + */ + static boolean isPackageAccess(int modifiers) { + return (modifiers & ACCESS_TEST) == 0; + } + + /** + * Returns whether a Member is accessible. + * @param m Member to check + * @return true if m is accessible + */ + static boolean isAccessible(Member m) { + return m != null && Modifier.isPublic(m.getModifiers()) && !m.isSynthetic(); + } + + /** + * Compares the relative fitness of two sets of parameter types in terms of + * matching a third set of runtime parameter types, such that a list ordered + * by the results of the comparison would return the best match first + * (least). + * + * @param left the "left" parameter set + * @param right the "right" parameter set + * @param actual the runtime parameter types to match against + * left/right + * @return int consistent with compare semantics + */ + public static int compareParameterTypes(Class[] left, Class[] right, Class[] actual) { + float leftCost = getTotalTransformationCost(actual, left); + float rightCost = getTotalTransformationCost(actual, right); + return leftCost < rightCost ? -1 : rightCost < leftCost ? 1 : 0; + } + + /** + * Returns the sum of the object transformation cost for each class in the + * source argument list. + * @param srcArgs The source arguments + * @param destArgs The destination arguments + * @return The total transformation cost + */ + private static float getTotalTransformationCost(Class[] srcArgs, Class[] destArgs) { + float totalCost = 0.0f; + for (int i = 0; i < srcArgs.length; i++) { + Class srcClass, destClass; + srcClass = srcArgs[i]; + destClass = destArgs[i]; + totalCost += getObjectTransformationCost(srcClass, destClass); + } + return totalCost; + } + + /** + * Gets the number of steps required needed to turn the source class into + * the destination class. This represents the number of steps in the object + * hierarchy graph. + * @param srcClass The source class + * @param destClass The destination class + * @return The cost of transforming an object + */ + private static float getObjectTransformationCost(Class srcClass, Class destClass) { + if (destClass.isPrimitive()) { + return getPrimitivePromotionCost(srcClass, destClass); + } + float cost = 0.0f; + while (srcClass != null && !destClass.equals(srcClass)) { + if (destClass.isInterface() && ClassUtils.isAssignable(srcClass, destClass)) { + // slight penalty for interface match. + // we still want an exact match to override an interface match, + // but + // an interface match should override anything where we have to + // get a superclass. + cost += 0.25f; + break; + } + cost++; + srcClass = srcClass.getSuperclass(); + } + /* + * If the destination class is null, we've travelled all the way up to + * an Object match. We'll penalize this by adding 1.5 to the cost. + */ + if (srcClass == null) { + cost += 1.5f; + } + return cost; + } + + /** + * Gets the number of steps required to promote a primitive number to another + * type. + * @param srcClass the (primitive) source class + * @param destClass the (primitive) destination class + * @return The cost of promoting the primitive + */ + private static float getPrimitivePromotionCost(final Class srcClass, final Class destClass) { + float cost = 0.0f; + Class cls = srcClass; + if (!cls.isPrimitive()) { + // slight unwrapping penalty + cost += 0.1f; + cls = ClassUtils.wrapperToPrimitive(cls); + } + for (int i = 0; cls != destClass && i < ORDERED_PRIMITIVE_TYPES.length; i++) { + if (cls == ORDERED_PRIMITIVE_TYPES[i]) { + cost += 0.1f; + if (i < ORDERED_PRIMITIVE_TYPES.length - 1) { + cls = ORDERED_PRIMITIVE_TYPES[i + 1]; + } + } + } + return cost; + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/MethodUtils.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/MethodUtils.java new file mode 100644 index 00000000..d72a1ebe --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/MethodUtils.java @@ -0,0 +1,537 @@ +/* + * 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 external.org.apache.commons.lang3.reflect; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import external.org.apache.commons.lang3.ArrayUtils; +import external.org.apache.commons.lang3.ClassUtils; + +/** + *

Utility reflection methods focused on methods, originally from Commons BeanUtils. + * Differences from the BeanUtils version may be noted, especially where similar functionality + * already existed within Lang. + *

+ * + *

Known Limitations

+ *

Accessing Public Methods In A Default Access Superclass

+ *

There is an issue when invoking public methods contained in a default access superclass on JREs prior to 1.4. + * Reflection locates these methods fine and correctly assigns them as public. + * However, an IllegalAccessException is thrown if the method is invoked.

+ * + *

MethodUtils contains a workaround for this situation. + * It will attempt to call setAccessible on this method. + * If this call succeeds, then the method can be invoked as normal. + * This call will only succeed when the application has sufficient security privileges. + * If this call fails then the method may fail.

+ * + * @since 2.5 + * @version $Id: MethodUtils.java 1166253 2011-09-07 16:27:42Z ggregory $ + */ +public class MethodUtils { + + /** + *

MethodUtils instances should NOT be constructed in standard programming. + * Instead, the class should be used as + * MethodUtils.getAccessibleMethod(method).

+ * + *

This constructor is public to permit tools that require a JavaBean + * instance to operate.

+ */ + public MethodUtils() { + super(); + } + + /** + *

Invokes a named method whose parameter type matches the object type.

+ * + *

This method delegates the method search to {@link #getMatchingAccessibleMethod(Class, String, Class[])}.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean object + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeMethod(Object object,String methodName, Object[] args, Class[] parameterTypes)}. + *

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the method invoked + * @throws IllegalAccessException if the requested method is not accessible via reflection + */ + public static Object invokeMethod(Object object, String methodName, + Object... args) throws NoSuchMethodException, + IllegalAccessException, InvocationTargetException { + if (args == null) { + args = ArrayUtils.EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeMethod(object, methodName, args, parameterTypes); + } + + /** + *

Invokes a named method whose parameter type matches the object type.

+ * + *

This method delegates the method search to {@link #getMatchingAccessibleMethod(Class, String, Class[])}.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean object + * would match a boolean primitive.

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the method invoked + * @throws IllegalAccessException if the requested method is not accessible via reflection + */ + public static Object invokeMethod(Object object, String methodName, + Object[] args, Class[] parameterTypes) + throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException { + if (parameterTypes == null) { + parameterTypes = ArrayUtils.EMPTY_CLASS_ARRAY; + } + if (args == null) { + args = ArrayUtils.EMPTY_OBJECT_ARRAY; + } + Method method = getMatchingAccessibleMethod(object.getClass(), + methodName, parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on object: " + + object.getClass().getName()); + } + return method.invoke(object, args); + } + + /** + *

Invokes a method whose parameter types match exactly the object + * types.

+ * + *

This uses reflection to invoke the method obtained from a call to + * getAccessibleMethod().

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactMethod(Object object, String methodName, + Object... args) throws NoSuchMethodException, + IllegalAccessException, InvocationTargetException { + if (args == null) { + args = ArrayUtils.EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeExactMethod(object, methodName, args, parameterTypes); + } + + /** + *

Invokes a method whose parameter types match exactly the parameter + * types given.

+ * + *

This uses reflection to invoke the method obtained from a call to + * getAccessibleMethod().

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactMethod(Object object, String methodName, + Object[] args, Class[] parameterTypes) + throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException { + if (args == null) { + args = ArrayUtils.EMPTY_OBJECT_ARRAY; + } + if (parameterTypes == null) { + parameterTypes = ArrayUtils.EMPTY_CLASS_ARRAY; + } + Method method = getAccessibleMethod(object.getClass(), methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on object: " + + object.getClass().getName()); + } + return method.invoke(object, args); + } + + /** + *

Invokes a static method whose parameter types match exactly the parameter + * types given.

+ * + *

This uses reflection to invoke the method obtained from a call to + * {@link #getAccessibleMethod(Class, String, Class[])}.

+ * + * @param cls invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactStaticMethod(Class cls, String methodName, + Object[] args, Class[] parameterTypes) + throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException { + if (args == null) { + args = ArrayUtils.EMPTY_OBJECT_ARRAY; + } + if (parameterTypes == null) { + parameterTypes = ArrayUtils.EMPTY_CLASS_ARRAY; + } + Method method = getAccessibleMethod(cls, methodName, parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on class: " + cls.getName()); + } + return method.invoke(null, args); + } + + /** + *

Invokes a named static method whose parameter type matches the object type.

+ * + *

This method delegates the method search to {@link #getMatchingAccessibleMethod(Class, String, Class[])}.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeStaticMethod(Class objectClass,String methodName,Object [] args,Class[] parameterTypes)}. + *

+ * + * @param cls invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeStaticMethod(Class cls, String methodName, + Object... args) throws NoSuchMethodException, + IllegalAccessException, InvocationTargetException { + if (args == null) { + args = ArrayUtils.EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeStaticMethod(cls, methodName, args, parameterTypes); + } + + /** + *

Invokes a named static method whose parameter type matches the object type.

+ * + *

This method delegates the method search to {@link #getMatchingAccessibleMethod(Class, String, Class[])}.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + * + * @param cls invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeStaticMethod(Class cls, String methodName, + Object[] args, Class[] parameterTypes) + throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException { + if (parameterTypes == null) { + parameterTypes = ArrayUtils.EMPTY_CLASS_ARRAY; + } + if (args == null) { + args = ArrayUtils.EMPTY_OBJECT_ARRAY; + } + Method method = getMatchingAccessibleMethod(cls, methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on class: " + cls.getName()); + } + return method.invoke(null, args); + } + + /** + *

Invokes a static method whose parameter types match exactly the object + * types.

+ * + *

This uses reflection to invoke the method obtained from a call to + * {@link #getAccessibleMethod(Class, String, Class[])}.

+ * + * @param cls invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactStaticMethod(Class cls, String methodName, + Object... args) throws NoSuchMethodException, + IllegalAccessException, InvocationTargetException { + if (args == null) { + args = ArrayUtils.EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeExactStaticMethod(cls, methodName, args, parameterTypes); + } + + /** + *

Returns an accessible method (that is, one that can be invoked via + * reflection) with given name and parameters. If no such method + * can be found, return null. + * This is just a convenient wrapper for + * {@link #getAccessibleMethod(Method method)}.

+ * + * @param cls get method from this class + * @param methodName get method with this name + * @param parameterTypes with these parameters types + * @return The accessible method + */ + public static Method getAccessibleMethod(Class cls, String methodName, + Class... parameterTypes) { + try { + return getAccessibleMethod(cls.getMethod(methodName, + parameterTypes)); + } catch (NoSuchMethodException e) { + return null; + } + } + + /** + *

Returns an accessible method (that is, one that can be invoked via + * reflection) that implements the specified Method. If no such method + * can be found, return null.

+ * + * @param method The method that we wish to call + * @return The accessible method + */ + public static Method getAccessibleMethod(Method method) { + if (!MemberUtils.isAccessible(method)) { + return null; + } + // If the declaring class is public, we are done + Class cls = method.getDeclaringClass(); + if (Modifier.isPublic(cls.getModifiers())) { + return method; + } + String methodName = method.getName(); + Class[] parameterTypes = method.getParameterTypes(); + + // Check the implemented interfaces and subinterfaces + method = getAccessibleMethodFromInterfaceNest(cls, methodName, + parameterTypes); + + // Check the superclass chain + if (method == null) { + method = getAccessibleMethodFromSuperclass(cls, methodName, + parameterTypes); + } + return method; + } + + /** + *

Returns an accessible method (that is, one that can be invoked via + * reflection) by scanning through the superclasses. If no such method + * can be found, return null.

+ * + * @param cls Class to be checked + * @param methodName Method name of the method we wish to call + * @param parameterTypes The parameter type signatures + * @return the accessible method or null if not found + */ + private static Method getAccessibleMethodFromSuperclass(Class cls, + String methodName, Class... parameterTypes) { + Class parentClass = cls.getSuperclass(); + while (parentClass != null) { + if (Modifier.isPublic(parentClass.getModifiers())) { + try { + return parentClass.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException e) { + return null; + } + } + parentClass = parentClass.getSuperclass(); + } + return null; + } + + /** + *

Returns an accessible method (that is, one that can be invoked via + * reflection) that implements the specified method, by scanning through + * all implemented interfaces and subinterfaces. If no such method + * can be found, return null.

+ * + *

There isn't any good reason why this method must be private. + * It is because there doesn't seem any reason why other classes should + * call this rather than the higher level methods.

+ * + * @param cls Parent class for the interfaces to be checked + * @param methodName Method name of the method we wish to call + * @param parameterTypes The parameter type signatures + * @return the accessible method or null if not found + */ + private static Method getAccessibleMethodFromInterfaceNest(Class cls, + String methodName, Class... parameterTypes) { + Method method = null; + + // Search up the superclass chain + for (; cls != null; cls = cls.getSuperclass()) { + + // Check the implemented interfaces of the parent class + Class[] interfaces = cls.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + // Is this interface public? + if (!Modifier.isPublic(interfaces[i].getModifiers())) { + continue; + } + // Does the method exist on this interface? + try { + method = interfaces[i].getDeclaredMethod(methodName, + parameterTypes); + } catch (NoSuchMethodException e) { // NOPMD + /* + * Swallow, if no method is found after the loop then this + * method returns null. + */ + } + if (method != null) { + break; + } + // Recursively check our parent interfaces + method = getAccessibleMethodFromInterfaceNest(interfaces[i], + methodName, parameterTypes); + if (method != null) { + break; + } + } + } + return method; + } + + /** + *

Finds an accessible method that matches the given name and has compatible parameters. + * Compatible parameters mean that every method parameter is assignable from + * the given parameters. + * In other words, it finds a method with the given name + * that will take the parameters given.

+ * + *

This method is used by + * {@link + * #invokeMethod(Object object, String methodName, Object[] args, Class[] parameterTypes)}. + * + *

This method can match primitive parameter by passing in wrapper classes. + * For example, a Boolean will match a primitive boolean + * parameter. + * + * @param cls find method in this class + * @param methodName find method with this name + * @param parameterTypes find method with most compatible parameters + * @return The accessible method + */ + public static Method getMatchingAccessibleMethod(Class cls, + String methodName, Class... parameterTypes) { + try { + Method method = cls.getMethod(methodName, parameterTypes); + MemberUtils.setAccessibleWorkaround(method); + return method; + } catch (NoSuchMethodException e) { // NOPMD - Swallow the exception + } + // search through all methods + Method bestMatch = null; + Method[] methods = cls.getMethods(); + for (Method method : methods) { + // compare name and parameters + if (method.getName().equals(methodName) && ClassUtils.isAssignable(parameterTypes, method.getParameterTypes(), true)) { + // get accessible version of method + Method accessibleMethod = getAccessibleMethod(method); + if (accessibleMethod != null && (bestMatch == null || MemberUtils.compareParameterTypes( + accessibleMethod.getParameterTypes(), + bestMatch.getParameterTypes(), + parameterTypes) < 0)) { + bestMatch = accessibleMethod; + } + } + } + if (bestMatch != null) { + MemberUtils.setAccessibleWorkaround(bestMatch); + } + return bestMatch; + } +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/package.html b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/package.html new file mode 100644 index 00000000..618b07a8 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/reflect/package.html @@ -0,0 +1,29 @@ + + + + + + + +Accumulates common high-level uses of the java.lang.reflect APIs. +@since 3.0 +

These classes are immutable, and therefore thread-safe.

+ + diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/ImmutablePair.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/ImmutablePair.java new file mode 100644 index 00000000..d3085be1 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/ImmutablePair.java @@ -0,0 +1,103 @@ +/* + * 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 external.org.apache.commons.lang3.tuple; + +/** + *

An immutable pair consisting of two {@code Object} elements.

+ * + *

Although the implementation is immutable, there is no restriction on the objects + * that may be stored. If mutable objects are stored in the pair, then the pair + * itself effectively becomes mutable. The class is also not {@code final}, so a subclass + * could add undesirable behaviour.

+ * + *

#ThreadSafe# if the objects are threadsafe

+ * + * @param the left element type + * @param the right element type + * + * @since Lang 3.0 + * @version $Id: ImmutablePair.java 1127544 2011-05-25 14:35:42Z scolebourne $ + */ +public final class ImmutablePair extends Pair { + + /** Serialization version */ + private static final long serialVersionUID = 4954918890077093841L; + + /** Left object */ + public final L left; + /** Right object */ + public final R right; + + /** + *

Obtains an immutable pair of from two objects inferring the generic types.

+ * + *

This factory allows the pair to be created using inference to + * obtain the generic types.

+ * + * @param the left element type + * @param the right element type + * @param left the left element, may be null + * @param right the right element, may be null + * @return a pair formed from the two parameters, not null + */ + public static ImmutablePair of(L left, R right) { + return new ImmutablePair(left, right); + } + + /** + * Create a new pair instance. + * + * @param left the left value, may be null + * @param right the right value, may be null + */ + public ImmutablePair(L left, R right) { + super(); + this.left = left; + this.right = right; + } + + //----------------------------------------------------------------------- + /** + * {@inheritDoc} + */ + @Override + public L getLeft() { + return left; + } + + /** + * {@inheritDoc} + */ + @Override + public R getRight() { + return right; + } + + /** + *

Throws {@code UnsupportedOperationException}.

+ * + *

This pair is immutable, so this operation is not supported.

+ * + * @param value the value to set + * @return never + * @throws UnsupportedOperationException as this operation is not supported + */ + public R setValue(R value) { + throw new UnsupportedOperationException(); + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/Pair.java b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/Pair.java new file mode 100644 index 00000000..e43bd8ab --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/Pair.java @@ -0,0 +1,177 @@ +/* + * 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 external.org.apache.commons.lang3.tuple; + +import java.io.Serializable; +import java.util.Map; + + +import external.org.apache.commons.lang3.ObjectUtils; +import external.org.apache.commons.lang3.builder.CompareToBuilder; + +/** + *

A pair consisting of two elements.

+ * + *

This class is an abstract implementation defining the basic API. + * It refers to the elements as 'left' and 'right'. It also implements the + * {@code Map.Entry} interface where the key is 'left' and the value is 'right'.

+ * + *

Subclass implementations may be mutable or immutable. + * However, there is no restriction on the type of the stored objects that may be stored. + * If mutable objects are stored in the pair, then the pair itself effectively becomes mutable.

+ * + * @param the left element type + * @param the right element type + * + * @since Lang 3.0 + * @version $Id: Pair.java 1142401 2011-07-03 08:30:12Z bayard $ + */ +public abstract class Pair implements Map.Entry, Comparable>, Serializable { + + /** Serialization version */ + private static final long serialVersionUID = 4954918890077093841L; + + /** + *

Obtains an immutable pair of from two objects inferring the generic types.

+ * + *

This factory allows the pair to be created using inference to + * obtain the generic types.

+ * + * @param the left element type + * @param the right element type + * @param left the left element, may be null + * @param right the right element, may be null + * @return a pair formed from the two parameters, not null + */ + public static Pair of(L left, R right) { + return new ImmutablePair(left, right); + } + + //----------------------------------------------------------------------- + /** + *

Gets the left element from this pair.

+ * + *

When treated as a key-value pair, this is the key.

+ * + * @return the left element, may be null + */ + public abstract L getLeft(); + + /** + *

Gets the right element from this pair.

+ * + *

When treated as a key-value pair, this is the value.

+ * + * @return the right element, may be null + */ + public abstract R getRight(); + + /** + *

Gets the key from this pair.

+ * + *

This method implements the {@code Map.Entry} interface returning the + * left element as the key.

+ * + * @return the left element as the key, may be null + */ + public final L getKey() { + return getLeft(); + } + + /** + *

Gets the value from this pair.

+ * + *

This method implements the {@code Map.Entry} interface returning the + * right element as the value.

+ * + * @return the right element as the value, may be null + */ + public R getValue() { + return getRight(); + } + + //----------------------------------------------------------------------- + /** + *

Compares the pair based on the left element followed by the right element. + * The types must be {@code Comparable}.

+ * + * @param other the other pair, not null + * @return negative if this is less, zero if equal, positive if greater + */ + public int compareTo(Pair other) { + return new CompareToBuilder().append(getLeft(), other.getLeft()) + .append(getRight(), other.getRight()).toComparison(); + } + + /** + *

Compares this pair to another based on the two elements.

+ * + * @param obj the object to compare to, null returns false + * @return true if the elements of the pair are equal + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Map.Entry) { + Map.Entry other = (Map.Entry) obj; + return ObjectUtils.equals(getKey(), other.getKey()) + && ObjectUtils.equals(getValue(), other.getValue()); + } + return false; + } + + /** + *

Returns a suitable hash code. + * The hash code follows the definition in {@code Map.Entry}.

+ * + * @return the hash code + */ + @Override + public int hashCode() { + // see Map.Entry API specification + return (getKey() == null ? 0 : getKey().hashCode()) ^ + (getValue() == null ? 0 : getValue().hashCode()); + } + + /** + *

Returns a String representation of this pair using the format {@code ($left,$right)}.

+ * + * @return a string describing this object, not null + */ + @Override + public String toString() { + return new StringBuilder().append('(').append(getLeft()).append(',').append(getRight()).append(')').toString(); + } + + /** + *

Formats the receiver using the given format.

+ * + *

This uses {@link java.util.Formattable} to perform the formatting. Two variables may + * be used to embed the left and right elements. Use {@code %1$s} for the left + * element (key) and {@code %2$s} for the right element (value). + * The default format used by {@code toString()} is {@code (%1$s,%2$s)}.

+ * + * @param format the format string, optionally containing {@code %1$s} and {@code %2$s}, not null + * @return the formatted string, not null + */ + public String toString(String format) { + return String.format(format, getLeft(), getRight()); + } + +} diff --git a/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/package.html b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/package.html new file mode 100644 index 00000000..db858539 --- /dev/null +++ b/Bridge/src/main/apacheCommonsLang/external/org/apache/commons/lang3/tuple/package.html @@ -0,0 +1,22 @@ + + + +Tuple classes, starting with a Pair class in version 3.0. +@since 3.0 + + diff --git a/Bridge/src/main/java/android/app/AndroidAppHelper.java b/Bridge/src/main/java/android/app/AndroidAppHelper.java new file mode 100644 index 00000000..c9160c5a --- /dev/null +++ b/Bridge/src/main/java/android/app/AndroidAppHelper.java @@ -0,0 +1,223 @@ +package android.app; + +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.res.CompatibilityInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.os.IBinder; +import android.view.Display; + +import java.lang.ref.WeakReference; +import java.util.Map; + +import de.robv.android.xposed.XSharedPreferences; +import de.robv.android.xposed.XposedBridge; + +import static de.robv.android.xposed.XposedHelpers.findClass; +import static de.robv.android.xposed.XposedHelpers.findFieldIfExists; +import static de.robv.android.xposed.XposedHelpers.findMethodExactIfExists; +import static de.robv.android.xposed.XposedHelpers.getObjectField; +import static de.robv.android.xposed.XposedHelpers.newInstance; +import static de.robv.android.xposed.XposedHelpers.setFloatField; + +/** + * Contains various methods for information about the current app. + * + *

For historical reasons, this class is in the {@code android.app} package. It can't be moved + * without breaking compatibility with existing modules. + */ +public final class AndroidAppHelper { + private AndroidAppHelper() {} + + private static final Class CLASS_RESOURCES_KEY; + private static final boolean HAS_IS_THEMEABLE; + private static final boolean HAS_THEME_CONFIG_PARAMETER; + + static { + CLASS_RESOURCES_KEY = (Build.VERSION.SDK_INT < 19) ? + findClass("android.app.ActivityThread$ResourcesKey", null) + : findClass("android.content.res.ResourcesKey", null); + + HAS_IS_THEMEABLE = findFieldIfExists(CLASS_RESOURCES_KEY, "mIsThemeable") != null; + HAS_THEME_CONFIG_PARAMETER = HAS_IS_THEMEABLE && Build.VERSION.SDK_INT >= 21 + && findMethodExactIfExists("android.app.ResourcesManager", null, "getThemeConfig") != null; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static Map getResourcesMap(ActivityThread activityThread) { + if (Build.VERSION.SDK_INT >= 24) { + Object resourcesManager = getObjectField(activityThread, "mResourcesManager"); + return (Map) getObjectField(resourcesManager, "mResourceImpls"); + } else if (Build.VERSION.SDK_INT >= 19) { + Object resourcesManager = getObjectField(activityThread, "mResourcesManager"); + return (Map) getObjectField(resourcesManager, "mActiveResources"); + } else { + return (Map) getObjectField(activityThread, "mActiveResources"); + } + } + + /* For SDK 15 & 16 */ + private static Object createResourcesKey(String resDir, float scale) { + try { + if (HAS_IS_THEMEABLE) + return newInstance(CLASS_RESOURCES_KEY, resDir, scale, false); + else + return newInstance(CLASS_RESOURCES_KEY, resDir, scale); + } catch (Throwable t) { + XposedBridge.log(t); + return null; + } + } + + /* For SDK 17 & 18 & 23 */ + private static Object createResourcesKey(String resDir, int displayId, Configuration overrideConfiguration, float scale) { + try { + if (HAS_THEME_CONFIG_PARAMETER) + return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale, false, null); + else if (HAS_IS_THEMEABLE) + return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale, false); + else + return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale); + } catch (Throwable t) { + XposedBridge.log(t); + return null; + } + } + + /* For SDK 19 - 22 */ + private static Object createResourcesKey(String resDir, int displayId, Configuration overrideConfiguration, float scale, IBinder token) { + try { + if (HAS_THEME_CONFIG_PARAMETER) + return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale, false, null, token); + else if (HAS_IS_THEMEABLE) + return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale, false, token); + else + return newInstance(CLASS_RESOURCES_KEY, resDir, displayId, overrideConfiguration, scale, token); + } catch (Throwable t) { + XposedBridge.log(t); + return null; + } + } + + /* For SDK 24+ */ + private static Object createResourcesKey(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo) { + try { + return newInstance(CLASS_RESOURCES_KEY, resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfiguration, compatInfo); + } catch (Throwable t) { + XposedBridge.log(t); + return null; + } + } + + /** @hide */ + public static void addActiveResource(String resDir, float scale, boolean isThemeable, Resources resources) { + addActiveResource(resDir, resources); + } + + /** @hide */ + public static void addActiveResource(String resDir, Resources resources) { + ActivityThread thread = ActivityThread.currentActivityThread(); + if (thread == null) { + return; + } + + Object resourcesKey; + if (Build.VERSION.SDK_INT >= 24) { + CompatibilityInfo compatInfo = (CompatibilityInfo) newInstance(CompatibilityInfo.class); + setFloatField(compatInfo, "applicationScale", resources.hashCode()); + resourcesKey = createResourcesKey(resDir, null, null, null, Display.DEFAULT_DISPLAY, null, compatInfo); + } else if (Build.VERSION.SDK_INT == 23) { + resourcesKey = createResourcesKey(resDir, Display.DEFAULT_DISPLAY, null, resources.hashCode()); + } else if (Build.VERSION.SDK_INT >= 19) { + resourcesKey = createResourcesKey(resDir, Display.DEFAULT_DISPLAY, null, resources.hashCode(), null); + } else if (Build.VERSION.SDK_INT >= 17) { + resourcesKey = createResourcesKey(resDir, Display.DEFAULT_DISPLAY, null, resources.hashCode()); + } else { + resourcesKey = createResourcesKey(resDir, resources.hashCode()); + } + + if (resourcesKey != null) { + if (Build.VERSION.SDK_INT >= 24) { + Object resImpl = getObjectField(resources, "mResourcesImpl"); + getResourcesMap(thread).put(resourcesKey, new WeakReference<>(resImpl)); + } else { + getResourcesMap(thread).put(resourcesKey, new WeakReference<>(resources)); + } + } + } + + /** + * Returns the name of the current process. It's usually the same as the main package name. + */ + public static String currentProcessName() { + String processName = ActivityThread.currentPackageName(); + if (processName == null) + return "android"; + return processName; + } + + /** + * Returns information about the main application in the current process. + * + *

In a few cases, multiple apps might run in the same process, e.g. the SystemUI and the + * Keyguard which both have {@code android:process="com.android.systemui"} set in their + * manifest. In those cases, the first application that was initialized will be returned. + */ + public static ApplicationInfo currentApplicationInfo() { + ActivityThread am = ActivityThread.currentActivityThread(); + if (am == null) + return null; + + Object boundApplication = getObjectField(am, "mBoundApplication"); + if (boundApplication == null) + return null; + + return (ApplicationInfo) getObjectField(boundApplication, "appInfo"); + } + + /** + * Returns the Android package name of the main application in the current process. + * + *

In a few cases, multiple apps might run in the same process, e.g. the SystemUI and the + * Keyguard which both have {@code android:process="com.android.systemui"} set in their + * manifest. In those cases, the first application that was initialized will be returned. + */ + public static String currentPackageName() { + ApplicationInfo ai = currentApplicationInfo(); + return (ai != null) ? ai.packageName : "android"; + } + + /** + * Returns the main {@link android.app.Application} object in the current process. + * + *

In a few cases, multiple apps might run in the same process, e.g. the SystemUI and the + * Keyguard which both have {@code android:process="com.android.systemui"} set in their + * manifest. In those cases, the first application that was initialized will be returned. + */ + public static Application currentApplication() { + return ActivityThread.currentApplication(); + } + + /** @deprecated Use {@link XSharedPreferences} instead. */ + @SuppressWarnings("UnusedParameters") + @Deprecated + public static SharedPreferences getSharedPreferencesForPackage(String packageName, String prefFileName, int mode) { + return new XSharedPreferences(packageName, prefFileName); + } + + /** @deprecated Use {@link XSharedPreferences} instead. */ + @Deprecated + public static SharedPreferences getDefaultSharedPreferencesForPackage(String packageName) { + return new XSharedPreferences(packageName); + } + + /** @deprecated Use {@link XSharedPreferences#reload} instead. */ + @Deprecated + public static void reloadSharedPreferencesIfNeeded(SharedPreferences pref) { + if (pref instanceof XSharedPreferences) { + ((XSharedPreferences) pref).reload(); + } + } +} diff --git a/Bridge/src/main/java/android/app/package-info.java b/Bridge/src/main/java/android/app/package-info.java new file mode 100644 index 00000000..98b6207b --- /dev/null +++ b/Bridge/src/main/java/android/app/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains {@link android.app.AndroidAppHelper} with various methods for information about the current app. + */ +package android.app; diff --git a/Bridge/src/main/java/android/content/res/XModuleResources.java b/Bridge/src/main/java/android/content/res/XModuleResources.java new file mode 100644 index 00000000..57464b35 --- /dev/null +++ b/Bridge/src/main/java/android/content/res/XModuleResources.java @@ -0,0 +1,54 @@ +package android.content.res; + +import android.app.AndroidAppHelper; +import android.util.DisplayMetrics; + +import de.robv.android.xposed.IXposedHookInitPackageResources; +import de.robv.android.xposed.IXposedHookZygoteInit; +import de.robv.android.xposed.IXposedHookZygoteInit.StartupParam; +import de.robv.android.xposed.callbacks.XC_InitPackageResources.InitPackageResourcesParam; + +/** + * Provides access to resources from a certain path (usually the module's own path). + */ +public class XModuleResources extends Resources { + private XModuleResources(AssetManager assets, DisplayMetrics metrics, Configuration config) { + super(assets, metrics, config); + } + + /** + * Creates a new instance. + * + *

This is usually called with {@link StartupParam#modulePath} from + * {@link IXposedHookZygoteInit#initZygote} and {@link InitPackageResourcesParam#res} from + * {@link IXposedHookInitPackageResources#handleInitPackageResources} (or {@code null} for + * system-wide replacements). + * + * @param path The path to the APK from which the resources should be loaded. + * @param origRes The resources object from which settings like the display metrics and the + * configuration should be copied. May be {@code null}. + */ + public static XModuleResources createInstance(String path, XResources origRes) { + if (path == null) + throw new IllegalArgumentException("path must not be null"); + + AssetManager assets = new AssetManager(); + assets.addAssetPath(path); + + XModuleResources res; + if (origRes != null) + res = new XModuleResources(assets, origRes.getDisplayMetrics(), origRes.getConfiguration()); + else + res = new XModuleResources(assets, null, null); + + AndroidAppHelper.addActiveResource(path, res); + return res; + } + + /** + * Creates an {@link XResForwarder} instance that forwards requests to {@code id} in this resource. + */ + public XResForwarder fwd(int id) { + return new XResForwarder(this, id); + } +} diff --git a/Bridge/src/main/java/android/content/res/XResForwarder.java b/Bridge/src/main/java/android/content/res/XResForwarder.java new file mode 100644 index 00000000..7d659052 --- /dev/null +++ b/Bridge/src/main/java/android/content/res/XResForwarder.java @@ -0,0 +1,34 @@ +package android.content.res; + +/** + * Instances of this class can be used for {@link XResources#setReplacement(String, String, String, Object)} + * and its variants. They forward the resource request to a different {@link android.content.res.Resources} + * instance with a possibly different ID. + * + *

Usually, instances aren't created directly but via {@link XModuleResources#fwd}. + */ +public class XResForwarder { + private final Resources res; + private final int id; + + /** + * Creates a new instance. + * + * @param res The target {@link android.content.res.Resources} instance to forward requests to. + * @param id The target resource ID. + */ + public XResForwarder(Resources res, int id) { + this.res = res; + this.id = id; + } + + /** Returns the target {@link android.content.res.Resources} instance. */ + public Resources getResources() { + return res; + } + + /** Returns the target resource ID. */ + public int getId() { + return id; + } +} diff --git a/Bridge/src/main/java/android/content/res/XResources.java b/Bridge/src/main/java/android/content/res/XResources.java new file mode 100644 index 00000000..e8d67258 --- /dev/null +++ b/Bridge/src/main/java/android/content/res/XResources.java @@ -0,0 +1,1738 @@ +package android.content.res; + +import android.content.Context; +import android.content.pm.PackageParser; +import android.content.pm.PackageParser.PackageParserException; +import android.graphics.Color; +import android.graphics.Movie; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.Html; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.xmlpull.v1.XmlPullParser; + +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.WeakHashMap; + +import de.robv.android.xposed.IXposedHookZygoteInit; +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XC_MethodHook.MethodHookParam; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedBridge.CopyOnWriteSortedSet; +import de.robv.android.xposed.callbacks.XC_LayoutInflated; +import de.robv.android.xposed.callbacks.XC_LayoutInflated.LayoutInflatedParam; +import de.robv.android.xposed.callbacks.XCallback; +import xposed.dummy.XResourcesSuperClass; +import xposed.dummy.XTypedArraySuperClass; + +import static de.robv.android.xposed.XposedHelpers.decrementMethodDepth; +import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; +import static de.robv.android.xposed.XposedHelpers.getIntField; +import static de.robv.android.xposed.XposedHelpers.getLongField; +import static de.robv.android.xposed.XposedHelpers.getObjectField; +import static de.robv.android.xposed.XposedHelpers.incrementMethodDepth; + +/** + * {@link android.content.res.Resources} subclass that allows replacing individual resources. + * + *

Xposed replaces the standard resources with this class, which overrides the methods used for + * retrieving individual resources and adds possibilities to replace them. These replacements can + * be set using the methods made available via the API methods in this class. + */ +@SuppressWarnings("JniMissingFunction") +public class XResources extends XResourcesSuperClass { + private static final SparseArray> sReplacements = new SparseArray<>(); + private static final SparseArray> sResourceNames = new SparseArray<>(); + + private static final byte[] sSystemReplacementsCache = new byte[256]; // bitmask: 0x000700ff => 2048 bit => 256 bytes + private byte[] mReplacementsCache; // bitmask: 0x0007007f => 1024 bit => 128 bytes + private static final HashMap sReplacementsCacheMap = new HashMap<>(); + private static final SparseArray sColorStateListCache = new SparseArray<>(0); + + private static final SparseArray>> sLayoutCallbacks = new SparseArray<>(); + private static final WeakHashMap sXmlInstanceDetails = new WeakHashMap<>(); + + private static final String EXTRA_XML_INSTANCE_DETAILS = "xmlInstanceDetails"; + private static final ThreadLocal> sIncludedLayouts = new ThreadLocal>() { + @Override + protected LinkedList initialValue() { + return new LinkedList<>(); + } + }; + + private static final HashMap sResDirLastModified = new HashMap<>(); + private static final HashMap sResDirPackageNames = new HashMap<>(); + private static ThreadLocal sLatestResKey = null; + + private boolean mIsObjectInited; + private String mResDir; + private String mPackageName; + + /** Dummy, will never be called (objects are transferred to this class only). */ + private XResources() { + throw new UnsupportedOperationException(); + } + + /** @hide */ + public void initObject(String resDir) { + if (mIsObjectInited) + throw new IllegalStateException("Object has already been initialized"); + + this.mResDir = resDir; + this.mPackageName = getPackageName(resDir); + + if (resDir != null) { + synchronized (sReplacementsCacheMap) { + mReplacementsCache = sReplacementsCacheMap.get(resDir); + if (mReplacementsCache == null) { + mReplacementsCache = new byte[128]; + sReplacementsCacheMap.put(resDir, mReplacementsCache); + } + } + } + + this.mIsObjectInited = true; + } + + /** @hide */ + public boolean isFirstLoad() { + synchronized (sReplacements) { + if (mResDir == null) + return false; + + Long lastModification = new File(mResDir).lastModified(); + Long oldModified = sResDirLastModified.get(mResDir); + if (lastModification.equals(oldModified)) + return false; + + sResDirLastModified.put(mResDir, lastModification); + + if (oldModified == null) + return true; + + // file was changed meanwhile => remove old replacements + for (int i = 0; i < sReplacements.size(); i++) { + sReplacements.valueAt(i).remove(mResDir); + } + Arrays.fill(mReplacementsCache, (byte) 0); + return true; + } + } + + /** @hide */ + public static void setPackageNameForResDir(String packageName, String resDir) { + synchronized (sResDirPackageNames) { + sResDirPackageNames.put(resDir, packageName); + } + } + + /** + * Returns the name of the package that these resources belong to, or "android" for system resources. + */ + public String getPackageName() { + return mPackageName; + } + + private static String getPackageName(String resDir) { + if (resDir == null) + return "android"; + + String packageName; + synchronized (sResDirPackageNames) { + packageName = sResDirPackageNames.get(resDir); + } + + if (packageName != null) + return packageName; + + PackageParser.PackageLite pkgInfo; + if (Build.VERSION.SDK_INT >= 21) { + try { + pkgInfo = PackageParser.parsePackageLite(new File(resDir), 0); + } catch (PackageParserException e) { + throw new IllegalStateException("Could not determine package name for " + resDir, e); + } + } else { + pkgInfo = PackageParser.parsePackageLite(resDir, 0); + } + if (pkgInfo != null && pkgInfo.packageName != null) { + Log.w(XposedBridge.TAG, "Package name for " + resDir + " had to be retrieved via parser"); + packageName = pkgInfo.packageName; + setPackageNameForResDir(packageName, resDir); + return packageName; + } + + throw new IllegalStateException("Could not determine package name for " + resDir); + } + + /** + * Special case of {@link #getPackageName} during object creation. + * + *

For a short moment during/after the creation of a new {@link android.content.res Resources} + * object, it isn't an instance of {@link XResources} yet. For any hooks that need information + * about the just created object during this particular stage, this method will return the + * package name. + * + *

If you call this method outside of {@code getTopLevelResources()}, it + * throws an {@code IllegalStateException}. + */ + public static String getPackageNameDuringConstruction() { + Object key; + if (sLatestResKey == null || (key = sLatestResKey.get()) == null) + throw new IllegalStateException("This method can only be called during getTopLevelResources()"); + + String resDir = (String) getObjectField(key, "mResDir"); + return getPackageName(resDir); + } + + /** @hide */ + public static void init(ThreadLocal latestResKey) throws Exception { + sLatestResKey = latestResKey; + + findAndHookMethod(LayoutInflater.class, "inflate", XmlPullParser.class, ViewGroup.class, boolean.class, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + if (param.hasThrowable()) + return; + + XMLInstanceDetails details; + synchronized (sXmlInstanceDetails) { + details = sXmlInstanceDetails.get(param.args[0]); + } + if (details != null) { + LayoutInflatedParam liparam = new LayoutInflatedParam(details.callbacks); + liparam.view = (View) param.getResult(); + liparam.resNames = details.resNames; + liparam.variant = details.variant; + liparam.res = details.res; + XCallback.callAll(liparam); + } + } + }); + + final XC_MethodHook parseIncludeHook = new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + sIncludedLayouts.get().push(param); + } + + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + sIncludedLayouts.get().pop(); + + if (param.hasThrowable()) + return; + + // filled in by our implementation of getLayout() + XMLInstanceDetails details = (XMLInstanceDetails) param.getObjectExtra(EXTRA_XML_INSTANCE_DETAILS); + if (details != null) { + LayoutInflatedParam liparam = new LayoutInflatedParam(details.callbacks); + ViewGroup group = (ViewGroup) param.args[(Build.VERSION.SDK_INT < 23) ? 1 : 2]; + liparam.view = group.getChildAt(group.getChildCount() - 1); + liparam.resNames = details.resNames; + liparam.variant = details.variant; + liparam.res = details.res; + XCallback.callAll(liparam); + } + } + }; + if (Build.VERSION.SDK_INT < 21) { + findAndHookMethod(LayoutInflater.class, "parseInclude", XmlPullParser.class, View.class, + AttributeSet.class, parseIncludeHook); + } else if (Build.VERSION.SDK_INT < 23) { + findAndHookMethod(LayoutInflater.class, "parseInclude", XmlPullParser.class, View.class, + AttributeSet.class, boolean.class, parseIncludeHook); + } else { + findAndHookMethod(LayoutInflater.class, "parseInclude", XmlPullParser.class, Context.class, + View.class, AttributeSet.class, parseIncludeHook); + } + } + + /** + * Wrapper for information about an indiviual resource. + */ + public static class ResourceNames { + /** The resource ID. */ + public final int id; + /** The resource package name as returned by {@link #getResourcePackageName}. */ + public final String pkg; + /** The resource entry name as returned by {@link #getResourceEntryName}. */ + public final String name; + /** The resource type name as returned by {@link #getResourceTypeName}. */ + public final String type; + /** The full resource nameas returned by {@link #getResourceName}. */ + public final String fullName; + + private ResourceNames(int id, String pkg, String name, String type) { + this.id = id; + this.pkg = pkg; + this.name = name; + this.type = type; + this.fullName = pkg + ":" + type + "/" + name; + } + + /** + * Returns whether all non-null parameters match the values of this object. + */ + public boolean equals(String pkg, String name, String type, int id) { + return (pkg == null || pkg.equals(this.pkg)) + && (name == null || name.equals(this.name)) + && (type == null || type.equals(this.type)) + && (id == 0 || id == this.id); + } + } + + private ResourceNames getResourceNames(int id) { + return new ResourceNames( + id, + getResourcePackageName(id), + getResourceTypeName(id), + getResourceEntryName(id)); + } + + private static ResourceNames getSystemResourceNames(int id) { + Resources sysRes = getSystem(); + return new ResourceNames( + id, + sysRes.getResourcePackageName(id), + sysRes.getResourceTypeName(id), + sysRes.getResourceEntryName(id)); + } + + private static void putResourceNames(String resDir, ResourceNames resNames) { + int id = resNames.id; + synchronized (sResourceNames) { + HashMap inner = sResourceNames.get(id); + if (inner == null) { + inner = new HashMap<>(); + sResourceNames.put(id, inner); + } + synchronized (inner) { + inner.put(resDir, resNames); + } + } + } + + // ======================================================= + // DEFINING REPLACEMENTS + // ======================================================= + + /** + * Sets a replacement for an individual resource. See {@link #setReplacement(String, String, String, Object)}. + * + * @param id The ID of the resource which should be replaced. + * @param replacement The replacement, see above. + */ + public void setReplacement(int id, Object replacement) { + setReplacement(id, replacement, this); + } + + /** + * Sets a replacement for an individual resource. See {@link #setReplacement(String, String, String, Object)}. + * + * @deprecated Use {@link #setReplacement(String, String, String, Object)} instead. + * + * @param fullName The full resource name, e.g. {@code com.example.myapplication:string/app_name}. + * See {@link #getResourceName}. + * @param replacement The replacement. + */ + @Deprecated + public void setReplacement(String fullName, Object replacement) { + int id = getIdentifier(fullName, null, null); + if (id == 0) + throw new NotFoundException(fullName); + setReplacement(id, replacement, this); + } + + /** + * Sets a replacement for an individual resource. If called more than once for the same ID, the + * replacement from the last call is used. Setting the replacement to {@code null} removes it. + * + *

The allowed replacements depend on the type of the source. All types accept an + * {@link XResForwarder} object, which is usually created with {@link XModuleResources#fwd}. + * The resource request will then be forwarded to another {@link android.content.res.Resources} + * object. In addition to that, the following replacement types are accepted: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Resource type Additional allowed replacement types (*) Returned from (**)
Animation none{@link #getAnimation}
Bool{@link Boolean}{@link #getBoolean}
Color{@link Integer} (you might want to use {@link Color#parseColor}){@link #getColor}
+ * {@link #getDrawable} (creates a {@link ColorDrawable})
+ * {@link #getColorStateList} (calls {@link android.content.res.ColorStateList#valueOf}) + *
Color State List{@link android.content.res.ColorStateList}
+ * {@link Integer} (calls {@link android.content.res.ColorStateList#valueOf}) + *
{@link #getColorStateList}
Dimension{@link DimensionReplacement} (since v50){@link #getDimension}
+ * {@link #getDimensionPixelOffset}
+ * {@link #getDimensionPixelSize} + *
Drawable + * (including mipmap){@link DrawableLoader}
+ * {@link Integer} (creates a {@link ColorDrawable}) + *
{@link #getDrawable}
+ * {@link #getDrawableForDensity} + *
Fraction none{@link #getFraction}
Integer{@link Integer}{@link #getInteger}
Integer Array{@code int[]}{@link #getIntArray}
Layout none, but see {@link #hookLayout}{@link #getLayout}
Movie none{@link #getMovie}
Quantity Strings (Plurals) none{@link #getQuantityString}
+ * {@link #getQuantityText} + *
String{@link String}
+ * {@link CharSequence} (for styled texts, see also {@link Html#fromHtml}) + *
{@link #getString}
+ * {@link #getText} + *
String Array{@code String[]}
+ * {@code CharSequence[]} (for styled texts, see also {@link Html#fromHtml}) + *
{@link #getStringArray}
+ * {@link #getTextArray} + *
XML none{@link #getXml}
+ * {@link #getQuantityText} + *
+ * + *

Other resource types, such as + * styles/themes, + * {@linkplain #openRawResource raw resources} and + * typed arrays + * can't be replaced. + * + *

+ * * Auto-boxing allows you to use literals like {@code 123} where an {@link Integer} is + * accepted, so you don't neeed to call methods like {@link Integer#valueOf(int)} manually.
+ * ** Some of these methods have multiple variants, only one of them is mentioned here. + *
+ * + * @param pkg The package name, e.g. {@code com.example.myapplication}. + * See {@link #getResourcePackageName}. + * @param type The type name, e.g. {@code string}. + * See {@link #getResourceTypeName}. + * @param name The entry name, e.g. {@code app_name}. + * See {@link #getResourceEntryName}. + * @param replacement The replacement. + */ + public void setReplacement(String pkg, String type, String name, Object replacement) { + int id = getIdentifier(name, type, pkg); + if (id == 0) + throw new NotFoundException(pkg + ":" + type + "/" + name); + setReplacement(id, replacement, this); + } + + /** + * Sets a replacement for an individual Android framework resource (in the {@code android} package). + * See {@link #setSystemWideReplacement(String, String, String, Object)}. + * + * @param id The ID of the resource which should be replaced. + * @param replacement The replacement. + */ + public static void setSystemWideReplacement(int id, Object replacement) { + setReplacement(id, replacement, null); + } + + /** + * Sets a replacement for an individual Android framework resource (in the {@code android} package). + * See {@link #setSystemWideReplacement(String, String, String, Object)}. + * + * @deprecated Use {@link #setSystemWideReplacement(String, String, String, Object)} instead. + * + * @param fullName The full resource name, e.g. {@code android:string/yes}. + * See {@link #getResourceName}. + * @param replacement The replacement. + */ + @Deprecated + public static void setSystemWideReplacement(String fullName, Object replacement) { + int id = getSystem().getIdentifier(fullName, null, null); + if (id == 0) + throw new NotFoundException(fullName); + setReplacement(id, replacement, null); + } + + /** + * Sets a replacement for an individual Android framework resource (in the {@code android} package). + * + *

Some resources are part of the Android framework and can be used in any app. They're + * accessible via {@link android.R android.R} and are not bound to a specific + * {@link android.content.res.Resources} instance. Such resources can be replaced in + * {@link IXposedHookZygoteInit#initZygote initZygote()} for all apps. As there is no + * {@link XResources} object easily available in that scope, this static method can be used + * to set resource replacements. All other details (e.g. how certain types can be replaced) are + * mentioned in {@link #setReplacement(String, String, String, Object)}. + * + * @param pkg The package name, should always be {@code android} here. + * See {@link #getResourcePackageName}. + * @param type The type name, e.g. {@code string}. + * See {@link #getResourceTypeName}. + * @param name The entry name, e.g. {@code yes}. + * See {@link #getResourceEntryName}. + * @param replacement The replacement. + */ + public static void setSystemWideReplacement(String pkg, String type, String name, Object replacement) { + int id = getSystem().getIdentifier(name, type, pkg); + if (id == 0) + throw new NotFoundException(pkg + ":" + type + "/" + name); + setReplacement(id, replacement, null); + } + + private static void setReplacement(int id, Object replacement, XResources res) { + String resDir = (res != null) ? res.mResDir : null; + if (id == 0) + throw new IllegalArgumentException("id 0 is not an allowed resource identifier"); + else if (resDir == null && id >= 0x7f000000) + throw new IllegalArgumentException("ids >= 0x7f000000 are app specific and cannot be set for the framework"); + + if (replacement instanceof Drawable) + throw new IllegalArgumentException("Drawable replacements are deprecated since Xposed 2.1. Use DrawableLoader instead."); + + // Cache that we have a replacement for this ID, false positives are accepted to save memory. + if (id < 0x7f000000) { + int cacheKey = (id & 0x00070000) >> 11 | (id & 0xf8) >> 3; + synchronized (sSystemReplacementsCache) { + sSystemReplacementsCache[cacheKey] |= 1 << (id & 7); + } + } else { + int cacheKey = (id & 0x00070000) >> 12 | (id & 0x78) >> 3; + synchronized (res.mReplacementsCache) { + res.mReplacementsCache[cacheKey] |= 1 << (id & 7); + } + } + + synchronized (sReplacements) { + HashMap inner = sReplacements.get(id); + if (inner == null) { + inner = new HashMap<>(); + sReplacements.put(id, inner); + } + inner.put(resDir, replacement); + } + } + + // ======================================================= + // RETURNING REPLACEMENTS + // ======================================================= + + private Object getReplacement(int id) { + if (id <= 0) + return null; + + // Check the cache whether it's worth looking for replacements + if (id < 0x7f000000) { + int cacheKey = (id & 0x00070000) >> 11 | (id & 0xf8) >> 3; + if ((sSystemReplacementsCache[cacheKey] & (1 << (id & 7))) == 0) + return null; + } else if (mResDir != null) { + int cacheKey = (id & 0x00070000) >> 12 | (id & 0x78) >> 3; + if ((mReplacementsCache[cacheKey] & (1 << (id & 7))) == 0) + return null; + } + + HashMap inner; + synchronized (sReplacements) { + inner = sReplacements.get(id); + } + + if (inner == null) + return null; + + synchronized (inner) { + Object result = inner.get(mResDir); + if (result != null || mResDir == null) + return result; + return inner.get(null); + } + } + + /** @hide */ + @Override + public XmlResourceParser getAnimation(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + + boolean loadedFromCache = isXmlCached(repRes, repId); + XmlResourceParser result = repRes.getAnimation(repId); + + if (!loadedFromCache) { + long parseState = (Build.VERSION.SDK_INT >= 21) + ? getLongField(result, "mParseState") + : getIntField(result, "mParseState"); + rewriteXmlReferencesNative(parseState, this, repRes); + } + + return result; + } + return super.getAnimation(id); + } + + /** @hide */ + @Override + public boolean getBoolean(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof Boolean) { + return (Boolean) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getBoolean(repId); + } + return super.getBoolean(id); + } + + /** @hide */ + @Override + public int getColor(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof Integer) { + return (Integer) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getColor(repId); + } + return super.getColor(id); + } + + /** @hide */ + @Override + public ColorStateList getColorStateList(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof ColorStateList) { + return (ColorStateList) replacement; + } else if (replacement instanceof Integer) { + int color = (Integer) replacement; + synchronized (sColorStateListCache) { + ColorStateList result = sColorStateListCache.get(color); + if (result == null) { + result = ColorStateList.valueOf(color); + sColorStateListCache.put(color, result); + } + return result; + } + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getColorStateList(repId); + } + return super.getColorStateList(id); + } + + /** @hide */ + @Override + public float getDimension(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof DimensionReplacement) { + return ((DimensionReplacement) replacement).getDimension(getDisplayMetrics()); + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDimension(repId); + } + return super.getDimension(id); + } + + /** @hide */ + @Override + public int getDimensionPixelOffset(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof DimensionReplacement) { + return ((DimensionReplacement) replacement).getDimensionPixelOffset(getDisplayMetrics()); + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDimensionPixelOffset(repId); + } + return super.getDimensionPixelOffset(id); + } + + /** @hide */ + @Override + public int getDimensionPixelSize(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof DimensionReplacement) { + return ((DimensionReplacement) replacement).getDimensionPixelSize(getDisplayMetrics()); + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDimensionPixelSize(repId); + } + return super.getDimensionPixelSize(id); + } + + /** @hide */ + @Override + public Drawable getDrawable(int id) throws NotFoundException { + try { + if (incrementMethodDepth("getDrawable") == 1) { + Object replacement = getReplacement(id); + if (replacement instanceof DrawableLoader) { + try { + Drawable result = ((DrawableLoader) replacement).newDrawable(this, id); + if (result != null) + return result; + } catch (Throwable t) { XposedBridge.log(t); } + } else if (replacement instanceof Integer) { + return new ColorDrawable((Integer) replacement); + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDrawable(repId); + } + } + return super.getDrawable(id); + } finally { + decrementMethodDepth("getDrawable"); + } + } + + /** @hide */ + @Override + public Drawable getDrawable(int id, Theme theme) throws NotFoundException { + try { + if (incrementMethodDepth("getDrawable") == 1) { + Object replacement = getReplacement(id); + if (replacement instanceof DrawableLoader) { + try { + Drawable result = ((DrawableLoader) replacement).newDrawable(this, id); + if (result != null) + return result; + } catch (Throwable t) { XposedBridge.log(t); } + } else if (replacement instanceof Integer) { + return new ColorDrawable((Integer) replacement); + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDrawable(repId); + } + } + return super.getDrawable(id, theme); + } finally { + decrementMethodDepth("getDrawable"); + } + } + + /** @hide */ + @Override + public Drawable getDrawable(int id, Theme theme, boolean supportComposedIcons) throws NotFoundException { + try { + if (incrementMethodDepth("getDrawable") == 1) { + Object replacement = getReplacement(id); + if (replacement instanceof DrawableLoader) { + try { + Drawable result = ((DrawableLoader) replacement).newDrawable(this, id); + if (result != null) + return result; + } catch (Throwable t) { XposedBridge.log(t); } + } else if (replacement instanceof Integer) { + return new ColorDrawable((Integer) replacement); + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDrawable(repId); + } + } + return super.getDrawable(id, theme, supportComposedIcons); + } finally { + decrementMethodDepth("getDrawable"); + } + } + + /** @hide */ + @Override + public Drawable getDrawableForDensity(int id, int density) throws NotFoundException { + try { + if (incrementMethodDepth("getDrawableForDensity") == 1) { + Object replacement = getReplacement(id); + if (replacement instanceof DrawableLoader) { + try { + Drawable result = ((DrawableLoader) replacement).newDrawableForDensity(this, id, density); + if (result != null) + return result; + } catch (Throwable t) { XposedBridge.log(t); } + } else if (replacement instanceof Integer) { + return new ColorDrawable((Integer) replacement); + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDrawableForDensity(repId, density); + } + } + return super.getDrawableForDensity(id, density); + } finally { + decrementMethodDepth("getDrawableForDensity"); + } + } + + /** @hide */ + @Override + public Drawable getDrawableForDensity(int id, int density, Theme theme) throws NotFoundException { + try { + if (incrementMethodDepth("getDrawableForDensity") == 1) { + Object replacement = getReplacement(id); + if (replacement instanceof DrawableLoader) { + try { + Drawable result = ((DrawableLoader) replacement).newDrawableForDensity(this, id, density); + if (result != null) + return result; + } catch (Throwable t) { XposedBridge.log(t); } + } else if (replacement instanceof Integer) { + return new ColorDrawable((Integer) replacement); + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDrawableForDensity(repId, density); + } + } + return super.getDrawableForDensity(id, density, theme); + } finally { + decrementMethodDepth("getDrawableForDensity"); + } + } + + /** @hide */ + @Override + public Drawable getDrawableForDensity(int id, int density, Theme theme, boolean supportComposedIcons) throws NotFoundException { + try { + if (incrementMethodDepth("getDrawableForDensity") == 1) { + Object replacement = getReplacement(id); + if (replacement instanceof DrawableLoader) { + try { + Drawable result = ((DrawableLoader) replacement).newDrawableForDensity(this, id, density); + if (result != null) + return result; + } catch (Throwable t) { XposedBridge.log(t); } + } else if (replacement instanceof Integer) { + return new ColorDrawable((Integer) replacement); + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDrawableForDensity(repId, density); + } + } + return super.getDrawableForDensity(id, density, theme, supportComposedIcons); + } finally { + decrementMethodDepth("getDrawableForDensity"); + } + } + + /** @hide */ + @Override + public float getFraction(int id, int base, int pbase) { + Object replacement = getReplacement(id); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getFraction(repId, base, pbase); + } + return super.getFraction(id, base, pbase); + } + + /** @hide */ + @Override + public int getInteger(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof Integer) { + return (Integer) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getInteger(repId); + } + return super.getInteger(id); + } + + /** @hide */ + @Override + public int[] getIntArray(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof int[]) { + return (int[]) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getIntArray(repId); + } + return super.getIntArray(id); + } + + /** @hide */ + @Override + public XmlResourceParser getLayout(int id) throws NotFoundException { + XmlResourceParser result; + Object replacement = getReplacement(id); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + + boolean loadedFromCache = isXmlCached(repRes, repId); + result = repRes.getLayout(repId); + + if (!loadedFromCache) { + long parseState = (Build.VERSION.SDK_INT >= 21) + ? getLongField(result, "mParseState") + : getIntField(result, "mParseState"); + rewriteXmlReferencesNative(parseState, this, repRes); + } + } else { + result = super.getLayout(id); + } + + // Check whether this layout is hooked + HashMap> inner; + synchronized (sLayoutCallbacks) { + inner = sLayoutCallbacks.get(id); + } + if (inner != null) { + CopyOnWriteSortedSet callbacks; + synchronized (inner) { + callbacks = inner.get(mResDir); + if (callbacks == null && mResDir != null) + callbacks = inner.get(null); + } + if (callbacks != null) { + String variant = "layout"; + TypedValue value = (TypedValue) getObjectField(this, "mTmpValue"); + getValue(id, value, true); + if (value.type == TypedValue.TYPE_STRING) { + String[] components = value.string.toString().split("/", 3); + if (components.length == 3) + variant = components[1]; + else + XposedBridge.log("Unexpected resource path \"" + value.string.toString() + + "\" for resource id 0x" + Integer.toHexString(id)); + } else { + XposedBridge.log(new NotFoundException("Could not find file name for resource id 0x") + Integer.toHexString(id)); + } + + synchronized (sXmlInstanceDetails) { + synchronized (sResourceNames) { + HashMap resNamesInner = sResourceNames.get(id); + if (resNamesInner != null) { + synchronized (resNamesInner) { + XMLInstanceDetails details = new XMLInstanceDetails(resNamesInner.get(mResDir), variant, callbacks); + sXmlInstanceDetails.put(result, details); + + // if we were called inside LayoutInflater.parseInclude, store the details for it + MethodHookParam top = sIncludedLayouts.get().peek(); + if (top != null) + top.setObjectExtra(EXTRA_XML_INSTANCE_DETAILS, details); + } + } + } + } + } + } + + return result; + } + + /** @hide */ + @Override + public Movie getMovie(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getMovie(repId); + } + return super.getMovie(id); + } + + /** @hide */ + @Override + public CharSequence getQuantityText(int id, int quantity) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getQuantityText(repId, quantity); + } + return super.getQuantityText(id, quantity); + } + // these are handled by getQuantityText: + // public String getQuantityString(int id, int quantity); + // public String getQuantityString(int id, int quantity, Object... formatArgs); + + /** @hide */ + @Override + public String[] getStringArray(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof String[]) { + return (String[]) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getStringArray(repId); + } + return super.getStringArray(id); + } + + /** @hide */ + @Override + public CharSequence getText(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof CharSequence) { + return (CharSequence) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getText(repId); + } + return super.getText(id); + } + // these are handled by getText: + // public String getString(int id); + // public String getString(int id, Object... formatArgs); + + /** @hide */ + @Override + public CharSequence getText(int id, CharSequence def) { + Object replacement = getReplacement(id); + if (replacement instanceof CharSequence) { + return (CharSequence) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getText(repId, def); + } + return super.getText(id, def); + } + + /** @hide */ + @Override + public CharSequence[] getTextArray(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof CharSequence[]) { + return (CharSequence[]) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getTextArray(repId); + } + return super.getTextArray(id); + } + + /** @hide */ + @Override + public XmlResourceParser getXml(int id) throws NotFoundException { + Object replacement = getReplacement(id); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + + boolean loadedFromCache = isXmlCached(repRes, repId); + XmlResourceParser result = repRes.getXml(repId); + + if (!loadedFromCache) { + long parseState = (Build.VERSION.SDK_INT >= 21) + ? getLongField(result, "mParseState") + : getIntField(result, "mParseState"); + rewriteXmlReferencesNative(parseState, this, repRes); + } + + return result; + } + return super.getXml(id); + } + + private static boolean isXmlCached(Resources res, int id) { + int[] mCachedXmlBlockIds = (int[]) getObjectField(res, "mCachedXmlBlockIds"); + synchronized (mCachedXmlBlockIds) { + for (int cachedId : mCachedXmlBlockIds) { + if (cachedId == id) + return true; + } + } + return false; + } + + private static native void rewriteXmlReferencesNative(long parserPtr, XResources origRes, Resources repRes); + + /** + * Used to replace reference IDs in XMLs. + * + * When resource requests are forwarded to modules, the may include references to resources with the same + * name as in the original resources, but the IDs generated by aapt will be different. rewriteXmlReferencesNative + * walks through all references and calls this function to find out the original ID, which it then writes to + * the compiled XML file in the memory. + */ + private static int translateResId(int id, XResources origRes, Resources repRes) { + try { + String entryName = repRes.getResourceEntryName(id); + String entryType = repRes.getResourceTypeName(id); + String origPackage = origRes.mPackageName; + int origResId = 0; + try { + // look for a resource with the same name and type in the original package + origResId = origRes.getIdentifier(entryName, entryType, origPackage); + } catch (NotFoundException ignored) {} + + boolean repResDefined = false; + try { + final TypedValue tmpValue = new TypedValue(); + repRes.getValue(id, tmpValue, false); + // if a resource has not been defined (i.e. only a resource ID has been created), it will equal "false" + // this means a boolean "false" value is not detected of it is directly referenced in an XML file + repResDefined = !(tmpValue.type == TypedValue.TYPE_INT_BOOLEAN && tmpValue.data == 0); + } catch (NotFoundException ignored) {} + + if (!repResDefined && origResId == 0 && !entryType.equals("id")) { + XposedBridge.log(entryType + "/" + entryName + " is neither defined in module nor in original resources"); + return 0; + } + + // exists only in module, so create a fake resource id + if (origResId == 0) + origResId = getFakeResId(repRes, id); + + // IDs will never be loaded, no need to set a replacement + if (repResDefined && !entryType.equals("id")) + origRes.setReplacement(origResId, new XResForwarder(repRes, id)); + + return origResId; + } catch (Exception e) { + XposedBridge.log(e); + return id; + } + } + + /** + * Generates a fake resource ID. + * + *

The parameter is just hashed, it doesn't have a deeper meaning. However, it's recommended + * to use values with a low risk for conflicts, such as a full resource name. Calling this + * method multiple times will return the same ID. + * + * @param resName A used for hashing, see above. + * @return The fake resource ID. + */ + public static int getFakeResId(String resName) { + return 0x7e000000 | (resName.hashCode() & 0x00ffffff); + } + + /** + * Generates a fake resource ID. + * + *

This variant uses the result of {@link #getResourceName} to create the hash that the ID is + * based on. The given resource doesn't need to match the {@link XResources} instance for which + * the fake resource ID is going to be used. + * + * @param res The {@link android.content.res.Resources} object to be used for hashing. + * @param id The resource ID to be used for hashing. + * @return The fake resource ID. + */ + public static int getFakeResId(Resources res, int id) { + return getFakeResId(res.getResourceName(id)); + } + + /** + * Makes any individual resource available from another {@link android.content.res.Resources} + * instance available in this {@link XResources} instance. + * + *

This method combines calls to {@link #getFakeResId(Resources, int)} and + * {@link #setReplacement(int, Object)} to generate a fake resource ID and set up a replacement + * for it which forwards to the given resource. + * + *

The returned ID can only be used to retrieve the resource, it won't work for methods like + * {@link #getResourceName} etc. + * + * @param res The target {@link android.content.res.Resources} instance. + * @param id The target resource ID. + * @return The fake resource ID (see above). + */ + public int addResource(Resources res, int id) { + int fakeId = getFakeResId(res, id); + synchronized (sReplacements) { + if (sReplacements.indexOfKey(fakeId) < 0) + setReplacement(fakeId, new XResForwarder(res, id)); + } + return fakeId; + } + + /** + * Similar to {@link #translateResId}, but used to determine the original ID of attribute names. + */ + private static int translateAttrId(String attrName, XResources origRes) { + String origPackage = origRes.mPackageName; + int origAttrId = 0; + try { + origAttrId = origRes.getIdentifier(attrName, "attr", origPackage); + } catch (NotFoundException e) { + XposedBridge.log("Attribute " + attrName + " not found in original resources"); + } + return origAttrId; + } + + // ======================================================= + // XTypedArray class + // ======================================================= + /** + * {@link android.content.res.TypedArray} replacement that replaces values on-the-fly. + * Mainly used when inflating layouts. + * @hide + */ + public static class XTypedArray extends XTypedArraySuperClass { + /** Dummy, will never be called (objects are transferred to this class only). */ + private XTypedArray() { + super(null, null, null, 0); + throw new UnsupportedOperationException(); + } + + @Override + public boolean getBoolean(int index, boolean defValue) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof Boolean) { + return (Boolean) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getBoolean(repId); + } + return super.getBoolean(index, defValue); + } + + @Override + public int getColor(int index, int defValue) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof Integer) { + return (Integer) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getColor(repId); + } + return super.getColor(index, defValue); + } + + @Override + public ColorStateList getColorStateList(int index) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof ColorStateList) { + return (ColorStateList) replacement; + } else if (replacement instanceof Integer) { + int color = (Integer) replacement; + synchronized (sColorStateListCache) { + ColorStateList result = sColorStateListCache.get(color); + if (result == null) { + result = ColorStateList.valueOf(color); + sColorStateListCache.put(color, result); + } + return result; + } + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getColorStateList(repId); + } + return super.getColorStateList(index); + } + + @Override + public float getDimension(int index, float defValue) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDimension(repId); + } + return super.getDimension(index, defValue); + } + + @Override + public int getDimensionPixelOffset(int index, int defValue) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDimensionPixelOffset(repId); + } + return super.getDimensionPixelOffset(index, defValue); + } + + @Override + public int getDimensionPixelSize(int index, int defValue) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDimensionPixelSize(repId); + } + return super.getDimensionPixelSize(index, defValue); + } + + @Override + public Drawable getDrawable(int index) { + final int resId = getResourceId(index, 0); + XResources xres = (XResources) getResources(); + Object replacement = xres.getReplacement(resId); + if (replacement instanceof DrawableLoader) { + try { + Drawable result = ((DrawableLoader) replacement).newDrawable(xres, resId); + if (result != null) + return result; + } catch (Throwable t) { XposedBridge.log(t); } + } else if (replacement instanceof Integer) { + return new ColorDrawable((Integer) replacement); + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDrawable(repId); + } + return super.getDrawable(index); + } + + @Override + public float getFloat(int index, float defValue) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + // dimensions seem to be the only way to define floats by references + return repRes.getDimension(repId); + } + return super.getFloat(index, defValue); + } + + @Override + public float getFraction(int index, int base, int pbase, float defValue) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + // dimensions seem to be the only way to define floats by references + return repRes.getFraction(repId, base, pbase); + } + return super.getFraction(index, base, pbase, defValue); + } + + @Override + public int getInt(int index, int defValue) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof Integer) { + return (Integer) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getInteger(repId); + } + return super.getInt(index, defValue); + } + + @Override + public int getInteger(int index, int defValue) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof Integer) { + return (Integer) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getInteger(repId); + } + return super.getInteger(index, defValue); + } + + @Override + public int getLayoutDimension(int index, int defValue) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDimensionPixelSize(repId); + } + return super.getLayoutDimension(index, defValue); + } + + @Override + public int getLayoutDimension(int index, String name) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getDimensionPixelSize(repId); + } + return super.getLayoutDimension(index, name); + } + + @Override + public String getString(int index) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof CharSequence) { + return replacement.toString(); + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getString(repId); + } + return super.getString(index); + } + + @Override + public CharSequence getText(int index) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof CharSequence) { + return (CharSequence) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getText(repId); + } + return super.getText(index); + } + + @Override + public CharSequence[] getTextArray(int index) { + Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); + if (replacement instanceof CharSequence[]) { + return (CharSequence[]) replacement; + } else if (replacement instanceof XResForwarder) { + Resources repRes = ((XResForwarder) replacement).getResources(); + int repId = ((XResForwarder) replacement).getId(); + return repRes.getTextArray(repId); + } + return super.getTextArray(index); + } + } + + + // ======================================================= + // DrawableLoader class + // ======================================================= + /** + * Callback for drawable replacements. Instances of this class can passed to + * {@link #setReplacement(String, String, String, Object)} and its variants. + * + *

Make sure to always return new {@link Drawable} instances, as drawables + * usually can't be reused. + */ + @SuppressWarnings("UnusedParameters") + public static abstract class DrawableLoader { + /** + * Constructor. + */ + public DrawableLoader() {} + + /** + * Called when the hooked drawable resource has been requested. + * + * @param res The {@link XResources} object in which the hooked drawable resides. + * @param id The resource ID which has been requested. + * @return The {@link Drawable} which should be used as replacement. {@code null} is ignored. + * @throws Throwable Everything the callback throws is caught and logged. + */ + public abstract Drawable newDrawable(XResources res, int id) throws Throwable; + + /** + * Like {@link #newDrawable}, but called for {@link #getDrawableForDensity}. The default + * implementation is to use the result of {@link #newDrawable}. + * + * @param res The {@link XResources} object in which the hooked drawable resides. + * @param id The resource ID which has been requested. + * @param density The desired screen density indicated by the resource as found in + * {@link DisplayMetrics}. + * @return The {@link Drawable} which should be used as replacement. {@code null} is ignored. + * @throws Throwable Everything the callback throws is caught and logged. + */ + public Drawable newDrawableForDensity(XResources res, int id, int density) throws Throwable { + return newDrawable(res, id); + } + } + + + // ======================================================= + // DimensionReplacement class + // ======================================================= + /** + * Callback for dimension replacements. Instances of this class can passed to + * {@link #setReplacement(String, String, String, Object)} and its variants. + */ + public static class DimensionReplacement { + private final float mValue; + private final int mUnit; + + /** + * Creates an instance that can be used for {@link #setReplacement(String, String, String, Object)} + * to replace a dimension resource. + * + * @param value The value of the replacement, in the unit specified with the next parameter. + * @param unit One of the {@code COMPLEX_UNIT_*} constants in {@link TypedValue}. + */ + public DimensionReplacement(float value, int unit) { + mValue = value; + mUnit = unit; + } + + /** Called by {@link android.content.res.Resources#getDimension}. */ + public float getDimension(DisplayMetrics metrics) { + return TypedValue.applyDimension(mUnit, mValue, metrics); + } + + /** Called by {@link android.content.res.Resources#getDimensionPixelOffset}. */ + public int getDimensionPixelOffset(DisplayMetrics metrics) { + return (int) TypedValue.applyDimension(mUnit, mValue, metrics); + } + + /** Called by {@link android.content.res.Resources#getDimensionPixelSize}. */ + public int getDimensionPixelSize(DisplayMetrics metrics) { + final float f = TypedValue.applyDimension(mUnit, mValue, metrics); + final int res = (int)(f+0.5f); + if (res != 0) return res; + if (mValue == 0) return 0; + if (mValue > 0) return 1; + return -1; + } + } + + // ======================================================= + // INFLATING LAYOUTS + // ======================================================= + + private class XMLInstanceDetails { + public final ResourceNames resNames; + public final String variant; + public final CopyOnWriteSortedSet callbacks; + public final XResources res = XResources.this; + + private XMLInstanceDetails(ResourceNames resNames, String variant, CopyOnWriteSortedSet callbacks) { + this.resNames = resNames; + this.variant = variant; + this.callbacks = callbacks; + } + } + + /** + * Hook the inflation of a layout. + * + * @param id The ID of the resource which should be replaced. + * @param callback The callback to be executed when the layout has been inflated. + * @return An object which can be used to remove the callback again. + */ + public XC_LayoutInflated.Unhook hookLayout(int id, XC_LayoutInflated callback) { + return hookLayoutInternal(mResDir, id, getResourceNames(id), callback); + } + + /** + * Hook the inflation of a layout. + * + * @deprecated Use {@link #hookLayout(String, String, String, XC_LayoutInflated)} instead. + * + * @param fullName The full resource name, e.g. {@code com.android.systemui:layout/statusbar}. + * See {@link #getResourceName}. + * @param callback The callback to be executed when the layout has been inflated. + * @return An object which can be used to remove the callback again. + */ + @Deprecated + public XC_LayoutInflated.Unhook hookLayout(String fullName, XC_LayoutInflated callback) { + int id = getIdentifier(fullName, null, null); + if (id == 0) + throw new NotFoundException(fullName); + return hookLayout(id, callback); + } + + /** + * Hook the inflation of a layout. + * + * @param pkg The package name, e.g. {@code com.android.systemui}. + * See {@link #getResourcePackageName}. + * @param type The type name, e.g. {@code layout}. + * See {@link #getResourceTypeName}. + * @param name The entry name, e.g. {@code statusbar}. + * See {@link #getResourceEntryName}. + * @param callback The callback to be executed when the layout has been inflated. + * @return An object which can be used to remove the callback again. + */ + public XC_LayoutInflated.Unhook hookLayout(String pkg, String type, String name, XC_LayoutInflated callback) { + int id = getIdentifier(name, type, pkg); + if (id == 0) + throw new NotFoundException(pkg + ":" + type + "/" + name); + return hookLayout(id, callback); + } + + /** + * Hook the inflation of an Android framework layout (in the {@code android} package). + * See {@link #hookSystemWideLayout(String, String, String, XC_LayoutInflated)}. + * + * @param id The ID of the resource which should be replaced. + * @param callback The callback to be executed when the layout has been inflated. + * @return An object which can be used to remove the callback again. + */ + public static XC_LayoutInflated.Unhook hookSystemWideLayout(int id, XC_LayoutInflated callback) { + if (id >= 0x7f000000) + throw new IllegalArgumentException("ids >= 0x7f000000 are app specific and cannot be set for the framework"); + return hookLayoutInternal(null, id, getSystemResourceNames(id), callback); + } + + /** + * Hook the inflation of an Android framework layout (in the {@code android} package). + * See {@link #hookSystemWideLayout(String, String, String, XC_LayoutInflated)}. + * + * @deprecated Use {@link #hookSystemWideLayout(String, String, String, XC_LayoutInflated)} instead. + * + * @param fullName The full resource name, e.g. {@code android:layout/simple_list_item_1}. + * See {@link #getResourceName}. + * @param callback The callback to be executed when the layout has been inflated. + * @return An object which can be used to remove the callback again. + */ + @Deprecated + public static XC_LayoutInflated.Unhook hookSystemWideLayout(String fullName, XC_LayoutInflated callback) { + int id = getSystem().getIdentifier(fullName, null, null); + if (id == 0) + throw new NotFoundException(fullName); + return hookSystemWideLayout(id, callback); + } + + /** + * Hook the inflation of an Android framework layout (in the {@code android} package). + * + *

Some layouts are part of the Android framework and can be used in any app. They're + * accessible via {@link android.R.layout android.R.layout} and are not bound to a specific + * {@link android.content.res.Resources} instance. Such resources can be replaced in + * {@link IXposedHookZygoteInit#initZygote initZygote()} for all apps. As there is no + * {@link XResources} object easily available in that scope, this static method can be used + * to hook layouts. + * + * @param pkg The package name, e.g. {@code android}. + * See {@link #getResourcePackageName}. + * @param type The type name, e.g. {@code layout}. + * See {@link #getResourceTypeName}. + * @param name The entry name, e.g. {@code simple_list_item_1}. + * See {@link #getResourceEntryName}. + * @param callback The callback to be executed when the layout has been inflated. + * @return An object which can be used to remove the callback again. + */ + public static XC_LayoutInflated.Unhook hookSystemWideLayout(String pkg, String type, String name, XC_LayoutInflated callback) { + int id = getSystem().getIdentifier(name, type, pkg); + if (id == 0) + throw new NotFoundException(pkg + ":" + type + "/" + name); + return hookSystemWideLayout(id, callback); + } + + private static XC_LayoutInflated.Unhook hookLayoutInternal(String resDir, int id, ResourceNames resNames, XC_LayoutInflated callback) { + if (id == 0) + throw new IllegalArgumentException("id 0 is not an allowed resource identifier"); + + HashMap> inner; + synchronized (sLayoutCallbacks) { + inner = sLayoutCallbacks.get(id); + if (inner == null) { + inner = new HashMap<>(); + sLayoutCallbacks.put(id, inner); + } + } + + CopyOnWriteSortedSet callbacks; + synchronized (inner) { + callbacks = inner.get(resDir); + if (callbacks == null) { + callbacks = new CopyOnWriteSortedSet<>(); + inner.put(resDir, callbacks); + } + } + + callbacks.add(callback); + + putResourceNames(resDir, resNames); + + return callback.new Unhook(resDir, id); + } + + /** @hide */ + public static void unhookLayout(String resDir, int id, XC_LayoutInflated callback) { + HashMap> inner; + synchronized (sLayoutCallbacks) { + inner = sLayoutCallbacks.get(id); + if (inner == null) + return; + } + + CopyOnWriteSortedSet callbacks; + synchronized (inner) { + callbacks = inner.get(resDir); + if (callbacks == null) + return; + } + + callbacks.remove(callback); + } +} diff --git a/Bridge/src/main/java/android/content/res/package-info.java b/Bridge/src/main/java/android/content/res/package-info.java new file mode 100644 index 00000000..4016398e --- /dev/null +++ b/Bridge/src/main/java/android/content/res/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes that are required for replacing resources. + */ +package android.content.res; diff --git a/Bridge/src/main/java/com/elderdrivers/riru/common/KeepAll.java b/Bridge/src/main/java/com/elderdrivers/riru/common/KeepAll.java new file mode 100644 index 00000000..173ad0b9 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/common/KeepAll.java @@ -0,0 +1,4 @@ +package com.elderdrivers.riru.common; + +public interface KeepAll { +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/common/KeepMembers.java b/Bridge/src/main/java/com/elderdrivers/riru/common/KeepMembers.java new file mode 100644 index 00000000..6f83c576 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/common/KeepMembers.java @@ -0,0 +1,4 @@ +package com.elderdrivers.riru.common; + +public interface KeepMembers { +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/Main.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/Main.java new file mode 100644 index 00000000..cac9a190 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/Main.java @@ -0,0 +1,83 @@ +package com.elderdrivers.riru.xposed; + +import android.annotation.SuppressLint; +import android.os.Build; + +import com.elderdrivers.riru.common.KeepAll; +import com.elderdrivers.riru.xposed.core.HookMethodResolver; +import com.elderdrivers.riru.xposed.entry.Router; + +import java.lang.reflect.Method; + +@SuppressLint("DefaultLocale") +public class Main implements KeepAll { + + // private static String sForkAndSpecializePramsStr = ""; +// private static String sForkSystemServerPramsStr = ""; + public static String sAppDataDir = ""; + + static { + init(Build.VERSION.SDK_INT); + HookMethodResolver.init(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // entry points + /////////////////////////////////////////////////////////////////////////////////////////////// + + @Deprecated + public static void forkAndSpecializePre(int uid, int gid, int[] gids, int debugFlags, + int[][] rlimits, int mountExternal, String seInfo, + String niceName, int[] fdsToClose, int[] fdsToIgnore, + boolean startChildZygote, String instructionSet, String appDataDir) { +// sForkAndSpecializePramsStr = String.format( +// "Zygote#forkAndSpecialize(%d, %d, %s, %d, %s, %d, %s, %s, %s, %s, %s, %s, %s)", +// uid, gid, Arrays.toString(gids), debugFlags, Arrays.toString(rlimits), +// mountExternal, seInfo, niceName, Arrays.toString(fdsToClose), +// Arrays.toString(fdsToIgnore), startChildZygote, instructionSet, appDataDir); + } + + public static void forkAndSpecializePost(int pid, String appDataDir) { +// Utils.logD(sForkAndSpecializePramsStr + " = " + pid); + if (pid == 0) { + // in app process + sAppDataDir = appDataDir; + Router.onProcessForked(false); + } else { + // in zygote process, res is child zygote pid + // don't print log here, see https://github.com/RikkaApps/Riru/blob/77adfd6a4a6a81bfd20569c910bc4854f2f84f5e/riru-core/jni/main/jni_native_method.cpp#L55-L66 + } + } + + public static void forkSystemServerPre(int uid, int gid, int[] gids, int debugFlags, int[][] rlimits, + long permittedCapabilities, long effectiveCapabilities) { +// sForkSystemServerPramsStr = String.format("Zygote#forkSystemServer(%d, %d, %s, %d, %s, %d, %d)", +// uid, gid, Arrays.toString(gids), debugFlags, Arrays.toString(rlimits), +// permittedCapabilities, effectiveCapabilities); + } + + public static void forkSystemServerPost(int pid) { +// Utils.logD(sForkSystemServerPramsStr + " = " + pid); + if (pid == 0) { + // in system_server process + sAppDataDir = "/data/data/android/"; + Router.onProcessForked(true); + } else { + // in zygote process, res is child zygote pid + // don't print log here, see https://github.com/RikkaApps/Riru/blob/77adfd6a4a6a81bfd20569c910bc4854f2f84f5e/riru-core/jni/main/jni_native_method.cpp#L55-L66 + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // native methods + /////////////////////////////////////////////////////////////////////////////////////////////// + + public static native boolean backupAndHookNative(Object target, Method hook, Method backup); + + public static native void ensureMethodCached(Method hook, Method backup); + + // JNI.ToReflectedMethod() could return either Method or Constructor + public static native Object findMethodNative(Class targetClass, String methodName, String methodSig); + + private static native void init(int SDK_version); +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/core/HookMain.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/core/HookMain.java new file mode 100644 index 00000000..db1188c7 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/core/HookMain.java @@ -0,0 +1,167 @@ +package com.elderdrivers.riru.xposed.core; + +import com.elderdrivers.riru.xposed.util.Utils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; + +import static com.elderdrivers.riru.xposed.Main.backupAndHookNative; +import static com.elderdrivers.riru.xposed.Main.findMethodNative; + +public class HookMain { + + public static void doHookDefault(ClassLoader patchClassLoader, ClassLoader originClassLoader, String hookInfoClassName) { + try { + Class hookInfoClass = Class.forName(hookInfoClassName, true, patchClassLoader); + String[] hookItemNames = (String[]) hookInfoClass.getField("hookItemNames").get(null); + for (String hookItemName : hookItemNames) { + doHookItemDefault(patchClassLoader, hookItemName, originClassLoader); + } + } catch (Throwable e) { + Utils.logE("error when hooking all in: " + hookInfoClassName, e); + } + } + + private static void doHookItemDefault(ClassLoader patchClassLoader, String hookItemName, ClassLoader originClassLoader) { + try { + Utils.logD("Start hooking with item " + hookItemName); + Class hookItem = Class.forName(hookItemName, true, patchClassLoader); + + String className = (String) hookItem.getField("className").get(null); + String methodName = (String) hookItem.getField("methodName").get(null); + String methodSig = (String) hookItem.getField("methodSig").get(null); + + if (className == null || className.equals("")) { + Utils.logW("No target class. Skipping..."); + return; + } + Class clazz = null; + try { + clazz = Class.forName(className, true, originClassLoader); + } catch (ClassNotFoundException cnfe) { + Utils.logE(className + " not found in " + originClassLoader); + return; + } + if (Modifier.isAbstract(clazz.getModifiers())) { + Utils.logW("Hook may fail for abstract class: " + className); + } + + Method hook = null; + Method backup = null; + for (Method method : hookItem.getDeclaredMethods()) { + if (method.getName().equals("hook") && Modifier.isStatic(method.getModifiers())) { + hook = method; + } else if (method.getName().equals("backup") && Modifier.isStatic(method.getModifiers())) { + backup = method; + } + } + if (hook == null) { + Utils.logE("Cannot find hook for " + methodName); + return; + } + findAndBackupAndHook(clazz, methodName, methodSig, hook, backup); + } catch (Throwable e) { + Utils.logE("error when hooking " + hookItemName, e); + } + } + + public static void findAndHook(Class targetClass, String methodName, String methodSig, Method hook) { + hook(findMethod(targetClass, methodName, methodSig), hook); + } + + public static void findAndBackupAndHook(Class targetClass, String methodName, String methodSig, + Method hook, Method backup) { + backupAndHook(findMethod(targetClass, methodName, methodSig), hook, backup); + } + + public static void hook(Object target, Method hook) { + backupAndHook(target, hook, null); + } + + public static void backupAndHook(Object target, Method hook, Method backup) { + if (target == null) { + throw new IllegalArgumentException("null target method"); + } + if (hook == null) { + throw new IllegalArgumentException("null hook method"); + } + + if (!Modifier.isStatic(hook.getModifiers())) { + throw new IllegalArgumentException("Hook must be a static method: " + hook); + } + checkCompatibleMethods(target, hook, "Original", "Hook"); + if (backup != null) { + if (!Modifier.isStatic(backup.getModifiers())) { + throw new IllegalArgumentException("Backup must be a static method: " + backup); + } + // backup is just a placeholder and the constraint could be less strict + checkCompatibleMethods(target, backup, "Original", "Backup"); + } + if (backup != null) { + HookMethodResolver.resolveMethod(hook, backup); + } + if (!backupAndHookNative(target, hook, backup)) { + throw new RuntimeException("Failed to hook " + target + " with " + hook); + } + } + + private static Object findMethod(Class cls, String methodName, String methodSig) { + if (cls == null) { + throw new IllegalArgumentException("null class"); + } + if (methodName == null) { + throw new IllegalArgumentException("null method name"); + } + if (methodSig == null) { + throw new IllegalArgumentException("null method signature"); + } + return findMethodNative(cls, methodName, methodSig); + } + + private static void checkCompatibleMethods(Object original, Method replacement, String originalName, String replacementName) { + ArrayList> originalParams; + if (original instanceof Method) { + originalParams = new ArrayList<>(Arrays.asList(((Method) original).getParameterTypes())); + } else if (original instanceof Constructor) { + originalParams = new ArrayList<>(Arrays.asList(((Constructor) original).getParameterTypes())); + } else { + throw new IllegalArgumentException("Type of target method is wrong"); + } + + ArrayList> replacementParams = new ArrayList<>(Arrays.asList(replacement.getParameterTypes())); + + if (original instanceof Method + && !Modifier.isStatic(((Method) original).getModifiers())) { + originalParams.add(0, ((Method) original).getDeclaringClass()); + } else if (original instanceof Constructor) { + originalParams.add(0, ((Constructor) original).getDeclaringClass()); + } + + + if (!Modifier.isStatic(replacement.getModifiers())) { + replacementParams.add(0, replacement.getDeclaringClass()); + } + + if (original instanceof Method + && !replacement.getReturnType().isAssignableFrom(((Method) original).getReturnType())) { + throw new IllegalArgumentException("Incompatible return types. " + originalName + ": " + ((Method) original).getReturnType() + ", " + replacementName + ": " + replacement.getReturnType()); + } else if (original instanceof Constructor) { + if (replacement.getReturnType().equals(Void.class)) { + throw new IllegalArgumentException("Incompatible return types. " + "" + ": " + "V" + ", " + replacementName + ": " + replacement.getReturnType()); + } + } + + if (originalParams.size() != replacementParams.size()) { + throw new IllegalArgumentException("Number of arguments don't match. " + originalName + ": " + originalParams.size() + ", " + replacementName + ": " + replacementParams.size()); + } + + for (int i = 0; i < originalParams.size(); i++) { + if (!replacementParams.get(i).isAssignableFrom(originalParams.get(i))) { + throw new IllegalArgumentException("Incompatible argument #" + i + ": " + originalName + ": " + originalParams.get(i) + ", " + replacementName + ": " + replacementParams.get(i)); + } + } + } +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/core/HookMethodResolver.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/core/HookMethodResolver.java new file mode 100644 index 00000000..ab97aba8 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/core/HookMethodResolver.java @@ -0,0 +1,156 @@ +package com.elderdrivers.riru.xposed.core; + +import android.os.Build; +import android.util.Log; + +import com.elderdrivers.riru.xposed.Main; +import com.elderdrivers.riru.xposed.util.Utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * create by Swift Gan on 14/01/2019 + * To ensure method in resolved cache + */ + +public class HookMethodResolver { + + public static Class artMethodClass; + + public static Field resolvedMethodsField; + public static Field dexCacheField; + public static Field dexMethodIndexField; + public static Field artMethodField; + + public static boolean canResolvedInJava = false; + public static boolean isArtMethod = false; + + public static long resolvedMethodsAddress = 0; + public static int dexMethodIndex = 0; + + public static Method testMethod; + public static Object testArtMethod; + + public static void init() { + checkSupport(); + } + + private static void checkSupport() { + try { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + isArtMethod = false; + canResolvedInJava = false; + return; + } + + testMethod = HookMethodResolver.class.getDeclaredMethod("init"); + artMethodField = getField(Method.class, "artMethod"); + + testArtMethod = artMethodField.get(testMethod); + + if (hasJavaArtMethod() && testArtMethod.getClass() == artMethodClass) { + checkSupportForArtMethod(); + isArtMethod = true; + } else if (testArtMethod instanceof Long) { + checkSupportForArtMethodId(); + isArtMethod = false; + } else { + canResolvedInJava = false; + } + + } catch (Throwable throwable) { + Utils.logE("error when checkSupport", throwable); + } + } + + // may 5.0 + private static void checkSupportForArtMethod() throws Exception { + dexMethodIndexField = getField(artMethodClass, "dexMethodIndex"); + dexCacheField = getField(Class.class, "dexCache"); + Object dexCache = dexCacheField.get(testMethod.getDeclaringClass()); + resolvedMethodsField = getField(dexCache.getClass(), "resolvedMethods"); + if (resolvedMethodsField.get(dexCache) instanceof Object[]) { + canResolvedInJava = true; + } + } + + // may 6.0 + private static void checkSupportForArtMethodId() throws Exception { + dexMethodIndexField = getField(Method.class, "dexMethodIndex"); + dexMethodIndex = (int) dexMethodIndexField.get(testMethod); + dexCacheField = getField(Class.class, "dexCache"); + Object dexCache = dexCacheField.get(testMethod.getDeclaringClass()); + resolvedMethodsField = getField(dexCache.getClass(), "resolvedMethods"); + Object resolvedMethods = resolvedMethodsField.get(dexCache); + if (resolvedMethods instanceof Long) { + canResolvedInJava = false; + resolvedMethodsAddress = (long) resolvedMethods; + } else if (resolvedMethods instanceof long[]) { + canResolvedInJava = true; + } + } + + public static void resolveMethod(Method hook, Method backup) { + if (canResolvedInJava && artMethodField != null) { + // in java + try { + resolveInJava(hook, backup); + } catch (Exception e) { + // in native + resolveInNative(hook, backup); + } + } else { + // in native + resolveInNative(hook, backup); + } + } + + private static void resolveInJava(Method hook, Method backup) throws Exception { + Object dexCache = dexCacheField.get(hook.getDeclaringClass()); + if (isArtMethod) { + Object artMethod = artMethodField.get(backup); + int dexMethodIndex = (int) dexMethodIndexField.get(artMethod); + Object resolvedMethods = resolvedMethodsField.get(dexCache); + ((Object[])resolvedMethods)[dexMethodIndex] = artMethod; + } else { + int dexMethodIndex = (int) dexMethodIndexField.get(backup); + Object resolvedMethods = resolvedMethodsField.get(dexCache); + long artMethod = (long) artMethodField.get(backup); + ((long[])resolvedMethods)[dexMethodIndex] = artMethod; + } + } + + private static void resolveInNative(Method hook, Method backup) { + Main.ensureMethodCached(hook, backup); + } + + public static Field getField(Class topClass, String fieldName) throws NoSuchFieldException { + while (topClass != null && topClass != Object.class) { + try { + Field field = topClass.getDeclaredField(fieldName); + field.setAccessible(true); + return field; + } catch (Exception e) { + } + topClass = topClass.getSuperclass(); + } + throw new NoSuchFieldException(fieldName); + } + + public static boolean hasJavaArtMethod() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return false; + } + if (artMethodClass != null) + return true; + try { + artMethodClass = Class.forName("java.lang.reflect.ArtMethod"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + +} \ No newline at end of file diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DexLog.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DexLog.java new file mode 100644 index 00000000..3802e49c --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DexLog.java @@ -0,0 +1,37 @@ +package com.elderdrivers.riru.xposed.dexmaker; + +import android.util.Log; + +import com.elderdrivers.riru.xposed.BuildConfig; + +public class DexLog { + + public static final String TAG = "EdXposed-dexmaker"; + + public static int v(String s) { + return Log.v(TAG, s); + } + + public static int i(String s) { + return Log.i(TAG, s); + } + + public static int d(String s) { + if (BuildConfig.DEBUG) { + return Log.d(TAG, s); + } + return 0; + } + + public static int w(String s) { + return Log.w(TAG, s); + } + + public static int e(String s) { + return Log.e(TAG, s); + } + + public static int e(String s, Throwable t) { + return Log.e(TAG, s, t); + } +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DexMakerUtils.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DexMakerUtils.java new file mode 100644 index 00000000..94374a98 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DexMakerUtils.java @@ -0,0 +1,186 @@ +package com.elderdrivers.riru.xposed.dexmaker; + +import external.com.android.dx.Code; +import external.com.android.dx.Local; +import external.com.android.dx.TypeId; + +import java.util.HashMap; +import java.util.Map; + +public class DexMakerUtils { + + public static void autoBoxIfNecessary(Code code, Local target, Local source) { + String boxMethod = "valueOf"; + TypeId boxTypeId; + TypeId typeId = source.getType(); + if (typeId.equals(TypeId.BOOLEAN)) { + boxTypeId = TypeId.get(Boolean.class); + code.invokeStatic(boxTypeId.getMethod(boxTypeId, boxMethod, TypeId.BOOLEAN), target, source); + } else if (typeId.equals(TypeId.BYTE)) { + boxTypeId = TypeId.get(Byte.class); + code.invokeStatic(boxTypeId.getMethod(boxTypeId, boxMethod, TypeId.BYTE), target, source); + } else if (typeId.equals(TypeId.CHAR)) { + boxTypeId = TypeId.get(Character.class); + code.invokeStatic(boxTypeId.getMethod(boxTypeId, boxMethod, TypeId.CHAR), target, source); + } else if (typeId.equals(TypeId.DOUBLE)) { + boxTypeId = TypeId.get(Double.class); + code.invokeStatic(boxTypeId.getMethod(boxTypeId, boxMethod, TypeId.DOUBLE), target, source); + } else if (typeId.equals(TypeId.FLOAT)) { + boxTypeId = TypeId.get(Float.class); + code.invokeStatic(boxTypeId.getMethod(boxTypeId, boxMethod, TypeId.FLOAT), target, source); + } else if (typeId.equals(TypeId.INT)) { + boxTypeId = TypeId.get(Integer.class); + code.invokeStatic(boxTypeId.getMethod(boxTypeId, boxMethod, TypeId.INT), target, source); + } else if (typeId.equals(TypeId.LONG)) { + boxTypeId = TypeId.get(Long.class); + code.invokeStatic(boxTypeId.getMethod(boxTypeId, boxMethod, TypeId.LONG), target, source); + } else if (typeId.equals(TypeId.SHORT)) { + boxTypeId = TypeId.get(Short.class); + code.invokeStatic(boxTypeId.getMethod(boxTypeId, boxMethod, TypeId.SHORT), target, source); + } else if (typeId.equals(TypeId.VOID)) { + code.loadConstant(target, null); + } else { + code.move(target, source); + } + } + + public static void autoUnboxIfNecessary(Code code, Local target, Local source) { + String unboxMethod; + TypeId typeId = target.getType(); + TypeId boxTypeId; + if (typeId.equals(TypeId.BOOLEAN)) { + unboxMethod = "booleanValue"; + boxTypeId = TypeId.get(Boolean.class); + code.invokeVirtual(boxTypeId.getMethod(TypeId.BOOLEAN, unboxMethod), target, source); + } else if (typeId.equals(TypeId.BYTE)) { + unboxMethod = "byteValue"; + boxTypeId = TypeId.get(Byte.class); + code.invokeVirtual(boxTypeId.getMethod(TypeId.BYTE, unboxMethod), target, source); + } else if (typeId.equals(TypeId.CHAR)) { + unboxMethod = "charValue"; + boxTypeId = TypeId.get(Character.class); + code.invokeVirtual(boxTypeId.getMethod(TypeId.CHAR, unboxMethod), target, source); + } else if (typeId.equals(TypeId.DOUBLE)) { + unboxMethod = "doubleValue"; + boxTypeId = TypeId.get(Double.class); + code.invokeVirtual(boxTypeId.getMethod(TypeId.DOUBLE, unboxMethod), target, source); + } else if (typeId.equals(TypeId.FLOAT)) { + unboxMethod = "floatValue"; + boxTypeId = TypeId.get(Float.class); + code.invokeVirtual(boxTypeId.getMethod(TypeId.FLOAT, unboxMethod), target, source); + } else if (typeId.equals(TypeId.INT)) { + unboxMethod = "intValue"; + boxTypeId = TypeId.get(Integer.class); + code.invokeVirtual(boxTypeId.getMethod(TypeId.INT, unboxMethod), target, source); + } else if (typeId.equals(TypeId.LONG)) { + unboxMethod = "longValue"; + boxTypeId = TypeId.get(Long.class); + code.invokeVirtual(boxTypeId.getMethod(TypeId.LONG, unboxMethod), target, source); + } else if (typeId.equals(TypeId.SHORT)) { + unboxMethod = "shortValue"; + boxTypeId = TypeId.get(Short.class); + code.invokeVirtual(boxTypeId.getMethod(TypeId.SHORT, unboxMethod), target, source); + } else if (typeId.equals(TypeId.VOID)) { + code.loadConstant(target, null); + } else { + code.move(target, source); + } + } + + public static Map createResultLocals(Code code) { + HashMap resultMap = new HashMap<>(); + Local booleanLocal = code.newLocal(TypeId.BOOLEAN); + Local byteLocal = code.newLocal(TypeId.BYTE); + Local charLocal = code.newLocal(TypeId.CHAR); + Local doubleLocal = code.newLocal(TypeId.DOUBLE); + Local floatLocal = code.newLocal(TypeId.FLOAT); + Local intLocal = code.newLocal(TypeId.INT); + Local longLocal = code.newLocal(TypeId.LONG); + Local shortLocal = code.newLocal(TypeId.SHORT); + Local voidLocal = code.newLocal(TypeId.VOID); + Local objectLocal = code.newLocal(TypeId.OBJECT); + + Local booleanObjLocal = code.newLocal(TypeId.get("Ljava/lang/Boolean;")); + Local byteObjLocal = code.newLocal(TypeId.get("Ljava/lang/Byte;")); + Local charObjLocal = code.newLocal(TypeId.get("Ljava/lang/Character;")); + Local doubleObjLocal = code.newLocal(TypeId.get("Ljava/lang/Double;")); + Local floatObjLocal = code.newLocal(TypeId.get("Ljava/lang/Float;")); + Local intObjLocal = code.newLocal(TypeId.get("Ljava/lang/Integer;")); + Local longObjLocal = code.newLocal(TypeId.get("Ljava/lang/Long;")); + Local shortObjLocal = code.newLocal(TypeId.get("Ljava/lang/Short;")); + Local voidObjLocal = code.newLocal(TypeId.get("Ljava/lang/Void;")); + + // backup need initialized locals + code.loadConstant(booleanLocal, Boolean.valueOf(false)); + code.loadConstant(byteLocal, Byte.valueOf("0")); + code.loadConstant(charLocal, Character.valueOf('\0')); + code.loadConstant(floatLocal, Float.valueOf(0)); + code.loadConstant(intLocal, 0); + code.loadConstant(longLocal, Long.valueOf(0)); + code.loadConstant(shortLocal, Short.valueOf("0")); + code.loadConstant(voidLocal, null); + code.loadConstant(objectLocal, null); + // all to null + code.loadConstant(booleanObjLocal, null); + code.loadConstant(byteObjLocal, null); + code.loadConstant(charObjLocal, null); + code.loadConstant(floatObjLocal, null); + code.loadConstant(intObjLocal, null); + code.loadConstant(longObjLocal, null); + code.loadConstant(shortObjLocal, null); + code.loadConstant(voidObjLocal, null); + // package all + resultMap.put(TypeId.BOOLEAN, booleanLocal); + resultMap.put(TypeId.BYTE, byteLocal); + resultMap.put(TypeId.CHAR, charLocal); + resultMap.put(TypeId.DOUBLE, doubleLocal); + resultMap.put(TypeId.FLOAT, floatLocal); + resultMap.put(TypeId.INT, intLocal); + resultMap.put(TypeId.LONG, longLocal); + resultMap.put(TypeId.SHORT, shortLocal); + resultMap.put(TypeId.VOID, voidLocal); + resultMap.put(TypeId.OBJECT, objectLocal); + + resultMap.put(TypeId.get("Ljava/lang/Boolean;"), booleanObjLocal); + resultMap.put(TypeId.get("Ljava/lang/Byte;"), byteObjLocal); + resultMap.put(TypeId.get("Ljava/lang/Character;"), charObjLocal); + resultMap.put(TypeId.get("Ljava/lang/Double;"), doubleObjLocal); + resultMap.put(TypeId.get("Ljava/lang/Float;"), floatObjLocal); + resultMap.put(TypeId.get("Ljava/lang/Integer;"), intObjLocal); + resultMap.put(TypeId.get("Ljava/lang/Long;"), longObjLocal); + resultMap.put(TypeId.get("Ljava/lang/Short;"), shortObjLocal); + resultMap.put(TypeId.get("Ljava/lang/Void;"), voidObjLocal); + + return resultMap; + } + + public static TypeId getObjTypeIdIfPrimitive(TypeId typeId) { + if (typeId.equals(TypeId.BOOLEAN)) { + return TypeId.get("Ljava/lang/Boolean;"); + } else if (typeId.equals(TypeId.BYTE)) { + return TypeId.get("Ljava/lang/Byte;"); + } else if (typeId.equals(TypeId.CHAR)) { + return TypeId.get("Ljava/lang/Character;"); + } else if (typeId.equals(TypeId.DOUBLE)) { + return TypeId.get("Ljava/lang/Double;"); + } else if (typeId.equals(TypeId.FLOAT)) { + return TypeId.get("Ljava/lang/Float;"); + } else if (typeId.equals(TypeId.INT)) { + return TypeId.get("Ljava/lang/Integer;"); + } else if (typeId.equals(TypeId.LONG)) { + return TypeId.get("Ljava/lang/Long;"); + } else if (typeId.equals(TypeId.SHORT)) { + return TypeId.get("Ljava/lang/Short;"); + } else if (typeId.equals(TypeId.VOID)) { + return TypeId.get("Ljava/lang/Void;"); + } else { + return typeId; + } + } + + public static void returnRightValue(Code code, Class returnType, Map resultLocals) { + String unboxMethod; + TypeId boxTypeId; + code.returnValue(resultLocals.get(returnType)); + } +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DynamicBridge.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DynamicBridge.java new file mode 100644 index 00000000..f8518e9c --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/DynamicBridge.java @@ -0,0 +1,107 @@ +package com.elderdrivers.riru.xposed.dexmaker; + +import android.app.AndroidAppHelper; +import android.os.Build; + +import com.elderdrivers.riru.xposed.Main; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; + +import de.robv.android.xposed.XposedBridge; + +public final class DynamicBridge { + + private static HashMap hookedInfo = new HashMap<>(); + + public static synchronized void hookMethod(Member hookMethod, XposedBridge.AdditionalHookInfo additionalHookInfo) { + + if (hookMethod.toString().contains("com.tencent.wcdb.database")) { + DexLog.w("wcdb not permitted."); + return; + } + if (!checkMember(hookMethod)) { + return; + } + + if (hookedInfo.containsKey(hookMethod)) { + DexLog.w("already hook method:" + hookMethod.toString()); + return; + } + + DexLog.d("start to generate class for: " + hookMethod); + try { + // for Android Oreo and later use InMemoryClassLoader + String dexDirPath = ""; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + // under Android Oreo, using DexClassLoader + String dataDir = Main.sAppDataDir; + String processName = AndroidAppHelper.currentProcessName(); + File dexDir = new File(dataDir, "cache/edhookers/" + processName + "/"); + dexDir.mkdirs(); + dexDirPath = dexDir.getAbsolutePath(); + } + HookerDexMaker dexMaker = new HookerDexMaker(); + dexMaker.start(hookMethod, additionalHookInfo, + hookMethod.getDeclaringClass().getClassLoader(), dexDirPath); + hookedInfo.put(hookMethod, dexMaker); + } catch (Exception e) { + DexLog.e("error occur when generating dex", e); + } + } + + private static boolean checkMember(Member member) { + + if (member instanceof Method) { + return true; + } else if (member instanceof Constructor) { + return true; + } else if (member.getDeclaringClass().isInterface()) { + DexLog.e("Cannot hook interfaces: " + member.toString()); + return false; + } else if (Modifier.isAbstract(member.getModifiers())) { + DexLog.e("Cannot hook abstract methods: " + member.toString()); + return false; + } else { + DexLog.e("Only methods and constructors can be hooked: " + member.toString()); + return false; + } + } + + public static Object invokeOriginalMethod(Member method, Object thisObject, Object[] args) + throws InvocationTargetException, IllegalAccessException { + HookerDexMaker dexMaker = hookedInfo.get(method); + if (dexMaker == null) { + throw new IllegalStateException("method not hooked, cannot call original method."); + } + Method callBackup = dexMaker.getCallBackupMethod(); + if (callBackup == null) { + throw new IllegalStateException("original method is null, something must be wrong!"); + } + if (!Modifier.isStatic(callBackup.getModifiers())) { + throw new IllegalStateException("original method is not static, something must be wrong!"); + } + callBackup.setAccessible(true); + if (args == null) { + args = new Object[0]; + } + final int argsSize = args.length; + if (Modifier.isStatic(method.getModifiers())) { + return callBackup.invoke(null, args); + } else { + Object[] newArgs = new Object[argsSize + 1]; + newArgs[0] = thisObject; + for (int i = 1; i < newArgs.length; i++) { + newArgs[i] = args[i - 1]; + } + return callBackup.invoke(null, newArgs); + } + } +} + + diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/HookerDexMaker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/HookerDexMaker.java new file mode 100644 index 00000000..da36f495 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/dexmaker/HookerDexMaker.java @@ -0,0 +1,540 @@ +package com.elderdrivers.riru.xposed.dexmaker; + +import android.os.Build; +import android.text.TextUtils; + +import external.com.android.dx.BinaryOp; +import external.com.android.dx.Code; +import external.com.android.dx.Comparison; +import external.com.android.dx.DexMaker; +import external.com.android.dx.FieldId; +import external.com.android.dx.Label; +import external.com.android.dx.Local; +import external.com.android.dx.MethodId; +import external.com.android.dx.TypeId; +import com.elderdrivers.riru.xposed.core.HookMain; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import dalvik.system.InMemoryDexClassLoader; +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; + +import static com.elderdrivers.riru.xposed.dexmaker.DexMakerUtils.autoBoxIfNecessary; +import static com.elderdrivers.riru.xposed.dexmaker.DexMakerUtils.autoUnboxIfNecessary; +import static com.elderdrivers.riru.xposed.dexmaker.DexMakerUtils.createResultLocals; +import static com.elderdrivers.riru.xposed.dexmaker.DexMakerUtils.getObjTypeIdIfPrimitive; + +public class HookerDexMaker { + + public static final String METHOD_NAME_BACKUP = "backup"; + public static final String METHOD_NAME_HOOK = "hook"; + public static final String METHOD_NAME_CALL_BACKUP = "callBackup"; + public static final String METHOD_NAME_SETUP = "setup"; + private static final String CLASS_DESC_PREFIX = "L"; + private static final String CLASS_NAME_PREFIX = "EdHooker"; + private static final String FIELD_NAME_HOOK_INFO = "additionalHookInfo"; + private static final String FIELD_NAME_METHOD = "method"; + private static final String PARAMS_FIELD_NAME_METHOD = "method"; + private static final String PARAMS_FIELD_NAME_THIS_OBJECT = "thisObject"; + private static final String PARAMS_FIELD_NAME_ARGS = "args"; + private static final String CALLBACK_METHOD_NAME_BEFORE = "callBeforeHookedMethod"; + private static final String CALLBACK_METHOD_NAME_AFTER = "callAfterHookedMethod"; + private static final String PARAMS_METHOD_NAME_IS_EARLY_RETURN = "isEarlyReturn"; + + public static final TypeId objArrayTypeId = TypeId.get(Object[].class); + private static final TypeId throwableTypeId = TypeId.get(Throwable.class); + private static final TypeId memberTypeId = TypeId.get(Member.class); + private static final TypeId callbackTypeId = TypeId.get(XC_MethodHook.class); + private static final TypeId hookInfoTypeId + = TypeId.get(XposedBridge.AdditionalHookInfo.class); + private static final TypeId callbacksTypeId + = TypeId.get(XposedBridge.CopyOnWriteSortedSet.class); + private static final TypeId paramTypeId + = TypeId.get(XC_MethodHook.MethodHookParam.class); + private static final MethodId setResultMethodId = + paramTypeId.getMethod(TypeId.VOID, "setResult", TypeId.OBJECT); + private static final MethodId setThrowableMethodId = + paramTypeId.getMethod(TypeId.VOID, "setThrowable", throwableTypeId); + private static final MethodId getResultMethodId = + paramTypeId.getMethod(TypeId.OBJECT, "getResult"); + private static final MethodId getThrowableMethodId = + paramTypeId.getMethod(throwableTypeId, "getThrowable"); + private static final MethodId hasThrowableMethodId = + paramTypeId.getMethod(TypeId.BOOLEAN, "hasThrowable"); + private static final MethodId callAfterCallbackMethodId = + callbackTypeId.getMethod(TypeId.VOID, CALLBACK_METHOD_NAME_AFTER, paramTypeId); + private static final MethodId callBeforeCallbackMethodId = + callbackTypeId.getMethod(TypeId.VOID, CALLBACK_METHOD_NAME_BEFORE, paramTypeId); + private static final FieldId returnEarlyFieldId = + paramTypeId.getField(TypeId.BOOLEAN, "returnEarly"); + private static final TypeId xposedBridgeTypeId = TypeId.get(XposedBridge.class); + private static final MethodId logMethodId = + xposedBridgeTypeId.getMethod(TypeId.VOID, "log", throwableTypeId); + + private static AtomicLong sClassNameSuffix = new AtomicLong(0); + + private FieldId mHookInfoFieldId; + private FieldId mMethodFieldId; + private MethodId mBackupMethodId; + private MethodId mCallBackupMethodId; + private MethodId mHookMethodId; + + private TypeId mHookerTypeId; + private TypeId[] mParameterTypeIds; + private Class[] mActualParameterTypes; + private Class mReturnType; + private TypeId mReturnTypeId; + private boolean mIsStatic; + // TODO use this to generate methods + private boolean mHasThrowable; + + private DexMaker mDexMaker; + private Member mMember; + private XposedBridge.AdditionalHookInfo mHookInfo; + private ClassLoader mAppClassLoader; + private Class mHookClass; + private Method mHookMethod; + private Method mBackupMethod; + private Method mCallBackupMethod; + private String mDexDirPath; + + private static TypeId[] getParameterTypeIds(Class[] parameterTypes, boolean isStatic) { + int parameterSize = parameterTypes.length; + int targetParameterSize = isStatic ? parameterSize : parameterSize + 1; + TypeId[] parameterTypeIds = new TypeId[targetParameterSize]; + int offset = 0; + if (!isStatic) { + parameterTypeIds[0] = TypeId.OBJECT; + offset = 1; + } + for (int i = 0; i < parameterTypes.length; i++) { + parameterTypeIds[i + offset] = TypeId.get(parameterTypes[i]); + } + return parameterTypeIds; + } + + private static Class[] getParameterTypes(Class[] parameterTypes, boolean isStatic) { + if (isStatic) { + return parameterTypes; + } + int parameterSize = parameterTypes.length; + int targetParameterSize = parameterSize + 1; + Class[] newParameterTypes = new Class[targetParameterSize]; + int offset = 1; + newParameterTypes[0] = Object.class; + System.arraycopy(parameterTypes, 0, newParameterTypes, offset, parameterTypes.length); + return newParameterTypes; + } + + public void start(Member member, XposedBridge.AdditionalHookInfo hookInfo, + ClassLoader appClassLoader, String dexDirPath) throws Exception { + if (member instanceof Method) { + Method method = (Method) member; + mIsStatic = Modifier.isStatic(method.getModifiers()); + mReturnType = method.getReturnType(); + if (mReturnType.equals(Void.class) || mReturnType.equals(void.class) + || mReturnType.isPrimitive()) { + mReturnTypeId = TypeId.get(mReturnType); + } else { + // all others fallback to plain Object for convenience + mReturnType = Object.class; + mReturnTypeId = TypeId.OBJECT; + } + mParameterTypeIds = getParameterTypeIds(method.getParameterTypes(), mIsStatic); + mActualParameterTypes = getParameterTypes(method.getParameterTypes(), mIsStatic); + mHasThrowable = method.getExceptionTypes().length > 0; + } else if (member instanceof Constructor) { + Constructor constructor = (Constructor) member; + mIsStatic = false; + mReturnType = void.class; + mReturnTypeId = TypeId.VOID; + mParameterTypeIds = getParameterTypeIds(constructor.getParameterTypes(), mIsStatic); + mActualParameterTypes = getParameterTypes(constructor.getParameterTypes(), mIsStatic); + mHasThrowable = constructor.getExceptionTypes().length > 0; + } else if (member.getDeclaringClass().isInterface()) { + throw new IllegalArgumentException("Cannot hook interfaces: " + member.toString()); + } else if (Modifier.isAbstract(member.getModifiers())) { + throw new IllegalArgumentException("Cannot hook abstract methods: " + member.toString()); + } else { + throw new IllegalArgumentException("Only methods and constructors can be hooked: " + member.toString()); + } + mMember = member; + mHookInfo = hookInfo; + mDexDirPath = dexDirPath; + if (appClassLoader == null + || appClassLoader.getClass().getName().equals("java.lang.BootClassLoader")) { + mAppClassLoader = this.getClass().getClassLoader(); + } else { + mAppClassLoader = appClassLoader; + } + doMake(); + } + + private void doMake() throws Exception { + mDexMaker = new DexMaker(); + // Generate a Hooker class. + String className = CLASS_NAME_PREFIX + sClassNameSuffix.getAndIncrement(); + String classDesc = CLASS_DESC_PREFIX + className + ";"; + mHookerTypeId = TypeId.get(classDesc); + mDexMaker.declare(mHookerTypeId, className + ".generated", Modifier.PUBLIC, TypeId.OBJECT); + generateFields(); + generateSetupMethod(); + generateBackupMethod(); + generateHookMethod(); + generateCallBackupMethod(); + + ClassLoader loader; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // in memory dex classloader + byte[] dexBytes = mDexMaker.generate(); + loader = new InMemoryDexClassLoader(ByteBuffer.wrap(dexBytes), mAppClassLoader); + } else { + if (TextUtils.isEmpty(mDexDirPath)) { + throw new IllegalArgumentException("dexDirPath should not be empty!!!"); + } + // Create the dex file and load it. + loader = mDexMaker.generateAndLoad(mAppClassLoader, new File(mDexDirPath)); + } + + mHookClass = loader.loadClass(className); + // Execute our newly-generated code in-process. + mHookClass.getMethod(METHOD_NAME_SETUP, Member.class, XposedBridge.AdditionalHookInfo.class) + .invoke(null, mMember, mHookInfo); + mHookMethod = mHookClass.getMethod(METHOD_NAME_HOOK, mActualParameterTypes); + mBackupMethod = mHookClass.getMethod(METHOD_NAME_BACKUP, mActualParameterTypes); + mCallBackupMethod = mHookClass.getMethod(METHOD_NAME_CALL_BACKUP, mActualParameterTypes); + HookMain.backupAndHook(mMember, mHookMethod, mBackupMethod); + } + + public Method getHookMethod() { + return mHookMethod; + } + + public Method getBackupMethod() { + return mBackupMethod; + } + + public Method getCallBackupMethod() { + return mCallBackupMethod; + } + + public Class getHookClass() { + return mHookClass; + } + + private void generateFields() { + mHookInfoFieldId = mHookerTypeId.getField(hookInfoTypeId, FIELD_NAME_HOOK_INFO); + mMethodFieldId = mHookerTypeId.getField(memberTypeId, FIELD_NAME_METHOD); + mDexMaker.declare(mHookInfoFieldId, Modifier.STATIC, null); + mDexMaker.declare(mMethodFieldId, Modifier.STATIC, null); + } + + private void generateSetupMethod() { + MethodId setupMethodId = mHookerTypeId.getMethod( + TypeId.VOID, METHOD_NAME_SETUP, memberTypeId, hookInfoTypeId); + Code code = mDexMaker.declare(setupMethodId, Modifier.PUBLIC | Modifier.STATIC); + // init logic + // get parameters + Local method = code.getParameter(0, memberTypeId); + Local hookInfo = code.getParameter(1, hookInfoTypeId); + // save params to static + code.sput(mMethodFieldId, method); + code.sput(mHookInfoFieldId, hookInfo); + code.returnVoid(); + } + + private void generateBackupMethod() { + mBackupMethodId = mHookerTypeId.getMethod(mReturnTypeId, METHOD_NAME_BACKUP, mParameterTypeIds); + Code code = mDexMaker.declare(mBackupMethodId, Modifier.PUBLIC | Modifier.STATIC); + Map resultLocals = createResultLocals(code); + // do nothing + if (mReturnTypeId.equals(TypeId.VOID)) { + code.returnVoid(); + } else { + // we have limited the returnType to primitives or Object, so this should be safe + code.returnValue(resultLocals.get(mReturnTypeId)); + } + } + + private void generateCallBackupMethod() { + mCallBackupMethodId = mHookerTypeId.getMethod(mReturnTypeId, METHOD_NAME_CALL_BACKUP, mParameterTypeIds); + Code code = mDexMaker.declare(mCallBackupMethodId, Modifier.PUBLIC | Modifier.STATIC); + // just call backup and return its result + Local[] allArgsLocals = createParameterLocals(code); + Map resultLocals = createResultLocals(code); + if (mReturnTypeId.equals(TypeId.VOID)) { + code.invokeStatic(mBackupMethodId, null, allArgsLocals); + code.returnVoid(); + } else { + Local result = resultLocals.get(mReturnTypeId); + code.invokeStatic(mBackupMethodId, result, allArgsLocals); + code.returnValue(result); + } + } + + private void generateHookMethod() { + mHookMethodId = mHookerTypeId.getMethod(mReturnTypeId, METHOD_NAME_HOOK, mParameterTypeIds); + Code code = mDexMaker.declare(mHookMethodId, Modifier.PUBLIC | Modifier.STATIC); + + // code starts + + // prepare common labels + Label noHookReturn = new Label(); + Label incrementAndCheckBefore = new Label(); + Label tryBeforeCatch = new Label(); + Label noExceptionBefore = new Label(); + Label checkAndCallBackup = new Label(); + Label beginCallBefore = new Label(); + Label beginCallAfter = new Label(); + Label tryOrigCatch = new Label(); + Label noExceptionOrig = new Label(); + Label tryAfterCatch = new Label(); + Label decrementAndCheckAfter = new Label(); + Label noBackupThrowable = new Label(); + Label throwThrowable = new Label(); + // prepare locals + Local disableHooks = code.newLocal(TypeId.BOOLEAN); + Local hookInfo = code.newLocal(hookInfoTypeId); + Local callbacks = code.newLocal(callbacksTypeId); + Local snapshot = code.newLocal(objArrayTypeId); + Local snapshotLen = code.newLocal(TypeId.INT); + Local callbackObj = code.newLocal(TypeId.OBJECT); + Local callback = code.newLocal(callbackTypeId); + + Local resultObj = code.newLocal(TypeId.OBJECT); + Local one = code.newLocal(TypeId.INT); + Local nullObj = code.newLocal(TypeId.OBJECT); + Local throwable = code.newLocal(throwableTypeId); + + Local param = code.newLocal(paramTypeId); + Local method = code.newLocal(memberTypeId); + Local thisObject = code.newLocal(TypeId.OBJECT); + Local args = code.newLocal(objArrayTypeId); + Local returnEarly = code.newLocal(TypeId.BOOLEAN); + + Local actualParamSize = code.newLocal(TypeId.INT); + Local argIndex = code.newLocal(TypeId.INT); + + Local beforeIdx = code.newLocal(TypeId.INT); + Local lastResult = code.newLocal(TypeId.OBJECT); + Local lastThrowable = code.newLocal(throwableTypeId); + Local hasThrowable = code.newLocal(TypeId.BOOLEAN); + + Local[] allArgsLocals = createParameterLocals(code); + + Map resultLocals = createResultLocals(code); + + code.loadConstant(args, null); + code.loadConstant(argIndex, 0); + code.loadConstant(one, 1); + code.loadConstant(snapshotLen, 0); + code.loadConstant(nullObj, null); + + // check XposedBridge.disableHooks flag + + FieldId disableHooksField = + xposedBridgeTypeId.getField(TypeId.BOOLEAN, "disableHooks"); + code.sget(disableHooksField, disableHooks); + // disableHooks == true => no hooking + code.compareZ(Comparison.NE, noHookReturn, disableHooks); + + // check callbacks length + code.sget(mHookInfoFieldId, hookInfo); + code.iget(hookInfoTypeId.getField(callbacksTypeId, "callbacks"), callbacks, hookInfo); + code.invokeVirtual(callbacksTypeId.getMethod(objArrayTypeId, "getSnapshot"), snapshot, callbacks); + code.arrayLength(snapshotLen, snapshot); + // snapshotLen == 0 => no hooking + code.compareZ(Comparison.EQ, noHookReturn, snapshotLen); + + // start hooking + + // prepare hooking locals + int paramsSize = mParameterTypeIds.length; + int offset = 0; + // thisObject + if (mIsStatic) { + // thisObject = null + code.loadConstant(thisObject, null); + } else { + // thisObject = args[0] + offset = 1; + code.move(thisObject, allArgsLocals[0]); + } + // actual args (exclude thisObject if this is not a static method) + code.loadConstant(actualParamSize, paramsSize - offset); + code.newArray(args, actualParamSize); + for (int i = offset; i < paramsSize; i++) { + Local parameter = allArgsLocals[i]; + // save parameter to resultObj as Object + autoBoxIfNecessary(code, resultObj, parameter); + code.loadConstant(argIndex, i - offset); + // save Object to args + code.aput(args, argIndex, resultObj); + } + // create param + code.newInstance(param, paramTypeId.getConstructor()); + // set method, thisObject, args + code.sget(mMethodFieldId, method); + code.iput(paramTypeId.getField(memberTypeId, "method"), param, method); + code.iput(paramTypeId.getField(TypeId.OBJECT, "thisObject"), param, thisObject); + code.iput(paramTypeId.getField(objArrayTypeId, "args"), param, args); + + // call beforeCallbacks + code.loadConstant(beforeIdx, 0); + + code.mark(beginCallBefore); + // start of try + code.addCatchClause(throwableTypeId, tryBeforeCatch); + + code.aget(callbackObj, snapshot, beforeIdx); + code.cast(callback, callbackObj); + code.invokeVirtual(callBeforeCallbackMethodId, null, callback, param); + code.jump(noExceptionBefore); + + // end of try + code.removeCatchClause(throwableTypeId); + + // start of catch + code.mark(tryBeforeCatch); + code.moveException(throwable); + code.invokeStatic(logMethodId, null, throwable); + code.invokeVirtual(setResultMethodId, null, param, nullObj); + code.loadConstant(returnEarly, false); + code.iput(returnEarlyFieldId, param, returnEarly); + code.jump(incrementAndCheckBefore); + + // no exception when calling beforeCallbacks + code.mark(noExceptionBefore); + code.iget(returnEarlyFieldId, returnEarly, param); + // if returnEarly == false, continue + code.compareZ(Comparison.EQ, incrementAndCheckBefore, returnEarly); + // returnEarly == true, break + code.op(BinaryOp.ADD, beforeIdx, beforeIdx, one); + code.jump(checkAndCallBackup); + + // increment and check to continue + code.mark(incrementAndCheckBefore); + code.op(BinaryOp.ADD, beforeIdx, beforeIdx, one); + code.compare(Comparison.LT, beginCallBefore, beforeIdx, snapshotLen); + + // check and call backup + code.mark(checkAndCallBackup); + code.iget(returnEarlyFieldId, returnEarly, param); + // if returnEarly == true, go to call afterCallbacks directly + code.compareZ(Comparison.NE, noExceptionOrig, returnEarly); + // try to call backup + // try start + code.addCatchClause(throwableTypeId, tryOrigCatch); + // get pre-created Local with a matching typeId + if (mReturnTypeId.equals(TypeId.VOID)) { + code.invokeStatic(mBackupMethodId, null, allArgsLocals); + // TODO maybe keep preset result to do some magic? + code.invokeVirtual(setResultMethodId, null, param, nullObj); + } else { + Local returnedResult = resultLocals.get(mReturnTypeId); + code.invokeStatic(mBackupMethodId, returnedResult, allArgsLocals); + // save returnedResult to resultObj as a Object + autoBoxIfNecessary(code, resultObj, returnedResult); + // save resultObj to param + code.invokeVirtual(setResultMethodId, null, param, resultObj); + } + // go to call afterCallbacks + code.jump(noExceptionOrig); + // try end + code.removeCatchClause(throwableTypeId); + // catch + code.mark(tryOrigCatch); + code.moveException(throwable); + // exception occurred when calling backup, save throwable to param + code.invokeVirtual(setThrowableMethodId, null, param, throwable); + + code.mark(noExceptionOrig); + code.op(BinaryOp.SUBTRACT, beforeIdx, beforeIdx, one); + + // call afterCallbacks + code.mark(beginCallAfter); + // save results of backup calling + code.invokeVirtual(getResultMethodId, lastResult, param); + code.invokeVirtual(getThrowableMethodId, lastThrowable, param); + // try start + code.addCatchClause(throwableTypeId, tryAfterCatch); + code.aget(callbackObj, snapshot, beforeIdx); + code.cast(callback, callbackObj); + code.invokeVirtual(callAfterCallbackMethodId, null, callback, param); + // all good, just continue + code.jump(decrementAndCheckAfter); + // try end + code.removeCatchClause(throwableTypeId); + // catch + code.mark(tryAfterCatch); + code.moveException(throwable); + code.invokeStatic(logMethodId, null, throwable); + // if lastThrowable == null, go to recover lastResult + code.compareZ(Comparison.EQ, noBackupThrowable, lastThrowable); + // lastThrowable != null, recover lastThrowable + code.invokeVirtual(setThrowableMethodId, null, param, lastThrowable); + // continue + code.jump(decrementAndCheckAfter); + code.mark(noBackupThrowable); + // recover lastResult and continue + code.invokeVirtual(setResultMethodId, null, param, lastResult); + // decrement and check continue + code.mark(decrementAndCheckAfter); + code.op(BinaryOp.SUBTRACT, beforeIdx, beforeIdx, one); + code.compareZ(Comparison.GE, beginCallAfter, beforeIdx); + + // callbacks end + // return + code.invokeVirtual(hasThrowableMethodId, hasThrowable, param); + // if hasThrowable, throw the throwable and return + code.compareZ(Comparison.NE, throwThrowable, hasThrowable); + // return getResult + if (mReturnTypeId.equals(TypeId.VOID)) { + code.returnVoid(); + } else { + // getResult always return an Object, so save to resultObj + code.invokeVirtual(getResultMethodId, resultObj, param); + // have to unbox it if returnType is primitive + // casting Object + TypeId objTypeId = getObjTypeIdIfPrimitive(mReturnTypeId); + Local matchObjLocal = resultLocals.get(objTypeId); + code.cast(matchObjLocal, resultObj); + // have to use matching typed Object(Integer, Double ...) to do unboxing + Local toReturn = resultLocals.get(mReturnTypeId); + autoUnboxIfNecessary(code, toReturn, matchObjLocal); + // return + code.returnValue(toReturn); + } + // throw throwable + code.mark(throwThrowable); + code.invokeVirtual(getThrowableMethodId, throwable, param); + code.throwValue(throwable); + + // call backup and return + code.mark(noHookReturn); + if (mReturnTypeId.equals(TypeId.VOID)) { + code.invokeStatic(mBackupMethodId, null, allArgsLocals); + code.returnVoid(); + } else { + Local result = resultLocals.get(mReturnTypeId); + code.invokeStatic(mBackupMethodId, result, allArgsLocals); + code.returnValue(result); + } + } + + private Local[] createParameterLocals(Code code) { + Local[] paramLocals = new Local[mParameterTypeIds.length]; + for (int i = 0; i < mParameterTypeIds.length; i++) { + paramLocals[i] = code.getParameter(i, mParameterTypeIds[i]); + } + return paramLocals; + } +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/Router.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/Router.java new file mode 100644 index 00000000..d22c8356 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/Router.java @@ -0,0 +1,49 @@ +package com.elderdrivers.riru.xposed.entry; + +import com.elderdrivers.riru.xposed.core.HookMain; +import com.elderdrivers.riru.xposed.entry.bootstrap.AppBootstrapHookInfo; +import com.elderdrivers.riru.xposed.entry.bootstrap.SysBootstrapHookInfo; +import com.elderdrivers.riru.xposed.entry.bootstrap.SysInnerHookInfo; +import com.elderdrivers.riru.xposed.entry.hooker.SystemMainHooker; +import com.elderdrivers.riru.xposed.util.Utils; + +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedInit; + +public class Router { + + public static void onProcessForked(boolean isSystem) { + // Initialize the Xposed framework and modules + try { + XposedInit.initForZygote(isSystem); + // FIXME some coredomain app can't reading modules.list + XposedInit.loadModules(); + } catch (Throwable t) { + Utils.logE("Errors during Xposed initialization", t); + XposedBridge.disableHooks = true; + } + } + + public static void startBootstrapHook(boolean isSystem) { + Utils.logD("startBootstrapHook starts: isSystem = " + isSystem); + ClassLoader classLoader = XposedBridge.BOOTCLASSLOADER; + if (isSystem) { + HookMain.doHookDefault( + Router.class.getClassLoader(), + classLoader, + SysBootstrapHookInfo.class.getName()); + } else { + HookMain.doHookDefault( + Router.class.getClassLoader(), + classLoader, + AppBootstrapHookInfo.class.getName()); + } + } + + public static void startSystemServerHook() { + HookMain.doHookDefault( + Router.class.getClassLoader(), + SystemMainHooker.systemServerCL, + SysInnerHookInfo.class.getName()); + } +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/AppBootstrapHookInfo.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/AppBootstrapHookInfo.java new file mode 100644 index 00000000..ea467d02 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/AppBootstrapHookInfo.java @@ -0,0 +1,16 @@ +package com.elderdrivers.riru.xposed.entry.bootstrap; + +import com.elderdrivers.riru.common.KeepMembers; +import com.elderdrivers.riru.xposed.entry.hooker.HandleBindAppHooker; +import com.elderdrivers.riru.xposed.entry.hooker.InstrumentationHooker; +import com.elderdrivers.riru.xposed.entry.hooker.LoadedApkConstructorHooker; +import com.elderdrivers.riru.xposed.entry.hooker.MakeAppHooker; + +public class AppBootstrapHookInfo implements KeepMembers { + public static String[] hookItemNames = { + InstrumentationHooker.CallAppOnCreate.class.getName(), + HandleBindAppHooker.class.getName(), +// MakeAppHooker.class.getName(), + LoadedApkConstructorHooker.class.getName() + }; +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/SysBootstrapHookInfo.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/SysBootstrapHookInfo.java new file mode 100644 index 00000000..22daa299 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/SysBootstrapHookInfo.java @@ -0,0 +1,18 @@ +package com.elderdrivers.riru.xposed.entry.bootstrap; + +import com.elderdrivers.riru.common.KeepMembers; +import com.elderdrivers.riru.xposed.entry.hooker.HandleBindAppHooker; +import com.elderdrivers.riru.xposed.entry.hooker.InstrumentationHooker; +import com.elderdrivers.riru.xposed.entry.hooker.LoadedApkConstructorHooker; +import com.elderdrivers.riru.xposed.entry.hooker.MakeAppHooker; +import com.elderdrivers.riru.xposed.entry.hooker.SystemMainHooker; + +public class SysBootstrapHookInfo implements KeepMembers { + public static String[] hookItemNames = { +// InstrumentationHooker.CallAppOnCreate.class.getName(), + HandleBindAppHooker.class.getName(), + MakeAppHooker.class.getName(), + SystemMainHooker.class.getName(), + LoadedApkConstructorHooker.class.getName() + }; +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/SysInnerHookInfo.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/SysInnerHookInfo.java new file mode 100644 index 00000000..c08293d7 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/bootstrap/SysInnerHookInfo.java @@ -0,0 +1,10 @@ +package com.elderdrivers.riru.xposed.entry.bootstrap; + +import com.elderdrivers.riru.common.KeepMembers; +import com.elderdrivers.riru.xposed.entry.hooker.StartBootstrapServicesHooker; + +public class SysInnerHookInfo implements KeepMembers { + public static String[] hookItemNames = { + StartBootstrapServicesHooker.class.getName() + }; +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/HandleBindAppHooker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/HandleBindAppHooker.java new file mode 100644 index 00000000..28444b6c --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/HandleBindAppHooker.java @@ -0,0 +1,74 @@ +package com.elderdrivers.riru.xposed.entry.hooker; + +import android.app.ActivityThread; +import android.app.LoadedApk; +import android.content.ComponentName; +import android.content.pm.ApplicationInfo; +import android.content.res.CompatibilityInfo; + +import com.elderdrivers.riru.common.KeepMembers; + +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +import static com.elderdrivers.riru.xposed.util.ClassLoaderUtils.replaceParentClassLoader; +import static de.robv.android.xposed.XposedHelpers.getObjectField; +import static de.robv.android.xposed.XposedHelpers.setObjectField; +import static de.robv.android.xposed.XposedInit.INSTALLER_PACKAGE_NAME; +import static de.robv.android.xposed.XposedInit.loadedPackagesInProcess; +import static de.robv.android.xposed.XposedInit.logD; +import static de.robv.android.xposed.XposedInit.logE; + +// normal process initialization (for new Activity, Service, BroadcastReceiver etc.) +public class HandleBindAppHooker implements KeepMembers { + + public static String className = "android.app.ActivityThread"; + public static String methodName = "handleBindApplication"; + public static String methodSig = "(Landroid/app/ActivityThread$AppBindData;)V"; + + public static void hook(Object thiz, Object bindData) { + backup(thiz, bindData); + if (XposedBridge.disableHooks) { + return; + } + try { + logD("ActivityThread#handleBindApplication() starts"); + ActivityThread activityThread = (ActivityThread) thiz; + ApplicationInfo appInfo = (ApplicationInfo) getObjectField(bindData, "appInfo"); + String reportedPackageName = appInfo.packageName.equals("android") ? "system" : appInfo.packageName; + ComponentName instrumentationName = (ComponentName) getObjectField(bindData, "instrumentationName"); + if (instrumentationName != null) { + logD("Instrumentation detected, disabling framework for"); + XposedBridge.disableHooks = true; + return; + } + CompatibilityInfo compatInfo = (CompatibilityInfo) getObjectField(bindData, "compatInfo"); + if (appInfo.sourceDir == null) { + return; + } + + setObjectField(activityThread, "mBoundApplication", bindData); + loadedPackagesInProcess.add(reportedPackageName); + LoadedApk loadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo); + + replaceParentClassLoader(loadedApk.getClassLoader()); + + XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); + lpparam.packageName = reportedPackageName; + lpparam.processName = (String) getObjectField(bindData, "processName"); + lpparam.classLoader = loadedApk.getClassLoader(); + lpparam.appInfo = appInfo; + lpparam.isFirstApplication = true; + XC_LoadPackage.callAll(lpparam); + + if (reportedPackageName.equals(INSTALLER_PACKAGE_NAME)) { + XposedInstallerHooker.hookXposedInstaller(lpparam.classLoader); + } + } catch (Throwable t) { + logE("error when hooking bindApp", t); + } + } + + public static void backup(Object thiz, Object bindData) { + } +} \ No newline at end of file diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/InstrumentationHooker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/InstrumentationHooker.java new file mode 100644 index 00000000..94bdef67 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/InstrumentationHooker.java @@ -0,0 +1,59 @@ +package com.elderdrivers.riru.xposed.entry.hooker; + +import android.app.AndroidAppHelper; +import android.app.Application; +import android.app.LoadedApk; + +import com.elderdrivers.riru.common.KeepMembers; + +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +import static com.elderdrivers.riru.xposed.util.ClassLoaderUtils.replaceParentClassLoader; +import static de.robv.android.xposed.XposedHelpers.getObjectField; +import static de.robv.android.xposed.XposedInit.INSTALLER_PACKAGE_NAME; +import static de.robv.android.xposed.XposedInit.loadedPackagesInProcess; +import static de.robv.android.xposed.XposedInit.logD; +import static de.robv.android.xposed.XposedInit.logE; + + +public class InstrumentationHooker { + + public static class CallAppOnCreate implements KeepMembers { + + public static String className = "android.app.Instrumentation"; + public static String methodName = "callApplicationOnCreate"; + public static String methodSig = "(Landroid/app/Application;)V"; + + public static void hook(Object thiz, Application application) { + try { + logD("Instrumentation#callApplicationOnCreate starts"); + LoadedApk loadedApk = (LoadedApk) getObjectField(application, "mLoadedApk"); + String reportedPackageName = application.getPackageName(); + loadedPackagesInProcess.add(reportedPackageName); + + replaceParentClassLoader(loadedApk.getClassLoader()); + + XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); + lpparam.packageName = reportedPackageName; + lpparam.processName = AndroidAppHelper.currentProcessName(); + lpparam.classLoader = loadedApk.getClassLoader(); + lpparam.appInfo = application.getApplicationInfo(); + lpparam.isFirstApplication = true; + XC_LoadPackage.callAll(lpparam); + + if (reportedPackageName.equals(INSTALLER_PACKAGE_NAME)) { + XposedInstallerHooker.hookXposedInstaller(lpparam.classLoader); + } + } catch (Throwable throwable) { + logE("error when hooking Instru#callAppOnCreate", throwable); + } + backup(thiz, application); + } + + public static void backup(Object thiz, Application application) { + + } + + } +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/LoadedApkConstructorHooker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/LoadedApkConstructorHooker.java new file mode 100644 index 00000000..f059f3f1 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/LoadedApkConstructorHooker.java @@ -0,0 +1,85 @@ +package com.elderdrivers.riru.xposed.entry.hooker; + +import android.app.ActivityThread; +import android.app.AndroidAppHelper; +import android.app.LoadedApk; +import android.content.pm.ApplicationInfo; +import android.content.res.CompatibilityInfo; + +import com.elderdrivers.riru.common.KeepMembers; + +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +import static com.elderdrivers.riru.xposed.util.ClassLoaderUtils.replaceParentClassLoader; +import static de.robv.android.xposed.XposedHelpers.getBooleanField; +import static de.robv.android.xposed.XposedHelpers.getObjectField; +import static de.robv.android.xposed.XposedInit.loadedPackagesInProcess; +import static de.robv.android.xposed.XposedInit.logD; +import static de.robv.android.xposed.XposedInit.logE; + +// when a package is loaded for an existing process, trigger the callbacks as well +// ed: remove resources related hooking +public class LoadedApkConstructorHooker implements KeepMembers { + public static String className = "android.app.LoadedApk"; + public static String methodName = ""; + public static String methodSig = "(Landroid/app/ActivityThread;" + + "Landroid/content/pm/ApplicationInfo;" + + "Landroid/content/res/CompatibilityInfo;" + + "Ljava/lang/ClassLoader;ZZZ)V"; + + public static void hook(Object thiz, ActivityThread activityThread, + ApplicationInfo aInfo, CompatibilityInfo compatInfo, + ClassLoader baseLoader, boolean securityViolation, + boolean includeCode, boolean registerPackage) { + + if (XposedBridge.disableHooks) { + backup(thiz, activityThread, aInfo, compatInfo, baseLoader, securityViolation, includeCode, registerPackage); + return; + } + + logD("LoadedApk# starts"); + backup(thiz, activityThread, aInfo, compatInfo, baseLoader, securityViolation, + includeCode, registerPackage); + + try { + LoadedApk loadedApk = (LoadedApk) thiz; + String packageName = loadedApk.getPackageName(); + Object mAppDir = getObjectField(thiz, "mAppDir"); + logD("LoadedApk# ends: " + mAppDir); + if (packageName.equals("android")) { + logD("LoadedApk# is android, skip: " + mAppDir); + return; + } + + if (!loadedPackagesInProcess.add(packageName)) { + logD("LoadedApk# has been loaded before, skip: " + mAppDir); + return; + } + + if (!getBooleanField(loadedApk, "mIncludeCode")) { + logD("LoadedApk# mIncludeCode == false: " + mAppDir); + return; + } + + replaceParentClassLoader(loadedApk.getClassLoader()); + + XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); + lpparam.packageName = packageName; + lpparam.processName = AndroidAppHelper.currentProcessName(); + lpparam.classLoader = loadedApk.getClassLoader(); + lpparam.appInfo = loadedApk.getApplicationInfo(); + lpparam.isFirstApplication = false; + XC_LoadPackage.callAll(lpparam); + } catch (Throwable t) { + logE("error when hooking LoadedApk.", t); + } + } + + public static void backup(Object thiz, ActivityThread activityThread, + ApplicationInfo aInfo, CompatibilityInfo compatInfo, + ClassLoader baseLoader, boolean securityViolation, + boolean includeCode, boolean registerPackage) { + + } +} \ No newline at end of file diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/MakeAppHooker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/MakeAppHooker.java new file mode 100644 index 00000000..4f025d33 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/MakeAppHooker.java @@ -0,0 +1,73 @@ +package com.elderdrivers.riru.xposed.entry.hooker; + +import android.app.AndroidAppHelper; +import android.app.Application; +import android.app.Instrumentation; +import android.app.LoadedApk; + +import com.elderdrivers.riru.common.KeepMembers; + +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +import static com.elderdrivers.riru.xposed.entry.hooker.XposedInstallerHooker.hookXposedInstaller; +import static com.elderdrivers.riru.xposed.util.ClassLoaderUtils.replaceParentClassLoader; +import static de.robv.android.xposed.XposedHelpers.getBooleanField; +import static de.robv.android.xposed.XposedHelpers.getObjectField; +import static de.robv.android.xposed.XposedInit.INSTALLER_PACKAGE_NAME; +import static de.robv.android.xposed.XposedInit.logD; +import static de.robv.android.xposed.XposedInit.logE; + + +public class MakeAppHooker implements KeepMembers { + + public static String className = "android.app.LoadedApk"; + public static String methodName = "makeApplication"; + public static String methodSig = "(ZLandroid/app/Instrumentation;)Landroid/app/Application;"; + + public static Application hook(Object thiz, boolean forceDefaultAppClass, + Instrumentation instrumentation) { + if (XposedBridge.disableHooks) { + return backup(thiz, forceDefaultAppClass, instrumentation); + } + logD("LoadedApk#makeApplication() starts"); + boolean shouldHook = getObjectField(thiz, "mApplication") == null; + logD("LoadedApk#makeApplication() shouldHook == " + shouldHook); + Application application = backup(thiz, forceDefaultAppClass, instrumentation); + if (shouldHook) { + try { + LoadedApk loadedApk = (LoadedApk) thiz; + String packageName = loadedApk.getPackageName(); + + if (!getBooleanField(loadedApk, "mIncludeCode")) { + logD("LoadedApk#makeApplication() mIncludeCode == false"); + return application; + } + + logD("LoadedApk#makeApplication() mIncludeCode == true"); + + replaceParentClassLoader(loadedApk.getClassLoader()); + + XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam( + XposedBridge.sLoadedPackageCallbacks); + lpparam.packageName = packageName; + lpparam.processName = AndroidAppHelper.currentProcessName(); + lpparam.classLoader = loadedApk.getClassLoader(); + lpparam.appInfo = loadedApk.getApplicationInfo(); + lpparam.isFirstApplication = true; + XC_LoadPackage.callAll(lpparam); + if (packageName.equals(INSTALLER_PACKAGE_NAME)) { + hookXposedInstaller(lpparam.classLoader); + } + } catch (Throwable t) { + logE("error when hooking LoadedApk#makeApplication", t); + } + } + return application; + } + + public static Application backup(Object thiz, boolean forceDefaultAppClass, + Instrumentation instrumentation) { + return null; + } +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/StartBootstrapServicesHooker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/StartBootstrapServicesHooker.java new file mode 100644 index 00000000..9ab4a1e3 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/StartBootstrapServicesHooker.java @@ -0,0 +1,66 @@ +package com.elderdrivers.riru.xposed.entry.hooker; + +import android.os.Build; + +import com.elderdrivers.riru.common.KeepMembers; + +import de.robv.android.xposed.XC_MethodReplacement; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +import static com.elderdrivers.riru.xposed.util.ClassLoaderUtils.replaceParentClassLoader; +import static com.elderdrivers.riru.xposed.util.Utils.logD; +import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; +import static de.robv.android.xposed.XposedInit.loadedPackagesInProcess; +import static de.robv.android.xposed.XposedInit.logE; + +public class StartBootstrapServicesHooker implements KeepMembers { + public static String className = "com.android.server.SystemServer"; + public static String methodName = "startBootstrapServices"; + public static String methodSig = "()V"; + + public static void hook(Object systemServer) { + + if (XposedBridge.disableHooks) { + backup(systemServer); + return; + } + + logD("SystemServer#startBootstrapServices() starts"); + + try { + loadedPackagesInProcess.add("android"); + + replaceParentClassLoader(SystemMainHooker.systemServerCL); + + XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); + lpparam.packageName = "android"; + lpparam.processName = "android"; // it's actually system_server, but other functions return this as well + lpparam.classLoader = SystemMainHooker.systemServerCL; + lpparam.appInfo = null; + lpparam.isFirstApplication = true; + XC_LoadPackage.callAll(lpparam); + + // Huawei + try { + findAndHookMethod("com.android.server.pm.HwPackageManagerService", SystemMainHooker.systemServerCL, "isOdexMode", XC_MethodReplacement.returnConstant(false)); + } catch (XposedHelpers.ClassNotFoundError | NoSuchMethodError ignored) { + } + + try { + String className = "com.android.server.pm." + (Build.VERSION.SDK_INT >= 23 ? "PackageDexOptimizer" : "PackageManagerService"); + findAndHookMethod(className, SystemMainHooker.systemServerCL, "dexEntryExists", String.class, XC_MethodReplacement.returnConstant(true)); + } catch (XposedHelpers.ClassNotFoundError | NoSuchMethodError ignored) { + } + } catch (Throwable t) { + logE("error when hooking startBootstrapServices", t); + } finally { + backup(systemServer); + } + } + + public static void backup(Object systemServer) { + + } +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/SystemMainHooker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/SystemMainHooker.java new file mode 100644 index 00000000..11f81373 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/SystemMainHooker.java @@ -0,0 +1,43 @@ +package com.elderdrivers.riru.xposed.entry.hooker; + +import android.app.ActivityThread; + +import com.elderdrivers.riru.common.KeepMembers; +import com.elderdrivers.riru.xposed.entry.Router; + +import de.robv.android.xposed.XposedBridge; + +import static de.robv.android.xposed.XposedInit.logD; +import static de.robv.android.xposed.XposedInit.logE; + + +// system_server initialization +// ed: only support sdk >= 21 for now +public class SystemMainHooker implements KeepMembers { + + public static String className = "android.app.ActivityThread"; + public static String methodName = "systemMain"; + public static String methodSig = "()Landroid/app/ActivityThread;"; + + public static ClassLoader systemServerCL; + + public static ActivityThread hook() { + if (XposedBridge.disableHooks) { + return backup(); + } + logD("ActivityThread#systemMain() starts"); + ActivityThread activityThread = backup(); + try { + // get system_server classLoader + systemServerCL = Thread.currentThread().getContextClassLoader(); + Router.startSystemServerHook(); + } catch (Throwable t) { + logE("error when hooking systemMain", t); + } + return activityThread; + } + + public static ActivityThread backup() { + return null; + } +} \ No newline at end of file diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/XposedInstallerHooker.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/XposedInstallerHooker.java new file mode 100644 index 00000000..32d8de19 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/entry/hooker/XposedInstallerHooker.java @@ -0,0 +1,68 @@ +package com.elderdrivers.riru.xposed.entry.hooker; + +import com.elderdrivers.riru.xposed.util.Utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XC_MethodReplacement; +import de.robv.android.xposed.XposedBridge; + +import static de.robv.android.xposed.XposedHelpers.callStaticMethod; +import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; +import static de.robv.android.xposed.XposedHelpers.findClass; +import static de.robv.android.xposed.XposedHelpers.getObjectField; +import static de.robv.android.xposed.XposedHelpers.setObjectField; +import static de.robv.android.xposed.XposedInit.INSTALLER_PACKAGE_NAME; + +public class XposedInstallerHooker { + + public static void hookXposedInstaller(ClassLoader classLoader) { + try { + final String xposedAppClass = INSTALLER_PACKAGE_NAME + ".XposedApp"; + final Class InstallZipUtil = findClass(INSTALLER_PACKAGE_NAME + + ".util.InstallZipUtil", classLoader); + findAndHookMethod(xposedAppClass, classLoader, "getActiveXposedVersion", + XC_MethodReplacement.returnConstant(XposedBridge.getXposedVersion())); + findAndHookMethod(xposedAppClass, classLoader, + "reloadXposedProp", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + Utils.logD("before reloadXposedProp..."); + final String propFieldName = "mXposedProp"; + final Object thisObject = param.thisObject; + if (getObjectField(thisObject, propFieldName) != null) { + param.setResult(null); + Utils.logD("reloadXposedProp already done, skip..."); + return; + } + File file = new File("/system/framework/edconfig.dex"); + FileInputStream is = null; + try { + is = new FileInputStream(file); + Object props = callStaticMethod(InstallZipUtil, + "parseXposedProp", is); + synchronized (thisObject) { + setObjectField(thisObject, propFieldName, props); + } + Utils.logD("reloadXposedProp done..."); + param.setResult(null); + } catch (IOException e) { + Utils.logE("Could not read " + file.getPath(), e); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ignored) { + } + } + } + } + }); + } catch (Throwable t) { + Utils.logE("Could not hook Xposed Installer", t); + } + } +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/ClassLoaderUtils.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/ClassLoaderUtils.java new file mode 100644 index 00000000..49712d72 --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/ClassLoaderUtils.java @@ -0,0 +1,106 @@ +package com.elderdrivers.riru.xposed.util; + +import android.os.Build; +import android.util.ArrayMap; + +import com.elderdrivers.riru.xposed.BuildConfig; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import dalvik.system.PathClassLoader; + +public class ClassLoaderUtils { + + public static final String DEXPATH = "/system/framework/edxposed.dex:/system/framework/eddalvikdx.dex:/system/framework/eddexmaker.dex"; + + public static void replaceParentClassLoader(ClassLoader appClassLoader) { + if (appClassLoader == null) { + Utils.logE("appClassLoader is null, you might be kidding me?"); + return; + } + try { + ClassLoader curCL = ClassLoaderUtils.class.getClassLoader(); + ClassLoader parent = appClassLoader; + ClassLoader lastChild = appClassLoader; + while (parent != null) { + ClassLoader tmp = parent.getParent(); + if (tmp == curCL) { + Utils.logD("replacing has been done before, skip."); + return; + } + if (tmp == null) { + Utils.logD("before replacing =========================================>"); + dumpClassLoaders(appClassLoader); + Field parentField = ClassLoader.class.getDeclaredField("parent"); + parentField.setAccessible(true); + parentField.set(curCL, parent); + parentField.set(lastChild, curCL); + Utils.logD("after replacing ==========================================>"); + dumpClassLoaders(appClassLoader); + } + lastChild = parent; + parent = tmp; + } + } catch (Throwable throwable) { + Utils.logE("error when replacing class loader.", throwable); + } + } + + private static void dumpClassLoaders(ClassLoader classLoader) { + if (BuildConfig.DEBUG) { + while (classLoader != null) { + Utils.logD(classLoader + " =>"); + classLoader = classLoader.getParent(); + } + } + } + + public static List getAppClassLoader() { + List cacheLoaders = new ArrayList<>(0); + try { + Utils.logD("start getting app classloader"); + Class appLoadersClass = Class.forName("android.app.ApplicationLoaders"); + Field loadersField = appLoadersClass.getDeclaredField("gApplicationLoaders"); + loadersField.setAccessible(true); + Object loaders = loadersField.get(null); + Field mLoaderMapField = loaders.getClass().getDeclaredField("mLoaders"); + mLoaderMapField.setAccessible(true); + ArrayMap mLoaderMap = (ArrayMap) mLoaderMapField.get(loaders); + Utils.logD("mLoaders size = " + mLoaderMap.size()); + cacheLoaders = new ArrayList<>(mLoaderMap.values()); + } catch (Exception ex) { + Utils.logE("error get app class loader.", ex); + } + return cacheLoaders; + } + + private static HashSet classLoaders = new HashSet<>(); + + public static boolean addPathToClassLoader(ClassLoader classLoader) { + if (!(classLoader instanceof PathClassLoader)) { + Utils.logW(classLoader + " is not a BaseDexClassLoader!!!"); + return false; + } + if (classLoaders.contains(classLoader)) { + Utils.logD(classLoader + " has been hooked before"); + return true; + } + try { + PathClassLoader baseDexClassLoader = (PathClassLoader) classLoader; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + baseDexClassLoader.addDexPath(DEXPATH); + } else { + DexUtils.injectDexAtFirst(DEXPATH, baseDexClassLoader); + } + classLoaders.add(classLoader); + return true; + } catch (Throwable throwable) { + Utils.logE("error when addPath to ClassLoader: " + classLoader, throwable); + } + return false; + } + +} diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/DexUtils.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/DexUtils.java new file mode 100644 index 00000000..7ef4ebec --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/DexUtils.java @@ -0,0 +1,67 @@ +package com.elderdrivers.riru.xposed.util; + +import android.annotation.TargetApi; +import android.os.Build; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; + +import dalvik.system.BaseDexClassLoader; +import dalvik.system.DexClassLoader; +import dalvik.system.PathClassLoader; + +/** + * For 6.0 only. + */ +@TargetApi(Build.VERSION_CODES.M) +public class DexUtils { + + public static void injectDexAtFirst(String dexPath, BaseDexClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { + DexClassLoader dexClassLoader = new DexClassLoader(dexPath, null, dexPath, classLoader); + Object baseDexElements = getDexElements(getPathList(classLoader)); + Object newDexElements = getDexElements(getPathList(dexClassLoader)); + Object allDexElements = combineArray(newDexElements, baseDexElements); + Object pathList = getPathList(classLoader); + setField(pathList, pathList.getClass(), "dexElements", allDexElements); + } + + private static Object getDexElements(Object paramObject) + throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException { + return getField(paramObject, paramObject.getClass(), "dexElements"); + } + + private static Object getPathList(Object baseDexClassLoader) + throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException { + return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); + } + + private static Object combineArray(Object firstArray, Object secondArray) { + Class localClass = firstArray.getClass().getComponentType(); + int firstArrayLength = Array.getLength(firstArray); + int allLength = firstArrayLength + Array.getLength(secondArray); + Object result = Array.newInstance(localClass, allLength); + for (int k = 0; k < allLength; ++k) { + if (k < firstArrayLength) { + Array.set(result, k, Array.get(firstArray, k)); + } else { + Array.set(result, k, Array.get(secondArray, k - firstArrayLength)); + } + } + return result; + } + + public static Object getField(Object obj, Class cl, String field) + throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { + Field localField = cl.getDeclaredField(field); + localField.setAccessible(true); + return localField.get(obj); + } + + public static void setField(Object obj, Class cl, String field, Object value) + throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { + Field localField = cl.getDeclaredField(field); + localField.setAccessible(true); + localField.set(obj, value); + } + +} \ No newline at end of file diff --git a/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/Utils.java b/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/Utils.java new file mode 100644 index 00000000..46f25e1b --- /dev/null +++ b/Bridge/src/main/java/com/elderdrivers/riru/xposed/util/Utils.java @@ -0,0 +1,45 @@ +package com.elderdrivers.riru.xposed.util; + +import android.util.Log; + +import com.elderdrivers.riru.xposed.BuildConfig; + + +public class Utils { + + public static final String LOG_TAG = "EdXposed-Fwk"; + + public static void logD(Object msg) { + if (BuildConfig.DEBUG) + Log.d(LOG_TAG, msg.toString()); + } + + public static void logD(String msg, Throwable throwable) { + if (BuildConfig.DEBUG) + Log.d(LOG_TAG, msg, throwable); + } + + public static void logW(String msg) { + Log.w(LOG_TAG, msg); + } + + public static void logW(String msg, Throwable throwable) { + Log.w(LOG_TAG, msg, throwable); + } + + public static void logI(String msg) { + Log.i(LOG_TAG, msg); + } + + public static void logI(String msg, Throwable throwable) { + Log.i(LOG_TAG, msg, throwable); + } + + public static void logE(String msg) { + Log.e(LOG_TAG, msg); + } + + public static void logE(String msg, Throwable throwable) { + Log.e(LOG_TAG, msg, throwable); + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/DexCreator.java b/Bridge/src/main/java/de/robv/android/xposed/DexCreator.java new file mode 100644 index 00000000..f79b4443 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/DexCreator.java @@ -0,0 +1,235 @@ +package de.robv.android.xposed; + +import android.os.Environment; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.zip.Adler32; + +import static de.robv.android.xposed.XposedHelpers.inputStreamToByteArray; + +/** + * Helper class which can create a very simple .dex file, containing only a class definition + * with a super class (no methods, fields, ...). + */ +/*package*/ class DexCreator { + public static File DALVIK_CACHE = new File(Environment.getDataDirectory(), "dalvik-cache"); + + /** Returns the default dex file name for the class. */ + public static File getDefaultFile(String childClz) { + return new File(DALVIK_CACHE, "xposed_" + childClz.substring(childClz.lastIndexOf('.') + 1) + ".dex"); + } + + /** + * Creates (or returns) the path to a dex file which defines the superclass of {@clz} as extending + * {@code realSuperClz}, which by itself must extend {@code topClz}. + */ + public static File ensure(String clz, Class realSuperClz, Class topClz) throws IOException { + if (!topClz.isAssignableFrom(realSuperClz)) { + throw new ClassCastException("Cannot initialize " + clz + " because " + realSuperClz + " does not extend " + topClz); + } + + try { + return ensure("xposed.dummy." + clz + "SuperClass", realSuperClz); + } catch (IOException e) { + throw new IOException("Failed to create a superclass for " + clz, e); + } + } + + /** Like {@link #ensure(File, String, String)}, just for the default dex file name. */ + public static File ensure(String childClz, Class superClz) throws IOException { + return ensure(getDefaultFile(childClz), childClz, superClz.getName()); + } + + /** + * Makes sure that the given file is a simple dex file containing the given classes. + * Creates the file if that's not the case. + */ + public static File ensure(File file, String childClz, String superClz) throws IOException { + // First check if a valid file exists. + try { + byte[] dex = inputStreamToByteArray(new FileInputStream(file)); + if (matches(dex, childClz, superClz)) { + return file; + } else { + file.delete(); + } + } catch (IOException e) { + file.delete(); + } + + // If not, create a new dex file. + byte[] dex = create(childClz, superClz); + FileOutputStream fos = new FileOutputStream(file); + fos.write(dex); + fos.close(); + return file; + } + + /** + * Checks whether the Dex file fits to the class names. + * Assumes that the file has been created with this class. + */ + public static boolean matches(byte[] dex, String childClz, String superClz) throws IOException { + boolean childFirst = childClz.compareTo(superClz) < 0; + byte[] childBytes = stringToBytes("L" + childClz.replace('.', '/') + ";"); + byte[] superBytes = stringToBytes("L" + superClz.replace('.', '/') + ";"); + + int pos = 0xa0; + if (pos + childBytes.length + superBytes.length >= dex.length) { + return false; + } + + for (byte b : childFirst ? childBytes : superBytes) { + if (dex[pos++] != b) { + return false; + } + } + + for (byte b : childFirst ? superBytes: childBytes) { + if (dex[pos++] != b) { + return false; + } + } + + return true; + } + + /** Creates the byte array for the dex file. */ + public static byte[] create(String childClz, String superClz) throws IOException { + boolean childFirst = childClz.compareTo(superClz) < 0; + byte[] childBytes = stringToBytes("L" + childClz.replace('.', '/') + ";"); + byte[] superBytes = stringToBytes("L" + superClz.replace('.', '/') + ";"); + int stringsSize = childBytes.length + superBytes.length; + int padding = -stringsSize & 3; + stringsSize += padding; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + // header + out.write("dex\n035\0".getBytes()); // magic + out.write(new byte[24]); // placeholder for checksum and signature + writeInt(out, 0xfc + stringsSize); // file size + writeInt(out, 0x70); // header size + writeInt(out, 0x12345678); // endian constant + writeInt(out, 0); // link size + writeInt(out, 0); // link offset + writeInt(out, 0xa4 + stringsSize); // map offset + writeInt(out, 2); // strings count + writeInt(out, 0x70); // strings offset + writeInt(out, 2); // types count + writeInt(out, 0x78); // types offset + writeInt(out, 0); // prototypes count + writeInt(out, 0); // prototypes offset + writeInt(out, 0); // fields count + writeInt(out, 0); // fields offset + writeInt(out, 0); // methods count + writeInt(out, 0); // methods offset + writeInt(out, 1); // classes count + writeInt(out, 0x80); // classes offset + writeInt(out, 0x5c + stringsSize); // data size + writeInt(out, 0xa0); // data offset + + // string map + writeInt(out, 0xa0); + writeInt(out, 0xa0 + (childFirst ? childBytes.length : superBytes.length)); + + // types + writeInt(out, 0); // first type = first string + writeInt(out, 1); // second type = second string + + // class definitions + writeInt(out, childFirst ? 0 : 1); // class to define = child type + writeInt(out, 1); // access flags = public + writeInt(out, childFirst ? 1 : 0); // super class = super type + writeInt(out, 0); // no interface + writeInt(out, -1); // no source file + writeInt(out, 0); // no annotations + writeInt(out, 0); // no class data + writeInt(out, 0); // no static values + + // string data + out.write(childFirst ? childBytes : superBytes); + out.write(childFirst ? superBytes : childBytes); + out.write(new byte[padding]); + + // annotations + writeInt(out, 0); // no items + + // map + writeInt(out, 7); // items count + writeMapItem(out, 0, 1, 0); // header + writeMapItem(out, 1, 2, 0x70); // strings + writeMapItem(out, 2, 2, 0x78); // types + writeMapItem(out, 6, 1, 0x80); // classes + writeMapItem(out, 0x2002, 2, 0xa0); // string data + writeMapItem(out, 0x1003, 1, 0xa0 + stringsSize); // annotations + writeMapItem(out, 0x1000, 1, 0xa4 + stringsSize); // map list + + byte[] buf = out.toByteArray(); + updateSignature(buf); + updateChecksum(buf); + return buf; + } + + private static void updateSignature(byte[] dex) { + // Update SHA-1 signature + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(dex, 32, dex.length - 32); + md.digest(dex, 12, 20); + } catch (NoSuchAlgorithmException | DigestException e) { + throw new RuntimeException(e); + } + } + + private static void updateChecksum(byte[] dex) { + // Update Adler32 checksum + Adler32 a32 = new Adler32(); + a32.update(dex, 12, dex.length - 12); + int chksum = (int) a32.getValue(); + dex[8] = (byte) (chksum & 0xff); + dex[9] = (byte) (chksum >> 8 & 0xff); + dex[10] = (byte) (chksum >> 16 & 0xff); + dex[11] = (byte) (chksum >> 24 & 0xff); + } + + private static void writeUleb128(OutputStream out, int value) throws IOException { + while (value > 0x7f) { + out.write((value & 0x7f) | 0x80); + value >>>= 7; + } + out.write(value); + } + + private static void writeInt(OutputStream out, int value) throws IOException { + out.write(value); + out.write(value >> 8); + out.write(value >> 16); + out.write(value >> 24); + } + + private static void writeMapItem(OutputStream out, int type, int count, int offset) throws IOException { + writeInt(out, type); + writeInt(out, count); + writeInt(out, offset); + } + + private static byte[] stringToBytes(String s) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + writeUleb128(bytes, s.length()); + // This isn't MUTF-8, but should be OK. + bytes.write(s.getBytes("UTF-8")); + bytes.write(0); + return bytes.toByteArray(); + } + + private DexCreator() {} +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/GeneClass_Template.java b/Bridge/src/main/java/de/robv/android/xposed/GeneClass_Template.java new file mode 100644 index 00000000..7d0e096e --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/GeneClass_Template.java @@ -0,0 +1,74 @@ +package de.robv.android.xposed; + +public class GeneClass_Template { + public static java.lang.reflect.Member method; + public static de.robv.android.xposed.XposedBridge.AdditionalHookInfo tAdditionalInfoObj; + + public static boolean backup(java.lang.Object obj, int i) { + return false; + } + + public static boolean hook(java.lang.Object obj, int i) throws Throwable { + java.lang.Throwable th; + if (!de.robv.android.xposed.XposedBridge.disableHooks) { + java.lang.Object[] snapshot = tAdditionalInfoObj.callbacks.getSnapshot(); + int length = snapshot.length; + if (length != 0) { + de.robv.android.xposed.XC_MethodHook.MethodHookParam methodHookParam = new de.robv.android.xposed.XC_MethodHook.MethodHookParam(); + methodHookParam.method = method; + java.lang.Object[] objArr = new java.lang.Object[1]; + methodHookParam.args = objArr; + methodHookParam.thisObject = obj; + objArr[0] = java.lang.Integer.valueOf(i); + int i2 = 0; + do { + try { + ((de.robv.android.xposed.XC_MethodHook) snapshot[i2]).callBeforeHookedMethod(methodHookParam); + if (methodHookParam.returnEarly) { + i2++; + break; + } + } catch (java.lang.Throwable th2) { + de.robv.android.xposed.XposedBridge.log(th2); + methodHookParam.setResult(null); + methodHookParam.returnEarly = false; + } + i2++; + } while (i2 < length); + if (!methodHookParam.returnEarly) { + try { + methodHookParam.setResult(java.lang.Boolean.valueOf(backup(obj, i))); + } catch (java.lang.Throwable th3) { + methodHookParam.setThrowable(th3); + } + } + i2--; + do { + java.lang.Object result = methodHookParam.getResult(); + Throwable th2 = methodHookParam.getThrowable(); + try { + ((de.robv.android.xposed.XC_MethodHook) snapshot[i2]).callAfterHookedMethod(methodHookParam); + } catch (java.lang.Throwable th4) { + de.robv.android.xposed.XposedBridge.log(th4); + if (th2 == null) { + methodHookParam.setResult(result); + } else { + methodHookParam.setThrowable(th2); + } + } + i2--; + } while (i2 >= 0); + if (!methodHookParam.hasThrowable()) { + return ((java.lang.Boolean) methodHookParam.getResult()).booleanValue(); + } + throw methodHookParam.getThrowable(); + } + } + return backup(obj, i); + } + + public static void setup(java.lang.reflect.Member member, de.robv.android.xposed.XposedBridge.AdditionalHookInfo additionalHookInfo) { + method = member; + tAdditionalInfoObj = additionalHookInfo; + } +} \ No newline at end of file diff --git a/Bridge/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java b/Bridge/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java new file mode 100644 index 00000000..6f99ac9d --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java @@ -0,0 +1,28 @@ +package de.robv.android.xposed; + + +/** + * Hook the initialization of Java-based command-line tools (like pm). + * + * @hide Xposed no longer hooks command-line tools, therefore this interface shouldn't be + * implemented anymore. + */ +public interface IXposedHookCmdInit extends IXposedMod { + /** + * Called very early during startup of a command-line tool. + * @param startupParam Details about the module itself and the started process. + * @throws Throwable Everything is caught, but it will prevent further initialization of the module. + */ + void initCmdApp(StartupParam startupParam) throws Throwable; + + /** Data holder for {@link #initCmdApp}. */ + final class StartupParam { + /*package*/ StartupParam() {} + + /** The path to the module's APK. */ + public String modulePath; + + /** The class name of the tools that the hook was invoked for. */ + public String startClassName; + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java b/Bridge/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java new file mode 100644 index 00000000..197ad661 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java @@ -0,0 +1,36 @@ +package de.robv.android.xposed; + +import android.content.res.XResources; + +import de.robv.android.xposed.callbacks.XC_InitPackageResources; +import de.robv.android.xposed.callbacks.XC_InitPackageResources.InitPackageResourcesParam; + +/** + * Get notified when the resources for an app are initialized. + * In {@link #handleInitPackageResources}, resource replacements can be created. + * + *

This interface should be implemented by the module's main class. Xposed will take care of + * registering it as a callback automatically. + */ +public interface IXposedHookInitPackageResources extends IXposedMod { + /** + * This method is called when resources for an app are being initialized. + * Modules can call special methods of the {@link XResources} class in order to replace resources. + * + * @param resparam Information about the resources. + * @throws Throwable Everything the callback throws is caught and logged. + */ + void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable; + + /** @hide */ + final class Wrapper extends XC_InitPackageResources { + private final IXposedHookInitPackageResources instance; + public Wrapper(IXposedHookInitPackageResources instance) { + this.instance = instance; + } + @Override + public void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable { + instance.handleInitPackageResources(resparam); + } + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java b/Bridge/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java new file mode 100644 index 00000000..46467b02 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java @@ -0,0 +1,37 @@ +package de.robv.android.xposed; + +import android.app.Application; + +import de.robv.android.xposed.callbacks.XC_LoadPackage; +import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam; + +/** + * Get notified when an app ("Android package") is loaded. + * This is especially useful to hook some app-specific methods. + * + *

This interface should be implemented by the module's main class. Xposed will take care of + * registering it as a callback automatically. + */ +public interface IXposedHookLoadPackage extends IXposedMod { + /** + * This method is called when an app is loaded. It's called very early, even before + * {@link Application#onCreate} is called. + * Modules can set up their app-specific hooks here. + * + * @param lpparam Information about the app. + * @throws Throwable Everything the callback throws is caught and logged. + */ + void handleLoadPackage(LoadPackageParam lpparam) throws Throwable; + + /** @hide */ + final class Wrapper extends XC_LoadPackage { + private final IXposedHookLoadPackage instance; + public Wrapper(IXposedHookLoadPackage instance) { + this.instance = instance; + } + @Override + public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable { + instance.handleLoadPackage(lpparam); + } + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java b/Bridge/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java new file mode 100644 index 00000000..dc130a76 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java @@ -0,0 +1,35 @@ +package de.robv.android.xposed; + +/** + * Hook the initialization of Zygote process(es), from which all the apps are forked. + * + *

Implement this interface in your module's main class in order to be notified when Android is + * starting up. In {@link IXposedHookZygoteInit}, you can modify objects and place hooks that should + * be applied for every app. Only the Android framework/system classes are available at that point + * in time. Use {@code null} as class loader for {@link XposedHelpers#findAndHookMethod(String, ClassLoader, String, Object...)} + * and its variants. + * + *

If you want to hook one/multiple specific apps, use {@link IXposedHookLoadPackage} instead. + */ +public interface IXposedHookZygoteInit extends IXposedMod { + /** + * Called very early during startup of Zygote. + * @param startupParam Details about the module itself and the started process. + * @throws Throwable everything is caught, but will prevent further initialization of the module. + */ + void initZygote(StartupParam startupParam) throws Throwable; + + /** Data holder for {@link #initZygote}. */ + final class StartupParam { + /*package*/ StartupParam() {} + + /** The path to the module's APK. */ + public String modulePath; + + /** + * Always {@code true} on 32-bit ROMs. On 64-bit, it's only {@code true} for the primary + * process that starts the system_server. + */ + public boolean startsSystemServer; + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/IXposedMod.java b/Bridge/src/main/java/de/robv/android/xposed/IXposedMod.java new file mode 100644 index 00000000..7723cd83 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/IXposedMod.java @@ -0,0 +1,4 @@ +package de.robv.android.xposed; + +/** Marker interface for Xposed modules. Cannot be implemented directly. */ +/* package */ interface IXposedMod {} diff --git a/Bridge/src/main/java/de/robv/android/xposed/SELinuxHelper.java b/Bridge/src/main/java/de/robv/android/xposed/SELinuxHelper.java new file mode 100644 index 00000000..4be8b5f1 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/SELinuxHelper.java @@ -0,0 +1,83 @@ +package de.robv.android.xposed; + +import android.os.SELinux; + +import de.robv.android.xposed.services.BaseService; +import de.robv.android.xposed.services.BinderService; +import de.robv.android.xposed.services.DirectAccessService; +import de.robv.android.xposed.services.ZygoteService; + +/** + * A helper to work with (or without) SELinux, abstracting much of its big complexity. + */ +public final class SELinuxHelper { + private SELinuxHelper() {} + + /** + * Determines whether SELinux is disabled or enabled. + * + * @return A boolean indicating whether SELinux is enabled. + */ + public static boolean isSELinuxEnabled() { + return sIsSELinuxEnabled; + } + + /** + * Determines whether SELinux is permissive or enforcing. + * + * @return A boolean indicating whether SELinux is enforcing. + */ + public static boolean isSELinuxEnforced() { + return sIsSELinuxEnabled && SELinux.isSELinuxEnforced(); + } + + /** + * Gets the security context of the current process. + * + * @return A String representing the security context of the current process. + */ + public static String getContext() { + return sIsSELinuxEnabled ? SELinux.getContext() : null; + } + + /** + * Retrieve the service to be used when accessing files in {@code /data/data/*}. + * + *

IMPORTANT: If you call this from the Zygote process, + * don't re-use the result in different process! + * + * @return An instance of the service. + */ + public static BaseService getAppDataFileService() { + if (sServiceAppDataFile != null) + return sServiceAppDataFile; + throw new UnsupportedOperationException(); + } + + + // ---------------------------------------------------------------------------- + private static boolean sIsSELinuxEnabled = false; + private static BaseService sServiceAppDataFile = new DirectAccessService(); // ed: initialized directly + + /*package*/ static void initOnce() { + // ed: we assume all selinux policies have been added lively using magiskpolicy +// try { +// sIsSELinuxEnabled = SELinux.isSELinuxEnabled(); +// } catch (NoClassDefFoundError ignored) {} + } + + /*package*/ static void initForProcess(String packageName) { + // ed: sServiceAppDataFile has been initialized with default value +// if (sIsSELinuxEnabled) { +// if (packageName == null) { // Zygote +// sServiceAppDataFile = new ZygoteService(); +// } else if (packageName.equals("android")) { //system_server +// sServiceAppDataFile = BinderService.getService(BinderService.TARGET_APP); +// } else { // app +// sServiceAppDataFile = new DirectAccessService(); +// } +// } else { +// sServiceAppDataFile = new DirectAccessService(); +// } + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/XC_MethodHook.java b/Bridge/src/main/java/de/robv/android/xposed/XC_MethodHook.java new file mode 100644 index 00000000..068616ab --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/XC_MethodHook.java @@ -0,0 +1,168 @@ +package de.robv.android.xposed; + +import java.lang.reflect.Member; + +import de.robv.android.xposed.callbacks.IXUnhook; +import de.robv.android.xposed.callbacks.XCallback; + +/** + * Callback class for method hooks. + * + *

Usually, anonymous subclasses of this class are created which override + * {@link #beforeHookedMethod} and/or {@link #afterHookedMethod}. + */ +public abstract class XC_MethodHook extends XCallback { + /** + * Creates a new callback with default priority. + */ + @SuppressWarnings("deprecation") + public XC_MethodHook() { + super(); + } + + /** + * Creates a new callback with a specific priority. + * + *

Note that {@link #afterHookedMethod} will be called in reversed order, i.e. + * the callback with the highest priority will be called last. This way, the callback has the + * final control over the return value. {@link #beforeHookedMethod} is called as usual, i.e. + * highest priority first. + * + * @param priority See {@link XCallback#priority}. + */ + public XC_MethodHook(int priority) { + super(priority); + } + + /** + * Called before the invocation of the method. + * + *

You can use {@link MethodHookParam#setResult} and {@link MethodHookParam#setThrowable} + * to prevent the original method from being called. + * + *

Note that implementations shouldn't call {@code super(param)}, it's not necessary. + * + * @param param Information about the method call. + * @throws Throwable Everything the callback throws is caught and logged. + */ + protected void beforeHookedMethod(MethodHookParam param) throws Throwable {} + + public void callBeforeHookedMethod(MethodHookParam param) throws Throwable { + beforeHookedMethod(param); + } + + /** + * Called after the invocation of the method. + * + *

You can use {@link MethodHookParam#setResult} and {@link MethodHookParam#setThrowable} + * to modify the return value of the original method. + * + *

Note that implementations shouldn't call {@code super(param)}, it's not necessary. + * + * @param param Information about the method call. + * @throws Throwable Everything the callback throws is caught and logged. + */ + protected void afterHookedMethod(MethodHookParam param) throws Throwable {} + + public void callAfterHookedMethod(MethodHookParam param) throws Throwable { + afterHookedMethod(param); + } + + /** + * Wraps information about the method call and allows to influence it. + */ + public static final class MethodHookParam extends XCallback.Param { + /** @hide */ + @SuppressWarnings("deprecation") + public MethodHookParam() { + super(); + } + + /** The hooked method/constructor. */ + public Member method; + + /** The {@code this} reference for an instance method, or {@code null} for static methods. */ + public Object thisObject; + + /** Arguments to the method call. */ + public Object[] args; + + private Object result = null; + private Throwable throwable = null; + public boolean returnEarly = false; + + /** Returns the result of the method call. */ + public Object getResult() { + return result; + } + + /** + * Modify the result of the method call. + * + *

If called from {@link #beforeHookedMethod}, it prevents the call to the original method. + */ + public void setResult(Object result) { + this.result = result; + this.throwable = null; + this.returnEarly = true; + } + + /** Returns the {@link Throwable} thrown by the method, or {@code null}. */ + public Throwable getThrowable() { + return throwable; + } + + /** Returns true if an exception was thrown by the method. */ + public boolean hasThrowable() { + return throwable != null; + } + + /** + * Modify the exception thrown of the method call. + * + *

If called from {@link #beforeHookedMethod}, it prevents the call to the original method. + */ + public void setThrowable(Throwable throwable) { + this.throwable = throwable; + this.result = null; + this.returnEarly = true; + } + + /** Returns the result of the method call, or throws the Throwable caused by it. */ + public Object getResultOrThrowable() throws Throwable { + if (throwable != null) + throw throwable; + return result; + } + } + + /** + * An object with which the method/constructor can be unhooked. + */ + public class Unhook implements IXUnhook { + private final Member hookMethod; + + /*package*/ Unhook(Member hookMethod) { + this.hookMethod = hookMethod; + } + + /** + * Returns the method/constructor that has been hooked. + */ + public Member getHookedMethod() { + return hookMethod; + } + + @Override + public XC_MethodHook getCallback() { + return XC_MethodHook.this; + } + + @SuppressWarnings("deprecation") + @Override + public void unhook() { + XposedBridge.unhookMethod(hookMethod, XC_MethodHook.this); + } + + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java b/Bridge/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java new file mode 100644 index 00000000..37b0a583 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java @@ -0,0 +1,87 @@ +package de.robv.android.xposed; + +import de.robv.android.xposed.callbacks.XCallback; + +/** + * A special case of {@link XC_MethodHook} which completely replaces the original method. + */ +public abstract class XC_MethodReplacement extends XC_MethodHook { + /** + * Creates a new callback with default priority. + */ + public XC_MethodReplacement() { + super(); + } + + /** + * Creates a new callback with a specific priority. + * + * @param priority See {@link XCallback#priority}. + */ + public XC_MethodReplacement(int priority) { + super(priority); + } + + /** @hide */ + @Override + protected final void beforeHookedMethod(MethodHookParam param) throws Throwable { + try { + Object result = replaceHookedMethod(param); + param.setResult(result); + } catch (Throwable t) { + param.setThrowable(t); + } + } + + /** @hide */ + @Override + @SuppressWarnings("EmptyMethod") + protected final void afterHookedMethod(MethodHookParam param) throws Throwable {} + + /** + * Shortcut for replacing a method completely. Whatever is returned/thrown here is taken + * instead of the result of the original method (which will not be called). + * + *

Note that implementations shouldn't call {@code super(param)}, it's not necessary. + * + * @param param Information about the method call. + * @throws Throwable Anything that is thrown by the callback will be passed on to the original caller. + */ + @SuppressWarnings("UnusedParameters") + protected abstract Object replaceHookedMethod(MethodHookParam param) throws Throwable; + + /** + * Predefined callback that skips the method without replacements. + */ + public static final XC_MethodReplacement DO_NOTHING = new XC_MethodReplacement(PRIORITY_HIGHEST*2) { + @Override + protected Object replaceHookedMethod(MethodHookParam param) throws Throwable { + return null; + } + }; + + /** + * Creates a callback which always returns a specific value. + * + * @param result The value that should be returned to callers of the hooked method. + */ + public static XC_MethodReplacement returnConstant(final Object result) { + return returnConstant(PRIORITY_DEFAULT, result); + } + + /** + * Like {@link #returnConstant(Object)}, but allows to specify a priority for the callback. + * + * @param priority See {@link XCallback#priority}. + * @param result The value that should be returned to callers of the hooked method. + */ + public static XC_MethodReplacement returnConstant(int priority, final Object result) { + return new XC_MethodReplacement(priority) { + @Override + protected Object replaceHookedMethod(MethodHookParam param) throws Throwable { + return result; + } + }; + } + +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/XSharedPreferences.java b/Bridge/src/main/java/de/robv/android/xposed/XSharedPreferences.java new file mode 100644 index 00000000..3adb29ab --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/XSharedPreferences.java @@ -0,0 +1,295 @@ +package de.robv.android.xposed; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import de.robv.android.xposed.services.FileResult; + +/** + * This class is basically the same as SharedPreferencesImpl from AOSP, but + * read-only and without listeners support. Instead, it is made to be + * compatible with all ROMs. + */ +public final class XSharedPreferences implements SharedPreferences { + private static final String TAG = "XSharedPreferences"; + private final File mFile; + private final String mFilename; + private Map mMap; + private boolean mLoaded = false; + private long mLastModified; + private long mFileSize; + + /** + * Read settings from the specified file. + * @param prefFile The file to read the preferences from. + */ + public XSharedPreferences(File prefFile) { + mFile = prefFile; + mFilename = mFile.getAbsolutePath(); + startLoadFromDisk(); + } + + /** + * Read settings from the default preferences for a package. + * These preferences are returned by {@link PreferenceManager#getDefaultSharedPreferences}. + * @param packageName The package name. + */ + public XSharedPreferences(String packageName) { + this(packageName, packageName + "_preferences"); + } + + /** + * Read settings from a custom preferences file for a package. + * These preferences are returned by {@link Context#getSharedPreferences(String, int)}. + * @param packageName The package name. + * @param prefFileName The file name without ".xml". + */ + public XSharedPreferences(String packageName, String prefFileName) { + mFile = new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/" + prefFileName + ".xml"); + mFilename = mFile.getAbsolutePath(); + startLoadFromDisk(); + } + + /** + * Tries to make the preferences file world-readable. + * + *

Warning: This is only meant to work around permission "fix" functions that are part + * of some recoveries. It doesn't replace the need to open preferences with {@code MODE_WORLD_READABLE} + * in the module's UI code. Otherwise, Android will set stricter permissions again during the next save. + * + *

This will only work if executed as root (e.g. {@code initZygote()}) and only if SELinux is disabled. + * + * @return {@code true} in case the file could be made world-readable. + */ + @SuppressLint("SetWorldReadable") + public boolean makeWorldReadable() { + if (!SELinuxHelper.getAppDataFileService().hasDirectFileAccess()) + return false; // It doesn't make much sense to make the file readable if we wouldn't be able to access it anyway. + + if (!mFile.exists()) // Just in case - the file should never be created if it doesn't exist. + return false; + + return mFile.setReadable(true, false); + } + + /** + * Returns the file that is backing these preferences. + * + *

Warning: The file might not be accessible directly. + */ + public File getFile() { + return mFile; + } + + private void startLoadFromDisk() { + synchronized (this) { + mLoaded = false; + } + new Thread("XSharedPreferences-load") { + @Override + public void run() { + synchronized (XSharedPreferences.this) { + loadFromDiskLocked(); + } + } + }.start(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void loadFromDiskLocked() { + if (mLoaded) { + return; + } + + Map map = null; + FileResult result = null; + try { + result = SELinuxHelper.getAppDataFileService().getFileInputStream(mFilename, mFileSize, mLastModified); + if (result.stream != null) { + map = XmlUtils.readMapXml(result.stream); + result.stream.close(); + } else { + // The file is unchanged, keep the current values + map = mMap; + } + } catch (XmlPullParserException e) { + Log.w(TAG, "getSharedPreferences", e); + } catch (FileNotFoundException ignored) { + // SharedPreferencesImpl has a canRead() check, so it doesn't log anything in case the file doesn't exist + } catch (IOException e) { + Log.w(TAG, "getSharedPreferences", e); + } finally { + if (result != null && result.stream != null) { + try { + result.stream.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + mLoaded = true; + if (map != null) { + mMap = map; + mLastModified = result.mtime; + mFileSize = result.size; + } else { + mMap = new HashMap<>(); + } + notifyAll(); + } + + /** + * Reload the settings from file if they have changed. + * + *

Warning: With enforcing SELinux, this call might be quite expensive. + */ + public synchronized void reload() { + if (hasFileChanged()) + startLoadFromDisk(); + } + + /** + * Check whether the file has changed since the last time it has been loaded. + * + *

Warning: With enforcing SELinux, this call might be quite expensive. + */ + public synchronized boolean hasFileChanged() { + try { + FileResult result = SELinuxHelper.getAppDataFileService().statFile(mFilename); + return mLastModified != result.mtime || mFileSize != result.size; + } catch (FileNotFoundException ignored) { + // SharedPreferencesImpl doesn't log anything in case the file doesn't exist + return true; + } catch (IOException e) { + Log.w(TAG, "hasFileChanged", e); + return true; + } + } + + private void awaitLoadedLocked() { + while (!mLoaded) { + try { + wait(); + } catch (InterruptedException unused) { + } + } + } + + /** @hide */ + @Override + public Map getAll() { + synchronized (this) { + awaitLoadedLocked(); + return new HashMap<>(mMap); + } + } + + /** @hide */ + @Override + public String getString(String key, String defValue) { + synchronized (this) { + awaitLoadedLocked(); + String v = (String)mMap.get(key); + return v != null ? v : defValue; + } + } + + /** @hide */ + @Override + @SuppressWarnings("unchecked") + public Set getStringSet(String key, Set defValues) { + synchronized (this) { + awaitLoadedLocked(); + Set v = (Set) mMap.get(key); + return v != null ? v : defValues; + } + } + + /** @hide */ + @Override + public int getInt(String key, int defValue) { + synchronized (this) { + awaitLoadedLocked(); + Integer v = (Integer)mMap.get(key); + return v != null ? v : defValue; + } + } + + /** @hide */ + @Override + public long getLong(String key, long defValue) { + synchronized (this) { + awaitLoadedLocked(); + Long v = (Long)mMap.get(key); + return v != null ? v : defValue; + } + } + + /** @hide */ + @Override + public float getFloat(String key, float defValue) { + synchronized (this) { + awaitLoadedLocked(); + Float v = (Float)mMap.get(key); + return v != null ? v : defValue; + } + } + + /** @hide */ + @Override + public boolean getBoolean(String key, boolean defValue) { + synchronized (this) { + awaitLoadedLocked(); + Boolean v = (Boolean)mMap.get(key); + return v != null ? v : defValue; + } + } + + /** @hide */ + @Override + public boolean contains(String key) { + synchronized (this) { + awaitLoadedLocked(); + return mMap.containsKey(key); + } + } + + /** @deprecated Not supported by this implementation. */ + @Deprecated + @Override + public Editor edit() { + throw new UnsupportedOperationException("read-only implementation"); + } + + /** @deprecated Not supported by this implementation. */ + @Deprecated + @Override + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException("listeners are not supported in this implementation"); + } + + /** @deprecated Not supported by this implementation. */ + @Deprecated + @Override + public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException("listeners are not supported in this implementation"); + } + +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/XposedBridge.java b/Bridge/src/main/java/de/robv/android/xposed/XposedBridge.java new file mode 100644 index 00000000..e53dc1c2 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/XposedBridge.java @@ -0,0 +1,529 @@ +package de.robv.android.xposed; + +import android.annotation.SuppressLint; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.elderdrivers.riru.xposed.dexmaker.DynamicBridge; +import de.robv.android.xposed.XC_MethodHook.MethodHookParam; +import de.robv.android.xposed.callbacks.XC_InitPackageResources; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +import static de.robv.android.xposed.XposedHelpers.getIntField; + +/** + * This class contains most of Xposed's central logic, such as initialization and callbacks used by + * the native side. It also includes methods to add new hooks. + */ +@SuppressWarnings("JniMissingFunction") +public final class XposedBridge { + /** + * The system class loader which can be used to locate Android framework classes. + * Application classes cannot be retrieved from it. + * + * @see ClassLoader#getSystemClassLoader + */ + public static final ClassLoader BOOTCLASSLOADER = XposedBridge.class.getClassLoader(); + + /** @hide */ + public static final String TAG = "EdXposed-Bridge"; + + /** @deprecated Use {@link #getXposedVersion()} instead. */ + @Deprecated + public static int XPOSED_BRIDGE_VERSION; + + /*package*/ static boolean isZygote = true; // ed: RuntimeInit.main() tool process not supported yet + + private static int runtime = 2; // ed: only support art + private static final int RUNTIME_DALVIK = 1; + private static final int RUNTIME_ART = 2; + + public static boolean disableHooks = false; + + // This field is set "magically" on MIUI. + /*package*/ static long BOOT_START_TIME; + + private static final Object[] EMPTY_ARRAY = new Object[0]; + + // built-in handlers + private static final Map> sHookedMethodCallbacks = new HashMap<>(); + public static final CopyOnWriteSortedSet sLoadedPackageCallbacks = new CopyOnWriteSortedSet<>(); + /*package*/ static final CopyOnWriteSortedSet sInitPackageResourcesCallbacks = new CopyOnWriteSortedSet<>(); + + private XposedBridge() {} + + /** + * Called when native methods and other things are initialized, but before preloading classes etc. + * @hide + */ + @SuppressWarnings("deprecation") + public static void main(String[] args) { + // ed: moved + } + + /** @hide */ +// protected static final class ToolEntryPoint { +// protected static void main(String[] args) { +// isZygote = false; +// XposedBridge.main(args); +// } +// } + + private static void initXResources() throws IOException { + // ed: no support for now + } + + @SuppressLint("SetWorldReadable") + private static File ensureSuperDexFile(String clz, Class realSuperClz, Class topClz) throws IOException { + XposedBridge.removeFinalFlagNative(realSuperClz); + File dexFile = DexCreator.ensure(clz, realSuperClz, topClz); + dexFile.setReadable(true, false); + return dexFile; + } + +// private static boolean hadInitErrors() { +// // ed: assuming never had errors +// return false; +// } +// private static native int getRuntime(); +// /*package*/ static native boolean startsSystemServer(); +// /*package*/ static native String getStartClassName(); +// /*package*/ native static boolean initXResourcesNative(); + + /** + * Returns the currently installed version of the Xposed framework. + */ + public static int getXposedVersion() { + // ed: fixed value for now + return 90; + } + + /** + * Writes a message to the Xposed error log. + * + *

DON'T FLOOD THE LOG!!! This is only meant for error logging. + * If you want to write information/debug messages, use logcat. + * + * @param text The log message. + */ + public synchronized static void log(String text) { + Log.i(TAG, text); + } + + /** + * Logs a stack trace to the Xposed error log. + * + *

DON'T FLOOD THE LOG!!! This is only meant for error logging. + * If you want to write information/debug messages, use logcat. + * + * @param t The Throwable object for the stack trace. + */ + public synchronized static void log(Throwable t) { + Log.e(TAG, Log.getStackTraceString(t)); + } + + /** + * Hook any method (or constructor) with the specified callback. See below for some wrappers + * that make it easier to find a method/constructor in one step. + * + * @param hookMethod The method to be hooked. + * @param callback The callback to be executed when the hooked method is called. + * @return An object that can be used to remove the hook. + * + * @see XposedHelpers#findAndHookMethod(String, ClassLoader, String, Object...) + * @see XposedHelpers#findAndHookMethod(Class, String, Object...) + * @see #hookAllMethods + * @see XposedHelpers#findAndHookConstructor(String, ClassLoader, Object...) + * @see XposedHelpers#findAndHookConstructor(Class, Object...) + * @see #hookAllConstructors + */ + public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) { + if (!(hookMethod instanceof Method) && !(hookMethod instanceof Constructor)) { + throw new IllegalArgumentException("Only methods and constructors can be hooked: " + hookMethod.toString()); + } else if (hookMethod.getDeclaringClass().isInterface()) { + throw new IllegalArgumentException("Cannot hook interfaces: " + hookMethod.toString()); + } else if (Modifier.isAbstract(hookMethod.getModifiers())) { + throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod.toString()); + } + + if (callback == null) { + throw new IllegalArgumentException("callback should not be null!"); + } + + boolean newMethod = false; + CopyOnWriteSortedSet callbacks; + synchronized (sHookedMethodCallbacks) { + callbacks = sHookedMethodCallbacks.get(hookMethod); + if (callbacks == null) { + callbacks = new CopyOnWriteSortedSet<>(); + sHookedMethodCallbacks.put(hookMethod, callbacks); + newMethod = true; + } + } + callbacks.add(callback); + + if (newMethod) { + Class declaringClass = hookMethod.getDeclaringClass(); + int slot; + Class[] parameterTypes; + Class returnType; + if (runtime == RUNTIME_ART) { + slot = 0; + parameterTypes = null; + returnType = null; + } else if (hookMethod instanceof Method) { + slot = getIntField(hookMethod, "slot"); + parameterTypes = ((Method) hookMethod).getParameterTypes(); + returnType = ((Method) hookMethod).getReturnType(); + } else { + slot = getIntField(hookMethod, "slot"); + parameterTypes = ((Constructor) hookMethod).getParameterTypes(); + returnType = null; + } + + AdditionalHookInfo additionalInfo = new AdditionalHookInfo(callbacks, parameterTypes, returnType); + hookMethodNative(hookMethod, declaringClass, slot, additionalInfo); + } + + return callback.new Unhook(hookMethod); + } + + /** + * Removes the callback for a hooked method/constructor. + * + * @deprecated Use {@link XC_MethodHook.Unhook#unhook} instead. An instance of the {@code Unhook} + * class is returned when you hook the method. + * + * @param hookMethod The method for which the callback should be removed. + * @param callback The reference to the callback as specified in {@link #hookMethod}. + */ + @Deprecated + public static void unhookMethod(Member hookMethod, XC_MethodHook callback) { + CopyOnWriteSortedSet callbacks; + synchronized (sHookedMethodCallbacks) { + callbacks = sHookedMethodCallbacks.get(hookMethod); + if (callbacks == null) + return; + } + callbacks.remove(callback); + } + + /** + * Hooks all methods with a certain name that were declared in the specified class. Inherited + * methods and constructors are not considered. For constructors, use + * {@link #hookAllConstructors} instead. + * + * @param hookClass The class to check for declared methods. + * @param methodName The name of the method(s) to hook. + * @param callback The callback to be executed when the hooked methods are called. + * @return A set containing one object for each found method which can be used to unhook it. + */ + @SuppressWarnings("UnusedReturnValue") + public static Set hookAllMethods(Class hookClass, String methodName, XC_MethodHook callback) { + Set unhooks = new HashSet<>(); + for (Member method : hookClass.getDeclaredMethods()) + if (method.getName().equals(methodName)) + unhooks.add(hookMethod(method, callback)); + return unhooks; + } + + /** + * Hook all constructors of the specified class. + * + * @param hookClass The class to check for constructors. + * @param callback The callback to be executed when the hooked constructors are called. + * @return A set containing one object for each found constructor which can be used to unhook it. + */ + @SuppressWarnings("UnusedReturnValue") + public static Set hookAllConstructors(Class hookClass, XC_MethodHook callback) { + Set unhooks = new HashSet<>(); + for (Member constructor : hookClass.getDeclaredConstructors()) + unhooks.add(hookMethod(constructor, callback)); + return unhooks; + } + + /** + * This method is called as a replacement for hooked methods. + */ + private static Object handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj, + Object thisObject, Object[] args) throws Throwable { + AdditionalHookInfo additionalInfo = (AdditionalHookInfo) additionalInfoObj; + + if (disableHooks) { + try { + return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes, + additionalInfo.returnType, thisObject, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + Object[] callbacksSnapshot = additionalInfo.callbacks.getSnapshot(); + final int callbacksLength = callbacksSnapshot.length; + if (callbacksLength == 0) { + try { + return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes, + additionalInfo.returnType, thisObject, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + MethodHookParam param = new MethodHookParam(); + param.method = method; + param.thisObject = thisObject; + param.args = args; + + // call "before method" callbacks + int beforeIdx = 0; + do { + try { + ((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param); + } catch (Throwable t) { + XposedBridge.log(t); + + // reset result (ignoring what the unexpectedly exiting callback did) + param.setResult(null); + param.returnEarly = false; + continue; + } + + if (param.returnEarly) { + // skip remaining "before" callbacks and corresponding "after" callbacks + beforeIdx++; + break; + } + } while (++beforeIdx < callbacksLength); + + // call original method if not requested otherwise + if (!param.returnEarly) { + try { + param.setResult(invokeOriginalMethodNative(method, originalMethodId, + additionalInfo.parameterTypes, additionalInfo.returnType, param.thisObject, param.args)); + } catch (InvocationTargetException e) { + param.setThrowable(e.getCause()); + } + } + + // call "after method" callbacks + int afterIdx = beforeIdx - 1; + do { + Object lastResult = param.getResult(); + Throwable lastThrowable = param.getThrowable(); + + try { + ((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param); + } catch (Throwable t) { + XposedBridge.log(t); + + // reset to last result (ignoring what the unexpectedly exiting callback did) + if (lastThrowable == null) + param.setResult(lastResult); + else + param.setThrowable(lastThrowable); + } + } while (--afterIdx >= 0); + + // return + if (param.hasThrowable()) + throw param.getThrowable(); + else + return param.getResult(); + } + + /** + * Adds a callback to be executed when an app ("Android package") is loaded. + * + *

You probably don't need to call this. Simply implement {@link IXposedHookLoadPackage} + * in your module class and Xposed will take care of registering it as a callback. + * + * @param callback The callback to be executed. + * @hide + */ + public static void hookLoadPackage(XC_LoadPackage callback) { + synchronized (sLoadedPackageCallbacks) { + sLoadedPackageCallbacks.add(callback); + } + } + + /** + * Adds a callback to be executed when the resources for an app are initialized. + * + *

You probably don't need to call this. Simply implement {@link IXposedHookInitPackageResources} + * in your module class and Xposed will take care of registering it as a callback. + * + * @param callback The callback to be executed. + * @hide + */ + public static void hookInitPackageResources(XC_InitPackageResources callback) { + // TODO not supported yet +// synchronized (sInitPackageResourcesCallbacks) { +// sInitPackageResourcesCallbacks.add(callback); +// } + } + + /** + * Intercept every call to the specified method and call a handler function instead. + * @param method The method to intercept + */ + private synchronized static void hookMethodNative(final Member method, Class declaringClass, + int slot, final Object additionalInfoObj) { + DynamicBridge.hookMethod(method, (AdditionalHookInfo) additionalInfoObj); + + } + + private static Object invokeOriginalMethodNative(Member method, int methodId, + Class[] parameterTypes, + Class returnType, + Object thisObject, Object[] args) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + return DynamicBridge.invokeOriginalMethod(method, thisObject, args); + } + + /** + * Basically the same as {@link Method#invoke}, but calls the original method + * as it was before the interception by Xposed. Also, access permissions are not checked. + * + *

There are very few cases where this method is needed. A common mistake is + * to replace a method and then invoke the original one based on dynamic conditions. This + * creates overhead and skips further hooks by other modules. Instead, just hook (don't replace) + * the method and call {@code param.setResult(null)} in {@link XC_MethodHook#beforeHookedMethod} + * if the original method should be skipped. + * + * @param method The method to be called. + * @param thisObject For non-static calls, the "this" pointer, otherwise {@code null}. + * @param args Arguments for the method call as Object[] array. + * @return The result returned from the invoked method. + * @throws NullPointerException + * if {@code receiver == null} for a non-static method + * @throws IllegalAccessException + * if this method is not accessible (see {@link AccessibleObject}) + * @throws IllegalArgumentException + * if the number of arguments doesn't match the number of parameters, the receiver + * is incompatible with the declaring class, or an argument could not be unboxed + * or converted by a widening conversion to the corresponding parameter type + * @throws InvocationTargetException + * if an exception was thrown by the invoked method + */ + public static Object invokeOriginalMethod(Member method, Object thisObject, Object[] args) + throws NullPointerException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + if (args == null) { + args = EMPTY_ARRAY; + } + + Class[] parameterTypes; + Class returnType; + if (runtime == RUNTIME_ART && (method instanceof Method || method instanceof Constructor)) { + parameterTypes = null; + returnType = null; + } else if (method instanceof Method) { + parameterTypes = ((Method) method).getParameterTypes(); + returnType = ((Method) method).getReturnType(); + } else if (method instanceof Constructor) { + parameterTypes = ((Constructor) method).getParameterTypes(); + returnType = null; + } else { + throw new IllegalArgumentException("method must be of type Method or Constructor"); + } + + return invokeOriginalMethodNative(method, 0, parameterTypes, returnType, thisObject, args); + } + + /*package*/ static void setObjectClass(Object obj, Class clazz) { + if (clazz.isAssignableFrom(obj.getClass())) { + throw new IllegalArgumentException("Cannot transfer object from " + obj.getClass() + " to " + clazz); + } + setObjectClassNative(obj, clazz); + } + + private static native void setObjectClassNative(Object obj, Class clazz); + /*package*/ static native void dumpObjectNative(Object obj); + + /*package*/ static Object cloneToSubclass(Object obj, Class targetClazz) { + if (obj == null) + return null; + + if (!obj.getClass().isAssignableFrom(targetClazz)) + throw new ClassCastException(targetClazz + " doesn't extend " + obj.getClass()); + + return cloneToSubclassNative(obj, targetClazz); + } + + private static native Object cloneToSubclassNative(Object obj, Class targetClazz); + + private static native void removeFinalFlagNative(Class clazz); + +// /*package*/ static native void closeFilesBeforeForkNative(); +// /*package*/ static native void reopenFilesAfterForkNative(); +// +// /*package*/ static native void invalidateCallersNative(Member[] methods); + + /** @hide */ + public static final class CopyOnWriteSortedSet { + private transient volatile Object[] elements = EMPTY_ARRAY; + + @SuppressWarnings("UnusedReturnValue") + public synchronized boolean add(E e) { + int index = indexOf(e); + if (index >= 0) + return false; + + Object[] newElements = new Object[elements.length + 1]; + System.arraycopy(elements, 0, newElements, 0, elements.length); + newElements[elements.length] = e; + Arrays.sort(newElements); + elements = newElements; + return true; + } + + @SuppressWarnings("UnusedReturnValue") + public synchronized boolean remove(E e) { + int index = indexOf(e); + if (index == -1) + return false; + + Object[] newElements = new Object[elements.length - 1]; + System.arraycopy(elements, 0, newElements, 0, index); + System.arraycopy(elements, index + 1, newElements, index, elements.length - index - 1); + elements = newElements; + return true; + } + + private int indexOf(Object o) { + for (int i = 0; i < elements.length; i++) { + if (o.equals(elements[i])) + return i; + } + return -1; + } + + public Object[] getSnapshot() { + return elements; + } + } + + public static class AdditionalHookInfo { + public final CopyOnWriteSortedSet callbacks; + public final Class[] parameterTypes; + public final Class returnType; + + private AdditionalHookInfo(CopyOnWriteSortedSet callbacks, Class[] parameterTypes, Class returnType) { + this.callbacks = callbacks; + this.parameterTypes = parameterTypes; + this.returnType = returnType; + } + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/XposedHelpers.java b/Bridge/src/main/java/de/robv/android/xposed/XposedHelpers.java new file mode 100644 index 00000000..337cd4f9 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/XposedHelpers.java @@ -0,0 +1,1725 @@ +package de.robv.android.xposed; + +import android.content.res.AssetManager; +import android.content.res.Resources; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.ZipFile; + +import dalvik.system.DexFile; +import external.org.apache.commons.lang3.ClassUtils; +import external.org.apache.commons.lang3.reflect.MemberUtils; + +/** + * Helpers that simplify hooking and calling methods/constructors, getting and settings fields, ... + */ +public final class XposedHelpers { + private XposedHelpers() {} + + private static final HashMap fieldCache = new HashMap<>(); + private static final HashMap methodCache = new HashMap<>(); + private static final HashMap> constructorCache = new HashMap<>(); + private static final WeakHashMap> additionalFields = new WeakHashMap<>(); + private static final HashMap> sMethodDepth = new HashMap<>(); + + /** + * Look up a class with the specified class loader. + * + *

There are various allowed syntaxes for the class name, but it's recommended to use one of + * these: + *

    + *
  • {@code java.lang.String} + *
  • {@code java.lang.String[]} (array) + *
  • {@code android.app.ActivityThread.ResourcesKey} + *
  • {@code android.app.ActivityThread$ResourcesKey} + *
+ * + * @param className The class name in one of the formats mentioned above. + * @param classLoader The class loader, or {@code null} for the boot class loader. + * @return A reference to the class. + * @throws ClassNotFoundError In case the class was not found. + */ + public static Class findClass(String className, ClassLoader classLoader) { + if (classLoader == null) + classLoader = XposedBridge.BOOTCLASSLOADER; + try { + return ClassUtils.getClass(classLoader, className, false); + } catch (ClassNotFoundException e) { + throw new ClassNotFoundError(e); + } + } + + /** + * Look up and return a class if it exists. + * Like {@link #findClass}, but doesn't throw an exception if the class doesn't exist. + * + * @param className The class name. + * @param classLoader The class loader, or {@code null} for the boot class loader. + * @return A reference to the class, or {@code null} if it doesn't exist. + */ + public static Class findClassIfExists(String className, ClassLoader classLoader) { + try { + return findClass(className, classLoader); + } catch (ClassNotFoundError e) { + return null; + } + } + + /** + * Look up a field in a class and set it to accessible. + * + * @param clazz The class which either declares or inherits the field. + * @param fieldName The field name. + * @return A reference to the field. + * @throws NoSuchFieldError In case the field was not found. + */ + public static Field findField(Class clazz, String fieldName) { + String fullFieldName = clazz.getName() + '#' + fieldName; + + if (fieldCache.containsKey(fullFieldName)) { + Field field = fieldCache.get(fullFieldName); + if (field == null) + throw new NoSuchFieldError(fullFieldName); + return field; + } + + try { + Field field = findFieldRecursiveImpl(clazz, fieldName); + field.setAccessible(true); + fieldCache.put(fullFieldName, field); + return field; + } catch (NoSuchFieldException e) { + fieldCache.put(fullFieldName, null); + throw new NoSuchFieldError(fullFieldName); + } + } + + /** + * Look up and return a field if it exists. + * Like {@link #findField}, but doesn't throw an exception if the field doesn't exist. + * + * @param clazz The class which either declares or inherits the field. + * @param fieldName The field name. + * @return A reference to the field, or {@code null} if it doesn't exist. + */ + public static Field findFieldIfExists(Class clazz, String fieldName) { + try { + return findField(clazz, fieldName); + } catch (NoSuchFieldError e) { + return null; + } + } + + private static Field findFieldRecursiveImpl(Class clazz, String fieldName) throws NoSuchFieldException { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + while (true) { + clazz = clazz.getSuperclass(); + if (clazz == null || clazz.equals(Object.class)) + break; + + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException ignored) {} + } + throw e; + } + } + + /** + * Returns the first field of the given type in a class. + * Might be useful for Proguard'ed classes to identify fields with unique types. + * + * @param clazz The class which either declares or inherits the field. + * @param type The type of the field. + * @return A reference to the first field of the given type. + * @throws NoSuchFieldError In case no matching field was not found. + */ + public static Field findFirstFieldByExactType(Class clazz, Class type) { + Class clz = clazz; + do { + for (Field field : clz.getDeclaredFields()) { + if (field.getType() == type) { + field.setAccessible(true); + return field; + } + } + } while ((clz = clz.getSuperclass()) != null); + + throw new NoSuchFieldError("Field of type " + type.getName() + " in class " + clazz.getName()); + } + + /** + * Look up a method and hook it. See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} + * for details. + */ + public static XC_MethodHook.Unhook findAndHookMethod(Class clazz, String methodName, Object... parameterTypesAndCallback) { + if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length-1] instanceof XC_MethodHook)) + throw new IllegalArgumentException("no callback defined"); + + XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length-1]; + Method m = findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback)); + + return XposedBridge.hookMethod(m, callback); + } + + /** + * Look up a method and hook it. The last argument must be the callback for the hook. + * + *

This combines calls to {@link #findMethodExact(Class, String, Object...)} and + * {@link XposedBridge#hookMethod}. + * + *

The method must be declared or overridden in the given class, inherited + * methods are not considered! That's because each method implementation exists only once in + * the memory, and when classes inherit it, they just get another reference to the implementation. + * Hooking a method therefore applies to all classes inheriting the same implementation. You + * have to expect that the hook applies to subclasses (unless they override the method), but you + * shouldn't have to worry about hooks applying to superclasses, hence this "limitation". + * There could be undesired or even dangerous hooks otherwise, e.g. if you hook + * {@code SomeClass.equals()} and that class doesn't override the {@code equals()} on some ROMs, + * making you hook {@code Object.equals()} instead. + * + *

There are two ways to specify the parameter types. If you already have a reference to the + * {@link Class}, use that. For Android framework classes, you can often use something like + * {@code String.class}. If you don't have the class reference, you can simply use the + * full class name as a string, e.g. {@code java.lang.String} or {@code com.example.MyClass}. + * It will be passed to {@link #findClass} with the same class loader that is used for the target + * method, see its documentation for the allowed notations. + * + *

Primitive types, such as {@code int}, can be specified using {@code int.class} (recommended) + * or {@code Integer.TYPE}. Note that {@code Integer.class} doesn't refer to {@code int} but to + * {@code Integer}, which is a normal class (boxed primitive). Therefore it must not be used when + * the method expects an {@code int} parameter - it has to be used for {@code Integer} parameters + * though, so check the method signature in detail. + * + *

As last argument to this method (after the list of target method parameters), you need + * to specify the callback that should be executed when the method is invoked. It's usually + * an anonymous subclass of {@link XC_MethodHook} or {@link XC_MethodReplacement}. + * + *

Example + *

+	 * // In order to hook this method ...
+	 * package com.example;
+	 * public class SomeClass {
+	 *   public int doSomething(String s, int i, MyClass m) {
+	 *     ...
+	 *   }
+	 * }
+	 *
+	 * // ... you can use this call:
+	 * findAndHookMethod("com.example.SomeClass", lpparam.classLoader, String.class, int.class, "com.example.MyClass", new XC_MethodHook() {
+	 *   @Override
+	 *   protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
+	 *     String oldText = (String) param.args[0];
+	 *     Log.d("MyModule", oldText);
+	 *
+	 *     param.args[0] = "test";
+	 *     param.args[1] = 42; // auto-boxing is working here
+	 *     setBooleanField(param.args[2], "great", true);
+	 *
+	 *     // This would not work (as MyClass can't be resolved at compile time):
+	 *     //   MyClass myClass = (MyClass) param.args[2];
+	 *     //   myClass.great = true;
+	 *   }
+	 * });
+	 * 
+ * + * @param className The name of the class which implements the method. + * @param classLoader The class loader for resolving the target and parameter classes. + * @param methodName The target method name. + * @param parameterTypesAndCallback The parameter types of the target method, plus the callback. + * @throws NoSuchMethodError In case the method was not found. + * @throws ClassNotFoundError In case the target class or one of the parameter types couldn't be resolved. + * @return An object which can be used to remove the callback again. + */ + public static XC_MethodHook.Unhook findAndHookMethod(String className, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback) { + return findAndHookMethod(findClass(className, classLoader), methodName, parameterTypesAndCallback); + } + + /** + * Look up a method in a class and set it to accessible. + * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. + */ + public static Method findMethodExact(Class clazz, String methodName, Object... parameterTypes) { + return findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypes)); + } + + /** + * Look up and return a method if it exists. + * See {@link #findMethodExactIfExists(String, ClassLoader, String, Object...)} for details. + */ + public static Method findMethodExactIfExists(Class clazz, String methodName, Object... parameterTypes) { + try { + return findMethodExact(clazz, methodName, parameterTypes); + } catch (ClassNotFoundError | NoSuchMethodError e) { + return null; + } + } + + /** + * Look up a method in a class and set it to accessible. + * The method must be declared or overridden in the given class. + * + *

See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} for details about + * the method and parameter type resolution. + * + * @param className The name of the class which implements the method. + * @param classLoader The class loader for resolving the target and parameter classes. + * @param methodName The target method name. + * @param parameterTypes The parameter types of the target method. + * @throws NoSuchMethodError In case the method was not found. + * @throws ClassNotFoundError In case the target class or one of the parameter types couldn't be resolved. + * @return A reference to the method. + */ + public static Method findMethodExact(String className, ClassLoader classLoader, String methodName, Object... parameterTypes) { + return findMethodExact(findClass(className, classLoader), methodName, getParameterClasses(classLoader, parameterTypes)); + } + + /** + * Look up and return a method if it exists. + * Like {@link #findMethodExact(String, ClassLoader, String, Object...)}, but doesn't throw an + * exception if the method doesn't exist. + * + * @param className The name of the class which implements the method. + * @param classLoader The class loader for resolving the target and parameter classes. + * @param methodName The target method name. + * @param parameterTypes The parameter types of the target method. + * @return A reference to the method, or {@code null} if it doesn't exist. + */ + public static Method findMethodExactIfExists(String className, ClassLoader classLoader, String methodName, Object... parameterTypes) { + try { + return findMethodExact(className, classLoader, methodName, parameterTypes); + } catch (ClassNotFoundError | NoSuchMethodError e) { + return null; + } + } + + /** + * Look up a method in a class and set it to accessible. + * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. + * + *

This variant requires that you already have reference to all the parameter types. + */ + public static Method findMethodExact(Class clazz, String methodName, Class... parameterTypes) { + String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#exact"; + + if (methodCache.containsKey(fullMethodName)) { + Method method = methodCache.get(fullMethodName); + if (method == null) + throw new NoSuchMethodError(fullMethodName); + return method; + } + + try { + Method method = clazz.getDeclaredMethod(methodName, parameterTypes); + method.setAccessible(true); + methodCache.put(fullMethodName, method); + return method; + } catch (NoSuchMethodException e) { + methodCache.put(fullMethodName, null); + throw new NoSuchMethodError(fullMethodName); + } + } + + /** + * Returns an array of all methods declared/overridden in a class with the specified parameter types. + * + *

The return type is optional, it will not be compared if it is {@code null}. + * Use {@code void.class} if you want to search for methods returning nothing. + * + * @param clazz The class to look in. + * @param returnType The return type, or {@code null} (see above). + * @param parameterTypes The parameter types. + * @return An array with matching methods, all set to accessible already. + */ + public static Method[] findMethodsByExactParameters(Class clazz, Class returnType, Class... parameterTypes) { + List result = new LinkedList<>(); + for (Method method : clazz.getDeclaredMethods()) { + if (returnType != null && returnType != method.getReturnType()) + continue; + + Class[] methodParameterTypes = method.getParameterTypes(); + if (parameterTypes.length != methodParameterTypes.length) + continue; + + boolean match = true; + for (int i = 0; i < parameterTypes.length; i++) { + if (parameterTypes[i] != methodParameterTypes[i]) { + match = false; + break; + } + } + + if (!match) + continue; + + method.setAccessible(true); + result.add(method); + } + return result.toArray(new Method[result.size()]); + } + + /** + * Look up a method in a class and set it to accessible. + * + *

This does'nt only look for exact matches, but for the best match. All considered candidates + * must be compatible with the given parameter types, i.e. the parameters must be assignable + * to the method's formal parameters. Inherited methods are considered here. + * + * @param clazz The class which declares, inherits or overrides the method. + * @param methodName The method name. + * @param parameterTypes The types of the method's parameters. + * @return A reference to the best-matching method. + * @throws NoSuchMethodError In case no suitable method was found. + */ + public static Method findMethodBestMatch(Class clazz, String methodName, Class... parameterTypes) { + String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#bestmatch"; + + if (methodCache.containsKey(fullMethodName)) { + Method method = methodCache.get(fullMethodName); + if (method == null) + throw new NoSuchMethodError(fullMethodName); + return method; + } + + try { + Method method = findMethodExact(clazz, methodName, parameterTypes); + methodCache.put(fullMethodName, method); + return method; + } catch (NoSuchMethodError ignored) {} + + Method bestMatch = null; + Class clz = clazz; + boolean considerPrivateMethods = true; + do { + for (Method method : clz.getDeclaredMethods()) { + // don't consider private methods of superclasses + if (!considerPrivateMethods && Modifier.isPrivate(method.getModifiers())) + continue; + + // compare name and parameters + if (method.getName().equals(methodName) && ClassUtils.isAssignable(parameterTypes, method.getParameterTypes(), true)) { + // get accessible version of method + if (bestMatch == null || MemberUtils.compareParameterTypes( + method.getParameterTypes(), + bestMatch.getParameterTypes(), + parameterTypes) < 0) { + bestMatch = method; + } + } + } + considerPrivateMethods = false; + } while ((clz = clz.getSuperclass()) != null); + + if (bestMatch != null) { + bestMatch.setAccessible(true); + methodCache.put(fullMethodName, bestMatch); + return bestMatch; + } else { + NoSuchMethodError e = new NoSuchMethodError(fullMethodName); + methodCache.put(fullMethodName, null); + throw e; + } + } + + /** + * Look up a method in a class and set it to accessible. + * + *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant + * determines the parameter types from the classes of the given objects. + */ + public static Method findMethodBestMatch(Class clazz, String methodName, Object... args) { + return findMethodBestMatch(clazz, methodName, getParameterTypes(args)); + } + + /** + * Look up a method in a class and set it to accessible. + * + *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant + * determines the parameter types from the classes of the given objects. For any item that is + * {@code null}, the type is taken from {@code parameterTypes} instead. + */ + public static Method findMethodBestMatch(Class clazz, String methodName, Class[] parameterTypes, Object[] args) { + Class[] argsClasses = null; + for (int i = 0; i < parameterTypes.length; i++) { + if (parameterTypes[i] != null) + continue; + if (argsClasses == null) + argsClasses = getParameterTypes(args); + parameterTypes[i] = argsClasses[i]; + } + return findMethodBestMatch(clazz, methodName, parameterTypes); + } + + /** + * Returns an array with the classes of the given objects. + */ + public static Class[] getParameterTypes(Object... args) { + Class[] clazzes = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + clazzes[i] = (args[i] != null) ? args[i].getClass() : null; + } + return clazzes; + } + + /** + * Retrieve classes from an array, where each element might either be a Class + * already, or a String with the full class name. + */ + private static Class[] getParameterClasses(ClassLoader classLoader, Object[] parameterTypesAndCallback) { + Class[] parameterClasses = null; + for (int i = parameterTypesAndCallback.length - 1; i >= 0; i--) { + Object type = parameterTypesAndCallback[i]; + if (type == null) + throw new ClassNotFoundError("parameter type must not be null", null); + + // ignore trailing callback + if (type instanceof XC_MethodHook) + continue; + + if (parameterClasses == null) + parameterClasses = new Class[i+1]; + + if (type instanceof Class) + parameterClasses[i] = (Class) type; + else if (type instanceof String) + parameterClasses[i] = findClass((String) type, classLoader); + else + throw new ClassNotFoundError("parameter type must either be specified as Class or String", null); + } + + // if there are no arguments for the method + if (parameterClasses == null) + parameterClasses = new Class[0]; + + return parameterClasses; + } + + /** + * Returns an array of the given classes. + */ + public static Class[] getClassesAsArray(Class... clazzes) { + return clazzes; + } + + private static String getParametersString(Class... clazzes) { + StringBuilder sb = new StringBuilder("("); + boolean first = true; + for (Class clazz : clazzes) { + if (first) + first = false; + else + sb.append(","); + + if (clazz != null) + sb.append(clazz.getCanonicalName()); + else + sb.append("null"); + } + sb.append(")"); + return sb.toString(); + } + + /** + * Look up a constructor of a class and set it to accessible. + * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. + */ + public static Constructor findConstructorExact(Class clazz, Object... parameterTypes) { + return findConstructorExact(clazz, getParameterClasses(clazz.getClassLoader(), parameterTypes)); + } + + /** + * Look up and return a constructor if it exists. + * See {@link #findMethodExactIfExists(String, ClassLoader, String, Object...)} for details. + */ + public static Constructor findConstructorExactIfExists(Class clazz, Object... parameterTypes) { + try { + return findConstructorExact(clazz, parameterTypes); + } catch (ClassNotFoundError | NoSuchMethodError e) { + return null; + } + } + + /** + * Look up a constructor of a class and set it to accessible. + * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. + */ + public static Constructor findConstructorExact(String className, ClassLoader classLoader, Object... parameterTypes) { + return findConstructorExact(findClass(className, classLoader), getParameterClasses(classLoader, parameterTypes)); + } + + /** + * Look up and return a constructor if it exists. + * See {@link #findMethodExactIfExists(String, ClassLoader, String, Object...)} for details. + */ + public static Constructor findConstructorExactIfExists(String className, ClassLoader classLoader, Object... parameterTypes) { + try { + return findConstructorExact(className, classLoader, parameterTypes); + } catch (ClassNotFoundError | NoSuchMethodError e) { + return null; + } + } + + /** + * Look up a constructor of a class and set it to accessible. + * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. + */ + public static Constructor findConstructorExact(Class clazz, Class... parameterTypes) { + String fullConstructorName = clazz.getName() + getParametersString(parameterTypes) + "#exact"; + + if (constructorCache.containsKey(fullConstructorName)) { + Constructor constructor = constructorCache.get(fullConstructorName); + if (constructor == null) + throw new NoSuchMethodError(fullConstructorName); + return constructor; + } + + try { + Constructor constructor = clazz.getDeclaredConstructor(parameterTypes); + constructor.setAccessible(true); + constructorCache.put(fullConstructorName, constructor); + return constructor; + } catch (NoSuchMethodException e) { + constructorCache.put(fullConstructorName, null); + throw new NoSuchMethodError(fullConstructorName); + } + } + + /** + * Look up a constructor and hook it. See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} + * for details. + */ + public static XC_MethodHook.Unhook findAndHookConstructor(Class clazz, Object... parameterTypesAndCallback) { + if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length-1] instanceof XC_MethodHook)) + throw new IllegalArgumentException("no callback defined"); + + XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length-1]; + Constructor m = findConstructorExact(clazz, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback)); + + return XposedBridge.hookMethod(m, callback); + } + + /** + * Look up a constructor and hook it. See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} + * for details. + */ + public static XC_MethodHook.Unhook findAndHookConstructor(String className, ClassLoader classLoader, Object... parameterTypesAndCallback) { + return findAndHookConstructor(findClass(className, classLoader), parameterTypesAndCallback); + } + + /** + * Look up a constructor in a class and set it to accessible. + * + *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. + */ + public static Constructor findConstructorBestMatch(Class clazz, Class... parameterTypes) { + String fullConstructorName = clazz.getName() + getParametersString(parameterTypes) + "#bestmatch"; + + if (constructorCache.containsKey(fullConstructorName)) { + Constructor constructor = constructorCache.get(fullConstructorName); + if (constructor == null) + throw new NoSuchMethodError(fullConstructorName); + return constructor; + } + + try { + Constructor constructor = findConstructorExact(clazz, parameterTypes); + constructorCache.put(fullConstructorName, constructor); + return constructor; + } catch (NoSuchMethodError ignored) {} + + Constructor bestMatch = null; + Constructor[] constructors = clazz.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + // compare name and parameters + if (ClassUtils.isAssignable(parameterTypes, constructor.getParameterTypes(), true)) { + // get accessible version of method + if (bestMatch == null || MemberUtils.compareParameterTypes( + constructor.getParameterTypes(), + bestMatch.getParameterTypes(), + parameterTypes) < 0) { + bestMatch = constructor; + } + } + } + + if (bestMatch != null) { + bestMatch.setAccessible(true); + constructorCache.put(fullConstructorName, bestMatch); + return bestMatch; + } else { + NoSuchMethodError e = new NoSuchMethodError(fullConstructorName); + constructorCache.put(fullConstructorName, null); + throw e; + } + } + + /** + * Look up a constructor in a class and set it to accessible. + * + *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant + * determines the parameter types from the classes of the given objects. + */ + public static Constructor findConstructorBestMatch(Class clazz, Object... args) { + return findConstructorBestMatch(clazz, getParameterTypes(args)); + } + + /** + * Look up a constructor in a class and set it to accessible. + * + *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant + * determines the parameter types from the classes of the given objects. For any item that is + * {@code null}, the type is taken from {@code parameterTypes} instead. + */ + public static Constructor findConstructorBestMatch(Class clazz, Class[] parameterTypes, Object[] args) { + Class[] argsClasses = null; + for (int i = 0; i < parameterTypes.length; i++) { + if (parameterTypes[i] != null) + continue; + if (argsClasses == null) + argsClasses = getParameterTypes(args); + parameterTypes[i] = argsClasses[i]; + } + return findConstructorBestMatch(clazz, parameterTypes); + } + + /** + * Thrown when a class loader is unable to find a class. Unlike {@link ClassNotFoundException}, + * callers are not forced to explicitly catch this. If uncaught, the error will be passed to the + * next caller in the stack. + */ + public static final class ClassNotFoundError extends Error { + private static final long serialVersionUID = -1070936889459514628L; + + /** @hide */ + public ClassNotFoundError(Throwable cause) { + super(cause); + } + + /** @hide */ + public ClassNotFoundError(String detailMessage, Throwable cause) { + super(detailMessage, cause); + } + } + + /** + * Returns the index of the first parameter declared with the given type. + * + * @throws NoSuchFieldError if there is no parameter with that type. + * @hide + */ + public static int getFirstParameterIndexByType(Member method, Class type) { + Class[] classes = (method instanceof Method) ? + ((Method) method).getParameterTypes() : ((Constructor) method).getParameterTypes(); + for (int i = 0 ; i < classes.length; i++) { + if (classes[i] == type) { + return i; + } + } + throw new NoSuchFieldError("No parameter of type " + type + " found in " + method); + } + + /** + * Returns the index of the parameter declared with the given type, ensuring that there is exactly one such parameter. + * + * @throws NoSuchFieldError if there is no or more than one parameter with that type. + * @hide + */ + public static int getParameterIndexByType(Member method, Class type) { + Class[] classes = (method instanceof Method) ? + ((Method) method).getParameterTypes() : ((Constructor) method).getParameterTypes(); + int idx = -1; + for (int i = 0 ; i < classes.length; i++) { + if (classes[i] == type) { + if (idx == -1) { + idx = i; + } else { + throw new NoSuchFieldError("More than one parameter of type " + type + " found in " + method); + } + } + } + if (idx != -1) { + return idx; + } else { + throw new NoSuchFieldError("No parameter of type " + type + " found in " + method); + } + } + + //################################################################################################# + /** Sets the value of an object field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static void setObjectField(Object obj, String fieldName, Object value) { + try { + findField(obj.getClass(), fieldName).set(obj, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a {@code boolean} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static void setBooleanField(Object obj, String fieldName, boolean value) { + try { + findField(obj.getClass(), fieldName).setBoolean(obj, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a {@code byte} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static void setByteField(Object obj, String fieldName, byte value) { + try { + findField(obj.getClass(), fieldName).setByte(obj, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a {@code char} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static void setCharField(Object obj, String fieldName, char value) { + try { + findField(obj.getClass(), fieldName).setChar(obj, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a {@code double} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static void setDoubleField(Object obj, String fieldName, double value) { + try { + findField(obj.getClass(), fieldName).setDouble(obj, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a {@code float} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static void setFloatField(Object obj, String fieldName, float value) { + try { + findField(obj.getClass(), fieldName).setFloat(obj, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of an {@code int} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static void setIntField(Object obj, String fieldName, int value) { + try { + findField(obj.getClass(), fieldName).setInt(obj, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a {@code long} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static void setLongField(Object obj, String fieldName, long value) { + try { + findField(obj.getClass(), fieldName).setLong(obj, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a {@code short} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static void setShortField(Object obj, String fieldName, short value) { + try { + findField(obj.getClass(), fieldName).setShort(obj, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + //################################################################################################# + /** Returns the value of an object field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static Object getObjectField(Object obj, String fieldName) { + try { + return findField(obj.getClass(), fieldName).get(obj); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** For inner classes, returns the surrounding instance, i.e. the {@code this} reference of the surrounding class. */ + public static Object getSurroundingThis(Object obj) { + return getObjectField(obj, "this$0"); + } + + /** Returns the value of a {@code boolean} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean getBooleanField(Object obj, String fieldName) { + try { + return findField(obj.getClass(), fieldName).getBoolean(obj); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Returns the value of a {@code byte} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static byte getByteField(Object obj, String fieldName) { + try { + return findField(obj.getClass(), fieldName).getByte(obj); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Returns the value of a {@code char} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static char getCharField(Object obj, String fieldName) { + try { + return findField(obj.getClass(), fieldName).getChar(obj); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Returns the value of a {@code double} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static double getDoubleField(Object obj, String fieldName) { + try { + return findField(obj.getClass(), fieldName).getDouble(obj); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Returns the value of a {@code float} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static float getFloatField(Object obj, String fieldName) { + try { + return findField(obj.getClass(), fieldName).getFloat(obj); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Returns the value of an {@code int} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static int getIntField(Object obj, String fieldName) { + try { + return findField(obj.getClass(), fieldName).getInt(obj); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Returns the value of a {@code long} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static long getLongField(Object obj, String fieldName) { + try { + return findField(obj.getClass(), fieldName).getLong(obj); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Returns the value of a {@code short} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ + public static short getShortField(Object obj, String fieldName) { + try { + return findField(obj.getClass(), fieldName).getShort(obj); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + //################################################################################################# + /** Sets the value of a static object field in the given class. See also {@link #findField}. */ + public static void setStaticObjectField(Class clazz, String fieldName, Object value) { + try { + findField(clazz, fieldName).set(null, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code boolean} field in the given class. See also {@link #findField}. */ + public static void setStaticBooleanField(Class clazz, String fieldName, boolean value) { + try { + findField(clazz, fieldName).setBoolean(null, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code byte} field in the given class. See also {@link #findField}. */ + public static void setStaticByteField(Class clazz, String fieldName, byte value) { + try { + findField(clazz, fieldName).setByte(null, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code char} field in the given class. See also {@link #findField}. */ + public static void setStaticCharField(Class clazz, String fieldName, char value) { + try { + findField(clazz, fieldName).setChar(null, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code double} field in the given class. See also {@link #findField}. */ + public static void setStaticDoubleField(Class clazz, String fieldName, double value) { + try { + findField(clazz, fieldName).setDouble(null, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code float} field in the given class. See also {@link #findField}. */ + public static void setStaticFloatField(Class clazz, String fieldName, float value) { + try { + findField(clazz, fieldName).setFloat(null, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code int} field in the given class. See also {@link #findField}. */ + public static void setStaticIntField(Class clazz, String fieldName, int value) { + try { + findField(clazz, fieldName).setInt(null, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code long} field in the given class. See also {@link #findField}. */ + public static void setStaticLongField(Class clazz, String fieldName, long value) { + try { + findField(clazz, fieldName).setLong(null, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code short} field in the given class. See also {@link #findField}. */ + public static void setStaticShortField(Class clazz, String fieldName, short value) { + try { + findField(clazz, fieldName).setShort(null, value); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + //################################################################################################# + /** Returns the value of a static object field in the given class. See also {@link #findField}. */ + public static Object getStaticObjectField(Class clazz, String fieldName) { + try { + return findField(clazz, fieldName).get(null); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Returns the value of a static {@code boolean} field in the given class. See also {@link #findField}. */ + public static boolean getStaticBooleanField(Class clazz, String fieldName) { + try { + return findField(clazz, fieldName).getBoolean(null); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code byte} field in the given class. See also {@link #findField}. */ + public static byte getStaticByteField(Class clazz, String fieldName) { + try { + return findField(clazz, fieldName).getByte(null); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code char} field in the given class. See also {@link #findField}. */ + public static char getStaticCharField(Class clazz, String fieldName) { + try { + return findField(clazz, fieldName).getChar(null); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code double} field in the given class. See also {@link #findField}. */ + public static double getStaticDoubleField(Class clazz, String fieldName) { + try { + return findField(clazz, fieldName).getDouble(null); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code float} field in the given class. See also {@link #findField}. */ + public static float getStaticFloatField(Class clazz, String fieldName) { + try { + return findField(clazz, fieldName).getFloat(null); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code int} field in the given class. See also {@link #findField}. */ + public static int getStaticIntField(Class clazz, String fieldName) { + try { + return findField(clazz, fieldName).getInt(null); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code long} field in the given class. See also {@link #findField}. */ + public static long getStaticLongField(Class clazz, String fieldName) { + try { + return findField(clazz, fieldName).getLong(null); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + /** Sets the value of a static {@code short} field in the given class. See also {@link #findField}. */ + public static short getStaticShortField(Class clazz, String fieldName) { + try { + return findField(clazz, fieldName).getShort(null); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } + } + + //################################################################################################# + /** + * Calls an instance or static method of the given object. + * The method is resolved using {@link #findMethodBestMatch(Class, String, Object...)}. + * + * @param obj The object instance. A class reference is not sufficient! + * @param methodName The method name. + * @param args The arguments for the method call. + * @throws NoSuchMethodError In case no suitable method was found. + * @throws InvocationTargetError In case an exception was thrown by the invoked method. + */ + public static Object callMethod(Object obj, String methodName, Object... args) { + try { + return findMethodBestMatch(obj.getClass(), methodName, args).invoke(obj, args); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } catch (InvocationTargetException e) { + throw new InvocationTargetError(e.getCause()); + } + } + + /** + * Calls an instance or static method of the given object. + * See {@link #callMethod(Object, String, Object...)}. + * + *

This variant allows you to specify parameter types, which can help in case there are multiple + * methods with the same name, especially if you call it with {@code null} parameters. + */ + public static Object callMethod(Object obj, String methodName, Class[] parameterTypes, Object... args) { + try { + return findMethodBestMatch(obj.getClass(), methodName, parameterTypes, args).invoke(obj, args); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } catch (InvocationTargetException e) { + throw new InvocationTargetError(e.getCause()); + } + } + + /** + * Calls a static method of the given class. + * The method is resolved using {@link #findMethodBestMatch(Class, String, Object...)}. + * + * @param clazz The class reference. + * @param methodName The method name. + * @param args The arguments for the method call. + * @throws NoSuchMethodError In case no suitable method was found. + * @throws InvocationTargetError In case an exception was thrown by the invoked method. + */ + public static Object callStaticMethod(Class clazz, String methodName, Object... args) { + try { + return findMethodBestMatch(clazz, methodName, args).invoke(null, args); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } catch (InvocationTargetException e) { + throw new InvocationTargetError(e.getCause()); + } + } + + /** + * Calls a static method of the given class. + * See {@link #callStaticMethod(Class, String, Object...)}. + * + *

This variant allows you to specify parameter types, which can help in case there are multiple + * methods with the same name, especially if you call it with {@code null} parameters. + */ + public static Object callStaticMethod(Class clazz, String methodName, Class[] parameterTypes, Object... args) { + try { + return findMethodBestMatch(clazz, methodName, parameterTypes, args).invoke(null, args); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } catch (InvocationTargetException e) { + throw new InvocationTargetError(e.getCause()); + } + } + + /** + * This class provides a wrapper for an exception thrown by a method invocation. + * + * @see #callMethod(Object, String, Object...) + * @see #callStaticMethod(Class, String, Object...) + * @see #newInstance(Class, Object...) + */ + public static final class InvocationTargetError extends Error { + private static final long serialVersionUID = -1070936889459514628L; + + /** @hide */ + public InvocationTargetError(Throwable cause) { + super(cause); + } + } + + //################################################################################################# + /** + * Creates a new instance of the given class. + * The constructor is resolved using {@link #findConstructorBestMatch(Class, Object...)}. + * + * @param clazz The class reference. + * @param args The arguments for the constructor call. + * @throws NoSuchMethodError In case no suitable constructor was found. + * @throws InvocationTargetError In case an exception was thrown by the invoked method. + * @throws InstantiationError In case the class cannot be instantiated. + */ + public static Object newInstance(Class clazz, Object... args) { + try { + return findConstructorBestMatch(clazz, args).newInstance(args); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } catch (InvocationTargetException e) { + throw new InvocationTargetError(e.getCause()); + } catch (InstantiationException e) { + throw new InstantiationError(e.getMessage()); + } + } + + /** + * Creates a new instance of the given class. + * See {@link #newInstance(Class, Object...)}. + * + *

This variant allows you to specify parameter types, which can help in case there are multiple + * constructors with the same name, especially if you call it with {@code null} parameters. + */ + public static Object newInstance(Class clazz, Class[] parameterTypes, Object... args) { + try { + return findConstructorBestMatch(clazz, parameterTypes, args).newInstance(args); + } catch (IllegalAccessException e) { + // should not happen + XposedBridge.log(e); + throw new IllegalAccessError(e.getMessage()); + } catch (IllegalArgumentException e) { + throw e; + } catch (InvocationTargetException e) { + throw new InvocationTargetError(e.getCause()); + } catch (InstantiationException e) { + throw new InstantiationError(e.getMessage()); + } + } + + //################################################################################################# + + /** + * Attaches any value to an object instance. This simulates adding an instance field. + * The value can be retrieved again with {@link #getAdditionalInstanceField}. + * + * @param obj The object instance for which the value should be stored. + * @param key The key in the value map for this object instance. + * @param value The value to store. + * @return The previously stored value for this instance/key combination, or {@code null} if there was none. + */ + public static Object setAdditionalInstanceField(Object obj, String key, Object value) { + if (obj == null) + throw new NullPointerException("object must not be null"); + if (key == null) + throw new NullPointerException("key must not be null"); + + HashMap objectFields; + synchronized (additionalFields) { + objectFields = additionalFields.get(obj); + if (objectFields == null) { + objectFields = new HashMap<>(); + additionalFields.put(obj, objectFields); + } + } + + synchronized (objectFields) { + return objectFields.put(key, value); + } + } + + /** + * Returns a value which was stored with {@link #setAdditionalInstanceField}. + * + * @param obj The object instance for which the value has been stored. + * @param key The key in the value map for this object instance. + * @return The stored value for this instance/key combination, or {@code null} if there is none. + */ + public static Object getAdditionalInstanceField(Object obj, String key) { + if (obj == null) + throw new NullPointerException("object must not be null"); + if (key == null) + throw new NullPointerException("key must not be null"); + + HashMap objectFields; + synchronized (additionalFields) { + objectFields = additionalFields.get(obj); + if (objectFields == null) + return null; + } + + synchronized (objectFields) { + return objectFields.get(key); + } + } + + /** + * Removes and returns a value which was stored with {@link #setAdditionalInstanceField}. + * + * @param obj The object instance for which the value has been stored. + * @param key The key in the value map for this object instance. + * @return The previously stored value for this instance/key combination, or {@code null} if there was none. + */ + public static Object removeAdditionalInstanceField(Object obj, String key) { + if (obj == null) + throw new NullPointerException("object must not be null"); + if (key == null) + throw new NullPointerException("key must not be null"); + + HashMap objectFields; + synchronized (additionalFields) { + objectFields = additionalFields.get(obj); + if (objectFields == null) + return null; + } + + synchronized (objectFields) { + return objectFields.remove(key); + } + } + + /** Like {@link #setAdditionalInstanceField}, but the value is stored for the class of {@code obj}. */ + public static Object setAdditionalStaticField(Object obj, String key, Object value) { + return setAdditionalInstanceField(obj.getClass(), key, value); + } + + /** Like {@link #getAdditionalInstanceField}, but the value is returned for the class of {@code obj}. */ + public static Object getAdditionalStaticField(Object obj, String key) { + return getAdditionalInstanceField(obj.getClass(), key); + } + + /** Like {@link #removeAdditionalInstanceField}, but the value is removed and returned for the class of {@code obj}. */ + public static Object removeAdditionalStaticField(Object obj, String key) { + return removeAdditionalInstanceField(obj.getClass(), key); + } + + /** Like {@link #setAdditionalInstanceField}, but the value is stored for {@code clazz}. */ + public static Object setAdditionalStaticField(Class clazz, String key, Object value) { + return setAdditionalInstanceField(clazz, key, value); + } + + /** Like {@link #setAdditionalInstanceField}, but the value is returned for {@code clazz}. */ + public static Object getAdditionalStaticField(Class clazz, String key) { + return getAdditionalInstanceField(clazz, key); + } + + /** Like {@link #setAdditionalInstanceField}, but the value is removed and returned for {@code clazz}. */ + public static Object removeAdditionalStaticField(Class clazz, String key) { + return removeAdditionalInstanceField(clazz, key); + } + + //################################################################################################# + /** + * Loads an asset from a resource object and returns the content as {@code byte} array. + * + * @param res The resources from which the asset should be loaded. + * @param path The path to the asset, as in {@link AssetManager#open}. + * @return The content of the asset. + */ + public static byte[] assetAsByteArray(Resources res, String path) throws IOException { + return inputStreamToByteArray(res.getAssets().open(path)); + } + + /*package*/ static byte[] inputStreamToByteArray(InputStream is) throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + byte[] temp = new byte[1024]; + int read; + + while ((read = is.read(temp)) > 0) { + buf.write(temp, 0, read); + } + is.close(); + return buf.toByteArray(); + } + + /** + * Invokes the {@link Closeable#close()} method, ignoring IOExceptions. + */ + /*package*/ static void closeSilently(Closeable c) { + if (c != null) { + try { + c.close(); + } catch (IOException ignored) {} + } + } + + /** + * Invokes the {@link DexFile#close()} method, ignoring IOExceptions. + */ + /*package*/ static void closeSilently(DexFile dexFile) { + if (dexFile != null) { + try { + dexFile.close(); + } catch (IOException ignored) {} + } + } + + /** + * Invokes the {@link ZipFile#close()} method, ignoring IOExceptions. + */ + /*package*/ static void closeSilently(ZipFile zipFile) { + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException ignored) {} + } + } + + /** + * Returns the lowercase hex string representation of a file's MD5 hash sum. + */ + public static String getMD5Sum(String file) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("MD5"); + InputStream is = new FileInputStream(file); + byte[] buffer = new byte[8192]; + int read; + while ((read = is.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + is.close(); + byte[] md5sum = digest.digest(); + BigInteger bigInt = new BigInteger(1, md5sum); + return bigInt.toString(16); + } catch (NoSuchAlgorithmException e) { + return ""; + } + } + + //################################################################################################# + /** + * Increments the depth counter for the given method. + * + *

The intention of the method depth counter is to keep track of the call depth for recursive + * methods, e.g. to override parameters only for the outer call. The Xposed framework uses this + * to load drawable replacements only once per call, even when multiple + * {@link Resources#getDrawable} variants call each other. + * + * @param method The method name. Should be prefixed with a unique, module-specific string. + * @return The updated depth. + */ + public static int incrementMethodDepth(String method) { + return getMethodDepthCounter(method).get().incrementAndGet(); + } + + /** + * Decrements the depth counter for the given method. + * See {@link #incrementMethodDepth} for details. + * + * @param method The method name. Should be prefixed with a unique, module-specific string. + * @return The updated depth. + */ + public static int decrementMethodDepth(String method) { + return getMethodDepthCounter(method).get().decrementAndGet(); + } + + /** + * Returns the current depth counter for the given method. + * See {@link #incrementMethodDepth} for details. + * + * @param method The method name. Should be prefixed with a unique, module-specific string. + * @return The updated depth. + */ + public static int getMethodDepth(String method) { + return getMethodDepthCounter(method).get().get(); + } + + private static ThreadLocal getMethodDepthCounter(String method) { + synchronized (sMethodDepth) { + ThreadLocal counter = sMethodDepth.get(method); + if (counter == null) { + counter = new ThreadLocal() { + @Override + protected AtomicInteger initialValue() { + return new AtomicInteger(); + } + }; + sMethodDepth.put(method, counter); + } + return counter; + } + } + + /*package*/ static boolean fileContains(File file, String str) throws IOException { + // There are certainly more efficient algorithms (e.g. Boyer-Moore used in grep), + // but the naive approach should be sufficient here. + BufferedReader in = null; + try { + in = new BufferedReader(new FileReader(file)); + String line; + while ((line = in.readLine()) != null) { + if (line.contains(str)) { + return true; + } + } + return false; + } finally { + closeSilently(in); + } + } + + //################################################################################################# + + /** + * Returns the method that is overridden by the given method. + * It returns {@code null} if the method doesn't override another method or if that method is + * abstract, i.e. if this is the first implementation in the hierarchy. + */ + /*package*/ static Method getOverriddenMethod(Method method) { + int modifiers = method.getModifiers(); + if (Modifier.isStatic(modifiers) || Modifier.isPrivate(modifiers)) { + return null; + } + + String name = method.getName(); + Class[] parameters = method.getParameterTypes(); + Class clazz = method.getDeclaringClass().getSuperclass(); + while (clazz != null) { + try { + Method superMethod = clazz.getDeclaredMethod(name, parameters); + modifiers = superMethod.getModifiers(); + if (!Modifier.isPrivate(modifiers) && !Modifier.isAbstract(modifiers)) { + return superMethod; + } else { + return null; + } + } catch (NoSuchMethodException ignored) { + clazz = clazz.getSuperclass(); + } + } + return null; + } + + /** + * Returns all methods which this class overrides. + */ + /*package*/ static Set getOverriddenMethods(Class clazz) { + Set methods = new HashSet<>(); + for (Method method : clazz.getDeclaredMethods()) { + Method overridden = getOverriddenMethod(method); + if (overridden != null) { + methods.add(overridden); + } + } + return methods; + } + + //################################################################################################# + // TODO helpers for view traversing + /*To make it easier, I will try and implement some more helpers: + - add view before/after existing view (I already mentioned that I think) + - get index of view in its parent + - get next/previous sibling (maybe with an optional argument "type", that might be ImageView.class and gives you the next sibling that is an ImageView)? + - get next/previous element (similar to the above, but would also work if the next element has a different parent, it would just go up the hierarchy and then down again until it finds a matching element) + - find the first child that is an instance of a specified class + - find all (direct or indirect) children of a specified class + */ + +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/XposedInit.java b/Bridge/src/main/java/de/robv/android/xposed/XposedInit.java new file mode 100644 index 00000000..8a8e44e4 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/XposedInit.java @@ -0,0 +1,234 @@ +package de.robv.android.xposed; + +import android.annotation.SuppressLint; +import android.app.AndroidAppHelper; +import android.os.Build; +import android.util.Log; + +import com.android.internal.os.ZygoteInit; +import com.elderdrivers.riru.xposed.entry.Router; +import com.elderdrivers.riru.xposed.util.Utils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import dalvik.system.DexFile; +import dalvik.system.PathClassLoader; +import de.robv.android.xposed.services.BaseService; + +import static de.robv.android.xposed.XposedHelpers.closeSilently; +import static de.robv.android.xposed.XposedHelpers.findClass; +import static de.robv.android.xposed.XposedHelpers.findFieldIfExists; +import static de.robv.android.xposed.XposedHelpers.setStaticBooleanField; +import static de.robv.android.xposed.XposedHelpers.setStaticLongField; + +public final class XposedInit { + private static final String TAG = XposedBridge.TAG; + private static boolean startsSystemServer = false; + private static final String startClassName = ""; // ed: no support for tool process anymore + + public static final String INSTALLER_PACKAGE_NAME = "de.robv.android.xposed.installer"; + @SuppressLint("SdCardPath") + private static final String BASE_DIR = Build.VERSION.SDK_INT >= 24 + ? "/data/user_de/0/" + INSTALLER_PACKAGE_NAME + "/" + : "/data/data/" + INSTALLER_PACKAGE_NAME + "/"; + private static final String INSTANT_RUN_CLASS = "com.android.tools.fd.runtime.BootstrapApplication"; + // TODO not supported yet + private static boolean disableResources = true; + private static final String[] XRESOURCES_CONFLICTING_PACKAGES = {"com.sygic.aura"}; + + private XposedInit() { + } + + /** + * Hook some methods which we want to create an easier interface for developers. + */ + /*package*/ + public static void initForZygote(boolean isSystem) throws Throwable { + startsSystemServer = isSystem; + Router.startBootstrapHook(isSystem); + // MIUI + if (findFieldIfExists(ZygoteInit.class, "BOOT_START_TIME") != null) { + setStaticLongField(ZygoteInit.class, "BOOT_START_TIME", XposedBridge.BOOT_START_TIME); + } + + // Samsung + if (Build.VERSION.SDK_INT >= 24) { + Class zygote = findClass("com.android.internal.os.Zygote", null); + try { + setStaticBooleanField(zygote, "isEnhancedZygoteASLREnabled", false); + } catch (NoSuchFieldError ignored) { + } + } + } + + /*package*/ + static void hookResources() throws Throwable { + // ed: not for now + } + + private static boolean needsToCloseFilesForFork() { + // ed: we always start to do our work after forking finishes + return false; + } + + /** + * Try to load all modules defined in BASE_DIR/conf/modules.list + */ + private static volatile AtomicBoolean modulesLoaded = new AtomicBoolean(false); + + public static void loadModules() throws IOException { + if (!modulesLoaded.compareAndSet(false, true)) { + return; + } + final String filename = BASE_DIR + "conf/modules.list"; + BaseService service = SELinuxHelper.getAppDataFileService(); + if (!service.checkFileExists(filename)) { + Log.e(TAG, "Cannot load any modules because " + filename + " was not found"); + return; + } + + ClassLoader topClassLoader = XposedBridge.BOOTCLASSLOADER; + ClassLoader parent; + while ((parent = topClassLoader.getParent()) != null) { + topClassLoader = parent; + } + + InputStream stream = service.getFileInputStream(filename); + BufferedReader apks = new BufferedReader(new InputStreamReader(stream)); + String apk; + while ((apk = apks.readLine()) != null) { + loadModule(apk, topClassLoader); + } + apks.close(); + } + + + /** + * Load a module from an APK by calling the init(String) method for all classes defined + * in assets/xposed_init. + */ + private static void loadModule(String apk, ClassLoader topClassLoader) { + Log.i(TAG, "Loading modules from " + apk); + + if (!new File(apk).exists()) { + Log.e(TAG, " File does not exist"); + return; + } + + DexFile dexFile; + try { + dexFile = new DexFile(apk); + } catch (IOException e) { + Log.e(TAG, " Cannot load module", e); + return; + } + + if (dexFile.loadClass(INSTANT_RUN_CLASS, topClassLoader) != null) { + Log.e(TAG, " Cannot load module, please disable \"Instant Run\" in Android Studio."); + closeSilently(dexFile); + return; + } + + if (dexFile.loadClass(XposedBridge.class.getName(), topClassLoader) != null) { + Log.e(TAG, " Cannot load module:"); + Log.e(TAG, " The Xposed API classes are compiled into the module's APK."); + Log.e(TAG, " This may cause strange issues and must be fixed by the module developer."); + Log.e(TAG, " For details, see: http://api.xposed.info/using.html"); + closeSilently(dexFile); + return; + } + + closeSilently(dexFile); + + ZipFile zipFile = null; + InputStream is; + try { + zipFile = new ZipFile(apk); + ZipEntry zipEntry = zipFile.getEntry("assets/xposed_init"); + if (zipEntry == null) { + Log.e(TAG, " assets/xposed_init not found in the APK"); + closeSilently(zipFile); + return; + } + is = zipFile.getInputStream(zipEntry); + } catch (IOException e) { + Log.e(TAG, " Cannot read assets/xposed_init in the APK", e); + closeSilently(zipFile); + return; + } + + ClassLoader mcl = new PathClassLoader(apk, XposedInit.class.getClassLoader()); + BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is)); + try { + String moduleClassName; + while ((moduleClassName = moduleClassesReader.readLine()) != null) { + moduleClassName = moduleClassName.trim(); + if (moduleClassName.isEmpty() || moduleClassName.startsWith("#")) + continue; + + try { + Log.i(TAG, " Loading class " + moduleClassName); + Class moduleClass = mcl.loadClass(moduleClassName); + + if (!IXposedMod.class.isAssignableFrom(moduleClass)) { + Log.e(TAG, " This class doesn't implement any sub-interface of IXposedMod, skipping it"); + continue; + } else if (disableResources && IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) { + Log.e(TAG, " This class requires resource-related hooks (which are disabled), skipping it."); + continue; + } + + final Object moduleInstance = moduleClass.newInstance(); + if (XposedBridge.isZygote) { + if (moduleInstance instanceof IXposedHookZygoteInit) { + IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam(); + param.modulePath = apk; + param.startsSystemServer = startsSystemServer; + ((IXposedHookZygoteInit) moduleInstance).initZygote(param); + } + + if (moduleInstance instanceof IXposedHookLoadPackage) + XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance)); + + if (moduleInstance instanceof IXposedHookInitPackageResources) + XposedBridge.hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance)); + } else { + if (moduleInstance instanceof IXposedHookCmdInit) { + IXposedHookCmdInit.StartupParam param = new IXposedHookCmdInit.StartupParam(); + param.modulePath = apk; + param.startClassName = startClassName; + ((IXposedHookCmdInit) moduleInstance).initCmdApp(param); + } + } + } catch (Throwable t) { + Log.e(TAG, " Failed to load class " + moduleClassName, t); + } + } + } catch (IOException e) { + Log.e(TAG, " Failed to load module from " + apk, e); + } finally { + closeSilently(is); + closeSilently(zipFile); + } + } + + public final static HashSet loadedPackagesInProcess = new HashSet<>(1); + + public static void logD(String prefix) { + Utils.logD(String.format("%s: pkg=%s, prc=%s", prefix, AndroidAppHelper.currentPackageName(), + AndroidAppHelper.currentProcessName())); + } + + public static void logE(String prefix, Throwable throwable) { + Utils.logE(String.format("%s: pkg=%s, prc=%s", prefix, AndroidAppHelper.currentPackageName(), + AndroidAppHelper.currentProcessName()), throwable); + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java b/Bridge/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java new file mode 100644 index 00000000..72766692 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java @@ -0,0 +1,25 @@ +package de.robv.android.xposed.callbacks; + +import de.robv.android.xposed.IXposedHookZygoteInit; + +/** + * Interface for objects that can be used to remove callbacks. + * + *

Just like hooking methods etc., unhooking applies only to the current process. + * In other process (or when the app is removed from memory and then restarted), the hook will still + * be active. The Zygote process (see {@link IXposedHookZygoteInit}) is an exception, the hook won't + * be inherited by any future processes forked from it in the future. + * + * @param The class of the callback. + */ +public interface IXUnhook { + /** + * Returns the callback that has been registered. + */ + T getCallback(); + + /** + * Removes the callback. + */ + void unhook(); +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java b/Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java new file mode 100644 index 00000000..33626b76 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java @@ -0,0 +1,57 @@ +package de.robv.android.xposed.callbacks; + +import android.content.res.XResources; + +import de.robv.android.xposed.IXposedHookInitPackageResources; +import de.robv.android.xposed.XposedBridge.CopyOnWriteSortedSet; + +/** + * This class is only used for internal purposes, except for the {@link InitPackageResourcesParam} + * subclass. + */ +public abstract class XC_InitPackageResources extends XCallback implements IXposedHookInitPackageResources { + /** + * Creates a new callback with default priority. + * @hide + */ + @SuppressWarnings("deprecation") + public XC_InitPackageResources() { + super(); + } + + /** + * Creates a new callback with a specific priority. + * + * @param priority See {@link XCallback#priority}. + * @hide + */ + public XC_InitPackageResources(int priority) { + super(priority); + } + + /** + * Wraps information about the resources being initialized. + */ + public static final class InitPackageResourcesParam extends XCallback.Param { + /** @hide */ + public InitPackageResourcesParam(CopyOnWriteSortedSet callbacks) { + super(callbacks); + } + + /** The name of the package for which resources are being loaded. */ + public String packageName; + + /** + * Reference to the resources that can be used for calls to + * {@link XResources#setReplacement(String, String, String, Object)}. + */ + public XResources res; + } + + /** @hide */ + @Override + protected void call(Param param) throws Throwable { + if (param instanceof InitPackageResourcesParam) + handleInitPackageResources((InitPackageResourcesParam) param); + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java b/Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java new file mode 100644 index 00000000..ff6ed3d4 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java @@ -0,0 +1,99 @@ +package de.robv.android.xposed.callbacks; + +import android.content.res.XResources; +import android.content.res.XResources.ResourceNames; +import android.view.View; + +import de.robv.android.xposed.XposedBridge.CopyOnWriteSortedSet; + +/** + * Callback for hooking layouts. Such callbacks can be passed to {@link XResources#hookLayout} + * and its variants. + */ +public abstract class XC_LayoutInflated extends XCallback { + /** + * Creates a new callback with default priority. + */ + @SuppressWarnings("deprecation") + public XC_LayoutInflated() { + super(); + } + + /** + * Creates a new callback with a specific priority. + * + * @param priority See {@link XCallback#priority}. + */ + public XC_LayoutInflated(int priority) { + super(priority); + } + + /** + * Wraps information about the inflated layout. + */ + public static final class LayoutInflatedParam extends XCallback.Param { + /** @hide */ + public LayoutInflatedParam(CopyOnWriteSortedSet callbacks) { + super(callbacks); + } + + /** The view that has been created from the layout. */ + public View view; + + /** Container with the ID and name of the underlying resource. */ + public ResourceNames resNames; + + /** Directory from which the layout was actually loaded (e.g. "layout-sw600dp"). */ + public String variant; + + /** Resources containing the layout. */ + public XResources res; + } + + /** @hide */ + @Override + protected void call(Param param) throws Throwable { + if (param instanceof LayoutInflatedParam) + handleLayoutInflated((LayoutInflatedParam) param); + } + + /** + * This method is called when the hooked layout has been inflated. + * + * @param liparam Information about the layout and the inflated view. + * @throws Throwable Everything the callback throws is caught and logged. + */ + public abstract void handleLayoutInflated(LayoutInflatedParam liparam) throws Throwable; + + /** + * An object with which the callback can be removed. + */ + public class Unhook implements IXUnhook { + private final String resDir; + private final int id; + + /** @hide */ + public Unhook(String resDir, int id) { + this.resDir = resDir; + this.id = id; + } + + /** + * Returns the resource ID of the hooked layout. + */ + public int getId() { + return id; + } + + @Override + public XC_LayoutInflated getCallback() { + return XC_LayoutInflated.this; + } + + @Override + public void unhook() { + XResources.unhookLayout(resDir, id, XC_LayoutInflated.this); + } + + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java b/Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java new file mode 100644 index 00000000..03f3a1d6 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java @@ -0,0 +1,63 @@ +package de.robv.android.xposed.callbacks; + +import android.content.pm.ApplicationInfo; + +import de.robv.android.xposed.IXposedHookLoadPackage; +import de.robv.android.xposed.XposedBridge.CopyOnWriteSortedSet; + +/** + * This class is only used for internal purposes, except for the {@link LoadPackageParam} + * subclass. + */ +public abstract class XC_LoadPackage extends XCallback implements IXposedHookLoadPackage { + /** + * Creates a new callback with default priority. + * @hide + */ + @SuppressWarnings("deprecation") + public XC_LoadPackage() { + super(); + } + + /** + * Creates a new callback with a specific priority. + * + * @param priority See {@link XCallback#priority}. + * @hide + */ + public XC_LoadPackage(int priority) { + super(priority); + } + + /** + * Wraps information about the app being loaded. + */ + public static final class LoadPackageParam extends XCallback.Param { + /** @hide */ + public LoadPackageParam(CopyOnWriteSortedSet callbacks) { + super(callbacks); + } + + /** The name of the package being loaded. */ + public String packageName; + + /** The process in which the package is executed. */ + public String processName; + + /** The ClassLoader used for this package. */ + public ClassLoader classLoader; + + /** More information about the application being loaded. */ + public ApplicationInfo appInfo; + + /** Set to {@code true} if this is the first (and main) application for this process. */ + public boolean isFirstApplication; + } + + /** @hide */ + @Override + protected void call(Param param) throws Throwable { + if (param instanceof LoadPackageParam) + handleLoadPackage((LoadPackageParam) param); + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/callbacks/XCallback.java b/Bridge/src/main/java/de/robv/android/xposed/callbacks/XCallback.java new file mode 100644 index 00000000..56930fb4 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/callbacks/XCallback.java @@ -0,0 +1,138 @@ +package de.robv.android.xposed.callbacks; + +import android.os.Bundle; + +import java.io.Serializable; + +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedBridge.CopyOnWriteSortedSet; + +/** + * Base class for Xposed callbacks. + * + * This class only keeps a priority for ordering multiple callbacks. + * The actual (abstract) callback methods are added by subclasses. + */ +public abstract class XCallback implements Comparable { + /** + * Callback priority, higher number means earlier execution. + * + *

This is usually set to {@link #PRIORITY_DEFAULT}. However, in case a certain callback should + * be executed earlier or later a value between {@link #PRIORITY_HIGHEST} and {@link #PRIORITY_LOWEST} + * can be set instead. The values are just for orientation though, Xposed doesn't enforce any + * boundaries on the priority values. + */ + public final int priority; + + /** @deprecated This constructor can't be hidden for technical reasons. Nevertheless, don't use it! */ + @Deprecated + public XCallback() { + this.priority = PRIORITY_DEFAULT; + } + + /** @hide */ + public XCallback(int priority) { + this.priority = priority; + } + + /** + * Base class for Xposed callback parameters. + */ + public static abstract class Param { + /** @hide */ + public final Object[] callbacks; + private Bundle extra; + + /** @deprecated This constructor can't be hidden for technical reasons. Nevertheless, don't use it! */ + @Deprecated + protected Param() { + callbacks = null; + } + + /** @hide */ + protected Param(CopyOnWriteSortedSet callbacks) { + this.callbacks = callbacks.getSnapshot(); + } + + /** + * This can be used to store any data for the scope of the callback. + * + *

Use this instead of instance variables, as it has a clear reference to e.g. each + * separate call to a method, even when the same method is called recursively. + * + * @see #setObjectExtra + * @see #getObjectExtra + */ + public synchronized Bundle getExtra() { + if (extra == null) + extra = new Bundle(); + return extra; + } + + /** + * Returns an object stored with {@link #setObjectExtra}. + */ + public Object getObjectExtra(String key) { + Serializable o = getExtra().getSerializable(key); + if (o instanceof SerializeWrapper) + return ((SerializeWrapper) o).object; + return null; + } + + /** + * Stores any object for the scope of the callback. For data types that support it, use + * the {@link Bundle} returned by {@link #getExtra} instead. + */ + public void setObjectExtra(String key, Object o) { + getExtra().putSerializable(key, new SerializeWrapper(o)); + } + + private static class SerializeWrapper implements Serializable { + private static final long serialVersionUID = 1L; + private final Object object; + public SerializeWrapper(Object o) { + object = o; + } + } + } + + /** @hide */ + public static void callAll(Param param) { + if (param.callbacks == null) + throw new IllegalStateException("This object was not created for use with callAll"); + + for (int i = 0; i < param.callbacks.length; i++) { + try { + ((XCallback) param.callbacks[i]).call(param); + } catch (Throwable t) { XposedBridge.log(t); } + } + } + + /** @hide */ + protected void call(Param param) throws Throwable {} + + /** @hide */ + @Override + public int compareTo(XCallback other) { + if (this == other) + return 0; + + // order descending by priority + if (other.priority != this.priority) + return other.priority - this.priority; + // then randomly + else if (System.identityHashCode(this) < System.identityHashCode(other)) + return -1; + else + return 1; + } + + /** The default priority, see {@link #priority}. */ + public static final int PRIORITY_DEFAULT = 50; + + /** Execute this callback late, see {@link #priority}. */ + public static final int PRIORITY_LOWEST = -10000; + + /** Execute this callback early, see {@link #priority}. */ + public static final int PRIORITY_HIGHEST = 10000; +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/callbacks/package-info.java b/Bridge/src/main/java/de/robv/android/xposed/callbacks/package-info.java new file mode 100644 index 00000000..3b00b7f5 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/callbacks/package-info.java @@ -0,0 +1,9 @@ +/** + * Contains the base classes for callbacks. + * + *

For historical reasons, {@link de.robv.android.xposed.XC_MethodHook} and + * {@link de.robv.android.xposed.XC_MethodReplacement} are directly in the + * {@code de.robv.android.xposed} package. + */ +package de.robv.android.xposed.callbacks; + diff --git a/Bridge/src/main/java/de/robv/android/xposed/package-info.java b/Bridge/src/main/java/de/robv/android/xposed/package-info.java new file mode 100644 index 00000000..c0fd7c56 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains the main classes of the Xposed framework. + */ +package de.robv.android.xposed; diff --git a/Bridge/src/main/java/de/robv/android/xposed/services/BaseService.java b/Bridge/src/main/java/de/robv/android/xposed/services/BaseService.java new file mode 100644 index 00000000..c0ccd186 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/services/BaseService.java @@ -0,0 +1,179 @@ +package de.robv.android.xposed.services; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import de.robv.android.xposed.SELinuxHelper; + +/** + * General definition of a file access service provided by the Xposed framework. + * + *

References to a concrete subclass should generally be retrieved from {@link SELinuxHelper}. + */ +public abstract class BaseService { + /** Flag for {@link #checkFileAccess}: Read access. */ + public static final int R_OK = 4; + /** Flag for {@link #checkFileAccess}: Write access. */ + public static final int W_OK = 2; + /** Flag for {@link #checkFileAccess}: Executable access. */ + public static final int X_OK = 1; + /** Flag for {@link #checkFileAccess}: File/directory exists. */ + public static final int F_OK = 0; + + /** + * Checks whether the services accesses files directly (instead of using IPC). + * + * @return {@code true} in case direct access is possible. + */ + public boolean hasDirectFileAccess() { + return false; + } + + /** + * Check whether a file is accessible. SELinux might enforce stricter checks. + * + * @param filename The absolute path of the file to check. + * @param mode The mode for POSIX's {@code access()} function. + * @return The result of the {@code access()} function. + */ + public abstract boolean checkFileAccess(String filename, int mode); + + /** + * Check whether a file exists. + * + * @param filename The absolute path of the file to check. + * @return The result of the {@code access()} function. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean checkFileExists(String filename) { + return checkFileAccess(filename, F_OK); + } + + /** + * Determine the size and modification time of a file. + * + * @param filename The absolute path of the file to check. + * @return A {@link FileResult} object holding the result. + * @throws IOException In case an error occurred while retrieving the information. + */ + public abstract FileResult statFile(String filename) throws IOException; + + /** + * Determine the size time of a file. + * + * @param filename The absolute path of the file to check. + * @return The file size. + * @throws IOException In case an error occurred while retrieving the information. + */ + public long getFileSize(String filename) throws IOException { + return statFile(filename).size; + } + + /** + * Determine the size time of a file. + * + * @param filename The absolute path of the file to check. + * @return The file modification time. + * @throws IOException In case an error occurred while retrieving the information. + */ + public long getFileModificationTime(String filename) throws IOException { + return statFile(filename).mtime; + } + + /** + * Read a file into memory. + * + * @param filename The absolute path of the file to read. + * @return A {@code byte} array with the file content. + * @throws IOException In case an error occurred while reading the file. + */ + public abstract byte[] readFile(String filename) throws IOException; + + /** + * Read a file into memory, but only if it has changed since the last time. + * + * @param filename The absolute path of the file to read. + * @param previousSize File size of last read. + * @param previousTime File modification time of last read. + * @return A {@link FileResult} object holding the result. + *

The {@link FileResult#content} field might be {@code null} if the file + * is unmodified ({@code previousSize} and {@code previousTime} are still valid). + * @throws IOException In case an error occurred while reading the file. + */ + public abstract FileResult readFile(String filename, long previousSize, long previousTime) throws IOException; + + /** + * Read a file into memory, optionally only if it has changed since the last time. + * + * @param filename The absolute path of the file to read. + * @param offset Number of bytes to skip at the beginning of the file. + * @param length Number of bytes to read (0 means read to end of file). + * @param previousSize Optional: File size of last read. + * @param previousTime Optional: File modification time of last read. + * @return A {@link FileResult} object holding the result. + *

The {@link FileResult#content} field might be {@code null} if the file + * is unmodified ({@code previousSize} and {@code previousTime} are still valid). + * @throws IOException In case an error occurred while reading the file. + */ + public abstract FileResult readFile(String filename, int offset, int length, + long previousSize, long previousTime) throws IOException; + + /** + * Get a stream to the file content. + * Depending on the service, it may or may not be read completely into memory. + * + * @param filename The absolute path of the file to read. + * @return An {@link InputStream} to the file content. + * @throws IOException In case an error occurred while reading the file. + */ + public InputStream getFileInputStream(String filename) throws IOException { + return new ByteArrayInputStream(readFile(filename)); + } + + /** + * Get a stream to the file content, but only if it has changed since the last time. + * Depending on the service, it may or may not be read completely into memory. + * + * @param filename The absolute path of the file to read. + * @param previousSize Optional: File size of last read. + * @param previousTime Optional: File modification time of last read. + * @return A {@link FileResult} object holding the result. + *

The {@link FileResult#stream} field might be {@code null} if the file + * is unmodified ({@code previousSize} and {@code previousTime} are still valid). + * @throws IOException In case an error occurred while reading the file. + */ + public FileResult getFileInputStream(String filename, long previousSize, long previousTime) throws IOException { + FileResult result = readFile(filename, previousSize, previousTime); + if (result.content == null) + return result; + return new FileResult(new ByteArrayInputStream(result.content), result.size, result.mtime); + } + + + // ---------------------------------------------------------------------------- + /*package*/ BaseService() {} + + /*package*/ static void ensureAbsolutePath(String filename) { + if (!filename.startsWith("/")) { + throw new IllegalArgumentException("Only absolute filenames are allowed: " + filename); + } + } + + /*package*/ static void throwCommonIOException(int errno, String errorMsg, String filename, String defaultText) throws IOException { + switch (errno) { + case 1: // EPERM + case 13: // EACCES + throw new FileNotFoundException(errorMsg != null ? errorMsg : "Permission denied: " + filename); + case 2: // ENOENT + throw new FileNotFoundException(errorMsg != null ? errorMsg : "No such file or directory: " + filename); + case 12: // ENOMEM + throw new OutOfMemoryError(errorMsg); + case 21: // EISDIR + throw new FileNotFoundException(errorMsg != null ? errorMsg : "Is a directory: " + filename); + default: + throw new IOException(errorMsg != null ? errorMsg : "Error " + errno + defaultText + filename); + } + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/services/BinderService.java b/Bridge/src/main/java/de/robv/android/xposed/services/BinderService.java new file mode 100644 index 00000000..1961f15e --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/services/BinderService.java @@ -0,0 +1,166 @@ +package de.robv.android.xposed.services; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.ServiceManager; + +import java.io.IOException; + +/** @hide */ +public final class BinderService extends BaseService { + public static final int TARGET_APP = 0; + public static final int TARGET_SYSTEM = 1; + + /** + * Retrieve the binder service running in the specified context. + * @param target Either {@link #TARGET_APP} or {@link #TARGET_SYSTEM}. + * @return A reference to the service. + * @throws IllegalStateException In case the service doesn't exist (should never happen). + */ + public static BinderService getService(int target) { + if (target < 0 || target > sServices.length) { + throw new IllegalArgumentException("Invalid service target " + target); + } + synchronized (sServices) { + if (sServices[target] == null) { + sServices[target] = new BinderService(target); + } + return sServices[target]; + } + } + + @Override + public boolean checkFileAccess(String filename, int mode) { + ensureAbsolutePath(filename); + + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(INTERFACE_TOKEN); + data.writeString(filename); + data.writeInt(mode); + + try { + mRemote.transact(ACCESS_FILE_TRANSACTION, data, reply, 0); + } catch (RemoteException e) { + data.recycle(); + reply.recycle(); + return false; + } + + reply.readException(); + int result = reply.readInt(); + reply.recycle(); + data.recycle(); + return result == 0; + } + + @Override + public FileResult statFile(String filename) throws IOException { + ensureAbsolutePath(filename); + + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(INTERFACE_TOKEN); + data.writeString(filename); + + try { + mRemote.transact(STAT_FILE_TRANSACTION, data, reply, 0); + } catch (RemoteException e) { + data.recycle(); + reply.recycle(); + throw new IOException(e); + } + + reply.readException(); + int errno = reply.readInt(); + if (errno != 0) + throwCommonIOException(errno, null, filename, " while retrieving attributes for "); + + long size = reply.readLong(); + long time = reply.readLong(); + reply.recycle(); + data.recycle(); + return new FileResult(size, time); + } + + @Override + public byte[] readFile(String filename) throws IOException { + return readFile(filename, 0, 0, 0, 0).content; + } + + @Override + public FileResult readFile(String filename, long previousSize, long previousTime) throws IOException { + return readFile(filename, 0, 0, previousSize, previousTime); + } + + @Override + public FileResult readFile(String filename, int offset, int length, + long previousSize, long previousTime) throws IOException { + ensureAbsolutePath(filename); + + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(INTERFACE_TOKEN); + data.writeString(filename); + data.writeInt(offset); + data.writeInt(length); + data.writeLong(previousSize); + data.writeLong(previousTime); + + try { + mRemote.transact(READ_FILE_TRANSACTION, data, reply, 0); + } catch (RemoteException e) { + data.recycle(); + reply.recycle(); + throw new IOException(e); + } + + reply.readException(); + int errno = reply.readInt(); + String errorMsg = reply.readString(); + long size = reply.readLong(); + long time = reply.readLong(); + byte[] content = reply.createByteArray(); + reply.recycle(); + data.recycle(); + + switch (errno) { + case 0: + return new FileResult(content, size, time); + case 22: // EINVAL + if (errorMsg != null) { + IllegalArgumentException iae = new IllegalArgumentException(errorMsg); + if (offset == 0 && length == 0) + throw new IOException(iae); + else + throw iae; + } else { + throw new IllegalArgumentException("Offset " + offset + " / Length " + length + + " is out of range for " + filename + " with size " + size); + } + default: + throwCommonIOException(errno, errorMsg, filename, " while reading "); + throw new IllegalStateException(); // not reached + } + } + + + // ---------------------------------------------------------------------------- + private static final String INTERFACE_TOKEN = "de.robv.android.xposed.IXposedService"; + + private static final int ACCESS_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2; + private static final int STAT_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3; + private static final int READ_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4; + + private static final String[] SERVICE_NAMES = { "user.xposed.app", "user.xposed.system" }; + private static final BinderService[] sServices = new BinderService[2]; + private final IBinder mRemote; + + private BinderService(int target) { + IBinder binder = ServiceManager.getService(SERVICE_NAMES[target]); + if (binder == null) + throw new IllegalStateException("Service " + SERVICE_NAMES[target] + " does not exist"); + this.mRemote = binder; + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/services/DirectAccessService.java b/Bridge/src/main/java/de/robv/android/xposed/services/DirectAccessService.java new file mode 100644 index 00000000..f2f7124b --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/services/DirectAccessService.java @@ -0,0 +1,113 @@ +package de.robv.android.xposed.services; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** @hide */ +public final class DirectAccessService extends BaseService { + @Override + public boolean hasDirectFileAccess() { + return true; + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean checkFileAccess(String filename, int mode) { + File file = new File(filename); + if (mode == F_OK && !file.exists()) return false; + if ((mode & R_OK) != 0 && !file.canRead()) return false; + if ((mode & W_OK) != 0 && !file.canWrite()) return false; + if ((mode & X_OK) != 0 && !file.canExecute()) return false; + return true; + } + + @Override + public boolean checkFileExists(String filename) { + return new File(filename).exists(); + } + + @Override + public FileResult statFile(String filename) throws IOException { + File file = new File(filename); + return new FileResult(file.length(), file.lastModified()); + } + + @Override + public byte[] readFile(String filename) throws IOException { + File file = new File(filename); + byte content[] = new byte[(int)file.length()]; + FileInputStream fis = new FileInputStream(file); + fis.read(content); + fis.close(); + return content; + } + + @Override + public FileResult readFile(String filename, long previousSize, long previousTime) throws IOException { + File file = new File(filename); + long size = file.length(); + long time = file.lastModified(); + if (previousSize == size && previousTime == time) + return new FileResult(size, time); + return new FileResult(readFile(filename), size, time); + } + + @Override + public FileResult readFile(String filename, int offset, int length, long previousSize, long previousTime) throws IOException { + File file = new File(filename); + long size = file.length(); + long time = file.lastModified(); + if (previousSize == size && previousTime == time) + return new FileResult(size, time); + + // Shortcut for the simple case + if (offset <= 0 && length <= 0) + return new FileResult(readFile(filename), size, time); + + // Check range + if (offset > 0 && offset >= size) { + throw new IllegalArgumentException("Offset " + offset + " is out of range for " + filename); + } else if (offset < 0) { + offset = 0; + } + + if (length > 0 && (offset + length) > size) { + throw new IllegalArgumentException("Length " + length + " is out of range for " + filename); + } else if (length <= 0) { + length = (int) (size - offset); + } + + byte content[] = new byte[length]; + FileInputStream fis = new FileInputStream(file); + fis.skip(offset); + fis.read(content); + fis.close(); + return new FileResult(content, size, time); + } + + /** + * {@inheritDoc} + *

This implementation returns a BufferedInputStream instead of loading the file into memory. + */ + @Override + public InputStream getFileInputStream(String filename) throws IOException { + return new BufferedInputStream(new FileInputStream(filename), 16*1024); + } + + /** + * {@inheritDoc} + *

This implementation returns a BufferedInputStream instead of loading the file into memory. + */ + @Override + public FileResult getFileInputStream(String filename, long previousSize, long previousTime) throws IOException { + File file = new File(filename); + long size = file.length(); + long time = file.lastModified(); + if (previousSize == size && previousTime == time) + return new FileResult(size, time); + return new FileResult(new BufferedInputStream(new FileInputStream(filename), 16*1024), size, time); + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/services/FileResult.java b/Bridge/src/main/java/de/robv/android/xposed/services/FileResult.java new file mode 100644 index 00000000..00da1dfa --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/services/FileResult.java @@ -0,0 +1,60 @@ +package de.robv.android.xposed.services; + +import java.io.InputStream; + +/** + * Holder for the result of a {@link BaseService#readFile} or {@link BaseService#statFile} call. + */ +public final class FileResult { + /** File content, might be {@code null} if the file wasn't read. */ + public final byte[] content; + /** File input stream, might be {@code null} if the file wasn't read. */ + public final InputStream stream; + /** File size. */ + public final long size; + /** File last modification time. */ + public final long mtime; + + /*package*/ FileResult(long size, long mtime) { + this.content = null; + this.stream = null; + this.size = size; + this.mtime = mtime; + } + + /*package*/ FileResult(byte[] content, long size, long mtime) { + this.content = content; + this.stream = null; + this.size = size; + this.mtime = mtime; + } + + /*package*/ FileResult(InputStream stream, long size, long mtime) { + this.content = null; + this.stream = stream; + this.size = size; + this.mtime = mtime; + } + + /** @hide */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("{"); + if (content != null) { + sb.append("content.length: "); + sb.append(content.length); + sb.append(", "); + } + if (stream != null) { + sb.append("stream: "); + sb.append(stream.toString()); + sb.append(", "); + } + sb.append("size: "); + sb.append(size); + sb.append(", mtime: "); + sb.append(mtime); + sb.append("}"); + return sb.toString(); + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/services/ZygoteService.java b/Bridge/src/main/java/de/robv/android/xposed/services/ZygoteService.java new file mode 100644 index 00000000..de1faa0d --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/services/ZygoteService.java @@ -0,0 +1,54 @@ +package de.robv.android.xposed.services; + +import java.io.IOException; +import java.util.Arrays; + +/** @hide */ +@SuppressWarnings("JniMissingFunction") +public final class ZygoteService extends BaseService { + @Override + public native boolean checkFileAccess(String filename, int mode); + + @Override + public native FileResult statFile(String filename) throws IOException; + + @Override + public native byte[] readFile(String filename) throws IOException; + + @Override + // Just for completeness, we don't expect this to be called often in Zygote. + public FileResult readFile(String filename, long previousSize, long previousTime) throws IOException { + FileResult stat = statFile(filename); + if (previousSize == stat.size && previousTime == stat.mtime) + return stat; + return new FileResult(readFile(filename), stat.size, stat.mtime); + } + + @Override + // Just for completeness, we don't expect this to be called often in Zygote. + public FileResult readFile(String filename, int offset, int length, long previousSize, long previousTime) throws IOException { + FileResult stat = statFile(filename); + if (previousSize == stat.size && previousTime == stat.mtime) + return stat; + + // Shortcut for the simple case + if (offset <= 0 && length <= 0) + return new FileResult(readFile(filename), stat.size, stat.mtime); + + // Check range + if (offset > 0 && offset >= stat.size) { + throw new IllegalArgumentException("offset " + offset + " >= size " + stat.size + " for " + filename); + } else if (offset < 0) { + offset = 0; + } + + if (length > 0 && (offset + length) > stat.size) { + throw new IllegalArgumentException("offset " + offset + " + length " + length + " > size " + stat.size + " for " + filename); + } else if (length <= 0) { + length = (int) (stat.size - offset); + } + + byte[] content = readFile(filename); + return new FileResult(Arrays.copyOfRange(content, offset, offset + length), stat.size, stat.mtime); + } +} diff --git a/Bridge/src/main/java/de/robv/android/xposed/services/package-info.java b/Bridge/src/main/java/de/robv/android/xposed/services/package-info.java new file mode 100644 index 00000000..270b32c4 --- /dev/null +++ b/Bridge/src/main/java/de/robv/android/xposed/services/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains file access services provided by the Xposed framework. + */ +package de.robv.android.xposed.services; diff --git a/Core/.gitignore b/Core/.gitignore new file mode 100644 index 00000000..57726a41 --- /dev/null +++ b/Core/.gitignore @@ -0,0 +1,7 @@ +/.externalNativeBuild +/build +/libs +/obj +/release +/template_override/system/framework/edxposed.dex +*.iml \ No newline at end of file diff --git a/Core/build-module.sh b/Core/build-module.sh new file mode 100644 index 00000000..c9284688 --- /dev/null +++ b/Core/build-module.sh @@ -0,0 +1,16 @@ +function copy_files { + # /data/misc/riru/modules/template exists -> libriru_template.so will be loaded + # Change "template" to your module name + # You can also use this folder as your config folder + NAME="edxposed" + mkdir -p $TMP_DIR_MAGISK/data/misc/riru/modules/$NAME + cp $MODULE_NAME/template_override/riru_module.prop $TMP_DIR_MAGISK/data/misc/riru/modules/$NAME/module.prop + + cp $MODULE_NAME/template_override/config.sh $TMP_DIR_MAGISK + cp $MODULE_NAME/template_override/module.prop $TMP_DIR_MAGISK + + cp -r $MODULE_NAME/template_override/system $TMP_DIR_MAGISK + cp -r $MODULE_NAME/template_override/common $TMP_DIR_MAGISK + cp -r $MODULE_NAME/template_override/META-INF $TMP_DIR_MAGISK +} + diff --git a/Core/build.gradle b/Core/build.gradle new file mode 100644 index 00000000..64828bbc --- /dev/null +++ b/Core/build.gradle @@ -0,0 +1,57 @@ +apply plugin: 'com.android.library' +version "v0.2.6_beta" +extensions["module_name"] = "EdXposed" +android { + compileSdkVersion 28 + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + + externalNativeBuild { + ndkBuild { + abiFilters 'arm64-v8a', 'armeabi-v7a' + arguments "NDK_PROJECT_PATH=jni/" + } + } + } + externalNativeBuild { + ndkBuild { + path 'jni/Android.mk' + } + } +} +afterEvaluate { + + android.libraryVariants.all { variant -> + def nameCapped = variant.name.capitalize() + def nameLowered = variant.name.toLowerCase() + + def zipTask = task("zip${nameCapped}", type: Exec, dependsOn: ":Bridge:makeAndCopy${nameCapped}") { + workingDir '..' + commandLine 'sh', 'build.sh',\ + project.name,\ + "${project.version}-${nameLowered}",\ + "${project.extensions['module_name']}" + } + +// def renameTask = task("build${nameCapped}", type: Copy) { +// from "release/magisk-${project.name}-arm-arm64-${project.version}.zip" +// into "release" +// rename("${project.name}", "${project.extensions['module_name']}") +// rename("${project.version}", "${project.version}-${nameLowered}") +// } + + def pushTask = task("push${nameCapped}", type: Exec) { + workingDir 'release' + commandLine 'cmd', '/c', + "adb push magisk-${project.extensions['module_name']}-arm-arm64" + + "-${project.version}-${nameLowered}.zip /sdcard/" + } + +// renameTask.dependsOn(zipTask) + pushTask.dependsOn(zipTask) + } + +} +dependencies { +} \ No newline at end of file diff --git a/Core/jni/.gitattributes b/Core/jni/.gitattributes new file mode 100644 index 00000000..63f9e342 --- /dev/null +++ b/Core/jni/.gitattributes @@ -0,0 +1 @@ +libs/** binary diff --git a/Core/jni/Android.mk b/Core/jni/Android.mk new file mode 100644 index 00000000..16d6dcf0 --- /dev/null +++ b/Core/jni/Android.mk @@ -0,0 +1,3 @@ +LOCAL_PATH := $(call my-dir) + +include $(call all-makefiles-under, $(LOCAL_PATH)) \ No newline at end of file diff --git a/Core/jni/Application.mk b/Core/jni/Application.mk new file mode 100644 index 00000000..7db84585 --- /dev/null +++ b/Core/jni/Application.mk @@ -0,0 +1,6 @@ +APP_ABI := arm64-v8a armeabi-v7a# x86 x86_64 +APP_PLATFORM := android-23 +APP_CFLAGS := -std=gnu99 +APP_CPPFLAGS := -std=c++11 +APP_STL := c++_static +APP_SHORT_COMMANDS := true \ No newline at end of file diff --git a/Core/jni/external/Android.mk b/Core/jni/external/Android.mk new file mode 100644 index 00000000..056ebfb0 --- /dev/null +++ b/Core/jni/external/Android.mk @@ -0,0 +1,16 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE := xhook +LOCAL_SRC_FILES := xhook/xhook.c \ + xhook/xh_core.c \ + xhook/xh_elf.c \ + xhook/xh_jni.c \ + xhook/xh_log.c \ + xhook/xh_util.c \ + xhook/xh_version.c +LOCAL_C_INCLUDES := $(LOCAL_PATH) +LOCAL_CFLAGS := -Wall -Wextra -Werror -fvisibility=hidden +LOCAL_CONLYFLAGS := -std=c11 +LOCAL_LDLIBS := -llog +include $(BUILD_STATIC_LIBRARY) \ No newline at end of file diff --git a/Core/jni/external/include/xhook/xhook.h b/Core/jni/external/include/xhook/xhook.h new file mode 100644 index 00000000..93dd5b4c --- /dev/null +++ b/Core/jni/external/include/xhook/xhook.h @@ -0,0 +1,50 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#ifndef XHOOK_H +#define XHOOK_H 1 + +#ifdef __cplusplus +extern "C" { +#endif + +#define XHOOK_EXPORT __attribute__((visibility("default"))) + +int xhook_register(const char *pathname_regex_str, const char *symbol, + void *new_func, void **old_func) XHOOK_EXPORT; + +int xhook_ignore(const char *pathname_regex_str, const char *symbol) XHOOK_EXPORT; + +int xhook_refresh(int async) XHOOK_EXPORT; + +void xhook_clear() XHOOK_EXPORT; + +void xhook_enable_debug(int flag) XHOOK_EXPORT; + +void xhook_enable_sigsegv_protection(int flag) XHOOK_EXPORT; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Core/jni/external/xhook/queue.h b/Core/jni/external/xhook/queue.h new file mode 100644 index 00000000..c2443bef --- /dev/null +++ b/Core/jni/external/xhook/queue.h @@ -0,0 +1,554 @@ +/*- + * Copyright (c) 1991, 1993 + * The Regents of the University of California. 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 University 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 REGENTS 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 REGENTS 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. + * + * @(#)queue.h 8.5 (Berkeley) 8/20/94 + * $FreeBSD: stable/9/sys/sys/queue.h 252365 2013-06-29 04:25:40Z lstewart $ + */ + +#ifndef QUEUE_H +#define QUEUE_H + +/* #include */ +#define __containerof(ptr, type, field) ((type *)((char *)(ptr) - ((char *)&((type *)0)->field))) + +/* + * This file defines four types of data structures: singly-linked lists, + * singly-linked tail queues, lists and tail queues. + * + * A singly-linked list is headed by a single forward pointer. The elements + * are singly linked for minimum space and pointer manipulation overhead at + * the expense of O(n) removal for arbitrary elements. New elements can be + * added to the list after an existing element or at the head of the list. + * Elements being removed from the head of the list should use the explicit + * macro for this purpose for optimum efficiency. A singly-linked list may + * only be traversed in the forward direction. Singly-linked lists are ideal + * for applications with large datasets and few or no removals or for + * implementing a LIFO queue. + * + * A singly-linked tail queue is headed by a pair of pointers, one to the + * head of the list and the other to the tail of the list. The elements are + * singly linked for minimum space and pointer manipulation overhead at the + * expense of O(n) removal for arbitrary elements. New elements can be added + * to the list after an existing element, at the head of the list, or at the + * end of the list. Elements being removed from the head of the tail queue + * should use the explicit macro for this purpose for optimum efficiency. + * A singly-linked tail queue may only be traversed in the forward direction. + * Singly-linked tail queues are ideal for applications with large datasets + * and few or no removals or for implementing a FIFO queue. + * + * A list is headed by a single forward pointer (or an array of forward + * pointers for a hash table header). The elements are doubly linked + * so that an arbitrary element can be removed without a need to + * traverse the list. New elements can be added to the list before + * or after an existing element or at the head of the list. A list + * may be traversed in either direction. + * + * A tail queue is headed by a pair of pointers, one to the head of the + * list and the other to the tail of the list. The elements are doubly + * linked so that an arbitrary element can be removed without a need to + * traverse the list. New elements can be added to the list before or + * after an existing element, at the head of the list, or at the end of + * the list. A tail queue may be traversed in either direction. + * + * For details on the use of these macros, see the queue(3) manual page. + * + * SLIST LIST STAILQ TAILQ + * _HEAD + + + + + * _HEAD_INITIALIZER + + + + + * _ENTRY + + + + + * _INIT + + + + + * _EMPTY + + + + + * _FIRST + + + + + * _NEXT + + + + + * _PREV - + - + + * _LAST - - + + + * _FOREACH + + + + + * _FOREACH_FROM + + + + + * _FOREACH_SAFE + + + + + * _FOREACH_FROM_SAFE + + + + + * _FOREACH_REVERSE - - - + + * _FOREACH_REVERSE_FROM - - - + + * _FOREACH_REVERSE_SAFE - - - + + * _FOREACH_REVERSE_FROM_SAFE - - - + + * _INSERT_HEAD + + + + + * _INSERT_BEFORE - + - + + * _INSERT_AFTER + + + + + * _INSERT_TAIL - - + + + * _CONCAT - - + + + * _REMOVE_AFTER + - + - + * _REMOVE_HEAD + - + - + * _REMOVE + + + + + * _SWAP + + + + + * + */ + +/* + * Singly-linked List declarations. + */ +#define SLIST_HEAD(name, type, qual) \ + struct name { \ + struct type *qual slh_first; /* first element */ \ + } + +#define SLIST_HEAD_INITIALIZER(head) \ + { NULL } + +#define SLIST_ENTRY(type, qual) \ + struct { \ + struct type *qual sle_next; /* next element */ \ + } + +/* + * Singly-linked List functions. + */ +#define SLIST_INIT(head) do { \ + SLIST_FIRST((head)) = NULL; \ + } while (0) + +#define SLIST_EMPTY(head) ((head)->slh_first == NULL) + +#define SLIST_FIRST(head) ((head)->slh_first) + +#define SLIST_NEXT(elm, field) ((elm)->field.sle_next) + +#define SLIST_FOREACH(var, head, field) \ + for ((var) = SLIST_FIRST((head)); \ + (var); \ + (var) = SLIST_NEXT((var), field)) + +#define SLIST_FOREACH_FROM(var, head, field) \ + for ((var) = ((var) ? (var) : SLIST_FIRST((head))); \ + (var); \ + (var) = SLIST_NEXT((var), field)) + +#define SLIST_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = SLIST_FIRST((head)); \ + (var) && ((tvar) = SLIST_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define SLIST_FOREACH_FROM_SAFE(var, head, field, tvar) \ + for ((var) = ((var) ? (var) : SLIST_FIRST((head))); \ + (var) && ((tvar) = SLIST_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define SLIST_INSERT_HEAD(head, elm, field) do { \ + SLIST_NEXT((elm), field) = SLIST_FIRST((head)); \ + SLIST_FIRST((head)) = (elm); \ + } while (0) + +#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \ + SLIST_NEXT((elm), field) = SLIST_NEXT((slistelm), field); \ + SLIST_NEXT((slistelm), field) = (elm); \ + } while (0) + +#define SLIST_REMOVE_AFTER(elm, field) do { \ + SLIST_NEXT(elm, field) = \ + SLIST_NEXT(SLIST_NEXT(elm, field), field); \ + } while (0) + +#define SLIST_REMOVE_HEAD(head, field) do { \ + SLIST_FIRST((head)) = SLIST_NEXT(SLIST_FIRST((head)), field); \ + } while (0) + +#define SLIST_REMOVE(head, elm, type, field) do { \ + if (SLIST_FIRST((head)) == (elm)) { \ + SLIST_REMOVE_HEAD((head), field); \ + } \ + else { \ + struct type *curelm = SLIST_FIRST((head)); \ + while (SLIST_NEXT(curelm, field) != (elm)) \ + curelm = SLIST_NEXT(curelm, field); \ + SLIST_REMOVE_AFTER(curelm, field); \ + } \ + } while (0) + +#define SLIST_SWAP(head1, head2, type) do { \ + struct type *swap_first = SLIST_FIRST(head1); \ + SLIST_FIRST(head1) = SLIST_FIRST(head2); \ + SLIST_FIRST(head2) = swap_first; \ + } while (0) + +/* + * List declarations. + */ +#define LIST_HEAD(name, type, qual) \ + struct name { \ + struct type *qual lh_first; /* first element */ \ + } + +#define LIST_HEAD_INITIALIZER(head) \ + { NULL } + +#define LIST_ENTRY(type, qual) \ + struct { \ + struct type *qual le_next; /* next element */ \ + struct type *qual *le_prev; /* address of previous next element */ \ + } + +/* + * List functions. + */ +#define LIST_INIT(head) do { \ + LIST_FIRST((head)) = NULL; \ + } while (0) + +#define LIST_EMPTY(head) ((head)->lh_first == NULL) + +#define LIST_FIRST(head) ((head)->lh_first) + +#define LIST_NEXT(elm, field) ((elm)->field.le_next) + +#define LIST_PREV(elm, head, type, field) \ + ((elm)->field.le_prev == &LIST_FIRST((head)) ? NULL : \ + __containerof((elm)->field.le_prev, struct type, field.le_next)) + +#define LIST_FOREACH(var, head, field) \ + for ((var) = LIST_FIRST((head)); \ + (var); \ + (var) = LIST_NEXT((var), field)) + +#define LIST_FOREACH_FROM(var, head, field) \ + for ((var) = ((var) ? (var) : LIST_FIRST((head))); \ + (var); \ + (var) = LIST_NEXT((var), field)) + +#define LIST_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = LIST_FIRST((head)); \ + (var) && ((tvar) = LIST_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define LIST_FOREACH_FROM_SAFE(var, head, field, tvar) \ + for ((var) = ((var) ? (var) : LIST_FIRST((head))); \ + (var) && ((tvar) = LIST_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define LIST_INSERT_HEAD(head, elm, field) do { \ + if ((LIST_NEXT((elm), field) = LIST_FIRST((head))) != NULL) \ + LIST_FIRST((head))->field.le_prev = &LIST_NEXT((elm), field); \ + LIST_FIRST((head)) = (elm); \ + (elm)->field.le_prev = &LIST_FIRST((head)); \ + } while (0) + +#define LIST_INSERT_BEFORE(listelm, elm, field) do { \ + (elm)->field.le_prev = (listelm)->field.le_prev; \ + LIST_NEXT((elm), field) = (listelm); \ + *(listelm)->field.le_prev = (elm); \ + (listelm)->field.le_prev = &LIST_NEXT((elm), field); \ + } while (0) + +#define LIST_INSERT_AFTER(listelm, elm, field) do { \ + if ((LIST_NEXT((elm), field) = LIST_NEXT((listelm), field)) != NULL) \ + LIST_NEXT((listelm), field)->field.le_prev = \ + &LIST_NEXT((elm), field); \ + LIST_NEXT((listelm), field) = (elm); \ + (elm)->field.le_prev = &LIST_NEXT((listelm), field); \ + } while (0) + +#define LIST_REMOVE(elm, field) do { \ + if (LIST_NEXT((elm), field) != NULL) \ + LIST_NEXT((elm), field)->field.le_prev = \ + (elm)->field.le_prev; \ + *(elm)->field.le_prev = LIST_NEXT((elm), field); \ + } while (0) + +#define LIST_SWAP(head1, head2, type, field) do { \ + struct type *swap_tmp = LIST_FIRST((head1)); \ + LIST_FIRST((head1)) = LIST_FIRST((head2)); \ + LIST_FIRST((head2)) = swap_tmp; \ + if ((swap_tmp = LIST_FIRST((head1))) != NULL) \ + swap_tmp->field.le_prev = &LIST_FIRST((head1)); \ + if ((swap_tmp = LIST_FIRST((head2))) != NULL) \ + swap_tmp->field.le_prev = &LIST_FIRST((head2)); \ + } while (0) + +/* + * Singly-linked Tail queue declarations. + */ +#define STAILQ_HEAD(name, type, qual) \ + struct name { \ + struct type *qual stqh_first;/* first element */ \ + struct type *qual *stqh_last;/* addr of last next element */ \ + } + +#define STAILQ_HEAD_INITIALIZER(head) \ + { NULL, &(head).stqh_first } + +#define STAILQ_ENTRY(type, qual) \ + struct { \ + struct type *qual stqe_next; /* next element */ \ + } + +/* + * Singly-linked Tail queue functions. + */ +#define STAILQ_INIT(head) do { \ + STAILQ_FIRST((head)) = NULL; \ + (head)->stqh_last = &STAILQ_FIRST((head)); \ + } while (0) + +#define STAILQ_EMPTY(head) ((head)->stqh_first == NULL) + +#define STAILQ_FIRST(head) ((head)->stqh_first) + +#define STAILQ_NEXT(elm, field) ((elm)->field.stqe_next) + +#define STAILQ_LAST(head, type, field) \ + (STAILQ_EMPTY((head)) ? NULL : \ + __containerof((head)->stqh_last, struct type, field.stqe_next)) + +#define STAILQ_FOREACH(var, head, field) \ + for((var) = STAILQ_FIRST((head)); \ + (var); \ + (var) = STAILQ_NEXT((var), field)) + +#define STAILQ_FOREACH_FROM(var, head, field) \ + for ((var) = ((var) ? (var) : STAILQ_FIRST((head))); \ + (var); \ + (var) = STAILQ_NEXT((var), field)) + +#define STAILQ_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = STAILQ_FIRST((head)); \ + (var) && ((tvar) = STAILQ_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define STAILQ_FOREACH_FROM_SAFE(var, head, field, tvar) \ + for ((var) = ((var) ? (var) : STAILQ_FIRST((head))); \ + (var) && ((tvar) = STAILQ_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define STAILQ_INSERT_HEAD(head, elm, field) do { \ + if ((STAILQ_NEXT((elm), field) = STAILQ_FIRST((head))) == NULL) \ + (head)->stqh_last = &STAILQ_NEXT((elm), field); \ + STAILQ_FIRST((head)) = (elm); \ + } while (0) + +#define STAILQ_INSERT_AFTER(head, tqelm, elm, field) do { \ + if ((STAILQ_NEXT((elm), field) = STAILQ_NEXT((tqelm), field)) == NULL) \ + (head)->stqh_last = &STAILQ_NEXT((elm), field); \ + STAILQ_NEXT((tqelm), field) = (elm); \ + } while (0) + +#define STAILQ_INSERT_TAIL(head, elm, field) do { \ + STAILQ_NEXT((elm), field) = NULL; \ + *(head)->stqh_last = (elm); \ + (head)->stqh_last = &STAILQ_NEXT((elm), field); \ + } while (0) + +#define STAILQ_CONCAT(head1, head2) do { \ + if (!STAILQ_EMPTY((head2))) { \ + *(head1)->stqh_last = (head2)->stqh_first; \ + (head1)->stqh_last = (head2)->stqh_last; \ + STAILQ_INIT((head2)); \ + } \ + } while (0) + +#define STAILQ_REMOVE_AFTER(head, elm, field) do { \ + if ((STAILQ_NEXT(elm, field) = \ + STAILQ_NEXT(STAILQ_NEXT(elm, field), field)) == NULL) \ + (head)->stqh_last = &STAILQ_NEXT((elm), field); \ + } while (0) + +#define STAILQ_REMOVE_HEAD(head, field) do { \ + if ((STAILQ_FIRST((head)) = \ + STAILQ_NEXT(STAILQ_FIRST((head)), field)) == NULL) \ + (head)->stqh_last = &STAILQ_FIRST((head)); \ + } while (0) + +#define STAILQ_REMOVE(head, elm, type, field) do { \ + if (STAILQ_FIRST((head)) == (elm)) { \ + STAILQ_REMOVE_HEAD((head), field); \ + } \ + else { \ + struct type *curelm = STAILQ_FIRST((head)); \ + while (STAILQ_NEXT(curelm, field) != (elm)) \ + curelm = STAILQ_NEXT(curelm, field); \ + STAILQ_REMOVE_AFTER(head, curelm, field); \ + } \ + } while (0) + +#define STAILQ_SWAP(head1, head2, type) do { \ + struct type *swap_first = STAILQ_FIRST(head1); \ + struct type **swap_last = (head1)->stqh_last; \ + STAILQ_FIRST(head1) = STAILQ_FIRST(head2); \ + (head1)->stqh_last = (head2)->stqh_last; \ + STAILQ_FIRST(head2) = swap_first; \ + (head2)->stqh_last = swap_last; \ + if (STAILQ_EMPTY(head1)) \ + (head1)->stqh_last = &STAILQ_FIRST(head1); \ + if (STAILQ_EMPTY(head2)) \ + (head2)->stqh_last = &STAILQ_FIRST(head2); \ + } while (0) + +/* + * Tail queue declarations. + */ +#define TAILQ_HEAD(name, type, qual) \ + struct name { \ + struct type *qual tqh_first; /* first element */ \ + struct type *qual *tqh_last; /* addr of last next element */ \ +} + +#define TAILQ_HEAD_INITIALIZER(head) \ + { NULL, &(head).tqh_first } + +#define TAILQ_ENTRY(type, qual) \ + struct { \ + struct type *qual tqe_next; /* next element */ \ + struct type *qual *tqe_prev; /* address of previous next element */ \ + } + +/* + * Tail queue functions. + */ +#define TAILQ_INIT(head) do { \ + TAILQ_FIRST((head)) = NULL; \ + (head)->tqh_last = &TAILQ_FIRST((head)); \ + } while (0) + +#define TAILQ_EMPTY(head) ((head)->tqh_first == NULL) + +#define TAILQ_FIRST(head) ((head)->tqh_first) + +#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next) + +#define TAILQ_PREV(elm, headname, field) \ + (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last)) + +#define TAILQ_LAST(head, headname) \ + (*(((struct headname *)((head)->tqh_last))->tqh_last)) + +#define TAILQ_FOREACH(var, head, field) \ + for ((var) = TAILQ_FIRST((head)); \ + (var); \ + (var) = TAILQ_NEXT((var), field)) + +#define TAILQ_FOREACH_FROM(var, head, field) \ + for ((var) = ((var) ? (var) : TAILQ_FIRST((head))); \ + (var); \ + (var) = TAILQ_NEXT((var), field)) + +#define TAILQ_FOREACH_SAFE(var, head, field, tvar) \ + for ((var) = TAILQ_FIRST((head)); \ + (var) && ((tvar) = TAILQ_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define TAILQ_FOREACH_FROM_SAFE(var, head, field, tvar) \ + for ((var) = ((var) ? (var) : TAILQ_FIRST((head))); \ + (var) && ((tvar) = TAILQ_NEXT((var), field), 1); \ + (var) = (tvar)) + +#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \ + for ((var) = TAILQ_LAST((head), headname); \ + (var); \ + (var) = TAILQ_PREV((var), headname, field)) + +#define TAILQ_FOREACH_REVERSE_FROM(var, head, headname, field) \ + for ((var) = ((var) ? (var) : TAILQ_LAST((head), headname)); \ + (var); \ + (var) = TAILQ_PREV((var), headname, field)) + +#define TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar) \ + for ((var) = TAILQ_LAST((head), headname); \ + (var) && ((tvar) = TAILQ_PREV((var), headname, field), 1); \ + (var) = (tvar)) + +#define TAILQ_FOREACH_REVERSE_FROM_SAFE(var, head, headname, field, tvar) \ + for ((var) = ((var) ? (var) : TAILQ_LAST((head), headname)); \ + (var) && ((tvar) = TAILQ_PREV((var), headname, field), 1); \ + (var) = (tvar)) + +#define TAILQ_INSERT_HEAD(head, elm, field) do { \ + if ((TAILQ_NEXT((elm), field) = TAILQ_FIRST((head))) != NULL) \ + TAILQ_FIRST((head))->field.tqe_prev = \ + &TAILQ_NEXT((elm), field); \ + else \ + (head)->tqh_last = &TAILQ_NEXT((elm), field); \ + TAILQ_FIRST((head)) = (elm); \ + (elm)->field.tqe_prev = &TAILQ_FIRST((head)); \ + } while (0) + +#define TAILQ_INSERT_BEFORE(listelm, elm, field) do { \ + (elm)->field.tqe_prev = (listelm)->field.tqe_prev; \ + TAILQ_NEXT((elm), field) = (listelm); \ + *(listelm)->field.tqe_prev = (elm); \ + (listelm)->field.tqe_prev = &TAILQ_NEXT((elm), field); \ + } while (0) + +#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do { \ + if ((TAILQ_NEXT((elm), field) = TAILQ_NEXT((listelm), field)) != NULL) \ + TAILQ_NEXT((elm), field)->field.tqe_prev = \ + &TAILQ_NEXT((elm), field); \ + else \ + (head)->tqh_last = &TAILQ_NEXT((elm), field); \ + TAILQ_NEXT((listelm), field) = (elm); \ + (elm)->field.tqe_prev = &TAILQ_NEXT((listelm), field); \ + } while (0) + +#define TAILQ_INSERT_TAIL(head, elm, field) do { \ + TAILQ_NEXT((elm), field) = NULL; \ + (elm)->field.tqe_prev = (head)->tqh_last; \ + *(head)->tqh_last = (elm); \ + (head)->tqh_last = &TAILQ_NEXT((elm), field); \ + } while (0) + +#define TAILQ_CONCAT(head1, head2, field) do { \ + if (!TAILQ_EMPTY(head2)) { \ + *(head1)->tqh_last = (head2)->tqh_first; \ + (head2)->tqh_first->field.tqe_prev = (head1)->tqh_last; \ + (head1)->tqh_last = (head2)->tqh_last; \ + TAILQ_INIT((head2)); \ + } \ + } while (0) + +#define TAILQ_REMOVE(head, elm, field) do { \ + if ((TAILQ_NEXT((elm), field)) != NULL) \ + TAILQ_NEXT((elm), field)->field.tqe_prev = \ + (elm)->field.tqe_prev; \ + else \ + (head)->tqh_last = (elm)->field.tqe_prev; \ + *(elm)->field.tqe_prev = TAILQ_NEXT((elm), field); \ + } while (0) + +#define TAILQ_SWAP(head1, head2, type, field) do { \ + struct type *swap_first = (head1)->tqh_first; \ + struct type **swap_last = (head1)->tqh_last; \ + (head1)->tqh_first = (head2)->tqh_first; \ + (head1)->tqh_last = (head2)->tqh_last; \ + (head2)->tqh_first = swap_first; \ + (head2)->tqh_last = swap_last; \ + if ((swap_first = (head1)->tqh_first) != NULL) \ + swap_first->field.tqe_prev = &(head1)->tqh_first; \ + else \ + (head1)->tqh_last = &(head1)->tqh_first; \ + if ((swap_first = (head2)->tqh_first) != NULL) \ + swap_first->field.tqe_prev = &(head2)->tqh_first; \ + else \ + (head2)->tqh_last = &(head2)->tqh_first; \ + } while (0) + +#endif diff --git a/Core/jni/external/xhook/tree.h b/Core/jni/external/xhook/tree.h new file mode 100644 index 00000000..dc938ae5 --- /dev/null +++ b/Core/jni/external/xhook/tree.h @@ -0,0 +1,768 @@ +/* $NetBSD: tree.h,v 1.8 2004/03/28 19:38:30 provos Exp $ */ +/* $OpenBSD: tree.h,v 1.7 2002/10/17 21:51:54 art Exp $ */ +/* $FreeBSD: stable/9/sys/sys/tree.h 189204 2009-03-01 04:57:23Z bms $ */ + +/*- + * Copyright 2002 Niels Provos + * 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. + */ + +#ifndef TREE_H +#define TREE_H + +/* #include */ +#ifndef __unused +#define __unused __attribute__((__unused__)) +#endif + +/* + * This file defines data structures for different types of trees: + * splay trees and red-black trees. + * + * A splay tree is a self-organizing data structure. Every operation + * on the tree causes a splay to happen. The splay moves the requested + * node to the root of the tree and partly rebalances it. + * + * This has the benefit that request locality causes faster lookups as + * the requested nodes move to the top of the tree. On the other hand, + * every lookup causes memory writes. + * + * The Balance Theorem bounds the total access time for m operations + * and n inserts on an initially empty tree as O((m + n)lg n). The + * amortized cost for a sequence of m accesses to a splay tree is O(lg n); + * + * A red-black tree is a binary search tree with the node color as an + * extra attribute. It fulfills a set of conditions: + * - every search path from the root to a leaf consists of the + * same number of black nodes, + * - each red node (except for the root) has a black parent, + * - each leaf node is black. + * + * Every operation on a red-black tree is bounded as O(lg n). + * The maximum height of a red-black tree is 2lg (n+1). + */ + +#define SPLAY_HEAD(name, type) \ +struct name { \ + struct type *sph_root; /* root of the tree */ \ +} + +#define SPLAY_INITIALIZER(root) \ + { NULL } + +#define SPLAY_INIT(root) do { \ + (root)->sph_root = NULL; \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_ENTRY(type) \ +struct { \ + struct type *spe_left; /* left element */ \ + struct type *spe_right; /* right element */ \ +} + +#define SPLAY_LEFT(elm, field) (elm)->field.spe_left +#define SPLAY_RIGHT(elm, field) (elm)->field.spe_right +#define SPLAY_ROOT(head) (head)->sph_root +#define SPLAY_EMPTY(head) (SPLAY_ROOT(head) == NULL) + +/* SPLAY_ROTATE_{LEFT,RIGHT} expect that tmp hold SPLAY_{RIGHT,LEFT} */ +#define SPLAY_ROTATE_RIGHT(head, tmp, field) do { \ + SPLAY_LEFT((head)->sph_root, field) = SPLAY_RIGHT(tmp, field); \ + SPLAY_RIGHT(tmp, field) = (head)->sph_root; \ + (head)->sph_root = tmp; \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_ROTATE_LEFT(head, tmp, field) do { \ + SPLAY_RIGHT((head)->sph_root, field) = SPLAY_LEFT(tmp, field); \ + SPLAY_LEFT(tmp, field) = (head)->sph_root; \ + (head)->sph_root = tmp; \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_LINKLEFT(head, tmp, field) do { \ + SPLAY_LEFT(tmp, field) = (head)->sph_root; \ + tmp = (head)->sph_root; \ + (head)->sph_root = SPLAY_LEFT((head)->sph_root, field); \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_LINKRIGHT(head, tmp, field) do { \ + SPLAY_RIGHT(tmp, field) = (head)->sph_root; \ + tmp = (head)->sph_root; \ + (head)->sph_root = SPLAY_RIGHT((head)->sph_root, field); \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_ASSEMBLE(head, node, left, right, field) do { \ + SPLAY_RIGHT(left, field) = SPLAY_LEFT((head)->sph_root, field); \ + SPLAY_LEFT(right, field) = SPLAY_RIGHT((head)->sph_root, field);\ + SPLAY_LEFT((head)->sph_root, field) = SPLAY_RIGHT(node, field); \ + SPLAY_RIGHT((head)->sph_root, field) = SPLAY_LEFT(node, field); \ +} while (/*CONSTCOND*/ 0) + +/* Generates prototypes and inline functions */ + +#define SPLAY_PROTOTYPE(name, type, field, cmp) \ +void name##_SPLAY(struct name *, struct type *); \ +void name##_SPLAY_MINMAX(struct name *, int); \ +struct type *name##_SPLAY_INSERT(struct name *, struct type *); \ +struct type *name##_SPLAY_REMOVE(struct name *, struct type *); \ + \ +/* Finds the node with the same key as elm */ \ +static __inline struct type * \ +name##_SPLAY_FIND(struct name *head, struct type *elm) \ +{ \ + if (SPLAY_EMPTY(head)) \ + return(NULL); \ + name##_SPLAY(head, elm); \ + if ((cmp)(elm, (head)->sph_root) == 0) \ + return (head->sph_root); \ + return (NULL); \ +} \ + \ +static __inline struct type * \ +name##_SPLAY_NEXT(struct name *head, struct type *elm) \ +{ \ + name##_SPLAY(head, elm); \ + if (SPLAY_RIGHT(elm, field) != NULL) { \ + elm = SPLAY_RIGHT(elm, field); \ + while (SPLAY_LEFT(elm, field) != NULL) { \ + elm = SPLAY_LEFT(elm, field); \ + } \ + } else \ + elm = NULL; \ + return (elm); \ +} \ + \ +static __inline struct type * \ +name##_SPLAY_MIN_MAX(struct name *head, int val) \ +{ \ + name##_SPLAY_MINMAX(head, val); \ + return (SPLAY_ROOT(head)); \ +} + +/* Main splay operation. + * Moves node close to the key of elm to top + */ +#define SPLAY_GENERATE(name, type, field, cmp) \ +struct type * \ +name##_SPLAY_INSERT(struct name *head, struct type *elm) \ +{ \ + if (SPLAY_EMPTY(head)) { \ + SPLAY_LEFT(elm, field) = SPLAY_RIGHT(elm, field) = NULL; \ + } else { \ + int __comp; \ + name##_SPLAY(head, elm); \ + __comp = (cmp)(elm, (head)->sph_root); \ + if(__comp < 0) { \ + SPLAY_LEFT(elm, field) = SPLAY_LEFT((head)->sph_root, field);\ + SPLAY_RIGHT(elm, field) = (head)->sph_root; \ + SPLAY_LEFT((head)->sph_root, field) = NULL; \ + } else if (__comp > 0) { \ + SPLAY_RIGHT(elm, field) = SPLAY_RIGHT((head)->sph_root, field);\ + SPLAY_LEFT(elm, field) = (head)->sph_root; \ + SPLAY_RIGHT((head)->sph_root, field) = NULL; \ + } else \ + return ((head)->sph_root); \ + } \ + (head)->sph_root = (elm); \ + return (NULL); \ +} \ + \ +struct type * \ +name##_SPLAY_REMOVE(struct name *head, struct type *elm) \ +{ \ + struct type *__tmp; \ + if (SPLAY_EMPTY(head)) \ + return (NULL); \ + name##_SPLAY(head, elm); \ + if ((cmp)(elm, (head)->sph_root) == 0) { \ + if (SPLAY_LEFT((head)->sph_root, field) == NULL) { \ + (head)->sph_root = SPLAY_RIGHT((head)->sph_root, field);\ + } else { \ + __tmp = SPLAY_RIGHT((head)->sph_root, field); \ + (head)->sph_root = SPLAY_LEFT((head)->sph_root, field);\ + name##_SPLAY(head, elm); \ + SPLAY_RIGHT((head)->sph_root, field) = __tmp; \ + } \ + return (elm); \ + } \ + return (NULL); \ +} \ + \ +void \ +name##_SPLAY(struct name *head, struct type *elm) \ +{ \ + struct type __node, *__left, *__right, *__tmp; \ + int __comp; \ +\ + SPLAY_LEFT(&__node, field) = SPLAY_RIGHT(&__node, field) = NULL;\ + __left = __right = &__node; \ +\ + while ((__comp = (cmp)(elm, (head)->sph_root)) != 0) { \ + if (__comp < 0) { \ + __tmp = SPLAY_LEFT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if ((cmp)(elm, __tmp) < 0){ \ + SPLAY_ROTATE_RIGHT(head, __tmp, field); \ + if (SPLAY_LEFT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKLEFT(head, __right, field); \ + } else if (__comp > 0) { \ + __tmp = SPLAY_RIGHT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if ((cmp)(elm, __tmp) > 0){ \ + SPLAY_ROTATE_LEFT(head, __tmp, field); \ + if (SPLAY_RIGHT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKRIGHT(head, __left, field); \ + } \ + } \ + SPLAY_ASSEMBLE(head, &__node, __left, __right, field); \ +} \ + \ +/* Splay with either the minimum or the maximum element \ + * Used to find minimum or maximum element in tree. \ + */ \ +void name##_SPLAY_MINMAX(struct name *head, int __comp) \ +{ \ + struct type __node, *__left, *__right, *__tmp; \ +\ + SPLAY_LEFT(&__node, field) = SPLAY_RIGHT(&__node, field) = NULL;\ + __left = __right = &__node; \ +\ + while (1) { \ + if (__comp < 0) { \ + __tmp = SPLAY_LEFT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if (__comp < 0){ \ + SPLAY_ROTATE_RIGHT(head, __tmp, field); \ + if (SPLAY_LEFT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKLEFT(head, __right, field); \ + } else if (__comp > 0) { \ + __tmp = SPLAY_RIGHT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if (__comp > 0) { \ + SPLAY_ROTATE_LEFT(head, __tmp, field); \ + if (SPLAY_RIGHT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKRIGHT(head, __left, field); \ + } \ + } \ + SPLAY_ASSEMBLE(head, &__node, __left, __right, field); \ +} + +#define SPLAY_NEGINF -1 +#define SPLAY_INF 1 + +#define SPLAY_INSERT(name, x, y) name##_SPLAY_INSERT(x, y) +#define SPLAY_REMOVE(name, x, y) name##_SPLAY_REMOVE(x, y) +#define SPLAY_FIND(name, x, y) name##_SPLAY_FIND(x, y) +#define SPLAY_NEXT(name, x, y) name##_SPLAY_NEXT(x, y) +#define SPLAY_MIN(name, x) (SPLAY_EMPTY(x) ? NULL \ + : name##_SPLAY_MIN_MAX(x, SPLAY_NEGINF)) +#define SPLAY_MAX(name, x) (SPLAY_EMPTY(x) ? NULL \ + : name##_SPLAY_MIN_MAX(x, SPLAY_INF)) + +#define SPLAY_FOREACH(x, name, head) \ + for ((x) = SPLAY_MIN(name, head); \ + (x) != NULL; \ + (x) = SPLAY_NEXT(name, head, x)) + +/* Macros that define a red-black tree */ +#define RB_HEAD(name, type) \ +struct name { \ + struct type *rbh_root; /* root of the tree */ \ +} + +#define RB_INITIALIZER(root) \ + { NULL } + +#define RB_INIT(root) do { \ + (root)->rbh_root = NULL; \ +} while (/*CONSTCOND*/ 0) + +#define RB_BLACK 0 +#define RB_RED 1 +#define RB_ENTRY(type) \ +struct { \ + struct type *rbe_left; /* left element */ \ + struct type *rbe_right; /* right element */ \ + struct type *rbe_parent; /* parent element */ \ + int rbe_color; /* node color */ \ +} + +#define RB_LEFT(elm, field) (elm)->field.rbe_left +#define RB_RIGHT(elm, field) (elm)->field.rbe_right +#define RB_PARENT(elm, field) (elm)->field.rbe_parent +#define RB_COLOR(elm, field) (elm)->field.rbe_color +#define RB_ROOT(head) (head)->rbh_root +#define RB_EMPTY(head) (RB_ROOT(head) == NULL) + +#define RB_SET(elm, parent, field) do { \ + RB_PARENT(elm, field) = parent; \ + RB_LEFT(elm, field) = RB_RIGHT(elm, field) = NULL; \ + RB_COLOR(elm, field) = RB_RED; \ +} while (/*CONSTCOND*/ 0) + +#define RB_SET_BLACKRED(black, red, field) do { \ + RB_COLOR(black, field) = RB_BLACK; \ + RB_COLOR(red, field) = RB_RED; \ +} while (/*CONSTCOND*/ 0) + +#ifndef RB_AUGMENT +#define RB_AUGMENT(x) do {} while (0) +#endif + +#define RB_ROTATE_LEFT(head, elm, tmp, field) do { \ + (tmp) = RB_RIGHT(elm, field); \ + if ((RB_RIGHT(elm, field) = RB_LEFT(tmp, field)) != NULL) { \ + RB_PARENT(RB_LEFT(tmp, field), field) = (elm); \ + } \ + RB_AUGMENT(elm); \ + if ((RB_PARENT(tmp, field) = RB_PARENT(elm, field)) != NULL) { \ + if ((elm) == RB_LEFT(RB_PARENT(elm, field), field)) \ + RB_LEFT(RB_PARENT(elm, field), field) = (tmp); \ + else \ + RB_RIGHT(RB_PARENT(elm, field), field) = (tmp); \ + } else \ + (head)->rbh_root = (tmp); \ + RB_LEFT(tmp, field) = (elm); \ + RB_PARENT(elm, field) = (tmp); \ + RB_AUGMENT(tmp); \ + if ((RB_PARENT(tmp, field))) \ + RB_AUGMENT(RB_PARENT(tmp, field)); \ +} while (/*CONSTCOND*/ 0) + +#define RB_ROTATE_RIGHT(head, elm, tmp, field) do { \ + (tmp) = RB_LEFT(elm, field); \ + if ((RB_LEFT(elm, field) = RB_RIGHT(tmp, field)) != NULL) { \ + RB_PARENT(RB_RIGHT(tmp, field), field) = (elm); \ + } \ + RB_AUGMENT(elm); \ + if ((RB_PARENT(tmp, field) = RB_PARENT(elm, field)) != NULL) { \ + if ((elm) == RB_LEFT(RB_PARENT(elm, field), field)) \ + RB_LEFT(RB_PARENT(elm, field), field) = (tmp); \ + else \ + RB_RIGHT(RB_PARENT(elm, field), field) = (tmp); \ + } else \ + (head)->rbh_root = (tmp); \ + RB_RIGHT(tmp, field) = (elm); \ + RB_PARENT(elm, field) = (tmp); \ + RB_AUGMENT(tmp); \ + if ((RB_PARENT(tmp, field))) \ + RB_AUGMENT(RB_PARENT(tmp, field)); \ +} while (/*CONSTCOND*/ 0) + +/* Generates prototypes and inline functions */ +#define RB_PROTOTYPE(name, type, field, cmp) \ + RB_PROTOTYPE_INTERNAL(name, type, field, cmp,) +#define RB_PROTOTYPE_STATIC(name, type, field, cmp) \ + RB_PROTOTYPE_INTERNAL(name, type, field, cmp, __unused static) +#define RB_PROTOTYPE_INTERNAL(name, type, field, cmp, attr) \ +attr void name##_RB_INSERT_COLOR(struct name *, struct type *); \ +attr void name##_RB_REMOVE_COLOR(struct name *, struct type *, struct type *);\ +attr struct type *name##_RB_REMOVE(struct name *, struct type *); \ +attr struct type *name##_RB_INSERT(struct name *, struct type *); \ +attr struct type *name##_RB_FIND(struct name *, struct type *); \ +attr struct type *name##_RB_NFIND(struct name *, struct type *); \ +attr struct type *name##_RB_NEXT(struct type *); \ +attr struct type *name##_RB_PREV(struct type *); \ +attr struct type *name##_RB_MINMAX(struct name *, int); \ + \ + +/* Main rb operation. + * Moves node close to the key of elm to top + */ +#define RB_GENERATE(name, type, field, cmp) \ + RB_GENERATE_INTERNAL(name, type, field, cmp,) +#define RB_GENERATE_STATIC(name, type, field, cmp) \ + RB_GENERATE_INTERNAL(name, type, field, cmp, __unused static) +#define RB_GENERATE_INTERNAL(name, type, field, cmp, attr) \ +attr void \ +name##_RB_INSERT_COLOR(struct name *head, struct type *elm) \ +{ \ + struct type *parent, *gparent, *tmp; \ + while ((parent = RB_PARENT(elm, field)) != NULL && \ + RB_COLOR(parent, field) == RB_RED) { \ + gparent = RB_PARENT(parent, field); \ + if (parent == RB_LEFT(gparent, field)) { \ + tmp = RB_RIGHT(gparent, field); \ + if (tmp && RB_COLOR(tmp, field) == RB_RED) { \ + RB_COLOR(tmp, field) = RB_BLACK; \ + RB_SET_BLACKRED(parent, gparent, field);\ + elm = gparent; \ + continue; \ + } \ + if (RB_RIGHT(parent, field) == elm) { \ + RB_ROTATE_LEFT(head, parent, tmp, field);\ + tmp = parent; \ + parent = elm; \ + elm = tmp; \ + } \ + RB_SET_BLACKRED(parent, gparent, field); \ + RB_ROTATE_RIGHT(head, gparent, tmp, field); \ + } else { \ + tmp = RB_LEFT(gparent, field); \ + if (tmp && RB_COLOR(tmp, field) == RB_RED) { \ + RB_COLOR(tmp, field) = RB_BLACK; \ + RB_SET_BLACKRED(parent, gparent, field);\ + elm = gparent; \ + continue; \ + } \ + if (RB_LEFT(parent, field) == elm) { \ + RB_ROTATE_RIGHT(head, parent, tmp, field);\ + tmp = parent; \ + parent = elm; \ + elm = tmp; \ + } \ + RB_SET_BLACKRED(parent, gparent, field); \ + RB_ROTATE_LEFT(head, gparent, tmp, field); \ + } \ + } \ + RB_COLOR(head->rbh_root, field) = RB_BLACK; \ +} \ + \ +attr void \ +name##_RB_REMOVE_COLOR(struct name *head, struct type *parent, struct type *elm) \ +{ \ + struct type *tmp; \ + while ((elm == NULL || RB_COLOR(elm, field) == RB_BLACK) && \ + elm != RB_ROOT(head)) { \ + if (RB_LEFT(parent, field) == elm) { \ + tmp = RB_RIGHT(parent, field); \ + if (RB_COLOR(tmp, field) == RB_RED) { \ + RB_SET_BLACKRED(tmp, parent, field); \ + RB_ROTATE_LEFT(head, parent, tmp, field);\ + tmp = RB_RIGHT(parent, field); \ + } \ + if ((RB_LEFT(tmp, field) == NULL || \ + RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) &&\ + (RB_RIGHT(tmp, field) == NULL || \ + RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK)) {\ + RB_COLOR(tmp, field) = RB_RED; \ + elm = parent; \ + parent = RB_PARENT(elm, field); \ + } else { \ + if (RB_RIGHT(tmp, field) == NULL || \ + RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK) {\ + struct type *oleft; \ + if ((oleft = RB_LEFT(tmp, field)) \ + != NULL) \ + RB_COLOR(oleft, field) = RB_BLACK;\ + RB_COLOR(tmp, field) = RB_RED; \ + RB_ROTATE_RIGHT(head, tmp, oleft, field);\ + tmp = RB_RIGHT(parent, field); \ + } \ + RB_COLOR(tmp, field) = RB_COLOR(parent, field);\ + RB_COLOR(parent, field) = RB_BLACK; \ + if (RB_RIGHT(tmp, field)) \ + RB_COLOR(RB_RIGHT(tmp, field), field) = RB_BLACK;\ + RB_ROTATE_LEFT(head, parent, tmp, field);\ + elm = RB_ROOT(head); \ + break; \ + } \ + } else { \ + tmp = RB_LEFT(parent, field); \ + if (RB_COLOR(tmp, field) == RB_RED) { \ + RB_SET_BLACKRED(tmp, parent, field); \ + RB_ROTATE_RIGHT(head, parent, tmp, field);\ + tmp = RB_LEFT(parent, field); \ + } \ + if ((RB_LEFT(tmp, field) == NULL || \ + RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) &&\ + (RB_RIGHT(tmp, field) == NULL || \ + RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK)) {\ + RB_COLOR(tmp, field) = RB_RED; \ + elm = parent; \ + parent = RB_PARENT(elm, field); \ + } else { \ + if (RB_LEFT(tmp, field) == NULL || \ + RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) {\ + struct type *oright; \ + if ((oright = RB_RIGHT(tmp, field)) \ + != NULL) \ + RB_COLOR(oright, field) = RB_BLACK;\ + RB_COLOR(tmp, field) = RB_RED; \ + RB_ROTATE_LEFT(head, tmp, oright, field);\ + tmp = RB_LEFT(parent, field); \ + } \ + RB_COLOR(tmp, field) = RB_COLOR(parent, field);\ + RB_COLOR(parent, field) = RB_BLACK; \ + if (RB_LEFT(tmp, field)) \ + RB_COLOR(RB_LEFT(tmp, field), field) = RB_BLACK;\ + RB_ROTATE_RIGHT(head, parent, tmp, field);\ + elm = RB_ROOT(head); \ + break; \ + } \ + } \ + } \ + if (elm) \ + RB_COLOR(elm, field) = RB_BLACK; \ +} \ + \ +attr struct type * \ +name##_RB_REMOVE(struct name *head, struct type *elm) \ +{ \ + struct type *child, *parent, *old = elm; \ + int color; \ + if (RB_LEFT(elm, field) == NULL) \ + child = RB_RIGHT(elm, field); \ + else if (RB_RIGHT(elm, field) == NULL) \ + child = RB_LEFT(elm, field); \ + else { \ + struct type *left; \ + elm = RB_RIGHT(elm, field); \ + while ((left = RB_LEFT(elm, field)) != NULL) \ + elm = left; \ + child = RB_RIGHT(elm, field); \ + parent = RB_PARENT(elm, field); \ + color = RB_COLOR(elm, field); \ + if (child) \ + RB_PARENT(child, field) = parent; \ + if (parent) { \ + if (RB_LEFT(parent, field) == elm) \ + RB_LEFT(parent, field) = child; \ + else \ + RB_RIGHT(parent, field) = child; \ + RB_AUGMENT(parent); \ + } else \ + RB_ROOT(head) = child; \ + if (RB_PARENT(elm, field) == old) \ + parent = elm; \ + (elm)->field = (old)->field; \ + if (RB_PARENT(old, field)) { \ + if (RB_LEFT(RB_PARENT(old, field), field) == old)\ + RB_LEFT(RB_PARENT(old, field), field) = elm;\ + else \ + RB_RIGHT(RB_PARENT(old, field), field) = elm;\ + RB_AUGMENT(RB_PARENT(old, field)); \ + } else \ + RB_ROOT(head) = elm; \ + RB_PARENT(RB_LEFT(old, field), field) = elm; \ + if (RB_RIGHT(old, field)) \ + RB_PARENT(RB_RIGHT(old, field), field) = elm; \ + if (parent) { \ + left = parent; \ + do { \ + RB_AUGMENT(left); \ + } while ((left = RB_PARENT(left, field)) != NULL); \ + } \ + goto color; \ + } \ + parent = RB_PARENT(elm, field); \ + color = RB_COLOR(elm, field); \ + if (child) \ + RB_PARENT(child, field) = parent; \ + if (parent) { \ + if (RB_LEFT(parent, field) == elm) \ + RB_LEFT(parent, field) = child; \ + else \ + RB_RIGHT(parent, field) = child; \ + RB_AUGMENT(parent); \ + } else \ + RB_ROOT(head) = child; \ +color: \ + if (color == RB_BLACK) \ + name##_RB_REMOVE_COLOR(head, parent, child); \ + return (old); \ +} \ + \ +/* Inserts a node into the RB tree */ \ +attr struct type * \ +name##_RB_INSERT(struct name *head, struct type *elm) \ +{ \ + struct type *tmp; \ + struct type *parent = NULL; \ + int comp = 0; \ + tmp = RB_ROOT(head); \ + while (tmp) { \ + parent = tmp; \ + comp = (cmp)(elm, parent); \ + if (comp < 0) \ + tmp = RB_LEFT(tmp, field); \ + else if (comp > 0) \ + tmp = RB_RIGHT(tmp, field); \ + else \ + return (tmp); \ + } \ + RB_SET(elm, parent, field); \ + if (parent != NULL) { \ + if (comp < 0) \ + RB_LEFT(parent, field) = elm; \ + else \ + RB_RIGHT(parent, field) = elm; \ + RB_AUGMENT(parent); \ + } else \ + RB_ROOT(head) = elm; \ + name##_RB_INSERT_COLOR(head, elm); \ + return (NULL); \ +} \ + \ +/* Finds the node with the same key as elm */ \ +attr struct type * \ +name##_RB_FIND(struct name *head, struct type *elm) \ +{ \ + struct type *tmp = RB_ROOT(head); \ + int comp; \ + while (tmp) { \ + comp = cmp(elm, tmp); \ + if (comp < 0) \ + tmp = RB_LEFT(tmp, field); \ + else if (comp > 0) \ + tmp = RB_RIGHT(tmp, field); \ + else \ + return (tmp); \ + } \ + return (NULL); \ +} \ + \ +/* Finds the first node greater than or equal to the search key */ \ +attr struct type * \ +name##_RB_NFIND(struct name *head, struct type *elm) \ +{ \ + struct type *tmp = RB_ROOT(head); \ + struct type *res = NULL; \ + int comp; \ + while (tmp) { \ + comp = cmp(elm, tmp); \ + if (comp < 0) { \ + res = tmp; \ + tmp = RB_LEFT(tmp, field); \ + } \ + else if (comp > 0) \ + tmp = RB_RIGHT(tmp, field); \ + else \ + return (tmp); \ + } \ + return (res); \ +} \ + \ +/* ARGSUSED */ \ +attr struct type * \ +name##_RB_NEXT(struct type *elm) \ +{ \ + if (RB_RIGHT(elm, field)) { \ + elm = RB_RIGHT(elm, field); \ + while (RB_LEFT(elm, field)) \ + elm = RB_LEFT(elm, field); \ + } else { \ + if (RB_PARENT(elm, field) && \ + (elm == RB_LEFT(RB_PARENT(elm, field), field))) \ + elm = RB_PARENT(elm, field); \ + else { \ + while (RB_PARENT(elm, field) && \ + (elm == RB_RIGHT(RB_PARENT(elm, field), field)))\ + elm = RB_PARENT(elm, field); \ + elm = RB_PARENT(elm, field); \ + } \ + } \ + return (elm); \ +} \ + \ +/* ARGSUSED */ \ +attr struct type * \ +name##_RB_PREV(struct type *elm) \ +{ \ + if (RB_LEFT(elm, field)) { \ + elm = RB_LEFT(elm, field); \ + while (RB_RIGHT(elm, field)) \ + elm = RB_RIGHT(elm, field); \ + } else { \ + if (RB_PARENT(elm, field) && \ + (elm == RB_RIGHT(RB_PARENT(elm, field), field))) \ + elm = RB_PARENT(elm, field); \ + else { \ + while (RB_PARENT(elm, field) && \ + (elm == RB_LEFT(RB_PARENT(elm, field), field)))\ + elm = RB_PARENT(elm, field); \ + elm = RB_PARENT(elm, field); \ + } \ + } \ + return (elm); \ +} \ + \ +attr struct type * \ +name##_RB_MINMAX(struct name *head, int val) \ +{ \ + struct type *tmp = RB_ROOT(head); \ + struct type *parent = NULL; \ + while (tmp) { \ + parent = tmp; \ + if (val < 0) \ + tmp = RB_LEFT(tmp, field); \ + else \ + tmp = RB_RIGHT(tmp, field); \ + } \ + return (parent); \ +} + +#define RB_NEGINF -1 +#define RB_INF 1 + +#define RB_INSERT(name, x, y) name##_RB_INSERT(x, y) +#define RB_REMOVE(name, x, y) name##_RB_REMOVE(x, y) +#define RB_FIND(name, x, y) name##_RB_FIND(x, y) +#define RB_NFIND(name, x, y) name##_RB_NFIND(x, y) +#define RB_NEXT(name, x, y) name##_RB_NEXT(y) +#define RB_PREV(name, x, y) name##_RB_PREV(y) +#define RB_MIN(name, x) name##_RB_MINMAX(x, RB_NEGINF) +#define RB_MAX(name, x) name##_RB_MINMAX(x, RB_INF) + +#define RB_FOREACH(x, name, head) \ + for ((x) = RB_MIN(name, head); \ + (x) != NULL; \ + (x) = name##_RB_NEXT(x)) + +#define RB_FOREACH_FROM(x, name, y) \ + for ((x) = (y); \ + ((x) != NULL) && ((y) = name##_RB_NEXT(x), (x) != NULL); \ + (x) = (y)) + +#define RB_FOREACH_SAFE(x, name, head, y) \ + for ((x) = RB_MIN(name, head); \ + ((x) != NULL) && ((y) = name##_RB_NEXT(x), (x) != NULL); \ + (x) = (y)) + +#define RB_FOREACH_REVERSE(x, name, head) \ + for ((x) = RB_MAX(name, head); \ + (x) != NULL; \ + (x) = name##_RB_PREV(x)) + +#define RB_FOREACH_REVERSE_FROM(x, name, y) \ + for ((x) = (y); \ + ((x) != NULL) && ((y) = name##_RB_PREV(x), (x) != NULL); \ + (x) = (y)) + +#define RB_FOREACH_REVERSE_SAFE(x, name, head, y) \ + for ((x) = RB_MAX(name, head); \ + ((x) != NULL) && ((y) = name##_RB_PREV(x), (x) != NULL); \ + (x) = (y)) + +#endif diff --git a/Core/jni/external/xhook/xh_core.c b/Core/jni/external/xhook/xh_core.c new file mode 100644 index 00000000..4d14ba47 --- /dev/null +++ b/Core/jni/external/xhook/xh_core.c @@ -0,0 +1,656 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "queue.h" +#include "tree.h" +#include "xh_errno.h" +#include "xh_log.h" +#include "xh_elf.h" +#include "xh_version.h" +#include "xh_core.h" + +#define XH_CORE_DEBUG 0 + +//registered hook info collection +typedef struct xh_core_hook_info +{ +#if XH_CORE_DEBUG + char *pathname_regex_str; +#endif + regex_t pathname_regex; + char *symbol; + void *new_func; + void **old_func; + TAILQ_ENTRY(xh_core_hook_info,) link; +} xh_core_hook_info_t; +typedef TAILQ_HEAD(xh_core_hook_info_queue, xh_core_hook_info,) xh_core_hook_info_queue_t; + +//ignored hook info collection +typedef struct xh_core_ignore_info +{ +#if XH_CORE_DEBUG + char *pathname_regex_str; +#endif + regex_t pathname_regex; + char *symbol; //NULL meaning for all symbols + TAILQ_ENTRY(xh_core_ignore_info,) link; +} xh_core_ignore_info_t; +typedef TAILQ_HEAD(xh_core_ignore_info_queue, xh_core_ignore_info,) xh_core_ignore_info_queue_t; + +//required info from /proc/self/maps +typedef struct xh_core_map_info +{ + char *pathname; + uintptr_t base_addr; + xh_elf_t elf; + RB_ENTRY(xh_core_map_info) link; +} xh_core_map_info_t; +static __inline__ int xh_core_map_info_cmp(xh_core_map_info_t *a, xh_core_map_info_t *b) +{ + return strcmp(a->pathname, b->pathname); +} +typedef RB_HEAD(xh_core_map_info_tree, xh_core_map_info) xh_core_map_info_tree_t; +RB_GENERATE_STATIC(xh_core_map_info_tree, xh_core_map_info, link, xh_core_map_info_cmp) + +//signal handler for SIGSEGV +//for xh_elf_init(), xh_elf_hook(), xh_elf_check_elfheader() +static int xh_core_sigsegv_enable = 1; //enable by default +static struct sigaction xh_core_sigsegv_act_old; +static volatile int xh_core_sigsegv_flag = 0; +static sigjmp_buf xh_core_sigsegv_env; +static void xh_core_sigsegv_handler(int sig) +{ + (void)sig; + + if(xh_core_sigsegv_flag) + siglongjmp(xh_core_sigsegv_env, 1); + else + sigaction(SIGSEGV, &xh_core_sigsegv_act_old, NULL); +} +static int xh_core_add_sigsegv_handler() +{ + struct sigaction act; + + if(!xh_core_sigsegv_enable) return 0; + + if(0 != sigemptyset(&act.sa_mask)) return (0 == errno ? XH_ERRNO_UNKNOWN : errno); + act.sa_handler = xh_core_sigsegv_handler; + + if(0 != sigaction(SIGSEGV, &act, &xh_core_sigsegv_act_old)) + return (0 == errno ? XH_ERRNO_UNKNOWN : errno); + + return 0; +} +static void xh_core_del_sigsegv_handler() +{ + if(!xh_core_sigsegv_enable) return; + + sigaction(SIGSEGV, &xh_core_sigsegv_act_old, NULL); +} + + +static xh_core_hook_info_queue_t xh_core_hook_info = TAILQ_HEAD_INITIALIZER(xh_core_hook_info); +static xh_core_ignore_info_queue_t xh_core_ignore_info = TAILQ_HEAD_INITIALIZER(xh_core_ignore_info); +static xh_core_map_info_tree_t xh_core_map_info = RB_INITIALIZER(&xh_core_map_info); +static pthread_mutex_t xh_core_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t xh_core_cond = PTHREAD_COND_INITIALIZER; +static volatile int xh_core_inited = 0; +static volatile int xh_core_init_ok = 0; +static volatile int xh_core_async_inited = 0; +static volatile int xh_core_async_init_ok = 0; +static pthread_mutex_t xh_core_refresh_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_t xh_core_refresh_thread_tid; +static volatile int xh_core_refresh_thread_running = 0; +static volatile int xh_core_refresh_thread_do = 0; + + +int xh_core_register(const char *pathname_regex_str, const char *symbol, + void *new_func, void **old_func) +{ + xh_core_hook_info_t *hi; + regex_t regex; + + if(NULL == pathname_regex_str || NULL == symbol || NULL == new_func) return XH_ERRNO_INVAL; + + if(xh_core_inited) + { + XH_LOG_ERROR("do not register hook after refresh(): %s, %s", pathname_regex_str, symbol); + return XH_ERRNO_INVAL; + } + + if(0 != regcomp(®ex, pathname_regex_str, REG_NOSUB)) return XH_ERRNO_INVAL; + + if(NULL == (hi = malloc(sizeof(xh_core_hook_info_t)))) return XH_ERRNO_NOMEM; + if(NULL == (hi->symbol = strdup(symbol))) + { + free(hi); + return XH_ERRNO_NOMEM; + } +#if XH_CORE_DEBUG + if(NULL == (hi->pathname_regex_str = strdup(pathname_regex_str))) + { + free(hi->symbol); + free(hi); + return XH_ERRNO_NOMEM; + } +#endif + hi->pathname_regex = regex; + hi->new_func = new_func; + hi->old_func = old_func; + + pthread_mutex_lock(&xh_core_mutex); + TAILQ_INSERT_TAIL(&xh_core_hook_info, hi, link); + pthread_mutex_unlock(&xh_core_mutex); + + return 0; +} + +int xh_core_ignore(const char *pathname_regex_str, const char *symbol) +{ + xh_core_ignore_info_t *ii; + regex_t regex; + + if(NULL == pathname_regex_str) return XH_ERRNO_INVAL; + + if(xh_core_inited) + { + XH_LOG_ERROR("do not ignore hook after refresh(): %s, %s", pathname_regex_str, symbol ? symbol : "ALL"); + return XH_ERRNO_INVAL; + } + + if(0 != regcomp(®ex, pathname_regex_str, REG_NOSUB)) return XH_ERRNO_INVAL; + + if(NULL == (ii = malloc(sizeof(xh_core_ignore_info_t)))) return XH_ERRNO_NOMEM; + if(NULL != symbol) + { + if(NULL == (ii->symbol = strdup(symbol))) + { + free(ii); + return XH_ERRNO_NOMEM; + } + } + else + { + ii->symbol = NULL; //ignore all symbols + } +#if XH_CORE_DEBUG + if(NULL == (ii->pathname_regex_str = strdup(pathname_regex_str))) + { + free(ii->symbol); + free(ii); + return XH_ERRNO_NOMEM; + } +#endif + ii->pathname_regex = regex; + + pthread_mutex_lock(&xh_core_mutex); + TAILQ_INSERT_TAIL(&xh_core_ignore_info, ii, link); + pthread_mutex_unlock(&xh_core_mutex); + + return 0; +} + +static int xh_core_check_elf_header(uintptr_t base_addr, const char *pathname) +{ + if(!xh_core_sigsegv_enable) + { + return xh_elf_check_elfheader(base_addr); + } + else + { + int ret = XH_ERRNO_UNKNOWN; + + xh_core_sigsegv_flag = 1; + if(0 == sigsetjmp(xh_core_sigsegv_env, 1)) + { + ret = xh_elf_check_elfheader(base_addr); + } + else + { + ret = XH_ERRNO_SEGVERR; + XH_LOG_WARN("catch SIGSEGV when check_elfheader: %s", pathname); + } + xh_core_sigsegv_flag = 0; + return ret; + } +} + +static void xh_core_hook_impl(xh_core_map_info_t *mi) +{ + //init + if(0 != xh_elf_init(&(mi->elf), mi->base_addr, mi->pathname)) return; + + //hook + xh_core_hook_info_t *hi; + xh_core_ignore_info_t *ii; + int ignore; + TAILQ_FOREACH(hi, &xh_core_hook_info, link) //find hook info + { + if(0 == regexec(&(hi->pathname_regex), mi->pathname, 0, NULL, 0)) + { + ignore = 0; + TAILQ_FOREACH(ii, &xh_core_ignore_info, link) //find ignore info + { + if(0 == regexec(&(ii->pathname_regex), mi->pathname, 0, NULL, 0)) + { + if(NULL == ii->symbol) //ignore all symbols + return; + + if(0 == strcmp(ii->symbol, hi->symbol)) //ignore the current symbol + { + ignore = 1; + break; + } + } + } + + if(0 == ignore) + xh_elf_hook(&(mi->elf), hi->symbol, hi->new_func, hi->old_func); + } + } +} + +static void xh_core_hook(xh_core_map_info_t *mi) +{ + if(!xh_core_sigsegv_enable) + { + xh_core_hook_impl(mi); + } + else + { + xh_core_sigsegv_flag = 1; + if(0 == sigsetjmp(xh_core_sigsegv_env, 1)) + { + xh_core_hook_impl(mi); + } + else + { + XH_LOG_WARN("catch SIGSEGV when init or hook: %s", mi->pathname); + } + xh_core_sigsegv_flag = 0; + } +} + +static void xh_core_refresh_impl() +{ + char line[512]; + FILE *fp; + uintptr_t base_addr; + char perm[5]; + unsigned long offset; + int pathname_pos; + char *pathname; + size_t pathname_len; + xh_core_map_info_t *mi, *mi_tmp; + xh_core_map_info_t mi_key; + xh_core_hook_info_t *hi; + xh_core_ignore_info_t *ii; + int match; + xh_core_map_info_tree_t map_info_refreshed = RB_INITIALIZER(&map_info_refreshed); + + if(NULL == (fp = fopen("/proc/self/maps", "r"))) + { + XH_LOG_ERROR("fopen /proc/self/maps failed"); + return; + } + + while(fgets(line, sizeof(line), fp)) + { + if(sscanf(line, "%"PRIxPTR"-%*lx %4s %lx %*x:%*x %*d%n", &base_addr, perm, &offset, &pathname_pos) != 3) continue; + + //check permission + if(perm[0] != 'r') continue; + if(perm[3] != 'p') continue; //do not touch the shared memory + + //check offset + // + //We are trying to find ELF header in memory. + //It can only be found at the beginning of a mapped memory regions + //whose offset is 0. + if(0 != offset) continue; + + //get pathname + while(isspace(line[pathname_pos]) && pathname_pos < (int)(sizeof(line) - 1)) + pathname_pos += 1; + if(pathname_pos >= (int)(sizeof(line) - 1)) continue; + pathname = line + pathname_pos; + pathname_len = strlen(pathname); + if(0 == pathname_len) continue; + if(pathname[pathname_len - 1] == '\n') + { + pathname[pathname_len - 1] = '\0'; + pathname_len -= 1; + } + if(0 == pathname_len) continue; + if('[' == pathname[0]) continue; + + //check pathname + //if we need to hook this elf? + match = 0; + TAILQ_FOREACH(hi, &xh_core_hook_info, link) //find hook info + { + if(0 == regexec(&(hi->pathname_regex), pathname, 0, NULL, 0)) + { + TAILQ_FOREACH(ii, &xh_core_ignore_info, link) //find ignore info + { + if(0 == regexec(&(ii->pathname_regex), pathname, 0, NULL, 0)) + { + if(NULL == ii->symbol) + goto check_finished; + + if(0 == strcmp(ii->symbol, hi->symbol)) + goto check_continue; + } + } + + match = 1; + check_continue: + break; + } + } + check_finished: + if(0 == match) continue; + + //check elf header format + //We are trying to do ELF header checking as late as possible. + if(0 != xh_core_check_elf_header(base_addr, pathname)) continue; + + //check existed map item + mi_key.pathname = pathname; + if(NULL != (mi = RB_FIND(xh_core_map_info_tree, &xh_core_map_info, &mi_key))) + { + //exist + RB_REMOVE(xh_core_map_info_tree, &xh_core_map_info, mi); + + //repeated? + //We only keep the first one, that is the real base address + if(NULL != RB_INSERT(xh_core_map_info_tree, &map_info_refreshed, mi)) + { +#if XH_CORE_DEBUG + XH_LOG_DEBUG("repeated map info when update: %s", line); +#endif + free(mi->pathname); + free(mi); + continue; + } + + //re-hook if base_addr changed + if(mi->base_addr != base_addr) + { + mi->base_addr = base_addr; + xh_core_hook(mi); + } + } + else + { + //not exist, create a new map info + if(NULL == (mi = (xh_core_map_info_t *)malloc(sizeof(xh_core_map_info_t)))) continue; + if(NULL == (mi->pathname = strdup(pathname))) + { + free(mi); + continue; + } + mi->base_addr = base_addr; + + //repeated? + //We only keep the first one, that is the real base address + if(NULL != RB_INSERT(xh_core_map_info_tree, &map_info_refreshed, mi)) + { +#if XH_CORE_DEBUG + XH_LOG_DEBUG("repeated map info when create: %s", line); +#endif + free(mi->pathname); + free(mi); + continue; + } + + //hook + xh_core_hook(mi); //hook + } + } + fclose(fp); + + //free all missing map item, maybe dlclosed? + RB_FOREACH_SAFE(mi, xh_core_map_info_tree, &xh_core_map_info, mi_tmp) + { +#if XH_CORE_DEBUG + XH_LOG_DEBUG("remove missing map info: %s", mi->pathname); +#endif + RB_REMOVE(xh_core_map_info_tree, &xh_core_map_info, mi); + if(mi->pathname) free(mi->pathname); + free(mi); + } + + //save the new refreshed map info tree + xh_core_map_info = map_info_refreshed; + + XH_LOG_INFO("map refreshed"); + +#if XH_CORE_DEBUG + RB_FOREACH(mi, xh_core_map_info_tree, &xh_core_map_info) + XH_LOG_DEBUG(" %"PRIxPTR" %s\n", mi->base_addr, mi->pathname); +#endif +} + +static void *xh_core_refresh_thread_func(void *arg) +{ + (void)arg; + + pthread_setname_np(pthread_self(), "xh_refresh_loop"); + + while(xh_core_refresh_thread_running) + { + //waiting for a refresh task or exit + pthread_mutex_lock(&xh_core_mutex); + while(!xh_core_refresh_thread_do && xh_core_refresh_thread_running) + { + pthread_cond_wait(&xh_core_cond, &xh_core_mutex); + } + if(!xh_core_refresh_thread_running) + { + pthread_mutex_unlock(&xh_core_mutex); + break; + } + xh_core_refresh_thread_do = 0; + pthread_mutex_unlock(&xh_core_mutex); + + //refresh + pthread_mutex_lock(&xh_core_refresh_mutex); + xh_core_refresh_impl(); + pthread_mutex_unlock(&xh_core_refresh_mutex); + } + + return NULL; +} + +static void xh_core_init_once() +{ + if(xh_core_inited) return; + + pthread_mutex_lock(&xh_core_mutex); + + if(xh_core_inited) goto end; + + xh_core_inited = 1; + + //dump debug info + XH_LOG_INFO("%s\n", xh_version_str_full()); +#if XH_CORE_DEBUG + xh_core_hook_info_t *hi; + TAILQ_FOREACH(hi, &xh_core_hook_info, link) + XH_LOG_INFO(" hook: %s @ %s, (%p, %p)\n", hi->symbol, hi->pathname_regex_str, + hi->new_func, hi->old_func); + xh_core_ignore_info_t *ii; + TAILQ_FOREACH(ii, &xh_core_ignore_info, link) + XH_LOG_INFO(" ignore: %s @ %s\n", ii->symbol ? ii->symbol : "ALL ", + ii->pathname_regex_str); +#endif + + //register signal handler + if(0 != xh_core_add_sigsegv_handler()) goto end; + + //OK + xh_core_init_ok = 1; + + end: + pthread_mutex_unlock(&xh_core_mutex); +} + +static void xh_core_init_async_once() +{ + if(xh_core_async_inited) return; + + pthread_mutex_lock(&xh_core_mutex); + + if(xh_core_async_inited) goto end; + + xh_core_async_inited = 1; + + //create async refresh thread + xh_core_refresh_thread_running = 1; + if(0 != pthread_create(&xh_core_refresh_thread_tid, NULL, &xh_core_refresh_thread_func, NULL)) + { + xh_core_refresh_thread_running = 0; + goto end; + } + + //OK + xh_core_async_init_ok = 1; + + end: + pthread_mutex_unlock(&xh_core_mutex); +} + +int xh_core_refresh(int async) +{ + //init + xh_core_init_once(); + if(!xh_core_init_ok) return XH_ERRNO_UNKNOWN; + + if(async) + { + //init for async + xh_core_init_async_once(); + if(!xh_core_async_init_ok) return XH_ERRNO_UNKNOWN; + + //refresh async + pthread_mutex_lock(&xh_core_mutex); + xh_core_refresh_thread_do = 1; + pthread_cond_signal(&xh_core_cond); + pthread_mutex_unlock(&xh_core_mutex); + } + else + { + //refresh sync + pthread_mutex_lock(&xh_core_refresh_mutex); + xh_core_refresh_impl(); + pthread_mutex_unlock(&xh_core_refresh_mutex); + } + + return 0; +} + +void xh_core_clear() +{ + //stop the async refresh thread + if(xh_core_async_init_ok) + { + pthread_mutex_lock(&xh_core_mutex); + xh_core_refresh_thread_running = 0; + pthread_cond_signal(&xh_core_cond); + pthread_mutex_unlock(&xh_core_mutex); + + pthread_join(xh_core_refresh_thread_tid, NULL); + xh_core_async_init_ok = 0; + } + xh_core_async_inited = 0; + + //unregister the sig handler + if(xh_core_init_ok) + { + xh_core_del_sigsegv_handler(); + xh_core_init_ok = 0; + } + xh_core_inited = 0; + + pthread_mutex_lock(&xh_core_mutex); + pthread_mutex_lock(&xh_core_refresh_mutex); + + //free all map info + xh_core_map_info_t *mi, *mi_tmp; + RB_FOREACH_SAFE(mi, xh_core_map_info_tree, &xh_core_map_info, mi_tmp) + { + RB_REMOVE(xh_core_map_info_tree, &xh_core_map_info, mi); + if(mi->pathname) free(mi->pathname); + free(mi); + } + + //free all hook info + xh_core_hook_info_t *hi, *hi_tmp; + TAILQ_FOREACH_SAFE(hi, &xh_core_hook_info, link, hi_tmp) + { + TAILQ_REMOVE(&xh_core_hook_info, hi, link); +#if XH_CORE_DEBUG + free(hi->pathname_regex_str); +#endif + regfree(&(hi->pathname_regex)); + free(hi->symbol); + free(hi); + } + + //free all ignore info + xh_core_ignore_info_t *ii, *ii_tmp; + TAILQ_FOREACH_SAFE(ii, &xh_core_ignore_info, link, ii_tmp) + { + TAILQ_REMOVE(&xh_core_ignore_info, ii, link); +#if XH_CORE_DEBUG + free(ii->pathname_regex_str); +#endif + regfree(&(ii->pathname_regex)); + free(ii->symbol); + free(ii); + } + + pthread_mutex_unlock(&xh_core_refresh_mutex); + pthread_mutex_unlock(&xh_core_mutex); +} + +void xh_core_enable_debug(int flag) +{ + xh_log_priority = (flag ? ANDROID_LOG_DEBUG : ANDROID_LOG_WARN); +} + +void xh_core_enable_sigsegv_protection(int flag) +{ + xh_core_sigsegv_enable = (flag ? 1 : 0); +} diff --git a/Core/jni/external/xhook/xh_core.h b/Core/jni/external/xhook/xh_core.h new file mode 100644 index 00000000..35087945 --- /dev/null +++ b/Core/jni/external/xhook/xh_core.h @@ -0,0 +1,48 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#ifndef XH_CORE_H +#define XH_CORE_H 1 + +#ifdef __cplusplus +extern "C" { +#endif + +int xh_core_register(const char *pathname_regex_str, const char *symbol, + void *new_func, void **old_func); + +int xh_core_ignore(const char *pathname_regex_str, const char *symbol); + +int xh_core_refresh(int async); + +void xh_core_clear(); + +void xh_core_enable_debug(int flag); + +void xh_core_enable_sigsegv_protection(int flag); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Core/jni/external/xhook/xh_elf.c b/Core/jni/external/xhook/xh_elf.c new file mode 100644 index 00000000..286ed878 --- /dev/null +++ b/Core/jni/external/xhook/xh_elf.c @@ -0,0 +1,1042 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "xh_errno.h" +#include "xh_log.h" +#include "xh_util.h" +#include "xh_elf.h" + +#define XH_ELF_DEBUG 0 + +#ifndef EI_ABIVERSION +#define EI_ABIVERSION 8 +#endif + +#if defined(__arm__) +#define XH_ELF_R_GENERIC_JUMP_SLOT R_ARM_JUMP_SLOT //.rel.plt +#define XH_ELF_R_GENERIC_GLOB_DAT R_ARM_GLOB_DAT //.rel.dyn +#define XH_ELF_R_GENERIC_ABS R_ARM_ABS32 //.rel.dyn +#elif defined(__aarch64__) +#define XH_ELF_R_GENERIC_JUMP_SLOT R_AARCH64_JUMP_SLOT +#define XH_ELF_R_GENERIC_GLOB_DAT R_AARCH64_GLOB_DAT +#define XH_ELF_R_GENERIC_ABS R_AARCH64_ABS64 +#elif defined(__i386__) +#define XH_ELF_R_GENERIC_JUMP_SLOT R_386_JMP_SLOT +#define XH_ELF_R_GENERIC_GLOB_DAT R_386_GLOB_DAT +#define XH_ELF_R_GENERIC_ABS R_386_32 +#elif defined(__x86_64__) +#define XH_ELF_R_GENERIC_JUMP_SLOT R_X86_64_JUMP_SLOT +#define XH_ELF_R_GENERIC_GLOB_DAT R_X86_64_GLOB_DAT +#define XH_ELF_R_GENERIC_ABS R_X86_64_64 +#endif + +#if defined(__LP64__) +#define XH_ELF_R_SYM(info) ELF64_R_SYM(info) +#define XH_ELF_R_TYPE(info) ELF64_R_TYPE(info) +#else +#define XH_ELF_R_SYM(info) ELF32_R_SYM(info) +#define XH_ELF_R_TYPE(info) ELF32_R_TYPE(info) +#endif + +//iterator for plain PLT +typedef struct +{ + uint8_t *cur; + uint8_t *end; + int is_use_rela; +} xh_elf_plain_reloc_iterator_t; + +static void xh_elf_plain_reloc_iterator_init(xh_elf_plain_reloc_iterator_t *self, + ElfW(Addr) rel, ElfW(Word) rel_sz, int is_use_rela) +{ + self->cur = (uint8_t *)rel; + self->end = self->cur + rel_sz; + self->is_use_rela = is_use_rela; +} + +static void *xh_elf_plain_reloc_iterator_next(xh_elf_plain_reloc_iterator_t *self) +{ + if(self->cur >= self->end) return NULL; + + self->cur += (self->is_use_rela ? sizeof(ElfW(Rela)) : sizeof(ElfW(Rel))); + return (void *)(self->cur); +} + +//sleb128 decoder +typedef struct +{ + uint8_t *cur; + uint8_t *end; +} xh_elf_sleb128_decoder_t; + +static void xh_elf_sleb128_decoder_init(xh_elf_sleb128_decoder_t *self, + ElfW(Addr) rel, ElfW(Word) rel_sz) +{ + self->cur = (uint8_t *)rel; + self->end = self->cur + rel_sz; +} + +static int xh_elf_sleb128_decoder_next(xh_elf_sleb128_decoder_t *self, size_t *ret) +{ + size_t value = 0; + static const size_t size = 8 * sizeof(value); + size_t shift = 0; + uint8_t byte; + + do + { + if(self->cur >= self->end) + return XH_ERRNO_FORMAT; + + byte = *(self->cur)++; + value |= ((size_t)(byte & 127) << shift); + shift += 7; + } while(byte & 128); + + if(shift < size && (byte & 64)) + { + value |= -((size_t)(1) << shift); + } + + *ret = value; + return 0; +} + +//iterator for sleb128 decoded packed PLT +typedef struct +{ + xh_elf_sleb128_decoder_t decoder; + size_t relocation_count; + size_t group_size; + size_t group_flags; + size_t group_r_offset_delta; + size_t relocation_index; + size_t relocation_group_index; + ElfW(Rela) rela; + ElfW(Rel) rel; + ElfW(Addr) r_offset; + size_t r_info; + ssize_t r_addend; + int is_use_rela; +} xh_elf_packed_reloc_iterator_t; + +const size_t RELOCATION_GROUPED_BY_INFO_FLAG = 1; +const size_t RELOCATION_GROUPED_BY_OFFSET_DELTA_FLAG = 2; +const size_t RELOCATION_GROUPED_BY_ADDEND_FLAG = 4; +const size_t RELOCATION_GROUP_HAS_ADDEND_FLAG = 8; + +static int xh_elf_packed_reloc_iterator_init(xh_elf_packed_reloc_iterator_t *self, + ElfW(Addr) rel, ElfW(Word) rel_sz, int is_use_rela) +{ + int r; + + memset(self, 0, sizeof(xh_elf_packed_reloc_iterator_t)); + xh_elf_sleb128_decoder_init(&(self->decoder), rel, rel_sz); + self->is_use_rela = is_use_rela; + + if(0 != (r = xh_elf_sleb128_decoder_next(&(self->decoder), &(self->relocation_count)))) return r; + if(0 != (r = xh_elf_sleb128_decoder_next(&(self->decoder), (size_t *)&(self->r_offset)))) return r; + return 0; +} + +static int xh_elf_packed_reloc_iterator_read_group_fields(xh_elf_packed_reloc_iterator_t *self) +{ + int r; + size_t val; + + if(0 != (r = xh_elf_sleb128_decoder_next(&(self->decoder), &(self->group_size)))) return r; + if(0 != (r = xh_elf_sleb128_decoder_next(&(self->decoder), &(self->group_flags)))) return r; + + if(self->group_flags & RELOCATION_GROUPED_BY_OFFSET_DELTA_FLAG) + if(0 != (r = xh_elf_sleb128_decoder_next(&(self->decoder), &(self->group_r_offset_delta)))) return r; + + if(self->group_flags & RELOCATION_GROUPED_BY_INFO_FLAG) + if(0 != (r = xh_elf_sleb128_decoder_next(&(self->decoder), (size_t *)&(self->r_info)))) return r; + + if((self->group_flags & RELOCATION_GROUP_HAS_ADDEND_FLAG) && + (self->group_flags & RELOCATION_GROUPED_BY_ADDEND_FLAG)) + { + if(0 == self->is_use_rela) + { + XH_LOG_ERROR("unexpected r_addend in android.rel section"); + return XH_ERRNO_FORMAT; + } + if(0 != (r = xh_elf_sleb128_decoder_next(&(self->decoder), &val))) return r; + self->r_addend += (ssize_t)val; + } + else if(0 == (self->group_flags & RELOCATION_GROUP_HAS_ADDEND_FLAG)) + { + self->r_addend = 0; + } + + self->relocation_group_index = 0; + return 0; +} + +static void *xh_elf_packed_reloc_iterator_next(xh_elf_packed_reloc_iterator_t *self) +{ + size_t val; + + if(self->relocation_index >= self->relocation_count) return NULL; + + if(self->relocation_group_index == self->group_size) + { + if(0 != xh_elf_packed_reloc_iterator_read_group_fields(self)) return NULL; + } + + if(self->group_flags & RELOCATION_GROUPED_BY_OFFSET_DELTA_FLAG) + { + self->r_offset += self->group_r_offset_delta; + } + else + { + if(0 != xh_elf_sleb128_decoder_next(&(self->decoder), &val)) return NULL; + self->r_offset += val; + } + + if(0 == (self->group_flags & RELOCATION_GROUPED_BY_INFO_FLAG)) + if(0 != xh_elf_sleb128_decoder_next(&(self->decoder), &(self->r_info))) return NULL; + + if(self->is_use_rela && + (self->group_flags & RELOCATION_GROUP_HAS_ADDEND_FLAG) && + (0 == (self->group_flags & RELOCATION_GROUPED_BY_ADDEND_FLAG))) + { + if(0 != xh_elf_sleb128_decoder_next(&(self->decoder), &val)) return NULL; + self->r_addend += (ssize_t)val; + } + + self->relocation_index++; + self->relocation_group_index++; + + if(self->is_use_rela) + { + self->rela.r_offset = self->r_offset; + self->rela.r_info = self->r_info; + self->rela.r_addend = self->r_addend; + return (void *)(&(self->rela)); + } + else + { + self->rel.r_offset = self->r_offset; + self->rel.r_info = self->r_info; + return (void *)(&(self->rel)); + } +} + +//ELF header checker +int xh_elf_check_elfheader(uintptr_t base_addr) +{ + ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)base_addr; + + //check magic + if(0 != memcmp(ehdr->e_ident, ELFMAG, SELFMAG)) return XH_ERRNO_FORMAT; + + //check class (64/32) +#if defined(__LP64__) + if(ELFCLASS64 != ehdr->e_ident[EI_CLASS]) return XH_ERRNO_FORMAT; +#else + if(ELFCLASS32 != ehdr->e_ident[EI_CLASS]) return XH_ERRNO_FORMAT; +#endif + + //check endian (little/big) + if(ELFDATA2LSB != ehdr->e_ident[EI_DATA]) return XH_ERRNO_FORMAT; + + //check version + if(EV_CURRENT != ehdr->e_ident[EI_VERSION]) return XH_ERRNO_FORMAT; + + //check type + if(ET_EXEC != ehdr->e_type && ET_DYN != ehdr->e_type) return XH_ERRNO_FORMAT; + + //check machine +#if defined(__arm__) + if(EM_ARM != ehdr->e_machine) return XH_ERRNO_FORMAT; +#elif defined(__aarch64__) + if(EM_AARCH64 != ehdr->e_machine) return XH_ERRNO_FORMAT; +#elif defined(__i386__) + if(EM_386 != ehdr->e_machine) return XH_ERRNO_FORMAT; +#elif defined(__x86_64__) + if(EM_X86_64 != ehdr->e_machine) return XH_ERRNO_FORMAT; +#else + return XH_ERRNO_FORMAT; +#endif + + //check version + if(EV_CURRENT != ehdr->e_version) return XH_ERRNO_FORMAT; + + return 0; +} + +//ELF hash func +static uint32_t xh_elf_hash(const uint8_t *name) +{ + uint32_t h = 0, g; + + while (*name) { + h = (h << 4) + *name++; + g = h & 0xf0000000; + h ^= g; + h ^= g >> 24; + } + + return h; +} + +//GNU hash func +static uint32_t xh_elf_gnu_hash(const uint8_t *name) +{ + uint32_t h = 5381; + + while(*name != 0) + { + h += (h << 5) + *name++; + } + return h; +} + +static ElfW(Phdr) *xh_elf_get_first_segment_by_type(xh_elf_t *self, ElfW(Word) type) +{ + ElfW(Phdr) *phdr; + + for(phdr = self->phdr; phdr < self->phdr + self->ehdr->e_phnum; phdr++) + { + if(phdr->p_type == type) + { + return phdr; + } + } + return NULL; +} + +static ElfW(Phdr) *xh_elf_get_first_segment_by_type_offset(xh_elf_t *self, ElfW(Word) type, ElfW(Off) offset) +{ + ElfW(Phdr) *phdr; + + for(phdr = self->phdr; phdr < self->phdr + self->ehdr->e_phnum; phdr++) + { + if(phdr->p_type == type && phdr->p_offset == offset) + { + return phdr; + } + } + return NULL; +} + +static int xh_elf_hash_lookup(xh_elf_t *self, const char *symbol, uint32_t *symidx) +{ + uint32_t hash = xh_elf_hash((uint8_t *)symbol); + const char *symbol_cur; + uint32_t i; + + for(i = self->bucket[hash % self->bucket_cnt]; 0 != i; i = self->chain[i]) + { + symbol_cur = self->strtab + self->symtab[i].st_name; + + if(0 == strcmp(symbol, symbol_cur)) + { + *symidx = i; + XH_LOG_INFO("found %s at symidx: %u (ELF_HASH)\n", symbol, *symidx); + return 0; + } + } + + return XH_ERRNO_NOTFND; +} + +static int xh_elf_gnu_hash_lookup_def(xh_elf_t *self, const char *symbol, uint32_t *symidx) +{ + uint32_t hash = xh_elf_gnu_hash((uint8_t *)symbol); + + static uint32_t elfclass_bits = sizeof(ElfW(Addr)) * 8; + size_t word = self->bloom[(hash / elfclass_bits) % self->bloom_sz]; + size_t mask = 0 + | (size_t)1 << (hash % elfclass_bits) + | (size_t)1 << ((hash >> self->bloom_shift) % elfclass_bits); + + //if at least one bit is not set, this symbol is surely missing + if((word & mask) != mask) return XH_ERRNO_NOTFND; + + //ignore STN_UNDEF + uint32_t i = self->bucket[hash % self->bucket_cnt]; + if(i < self->symoffset) return XH_ERRNO_NOTFND; + + //loop through the chain + while(1) + { + const char *symname = self->strtab + self->symtab[i].st_name; + const uint32_t symhash = self->chain[i - self->symoffset]; + + if((hash | (uint32_t)1) == (symhash | (uint32_t)1) && 0 == strcmp(symbol, symname)) + { + *symidx = i; + XH_LOG_INFO("found %s at symidx: %u (GNU_HASH DEF)\n", symbol, *symidx); + return 0; + } + + //chain ends with an element with the lowest bit set to 1 + if(symhash & (uint32_t)1) break; + + i++; + } + + return XH_ERRNO_NOTFND; +} + +static int xh_elf_gnu_hash_lookup_undef(xh_elf_t *self, const char *symbol, uint32_t *symidx) +{ + uint32_t i; + + for(i = 0; i < self->symoffset; i++) + { + const char *symname = self->strtab + self->symtab[i].st_name; + if(0 == strcmp(symname, symbol)) + { + *symidx = i; + XH_LOG_INFO("found %s at symidx: %u (GNU_HASH UNDEF)\n", symbol, *symidx); + return 0; + } + } + return XH_ERRNO_NOTFND; +} + +static int xh_elf_gnu_hash_lookup(xh_elf_t *self, const char *symbol, uint32_t *symidx) +{ + if(0 == xh_elf_gnu_hash_lookup_def(self, symbol, symidx)) return 0; + if(0 == xh_elf_gnu_hash_lookup_undef(self, symbol, symidx)) return 0; + return XH_ERRNO_NOTFND; +} + +static int xh_elf_find_symidx_by_name(xh_elf_t *self, const char *symbol, uint32_t *symidx) +{ + if(self->is_use_gnu_hash) + return xh_elf_gnu_hash_lookup(self, symbol, symidx); + else + return xh_elf_hash_lookup(self, symbol, symidx); +} + +static int xh_elf_replace_function(xh_elf_t *self, const char *symbol, ElfW(Addr) addr, void *new_func, void **old_func) +{ + void *old_addr; + unsigned int old_prot = 0; + unsigned int need_prot = PROT_READ | PROT_WRITE; + int r; + + //already replaced? + //here we assume that we always have read permission, is this a problem? + if(*(void **)addr == new_func) return 0; + + //get old prot + if(0 != (r = xh_util_get_addr_protect(addr, self->pathname, &old_prot))) + { + XH_LOG_ERROR("get addr prot failed. ret: %d", r); + return r; + } + + if(old_prot != need_prot) + { + //set new prot + if(0 != (r = xh_util_set_addr_protect(addr, need_prot))) + { + XH_LOG_ERROR("set addr prot failed. ret: %d", r); + return r; + } + } + + //save old func + old_addr = *(void **)addr; + if(NULL != old_func) *old_func = old_addr; + + //replace func + *(void **)addr = new_func; //segmentation fault sometimes + + if(old_prot != need_prot) + { + //restore the old prot + if(0 != (r = xh_util_set_addr_protect(addr, old_prot))) + { + XH_LOG_WARN("restore addr prot failed. ret: %d", r); + } + } + + //clear cache + xh_util_flush_instruction_cache(addr); + + XH_LOG_INFO("XH_HK_OK %p: %p -> %p %s %s\n", (void *)addr, old_addr, new_func, symbol, self->pathname); + return 0; +} + +static int xh_elf_check(xh_elf_t *self) +{ + if(0 == self->base_addr) + { + XH_LOG_ERROR("base_addr == 0\n"); + return 1; + } + if(0 == self->bias_addr) + { + XH_LOG_ERROR("bias_addr == 0\n"); + return 1; + } + if(NULL == self->ehdr) + { + XH_LOG_ERROR("ehdr == NULL\n"); + return 1; + } + if(NULL == self->phdr) + { + XH_LOG_ERROR("phdr == NULL\n"); + return 1; + } + if(NULL == self->strtab) + { + XH_LOG_ERROR("strtab == NULL\n"); + return 1; + } + if(NULL == self->symtab) + { + XH_LOG_ERROR("symtab == NULL\n"); + return 1; + } + if(NULL == self->bucket) + { + XH_LOG_ERROR("bucket == NULL\n"); + return 1; + } + if(NULL == self->chain) + { + XH_LOG_ERROR("chain == NULL\n"); + return 1; + } + if(1 == self->is_use_gnu_hash && NULL == self->bloom) + { + XH_LOG_ERROR("bloom == NULL\n"); + return 1; + } + + return 0; +} + +#if XH_ELF_DEBUG + +static void xh_elf_dump_elfheader(xh_elf_t *self) +{ + static char alpha_tab[17] = "0123456789ABCDEF"; + int i; + uint8_t ch; + char buff[EI_NIDENT * 3 + 1]; + + for(i = 0; i < EI_NIDENT; i++) + { + ch = self->ehdr->e_ident[i]; + buff[i * 3 + 0] = alpha_tab[(int)((ch >> 4) & 0x0F)]; + buff[i * 3 + 1] = alpha_tab[(int)(ch & 0x0F)]; + buff[i * 3 + 2] = ' '; + } + buff[EI_NIDENT * 3] = '\0'; + + XH_LOG_DEBUG("Elf Header:\n"); + XH_LOG_DEBUG(" Magic: %s\n", buff); + XH_LOG_DEBUG(" Class: %#x\n", self->ehdr->e_ident[EI_CLASS]); + XH_LOG_DEBUG(" Data: %#x\n", self->ehdr->e_ident[EI_DATA]); + XH_LOG_DEBUG(" Version: %#x\n", self->ehdr->e_ident[EI_VERSION]); + XH_LOG_DEBUG(" OS/ABI: %#x\n", self->ehdr->e_ident[EI_OSABI]); + XH_LOG_DEBUG(" ABI Version: %#x\n", self->ehdr->e_ident[EI_ABIVERSION]); + XH_LOG_DEBUG(" Type: %#x\n", self->ehdr->e_type); + XH_LOG_DEBUG(" Machine: %#x\n", self->ehdr->e_machine); + XH_LOG_DEBUG(" Version: %#x\n", self->ehdr->e_version); + XH_LOG_DEBUG(" Entry point address: %"XH_UTIL_FMT_X"\n", self->ehdr->e_entry); + XH_LOG_DEBUG(" Start of program headers: %"XH_UTIL_FMT_X" (bytes into file)\n", self->ehdr->e_phoff); + XH_LOG_DEBUG(" Start of section headers: %"XH_UTIL_FMT_X" (bytes into file)\n", self->ehdr->e_shoff); + XH_LOG_DEBUG(" Flags: %#x\n", self->ehdr->e_flags); + XH_LOG_DEBUG(" Size of this header: %u (bytes)\n", self->ehdr->e_ehsize); + XH_LOG_DEBUG(" Size of program headers: %u (bytes)\n", self->ehdr->e_phentsize); + XH_LOG_DEBUG(" Number of program headers: %u\n", self->ehdr->e_phnum); + XH_LOG_DEBUG(" Size of section headers: %u (bytes)\n", self->ehdr->e_shentsize); + XH_LOG_DEBUG(" Number of section headers: %u\n", self->ehdr->e_shnum); + XH_LOG_DEBUG(" Section header string table index: %u\n", self->ehdr->e_shstrndx); +} + +static void xh_elf_dump_programheader(xh_elf_t *self) +{ + ElfW(Phdr) *phdr = self->phdr; + size_t i; + + XH_LOG_DEBUG("Program Headers:\n"); + XH_LOG_DEBUG(" %-8s " \ + "%-"XH_UTIL_FMT_FIXED_S" " \ + "%-"XH_UTIL_FMT_FIXED_S" " \ + "%-"XH_UTIL_FMT_FIXED_S" " \ + "%-"XH_UTIL_FMT_FIXED_S" " \ + "%-"XH_UTIL_FMT_FIXED_S" " \ + "%-8s " \ + "%-s\n", + "Type", + "Offset", + "VirtAddr", + "PhysAddr", + "FileSiz", + "MemSiz", + "Flg", + "Align"); + for(i = 0; i < self->ehdr->e_phnum; i++, phdr++) + { + XH_LOG_DEBUG(" %-8x " \ + "%."XH_UTIL_FMT_FIXED_X" " \ + "%."XH_UTIL_FMT_FIXED_X" " \ + "%."XH_UTIL_FMT_FIXED_X" " \ + "%."XH_UTIL_FMT_FIXED_X" " \ + "%."XH_UTIL_FMT_FIXED_X" " \ + "%-8x " \ + "%"XH_UTIL_FMT_X"\n", + phdr->p_type, + phdr->p_offset, + phdr->p_vaddr, + phdr->p_paddr, + phdr->p_filesz, + phdr->p_memsz, + phdr->p_flags, + phdr->p_align); + } +} + +static void xh_elf_dump_dynamic(xh_elf_t *self) +{ + ElfW(Dyn) *dyn = self->dyn; + size_t dyn_cnt = (self->dyn_sz / sizeof(ElfW(Dyn))); + size_t i; + + XH_LOG_DEBUG("Dynamic section contains %zu entries:\n", dyn_cnt); + XH_LOG_DEBUG(" %-"XH_UTIL_FMT_FIXED_S" " \ + "%s\n", + "Tag", + "Val"); + for(i = 0; i < dyn_cnt; i++, dyn++) + { + XH_LOG_DEBUG(" %-"XH_UTIL_FMT_FIXED_X" " \ + "%-"XH_UTIL_FMT_X"\n", + dyn->d_tag, + dyn->d_un.d_val); + } +} + +static void xh_elf_dump_rel(xh_elf_t *self, const char *type, ElfW(Addr) rel_addr, ElfW(Word) rel_sz) +{ + ElfW(Rela) *rela; + ElfW(Rel) *rel; + ElfW(Word) cnt; + ElfW(Word) i; + ElfW(Sym) *sym; + + if(self->is_use_rela) + { + rela = (ElfW(Rela) *)(rel_addr); + cnt = rel_sz / sizeof(ElfW(Rela)); + } + else + { + rel = (ElfW(Rel) *)(rel_addr); + cnt = rel_sz / sizeof(ElfW(Rel)); + } + + XH_LOG_DEBUG("Relocation section '.rel%s%s' contains %u entries:\n", + (self->is_use_rela ? "a" : ""), type, cnt); + XH_LOG_DEBUG(" %-"XH_UTIL_FMT_FIXED_S" " \ + "%-"XH_UTIL_FMT_FIXED_S" " \ + "%-8s " \ + "%-8s " \ + "%-8s " \ + "%s\n", + "Offset", + "Info", + "Type", + "Sym.Idx", + "Sym.Val", + "Sym.Name"); + const char *fmt = " %."XH_UTIL_FMT_FIXED_X" " \ + "%."XH_UTIL_FMT_FIXED_X" " \ + "%.8x " \ + "%.8u " \ + "%.8x " \ + "%s\n"; + for(i = 0; i < cnt; i++) + { + if(self->is_use_rela) + { + sym = &(self->symtab[XH_ELF_R_SYM(rela[i].r_info)]); + XH_LOG_DEBUG(fmt, + rela[i].r_offset, + rela[i].r_info, + XH_ELF_R_TYPE(rela[i].r_info), + XH_ELF_R_SYM(rela[i].r_info), + sym->st_value, + self->strtab + sym->st_name); + } + else + { + sym = &(self->symtab[XH_ELF_R_SYM(rel[i].r_info)]); + XH_LOG_DEBUG(fmt, + rel[i].r_offset, + rel[i].r_info, + XH_ELF_R_TYPE(rel[i].r_info), + XH_ELF_R_SYM(rel[i].r_info), + sym->st_value, + self->strtab + sym->st_name); + } + } +} + +static void xh_elf_dump_symtab(xh_elf_t *self) +{ + if(self->is_use_gnu_hash) return; + + ElfW(Word) symtab_cnt = self->chain_cnt; + ElfW(Word) i; + + XH_LOG_DEBUG("Symbol table '.dynsym' contains %u entries:\n", symtab_cnt); + XH_LOG_DEBUG(" %-8s " \ + "%-"XH_UTIL_FMT_FIXED_S" " \ + "%s\n", + "Idx", + "Value", + "Name"); + for(i = 0; i < symtab_cnt; i++) + { + XH_LOG_DEBUG(" %-8u " \ + "%."XH_UTIL_FMT_FIXED_X" " \ + "%s\n", + i, + self->symtab[i].st_value, + self->strtab + self->symtab[i].st_name); + } +} + +static void xh_elf_dump(xh_elf_t *self) +{ + if(xh_log_priority < ANDROID_LOG_DEBUG) return; + + XH_LOG_DEBUG("Elf Pathname: %s\n", self->pathname); + XH_LOG_DEBUG("Elf bias addr: %p\n", (void *)self->bias_addr); + xh_elf_dump_elfheader(self); + xh_elf_dump_programheader(self); + xh_elf_dump_dynamic(self); + xh_elf_dump_rel(self, ".plt", self->relplt, self->relplt_sz); + xh_elf_dump_rel(self, ".dyn", self->reldyn, self->reldyn_sz); + xh_elf_dump_symtab(self); +} + +#endif + +int xh_elf_init(xh_elf_t *self, uintptr_t base_addr, const char *pathname) +{ + if(0 == base_addr || NULL == pathname) return XH_ERRNO_INVAL; + + //always reset + memset(self, 0, sizeof(xh_elf_t)); + + self->pathname = pathname; + self->base_addr = (ElfW(Addr))base_addr; + self->ehdr = (ElfW(Ehdr) *)base_addr; + self->phdr = (ElfW(Phdr) *)(base_addr + self->ehdr->e_phoff); //segmentation fault sometimes + + //find the first load-segment with offset 0 + ElfW(Phdr) *phdr0 = xh_elf_get_first_segment_by_type_offset(self, PT_LOAD, 0); + if(NULL == phdr0) + { + XH_LOG_ERROR("Can NOT found the first load segment. %s", pathname); + return XH_ERRNO_FORMAT; + } + +#if XH_ELF_DEBUG + if(0 != phdr0->p_vaddr) + XH_LOG_DEBUG("first load-segment vaddr NOT 0 (vaddr: %p). %s", + (void *)(phdr0->p_vaddr), pathname); +#endif + + //save load bias addr + if(self->base_addr < phdr0->p_vaddr) return XH_ERRNO_FORMAT; + self->bias_addr = self->base_addr - phdr0->p_vaddr; + + //find dynamic-segment + ElfW(Phdr) *dhdr = xh_elf_get_first_segment_by_type(self, PT_DYNAMIC); + if(NULL == dhdr) + { + XH_LOG_ERROR("Can NOT found dynamic segment. %s", pathname); + return XH_ERRNO_FORMAT; + } + + //parse dynamic-segment + self->dyn = (ElfW(Dyn) *)(self->bias_addr + dhdr->p_vaddr); + self->dyn_sz = dhdr->p_memsz; + ElfW(Dyn) *dyn = self->dyn; + ElfW(Dyn) *dyn_end = self->dyn + (self->dyn_sz / sizeof(ElfW(Dyn))); + uint32_t *raw; + for(; dyn < dyn_end; dyn++) + { + switch(dyn->d_tag) //segmentation fault sometimes + { + case DT_NULL: + //the end of the dynamic-section + dyn = dyn_end; + break; + case DT_STRTAB: + { + self->strtab = (const char *)(self->bias_addr + dyn->d_un.d_ptr); + if((ElfW(Addr))(self->strtab) < self->base_addr) return XH_ERRNO_FORMAT; + break; + } + case DT_SYMTAB: + { + self->symtab = (ElfW(Sym) *)(self->bias_addr + dyn->d_un.d_ptr); + if((ElfW(Addr))(self->symtab) < self->base_addr) return XH_ERRNO_FORMAT; + break; + } + case DT_PLTREL: + //use rel or rela? + self->is_use_rela = (dyn->d_un.d_val == DT_RELA ? 1 : 0); + break; + case DT_JMPREL: + { + self->relplt = (ElfW(Addr))(self->bias_addr + dyn->d_un.d_ptr); + if((ElfW(Addr))(self->relplt) < self->base_addr) return XH_ERRNO_FORMAT; + break; + } + case DT_PLTRELSZ: + self->relplt_sz = dyn->d_un.d_val; + break; + case DT_REL: + case DT_RELA: + { + self->reldyn = (ElfW(Addr))(self->bias_addr + dyn->d_un.d_ptr); + if((ElfW(Addr))(self->reldyn) < self->base_addr) return XH_ERRNO_FORMAT; + break; + } + case DT_RELSZ: + case DT_RELASZ: + self->reldyn_sz = dyn->d_un.d_val; + break; + case DT_ANDROID_REL: + case DT_ANDROID_RELA: + { + self->relandroid = (ElfW(Addr))(self->bias_addr + dyn->d_un.d_ptr); + if((ElfW(Addr))(self->relandroid) < self->base_addr) return XH_ERRNO_FORMAT; + break; + } + case DT_ANDROID_RELSZ: + case DT_ANDROID_RELASZ: + self->relandroid_sz = dyn->d_un.d_val; + break; + case DT_HASH: + { + raw = (uint32_t *)(self->bias_addr + dyn->d_un.d_ptr); + if((ElfW(Addr))raw < self->base_addr) return XH_ERRNO_FORMAT; + self->bucket_cnt = raw[0]; + self->chain_cnt = raw[1]; + self->bucket = &raw[2]; + self->chain = &(self->bucket[self->bucket_cnt]); + break; + } + case DT_GNU_HASH: + { + raw = (uint32_t *)(self->bias_addr + dyn->d_un.d_ptr); + if((ElfW(Addr))raw < self->base_addr) return XH_ERRNO_FORMAT; + self->bucket_cnt = raw[0]; + self->symoffset = raw[1]; + self->bloom_sz = raw[2]; + self->bloom_shift = raw[3]; + self->bloom = (ElfW(Addr) *)(&raw[4]); + self->bucket = (uint32_t *)(&(self->bloom[self->bloom_sz])); + self->chain = (uint32_t *)(&(self->bucket[self->bucket_cnt])); + self->is_use_gnu_hash = 1; + break; + } + default: + break; + } + } + + //check android rel/rela + if(0 != self->relandroid) + { + const char *rel = (const char *)self->relandroid; + if(self->relandroid_sz < 4 || + rel[0] != 'A' || + rel[1] != 'P' || + rel[2] != 'S' || + rel[3] != '2') + { + XH_LOG_ERROR("android rel/rela format error\n"); + return XH_ERRNO_FORMAT; + } + + self->relandroid += 4; + self->relandroid_sz -= 4; + } + + //check elf info + if(0 != xh_elf_check(self)) + { + XH_LOG_ERROR("elf init check failed. %s", pathname); + return XH_ERRNO_FORMAT; + } + +#if XH_ELF_DEBUG + xh_elf_dump(self); +#endif + + XH_LOG_INFO("init OK: %s (%s %s PLT:%u DYN:%u ANDROID:%u)\n", self->pathname, + self->is_use_rela ? "RELA" : "REL", + self->is_use_gnu_hash ? "GNU_HASH" : "ELF_HASH", + self->relplt_sz, self->reldyn_sz, self->relandroid_sz); + + return 0; +} + +static int xh_elf_find_and_replace_func(xh_elf_t *self, const char *section, + int is_plt, const char *symbol, + void *new_func, void **old_func, + uint32_t symidx, void *rel_common, + int *found) +{ + ElfW(Rela) *rela; + ElfW(Rel) *rel; + ElfW(Addr) r_offset; + size_t r_info; + size_t r_sym; + size_t r_type; + ElfW(Addr) addr; + int r; + + if(NULL != found) *found = 0; + + if(self->is_use_rela) + { + rela = (ElfW(Rela) *)rel_common; + r_info = rela->r_info; + r_offset = rela->r_offset; + } + else + { + rel = (ElfW(Rel) *)rel_common; + r_info = rel->r_info; + r_offset = rel->r_offset; + } + + //check sym + r_sym = XH_ELF_R_SYM(r_info); + if(r_sym != symidx) return 0; + + //check type + r_type = XH_ELF_R_TYPE(r_info); + if(is_plt && r_type != XH_ELF_R_GENERIC_JUMP_SLOT) return 0; + if(!is_plt && (r_type != XH_ELF_R_GENERIC_GLOB_DAT && r_type != XH_ELF_R_GENERIC_ABS)) return 0; + + //we found it + XH_LOG_INFO("found %s at %s offset: %p\n", symbol, section, (void *)r_offset); + if(NULL != found) *found = 1; + + //do replace + addr = self->bias_addr + r_offset; + if(addr < self->base_addr) return XH_ERRNO_FORMAT; + if(0 != (r = xh_elf_replace_function(self, symbol, addr, new_func, old_func))) + { + XH_LOG_ERROR("replace function failed: %s at %s\n", symbol, section); + return r; + } + + return 0; +} + +int xh_elf_hook(xh_elf_t *self, const char *symbol, void *new_func, void **old_func) +{ + uint32_t symidx; + void *rel_common; + xh_elf_plain_reloc_iterator_t plain_iter; + xh_elf_packed_reloc_iterator_t packed_iter; + int found; + int r; + + if(NULL == self->pathname) + { + XH_LOG_ERROR("not inited\n"); + return XH_ERRNO_ELFINIT; //not inited? + } + + if(NULL == symbol || NULL == new_func) return XH_ERRNO_INVAL; + + XH_LOG_INFO("hooking %s in %s\n", symbol, self->pathname); + + //find symbol index by symbol name + if(0 != (r = xh_elf_find_symidx_by_name(self, symbol, &symidx))) return 0; + + //replace for .rel(a).plt + if(0 != self->relplt) + { + xh_elf_plain_reloc_iterator_init(&plain_iter, self->relplt, self->relplt_sz, self->is_use_rela); + while(NULL != (rel_common = xh_elf_plain_reloc_iterator_next(&plain_iter))) + { + if(0 != (r = xh_elf_find_and_replace_func(self, + (self->is_use_rela ? ".rela.plt" : ".rel.plt"), 1, + symbol, new_func, old_func, + symidx, rel_common, &found))) return r; + if(found) break; + } + } + + //replace for .rel(a).dyn + if(0 != self->reldyn) + { + xh_elf_plain_reloc_iterator_init(&plain_iter, self->reldyn, self->reldyn_sz, self->is_use_rela); + while(NULL != (rel_common = xh_elf_plain_reloc_iterator_next(&plain_iter))) + { + if(0 != (r = xh_elf_find_and_replace_func(self, + (self->is_use_rela ? ".rela.dyn" : ".rel.dyn"), 0, + symbol, new_func, old_func, + symidx, rel_common, NULL))) return r; + } + } + + //replace for .rel(a).android + if(0 != self->relandroid) + { + xh_elf_packed_reloc_iterator_init(&packed_iter, self->relandroid, self->relandroid_sz, self->is_use_rela); + while(NULL != (rel_common = xh_elf_packed_reloc_iterator_next(&packed_iter))) + { + if(0 != (r = xh_elf_find_and_replace_func(self, + (self->is_use_rela ? ".rela.android" : ".rel.android"), 0, + symbol, new_func, old_func, + symidx, rel_common, NULL))) return r; + } + } + + return 0; +} diff --git a/Core/jni/external/xhook/xh_elf.h b/Core/jni/external/xhook/xh_elf.h new file mode 100644 index 00000000..1697dc48 --- /dev/null +++ b/Core/jni/external/xhook/xh_elf.h @@ -0,0 +1,85 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#ifndef XH_ELF_H +#define XH_ELF_H 1 + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct +{ + const char *pathname; + + ElfW(Addr) base_addr; + ElfW(Addr) bias_addr; + + ElfW(Ehdr) *ehdr; + ElfW(Phdr) *phdr; + + ElfW(Dyn) *dyn; //.dynamic + ElfW(Word) dyn_sz; + + const char *strtab; //.dynstr (string-table) + ElfW(Sym) *symtab; //.dynsym (symbol-index to string-table's offset) + + ElfW(Addr) relplt; //.rel.plt or .rela.plt + ElfW(Word) relplt_sz; + + ElfW(Addr) reldyn; //.rel.dyn or .rela.dyn + ElfW(Word) reldyn_sz; + + ElfW(Addr) relandroid; //android compressed rel or rela + ElfW(Word) relandroid_sz; + + //for ELF hash + uint32_t *bucket; + uint32_t bucket_cnt; + uint32_t *chain; + uint32_t chain_cnt; //invalid for GNU hash + + //append for GNU hash + uint32_t symoffset; + ElfW(Addr) *bloom; + uint32_t bloom_sz; + uint32_t bloom_shift; + + int is_use_rela; + int is_use_gnu_hash; +} xh_elf_t; + +int xh_elf_init(xh_elf_t *self, uintptr_t base_addr, const char *pathname); +int xh_elf_hook(xh_elf_t *self, const char *symbol, void *new_func, void **old_func); + +int xh_elf_check_elfheader(uintptr_t base_addr); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Core/jni/external/xhook/xh_errno.h b/Core/jni/external/xhook/xh_errno.h new file mode 100644 index 00000000..e628cd77 --- /dev/null +++ b/Core/jni/external/xhook/xh_errno.h @@ -0,0 +1,37 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#ifndef XH_ERRNO_H +#define XH_ERRNO_H 1 + +#define XH_ERRNO_UNKNOWN 1001 +#define XH_ERRNO_INVAL 1002 +#define XH_ERRNO_NOMEM 1003 +#define XH_ERRNO_REPEAT 1004 +#define XH_ERRNO_NOTFND 1005 +#define XH_ERRNO_BADMAPS 1006 +#define XH_ERRNO_FORMAT 1007 +#define XH_ERRNO_ELFINIT 1008 +#define XH_ERRNO_SEGVERR 1009 + +#endif diff --git a/Core/jni/external/xhook/xh_jni.c b/Core/jni/external/xhook/xh_jni.c new file mode 100644 index 00000000..f8ae223d --- /dev/null +++ b/Core/jni/external/xhook/xh_jni.c @@ -0,0 +1,59 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#include +#include "xhook.h" + +#define JNI_API_DEF(f) Java_com_qiyi_xhook_NativeHandler_##f + +JNIEXPORT jint JNI_API_DEF(refresh)(JNIEnv *env, jobject obj, jboolean async) +{ + (void)env; + (void)obj; + + return xhook_refresh(async ? 1 : 0); +} + +JNIEXPORT void JNI_API_DEF(clear)(JNIEnv *env, jobject obj) +{ + (void)env; + (void)obj; + + xhook_clear(); +} + +JNIEXPORT void JNI_API_DEF(enableDebug)(JNIEnv *env, jobject obj, jboolean flag) +{ + (void)env; + (void)obj; + + xhook_enable_debug(flag ? 1 : 0); +} + +JNIEXPORT void JNI_API_DEF(enableSigSegvProtection)(JNIEnv *env, jobject obj, jboolean flag) +{ + (void)env; + (void)obj; + + xhook_enable_sigsegv_protection(flag ? 1 : 0); +} diff --git a/Core/jni/external/xhook/xh_log.c b/Core/jni/external/xhook/xh_log.c new file mode 100644 index 00000000..6cba9478 --- /dev/null +++ b/Core/jni/external/xhook/xh_log.c @@ -0,0 +1,27 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#include +#include "xh_log.h" + +android_LogPriority xh_log_priority = ANDROID_LOG_WARN; diff --git a/Core/jni/external/xhook/xh_log.h b/Core/jni/external/xhook/xh_log.h new file mode 100644 index 00000000..e108c4b0 --- /dev/null +++ b/Core/jni/external/xhook/xh_log.h @@ -0,0 +1,45 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#ifndef XH_LOG_H +#define XH_LOG_H 1 + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern android_LogPriority xh_log_priority; + +#define XH_LOG_TAG "xhook" +#define XH_LOG_DEBUG(fmt, ...) do{if(xh_log_priority <= ANDROID_LOG_DEBUG) __android_log_print(ANDROID_LOG_DEBUG, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) +#define XH_LOG_INFO(fmt, ...) do{if(xh_log_priority <= ANDROID_LOG_INFO) __android_log_print(ANDROID_LOG_INFO, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) +#define XH_LOG_WARN(fmt, ...) do{if(xh_log_priority <= ANDROID_LOG_WARN) __android_log_print(ANDROID_LOG_WARN, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) +#define XH_LOG_ERROR(fmt, ...) do{if(xh_log_priority <= ANDROID_LOG_ERROR) __android_log_print(ANDROID_LOG_ERROR, XH_LOG_TAG, fmt, ##__VA_ARGS__);}while(0) + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Core/jni/external/xhook/xh_util.c b/Core/jni/external/xhook/xh_util.c new file mode 100644 index 00000000..0e2dca20 --- /dev/null +++ b/Core/jni/external/xhook/xh_util.c @@ -0,0 +1,121 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "xh_util.h" +#include "xh_errno.h" +#include "xh_log.h" + +#define PAGE_START(addr) ((addr) & PAGE_MASK) +#define PAGE_END(addr) (PAGE_START(addr + sizeof(uintptr_t) - 1) + PAGE_SIZE) +#define PAGE_COVER(addr) (PAGE_END(addr) - PAGE_START(addr)) + +int xh_util_get_mem_protect(uintptr_t addr, size_t len, const char *pathname, unsigned int *prot) +{ + uintptr_t start_addr = addr; + uintptr_t end_addr = addr + len; + FILE *fp; + char line[512]; + uintptr_t start, end; + char perm[5]; + int load0 = 1; + int found_all = 0; + + *prot = 0; + + if(NULL == (fp = fopen("/proc/self/maps", "r"))) return XH_ERRNO_BADMAPS; + + while(fgets(line, sizeof(line), fp)) + { + if(NULL != pathname) + if(NULL == strstr(line, pathname)) continue; + + if(sscanf(line, "%"PRIxPTR"-%"PRIxPTR" %4s ", &start, &end, perm) != 3) continue; + + if(perm[3] != 'p') continue; + + if(start_addr >= start && start_addr < end) + { + if(load0) + { + //first load segment + if(perm[0] == 'r') *prot |= PROT_READ; + if(perm[1] == 'w') *prot |= PROT_WRITE; + if(perm[2] == 'x') *prot |= PROT_EXEC; + load0 = 0; + } + else + { + //others + if(perm[0] != 'r') *prot &= ~PROT_READ; + if(perm[1] != 'w') *prot &= ~PROT_WRITE; + if(perm[2] != 'x') *prot &= ~PROT_EXEC; + } + + if(end_addr <= end) + { + found_all = 1; + break; //finished + } + else + { + start_addr = end; //try to find the next load segment + } + } + } + + fclose(fp); + + if(!found_all) return XH_ERRNO_SEGVERR; + + return 0; +} + +int xh_util_get_addr_protect(uintptr_t addr, const char *pathname, unsigned int *prot) +{ + return xh_util_get_mem_protect(addr, sizeof(addr), pathname, prot); +} + +int xh_util_set_addr_protect(uintptr_t addr, unsigned int prot) +{ + if(0 != mprotect((void *)PAGE_START(addr), PAGE_COVER(addr), (int)prot)) + return 0 == errno ? XH_ERRNO_UNKNOWN : errno; + + return 0; +} + +void xh_util_flush_instruction_cache(uintptr_t addr) +{ + __builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr)); +} diff --git a/Core/jni/external/xhook/xh_util.h b/Core/jni/external/xhook/xh_util.h new file mode 100644 index 00000000..b57f8dc6 --- /dev/null +++ b/Core/jni/external/xhook/xh_util.h @@ -0,0 +1,51 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#ifndef XH_UTILS_H +#define XH_UTILS_H 1 + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(__LP64__) +#define XH_UTIL_FMT_LEN "16" +#define XH_UTIL_FMT_X "llx" +#else +#define XH_UTIL_FMT_LEN "8" +#define XH_UTIL_FMT_X "x" +#endif + +#define XH_UTIL_FMT_FIXED_X XH_UTIL_FMT_LEN XH_UTIL_FMT_X +#define XH_UTIL_FMT_FIXED_S XH_UTIL_FMT_LEN "s" + +int xh_util_get_mem_protect(uintptr_t addr, size_t len, const char *pathname, unsigned int *prot); +int xh_util_get_addr_protect(uintptr_t addr, const char *pathname, unsigned int *prot); +int xh_util_set_addr_protect(uintptr_t addr, unsigned int prot); +void xh_util_flush_instruction_cache(uintptr_t addr); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Core/jni/external/xhook/xh_version.c b/Core/jni/external/xhook/xh_version.c new file mode 100644 index 00000000..b237f169 --- /dev/null +++ b/Core/jni/external/xhook/xh_version.c @@ -0,0 +1,66 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#include "xh_version.h" + +#define XH_VERSION_MAJOR 1 +#define XH_VERSION_MINOR 1 +#define XH_VERSION_EXTRA 9 + +#define XH_VERSION ((XH_VERSION_MAJOR << 16) | (XH_VERSION_MINOR << 8) | (XH_VERSION_EXTRA)) + +#define XH_VERSION_TO_STR_HELPER(x) #x +#define XH_VERSION_TO_STR(x) XH_VERSION_TO_STR_HELPER(x) + +#define XH_VERSION_STR XH_VERSION_TO_STR(XH_VERSION_MAJOR) "." \ + XH_VERSION_TO_STR(XH_VERSION_MINOR) "." \ + XH_VERSION_TO_STR(XH_VERSION_EXTRA) + +#if defined(__arm__) +#define XH_VERSION_ARCH "arm" +#elif defined(__aarch64__) +#define XH_VERSION_ARCH "aarch64" +#elif defined(__i386__) +#define XH_VERSION_ARCH "x86" +#elif defined(__x86_64__) +#define XH_VERSION_ARCH "x86_64" +#else +#define XH_VERSION_ARCH "unknown" +#endif + +#define XH_VERSION_STR_FULL "libxhook "XH_VERSION_STR" ("XH_VERSION_ARCH")" + +unsigned int xh_version() +{ + return XH_VERSION; +} + +const char *xh_version_str() +{ + return XH_VERSION_STR; +} + +const char *xh_version_str_full() +{ + return XH_VERSION_STR_FULL; +} diff --git a/Core/jni/external/xhook/xh_version.h b/Core/jni/external/xhook/xh_version.h new file mode 100644 index 00000000..b70b4f2f --- /dev/null +++ b/Core/jni/external/xhook/xh_version.h @@ -0,0 +1,41 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#ifndef XH_VERSION_H +#define XH_VERSION_H 1 + +#ifdef __cplusplus +extern "C" { +#endif + +unsigned int xh_version(); + +const char *xh_version_str(); + +const char *xh_version_str_full(); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Core/jni/external/xhook/xhook.c b/Core/jni/external/xhook/xhook.c new file mode 100644 index 00000000..49dfc596 --- /dev/null +++ b/Core/jni/external/xhook/xhook.c @@ -0,0 +1,56 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#include "xh_core.h" +#include "xhook.h" + +int xhook_register(const char *pathname_regex_str, const char *symbol, + void *new_func, void **old_func) +{ + return xh_core_register(pathname_regex_str, symbol, new_func, old_func); +} + +int xhook_ignore(const char *pathname_regex_str, const char *symbol) +{ + return xh_core_ignore(pathname_regex_str, symbol); +} + +int xhook_refresh(int async) +{ + return xh_core_refresh(async); +} + +void xhook_clear() +{ + return xh_core_clear(); +} + +void xhook_enable_debug(int flag) +{ + return xh_core_enable_debug(flag); +} + +void xhook_enable_sigsegv_protection(int flag) +{ + return xh_core_enable_sigsegv_protection(flag); +} diff --git a/Core/jni/external/xhook/xhook.h b/Core/jni/external/xhook/xhook.h new file mode 100644 index 00000000..93dd5b4c --- /dev/null +++ b/Core/jni/external/xhook/xhook.h @@ -0,0 +1,50 @@ +// Copyright (c) 2018-present, iQIYI, Inc. 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. +// + +// Created by caikelun on 2018-04-11. + +#ifndef XHOOK_H +#define XHOOK_H 1 + +#ifdef __cplusplus +extern "C" { +#endif + +#define XHOOK_EXPORT __attribute__((visibility("default"))) + +int xhook_register(const char *pathname_regex_str, const char *symbol, + void *new_func, void **old_func) XHOOK_EXPORT; + +int xhook_ignore(const char *pathname_regex_str, const char *symbol) XHOOK_EXPORT; + +int xhook_refresh(int async) XHOOK_EXPORT; + +void xhook_clear() XHOOK_EXPORT; + +void xhook_enable_debug(int flag) XHOOK_EXPORT; + +void xhook_enable_sigsegv_protection(int flag) XHOOK_EXPORT; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Core/jni/main/Android.mk b/Core/jni/main/Android.mk new file mode 100644 index 00000000..b8bff4fe --- /dev/null +++ b/Core/jni/main/Android.mk @@ -0,0 +1,23 @@ +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE := libriru_edxposed +LOCAL_C_INCLUDES := \ + $(LOCAL_PATH) \ + jni/external/include +LOCAL_CPPFLAGS += $(CPPFLAGS) +LOCAL_STATIC_LIBRARIES := xhook +LOCAL_LDLIBS += -ldl -llog +LOCAL_LDFLAGS := -Wl + +LOCAL_SRC_FILES:= \ + main.cpp \ + native_hook/native_hook.cpp \ + include/misc.cpp \ + include/riru.c \ + yahfa/HookMain.c \ + yahfa/trampoline.c \ + java_hook/java_hook.cpp \ + inject/framework_hook.cpp + +include $(BUILD_SHARED_LIBRARY) \ No newline at end of file diff --git a/Core/jni/main/include/JNIHelper.h b/Core/jni/main/include/JNIHelper.h new file mode 100644 index 00000000..2436538f --- /dev/null +++ b/Core/jni/main/include/JNIHelper.h @@ -0,0 +1,73 @@ +#ifndef JNIHELPER_H +#define JNIHELPER_H + +#include +#include "logging.h" + +int ClearException(JNIEnv *env) { + jthrowable exception = env->ExceptionOccurred(); + if (exception != nullptr) { + env->ExceptionDescribe(); + env->ExceptionClear(); + return true; + } + return false; +} + +#define JNI_FindClass(env, name) \ + env->FindClass(name); \ + if (ClearException(env)) LOGE("FindClass " #name); + +#define JNI_GetObjectClass(env, obj) \ + env->GetObjectClass(obj); \ + if (ClearException(env)) LOGE("GetObjectClass " #obj); + +#define JNI_GetFieldID(env, class, name, sig) \ + env->GetFieldID(class, name, sig); \ + if (ClearException(env)) LOGE("GetFieldID " #name); + +#define JNI_GetObjectField(env, class, fieldId) \ + env->GetObjectField(class, fieldId); \ + if (ClearException(env)) LOGE("GetObjectField " #fieldId); + +#define JNI_GetMethodID(env, class, name, sig) \ + env->GetMethodID(class, name, sig); \ + if (ClearException(env)) LOGE("GetMethodID " #name); + +#define JNI_CallObjectMethod(env, obj, ...) \ + env->CallObjectMethod(obj, __VA_ARGS__); \ + if (ClearException(env)) LOGE("CallObjectMethod " #obj " " #__VA_ARGS__); + +#define JNI_CallVoidMethod(env, obj, ...) \ + env->CallVoidMethod(obj, __VA_ARGS__); \ + if (ClearException(env)) LOGE("CallVoidMethod " #obj " " #__VA_ARGS__); + +#define JNI_GetStaticFieldID(env, class, name, sig) \ + env->GetStaticFieldID(class, name, sig); \ + if (ClearException(env)) LOGE("GetStaticFieldID " #name " " #sig); + +#define JNI_GetStaticObjectField(env, class, fieldId) \ + env->GetStaticObjectField(class, fieldId); \ + if (ClearException(env)) LOGE("GetStaticObjectField " #fieldId); + +#define JNI_GetStaticMethodID(env, class, name, sig) \ + env->GetStaticMethodID(class, name, sig); \ + if (ClearException(env)) LOGE("GetStaticMethodID " #name); + +#define JNI_CallStaticVoidMethod(env, obj, ...) \ + env->CallStaticVoidMethod(obj, __VA_ARGS__); \ + if (ClearException(env)) LOGE("CallStaticVoidMethod " #obj " " #__VA_ARGS__); + +#define JNI_GetArrayLength(env, array) \ + env->GetArrayLength(array); \ + if (ClearException(env)) LOGE("GetArrayLength " #array); + +#define JNI_NewObject(env, class, ...) \ + env->NewObject(class, __VA_ARGS__); \ + if (ClearException(env)) LOGE("NewObject " #class " " #__VA_ARGS__); + +#define JNI_RegisterNatives(env, class, methods, size) \ + env->RegisterNatives(class, methods, size); \ + if (ClearException(env)) LOGE("RegisterNatives " #class); + +#endif // JNIHELPER_H diff --git a/Core/jni/main/include/config.h b/Core/jni/main/include/config.h new file mode 100644 index 00000000..7675c805 --- /dev/null +++ b/Core/jni/main/include/config.h @@ -0,0 +1,20 @@ +#include +#include + +#ifndef CONFIG_H +#define CONFIG_H + +//#define LOG_DISABLED +//#define DEBUG + +#ifdef DEBUG +#define INJECT_DEX_PATH \ +"/data/local/tmp/edxposed.dex:/data/local/tmp/eddalvikdx.dex:/data/local/tmp/eddexmaker.dex" +#else +#define INJECT_DEX_PATH \ +"/system/framework/edxposed.dex:/system/framework/eddalvikdx.dex:/system/framework/eddexmaker.dex" +#endif + +#define ENTRY_CLASS_NAME "com.elderdrivers.riru.xposed.Main" + +#endif //CONFIG_H \ No newline at end of file diff --git a/Core/jni/main/include/logging.h b/Core/jni/main/include/logging.h new file mode 100644 index 00000000..0a1eb5c6 --- /dev/null +++ b/Core/jni/main/include/logging.h @@ -0,0 +1,32 @@ +#ifndef _LOGGING_H +#define _LOGGING_H + +#include +#include "android/log.h" + +#ifndef LOG_TAG +#define LOG_TAG "EdXposed-Core-Native" +#endif + +#include "config.h" + +#ifdef LOG_DISABLED +#define LOGD(...) +#define LOGV(...) +#define LOGI(...) +#define LOGW(...) +#define LOGE(...) +#else +#ifdef DEBUG +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) +#else +#define LOGD(...) +#endif +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) +#endif + +#endif // _LOGGING_H diff --git a/Core/jni/main/include/misc.cpp b/Core/jni/main/include/misc.cpp new file mode 100644 index 00000000..04e9055a --- /dev/null +++ b/Core/jni/main/include/misc.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "java_hook/java_hook.h" +#include "include/logging.h" +#include "misc.h" + +ssize_t fdgets(char *buf, const size_t size, int fd) { + ssize_t len = 0; + buf[0] = '\0'; + while (len < size - 1) { + ssize_t ret = read(fd, buf + len, 1); + if (ret < 0) + return -1; + if (ret == 0) + break; + if (buf[len] == '\0' || buf[len++] == '\n') { + break; + } + } + buf[len] = '\0'; + buf[size - 1] = '\0'; + return len; +} + +char *get_cmdline_from_pid(pid_t pid, char *buf, size_t len) { + char filename[32]; + if (pid < 1 || buf == NULL) { + printf("%s: illegal para\n", __func__); + return NULL; + } + + snprintf(filename, 32, "/proc/%d/cmdline", pid); + int read_ret = read_to_buf(filename, buf, len); + if (read_ret < 0) + return NULL; + + if (buf[read_ret - 1] == '\n') + buf[--read_ret] = 0; + + char *name = buf; + while (read_ret) { + if (((unsigned char) *name) < ' ') + *name = ' '; + name++; + read_ret--; + } + *name = 0; + name = NULL; + + LOGV("cmdline:%s\n", buf); + return buf; +} + +int read_to_buf(const char *filename, void *buf, size_t len) { + int fd; + int ret; + if (buf == NULL) { + printf("%s: illegal para\n", __func__); + return -1; + } + memset(buf, 0, len); + fd = open(filename, O_RDONLY); + if (fd < 0) { + perror("open"); + return -1; + } + ret = (int) read(fd, buf, len); + close(fd); + return ret; +} + +char *getAppId(char *application_id, size_t size) { + pid_t pid = getpid(); +// LOGV("process new id %d", pid); + char path[64] = {0}; + sprintf(path, "/proc/%d/cmdline", pid); + FILE *cmdline = fopen(path, "r"); + if (cmdline) { + fread(application_id, size, 1, cmdline); +// LOGV("application id %s", application_id); + fclose(cmdline); + } + return application_id; +} \ No newline at end of file diff --git a/Core/jni/main/include/misc.h b/Core/jni/main/include/misc.h new file mode 100644 index 00000000..d5fa4ee2 --- /dev/null +++ b/Core/jni/main/include/misc.h @@ -0,0 +1,14 @@ +#ifndef MISC_H +#define MISC_H + +#include + +ssize_t fdgets(char *buf, const size_t size, int fd); + +char *get_cmdline_from_pid(pid_t pid, char *buf, size_t len); + +int read_to_buf(const char *filename, void *buf, size_t len); + +char *getAppId(char *application_id, size_t size); + +#endif // MISC_H diff --git a/Core/jni/main/include/riru.c b/Core/jni/main/include/riru.c new file mode 100644 index 00000000..34f125a4 --- /dev/null +++ b/Core/jni/main/include/riru.c @@ -0,0 +1,76 @@ +#include +#include +#include + +#ifdef __LP64__ +#define LIB "/system/lib64/libmemtrack.so" +#else +#define LIB "/system/lib/libmemtrack.so" +#endif + +static void *riru_handle; +static char *riru_module_name; + +static void *get_handle() { + if (riru_handle == NULL) + riru_handle = dlopen(LIB, RTLD_NOW | RTLD_GLOBAL); + + return riru_handle; +} + +const char *riru_get_module_name() { + return riru_module_name; +} + +void riru_set_module_name(const char *name) { + riru_module_name = strdup(name); +} + +int riru_get_version() { + static void **sym; + void *handle; + if ((handle = get_handle()) == NULL) return -1; + if (sym == NULL) sym = dlsym(handle, "riru_get_version"); + if (sym) return ((int (*)()) sym)(); + return -1; +} + +void *riru_get_func(const char *name) { + static void **sym; + void *handle; + if ((handle = get_handle()) == NULL) return NULL; + if (sym == NULL) sym = dlsym(handle, "riru_get_func"); + if (sym) return ((void *(*)(const char *, const char *)) sym)(riru_get_module_name(), name); + return NULL; +} + +void *riru_get_native_method_func(const char *className, const char *name, const char *signature) { + static void **sym; + void *handle; + if ((handle = get_handle()) == NULL) return NULL; + if (sym == NULL) sym = dlsym(handle, "riru_get_native_method_func"); + if (sym) + return ((void *(*)(const char *, const char *, const char *, const char *)) sym)( + riru_get_module_name(), className, name, signature); + return NULL; +} + +void riru_set_func(const char *name, void *func) { + static void **sym; + void *handle; + if ((handle = get_handle()) == NULL) return; + if (sym == NULL) sym = dlsym(handle, "riru_set_func"); + if (sym) + ((void *(*)(const char *, const char *, void *)) sym)(riru_get_module_name(), name, func); +} + +void riru_set_native_method_func(const char *className, const char *name, const char *signature, + void *func) { + static void **sym; + void *handle; + if ((handle = get_handle()) == NULL) return; + if (sym == NULL) sym = dlsym(handle, "riru_set_native_method_func"); + if (sym) + ((void *(*)(const char *, const char *, const char *, const char *, void *)) sym)( + riru_get_module_name(), className, name, signature, func); +} \ No newline at end of file diff --git a/Core/jni/main/include/riru.h b/Core/jni/main/include/riru.h new file mode 100644 index 00000000..4c0ab3a8 --- /dev/null +++ b/Core/jni/main/include/riru.h @@ -0,0 +1,57 @@ +#ifndef RIRU_H +#define RIRU_H + +#ifdef __cplusplus +extern "C" { +#endif +__attribute__((visibility("default"))) void riru_set_module_name(const char *name); + +/** + * Get Riru version. + * + * @return Riru version + */ +int riru_get_version(); + +/* + * Get new_func address from last module which hook func. + * Use this as your old_func if you want to hook func. + * + * @param name a unique name + * @return new_func from last module or null + */ +void *riru_get_func(const char *name); + +/* + * Java native version of riru_get_func. + * + * @param className class name + * @param name method name + * @param signature method signature + * @return new_func address from last module or original address + */ +void *riru_get_native_method_func(const char *className, const char *name, const char *signature); + +/* + * Set new_func address for next module which wants to hook func. + * + * @param name a unique name + * @param func your new_func address + */ +void riru_set_func(const char *name, void *func); + +/* + * Java native method version of riru_set_func. + * + * @param className class name + * @param name method name + * @param signature method signature + * @param func your new_func address + */ +void riru_set_native_method_func(const char *className, const char *name, const char *signature, + void *func); +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/Core/jni/main/inject/framework_hook.cpp b/Core/jni/main/inject/framework_hook.cpp new file mode 100644 index 00000000..69730328 --- /dev/null +++ b/Core/jni/main/inject/framework_hook.cpp @@ -0,0 +1,91 @@ + + +#include +#include +#include +#include "framework_hook.h" +#include "include/misc.h" + +static jclass sEntryClass; +static jstring sAppDataDir; + +void prepareJavaEnv(JNIEnv *env) { + loadDexAndInit(env, INJECT_DEX_PATH); + sEntryClass = findClassFromLoader(env, gInjectDexClassLoader, ENTRY_CLASS_NAME); +} + +void findAndCall(JNIEnv *env, const char *methodName, const char *methodSig, ...) { + if (!sEntryClass) { + LOGE("cannot call method %s, entry class is null", methodName); + return; + } + jmethodID mid = env->GetStaticMethodID(sEntryClass, methodName, methodSig); + if (env->ExceptionOccurred()) { + env->ExceptionClear(); + LOGE("method %s not found in entry class", methodName); + mid = NULL; + } + if (mid) { + va_list args; + va_start(args, methodSig); + env->functions->CallStaticVoidMethodV(env, sEntryClass, mid, args); + va_end(args); + } else { + LOGE("method %s id is null", methodName); + } +} + +void onNativeForkSystemServerPre(JNIEnv *env, jclass clazz, uid_t uid, gid_t gid, jintArray gids, + jint runtime_flags, jobjectArray rlimits, + jlong permittedCapabilities, jlong effectiveCapabilities) { + prepareJavaEnv(env); + // jump to java code + findAndCall(env, "forkSystemServerPre", "(II[II[[IJJ)V", uid, gid, gids, runtime_flags, rlimits, + permittedCapabilities, effectiveCapabilities); +} + + +int onNativeForkSystemServerPost(JNIEnv *env, jclass clazz, jint res) { + if (res == 0) { + prepareJavaEnv(env); + // only do work in child since findAndCall would print log + findAndCall(env, "forkSystemServerPost", "(I)V", res); + } else { + // in zygote process, res is child zygote pid + // don't print log here, see https://github.com/RikkaApps/Riru/blob/77adfd6a4a6a81bfd20569c910bc4854f2f84f5e/riru-core/jni/main/jni_native_method.cpp#L55-L66 + } + return 0; +} + +void onNativeForkAndSpecializePre(JNIEnv *env, jclass clazz, + jint uid, jint gid, + jintArray gids, + jint runtime_flags, + jobjectArray rlimits, + jint _mount_external, + jstring se_info, + jstring se_name, + jintArray fdsToClose, + jintArray fdsToIgnore, + jboolean is_child_zygote, + jstring instructionSet, + jstring appDataDir) { + prepareJavaEnv(env); + findAndCall(env, "forkAndSpecializePre", + "(II[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/String;)V", + uid, gid, gids, runtime_flags, rlimits, + _mount_external, se_info, se_name, fdsToClose, fdsToIgnore, + is_child_zygote, instructionSet, appDataDir); + sAppDataDir = appDataDir; +} + +int onNativeForkAndSpecializePost(JNIEnv *env, jclass clazz, jint res) { + if (res == 0) { + prepareJavaEnv(env); + findAndCall(env, "forkAndSpecializePost", "(ILjava/lang/String;)V", res, sAppDataDir); + } else { + // in zygote process, res is child zygote pid + // don't print log here, see https://github.com/RikkaApps/Riru/blob/77adfd6a4a6a81bfd20569c910bc4854f2f84f5e/riru-core/jni/main/jni_native_method.cpp#L55-L66 + } + return 0; +} \ No newline at end of file diff --git a/Core/jni/main/inject/framework_hook.h b/Core/jni/main/inject/framework_hook.h new file mode 100644 index 00000000..6e2cffab --- /dev/null +++ b/Core/jni/main/inject/framework_hook.h @@ -0,0 +1,30 @@ + +#ifndef RIRU_FRAMEWORK_HOOK_H +#define RIRU_FRAMEWORK_HOOK_H + + +#include + +void onNativeForkSystemServerPre(JNIEnv *env, jclass clazz, uid_t uid, gid_t gid, jintArray gids, + jint runtime_flags, jobjectArray rlimits, + jlong permittedCapabilities, jlong effectiveCapabilities); + +int onNativeForkSystemServerPost(JNIEnv *env, jclass clazz, jint res); + +void onNativeForkAndSpecializePre(JNIEnv *env, jclass clazz, + jint _uid, jint gid, + jintArray gids, + jint runtime_flags, + jobjectArray rlimits, + jint _mount_external, + jstring se_info, + jstring se_name, + jintArray fdsToClose, + jintArray fdsToIgnore, + jboolean is_child_zygote, + jstring instructionSet, + jstring appDataDir); + +int onNativeForkAndSpecializePost(JNIEnv *env, jclass clazz, jint res); + +#endif //RIRU_FRAMEWORK_HOOK_H diff --git a/Core/jni/main/java_hook/java_hook.cpp b/Core/jni/main/java_hook/java_hook.cpp new file mode 100644 index 00000000..a5e231fa --- /dev/null +++ b/Core/jni/main/java_hook/java_hook.cpp @@ -0,0 +1,140 @@ +#include +#include +#include +#include +#include +#include +#include +#include "java_hook/java_hook.h" +#include "include/logging.h" + +extern "C" +{ +#include "../yahfa/HookMain.h" +} + +jobject gInjectDexClassLoader; + +static bool isInited = false; + +static JNINativeMethod hookMethods[] = { + { + "init", + "(I)V", + (void *) Java_lab_galaxy_yahfa_HookMain_init + }, + { + "backupAndHookNative", + "(Ljava/lang/Object;Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)Z", + (void *) Java_lab_galaxy_yahfa_HookMain_backupAndHookNative + }, + { + "findMethodNative", + "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (void *) Java_lab_galaxy_yahfa_HookMain_findMethodNative + }, + { + "ensureMethodCached", + "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V", + (void *) Java_lab_galaxy_yahfa_HookMain_ensureMethodCached + } +}; + +void loadDexAndInit(JNIEnv *env, const char *dexPath) { + if (isInited) { + return; + } + jclass clzClassLoader = env->FindClass("java/lang/ClassLoader"); + LOGD("java/lang/ClassLoader: %p", clzClassLoader); + jmethodID mdgetSystemClassLoader = env->GetStaticMethodID(clzClassLoader, + "getSystemClassLoader", + "()Ljava/lang/ClassLoader;"); + LOGD("java/lang/ClassLoader.getSystemClassLoader method: %p", mdgetSystemClassLoader); + jobject systemClassLoader = env->CallStaticObjectMethod(clzClassLoader, mdgetSystemClassLoader); + LOGD("java/lang/ClassLoader.getSystemClassLoader method result: %p", systemClassLoader); + if (NULL == systemClassLoader) { + LOGE("getSystemClassLoader failed!!!"); + return; + } + jclass clzPathClassLoader = env->FindClass("dalvik/system/PathClassLoader"); + LOGD("dalvik/system/PathClassLoader: %p", clzClassLoader); + jmethodID mdinitPathCL = env->GetMethodID(clzPathClassLoader, "", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V"); + LOGD("dalvik/system/PathClassLoader.: %p", clzClassLoader); + jstring jarpath_str = env->NewStringUTF(dexPath); + jobject myClassLoader = env->NewObject(clzPathClassLoader, mdinitPathCL, + jarpath_str, NULL, systemClassLoader); + if (NULL == myClassLoader) { + LOGE("PathClassLoader creation failed!!!"); + return; + } + gInjectDexClassLoader = env->NewGlobalRef(myClassLoader); + LOGD("PathClassLoader created: %p", myClassLoader); + LOGD("PathClassLoader loading dexPath[%s]\n", dexPath); + jclass entry_class = findClassFromLoader(env, myClassLoader, ENTRY_CLASS_NAME); + if (NULL != entry_class) { + LOGD("HookEntry Class %p", entry_class); + env->RegisterNatives(entry_class, hookMethods, 4); + isInited = true; + LOGD("RegisterNatives succeed for HookEntry."); + } else { + LOGE("HookEntry class is null. %d", getpid()); + } +} + +jstring getThrowableMessage(JNIEnv *env, jobject throwable) { + if (!throwable) { + LOGE("throwable is null."); + return NULL; + } + jclass jthrowableClass = env->GetObjectClass(throwable); + jmethodID getMsgMid = env->GetMethodID(jthrowableClass, "getMessage", "()Ljava/lang/String;"); + if (getMsgMid == 0) { + LOGE("get Throwable.getMessage method id failed."); + return NULL; + } + return (jstring) env->CallObjectMethod(throwable, getMsgMid); +} + +jclass findClassFromLoader(JNIEnv *env, jobject classLoader, const char *className) { + jclass clz = env->GetObjectClass(classLoader); + jmethodID mid = env->GetMethodID(clz, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); + if (env->ExceptionOccurred()) { + LOGE("loadClass method not found"); + env->ExceptionClear(); + } else { + LOGD("loadClass method %p", mid); + } + jclass ret = NULL; + if (!mid) { + mid = env->GetMethodID(clz, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); + if (env->ExceptionOccurred()) { + LOGE("findClass method not found"); + env->ExceptionClear(); + } else { + LOGD("findClass method %p", mid); + } + } + if (mid) { + jstring className_str = env->NewStringUTF(className); + jobject tmp = env->CallObjectMethod(classLoader, mid, className_str); + jthrowable exception = env->ExceptionOccurred(); + if (exception) { + jstring message = getThrowableMessage(env, exception); + const char *message_char = env->GetStringUTFChars(message, JNI_FALSE); + LOGE("Error when findClass %s: %s", className, message_char); + env->ReleaseStringUTFChars(message, message_char); + env->ExceptionClear(); + } + if (NULL != tmp) { + LOGD("findClassFromLoader %p", tmp); + ret = (jclass) tmp; + } + } else { + LOGE("no method found"); + } + if (ret == NULL) { + LOGE("class %s not found.", className); + } + return ret; +} \ No newline at end of file diff --git a/Core/jni/main/java_hook/java_hook.h b/Core/jni/main/java_hook/java_hook.h new file mode 100644 index 00000000..ac6e2939 --- /dev/null +++ b/Core/jni/main/java_hook/java_hook.h @@ -0,0 +1,13 @@ +#ifndef _JAVAHOOK_H +#define _JAVAHOOK_H + +#include +#include + +extern jobject gInjectDexClassLoader; + +void loadDexAndInit(JNIEnv *env, const char *dexPath); + +jclass findClassFromLoader(JNIEnv *env, jobject classLoader, const char *className); + +#endif // _JAVAHOOK_H \ No newline at end of file diff --git a/Core/jni/main/main.cpp b/Core/jni/main/main.cpp new file mode 100644 index 00000000..9f6bcb82 --- /dev/null +++ b/Core/jni/main/main.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "include/logging.h" +#include "include/misc.h" + +#include "include/config.h" + +extern "C" { +__attribute__((visibility("default"))) void nativeForkAndSpecializePre(JNIEnv *env, jclass clazz, + jint _uid, jint gid, + jintArray gids, + jint runtime_flags, + jobjectArray rlimits, + jint _mount_external, + jstring se_info, + jstring se_name, + jintArray fdsToClose, + jintArray fdsToIgnore, + jboolean is_child_zygote, + jstring instructionSet, + jstring appDataDir) { + onNativeForkAndSpecializePre(env, clazz, _uid, gid, gids, runtime_flags, rlimits, + _mount_external, se_info, se_name, fdsToClose, fdsToIgnore, + is_child_zygote, instructionSet, appDataDir); +} + +__attribute__((visibility("default"))) int nativeForkAndSpecializePost(JNIEnv *env, jclass clazz, + jint res) { + return onNativeForkAndSpecializePost(env, clazz, res); +} + +__attribute__((visibility("default"))) void onModuleLoaded() { + +} + +__attribute__((visibility("default"))) +void nativeForkSystemServerPre(JNIEnv *env, jclass clazz, uid_t uid, gid_t gid, jintArray gids, + jint runtime_flags, jobjectArray rlimits, + jlong permittedCapabilities, jlong effectiveCapabilities) { + onNativeForkSystemServerPre(env, clazz, uid, gid, gids, runtime_flags, rlimits, + permittedCapabilities, effectiveCapabilities); +} + + +__attribute__((visibility("default"))) +int nativeForkSystemServerPost(JNIEnv *env, jclass clazz, jint res) { + return onNativeForkSystemServerPost(env, clazz, res); +} + +} diff --git a/Core/jni/main/native_hook/native_hook.cpp b/Core/jni/main/native_hook/native_hook.cpp new file mode 100644 index 00000000..6e1534aa --- /dev/null +++ b/Core/jni/main/native_hook/native_hook.cpp @@ -0,0 +1,20 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "include/riru.h" +#include "include/logging.h" +#include "native_hook.h" +#include "java_hook/java_hook.h" +#include "inject/framework_hook.h" \ No newline at end of file diff --git a/Core/jni/main/native_hook/native_hook.h b/Core/jni/main/native_hook/native_hook.h new file mode 100644 index 00000000..ecb90cce --- /dev/null +++ b/Core/jni/main/native_hook/native_hook.h @@ -0,0 +1,14 @@ +#ifndef HOOK_H +#define HOOK_H + +#include + +#define XHOOK_REGISTER(NAME) \ + if (xhook_register(".*", #NAME, (void*) new_##NAME, (void **) &old_##NAME) != 0) \ + LOGE("failed to register hook " #NAME "."); \ + +#define NEW_FUNC_DEF(ret, func, ...) \ + static ret (*old_##func)(__VA_ARGS__); \ + static ret new_##func(__VA_ARGS__) + +#endif // HOOK_H diff --git a/Core/jni/main/yahfa/HookMain.c b/Core/jni/main/yahfa/HookMain.c new file mode 100644 index 00000000..9c65e053 --- /dev/null +++ b/Core/jni/main/yahfa/HookMain.c @@ -0,0 +1,314 @@ +#include "jni.h" +#include +#include +#include + +#include "common.h" +#include "env.h" +#include "trampoline.h" +#include "HookMain.h" + +int SDKVersion; +static int OFFSET_entry_point_from_interpreter_in_ArtMethod; +int OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod; +static int OFFSET_dex_method_index_in_ArtMethod; +static int OFFSET_dex_cache_resolved_methods_in_ArtMethod; +static int OFFSET_array_in_PointerArray; +static int OFFSET_ArtMehod_in_Object; +static int OFFSET_access_flags_in_ArtMethod; +static size_t ArtMethodSize; +static int kAccNative = 0x0100; +static int kAccCompileDontBother = 0x01000000; + +static inline uint16_t read16(void *addr) { + return *((uint16_t *) addr); +} + +static inline uint32_t read32(void *addr) { + return *((uint32_t *) addr); +} + +static inline uint64_t read64(void *addr) { + return *((uint64_t *) addr); +} + +void Java_lab_galaxy_yahfa_HookMain_init(JNIEnv *env, jclass clazz, jint sdkVersion) { + int i; + SDKVersion = sdkVersion; + LOGI("init to SDK %d", sdkVersion); + switch (sdkVersion) { + case ANDROID_P: + kAccCompileDontBother = 0x02000000; + OFFSET_ArtMehod_in_Object = 0; + OFFSET_access_flags_in_ArtMethod = 4; + OFFSET_dex_method_index_in_ArtMethod = 4 * 3; + OFFSET_array_in_PointerArray = 0; + OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod = + roundUpToPtrSize(4 * 4 + 2 * 2) + pointer_size; + ArtMethodSize = roundUpToPtrSize(4 * 4 + 2 * 2) + pointer_size * 2; + break; + case ANDROID_O2: + kAccCompileDontBother = 0x02000000; + case ANDROID_O: + OFFSET_ArtMehod_in_Object = 0; + OFFSET_access_flags_in_ArtMethod = 4; + OFFSET_dex_method_index_in_ArtMethod = 4 * 3; + OFFSET_dex_cache_resolved_methods_in_ArtMethod = roundUpToPtrSize(4 * 4 + 2 * 2); + OFFSET_array_in_PointerArray = 0; + OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod = + roundUpToPtrSize(4 * 4 + 2 * 2) + pointer_size * 2; + ArtMethodSize = roundUpToPtrSize(4 * 4 + 2 * 2) + pointer_size * 3; + break; + case ANDROID_N2: + case ANDROID_N: + OFFSET_ArtMehod_in_Object = 0; + OFFSET_access_flags_in_ArtMethod = 4; // sizeof(GcRoot) = 4 + OFFSET_dex_method_index_in_ArtMethod = 4 * 3; + OFFSET_dex_cache_resolved_methods_in_ArtMethod = roundUpToPtrSize(4 * 4 + 2 * 2); + OFFSET_array_in_PointerArray = 0; + + // ptr_sized_fields_ is rounded up to pointer_size in ArtMethod + OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod = + roundUpToPtrSize(4 * 4 + 2 * 2) + pointer_size * 3; + + ArtMethodSize = roundUpToPtrSize(4 * 4 + 2 * 2) + pointer_size * 4; + break; + case ANDROID_M: + OFFSET_ArtMehod_in_Object = 0; + OFFSET_entry_point_from_interpreter_in_ArtMethod = roundUpToPtrSize(4 * 7); + OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod = + OFFSET_entry_point_from_interpreter_in_ArtMethod + pointer_size * 2; + OFFSET_dex_method_index_in_ArtMethod = 4 * 5; + OFFSET_dex_cache_resolved_methods_in_ArtMethod = 4; + OFFSET_array_in_PointerArray = 4 * 3; + ArtMethodSize = roundUpToPtrSize(4 * 7) + pointer_size * 3; + break; + case ANDROID_L2: + OFFSET_ArtMehod_in_Object = 4 * 2; + OFFSET_entry_point_from_interpreter_in_ArtMethod = roundUpToPtrSize( + OFFSET_ArtMehod_in_Object + 4 * 7); + OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod = + OFFSET_entry_point_from_interpreter_in_ArtMethod + pointer_size * 2; + OFFSET_dex_method_index_in_ArtMethod = OFFSET_ArtMehod_in_Object + 4 * 5; + OFFSET_dex_cache_resolved_methods_in_ArtMethod = OFFSET_ArtMehod_in_Object + 4; + OFFSET_array_in_PointerArray = 12; + ArtMethodSize = OFFSET_entry_point_from_interpreter_in_ArtMethod + pointer_size * 3; + break; + case ANDROID_L: + OFFSET_ArtMehod_in_Object = 4 * 2; + OFFSET_entry_point_from_interpreter_in_ArtMethod = OFFSET_ArtMehod_in_Object + 4 * 4; + OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod = + OFFSET_entry_point_from_interpreter_in_ArtMethod + 8 * 2; + OFFSET_dex_method_index_in_ArtMethod = + OFFSET_ArtMehod_in_Object + 4 * 4 + 8 * 4 + 4 * 2; + OFFSET_dex_cache_resolved_methods_in_ArtMethod = OFFSET_ArtMehod_in_Object + 4; + OFFSET_array_in_PointerArray = 12; + ArtMethodSize = OFFSET_ArtMehod_in_Object + 4 * 4 + 8 * 4 + 4 * 4; + break; + default: + LOGE("not compatible with SDK %d", sdkVersion); + break; + } + + setupTrampoline(); +} + +static void setNonCompilable(void *method) { + int access_flags = read32((char *) method + OFFSET_access_flags_in_ArtMethod); + LOGI("setNonCompilable: access flags is 0x%x", access_flags); + access_flags |= kAccCompileDontBother; + memcpy( + (char *) method + OFFSET_access_flags_in_ArtMethod, + &access_flags, + 4 + ); +} + +static int doBackupAndHook(JNIEnv *env, void *targetMethod, void *hookMethod, void *backupMethod) { + if (hookCount >= hookCap) { + LOGI("not enough capacity. Allocating..."); + if (doInitHookCap(DEFAULT_CAP)) { + LOGE("cannot hook method"); + return 1; + } + LOGI("Allocating done"); + } + + LOGI("target method is at %p, hook method is at %p, backup method is at %p", + targetMethod, hookMethod, backupMethod); + + + // set kAccCompileDontBother for a method we do not want the compiler to compile + // so that we don't need to worry about hotness_count_ + if (SDKVersion >= ANDROID_N) { + setNonCompilable(targetMethod); + setNonCompilable(hookMethod); + } + + if (backupMethod) {// do method backup + // have to copy the whole target ArtMethod here + // if the target method calls other methods which are to be resolved + // then ToDexPC would be invoked for the caller(origin method) + // in which case ToDexPC would use the entrypoint as a base for mapping pc to dex offset + // so any changes to the target method's entrypoint would result in a wrong dex offset + // and artQuickResolutionTrampoline would fail for methods called by the origin method + memcpy(backupMethod, targetMethod, ArtMethodSize); + } + + // replace entry point + void *newEntrypoint = genTrampoline(hookMethod); + LOGI("origin ep is %p, new ep is %p", + readAddr((char *) targetMethod + OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod), + newEntrypoint + ); + if (newEntrypoint) { + memcpy((char *) targetMethod + OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod, + &newEntrypoint, + pointer_size); + } else { + LOGE("failed to allocate space for trampoline of target method"); + return 1; + } + + if (OFFSET_entry_point_from_interpreter_in_ArtMethod != 0) { + memcpy((char *) targetMethod + OFFSET_entry_point_from_interpreter_in_ArtMethod, + (char *) hookMethod + OFFSET_entry_point_from_interpreter_in_ArtMethod, + pointer_size); + } + + // set the target method to native so that Android O wouldn't invoke it with interpreter + if (SDKVersion >= ANDROID_O) { + int access_flags = read32((char *) targetMethod + OFFSET_access_flags_in_ArtMethod); + access_flags |= kAccNative; + memcpy( + (char *) targetMethod + OFFSET_access_flags_in_ArtMethod, + &access_flags, + 4 + ); + LOGI("access flags is 0x%x", access_flags); + } + + LOGI("hook and backup done"); + hookCount += 1; + return 0; +} + +static void ensureMethodCached(void *hookMethod, void *backupMethod, + void *hookClassResolvedMethods) { + void *dexCacheResolvedMethods; + // then we get the dex method index of the static backup method + int methodIndex = read32( + (void *) ((char *) backupMethod + OFFSET_dex_method_index_in_ArtMethod)); + + LOGI("methodIndex = %d", methodIndex); + + // update the cached method manually + // first we find the array of cached methods + dexCacheResolvedMethods = hookClassResolvedMethods; + + if (!dexCacheResolvedMethods) { + LOGE("dexCacheResolvedMethods is null"); + return; + } + + // finally the addr of backup method is put at the corresponding location in cached methods array + if (SDKVersion >= ANDROID_O2) { + // array of MethodDexCacheType is used as dexCacheResolvedMethods in Android 8.1 + // struct: + // struct NativeDexCachePair = { T*, size_t idx } + // MethodDexCachePair = NativeDexCachePair = { ArtMethod*, size_t idx } + // MethodDexCacheType = std::atomic + memcpy((char *) dexCacheResolvedMethods + OFFSET_array_in_PointerArray + + pointer_size * 2 * methodIndex, + (&backupMethod), + pointer_size + ); + memcpy((char *) dexCacheResolvedMethods + OFFSET_array_in_PointerArray + + pointer_size * 2 * methodIndex + pointer_size, + &methodIndex, + pointer_size + ); + } else { + memcpy((char *) dexCacheResolvedMethods + OFFSET_array_in_PointerArray + + pointer_size * methodIndex, + (&backupMethod), + pointer_size); + } +} + +jobject Java_lab_galaxy_yahfa_HookMain_findMethodNative(JNIEnv *env, jclass clazz, + jclass targetClass, jstring methodName, + jstring methodSig) { + const char *c_methodName = (*env)->GetStringUTFChars(env, methodName, NULL); + const char *c_methodSig = (*env)->GetStringUTFChars(env, methodSig, NULL); + jobject ret = NULL; + + + //Try both GetMethodID and GetStaticMethodID -- Whatever works :) + jmethodID method = (*env)->GetMethodID(env, targetClass, c_methodName, c_methodSig); + if (!(*env)->ExceptionCheck(env)) { + ret = (*env)->ToReflectedMethod(env, targetClass, method, JNI_FALSE); + } else { + (*env)->ExceptionClear(env); + method = (*env)->GetStaticMethodID(env, targetClass, c_methodName, c_methodSig); + if (!(*env)->ExceptionCheck(env)) { + ret = (*env)->ToReflectedMethod(env, targetClass, method, JNI_TRUE); + } else { + (*env)->ExceptionClear(env); + } + } + + (*env)->ReleaseStringUTFChars(env, methodName, c_methodName); + (*env)->ReleaseStringUTFChars(env, methodSig, c_methodSig); + return ret; +} + +jboolean Java_lab_galaxy_yahfa_HookMain_backupAndHookNative(JNIEnv *env, jclass clazz, + jobject target, jobject hook, + jobject backup) { + + if (!doBackupAndHook(env, + (void *) (*env)->FromReflectedMethod(env, target), + (void *) (*env)->FromReflectedMethod(env, hook), + backup == NULL ? NULL : (void *) (*env)->FromReflectedMethod(env, backup) + )) { + (*env)->NewGlobalRef(env, + hook); // keep a global ref so that the hook method would not be GCed + return JNI_TRUE; + } else { + return JNI_FALSE; + } +} + +void Java_lab_galaxy_yahfa_HookMain_ensureMethodCached(JNIEnv *env, jclass clazz, + jobject hook, + jobject backup) { + ensureMethodCached((void *) (*env)->FromReflectedMethod(env, hook), + backup == NULL ? NULL : (void *) (*env)->FromReflectedMethod(env, backup), + getResolvedMethodsAddr(env, hook)); +} + +static void *getResolvedMethodsAddr(JNIEnv *env, jobject hook) { + // get backup class + jclass methodClass = (*env)->FindClass(env, "java/lang/reflect/Method"); + jmethodID getClassMid = (*env)->GetMethodID(env, methodClass, "getDeclaringClass", + "()Ljava/lang/Class;"); + jclass backupClass = (*env)->CallObjectMethod(env, hook, getClassMid); + // get dexCache of backup class + jclass classClass = (*env)->FindClass(env, "java/lang/Class"); + jfieldID dexCacheFid = (*env)->GetFieldID(env, classClass, "dexCache", "Ljava/lang/Object;"); + jobject dexCacheObj = (*env)->GetObjectField(env, backupClass, dexCacheFid); + // get resolvedMethods address + jclass dexCacheClass = (*env)->GetObjectClass(env, dexCacheObj); + if (SDKVersion >= ANDROID_N) { + jfieldID resolvedMethodsFid = (*env)->GetFieldID(env, dexCacheClass, "resolvedMethods", + "J"); + return (void *) (*env)->GetLongField(env, dexCacheObj, resolvedMethodsFid); + } else if (SDKVersion >= ANDROID_L) { + LOGE("this should has been done in java world: %d", SDKVersion); + return 0; + } else { + LOGE("not compatible with SDK %d", SDKVersion); + return 0; + } +} diff --git a/Core/jni/main/yahfa/HookMain.h b/Core/jni/main/yahfa/HookMain.h new file mode 100644 index 00000000..5768d79b --- /dev/null +++ b/Core/jni/main/yahfa/HookMain.h @@ -0,0 +1,21 @@ +#ifndef HOOK_MAIN_H +#define HOOK_MAIN_H + +#include + +void Java_lab_galaxy_yahfa_HookMain_init(JNIEnv *env, jclass clazz, jint sdkVersion); + +jobject Java_lab_galaxy_yahfa_HookMain_findMethodNative(JNIEnv *env, jclass clazz, + jclass targetClass, jstring methodName, jstring methodSig); + +jboolean Java_lab_galaxy_yahfa_HookMain_backupAndHookNative(JNIEnv *env, jclass clazz, + jobject target, jobject hook, + jobject backup); + +void Java_lab_galaxy_yahfa_HookMain_ensureMethodCached(JNIEnv *env, jclass clazz, + jobject hook, + jobject backup); + +static void* getResolvedMethodsAddr(JNIEnv*, jobject); + +#endif // HOOK_MAIN_H \ No newline at end of file diff --git a/Core/jni/main/yahfa/common.h b/Core/jni/main/yahfa/common.h new file mode 100644 index 00000000..11d0818e --- /dev/null +++ b/Core/jni/main/yahfa/common.h @@ -0,0 +1,29 @@ +// +// Created by liuruikai756 on 05/07/2017. +// +#include + +#ifndef YAHFA_COMMON_H +#define YAHFA_COMMON_H + +//#define DEBUG +//#define LOG_DISABLED + +#ifdef LOG_DISABLED +#define LOGI(...) +#define LOGW(...) +#define LOGE(...) +#else +#define LOG_TAG "EdXposed-YAHFA" +#ifdef DEBUG +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) +#else +#define LOGI(...) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) +#endif // DEBUG +#endif // LOG_DISABLED + +#endif //YAHFA_COMMON_H diff --git a/Core/jni/main/yahfa/env.h b/Core/jni/main/yahfa/env.h new file mode 100644 index 00000000..105c4586 --- /dev/null +++ b/Core/jni/main/yahfa/env.h @@ -0,0 +1,32 @@ +// +// Created by liuruikai756 on 05/07/2017. +// + +#ifndef YAHFA_ENV_H +#define YAHFA_ENV_H + +#define ANDROID_L 21 +#define ANDROID_L2 22 +#define ANDROID_M 23 +#define ANDROID_N 24 +#define ANDROID_N2 25 +#define ANDROID_O 26 +#define ANDROID_O2 27 +#define ANDROID_P 28 + +#define roundUpTo4(v) ((v+4-1) - ((v+4-1)&3)) +#define roundUpTo8(v) ((v+8-1) - ((v+8-1)&7)) + +#if defined(__i386__) || defined(__arm__) +#define pointer_size 4 +#define readAddr(addr) read32(addr) +#define roundUpToPtrSize(x) roundUpTo4(x) +#elif defined(__aarch64__) || defined(__x86_64__) +#define pointer_size 8 +#define readAddr(addr) read64(addr) +#define roundUpToPtrSize(x) roundUpTo8(x) +#else +#error Unsupported architecture +#endif + +#endif //YAHFA_ENV_H diff --git a/Core/jni/main/yahfa/trampoline.c b/Core/jni/main/yahfa/trampoline.c new file mode 100644 index 00000000..8b8daebf --- /dev/null +++ b/Core/jni/main/yahfa/trampoline.c @@ -0,0 +1,131 @@ +// +// Created by liuruikai756 on 05/07/2017. +// +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "env.h" +#include "trampoline.h" + +static unsigned char *trampolineCode; // place where trampolines are saved +static unsigned int trampolineSize; // trampoline size required for each hook + +unsigned int hookCap = 0; +unsigned int hookCount = 0; + +// trampoline: +// 1. set eax/r0/x0 to the hook ArtMethod addr +// 2. jump into its entry point +#if defined(__i386__) +// b8 78 56 34 12 ; mov eax, 0x12345678 (addr of the hook method) +// ff 70 20 ; push DWORD PTR [eax + 0x20] +// c3 ; ret +unsigned char trampoline[] = { + 0xb8, 0x78, 0x56, 0x34, 0x12, + 0xff, 0x70, 0x20, + 0xc3 +}; + +#elif defined(__x86_64__) +// 48 bf 78 56 34 12 78 56 34 12 ; movabs rdi, 0x1234567812345678 +// ff 77 20 ; push QWORD PTR [rdi + 0x20] +// c3 ; ret +unsigned char trampoline[] = { + 0x48, 0xbf, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, + 0xff, 0x77, 0x20, + 0xc3 +}; + +#elif defined(__arm__) +// 00 00 9F E5 ; ldr r0, [pc, #0] +// 20 F0 90 E5 ; ldr pc, [r0, 0x20] +// 78 56 34 12 ; 0x12345678 (addr of the hook method) +unsigned char trampoline[] = { + 0x00, 0x00, 0x9f, 0xe5, + 0x20, 0xf0, 0x90, 0xe5, + 0x78, 0x56, 0x34, 0x12 +}; + +#elif defined(__aarch64__) +// 60 00 00 58 ; ldr x0, 12 +// 10 00 40 F8 ; ldr x16, [x0, #0x00] +// 00 02 1f d6 ; br x16 +// 78 56 34 12 +// 89 67 45 23 ; 0x2345678912345678 (addr of the hook method) +unsigned char trampoline[] = { + 0x60, 0x00, 0x00, 0x58, + 0x10, 0x00, 0x40, 0xf8, + 0x00, 0x02, 0x1f, 0xd6, + 0x78, 0x56, 0x34, 0x12, + 0x89, 0x67, 0x45, 0x23 +}; +#endif +static unsigned int trampolineSize = roundUpToPtrSize(sizeof(trampoline)); + +void *genTrampoline(void *hookMethod) { + void *targetAddr; + + targetAddr = trampolineCode + trampolineSize * hookCount; + memcpy(targetAddr, trampoline, + sizeof(trampoline)); // do not use trampolineSize since it's a rounded size + + // replace with the actual ArtMethod addr +#if defined(__i386__) + memcpy(targetAddr+1, &hookMethod, pointer_size); + +#elif defined(__x86_64__) + memcpy((char*)targetAddr + 2, &hookMethod, pointer_size); + +#elif defined(__arm__) + memcpy(targetAddr+8, &hookMethod, pointer_size); + +#elif defined(__aarch64__) + memcpy(targetAddr + 12, &hookMethod, pointer_size); +#endif + + return targetAddr; +} + +void setupTrampoline() { +#if defined(__i386__) + trampoline[7] = (unsigned char)OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod; +#elif defined(__x86_64__) + trampoline[12] = (unsigned char)OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod; +#elif defined(__arm__) + trampoline[4] = (unsigned char)OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod; +#elif defined(__aarch64__) + trampoline[5] |= + ((unsigned char) OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod) << 4; + trampoline[6] |= + ((unsigned char) OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod) >> 4; +#endif +} + +int doInitHookCap(unsigned int cap) { + if (cap == 0) { + LOGE("invalid capacity: %d", cap); + return 1; + } + if (hookCap) { + LOGI("allocating new space for trampoline code"); + } + unsigned int allSize = trampolineSize * cap; + unsigned char *buf = mmap(NULL, allSize, PROT_READ | PROT_WRITE | PROT_EXEC, + MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); + if (buf == MAP_FAILED) { + LOGE("mmap failed, errno = %s", strerror(errno)); + return 1; + } + hookCap = cap; + hookCount = 0; + trampolineCode = buf; + return 0; +} diff --git a/Core/jni/main/yahfa/trampoline.h b/Core/jni/main/yahfa/trampoline.h new file mode 100644 index 00000000..ba7a46b3 --- /dev/null +++ b/Core/jni/main/yahfa/trampoline.h @@ -0,0 +1,22 @@ +// +// Created by liuruikai756 on 05/07/2017. +// + +#ifndef YAHFA_TAMPOLINE_H +#define YAHFA_TAMPOLINE_H + +extern int SDKVersion; +extern int OFFSET_entry_point_from_quick_compiled_code_in_ArtMethod; + +extern unsigned int hookCap; // capacity for trampolines +extern unsigned int hookCount; // current count of used trampolines + +extern unsigned char trampoline[]; + +int doInitHookCap(unsigned int cap); +void setupTrampoline(); +void *genTrampoline(void *hookMethod); + +#define DEFAULT_CAP 100 //size of each trampoline area would be no more than 4k Bytes(one page) + +#endif //YAHFA_TAMPOLINE_H diff --git a/Core/src/main/AndroidManifest.xml b/Core/src/main/AndroidManifest.xml new file mode 100644 index 00000000..6dcea510 --- /dev/null +++ b/Core/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/Core/template_override/.gitattributes b/Core/template_override/.gitattributes new file mode 100644 index 00000000..8980df1a --- /dev/null +++ b/Core/template_override/.gitattributes @@ -0,0 +1,8 @@ +# Declare files that will always have LF line endings on checkout. +META-INF/** text eol=lf +*.prop text eol=lf +*.sh text eol=lf +*.md text eol=lf + +# Denote all files that are truly binary and should not be modified. +system/** binary diff --git a/Core/template_override/META-INF/com/google/android/update-binary b/Core/template_override/META-INF/com/google/android/update-binary new file mode 100644 index 00000000..f7e3cc1c --- /dev/null +++ b/Core/template_override/META-INF/com/google/android/update-binary @@ -0,0 +1,159 @@ +#!/sbin/sh +########################################################################################## +# +# Magisk Module Template Install Script +# by topjohnwu +# +########################################################################################## + +TMPDIR=/dev/tmp +INSTALLER=$TMPDIR/install +# Always mount under tmp +MOUNTPATH=$TMPDIR/magisk_img + +# Default permissions +umask 022 + +# Initial cleanup +rm -rf $TMPDIR 2>/dev/null +mkdir -p $INSTALLER + +# echo before loading util_functions +ui_print() { echo "$1"; } + +require_new_magisk() { + ui_print "*******************************" + ui_print " Please install Magisk v17.0+! " + ui_print "*******************************" + exit 1 +} + +########################################################################################## +# Environment +########################################################################################## + +OUTFD=$2 +ZIP=$3 + +mount /data 2>/dev/null + +# Load utility functions +if [ -f /data/adb/magisk/util_functions.sh ]; then + . /data/adb/magisk/util_functions.sh +elif [ -f /data/magisk/util_functions.sh ]; then + NVBASE=/data + . /data/magisk/util_functions.sh +else + require_new_magisk +fi + +# Use alternative image if in BOOTMODE +$BOOTMODE && IMG=$NVBASE/magisk_merge.img + +# Preperation for flashable zips +setup_flashable + +# Mount partitions +mount_partitions + +# Detect version and architecture +api_level_arch_detect + +# You can get the Android API version from $API, the CPU architecture from $ARCH +# Useful if you are creating Android version / platform dependent mods + +# Setup busybox and binaries +$BOOTMODE && boot_actions || recovery_actions + +########################################################################################## +# Preparation +########################################################################################## + +# Extract common files +unzip -o "$ZIP" module.prop config.sh 'common/*' -d $INSTALLER >&2 + +[ ! -f $INSTALLER/config.sh ] && abort "! Unable to extract zip file!" +# Load configurations +. $INSTALLER/config.sh + +# Check architecture +check_architecture + +# Check the installed magisk version +MIN_VER=`grep_prop minMagisk $INSTALLER/module.prop` +[ ! -z $MAGISK_VER_CODE -a $MAGISK_VER_CODE -ge $MIN_VER ] || require_new_magisk +MODID=`grep_prop id $INSTALLER/module.prop` +MODPATH=$MOUNTPATH/$MODID + +# Print mod name +print_modname + +# Please leave this message in your flashable zip for credits :) +ui_print "******************************" +ui_print "Powered by Magisk (@topjohnwu)" +ui_print "******************************" + +########################################################################################## +# Install +########################################################################################## + +# Get the variable reqSizeM. Use your own method to determine reqSizeM if needed +request_zip_size_check "$ZIP" + +# This function will mount $IMG to $MOUNTPATH, and resize the image based on $reqSizeM +mount_magisk_img + +# Create mod paths +rm -rf $MODPATH 2>/dev/null +mkdir -p $MODPATH + +# Extract files to system. Use your own method if needed +ui_print "- Extracting module files" +unzip -o "$ZIP" 'system/*' -d $MODPATH >&2 + +# Remove placeholder +rm -f $MODPATH/system/placeholder 2>/dev/null + +# Extra copy file function +copy_files + +# Handle replace folders +for TARGET in $REPLACE; do + mktouch $MODPATH$TARGET/.replace +done + +# Auto Mount +$AUTOMOUNT && touch $MODPATH/auto_mount + +# prop files +$PROPFILE && cp -af $INSTALLER/common/system.prop $MODPATH/system.prop + +# Module info +cp -af $INSTALLER/module.prop $MODPATH/module.prop +if $BOOTMODE; then + # Update info for Magisk Manager + mktouch /sbin/.core/img/$MODID/update + cp -af $INSTALLER/module.prop /sbin/.core/img/$MODID/module.prop +fi + +# post-fs-data mode scripts +$POSTFSDATA && cp -af $INSTALLER/common/post-fs-data.sh $MODPATH/post-fs-data.sh + +# service mode scripts +$LATESTARTSERVICE && cp -af $INSTALLER/common/service.sh $MODPATH/service.sh + +ui_print "- Setting permissions" +set_permissions + +########################################################################################## +# Finalizing +########################################################################################## + +# Unmount magisk image and shrink if possible +unmount_magisk_img + +$BOOTMODE || recovery_cleanup +rm -rf $TMPDIR + +ui_print "- Done" +exit 0 diff --git a/Core/template_override/META-INF/com/google/android/updater-script b/Core/template_override/META-INF/com/google/android/updater-script new file mode 100644 index 00000000..11d5c96e --- /dev/null +++ b/Core/template_override/META-INF/com/google/android/updater-script @@ -0,0 +1 @@ +#MAGISK diff --git a/Core/template_override/common/post-fs-data.sh b/Core/template_override/common/post-fs-data.sh new file mode 100644 index 00000000..7dc438fd --- /dev/null +++ b/Core/template_override/common/post-fs-data.sh @@ -0,0 +1,20 @@ +#!/system/bin/sh +# Please don't hardcode /magisk/modname/... ; instead, please use $MODDIR/... +# This will make your scripts compatible even if Magisk change its mount point in the future +MODDIR=${0%/*} + +# This script will be executed in post-fs-data mode +# More info in the main Magisk thread + +# necessary for using mmap in system_server process +supolicy --live "allow system_server system_server process {execmem}" +# supolicy --live "allow system_server system_server memprotect {mmap_zero}" + +# for built-in apps // TODO maybe narrow down the target classes +supolicy --live "allow coredomain coredomain process {execmem}" + +# read configs set in our app +supolicy --live "allow coredomain app_data_file * *" + +# read module apk file in zygote +supolicy --live "allow zygote apk_data_file * *" diff --git a/Core/template_override/common/service.sh b/Core/template_override/common/service.sh new file mode 100644 index 00000000..45124171 --- /dev/null +++ b/Core/template_override/common/service.sh @@ -0,0 +1,7 @@ +#!/system/bin/sh +# Please don't hardcode /magisk/modname/... ; instead, please use $MODDIR/... +# This will make your scripts compatible even if Magisk change its mount point in the future +MODDIR=${0%/*} + +# This script will be executed in late_start service mode +# More info in the main Magisk thread diff --git a/Core/template_override/common/system.prop b/Core/template_override/common/system.prop new file mode 100644 index 00000000..150cf23f --- /dev/null +++ b/Core/template_override/common/system.prop @@ -0,0 +1 @@ +# dalvik.vm.dex2oat-filter=speed \ No newline at end of file diff --git a/Core/template_override/config.sh b/Core/template_override/config.sh new file mode 100644 index 00000000..dbfdd29a --- /dev/null +++ b/Core/template_override/config.sh @@ -0,0 +1,131 @@ +########################################################################################## +# +# Magisk Module Template Config Script +# by topjohnwu +# +########################################################################################## +########################################################################################## +# +# Instructions: +# +# 1. Place your files into system folder (delete the placeholder file) +# 2. Fill in your module's info into module.prop +# 3. Configure the settings in this file (config.sh) +# 4. If you need boot scripts, add them into common/post-fs-data.sh or common/service.sh +# 5. Add your additional or modified system properties into common/system.prop +# +########################################################################################## + +########################################################################################## +# Configs +########################################################################################## + +# Set to true if you need to enable Magic Mount +# Most mods would like it to be enabled +AUTOMOUNT=true + +# Set to true if you need to load system.prop +PROPFILE=false + +# Set to true if you need post-fs-data script +POSTFSDATA=true + +# Set to true if you need late_start service script +LATESTARTSERVICE=false + +########################################################################################## +# Installation Message +########################################################################################## + +# Set what you want to show when installing your mod + +print_modname() { + ui_print "************************************" + ui_print " Riru - Ed Xposed v0.2.6 " + ui_print "************************************" +} + +########################################################################################## +# Replace list +########################################################################################## + +# List all directories you want to directly replace in the system +# Check the documentations for more info about how Magic Mount works, and why you need this + +# This is an example +REPLACE=" +/system/app/Youtube +/system/priv-app/SystemUI +/system/priv-app/Settings +/system/framework +" + +# Construct your own list here, it will override the example above +# !DO NOT! remove this if you don't need to replace anything, leave it empty as it is now +REPLACE=" +" + +########################################################################################## +# Permissions +########################################################################################## + +set_permissions() { + # Only some special files require specific permissions + # The default permissions should be good enough for most cases + + # Here are some examples for the set_perm functions: + + # set_perm_recursive (default: u:object_r:system_file:s0) + # set_perm_recursive $MODPATH/system/lib 0 0 0755 0644 + + # set_perm (default: u:object_r:system_file:s0) + # set_perm $MODPATH/system/bin/app_process32 0 2000 0755 u:object_r:zygote_exec:s0 + # set_perm $MODPATH/system/bin/dex2oat 0 2000 0755 u:object_r:dex2oat_exec:s0 + # set_perm $MODPATH/system/lib/libart.so 0 0 0644 + + # The following is default permissions, DO NOT remove + set_perm_recursive $MODPATH 0 0 0755 0644 +} + +########################################################################################## +# Custom Functions +########################################################################################## + +# This file (config.sh) will be sourced by the main flash script after util_functions.sh +# If you need custom logic, please add them here as functions, and call these functions in +# update-binary. Refrain from adding code directly into update-binary, as it will make it +# difficult for you to migrate your modules to newer template versions. +# Make update-binary as clean as possible, try to only do function calls in it. +fail() { + echo "$1" + exit 1 +} + +check_architecture() { + if [[ "$ARCH" != "arm" && "$ARCH" != "arm64" ]]; then + ui_print "- Unsupported platform: $ARCH" + exit 1 + else + ui_print "- Device platform: $ARCH" + fi +} + +copy_files() { + if [ $IS64BIT = false ]; then + ui_print "- Removing unnecessary files" + rm -rf "$MODPATH/system/lib64" + fi + + ui_print "- Extracting extra files" + unzip -o "$ZIP" 'data/*' -d $MODPATH >&2 + + TARGET="/data/misc/riru/modules" + + # TODO: do not overwrite if file exists + [ -d $TARGET ] || mkdir -p $TARGET || fail "- Can't mkdir -p $TARGET" + cp -af "$MODPATH$TARGET/." "$TARGET" || fail "- Can't cp -af $MODPATH$TARGET/. $TARGET" + + rm -rf $MODPATH/data 2>/dev/null + + ui_print "- Files copied" +} \ No newline at end of file diff --git a/Core/template_override/module.prop b/Core/template_override/module.prop new file mode 100644 index 00000000..a705405b --- /dev/null +++ b/Core/template_override/module.prop @@ -0,0 +1,7 @@ +id=riru_edxposed +name=Riru - Ed Xposed +version=v0.2.6_beta +versionCode=1 +author=givein2u +description=Magisk version of Xposed. Require Riru - Core installed. +minMagisk=17000 diff --git a/Core/template_override/riru_module.prop b/Core/template_override/riru_module.prop new file mode 100644 index 00000000..4ddae6e3 --- /dev/null +++ b/Core/template_override/riru_module.prop @@ -0,0 +1,5 @@ +name=Ed Xposed +version=v0.2.6_beta +versionCode=1 +author=givein2u +description=Elder driver Xposed for Android Pie. Require Riru - Core installed. \ No newline at end of file diff --git a/Core/template_override/system/framework/edconfig.dex b/Core/template_override/system/framework/edconfig.dex new file mode 100644 index 00000000..e7bf7a20 --- /dev/null +++ b/Core/template_override/system/framework/edconfig.dex @@ -0,0 +1,5 @@ +version=90.0-beta-0.2.6 (by Elder Driver) +arch=arm64 +minsdk=23 +maxsdk=28 +requires:fbe_aware=1 diff --git a/Core/template_override/system/framework/eddalvikdx.dex b/Core/template_override/system/framework/eddalvikdx.dex new file mode 100644 index 0000000000000000000000000000000000000000..d733ee640d74e2149bca10f8683be0920749b694 GIT binary patch literal 869496 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 z�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 E*zLnlixpYlW%`eA>OP}y|iWb+qM;G4+dD9-t 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 z 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 G^8Wx^teO}A literal 0 HcmV?d00001 diff --git a/Core/template_override/system/framework/eddexmaker.dex b/Core/template_override/system/framework/eddexmaker.dex new file mode 100644 index 0000000000000000000000000000000000000000..b6fd1bc1a8b680ae0c555dff33584f09f02ef1be GIT binary patch literal 75744 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 []" + exit 1 +fi + +if [ ! -d "$MODULE_NAME" ]; then + echo "$MODULE_NAME not exists" + exit 1 +fi + +VERSION=$2 +[[ "$VERSION" == "" ]] && VERSION=v1 + +ZIP_NAME_PREFIX=$3 + +LIBS_OUTPUT=$MODULE_NAME/build/ndkBuild/libs +NDK_OUT=$MODULE_NAME/build/ndkBuild/obj + +# build +NDK_BUILD=ndk-build +[[ "$OSTYPE" == "msys" ]] && NDK_BUILD=ndk-build.cmd +[[ "$OSTYPE" == "cygwin" ]] && NDK_BUILD=ndk-build.cmd + +(cd $MODULE_NAME; $NDK_BUILD NDK_LIBS_OUT=build/ndkBuild/libs NDK_OUT=build/ndkBuild/obj) + +# elf cleaner +function run_elf_cleaner { + for file in $1/* + do + if [ -f $file ]; then + clean_elf $file > /dev/null + fi + done +} + +if [ -f elf-cleaner.sh ]; then + source elf-cleaner.sh + run_elf_cleaner $LIBS_OUTPUT/arm64-v8a + run_elf_cleaner $LIBS_OUTPUT/armeabi-v7a +fi + +# create tmp dir +TMP_DIR=build/zip +TMP_DIR_MAGISK=$TMP_DIR/magisk + +rm -rf $TMP_DIR +mkdir -p $TMP_DIR + +# copy files +mkdir -p $TMP_DIR_MAGISK/system/lib64 +mkdir -p $TMP_DIR_MAGISK/system/lib +cp -a $LIBS_OUTPUT/arm64-v8a/. $TMP_DIR_MAGISK/system/lib64 +cp -a $LIBS_OUTPUT/armeabi-v7a/. $TMP_DIR_MAGISK/system/lib + +# run custom script +if [ -f $MODULE_NAME/build-module.sh ]; then + source $MODULE_NAME/build-module.sh + copy_files +fi + +# zip +mkdir -p $MODULE_NAME/release +ZIP_NAME=magisk-$ZIP_NAME_PREFIX-arm-arm64-"$VERSION".zip +rm -f $MODULE_NAME/release/$ZIP_NAME +rm -f $TMP_DIR_MAGISK/$ZIP_NAME +(cd $TMP_DIR_MAGISK; zip -r $ZIP_NAME * > /dev/null) +mv $TMP_DIR_MAGISK/$ZIP_NAME $MODULE_NAME/release/$ZIP_NAME + +# clean tmp dir +rm -rf $TMP_DIR diff --git a/dalvikdx/.gitignore b/dalvikdx/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/dalvikdx/.gitignore @@ -0,0 +1 @@ +/build diff --git a/dalvikdx/build.gradle b/dalvikdx/build.gradle new file mode 100644 index 00000000..68bb7760 --- /dev/null +++ b/dalvikdx/build.gradle @@ -0,0 +1,8 @@ +apply plugin: 'java-library' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) +} + +sourceCompatibility = "7" +targetCompatibility = "7" diff --git a/dalvikdx/src/main/java/external/com/android/dex/Annotation.java b/dalvikdx/src/main/java/external/com/android/dex/Annotation.java new file mode 100644 index 00000000..80c639fd --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/Annotation.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import static external.com.android.dex.EncodedValueReader.ENCODED_ANNOTATION; + +/** + * An annotation. + */ +public final class Annotation implements Comparable { + private final Dex dex; + private final byte visibility; + private final EncodedValue encodedAnnotation; + + public Annotation(Dex dex, byte visibility, EncodedValue encodedAnnotation) { + this.dex = dex; + this.visibility = visibility; + this.encodedAnnotation = encodedAnnotation; + } + + public byte getVisibility() { + return visibility; + } + + public EncodedValueReader getReader() { + return new EncodedValueReader(encodedAnnotation, ENCODED_ANNOTATION); + } + + public int getTypeIndex() { + EncodedValueReader reader = getReader(); + reader.readAnnotation(); + return reader.getAnnotationType(); + } + + public void writeTo(Dex.Section out) { + out.writeByte(visibility); + encodedAnnotation.writeTo(out); + } + + @Override + public int compareTo(Annotation other) { + return encodedAnnotation.compareTo(other.encodedAnnotation); + } + + @Override + public String toString() { + return dex == null + ? visibility + " " + getTypeIndex() + : visibility + " " + dex.typeNames().get(getTypeIndex()); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/CallSiteId.java b/dalvikdx/src/main/java/external/com/android/dex/CallSiteId.java new file mode 100644 index 00000000..73f5b2b2 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/CallSiteId.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.Dex.Section; +import external.com.android.dex.util.Unsigned; + +/** + * A call_site_id_item: https://source.android.com/devices/tech/dalvik/dex-format#call-site-id-item + */ +public class CallSiteId implements Comparable { + + private final Dex dex; + private final int offset; + + public CallSiteId(Dex dex, int offset) { + this.dex = dex; + this.offset = offset; + } + + @Override + public int compareTo(CallSiteId o) { + return Unsigned.compare(offset, o.offset); + } + + public int getCallSiteOffset() { + return offset; + } + + public void writeTo(Section out) { + out.writeInt(offset); + } + + @Override + public String toString() { + if (dex == null) { + return String.valueOf(offset); + } + return dex.protoIds().get(offset).toString(); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/ClassData.java b/dalvikdx/src/main/java/external/com/android/dex/ClassData.java new file mode 100644 index 00000000..303b0120 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/ClassData.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +public final class ClassData { + private final Field[] staticFields; + private final Field[] instanceFields; + private final Method[] directMethods; + private final Method[] virtualMethods; + + public ClassData(Field[] staticFields, Field[] instanceFields, + Method[] directMethods, Method[] virtualMethods) { + this.staticFields = staticFields; + this.instanceFields = instanceFields; + this.directMethods = directMethods; + this.virtualMethods = virtualMethods; + } + + public Field[] getStaticFields() { + return staticFields; + } + + public Field[] getInstanceFields() { + return instanceFields; + } + + public Method[] getDirectMethods() { + return directMethods; + } + + public Method[] getVirtualMethods() { + return virtualMethods; + } + + public Field[] allFields() { + Field[] result = new Field[staticFields.length + instanceFields.length]; + System.arraycopy(staticFields, 0, result, 0, staticFields.length); + System.arraycopy(instanceFields, 0, result, staticFields.length, instanceFields.length); + return result; + } + + public Method[] allMethods() { + Method[] result = new Method[directMethods.length + virtualMethods.length]; + System.arraycopy(directMethods, 0, result, 0, directMethods.length); + System.arraycopy(virtualMethods, 0, result, directMethods.length, virtualMethods.length); + return result; + } + + public static class Field { + private final int fieldIndex; + private final int accessFlags; + + public Field(int fieldIndex, int accessFlags) { + this.fieldIndex = fieldIndex; + this.accessFlags = accessFlags; + } + + public int getFieldIndex() { + return fieldIndex; + } + + public int getAccessFlags() { + return accessFlags; + } + } + + public static class Method { + private final int methodIndex; + private final int accessFlags; + private final int codeOffset; + + public Method(int methodIndex, int accessFlags, int codeOffset) { + this.methodIndex = methodIndex; + this.accessFlags = accessFlags; + this.codeOffset = codeOffset; + } + + public int getMethodIndex() { + return methodIndex; + } + + public int getAccessFlags() { + return accessFlags; + } + + public int getCodeOffset() { + return codeOffset; + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/ClassDef.java b/dalvikdx/src/main/java/external/com/android/dex/ClassDef.java new file mode 100644 index 00000000..465fd489 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/ClassDef.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +/** + * A type definition. + */ +public final class ClassDef { + public static final int NO_INDEX = -1; + private final Dex buffer; + private final int offset; + private final int typeIndex; + private final int accessFlags; + private final int supertypeIndex; + private final int interfacesOffset; + private final int sourceFileIndex; + private final int annotationsOffset; + private final int classDataOffset; + private final int staticValuesOffset; + + public ClassDef(Dex buffer, int offset, int typeIndex, int accessFlags, + int supertypeIndex, int interfacesOffset, int sourceFileIndex, + int annotationsOffset, int classDataOffset, int staticValuesOffset) { + this.buffer = buffer; + this.offset = offset; + this.typeIndex = typeIndex; + this.accessFlags = accessFlags; + this.supertypeIndex = supertypeIndex; + this.interfacesOffset = interfacesOffset; + this.sourceFileIndex = sourceFileIndex; + this.annotationsOffset = annotationsOffset; + this.classDataOffset = classDataOffset; + this.staticValuesOffset = staticValuesOffset; + } + + public int getOffset() { + return offset; + } + + public int getTypeIndex() { + return typeIndex; + } + + public int getSupertypeIndex() { + return supertypeIndex; + } + + public int getInterfacesOffset() { + return interfacesOffset; + } + + public short[] getInterfaces() { + return buffer.readTypeList(interfacesOffset).getTypes(); + } + + public int getAccessFlags() { + return accessFlags; + } + + public int getSourceFileIndex() { + return sourceFileIndex; + } + + public int getAnnotationsOffset() { + return annotationsOffset; + } + + public int getClassDataOffset() { + return classDataOffset; + } + + public int getStaticValuesOffset() { + return staticValuesOffset; + } + + @Override + public String toString() { + if (buffer == null) { + return typeIndex + " " + supertypeIndex; + } + + StringBuilder result = new StringBuilder(); + result.append(buffer.typeNames().get(typeIndex)); + if (supertypeIndex != NO_INDEX) { + result.append(" extends ").append(buffer.typeNames().get(supertypeIndex)); + } + return result.toString(); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/Code.java b/dalvikdx/src/main/java/external/com/android/dex/Code.java new file mode 100644 index 00000000..52c7c8f2 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/Code.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +public final class Code { + private final int registersSize; + private final int insSize; + private final int outsSize; + private final int debugInfoOffset; + private final short[] instructions; + private final Try[] tries; + private final CatchHandler[] catchHandlers; + + public Code(int registersSize, int insSize, int outsSize, int debugInfoOffset, + short[] instructions, Try[] tries, CatchHandler[] catchHandlers) { + this.registersSize = registersSize; + this.insSize = insSize; + this.outsSize = outsSize; + this.debugInfoOffset = debugInfoOffset; + this.instructions = instructions; + this.tries = tries; + this.catchHandlers = catchHandlers; + } + + public int getRegistersSize() { + return registersSize; + } + + public int getInsSize() { + return insSize; + } + + public int getOutsSize() { + return outsSize; + } + + public int getDebugInfoOffset() { + return debugInfoOffset; + } + + public short[] getInstructions() { + return instructions; + } + + public Try[] getTries() { + return tries; + } + + public CatchHandler[] getCatchHandlers() { + return catchHandlers; + } + + public static class Try { + final int startAddress; + final int instructionCount; + final int catchHandlerIndex; + + Try(int startAddress, int instructionCount, int catchHandlerIndex) { + this.startAddress = startAddress; + this.instructionCount = instructionCount; + this.catchHandlerIndex = catchHandlerIndex; + } + + public int getStartAddress() { + return startAddress; + } + + public int getInstructionCount() { + return instructionCount; + } + + /** + * Returns this try's catch handler index. Note that + * this is distinct from the its catch handler offset. + */ + public int getCatchHandlerIndex() { + return catchHandlerIndex; + } + } + + public static class CatchHandler { + final int[] typeIndexes; + final int[] addresses; + final int catchAllAddress; + final int offset; + + public CatchHandler(int[] typeIndexes, int[] addresses, int catchAllAddress, int offset) { + this.typeIndexes = typeIndexes; + this.addresses = addresses; + this.catchAllAddress = catchAllAddress; + this.offset = offset; + } + + public int[] getTypeIndexes() { + return typeIndexes; + } + + public int[] getAddresses() { + return addresses; + } + + public int getCatchAllAddress() { + return catchAllAddress; + } + + public int getOffset() { + return offset; + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/Dex.java b/dalvikdx/src/main/java/external/com/android/dex/Dex.java new file mode 100644 index 00000000..ca49ffc8 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/Dex.java @@ -0,0 +1,819 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.Code.CatchHandler; +import external.com.android.dex.Code.Try; +import external.com.android.dex.MethodHandle.MethodHandleType; +import external.com.android.dex.util.ByteInput; +import external.com.android.dex.util.ByteOutput; +import external.com.android.dex.util.FileUtils; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UTFDataFormatException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.AbstractList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.RandomAccess; +import java.util.zip.Adler32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * The bytes of a dex file in memory for reading and writing. All int offsets + * are unsigned. + */ +public final class Dex { + private static final int CHECKSUM_OFFSET = 8; + private static final int CHECKSUM_SIZE = 4; + private static final int SIGNATURE_OFFSET = CHECKSUM_OFFSET + CHECKSUM_SIZE; + private static final int SIGNATURE_SIZE = 20; + // Provided as a convenience to avoid a memory allocation to benefit Dalvik. + // Note: libcore.util.EmptyArray cannot be accessed when this code isn't run on Dalvik. + static final short[] EMPTY_SHORT_ARRAY = new short[0]; + + private ByteBuffer data; + private final TableOfContents tableOfContents = new TableOfContents(); + private int nextSectionStart = 0; + private final StringTable strings = new StringTable(); + private final TypeIndexToDescriptorIndexTable typeIds = new TypeIndexToDescriptorIndexTable(); + private final TypeIndexToDescriptorTable typeNames = new TypeIndexToDescriptorTable(); + private final ProtoIdTable protoIds = new ProtoIdTable(); + private final FieldIdTable fieldIds = new FieldIdTable(); + private final MethodIdTable methodIds = new MethodIdTable(); + + /** + * Creates a new dex that reads from {@code data}. It is an error to modify + * {@code data} after using it to create a dex buffer. + */ + public Dex(byte[] data) throws IOException { + this(ByteBuffer.wrap(data)); + } + + private Dex(ByteBuffer data) throws IOException { + this.data = data; + this.data.order(ByteOrder.LITTLE_ENDIAN); + this.tableOfContents.readFrom(this); + } + + /** + * Creates a new empty dex of the specified size. + */ + public Dex(int byteCount) throws IOException { + this.data = ByteBuffer.wrap(new byte[byteCount]); + this.data.order(ByteOrder.LITTLE_ENDIAN); + } + + /** + * Creates a new dex buffer of the dex in {@code in}, and closes {@code in}. + */ + public Dex(InputStream in) throws IOException { + try { + loadFrom(in); + } finally { + in.close(); + } + } + + /** + * Creates a new dex buffer from the dex file {@code file}. + */ + public Dex(File file) throws IOException { + if (FileUtils.hasArchiveSuffix(file.getName())) { + ZipFile zipFile = new ZipFile(file); + ZipEntry entry = zipFile.getEntry(DexFormat.DEX_IN_JAR_NAME); + if (entry != null) { + try (InputStream inputStream = zipFile.getInputStream(entry)) { + loadFrom(inputStream); + } + zipFile.close(); + } else { + throw new DexException("Expected " + DexFormat.DEX_IN_JAR_NAME + " in " + file); + } + } else if (file.getName().endsWith(".dex")) { + try (InputStream inputStream = new FileInputStream(file)) { + loadFrom(inputStream); + } + } else { + throw new DexException("unknown output extension: " + file); + } + } + + /** + * It is the caller's responsibility to close {@code in}. + */ + private void loadFrom(InputStream in) throws IOException { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + + int count; + while ((count = in.read(buffer)) != -1) { + bytesOut.write(buffer, 0, count); + } + + this.data = ByteBuffer.wrap(bytesOut.toByteArray()); + this.data.order(ByteOrder.LITTLE_ENDIAN); + this.tableOfContents.readFrom(this); + } + + private static void checkBounds(int index, int length) { + if (index < 0 || index >= length) { + throw new IndexOutOfBoundsException("index:" + index + ", length=" + length); + } + } + + public void writeTo(OutputStream out) throws IOException { + byte[] buffer = new byte[8192]; + ByteBuffer data = this.data.duplicate(); // positioned ByteBuffers aren't thread safe + data.clear(); + while (data.hasRemaining()) { + int count = Math.min(buffer.length, data.remaining()); + data.get(buffer, 0, count); + out.write(buffer, 0, count); + } + } + + public void writeTo(File dexOut) throws IOException { + try (OutputStream out = new FileOutputStream(dexOut)) { + writeTo(out); + } + } + + public TableOfContents getTableOfContents() { + return tableOfContents; + } + + public Section open(int position) { + if (position < 0 || position >= data.capacity()) { + throw new IllegalArgumentException("position=" + position + + " length=" + data.capacity()); + } + ByteBuffer sectionData = data.duplicate(); + sectionData.order(ByteOrder.LITTLE_ENDIAN); // necessary? + sectionData.position(position); + sectionData.limit(data.capacity()); + return new Section("section", sectionData); + } + + public Section appendSection(int maxByteCount, String name) { + if ((maxByteCount & 3) != 0) { + throw new IllegalStateException("Not four byte aligned!"); + } + int limit = nextSectionStart + maxByteCount; + ByteBuffer sectionData = data.duplicate(); + sectionData.order(ByteOrder.LITTLE_ENDIAN); // necessary? + sectionData.position(nextSectionStart); + sectionData.limit(limit); + Section result = new Section(name, sectionData); + nextSectionStart = limit; + return result; + } + + public int getLength() { + return data.capacity(); + } + + public int getNextSectionStart() { + return nextSectionStart; + } + + /** + * Returns a copy of the the bytes of this dex. + */ + public byte[] getBytes() { + ByteBuffer data = this.data.duplicate(); // positioned ByteBuffers aren't thread safe + byte[] result = new byte[data.capacity()]; + data.position(0); + data.get(result); + return result; + } + + public List strings() { + return strings; + } + + public List typeIds() { + return typeIds; + } + + public List typeNames() { + return typeNames; + } + + public List protoIds() { + return protoIds; + } + + public List fieldIds() { + return fieldIds; + } + + public List methodIds() { + return methodIds; + } + + public Iterable classDefs() { + return new ClassDefIterable(); + } + + public TypeList readTypeList(int offset) { + if (offset == 0) { + return TypeList.EMPTY; + } + return open(offset).readTypeList(); + } + + public ClassData readClassData(ClassDef classDef) { + int offset = classDef.getClassDataOffset(); + if (offset == 0) { + throw new IllegalArgumentException("offset == 0"); + } + return open(offset).readClassData(); + } + + public Code readCode(ClassData.Method method) { + int offset = method.getCodeOffset(); + if (offset == 0) { + throw new IllegalArgumentException("offset == 0"); + } + return open(offset).readCode(); + } + + /** + * Returns the signature of all but the first 32 bytes of this dex. The + * first 32 bytes of dex files are not specified to be included in the + * signature. + */ + public byte[] computeSignature() throws IOException { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(); + } + byte[] buffer = new byte[8192]; + ByteBuffer data = this.data.duplicate(); // positioned ByteBuffers aren't thread safe + data.limit(data.capacity()); + data.position(SIGNATURE_OFFSET + SIGNATURE_SIZE); + while (data.hasRemaining()) { + int count = Math.min(buffer.length, data.remaining()); + data.get(buffer, 0, count); + digest.update(buffer, 0, count); + } + return digest.digest(); + } + + /** + * Returns the checksum of all but the first 12 bytes of {@code dex}. + */ + public int computeChecksum() throws IOException { + Adler32 adler32 = new Adler32(); + byte[] buffer = new byte[8192]; + ByteBuffer data = this.data.duplicate(); // positioned ByteBuffers aren't thread safe + data.limit(data.capacity()); + data.position(CHECKSUM_OFFSET + CHECKSUM_SIZE); + while (data.hasRemaining()) { + int count = Math.min(buffer.length, data.remaining()); + data.get(buffer, 0, count); + adler32.update(buffer, 0, count); + } + return (int) adler32.getValue(); + } + + /** + * Generates the signature and checksum of the dex file {@code out} and + * writes them to the file. + */ + public void writeHashes() throws IOException { + open(SIGNATURE_OFFSET).write(computeSignature()); + open(CHECKSUM_OFFSET).writeInt(computeChecksum()); + } + + /** + * Look up a descriptor index from a type index. Cheaper than: + * {@code open(tableOfContents.typeIds.off + (index * SizeOf.TYPE_ID_ITEM)).readInt();} + */ + public int descriptorIndexFromTypeIndex(int typeIndex) { + checkBounds(typeIndex, tableOfContents.typeIds.size); + int position = tableOfContents.typeIds.off + (SizeOf.TYPE_ID_ITEM * typeIndex); + return data.getInt(position); + } + + + public final class Section implements ByteInput, ByteOutput { + private final String name; + private final ByteBuffer data; + private final int initialPosition; + + private Section(String name, ByteBuffer data) { + this.name = name; + this.data = data; + this.initialPosition = data.position(); + } + + public int getPosition() { + return data.position(); + } + + public int readInt() { + return data.getInt(); + } + + public short readShort() { + return data.getShort(); + } + + public int readUnsignedShort() { + return readShort() & 0xffff; + } + + @Override + public byte readByte() { + return data.get(); + } + + public byte[] readByteArray(int length) { + byte[] result = new byte[length]; + data.get(result); + return result; + } + + public short[] readShortArray(int length) { + if (length == 0) { + return EMPTY_SHORT_ARRAY; + } + short[] result = new short[length]; + for (int i = 0; i < length; i++) { + result[i] = readShort(); + } + return result; + } + + public int readUleb128() { + return Leb128.readUnsignedLeb128(this); + } + + public int readUleb128p1() { + return Leb128.readUnsignedLeb128(this) - 1; + } + + public int readSleb128() { + return Leb128.readSignedLeb128(this); + } + + public void writeUleb128p1(int i) { + writeUleb128(i + 1); + } + + public TypeList readTypeList() { + int size = readInt(); + short[] types = readShortArray(size); + alignToFourBytes(); + return new TypeList(Dex.this, types); + } + + public String readString() { + int offset = readInt(); + int savedPosition = data.position(); + int savedLimit = data.limit(); + data.position(offset); + data.limit(data.capacity()); + try { + int expectedLength = readUleb128(); + String result = Mutf8.decode(this, new char[expectedLength]); + if (result.length() != expectedLength) { + throw new DexException("Declared length " + expectedLength + + " doesn't match decoded length of " + result.length()); + } + return result; + } catch (UTFDataFormatException e) { + throw new DexException(e); + } finally { + data.position(savedPosition); + data.limit(savedLimit); + } + } + + public FieldId readFieldId() { + int declaringClassIndex = readUnsignedShort(); + int typeIndex = readUnsignedShort(); + int nameIndex = readInt(); + return new FieldId(Dex.this, declaringClassIndex, typeIndex, nameIndex); + } + + public MethodId readMethodId() { + int declaringClassIndex = readUnsignedShort(); + int protoIndex = readUnsignedShort(); + int nameIndex = readInt(); + return new MethodId(Dex.this, declaringClassIndex, protoIndex, nameIndex); + } + + public ProtoId readProtoId() { + int shortyIndex = readInt(); + int returnTypeIndex = readInt(); + int parametersOffset = readInt(); + return new ProtoId(Dex.this, shortyIndex, returnTypeIndex, parametersOffset); + } + + public CallSiteId readCallSiteId() { + int offset = readInt(); + return new CallSiteId(Dex.this, offset); + } + + public MethodHandle readMethodHandle() { + MethodHandleType methodHandleType = MethodHandleType.fromValue(readUnsignedShort()); + int unused1 = readUnsignedShort(); + int fieldOrMethodId = readUnsignedShort(); + int unused2 = readUnsignedShort(); + return new MethodHandle(Dex.this, methodHandleType, unused1, fieldOrMethodId, unused2); + } + + public ClassDef readClassDef() { + int offset = getPosition(); + int type = readInt(); + int accessFlags = readInt(); + int supertype = readInt(); + int interfacesOffset = readInt(); + int sourceFileIndex = readInt(); + int annotationsOffset = readInt(); + int classDataOffset = readInt(); + int staticValuesOffset = readInt(); + return new ClassDef(Dex.this, offset, type, accessFlags, supertype, + interfacesOffset, sourceFileIndex, annotationsOffset, classDataOffset, + staticValuesOffset); + } + + private Code readCode() { + int registersSize = readUnsignedShort(); + int insSize = readUnsignedShort(); + int outsSize = readUnsignedShort(); + int triesSize = readUnsignedShort(); + int debugInfoOffset = readInt(); + int instructionsSize = readInt(); + short[] instructions = readShortArray(instructionsSize); + Try[] tries; + CatchHandler[] catchHandlers; + if (triesSize > 0) { + if (instructions.length % 2 == 1) { + readShort(); // padding + } + + /* + * We can't read the tries until we've read the catch handlers. + * Unfortunately they're in the opposite order in the dex file + * so we need to read them out-of-order. + */ + Section triesSection = open(data.position()); + skip(triesSize * SizeOf.TRY_ITEM); + catchHandlers = readCatchHandlers(); + tries = triesSection.readTries(triesSize, catchHandlers); + } else { + tries = new Try[0]; + catchHandlers = new CatchHandler[0]; + } + return new Code(registersSize, insSize, outsSize, debugInfoOffset, instructions, + tries, catchHandlers); + } + + private CatchHandler[] readCatchHandlers() { + int baseOffset = data.position(); + int catchHandlersSize = readUleb128(); + CatchHandler[] result = new CatchHandler[catchHandlersSize]; + for (int i = 0; i < catchHandlersSize; i++) { + int offset = data.position() - baseOffset; + result[i] = readCatchHandler(offset); + } + return result; + } + + private Try[] readTries(int triesSize, CatchHandler[] catchHandlers) { + Try[] result = new Try[triesSize]; + for (int i = 0; i < triesSize; i++) { + int startAddress = readInt(); + int instructionCount = readUnsignedShort(); + int handlerOffset = readUnsignedShort(); + int catchHandlerIndex = findCatchHandlerIndex(catchHandlers, handlerOffset); + result[i] = new Try(startAddress, instructionCount, catchHandlerIndex); + } + return result; + } + + private int findCatchHandlerIndex(CatchHandler[] catchHandlers, int offset) { + for (int i = 0; i < catchHandlers.length; i++) { + CatchHandler catchHandler = catchHandlers[i]; + if (catchHandler.getOffset() == offset) { + return i; + } + } + throw new IllegalArgumentException(); + } + + private CatchHandler readCatchHandler(int offset) { + int size = readSleb128(); + int handlersCount = Math.abs(size); + int[] typeIndexes = new int[handlersCount]; + int[] addresses = new int[handlersCount]; + for (int i = 0; i < handlersCount; i++) { + typeIndexes[i] = readUleb128(); + addresses[i] = readUleb128(); + } + int catchAllAddress = size <= 0 ? readUleb128() : -1; + return new CatchHandler(typeIndexes, addresses, catchAllAddress, offset); + } + + private ClassData readClassData() { + int staticFieldsSize = readUleb128(); + int instanceFieldsSize = readUleb128(); + int directMethodsSize = readUleb128(); + int virtualMethodsSize = readUleb128(); + ClassData.Field[] staticFields = readFields(staticFieldsSize); + ClassData.Field[] instanceFields = readFields(instanceFieldsSize); + ClassData.Method[] directMethods = readMethods(directMethodsSize); + ClassData.Method[] virtualMethods = readMethods(virtualMethodsSize); + return new ClassData(staticFields, instanceFields, directMethods, virtualMethods); + } + + private ClassData.Field[] readFields(int count) { + ClassData.Field[] result = new ClassData.Field[count]; + int fieldIndex = 0; + for (int i = 0; i < count; i++) { + fieldIndex += readUleb128(); // field index diff + int accessFlags = readUleb128(); + result[i] = new ClassData.Field(fieldIndex, accessFlags); + } + return result; + } + + private ClassData.Method[] readMethods(int count) { + ClassData.Method[] result = new ClassData.Method[count]; + int methodIndex = 0; + for (int i = 0; i < count; i++) { + methodIndex += readUleb128(); // method index diff + int accessFlags = readUleb128(); + int codeOff = readUleb128(); + result[i] = new ClassData.Method(methodIndex, accessFlags, codeOff); + } + return result; + } + + /** + * Returns a byte array containing the bytes from {@code start} to this + * section's current position. + */ + private byte[] getBytesFrom(int start) { + int end = data.position(); + byte[] result = new byte[end - start]; + data.position(start); + data.get(result); + return result; + } + + public Annotation readAnnotation() { + byte visibility = readByte(); + int start = data.position(); + new EncodedValueReader(this, EncodedValueReader.ENCODED_ANNOTATION).skipValue(); + return new Annotation(Dex.this, visibility, new EncodedValue(getBytesFrom(start))); + } + + public EncodedValue readEncodedArray() { + int start = data.position(); + new EncodedValueReader(this, EncodedValueReader.ENCODED_ARRAY).skipValue(); + return new EncodedValue(getBytesFrom(start)); + } + + public void skip(int count) { + if (count < 0) { + throw new IllegalArgumentException(); + } + data.position(data.position() + count); + } + + /** + * Skips bytes until the position is aligned to a multiple of 4. + */ + public void alignToFourBytes() { + data.position((data.position() + 3) & ~3); + } + + /** + * Writes 0x00 until the position is aligned to a multiple of 4. + */ + public void alignToFourBytesWithZeroFill() { + while ((data.position() & 3) != 0) { + data.put((byte) 0); + } + } + + public void assertFourByteAligned() { + if ((data.position() & 3) != 0) { + throw new IllegalStateException("Not four byte aligned!"); + } + } + + public void write(byte[] bytes) { + this.data.put(bytes); + } + + @Override + public void writeByte(int b) { + data.put((byte) b); + } + + public void writeShort(short i) { + data.putShort(i); + } + + public void writeUnsignedShort(int i) { + short s = (short) i; + if (i != (s & 0xffff)) { + throw new IllegalArgumentException("Expected an unsigned short: " + i); + } + writeShort(s); + } + + public void write(short[] shorts) { + for (short s : shorts) { + writeShort(s); + } + } + + public void writeInt(int i) { + data.putInt(i); + } + + public void writeUleb128(int i) { + try { + Leb128.writeUnsignedLeb128(this, i); + } catch (ArrayIndexOutOfBoundsException e) { + throw new DexException("Section limit " + data.limit() + " exceeded by " + name); + } + } + + public void writeSleb128(int i) { + try { + Leb128.writeSignedLeb128(this, i); + } catch (ArrayIndexOutOfBoundsException e) { + throw new DexException("Section limit " + data.limit() + " exceeded by " + name); + } + } + + public void writeStringData(String value) { + try { + int length = value.length(); + writeUleb128(length); + write(Mutf8.encode(value)); + writeByte(0); + } catch (UTFDataFormatException e) { + throw new AssertionError(); + } + } + + public void writeTypeList(TypeList typeList) { + short[] types = typeList.getTypes(); + writeInt(types.length); + for (short type : types) { + writeShort(type); + } + alignToFourBytesWithZeroFill(); + } + + /** + * Returns the number of bytes used by this section. + */ + public int used() { + return data.position() - initialPosition; + } + } + + private final class StringTable extends AbstractList implements RandomAccess { + @Override + public String get(int index) { + checkBounds(index, tableOfContents.stringIds.size); + return open(tableOfContents.stringIds.off + (index * SizeOf.STRING_ID_ITEM)) + .readString(); + } + @Override + public int size() { + return tableOfContents.stringIds.size; + } + } + + private final class TypeIndexToDescriptorIndexTable extends AbstractList + implements RandomAccess { + @Override + public Integer get(int index) { + return descriptorIndexFromTypeIndex(index); + } + @Override + public int size() { + return tableOfContents.typeIds.size; + } + } + + private final class TypeIndexToDescriptorTable extends AbstractList + implements RandomAccess { + @Override + public String get(int index) { + return strings.get(descriptorIndexFromTypeIndex(index)); + } + @Override + public int size() { + return tableOfContents.typeIds.size; + } + } + + private final class ProtoIdTable extends AbstractList implements RandomAccess { + @Override + public ProtoId get(int index) { + checkBounds(index, tableOfContents.protoIds.size); + return open(tableOfContents.protoIds.off + (SizeOf.PROTO_ID_ITEM * index)) + .readProtoId(); + } + @Override + public int size() { + return tableOfContents.protoIds.size; + } + } + + private final class FieldIdTable extends AbstractList implements RandomAccess { + @Override + public FieldId get(int index) { + checkBounds(index, tableOfContents.fieldIds.size); + return open(tableOfContents.fieldIds.off + (SizeOf.MEMBER_ID_ITEM * index)) + .readFieldId(); + } + @Override + public int size() { + return tableOfContents.fieldIds.size; + } + } + + private final class MethodIdTable extends AbstractList implements RandomAccess { + @Override + public MethodId get(int index) { + checkBounds(index, tableOfContents.methodIds.size); + return open(tableOfContents.methodIds.off + (SizeOf.MEMBER_ID_ITEM * index)) + .readMethodId(); + } + @Override + public int size() { + return tableOfContents.methodIds.size; + } + } + + private final class ClassDefIterator implements Iterator { + private final Dex.Section in = open(tableOfContents.classDefs.off); + private int count = 0; + + @Override + public boolean hasNext() { + return count < tableOfContents.classDefs.size; + } + @Override + public ClassDef next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + count++; + return in.readClassDef(); + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private final class ClassDefIterable implements Iterable { + @Override + public Iterator iterator() { + return !tableOfContents.classDefs.exists() + ? Collections.emptySet().iterator() + : new ClassDefIterator(); + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/DexException.java b/dalvikdx/src/main/java/external/com/android/dex/DexException.java new file mode 100644 index 00000000..db9fb8e8 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/DexException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.util.ExceptionWithContext; + +/** + * Thrown when there's a format problem reading, writing, or generally + * processing a dex file. + */ +public class DexException extends ExceptionWithContext { + public DexException(String message) { + super(message); + } + + public DexException(Throwable cause) { + super(cause); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/DexFormat.java b/dalvikdx/src/main/java/external/com/android/dex/DexFormat.java new file mode 100644 index 00000000..092a8335 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/DexFormat.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +/** + * Constants that show up in and are otherwise related to {@code .dex} + * files, and helper methods for same. + */ +public final class DexFormat { + private DexFormat() {} + + /** API level to target in order to generate const-method-handle and const-method-type */ + public static final int API_CONST_METHOD_HANDLE = 28; + + /** API level to target in order to generate invoke-polymorphic and invoke-custom */ + public static final int API_METHOD_HANDLES = 26; + + /** API level to target in order to define default and static interface methods */ + public static final int API_DEFINE_INTERFACE_METHODS = 24; + + /** API level to target in order to invoke default and static interface methods */ + public static final int API_INVOKE_INTERFACE_METHODS = 24; + + /** API level at which the invocation of static interface methods is permitted by dx. + * This value has been determined experimentally by testing on different VM versions. */ + public static final int API_INVOKE_STATIC_INTERFACE_METHODS = 21; + + /** API level to target in order to suppress extended opcode usage */ + public static final int API_NO_EXTENDED_OPCODES = 13; + + /** + * API level to target in order to produce the most modern file + * format + */ + public static final int API_CURRENT = API_CONST_METHOD_HANDLE; + + /** dex file version number for API level 28 and earlier */ + public static final String VERSION_FOR_API_28 = "039"; + + /** dex file version number for API level 26 and earlier */ + public static final String VERSION_FOR_API_26 = "038"; + + /** dex file version number for API level 24 and earlier */ + public static final String VERSION_FOR_API_24 = "037"; + + /** dex file version number for API level 13 and earlier */ + public static final String VERSION_FOR_API_13 = "035"; + + /** + * Dex file version number for dalvik. + *

+ * Note: Dex version 36 was loadable in some versions of Dalvik but was never fully supported or + * completed and is not considered a valid dex file format. + *

+ */ + public static final String VERSION_CURRENT = VERSION_FOR_API_28; + + /** + * file name of the primary {@code .dex} file inside an + * application or library {@code .jar} file + */ + public static final String DEX_IN_JAR_NAME = "classes.dex"; + + /** common prefix for all dex file "magic numbers" */ + public static final String MAGIC_PREFIX = "dex\n"; + + /** common suffix for all dex file "magic numbers" */ + public static final String MAGIC_SUFFIX = "\0"; + + /** + * value used to indicate endianness of file contents + */ + public static final int ENDIAN_TAG = 0x12345678; + + /** + * Maximum addressable field or method index. + * The largest addressable member is 0xffff, in the "instruction formats" spec as field@CCCC or + * meth@CCCC. + */ + public static final int MAX_MEMBER_IDX = 0xFFFF; + + /** + * Maximum addressable type index. + * The largest addressable type is 0xffff, in the "instruction formats" spec as type@CCCC. + */ + public static final int MAX_TYPE_IDX = 0xFFFF; + + /** + * Returns the API level corresponding to the given magic number, + * or {@code -1} if the given array is not a well-formed dex file + * magic number. + * + * @param magic array of bytes containing DEX file magic string + * @return API level corresponding to magic string if valid, -1 otherwise. + */ + public static int magicToApi(byte[] magic) { + if (magic.length != 8) { + return -1; + } + + if ((magic[0] != 'd') || (magic[1] != 'e') || (magic[2] != 'x') || (magic[3] != '\n') || + (magic[7] != '\0')) { + return -1; + } + + String version = "" + ((char) magic[4]) + ((char) magic[5]) +((char) magic[6]); + + if (version.equals(VERSION_FOR_API_13)) { + return API_NO_EXTENDED_OPCODES; + } else if (version.equals(VERSION_FOR_API_24)) { + return API_DEFINE_INTERFACE_METHODS; + } else if (version.equals(VERSION_FOR_API_26)) { + return API_METHOD_HANDLES; + } else if (version.equals(VERSION_FOR_API_28)) { + return API_CONST_METHOD_HANDLE; + } else if (version.equals(VERSION_CURRENT)) { + return API_CURRENT; + } + + return -1; + } + + /** + * Returns the magic number corresponding to the given target API level. + * + * @param targetApiLevel level of API (minimum supported value 13). + * @return Magic string corresponding to API level supplied. + */ + public static String apiToMagic(int targetApiLevel) { + String version; + + if (targetApiLevel >= API_CURRENT) { + version = VERSION_CURRENT; + } else if (targetApiLevel >= API_CONST_METHOD_HANDLE) { + version = VERSION_FOR_API_28; + } else if (targetApiLevel >= API_METHOD_HANDLES) { + version = VERSION_FOR_API_26; + } else if (targetApiLevel >= API_DEFINE_INTERFACE_METHODS) { + version = VERSION_FOR_API_24; + } else { + version = VERSION_FOR_API_13; + } + + return MAGIC_PREFIX + version + MAGIC_SUFFIX; + } + + /** + * Checks whether a DEX file magic string is supported. + * @param magic string from DEX file + * @return + */ + public static boolean isSupportedDexMagic(byte[] magic) { + int api = magicToApi(magic); + return api > 0; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/DexIndexOverflowException.java b/dalvikdx/src/main/java/external/com/android/dex/DexIndexOverflowException.java new file mode 100644 index 00000000..cf16c3d8 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/DexIndexOverflowException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2013 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. + */ + +package external.com.android.dex; + +/** + * Thrown when there's an index overflow writing a dex file. + */ +public final class DexIndexOverflowException extends DexException { + public DexIndexOverflowException(String message) { + super(message); + } + + public DexIndexOverflowException(Throwable cause) { + super(cause); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/EncodedValue.java b/dalvikdx/src/main/java/external/com/android/dex/EncodedValue.java new file mode 100644 index 00000000..59dad5c4 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/EncodedValue.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.util.ByteArrayByteInput; +import external.com.android.dex.util.ByteInput; + +/** + * An encoded value or array. + */ +public final class EncodedValue implements Comparable { + private final byte[] data; + + public EncodedValue(byte[] data) { + this.data = data; + } + + public ByteInput asByteInput() { + return new ByteArrayByteInput(data); + } + + public byte[] getBytes() { + return data; + } + + public void writeTo(Dex.Section out) { + out.write(data); + } + + @Override + public int compareTo(EncodedValue other) { + int size = Math.min(data.length, other.data.length); + for (int i = 0; i < size; i++) { + if (data[i] != other.data[i]) { + return (data[i] & 0xff) - (other.data[i] & 0xff); + } + } + return data.length - other.data.length; + } + + @Override + public String toString() { + return Integer.toHexString(data[0] & 0xff) + "...(" + data.length + ")"; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/EncodedValueCodec.java b/dalvikdx/src/main/java/external/com/android/dex/EncodedValueCodec.java new file mode 100644 index 00000000..ed4300ba --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/EncodedValueCodec.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.util.ByteInput; +import external.com.android.dex.util.ByteOutput; + +/** + * Read and write {@code encoded_value} primitives. + */ +public final class EncodedValueCodec { + private EncodedValueCodec() { + } + + /** + * Writes a signed integral to {@code out}. + */ + public static void writeSignedIntegralValue(ByteOutput out, int type, long value) { + /* + * Figure out how many bits are needed to represent the value, + * including a sign bit: The bit count is subtracted from 65 + * and not 64 to account for the sign bit. The xor operation + * has the effect of leaving non-negative values alone and + * unary complementing negative values (so that a leading zero + * count always returns a useful number for our present + * purpose). + */ + int requiredBits = 65 - Long.numberOfLeadingZeros(value ^ (value >> 63)); + + // Round up the requiredBits to a number of bytes. + int requiredBytes = (requiredBits + 0x07) >> 3; + + /* + * Write the header byte, which includes the type and + * requiredBytes - 1. + */ + out.writeByte(type | ((requiredBytes - 1) << 5)); + + // Write the value, per se. + while (requiredBytes > 0) { + out.writeByte((byte) value); + value >>= 8; + requiredBytes--; + } + } + + /** + * Writes an unsigned integral to {@code out}. + */ + public static void writeUnsignedIntegralValue(ByteOutput out, int type, long value) { + // Figure out how many bits are needed to represent the value. + int requiredBits = 64 - Long.numberOfLeadingZeros(value); + if (requiredBits == 0) { + requiredBits = 1; + } + + // Round up the requiredBits to a number of bytes. + int requiredBytes = (requiredBits + 0x07) >> 3; + + /* + * Write the header byte, which includes the type and + * requiredBytes - 1. + */ + out.writeByte(type | ((requiredBytes - 1) << 5)); + + // Write the value, per se. + while (requiredBytes > 0) { + out.writeByte((byte) value); + value >>= 8; + requiredBytes--; + } + } + + /** + * Writes a right-zero-extended value to {@code out}. + */ + public static void writeRightZeroExtendedValue(ByteOutput out, int type, long value) { + // Figure out how many bits are needed to represent the value. + int requiredBits = 64 - Long.numberOfTrailingZeros(value); + if (requiredBits == 0) { + requiredBits = 1; + } + + // Round up the requiredBits to a number of bytes. + int requiredBytes = (requiredBits + 0x07) >> 3; + + // Scootch the first bits to be written down to the low-order bits. + value >>= 64 - (requiredBytes * 8); + + /* + * Write the header byte, which includes the type and + * requiredBytes - 1. + */ + out.writeByte(type | ((requiredBytes - 1) << 5)); + + // Write the value, per se. + while (requiredBytes > 0) { + out.writeByte((byte) value); + value >>= 8; + requiredBytes--; + } + } + + /** + * Read a signed integer. + * + * @param zwidth byte count minus one + */ + public static int readSignedInt(ByteInput in, int zwidth) { + int result = 0; + for (int i = zwidth; i >= 0; i--) { + result = (result >>> 8) | ((in.readByte() & 0xff) << 24); + } + result >>= (3 - zwidth) * 8; + return result; + } + + /** + * Read an unsigned integer. + * + * @param zwidth byte count minus one + * @param fillOnRight true to zero fill on the right; false on the left + */ + public static int readUnsignedInt(ByteInput in, int zwidth, boolean fillOnRight) { + int result = 0; + if (!fillOnRight) { + for (int i = zwidth; i >= 0; i--) { + result = (result >>> 8) | ((in.readByte() & 0xff) << 24); + } + result >>>= (3 - zwidth) * 8; + } else { + for (int i = zwidth; i >= 0; i--) { + result = (result >>> 8) | ((in.readByte() & 0xff) << 24); + } + } + return result; + } + + /** + * Read a signed long. + * + * @param zwidth byte count minus one + */ + public static long readSignedLong(ByteInput in, int zwidth) { + long result = 0; + for (int i = zwidth; i >= 0; i--) { + result = (result >>> 8) | ((in.readByte() & 0xffL) << 56); + } + result >>= (7 - zwidth) * 8; + return result; + } + + /** + * Read an unsigned long. + * + * @param zwidth byte count minus one + * @param fillOnRight true to zero fill on the right; false on the left + */ + public static long readUnsignedLong(ByteInput in, int zwidth, boolean fillOnRight) { + long result = 0; + if (!fillOnRight) { + for (int i = zwidth; i >= 0; i--) { + result = (result >>> 8) | ((in.readByte() & 0xffL) << 56); + } + result >>>= (7 - zwidth) * 8; + } else { + for (int i = zwidth; i >= 0; i--) { + result = (result >>> 8) | ((in.readByte() & 0xffL) << 56); + } + } + return result; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/EncodedValueReader.java b/dalvikdx/src/main/java/external/com/android/dex/EncodedValueReader.java new file mode 100644 index 00000000..fe8fa1f1 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/EncodedValueReader.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.util.ByteInput; + +/** + * Pull parser for encoded values. + */ +public final class EncodedValueReader { + public static final int ENCODED_BYTE = 0x00; + public static final int ENCODED_SHORT = 0x02; + public static final int ENCODED_CHAR = 0x03; + public static final int ENCODED_INT = 0x04; + public static final int ENCODED_LONG = 0x06; + public static final int ENCODED_FLOAT = 0x10; + public static final int ENCODED_DOUBLE = 0x11; + public static final int ENCODED_METHOD_TYPE = 0x15; + public static final int ENCODED_METHOD_HANDLE = 0x16; + public static final int ENCODED_STRING = 0x17; + public static final int ENCODED_TYPE = 0x18; + public static final int ENCODED_FIELD = 0x19; + public static final int ENCODED_ENUM = 0x1b; + public static final int ENCODED_METHOD = 0x1a; + public static final int ENCODED_ARRAY = 0x1c; + public static final int ENCODED_ANNOTATION = 0x1d; + public static final int ENCODED_NULL = 0x1e; + public static final int ENCODED_BOOLEAN = 0x1f; + + /** placeholder type if the type is not yet known */ + private static final int MUST_READ = -1; + + protected final ByteInput in; + private int type = MUST_READ; + private int annotationType; + private int arg; + + public EncodedValueReader(ByteInput in) { + this.in = in; + } + + public EncodedValueReader(EncodedValue in) { + this(in.asByteInput()); + } + + /** + * Creates a new encoded value reader whose only value is the specified + * known type. This is useful for encoded values without a type prefix, + * such as class_def_item's encoded_array or annotation_item's + * encoded_annotation. + */ + public EncodedValueReader(ByteInput in, int knownType) { + this.in = in; + this.type = knownType; + } + + public EncodedValueReader(EncodedValue in, int knownType) { + this(in.asByteInput(), knownType); + } + + /** + * Returns the type of the next value to read. + */ + public int peek() { + if (type == MUST_READ) { + int argAndType = in.readByte() & 0xff; + type = argAndType & 0x1f; + arg = (argAndType & 0xe0) >> 5; + } + return type; + } + + /** + * Begins reading the elements of an array, returning the array's size. The + * caller must follow up by calling a read method for each element in the + * array. For example, this reads a byte array:
   {@code
+     *   int arraySize = readArray();
+     *   for (int i = 0, i < arraySize; i++) {
+     *     readByte();
+     *   }
+     * }
+ */ + public int readArray() { + checkType(ENCODED_ARRAY); + type = MUST_READ; + return Leb128.readUnsignedLeb128(in); + } + + /** + * Begins reading the fields of an annotation, returning the number of + * fields. The caller must follow up by making alternating calls to {@link + * #readAnnotationName()} and another read method. For example, this reads + * an annotation whose fields are all bytes:
   {@code
+     *   int fieldCount = readAnnotation();
+     *   int annotationType = getAnnotationType();
+     *   for (int i = 0; i < fieldCount; i++) {
+     *       readAnnotationName();
+     *       readByte();
+     *   }
+     * }
+ */ + public int readAnnotation() { + checkType(ENCODED_ANNOTATION); + type = MUST_READ; + annotationType = Leb128.readUnsignedLeb128(in); + return Leb128.readUnsignedLeb128(in); + } + + /** + * Returns the type of the annotation just returned by {@link + * #readAnnotation()}. This method's value is undefined unless the most + * recent call was to {@link #readAnnotation()}. + */ + public int getAnnotationType() { + return annotationType; + } + + public int readAnnotationName() { + return Leb128.readUnsignedLeb128(in); + } + + public byte readByte() { + checkType(ENCODED_BYTE); + type = MUST_READ; + return (byte) EncodedValueCodec.readSignedInt(in, arg); + } + + public short readShort() { + checkType(ENCODED_SHORT); + type = MUST_READ; + return (short) EncodedValueCodec.readSignedInt(in, arg); + } + + public char readChar() { + checkType(ENCODED_CHAR); + type = MUST_READ; + return (char) EncodedValueCodec.readUnsignedInt(in, arg, false); + } + + public int readInt() { + checkType(ENCODED_INT); + type = MUST_READ; + return EncodedValueCodec.readSignedInt(in, arg); + } + + public long readLong() { + checkType(ENCODED_LONG); + type = MUST_READ; + return EncodedValueCodec.readSignedLong(in, arg); + } + + public float readFloat() { + checkType(ENCODED_FLOAT); + type = MUST_READ; + return Float.intBitsToFloat(EncodedValueCodec.readUnsignedInt(in, arg, true)); + } + + public double readDouble() { + checkType(ENCODED_DOUBLE); + type = MUST_READ; + return Double.longBitsToDouble(EncodedValueCodec.readUnsignedLong(in, arg, true)); + } + + public int readMethodType() { + checkType(ENCODED_METHOD_TYPE); + type = MUST_READ; + return EncodedValueCodec.readUnsignedInt(in, arg, false); + } + + public int readMethodHandle() { + checkType(ENCODED_METHOD_HANDLE); + type = MUST_READ; + return EncodedValueCodec.readUnsignedInt(in, arg, false); + } + + public int readString() { + checkType(ENCODED_STRING); + type = MUST_READ; + return EncodedValueCodec.readUnsignedInt(in, arg, false); + } + + public int readType() { + checkType(ENCODED_TYPE); + type = MUST_READ; + return EncodedValueCodec.readUnsignedInt(in, arg, false); + } + + public int readField() { + checkType(ENCODED_FIELD); + type = MUST_READ; + return EncodedValueCodec.readUnsignedInt(in, arg, false); + } + + public int readEnum() { + checkType(ENCODED_ENUM); + type = MUST_READ; + return EncodedValueCodec.readUnsignedInt(in, arg, false); + } + + public int readMethod() { + checkType(ENCODED_METHOD); + type = MUST_READ; + return EncodedValueCodec.readUnsignedInt(in, arg, false); + } + + public void readNull() { + checkType(ENCODED_NULL); + type = MUST_READ; + } + + public boolean readBoolean() { + checkType(ENCODED_BOOLEAN); + type = MUST_READ; + return arg != 0; + } + + /** + * Skips a single value, including its nested values if it is an array or + * annotation. + */ + public void skipValue() { + switch (peek()) { + case ENCODED_BYTE: + readByte(); + break; + case ENCODED_SHORT: + readShort(); + break; + case ENCODED_CHAR: + readChar(); + break; + case ENCODED_INT: + readInt(); + break; + case ENCODED_LONG: + readLong(); + break; + case ENCODED_FLOAT: + readFloat(); + break; + case ENCODED_DOUBLE: + readDouble(); + break; + case ENCODED_METHOD_TYPE: + readMethodType(); + break; + case ENCODED_METHOD_HANDLE: + readMethodHandle(); + break; + case ENCODED_STRING: + readString(); + break; + case ENCODED_TYPE: + readType(); + break; + case ENCODED_FIELD: + readField(); + break; + case ENCODED_ENUM: + readEnum(); + break; + case ENCODED_METHOD: + readMethod(); + break; + case ENCODED_ARRAY: + for (int i = 0, size = readArray(); i < size; i++) { + skipValue(); + } + break; + case ENCODED_ANNOTATION: + for (int i = 0, size = readAnnotation(); i < size; i++) { + readAnnotationName(); + skipValue(); + } + break; + case ENCODED_NULL: + readNull(); + break; + case ENCODED_BOOLEAN: + readBoolean(); + break; + default: + throw new DexException("Unexpected type: " + Integer.toHexString(type)); + } + } + + private void checkType(int expected) { + if (peek() != expected) { + throw new IllegalStateException( + String.format("Expected %x but was %x", expected, peek())); + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/FieldId.java b/dalvikdx/src/main/java/external/com/android/dex/FieldId.java new file mode 100644 index 00000000..5b71e18b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/FieldId.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.util.Unsigned; + +public final class FieldId implements Comparable { + private final Dex dex; + private final int declaringClassIndex; + private final int typeIndex; + private final int nameIndex; + + public FieldId(Dex dex, int declaringClassIndex, int typeIndex, int nameIndex) { + this.dex = dex; + this.declaringClassIndex = declaringClassIndex; + this.typeIndex = typeIndex; + this.nameIndex = nameIndex; + } + + public int getDeclaringClassIndex() { + return declaringClassIndex; + } + + public int getTypeIndex() { + return typeIndex; + } + + public int getNameIndex() { + return nameIndex; + } + + @Override + public int compareTo(FieldId other) { + if (declaringClassIndex != other.declaringClassIndex) { + return Unsigned.compare(declaringClassIndex, other.declaringClassIndex); + } + if (nameIndex != other.nameIndex) { + return Unsigned.compare(nameIndex, other.nameIndex); + } + return Unsigned.compare(typeIndex, other.typeIndex); // should always be 0 + } + + public void writeTo(Dex.Section out) { + out.writeUnsignedShort(declaringClassIndex); + out.writeUnsignedShort(typeIndex); + out.writeInt(nameIndex); + } + + @Override + public String toString() { + if (dex == null) { + return declaringClassIndex + " " + typeIndex + " " + nameIndex; + } + return dex.typeNames().get(typeIndex) + "." + dex.strings().get(nameIndex); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/Leb128.java b/dalvikdx/src/main/java/external/com/android/dex/Leb128.java new file mode 100644 index 00000000..80acd8d8 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/Leb128.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2008 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.util.ByteInput; +import external.com.android.dex.util.ByteOutput; + +/** + * Reads and writes DWARFv3 LEB 128 signed and unsigned integers. See DWARF v3 + * section 7.6. + */ +public final class Leb128 { + private Leb128() { + } + + /** + * Gets the number of bytes in the unsigned LEB128 encoding of the + * given value. + * + * @param value the value in question + * @return its write size, in bytes + */ + public static int unsignedLeb128Size(int value) { + // TODO: This could be much cleverer. + + int remaining = value >> 7; + int count = 0; + + while (remaining != 0) { + remaining >>= 7; + count++; + } + + return count + 1; + } + + /** + * Reads an signed integer from {@code in}. + */ + public static int readSignedLeb128(ByteInput in) { + int result = 0; + int cur; + int count = 0; + int signBits = -1; + + do { + cur = in.readByte() & 0xff; + 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; + } + + /** + * Reads an unsigned integer from {@code in}. + */ + public static int readUnsignedLeb128(ByteInput in) { + int result = 0; + int cur; + int count = 0; + + do { + cur = in.readByte() & 0xff; + result |= (cur & 0x7f) << (count * 7); + count++; + } while (((cur & 0x80) == 0x80) && count < 5); + + if ((cur & 0x80) == 0x80) { + throw new DexException("invalid LEB128 sequence"); + } + + return result; + } + + /** + * Writes {@code value} as an unsigned integer to {@code out}, starting at + * {@code offset}. Returns the number of bytes written. + */ + public static void writeUnsignedLeb128(ByteOutput out, int value) { + int remaining = value >>> 7; + + while (remaining != 0) { + out.writeByte((byte) ((value & 0x7f) | 0x80)); + value = remaining; + remaining >>>= 7; + } + + out.writeByte((byte) (value & 0x7f)); + } + + /** + * Writes {@code value} as a signed integer to {@code out}, starting at + * {@code offset}. Returns the number of bytes written. + */ + public static void writeSignedLeb128(ByteOutput out, int value) { + int remaining = value >> 7; + boolean hasMore = true; + int end = ((value & Integer.MIN_VALUE) == 0) ? 0 : -1; + + while (hasMore) { + hasMore = (remaining != end) + || ((remaining & 1) != ((value >> 6) & 1)); + + out.writeByte((byte) ((value & 0x7f) | (hasMore ? 0x80 : 0))); + value = remaining; + remaining >>= 7; + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/MethodHandle.java b/dalvikdx/src/main/java/external/com/android/dex/MethodHandle.java new file mode 100644 index 00000000..7c84e76d --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/MethodHandle.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2017 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.Dex.Section; +import external.com.android.dex.util.Unsigned; + +/** + * A method_handle_item: + * https://source.android.com/devices/tech/dalvik/dex-format#method-handle-item + */ +public class MethodHandle implements Comparable { + + /** + * A method handle type code: + * https://source.android.com/devices/tech/dalvik/dex-format#method-handle-type-codes + */ + public enum MethodHandleType { + METHOD_HANDLE_TYPE_STATIC_PUT(0x00), + METHOD_HANDLE_TYPE_STATIC_GET(0x01), + METHOD_HANDLE_TYPE_INSTANCE_PUT(0x02), + METHOD_HANDLE_TYPE_INSTANCE_GET(0x03), + METHOD_HANDLE_TYPE_INVOKE_STATIC(0x04), + METHOD_HANDLE_TYPE_INVOKE_INSTANCE(0x05), + METHOD_HANDLE_TYPE_INVOKE_DIRECT(0x06), + METHOD_HANDLE_TYPE_INVOKE_CONSTRUCTOR(0x07), + METHOD_HANDLE_TYPE_INVOKE_INTERFACE(0x08); + + private final int value; + + MethodHandleType(int value) { + this.value = value; + } + + static MethodHandleType fromValue(int value) { + for (MethodHandleType methodHandleType : values()) { + if (methodHandleType.value == value) { + return methodHandleType; + } + } + throw new IllegalArgumentException(String.valueOf(value)); + } + + public boolean isField() { + switch (this) { + case METHOD_HANDLE_TYPE_STATIC_PUT: + case METHOD_HANDLE_TYPE_STATIC_GET: + case METHOD_HANDLE_TYPE_INSTANCE_PUT: + case METHOD_HANDLE_TYPE_INSTANCE_GET: + return true; + default: + return false; + } + } + } + + private final Dex dex; + private final MethodHandleType methodHandleType; + private final int unused1; + private final int fieldOrMethodId; + private final int unused2; + + public MethodHandle( + Dex dex, + MethodHandleType methodHandleType, + int unused1, + int fieldOrMethodId, + int unused2) { + this.dex = dex; + this.methodHandleType = methodHandleType; + this.unused1 = unused1; + this.fieldOrMethodId = fieldOrMethodId; + this.unused2 = unused2; + } + + @Override + public int compareTo(MethodHandle o) { + if (methodHandleType != o.methodHandleType) { + return methodHandleType.compareTo(o.methodHandleType); + } + return Unsigned.compare(fieldOrMethodId, o.fieldOrMethodId); + } + + public MethodHandleType getMethodHandleType() { + return methodHandleType; + } + + public int getUnused1() { + return unused1; + } + + public int getFieldOrMethodId() { + return fieldOrMethodId; + } + + public int getUnused2() { + return unused2; + } + + public void writeTo(Section out) { + out.writeUnsignedShort(methodHandleType.value); + out.writeUnsignedShort(unused1); + out.writeUnsignedShort(fieldOrMethodId); + out.writeUnsignedShort(unused2); + } + + @Override + public String toString() { + if (dex == null) { + return methodHandleType + " " + fieldOrMethodId; + } + return methodHandleType + + " " + + (methodHandleType.isField() + ? dex.fieldIds().get(fieldOrMethodId) + : dex.methodIds().get(fieldOrMethodId)); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/MethodId.java b/dalvikdx/src/main/java/external/com/android/dex/MethodId.java new file mode 100644 index 00000000..69a1ff6a --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/MethodId.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.util.Unsigned; + +public final class MethodId implements Comparable { + private final Dex dex; + private final int declaringClassIndex; + private final int protoIndex; + private final int nameIndex; + + public MethodId(Dex dex, int declaringClassIndex, int protoIndex, int nameIndex) { + this.dex = dex; + this.declaringClassIndex = declaringClassIndex; + this.protoIndex = protoIndex; + this.nameIndex = nameIndex; + } + + public int getDeclaringClassIndex() { + return declaringClassIndex; + } + + public int getProtoIndex() { + return protoIndex; + } + + public int getNameIndex() { + return nameIndex; + } + + @Override + public int compareTo(MethodId other) { + if (declaringClassIndex != other.declaringClassIndex) { + return Unsigned.compare(declaringClassIndex, other.declaringClassIndex); + } + if (nameIndex != other.nameIndex) { + return Unsigned.compare(nameIndex, other.nameIndex); + } + return Unsigned.compare(protoIndex, other.protoIndex); + } + + public void writeTo(Dex.Section out) { + out.writeUnsignedShort(declaringClassIndex); + out.writeUnsignedShort(protoIndex); + out.writeInt(nameIndex); + } + + @Override + public String toString() { + if (dex == null) { + return declaringClassIndex + " " + protoIndex + " " + nameIndex; + } + return dex.typeNames().get(declaringClassIndex) + + "." + dex.strings().get(nameIndex) + + dex.readTypeList(dex.protoIds().get(protoIndex).getParametersOffset()); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/Mutf8.java b/dalvikdx/src/main/java/external/com/android/dex/Mutf8.java new file mode 100644 index 00000000..d9c6b71b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/Mutf8.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.util.ByteInput; +import java.io.UTFDataFormatException; + +/** + * Modified UTF-8 as described in the dex file format spec. + * + *

Derived from libcore's MUTF-8 encoder at java.nio.charset.ModifiedUtf8. + */ +public final class Mutf8 { + private Mutf8() {} + + /** + * Decodes bytes from {@code in} into {@code out} until a delimiter 0x00 is + * encountered. Returns a new string containing the decoded characters. + */ + public static String decode(ByteInput in, char[] out) throws UTFDataFormatException { + int s = 0; + while (true) { + char a = (char) (in.readByte() & 0xff); + if (a == 0) { + return new String(out, 0, s); + } + out[s] = a; + if (a < '\u0080') { + s++; + } else if ((a & 0xe0) == 0xc0) { + int b = in.readByte() & 0xff; + if ((b & 0xC0) != 0x80) { + throw new UTFDataFormatException("bad second byte"); + } + out[s++] = (char) (((a & 0x1F) << 6) | (b & 0x3F)); + } else if ((a & 0xf0) == 0xe0) { + int b = in.readByte() & 0xff; + int c = in.readByte() & 0xff; + if (((b & 0xC0) != 0x80) || ((c & 0xC0) != 0x80)) { + throw new UTFDataFormatException("bad second or third byte"); + } + out[s++] = (char) (((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F)); + } else { + throw new UTFDataFormatException("bad byte"); + } + } + } + + /** + * Returns the number of bytes the modified UTF8 representation of 's' would take. + */ + private static long countBytes(String s, boolean shortLength) throws UTFDataFormatException { + long result = 0; + final int length = s.length(); + for (int i = 0; i < length; ++i) { + char ch = s.charAt(i); + if (ch != 0 && ch <= 127) { // U+0000 uses two bytes. + ++result; + } else if (ch <= 2047) { + result += 2; + } else { + result += 3; + } + if (shortLength && result > 65535) { + throw new UTFDataFormatException("String more than 65535 UTF bytes long"); + } + } + return result; + } + + /** + * Encodes the modified UTF-8 bytes corresponding to {@code s} into {@code + * dst}, starting at {@code offset}. + */ + public static void encode(byte[] dst, int offset, String s) { + final int length = s.length(); + for (int i = 0; i < length; i++) { + char ch = s.charAt(i); + if (ch != 0 && ch <= 127) { // U+0000 uses two bytes. + dst[offset++] = (byte) ch; + } else if (ch <= 2047) { + dst[offset++] = (byte) (0xc0 | (0x1f & (ch >> 6))); + dst[offset++] = (byte) (0x80 | (0x3f & ch)); + } else { + dst[offset++] = (byte) (0xe0 | (0x0f & (ch >> 12))); + dst[offset++] = (byte) (0x80 | (0x3f & (ch >> 6))); + dst[offset++] = (byte) (0x80 | (0x3f & ch)); + } + } + } + + /** + * Returns an array containing the modified UTF-8 form of {@code s}. + */ + public static byte[] encode(String s) throws UTFDataFormatException { + int utfCount = (int) countBytes(s, true); + byte[] result = new byte[utfCount]; + encode(result, 0, s); + return result; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/ProtoId.java b/dalvikdx/src/main/java/external/com/android/dex/ProtoId.java new file mode 100644 index 00000000..d8602603 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/ProtoId.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.util.Unsigned; + +public final class ProtoId implements Comparable { + private final Dex dex; + private final int shortyIndex; + private final int returnTypeIndex; + private final int parametersOffset; + + public ProtoId(Dex dex, int shortyIndex, int returnTypeIndex, int parametersOffset) { + this.dex = dex; + this.shortyIndex = shortyIndex; + this.returnTypeIndex = returnTypeIndex; + this.parametersOffset = parametersOffset; + } + + @Override + public int compareTo(ProtoId other) { + if (returnTypeIndex != other.returnTypeIndex) { + return Unsigned.compare(returnTypeIndex, other.returnTypeIndex); + } + return Unsigned.compare(parametersOffset, other.parametersOffset); + } + + public int getShortyIndex() { + return shortyIndex; + } + + public int getReturnTypeIndex() { + return returnTypeIndex; + } + + public int getParametersOffset() { + return parametersOffset; + } + + public void writeTo(Dex.Section out) { + out.writeInt(shortyIndex); + out.writeInt(returnTypeIndex); + out.writeInt(parametersOffset); + } + + @Override + public String toString() { + if (dex == null) { + return shortyIndex + " " + returnTypeIndex + " " + parametersOffset; + } + + return dex.strings().get(shortyIndex) + + ": " + dex.typeNames().get(returnTypeIndex) + + " " + dex.readTypeList(parametersOffset); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/SizeOf.java b/dalvikdx/src/main/java/external/com/android/dex/SizeOf.java new file mode 100644 index 00000000..35970062 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/SizeOf.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +public final class SizeOf { + private SizeOf() {} + + public static final int UBYTE = 1; + public static final int USHORT = 2; + public static final int UINT = 4; + + public static final int SIGNATURE = UBYTE * 20; + + /** + * magic ubyte[8] + * checksum uint + * signature ubyte[20] + * file_size uint + * header_size uint + * endian_tag uint + * link_size uint + * link_off uint + * map_off uint + * string_ids_size uint + * string_ids_off uint + * type_ids_size uint + * type_ids_off uint + * proto_ids_size uint + * proto_ids_off uint + * field_ids_size uint + * field_ids_off uint + * method_ids_size uint + * method_ids_off uint + * class_defs_size uint + * class_defs_off uint + * data_size uint + * data_off uint + */ + public static final int HEADER_ITEM = (8 * UBYTE) + UINT + SIGNATURE + (20 * UINT); // 0x70 + + /** + * string_data_off uint + */ + public static final int STRING_ID_ITEM = UINT; + + /** + * descriptor_idx uint + */ + public static final int TYPE_ID_ITEM = UINT; + + /** + * type_idx ushort + */ + public static final int TYPE_ITEM = USHORT; + + /** + * shorty_idx uint + * return_type_idx uint + * return_type_idx uint + */ + public static final int PROTO_ID_ITEM = UINT + UINT + UINT; + + /** + * class_idx ushort + * type_idx/proto_idx ushort + * name_idx uint + */ + public static final int MEMBER_ID_ITEM = USHORT + USHORT + UINT; + + /** + * class_idx uint + * access_flags uint + * superclass_idx uint + * interfaces_off uint + * source_file_idx uint + * annotations_off uint + * class_data_off uint + * static_values_off uint + */ + public static final int CLASS_DEF_ITEM = 8 * UINT; + + /** + * type ushort + * unused ushort + * size uint + * offset uint + */ + public static final int MAP_ITEM = USHORT + USHORT + UINT + UINT; + + /** + * start_addr uint + * insn_count ushort + * handler_off ushort + */ + public static final int TRY_ITEM = UINT + USHORT + USHORT; + + /** + * call_site_off uint + */ + public static final int CALL_SITE_ID_ITEM = UINT; + + /** + * method_handle_type ushort + * unused ushort + * field_or_method_id ushort + * unused ushort + */ + public static final int METHOD_HANDLE_ITEM = USHORT + USHORT + USHORT + USHORT; +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/TableOfContents.java b/dalvikdx/src/main/java/external/com/android/dex/TableOfContents.java new file mode 100644 index 00000000..cafcdb3b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/TableOfContents.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +/** + * The file header and map. + */ +public final class TableOfContents { + + /* + * TODO: factor out ID constants. + */ + + public final Section header = new Section(0x0000); + public final Section stringIds = new Section(0x0001); + public final Section typeIds = new Section(0x0002); + public final Section protoIds = new Section(0x0003); + public final Section fieldIds = new Section(0x0004); + public final Section methodIds = new Section(0x0005); + public final Section classDefs = new Section(0x0006); + public final Section callSiteIds = new Section(0x0007); + public final Section methodHandles = new Section(0x0008); + public final Section mapList = new Section(0x1000); + public final Section typeLists = new Section(0x1001); + public final Section annotationSetRefLists = new Section(0x1002); + public final Section annotationSets = new Section(0x1003); + public final Section classDatas = new Section(0x2000); + public final Section codes = new Section(0x2001); + public final Section stringDatas = new Section(0x2002); + public final Section debugInfos = new Section(0x2003); + public final Section annotations = new Section(0x2004); + public final Section encodedArrays = new Section(0x2005); + public final Section annotationsDirectories = new Section(0x2006); + public final Section[] sections = { + header, stringIds, typeIds, protoIds, fieldIds, methodIds, classDefs, mapList, callSiteIds, + methodHandles, typeLists, annotationSetRefLists, annotationSets, classDatas, codes, + stringDatas, debugInfos, annotations, encodedArrays, annotationsDirectories + }; + + public int apiLevel; + public int checksum; + public byte[] signature; + public int fileSize; + public int linkSize; + public int linkOff; + public int dataSize; + public int dataOff; + + public TableOfContents() { + signature = new byte[20]; + } + + public void readFrom(Dex dex) throws IOException { + readHeader(dex.open(0)); + readMap(dex.open(mapList.off)); + computeSizesFromOffsets(); + } + + private void readHeader(Dex.Section headerIn) throws UnsupportedEncodingException { + byte[] magic = headerIn.readByteArray(8); + + if (!DexFormat.isSupportedDexMagic(magic)) { + String msg = + String.format("Unexpected magic: [0x%02x, 0x%02x, 0x%02x, 0x%02x, " + + "0x%02x, 0x%02x, 0x%02x, 0x%02x]", + magic[0], magic[1], magic[2], magic[3], + magic[4], magic[5], magic[6], magic[7]); + throw new DexException(msg); + } + + apiLevel = DexFormat.magicToApi(magic); + checksum = headerIn.readInt(); + signature = headerIn.readByteArray(20); + fileSize = headerIn.readInt(); + int headerSize = headerIn.readInt(); + if (headerSize != SizeOf.HEADER_ITEM) { + throw new DexException("Unexpected header: 0x" + Integer.toHexString(headerSize)); + } + int endianTag = headerIn.readInt(); + if (endianTag != DexFormat.ENDIAN_TAG) { + throw new DexException("Unexpected endian tag: 0x" + Integer.toHexString(endianTag)); + } + linkSize = headerIn.readInt(); + linkOff = headerIn.readInt(); + mapList.off = headerIn.readInt(); + if (mapList.off == 0) { + throw new DexException("Cannot merge dex files that do not contain a map"); + } + stringIds.size = headerIn.readInt(); + stringIds.off = headerIn.readInt(); + typeIds.size = headerIn.readInt(); + typeIds.off = headerIn.readInt(); + protoIds.size = headerIn.readInt(); + protoIds.off = headerIn.readInt(); + fieldIds.size = headerIn.readInt(); + fieldIds.off = headerIn.readInt(); + methodIds.size = headerIn.readInt(); + methodIds.off = headerIn.readInt(); + classDefs.size = headerIn.readInt(); + classDefs.off = headerIn.readInt(); + dataSize = headerIn.readInt(); + dataOff = headerIn.readInt(); + } + + private void readMap(Dex.Section in) throws IOException { + int mapSize = in.readInt(); + Section previous = null; + for (int i = 0; i < mapSize; i++) { + short type = in.readShort(); + in.readShort(); // unused + Section section = getSection(type); + int size = in.readInt(); + int offset = in.readInt(); + + if ((section.size != 0 && section.size != size) + || (section.off != -1 && section.off != offset)) { + throw new DexException("Unexpected map value for 0x" + Integer.toHexString(type)); + } + + section.size = size; + section.off = offset; + + if (previous != null && previous.off > section.off) { + throw new DexException("Map is unsorted at " + previous + ", " + section); + } + + previous = section; + } + Arrays.sort(sections); + } + + public void computeSizesFromOffsets() { + int end = dataOff + dataSize; + for (int i = sections.length - 1; i >= 0; i--) { + Section section = sections[i]; + if (section.off == -1) { + continue; + } + if (section.off > end) { + throw new DexException("Map is unsorted at " + section); + } + section.byteCount = end - section.off; + end = section.off; + } + } + + private Section getSection(short type) { + for (Section section : sections) { + if (section.type == type) { + return section; + } + } + throw new IllegalArgumentException("No such map item: " + type); + } + + public void writeHeader(Dex.Section out, int api) throws IOException { + out.write(DexFormat.apiToMagic(api).getBytes("UTF-8")); + out.writeInt(checksum); + out.write(signature); + out.writeInt(fileSize); + out.writeInt(SizeOf.HEADER_ITEM); + out.writeInt(DexFormat.ENDIAN_TAG); + out.writeInt(linkSize); + out.writeInt(linkOff); + out.writeInt(mapList.off); + out.writeInt(stringIds.size); + out.writeInt(stringIds.off); + out.writeInt(typeIds.size); + out.writeInt(typeIds.off); + out.writeInt(protoIds.size); + out.writeInt(protoIds.off); + out.writeInt(fieldIds.size); + out.writeInt(fieldIds.off); + out.writeInt(methodIds.size); + out.writeInt(methodIds.off); + out.writeInt(classDefs.size); + out.writeInt(classDefs.off); + out.writeInt(dataSize); + out.writeInt(dataOff); + } + + public void writeMap(Dex.Section out) throws IOException { + int count = 0; + for (Section section : sections) { + if (section.exists()) { + count++; + } + } + + out.writeInt(count); + for (Section section : sections) { + if (section.exists()) { + out.writeShort(section.type); + out.writeShort((short) 0); + out.writeInt(section.size); + out.writeInt(section.off); + } + } + } + + public static class Section implements Comparable

{ + public final short type; + public int size = 0; + public int off = -1; + public int byteCount = 0; + + public Section(int type) { + this.type = (short) type; + } + + public boolean exists() { + return size > 0; + } + + @Override + public int compareTo(Section section) { + if (off != section.off) { + return off < section.off ? -1 : 1; + } + return 0; + } + + @Override + public String toString() { + return String.format("Section[type=%#x,off=%#x,size=%#x]", type, off, size); + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/TypeList.java b/dalvikdx/src/main/java/external/com/android/dex/TypeList.java new file mode 100644 index 00000000..a75d8dd5 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/TypeList.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex; + +import external.com.android.dex.util.Unsigned; + +public final class TypeList implements Comparable { + + public static final TypeList EMPTY = new TypeList(null, Dex.EMPTY_SHORT_ARRAY); + + private final Dex dex; + private final short[] types; + + public TypeList(Dex dex, short[] types) { + this.dex = dex; + this.types = types; + } + + public short[] getTypes() { + return types; + } + + @Override + public int compareTo(TypeList other) { + for (int i = 0; i < types.length && i < other.types.length; i++) { + if (types[i] != other.types[i]) { + return Unsigned.compare(types[i], other.types[i]); + } + } + return Unsigned.compare(types.length, other.types.length); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("("); + for (int i = 0, typesLength = types.length; i < typesLength; i++) { + result.append(dex != null ? dex.typeNames().get(types[i]) : types[i]); + } + result.append(")"); + return result.toString(); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/util/ByteArrayByteInput.java b/dalvikdx/src/main/java/external/com/android/dex/util/ByteArrayByteInput.java new file mode 100644 index 00000000..240190dd --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/util/ByteArrayByteInput.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex.util; + +public final class ByteArrayByteInput implements ByteInput { + + private final byte[] bytes; + private int position; + + public ByteArrayByteInput(byte... bytes) { + this.bytes = bytes; + } + + @Override + public byte readByte() { + return bytes[position++]; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/util/ByteInput.java b/dalvikdx/src/main/java/external/com/android/dex/util/ByteInput.java new file mode 100644 index 00000000..5daf846b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/util/ByteInput.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex.util; + +/** + * A byte source. + */ +public interface ByteInput { + + /** + * Returns a byte. + * + * @throws IndexOutOfBoundsException if all bytes have been read. + */ + byte readByte(); +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/util/ByteOutput.java b/dalvikdx/src/main/java/external/com/android/dex/util/ByteOutput.java new file mode 100644 index 00000000..639226b2 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/util/ByteOutput.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex.util; + +/** + * A byte sink. + */ +public interface ByteOutput { + + /** + * Writes a byte. + * + * @throws IndexOutOfBoundsException if all bytes have been written. + */ + void writeByte(int i); +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/util/ExceptionWithContext.java b/dalvikdx/src/main/java/external/com/android/dex/util/ExceptionWithContext.java new file mode 100644 index 00000000..adce1b59 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/util/ExceptionWithContext.java @@ -0,0 +1,148 @@ +/* + * 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. + */ + +package external.com.android.dex.util; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * Exception which carries around structured context. + */ +public class ExceptionWithContext extends RuntimeException { + /** {@code non-null;} human-oriented context of the exception */ + private StringBuffer context; + + /** + * Augments the given exception with the given context, and return the + * result. The result is either the given exception if it was an + * {@link ExceptionWithContext}, or a newly-constructed exception if it + * was not. + * + * @param ex {@code non-null;} the exception to augment + * @param str {@code non-null;} context to add + * @return {@code non-null;} an appropriate instance + */ + public static ExceptionWithContext withContext(Throwable ex, String str) { + ExceptionWithContext ewc; + + if (ex instanceof ExceptionWithContext) { + ewc = (ExceptionWithContext) ex; + } else { + ewc = new ExceptionWithContext(ex); + } + + ewc.addContext(str); + return ewc; + } + + /** + * Constructs an instance. + * + * @param message human-oriented message + */ + public ExceptionWithContext(String message) { + this(message, null); + } + + /** + * Constructs an instance. + * + * @param cause {@code null-ok;} exception that caused this one + */ + public ExceptionWithContext(Throwable cause) { + this(null, cause); + } + + /** + * Constructs an instance. + * + * @param message human-oriented message + * @param cause {@code null-ok;} exception that caused this one + */ + public ExceptionWithContext(String message, Throwable cause) { + super((message != null) ? message : + (cause != null) ? cause.getMessage() : null, + cause); + + if (cause instanceof ExceptionWithContext) { + String ctx = ((ExceptionWithContext) cause).context.toString(); + context = new StringBuffer(ctx.length() + 200); + context.append(ctx); + } else { + context = new StringBuffer(200); + } + } + + /** {@inheritDoc} */ + @Override + public void printStackTrace(PrintStream out) { + super.printStackTrace(out); + out.println(context); + } + + /** {@inheritDoc} */ + @Override + public void printStackTrace(PrintWriter out) { + super.printStackTrace(out); + out.println(context); + } + + /** + * Adds a line of context to this instance. + * + * @param str {@code non-null;} new context + */ + public void addContext(String str) { + if (str == null) { + throw new NullPointerException("str == null"); + } + + context.append(str); + if (!str.endsWith("\n")) { + context.append('\n'); + } + } + + /** + * Gets the context. + * + * @return {@code non-null;} the context + */ + public String getContext() { + return context.toString(); + } + + /** + * Prints the message and context. + * + * @param out {@code non-null;} where to print to + */ + public void printContext(PrintStream out) { + out.println(getMessage()); + out.print(context); + } + + /** + * Prints the message and context. + * + * @param out {@code non-null;} where to print to + */ + public void printContext(PrintWriter out) { + out.println(getMessage()); + out.print(context); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/util/FileUtils.java b/dalvikdx/src/main/java/external/com/android/dex/util/FileUtils.java new file mode 100644 index 00000000..0a607411 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/util/FileUtils.java @@ -0,0 +1,97 @@ +/* + * 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. + */ + +package external.com.android.dex.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * File I/O utilities. + */ +public final class FileUtils { + private FileUtils() { + } + + /** + * Reads the named file, translating {@link IOException} to a + * {@link RuntimeException} of some sort. + * + * @param fileName {@code non-null;} name of the file to read + * @return {@code non-null;} contents of the file + */ + public static byte[] readFile(String fileName) { + File file = new File(fileName); + return readFile(file); + } + + /** + * Reads the given file, translating {@link IOException} to a + * {@link RuntimeException} of some sort. + * + * @param file {@code non-null;} the file to read + * @return {@code non-null;} contents of the file + */ + public static byte[] readFile(File file) { + if (!file.exists()) { + throw new RuntimeException(file + ": file not found"); + } + + if (!file.isFile()) { + throw new RuntimeException(file + ": not a file"); + } + + if (!file.canRead()) { + throw new RuntimeException(file + ": file not readable"); + } + + long longLength = file.length(); + int length = (int) longLength; + if (length != longLength) { + throw new RuntimeException(file + ": file too long"); + } + + byte[] result = new byte[length]; + + try { + FileInputStream in = new FileInputStream(file); + int at = 0; + while (length > 0) { + int amt = in.read(result, at, length); + if (amt == -1) { + throw new RuntimeException(file + ": unexpected EOF"); + } + at += amt; + length -= amt; + } + in.close(); + } catch (IOException ex) { + throw new RuntimeException(file + ": trouble reading", ex); + } + + return result; + } + + /** + * Returns true if {@code fileName} names a .zip, .jar, or .apk. + */ + public static boolean hasArchiveSuffix(String fileName) { + return fileName.endsWith(".zip") + || fileName.endsWith(".jar") + || fileName.endsWith(".apk"); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dex/util/Unsigned.java b/dalvikdx/src/main/java/external/com/android/dex/util/Unsigned.java new file mode 100644 index 00000000..edbab803 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dex/util/Unsigned.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 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. + */ + +package external.com.android.dex.util; + +/** + * Unsigned arithmetic over Java's signed types. + */ +public final class Unsigned { + private Unsigned() {} + + public static int compare(short ushortA, short ushortB) { + if (ushortA == ushortB) { + return 0; + } + int a = ushortA & 0xFFFF; + int b = ushortB & 0xFFFF; + return a < b ? -1 : 1; + } + + public static int compare(int uintA, int uintB) { + if (uintA == uintB) { + return 0; + } + long a = uintA & 0xFFFFFFFFL; + long b = uintB & 0xFFFFFFFFL; + return a < b ? -1 : 1; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/Version.java b/dalvikdx/src/main/java/external/com/android/dx/Version.java new file mode 100644 index 00000000..386b10c0 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/Version.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +package external.com.android.dx; + +/** + * Version number for dx. + */ +public class Version { + /** {@code non-null;} version string */ + public static final String VERSION = "1.16"; +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttAnnotationDefault.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttAnnotationDefault.java new file mode 100644 index 00000000..fd46a7d4 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttAnnotationDefault.java @@ -0,0 +1,68 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.cst.Constant; + +/** + * Attribute class for {@code AnnotationDefault} attributes. + */ +public final class AttAnnotationDefault extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "AnnotationDefault"; + + /** {@code non-null;} the annotation default value */ + private final Constant value; + + /** {@code >= 0;} attribute data length in the original classfile (not + * including the attribute header) */ + private final int byteLength; + + /** + * Constructs an instance. + * + * @param value {@code non-null;} the annotation default value + * @param byteLength {@code >= 0;} attribute data length in the original + * classfile (not including the attribute header) + */ + public AttAnnotationDefault(Constant value, int byteLength) { + super(ATTRIBUTE_NAME); + + if (value == null) { + throw new NullPointerException("value == null"); + } + + this.value = value; + this.byteLength = byteLength; + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + // Add six for the standard attribute header. + return byteLength + 6; + } + + /** + * Gets the annotation default value. + * + * @return {@code non-null;} the value + */ + public Constant getValue() { + return value; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttBootstrapMethods.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttBootstrapMethods.java new file mode 100644 index 00000000..803a94ac --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttBootstrapMethods.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 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. + */ +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.cf.code.BootstrapMethodsList; + +/** + * Attribute class for standard {@code AttBootstrapMethods} attributes. + */ +public class AttBootstrapMethods extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "BootstrapMethods"; + + private static final int ATTRIBUTE_HEADER_BYTES = 8; + private static final int BOOTSTRAP_METHOD_BYTES = 4; + private static final int BOOTSTRAP_ARGUMENT_BYTES = 2; + + private final BootstrapMethodsList bootstrapMethods; + + private final int byteLength; + + public AttBootstrapMethods(BootstrapMethodsList bootstrapMethods) { + super(ATTRIBUTE_NAME); + this.bootstrapMethods = bootstrapMethods; + + int bytes = ATTRIBUTE_HEADER_BYTES + bootstrapMethods.size() * BOOTSTRAP_METHOD_BYTES; + for (int i = 0; i < bootstrapMethods.size(); ++i) { + int numberOfArguments = bootstrapMethods.get(i).getBootstrapMethodArguments().size(); + bytes += numberOfArguments * BOOTSTRAP_ARGUMENT_BYTES; + } + this.byteLength = bytes; + } + + @Override + public int byteLength() { + return byteLength; + } + + /** + * Get the bootstrap methods present in attribute. + * @return bootstrap methods list + */ + public BootstrapMethodsList getBootstrapMethods() { + return bootstrapMethods; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttCode.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttCode.java new file mode 100644 index 00000000..0d23781b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttCode.java @@ -0,0 +1,146 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.cf.code.ByteCatchList; +import external.com.android.dx.cf.code.BytecodeArray; +import external.com.android.dx.cf.iface.AttributeList; +import external.com.android.dx.util.MutabilityException; + +/** + * Attribute class for standard {@code Code} attributes. + */ +public final class AttCode extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "Code"; + + /** {@code >= 0;} the stack size */ + private final int maxStack; + + /** {@code >= 0;} the number of locals */ + private final int maxLocals; + + /** {@code non-null;} array containing the bytecode per se */ + private final BytecodeArray code; + + /** {@code non-null;} the exception table */ + private final ByteCatchList catches; + + /** {@code non-null;} the associated list of attributes */ + private final AttributeList attributes; + + /** + * Constructs an instance. + * + * @param maxStack {@code >= 0;} the stack size + * @param maxLocals {@code >= 0;} the number of locals + * @param code {@code non-null;} array containing the bytecode per se + * @param catches {@code non-null;} the exception table + * @param attributes {@code non-null;} the associated list of attributes + */ + public AttCode(int maxStack, int maxLocals, BytecodeArray code, + ByteCatchList catches, AttributeList attributes) { + super(ATTRIBUTE_NAME); + + if (maxStack < 0) { + throw new IllegalArgumentException("maxStack < 0"); + } + + if (maxLocals < 0) { + throw new IllegalArgumentException("maxLocals < 0"); + } + + if (code == null) { + throw new NullPointerException("code == null"); + } + + try { + if (catches.isMutable()) { + throw new MutabilityException("catches.isMutable()"); + } + } catch (NullPointerException ex) { + // Translate the exception. + throw new NullPointerException("catches == null"); + } + + try { + if (attributes.isMutable()) { + throw new MutabilityException("attributes.isMutable()"); + } + } catch (NullPointerException ex) { + // Translate the exception. + throw new NullPointerException("attributes == null"); + } + + this.maxStack = maxStack; + this.maxLocals = maxLocals; + this.code = code; + this.catches = catches; + this.attributes = attributes; + } + + @Override + public int byteLength() { + return 10 + code.byteLength() + catches.byteLength() + + attributes.byteLength(); + } + + /** + * Gets the maximum stack size. + * + * @return {@code >= 0;} the maximum stack size + */ + public int getMaxStack() { + return maxStack; + } + + /** + * Gets the number of locals. + * + * @return {@code >= 0;} the number of locals + */ + public int getMaxLocals() { + return maxLocals; + } + + /** + * Gets the bytecode array. + * + * @return {@code non-null;} the bytecode array + */ + public BytecodeArray getCode() { + return code; + } + + /** + * Gets the exception table. + * + * @return {@code non-null;} the exception table + */ + public ByteCatchList getCatches() { + return catches; + } + + /** + * Gets the associated attribute list. + * + * @return {@code non-null;} the attribute list + */ + public AttributeList getAttributes() { + return attributes; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttConstantValue.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttConstantValue.java new file mode 100644 index 00000000..325ff273 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttConstantValue.java @@ -0,0 +1,78 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.cst.CstDouble; +import external.com.android.dx.rop.cst.CstFloat; +import external.com.android.dx.rop.cst.CstInteger; +import external.com.android.dx.rop.cst.CstLong; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.TypedConstant; + +/** + * Attribute class for standard {@code ConstantValue} attributes. + */ +public final class AttConstantValue extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "ConstantValue"; + + /** {@code non-null;} the constant value */ + private final TypedConstant constantValue; + + /** + * Constructs an instance. + * + * @param constantValue {@code non-null;} the constant value, which must + * be an instance of one of: {@code CstString}, + * {@code CstInteger}, {@code CstLong}, + * {@code CstFloat}, or {@code CstDouble} + */ + public AttConstantValue(TypedConstant constantValue) { + super(ATTRIBUTE_NAME); + + if (!((constantValue instanceof CstString) || + (constantValue instanceof CstInteger) || + (constantValue instanceof CstLong) || + (constantValue instanceof CstFloat) || + (constantValue instanceof CstDouble))) { + if (constantValue == null) { + throw new NullPointerException("constantValue == null"); + } + throw new IllegalArgumentException("bad type for constantValue"); + } + + this.constantValue = constantValue; + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + return 8; + } + + /** + * Gets the constant value of this instance. The returned value + * is an instance of one of: {@code CstString}, + * {@code CstInteger}, {@code CstLong}, + * {@code CstFloat}, or {@code CstDouble}. + * + * @return {@code non-null;} the constant value + */ + public TypedConstant getConstantValue() { + return constantValue; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttDeprecated.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttDeprecated.java new file mode 100644 index 00000000..e54e097a --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttDeprecated.java @@ -0,0 +1,38 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +/** + * Attribute class for standard {@code Deprecated} attributes. + */ +public final class AttDeprecated extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "Deprecated"; + + /** + * Constructs an instance. + */ + public AttDeprecated() { + super(ATTRIBUTE_NAME); + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + return 6; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttEnclosingMethod.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttEnclosingMethod.java new file mode 100644 index 00000000..da43f46d --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttEnclosingMethod.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstType; + +/** + * Attribute class for standards-track {@code EnclosingMethod} + * attributes. + */ +public final class AttEnclosingMethod extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "EnclosingMethod"; + + /** {@code non-null;} the innermost enclosing class */ + private final CstType type; + + /** {@code null-ok;} the name-and-type of the innermost enclosing method, if any */ + private final CstNat method; + + /** + * Constructs an instance. + * + * @param type {@code non-null;} the innermost enclosing class + * @param method {@code null-ok;} the name-and-type of the innermost enclosing + * method, if any + */ + public AttEnclosingMethod(CstType type, CstNat method) { + super(ATTRIBUTE_NAME); + + if (type == null) { + throw new NullPointerException("type == null"); + } + + this.type = type; + this.method = method; + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + return 10; + } + + /** + * Gets the innermost enclosing class. + * + * @return {@code non-null;} the innermost enclosing class + */ + public CstType getEnclosingClass() { + return type; + } + + /** + * Gets the name-and-type of the innermost enclosing method, if + * any. + * + * @return {@code null-ok;} the name-and-type of the innermost enclosing + * method, if any + */ + public CstNat getMethod() { + return method; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttExceptions.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttExceptions.java new file mode 100644 index 00000000..011490df --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttExceptions.java @@ -0,0 +1,69 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.type.TypeList; +import external.com.android.dx.util.MutabilityException; + +/** + * Attribute class for standard {@code Exceptions} attributes. + */ +public final class AttExceptions extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "Exceptions"; + + /** {@code non-null;} list of exception classes */ + private final TypeList exceptions; + + /** + * Constructs an instance. + * + * @param exceptions {@code non-null;} list of classes, presumed but not + * verified to be subclasses of {@code Throwable} + */ + public AttExceptions(TypeList exceptions) { + super(ATTRIBUTE_NAME); + + try { + if (exceptions.isMutable()) { + throw new MutabilityException("exceptions.isMutable()"); + } + } catch (NullPointerException ex) { + // Translate the exception. + throw new NullPointerException("exceptions == null"); + } + + this.exceptions = exceptions; + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + return 8 + exceptions.size() * 2; + } + + /** + * Gets the list of classes associated with this instance. In + * general, these classes are not pre-verified to be subclasses of + * {@code Throwable}. + * + * @return {@code non-null;} the list of classes + */ + public TypeList getExceptions() { + return exceptions; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttInnerClasses.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttInnerClasses.java new file mode 100644 index 00000000..2809f35b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttInnerClasses.java @@ -0,0 +1,65 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.util.MutabilityException; + +/** + * Attribute class for standard {@code InnerClasses} attributes. + */ +public final class AttInnerClasses extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "InnerClasses"; + + /** {@code non-null;} list of inner class entries */ + private final InnerClassList innerClasses; + + /** + * Constructs an instance. + * + * @param innerClasses {@code non-null;} list of inner class entries + */ + public AttInnerClasses(InnerClassList innerClasses) { + super(ATTRIBUTE_NAME); + + try { + if (innerClasses.isMutable()) { + throw new MutabilityException("innerClasses.isMutable()"); + } + } catch (NullPointerException ex) { + // Translate the exception. + throw new NullPointerException("innerClasses == null"); + } + + this.innerClasses = innerClasses; + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + return 8 + innerClasses.size() * 8; + } + + /** + * Gets the list of "inner class" entries associated with this instance. + * + * @return {@code non-null;} the list + */ + public InnerClassList getInnerClasses() { + return innerClasses; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLineNumberTable.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLineNumberTable.java new file mode 100644 index 00000000..6e300224 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLineNumberTable.java @@ -0,0 +1,66 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.cf.code.LineNumberList; +import external.com.android.dx.util.MutabilityException; + +/** + * Attribute class for standard {@code LineNumberTable} attributes. + */ +public final class AttLineNumberTable extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "LineNumberTable"; + + /** {@code non-null;} list of line number entries */ + private final LineNumberList lineNumbers; + + /** + * Constructs an instance. + * + * @param lineNumbers {@code non-null;} list of line number entries + */ + public AttLineNumberTable(LineNumberList lineNumbers) { + super(ATTRIBUTE_NAME); + + try { + if (lineNumbers.isMutable()) { + throw new MutabilityException("lineNumbers.isMutable()"); + } + } catch (NullPointerException ex) { + // Translate the exception. + throw new NullPointerException("lineNumbers == null"); + } + + this.lineNumbers = lineNumbers; + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + return 8 + 4 * lineNumbers.size(); + } + + /** + * Gets the list of "line number" entries associated with this instance. + * + * @return {@code non-null;} the list + */ + public LineNumberList getLineNumbers() { + return lineNumbers; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLocalVariableTable.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLocalVariableTable.java new file mode 100644 index 00000000..9aa1dd96 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLocalVariableTable.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.cf.code.LocalVariableList; + +/** + * Attribute class for standard {@code LocalVariableTable} attributes. + */ +public final class AttLocalVariableTable extends BaseLocalVariables { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "LocalVariableTable"; + + /** + * Constructs an instance. + * + * @param localVariables {@code non-null;} list of local variable entries + */ + public AttLocalVariableTable(LocalVariableList localVariables) { + super(ATTRIBUTE_NAME, localVariables); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLocalVariableTypeTable.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLocalVariableTypeTable.java new file mode 100644 index 00000000..3673464b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttLocalVariableTypeTable.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.cf.code.LocalVariableList; + +/** + * Attribute class for standard {@code LocalVariableTypeTable} attributes. + */ +public final class AttLocalVariableTypeTable extends BaseLocalVariables { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "LocalVariableTypeTable"; + + /** + * Constructs an instance. + * + * @param localVariables {@code non-null;} list of local variable entries + */ + public AttLocalVariableTypeTable(LocalVariableList localVariables) { + super(ATTRIBUTE_NAME, localVariables); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeInvisibleAnnotations.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeInvisibleAnnotations.java new file mode 100644 index 00000000..1237ad73 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeInvisibleAnnotations.java @@ -0,0 +1,40 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.annotation.Annotations; + +/** + * Attribute class for standard {@code RuntimeInvisibleAnnotations} + * attributes. + */ +public final class AttRuntimeInvisibleAnnotations extends BaseAnnotations { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "RuntimeInvisibleAnnotations"; + + /** + * Constructs an instance. + * + * @param annotations {@code non-null;} the list of annotations + * @param byteLength {@code >= 0;} attribute data length in the original + * classfile (not including the attribute header) + */ + public AttRuntimeInvisibleAnnotations(Annotations annotations, + int byteLength) { + super(ATTRIBUTE_NAME, annotations, byteLength); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeInvisibleParameterAnnotations.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeInvisibleParameterAnnotations.java new file mode 100644 index 00000000..d729e358 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeInvisibleParameterAnnotations.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.annotation.AnnotationsList; + +/** + * Attribute class for standard + * {@code RuntimeInvisibleParameterAnnotations} attributes. + */ +public final class AttRuntimeInvisibleParameterAnnotations + extends BaseParameterAnnotations { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = + "RuntimeInvisibleParameterAnnotations"; + + /** + * Constructs an instance. + * + * @param parameterAnnotations {@code non-null;} the parameter annotations + * @param byteLength {@code >= 0;} attribute data length in the original + * classfile (not including the attribute header) + */ + public AttRuntimeInvisibleParameterAnnotations( + AnnotationsList parameterAnnotations, int byteLength) { + super(ATTRIBUTE_NAME, parameterAnnotations, byteLength); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeVisibleAnnotations.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeVisibleAnnotations.java new file mode 100644 index 00000000..3b553eac --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeVisibleAnnotations.java @@ -0,0 +1,40 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.annotation.Annotations; + +/** + * Attribute class for standard {@code RuntimeVisibleAnnotations} + * attributes. + */ +public final class AttRuntimeVisibleAnnotations extends BaseAnnotations { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "RuntimeVisibleAnnotations"; + + /** + * Constructs an instance. + * + * @param annotations {@code non-null;} the list of annotations + * @param byteLength {@code >= 0;} attribute data length in the original + * classfile (not including the attribute header) + */ + public AttRuntimeVisibleAnnotations(Annotations annotations, + int byteLength) { + super(ATTRIBUTE_NAME, annotations, byteLength); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeVisibleParameterAnnotations.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeVisibleParameterAnnotations.java new file mode 100644 index 00000000..0f9e4831 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttRuntimeVisibleParameterAnnotations.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.annotation.AnnotationsList; + +/** + * Attribute class for standard {@code RuntimeVisibleParameterAnnotations} + * attributes. + */ +public final class AttRuntimeVisibleParameterAnnotations + extends BaseParameterAnnotations { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = + "RuntimeVisibleParameterAnnotations"; + + /** + * Constructs an instance. + * + * @param annotations {@code non-null;} the parameter annotations + * @param byteLength {@code >= 0;} attribute data length in the original + * classfile (not including the attribute header) + */ + public AttRuntimeVisibleParameterAnnotations( + AnnotationsList annotations, int byteLength) { + super(ATTRIBUTE_NAME, annotations, byteLength); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSignature.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSignature.java new file mode 100644 index 00000000..1a1eb012 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSignature.java @@ -0,0 +1,60 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.cst.CstString; + +/** + * Attribute class for standards-track {@code Signature} attributes. + */ +public final class AttSignature extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "Signature"; + + /** {@code non-null;} the signature string */ + private final CstString signature; + + /** + * Constructs an instance. + * + * @param signature {@code non-null;} the signature string + */ + public AttSignature(CstString signature) { + super(ATTRIBUTE_NAME); + + if (signature == null) { + throw new NullPointerException("signature == null"); + } + + this.signature = signature; + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + return 8; + } + + /** + * Gets the signature string. + * + * @return {@code non-null;} the signature string + */ + public CstString getSignature() { + return signature; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSourceDebugExtension.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSourceDebugExtension.java new file mode 100644 index 00000000..44717e66 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSourceDebugExtension.java @@ -0,0 +1,62 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.cst.CstString; + +/** + * Attribute class for standard {@code SourceDebugExtension} attributes. + */ +public final class AttSourceDebugExtension extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "SourceDebugExtension"; + + /** {@code non-null;} Contents of SMAP */ + private final CstString smapString; + + /** + * Constructs an instance. + * + * @param smapString {@code non-null;} the SMAP data from the class file. + */ + public AttSourceDebugExtension(CstString smapString) { + super(ATTRIBUTE_NAME); + + if (smapString == null) { + throw new NullPointerException("smapString == null"); + } + + this.smapString = smapString; + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + // Add 6 for the standard attribute header: the attribute name + // index (2 bytes) and the attribute length (4 bytes). + return 6 + smapString.getUtf8Size(); + } + + /** + * Gets the SMAP data of this instance. + * + * @return {@code non-null;} the SMAP data. + */ + public CstString getSmapString() { + return smapString; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSourceFile.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSourceFile.java new file mode 100644 index 00000000..54dc77fd --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSourceFile.java @@ -0,0 +1,60 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.cst.CstString; + +/** + * Attribute class for standard {@code SourceFile} attributes. + */ +public final class AttSourceFile extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "SourceFile"; + + /** {@code non-null;} name of the source file */ + private final CstString sourceFile; + + /** + * Constructs an instance. + * + * @param sourceFile {@code non-null;} the name of the source file + */ + public AttSourceFile(CstString sourceFile) { + super(ATTRIBUTE_NAME); + + if (sourceFile == null) { + throw new NullPointerException("sourceFile == null"); + } + + this.sourceFile = sourceFile; + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + return 8; + } + + /** + * Gets the source file name of this instance. + * + * @return {@code non-null;} the source file + */ + public CstString getSourceFile() { + return sourceFile; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSynthetic.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSynthetic.java new file mode 100644 index 00000000..525da01c --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/AttSynthetic.java @@ -0,0 +1,38 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +/** + * Attribute class for standard {@code Synthetic} attributes. + */ +public final class AttSynthetic extends BaseAttribute { + /** {@code non-null;} attribute name for attributes of this type */ + public static final String ATTRIBUTE_NAME = "Synthetic"; + + /** + * Constructs an instance. + */ + public AttSynthetic() { + super(ATTRIBUTE_NAME); + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + return 6; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseAnnotations.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseAnnotations.java new file mode 100644 index 00000000..1a4b0f2b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseAnnotations.java @@ -0,0 +1,73 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.annotation.Annotations; +import external.com.android.dx.util.MutabilityException; + +/** + * Base class for annotations attributes. + */ +public abstract class BaseAnnotations extends BaseAttribute { + /** {@code non-null;} list of annotations */ + private final Annotations annotations; + + /** {@code >= 0;} attribute data length in the original classfile (not + * including the attribute header) */ + private final int byteLength; + + /** + * Constructs an instance. + * + * @param attributeName {@code non-null;} the name of the attribute + * @param annotations {@code non-null;} the list of annotations + * @param byteLength {@code >= 0;} attribute data length in the original + * classfile (not including the attribute header) + */ + public BaseAnnotations(String attributeName, Annotations annotations, + int byteLength) { + super(attributeName); + + try { + if (annotations.isMutable()) { + throw new MutabilityException("annotations.isMutable()"); + } + } catch (NullPointerException ex) { + // Translate the exception. + throw new NullPointerException("annotations == null"); + } + + this.annotations = annotations; + this.byteLength = byteLength; + } + + /** {@inheritDoc} */ + @Override + public final int byteLength() { + // Add six for the standard attribute header. + return byteLength + 6; + } + + /** + * Gets the list of annotations associated with this instance. + * + * @return {@code non-null;} the list + */ + public final Annotations getAnnotations() { + return annotations; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseAttribute.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseAttribute.java new file mode 100644 index 00000000..8bc247dd --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseAttribute.java @@ -0,0 +1,47 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.cf.iface.Attribute; + +/** + * Base implementation of {@link Attribute}, which directly stores + * the attribute name but leaves the rest up to subclasses. + */ +public abstract class BaseAttribute implements Attribute { + /** {@code non-null;} attribute name */ + private final String name; + + /** + * Constructs an instance. + * + * @param name {@code non-null;} attribute name + */ + public BaseAttribute(String name) { + if (name == null) { + throw new NullPointerException("name == null"); + } + + this.name = name; + } + + /** {@inheritDoc} */ + @Override + public String getName() { + return name; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseLocalVariables.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseLocalVariables.java new file mode 100644 index 00000000..d50d09c8 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseLocalVariables.java @@ -0,0 +1,66 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.cf.code.LocalVariableList; +import external.com.android.dx.util.MutabilityException; + +/** + * Base attribute class for standard {@code LocalVariableTable} + * and {@code LocalVariableTypeTable} attributes. + */ +public abstract class BaseLocalVariables extends BaseAttribute { + /** {@code non-null;} list of local variable entries */ + private final LocalVariableList localVariables; + + /** + * Constructs an instance. + * + * @param name {@code non-null;} attribute name + * @param localVariables {@code non-null;} list of local variable entries + */ + public BaseLocalVariables(String name, + LocalVariableList localVariables) { + super(name); + + try { + if (localVariables.isMutable()) { + throw new MutabilityException("localVariables.isMutable()"); + } + } catch (NullPointerException ex) { + // Translate the exception. + throw new NullPointerException("localVariables == null"); + } + + this.localVariables = localVariables; + } + + /** {@inheritDoc} */ + @Override + public final int byteLength() { + return 8 + localVariables.size() * 10; + } + + /** + * Gets the list of "local variable" entries associated with this instance. + * + * @return {@code non-null;} the list + */ + public final LocalVariableList getLocalVariables() { + return localVariables; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseParameterAnnotations.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseParameterAnnotations.java new file mode 100644 index 00000000..643f8f0d --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/BaseParameterAnnotations.java @@ -0,0 +1,74 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.annotation.AnnotationsList; +import external.com.android.dx.util.MutabilityException; + +/** + * Base class for parameter annotation list attributes. + */ +public abstract class BaseParameterAnnotations extends BaseAttribute { + /** {@code non-null;} list of annotations */ + private final AnnotationsList parameterAnnotations; + + /** {@code >= 0;} attribute data length in the original classfile (not + * including the attribute header) */ + private final int byteLength; + + /** + * Constructs an instance. + * + * @param attributeName {@code non-null;} the name of the attribute + * @param parameterAnnotations {@code non-null;} the annotations + * @param byteLength {@code >= 0;} attribute data length in the original + * classfile (not including the attribute header) + */ + public BaseParameterAnnotations(String attributeName, + AnnotationsList parameterAnnotations, int byteLength) { + super(attributeName); + + try { + if (parameterAnnotations.isMutable()) { + throw new MutabilityException( + "parameterAnnotations.isMutable()"); + } + } catch (NullPointerException ex) { + // Translate the exception. + throw new NullPointerException("parameterAnnotations == null"); + } + + this.parameterAnnotations = parameterAnnotations; + this.byteLength = byteLength; + } + + /** {@inheritDoc} */ + @Override + public final int byteLength() { + // Add six for the standard attribute header. + return byteLength + 6; + } + + /** + * Gets the list of annotation lists associated with this instance. + * + * @return {@code non-null;} the list + */ + public final AnnotationsList getParameterAnnotations() { + return parameterAnnotations; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/InnerClassList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/InnerClassList.java new file mode 100644 index 00000000..a91554f3 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/InnerClassList.java @@ -0,0 +1,137 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.util.FixedSizeList; + +/** + * List of "inner class" entries, which are the contents of + * {@code InnerClasses} attributes. + */ +public final class InnerClassList extends FixedSizeList { + /** + * Constructs an instance. + * + * @param count the number of elements to be in the list of inner classes + */ + public InnerClassList(int count) { + super(count); + } + + /** + * Gets the indicated item. + * + * @param n {@code >= 0;} which item + * @return {@code null-ok;} the indicated item + */ + public Item get(int n) { + return (Item) get0(n); + } + + /** + * Sets the item at the given index. + * + * @param n {@code >= 0, < size();} which class + * @param innerClass {@code non-null;} class this item refers to + * @param outerClass {@code null-ok;} outer class that this class is a + * member of, if any + * @param innerName {@code null-ok;} original simple name of this class, + * if not anonymous + * @param accessFlags original declared access flags + */ + public void set(int n, CstType innerClass, CstType outerClass, + CstString innerName, int accessFlags) { + set0(n, new Item(innerClass, outerClass, innerName, accessFlags)); + } + + /** + * Item in an inner classes list. + */ + public static class Item { + /** {@code non-null;} class this item refers to */ + private final CstType innerClass; + + /** {@code null-ok;} outer class that this class is a member of, if any */ + private final CstType outerClass; + + /** {@code null-ok;} original simple name of this class, if not anonymous */ + private final CstString innerName; + + /** original declared access flags */ + private final int accessFlags; + + /** + * Constructs an instance. + * + * @param innerClass {@code non-null;} class this item refers to + * @param outerClass {@code null-ok;} outer class that this class is a + * member of, if any + * @param innerName {@code null-ok;} original simple name of this + * class, if not anonymous + * @param accessFlags original declared access flags + */ + public Item(CstType innerClass, CstType outerClass, + CstString innerName, int accessFlags) { + if (innerClass == null) { + throw new NullPointerException("innerClass == null"); + } + + this.innerClass = innerClass; + this.outerClass = outerClass; + this.innerName = innerName; + this.accessFlags = accessFlags; + } + + /** + * Gets the class this item refers to. + * + * @return {@code non-null;} the class + */ + public CstType getInnerClass() { + return innerClass; + } + + /** + * Gets the outer class that this item's class is a member of, if any. + * + * @return {@code null-ok;} the class + */ + public CstType getOuterClass() { + return outerClass; + } + + /** + * Gets the original name of this item's class, if not anonymous. + * + * @return {@code null-ok;} the name + */ + public CstString getInnerName() { + return innerName; + } + + /** + * Gets the original declared access flags. + * + * @return the access flags + */ + public int getAccessFlags() { + return accessFlags; + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/RawAttribute.java b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/RawAttribute.java new file mode 100644 index 00000000..e3e9fd9f --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/RawAttribute.java @@ -0,0 +1,92 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.attrib; + +import external.com.android.dx.rop.cst.ConstantPool; +import external.com.android.dx.util.ByteArray; + +/** + * Raw attribute, for holding onto attributes that are unrecognized. + */ +public final class RawAttribute extends BaseAttribute { + /** {@code non-null;} attribute data */ + private final ByteArray data; + + /** + * {@code null-ok;} constant pool to use for resolution of cpis in {@link + * #data} + */ + private final ConstantPool pool; + + /** + * Constructs an instance. + * + * @param name {@code non-null;} attribute name + * @param data {@code non-null;} attribute data + * @param pool {@code null-ok;} constant pool to use for cpi resolution + */ + public RawAttribute(String name, ByteArray data, ConstantPool pool) { + super(name); + + if (data == null) { + throw new NullPointerException("data == null"); + } + + this.data = data; + this.pool = pool; + } + + /** + * Constructs an instance from a sub-array of a {@link ByteArray}. + * + * @param name {@code non-null;} attribute name + * @param data {@code non-null;} array containing the attribute data + * @param offset offset in {@code data} to the attribute data + * @param length length of the attribute data, in bytes + * @param pool {@code null-ok;} constant pool to use for cpi resolution + */ + public RawAttribute(String name, ByteArray data, int offset, + int length, ConstantPool pool) { + this(name, data.slice(offset, offset + length), pool); + } + + /** + * Get the raw data of the attribute. + * + * @return {@code non-null;} the data + */ + public ByteArray getData() { + return data; + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + return data.size() + 6; + } + + /** + * Gets the constant pool to use for cpi resolution, if any. It + * presumably came from the class file that this attribute came + * from. + * + * @return {@code null-ok;} the constant pool + */ + public ConstantPool getPool() { + return pool; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/package.html b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/package.html new file mode 100644 index 00000000..727ae5cd --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/attrib/package.html @@ -0,0 +1,11 @@ + +

Implementation of containers and utilities for all the standard Java +attribute types.

+ +

PACKAGES USED: +

    +
  • external.com.android.dx.cf.iface
  • +
  • external.com.android.dx.rop.pool
  • +
  • external.com.android.dx.util
  • +
+ diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/BaseMachine.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/BaseMachine.java new file mode 100644 index 00000000..59c06ded --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/BaseMachine.java @@ -0,0 +1,595 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.rop.code.LocalItem; +import external.com.android.dx.rop.code.RegisterSpec; +import external.com.android.dx.rop.cst.Constant; +import external.com.android.dx.rop.type.Prototype; +import external.com.android.dx.rop.type.StdTypeList; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.rop.type.TypeBearer; +import java.util.ArrayList; + +/** + * Base implementation of {@link Machine}. + * + *

Note: For the most part, the documentation for this class + * ignores the distinction between {@link Type} and {@link + * TypeBearer}.

+ */ +public abstract class BaseMachine implements Machine { + /* {@code non-null;} the prototype for the associated method */ + private final Prototype prototype; + + /** {@code non-null;} primary arguments */ + private TypeBearer[] args; + + /** {@code >= 0;} number of primary arguments */ + private int argCount; + + /** {@code null-ok;} type of the operation, if salient */ + private Type auxType; + + /** auxiliary {@code int} argument */ + private int auxInt; + + /** {@code null-ok;} auxiliary constant argument */ + private Constant auxCst; + + /** auxiliary branch target argument */ + private int auxTarget; + + /** {@code null-ok;} auxiliary switch cases argument */ + private SwitchList auxCases; + + /** {@code null-ok;} auxiliary initial value list for newarray */ + private ArrayList auxInitValues; + + /** {@code >= -1;} last local accessed */ + private int localIndex; + + /** specifies if local has info in the local variable table */ + private boolean localInfo; + + /** {@code null-ok;} local target spec, if salient and calculated */ + private RegisterSpec localTarget; + + /** {@code non-null;} results */ + private TypeBearer[] results; + + /** + * {@code >= -1;} count of the results, or {@code -1} if no results + * have been set + */ + private int resultCount; + + /** + * Constructs an instance. + * + * @param prototype {@code non-null;} the prototype for the + * associated method + */ + public BaseMachine(Prototype prototype) { + if (prototype == null) { + throw new NullPointerException("prototype == null"); + } + + this.prototype = prototype; + args = new TypeBearer[10]; + results = new TypeBearer[6]; + clearArgs(); + } + + /** {@inheritDoc} */ + @Override + public Prototype getPrototype() { + return prototype; + } + + /** {@inheritDoc} */ + @Override + public final void clearArgs() { + argCount = 0; + auxType = null; + auxInt = 0; + auxCst = null; + auxTarget = 0; + auxCases = null; + auxInitValues = null; + localIndex = -1; + localInfo = false; + localTarget = null; + resultCount = -1; + } + + /** {@inheritDoc} */ + @Override + public final void popArgs(Frame frame, int count) { + ExecutionStack stack = frame.getStack(); + + clearArgs(); + + if (count > args.length) { + // Grow args, and add a little extra room to grow even more. + args = new TypeBearer[count + 10]; + } + + for (int i = count - 1; i >= 0; i--) { + args[i] = stack.pop(); + } + + argCount = count; + } + + /** {@inheritDoc} */ + @Override + public void popArgs(Frame frame, Prototype prototype) { + StdTypeList types = prototype.getParameterTypes(); + int size = types.size(); + + // Use the above method to do the actual popping... + popArgs(frame, size); + + // ...and then verify the popped types. + + for (int i = 0; i < size; i++) { + if (! Merger.isPossiblyAssignableFrom(types.getType(i), args[i])) { + throw new SimException("at stack depth " + (size - 1 - i) + + ", expected type " + types.getType(i).toHuman() + + " but found " + args[i].getType().toHuman()); + } + } + } + + @Override + public final void popArgs(Frame frame, Type type) { + // Use the above method to do the actual popping... + popArgs(frame, 1); + + // ...and then verify the popped type. + if (! Merger.isPossiblyAssignableFrom(type, args[0])) { + throw new SimException("expected type " + type.toHuman() + + " but found " + args[0].getType().toHuman()); + } + } + + /** {@inheritDoc} */ + @Override + public final void popArgs(Frame frame, Type type1, Type type2) { + // Use the above method to do the actual popping... + popArgs(frame, 2); + + // ...and then verify the popped types. + + if (! Merger.isPossiblyAssignableFrom(type1, args[0])) { + throw new SimException("expected type " + type1.toHuman() + + " but found " + args[0].getType().toHuman()); + } + + if (! Merger.isPossiblyAssignableFrom(type2, args[1])) { + throw new SimException("expected type " + type2.toHuman() + + " but found " + args[1].getType().toHuman()); + } + } + + /** {@inheritDoc} */ + @Override + public final void popArgs(Frame frame, Type type1, Type type2, + Type type3) { + // Use the above method to do the actual popping... + popArgs(frame, 3); + + // ...and then verify the popped types. + + if (! Merger.isPossiblyAssignableFrom(type1, args[0])) { + throw new SimException("expected type " + type1.toHuman() + + " but found " + args[0].getType().toHuman()); + } + + if (! Merger.isPossiblyAssignableFrom(type2, args[1])) { + throw new SimException("expected type " + type2.toHuman() + + " but found " + args[1].getType().toHuman()); + } + + if (! Merger.isPossiblyAssignableFrom(type3, args[2])) { + throw new SimException("expected type " + type3.toHuman() + + " but found " + args[2].getType().toHuman()); + } + } + + /** {@inheritDoc} */ + @Override + public final void localArg(Frame frame, int idx) { + clearArgs(); + args[0] = frame.getLocals().get(idx); + argCount = 1; + localIndex = idx; + } + + /** {@inheritDoc} */ + @Override + public final void localInfo(boolean local) { + localInfo = local; + } + + /** {@inheritDoc} */ + @Override + public final void auxType(Type type) { + auxType = type; + } + + /** {@inheritDoc} */ + @Override + public final void auxIntArg(int value) { + auxInt = value; + } + + /** {@inheritDoc} */ + @Override + public final void auxCstArg(Constant cst) { + if (cst == null) { + throw new NullPointerException("cst == null"); + } + + auxCst = cst; + } + + /** {@inheritDoc} */ + @Override + public final void auxTargetArg(int target) { + auxTarget = target; + } + + /** {@inheritDoc} */ + @Override + public final void auxSwitchArg(SwitchList cases) { + if (cases == null) { + throw new NullPointerException("cases == null"); + } + + auxCases = cases; + } + + /** {@inheritDoc} */ + @Override + public final void auxInitValues(ArrayList initValues) { + auxInitValues = initValues; + } + + /** {@inheritDoc} */ + @Override + public final void localTarget(int idx, Type type, LocalItem local) { + localTarget = RegisterSpec.makeLocalOptional(idx, type, local); + } + + /** + * Gets the number of primary arguments. + * + * @return {@code >= 0;} the number of primary arguments + */ + protected final int argCount() { + return argCount; + } + + /** + * Gets the width of the arguments (where a category-2 value counts as + * two). + * + * @return {@code >= 0;} the argument width + */ + protected final int argWidth() { + int result = 0; + + for (int i = 0; i < argCount; i++) { + result += args[i].getType().getCategory(); + } + + return result; + } + + /** + * Gets the {@code n}th primary argument. + * + * @param n {@code >= 0, < argCount();} which argument + * @return {@code non-null;} the indicated argument + */ + protected final TypeBearer arg(int n) { + if (n >= argCount) { + throw new IllegalArgumentException("n >= argCount"); + } + + try { + return args[n]; + } catch (ArrayIndexOutOfBoundsException ex) { + // Translate the exception. + throw new IllegalArgumentException("n < 0"); + } + } + + /** + * Gets the type auxiliary argument. + * + * @return {@code null-ok;} the salient type + */ + protected final Type getAuxType() { + return auxType; + } + + /** + * Gets the {@code int} auxiliary argument. + * + * @return the argument value + */ + protected final int getAuxInt() { + return auxInt; + } + + /** + * Gets the constant auxiliary argument. + * + * @return {@code null-ok;} the argument value + */ + protected final Constant getAuxCst() { + return auxCst; + } + + /** + * Gets the branch target auxiliary argument. + * + * @return the argument value + */ + protected final int getAuxTarget() { + return auxTarget; + } + + /** + * Gets the switch cases auxiliary argument. + * + * @return {@code null-ok;} the argument value + */ + protected final SwitchList getAuxCases() { + return auxCases; + } + + /** + * Gets the init values auxiliary argument. + * + * @return {@code null-ok;} the argument value + */ + protected final ArrayList getInitValues() { + return auxInitValues; + } + /** + * Gets the last local index accessed. + * + * @return {@code >= -1;} the salient local index or {@code -1} if none + * was set since the last time {@link #clearArgs} was called + */ + protected final int getLocalIndex() { + return localIndex; + } + + /** + * Gets whether the loaded local has info in the local variable table. + * + * @return {@code true} if local arg has info in the local variable table + */ + protected final boolean getLocalInfo() { + return localInfo; + } + + /** + * Gets the target local register spec of the current operation, if any. + * The local target spec is the combination of the values indicated + * by a previous call to {@link #localTarget} with the type of what + * should be the sole result set by a call to {@link #setResult} (or + * the combination {@link #clearResult} then {@link #addResult}. + * + * @param isMove {@code true} if the operation being performed on the + * local is a move. This will cause constant values to be propagated + * to the returned local + * @return {@code null-ok;} the salient register spec or {@code null} if no + * local target was set since the last time {@link #clearArgs} was + * called + */ + protected final RegisterSpec getLocalTarget(boolean isMove) { + if (localTarget == null) { + return null; + } + + if (resultCount != 1) { + throw new SimException("local target with " + + ((resultCount == 0) ? "no" : "multiple") + " results"); + } + + TypeBearer result = results[0]; + Type resultType = result.getType(); + Type localType = localTarget.getType(); + + if (resultType == localType) { + /* + * If this is to be a move operation and the result is a + * known value, make the returned localTarget embody that + * value. + */ + if (isMove) { + return localTarget.withType(result); + } else { + return localTarget; + } + } + + if (! Merger.isPossiblyAssignableFrom(localType, resultType)) { + // The result and local types are inconsistent. Complain! + throwLocalMismatch(resultType, localType); + return null; + } + + if (localType == Type.OBJECT) { + /* + * The result type is more specific than the local type, + * so use that instead. + */ + localTarget = localTarget.withType(result); + } + + return localTarget; + } + + /** + * Clears the results. + */ + protected final void clearResult() { + resultCount = 0; + } + + /** + * Sets the results list to be the given single value. + * + *

Note: If there is more than one result value, the + * others may be added by using {@link #addResult}.

+ * + * @param result {@code non-null;} result value + */ + protected final void setResult(TypeBearer result) { + if (result == null) { + throw new NullPointerException("result == null"); + } + + results[0] = result; + resultCount = 1; + } + + /** + * Adds an additional element to the list of results. + * + * @see #setResult + * + * @param result {@code non-null;} result value + */ + protected final void addResult(TypeBearer result) { + if (result == null) { + throw new NullPointerException("result == null"); + } + + results[resultCount] = result; + resultCount++; + } + + /** + * Gets the count of results. This throws an exception if results were + * never set. (Explicitly clearing the results counts as setting them.) + * + * @return {@code >= 0;} the count + */ + protected final int resultCount() { + if (resultCount < 0) { + throw new SimException("results never set"); + } + + return resultCount; + } + + /** + * Gets the width of the results (where a category-2 value counts as + * two). + * + * @return {@code >= 0;} the result width + */ + protected final int resultWidth() { + int width = 0; + + for (int i = 0; i < resultCount; i++) { + width += results[i].getType().getCategory(); + } + + return width; + } + + /** + * Gets the {@code n}th result value. + * + * @param n {@code >= 0, < resultCount();} which result + * @return {@code non-null;} the indicated result value + */ + protected final TypeBearer result(int n) { + if (n >= resultCount) { + throw new IllegalArgumentException("n >= resultCount"); + } + + try { + return results[n]; + } catch (ArrayIndexOutOfBoundsException ex) { + // Translate the exception. + throw new IllegalArgumentException("n < 0"); + } + } + + /** + * Stores the results of the latest operation into the given frame. If + * there is a local target (see {@link #localTarget}), then the sole + * result is stored to that target; otherwise any results are pushed + * onto the stack. + * + * @param frame {@code non-null;} frame to operate on + */ + protected final void storeResults(Frame frame) { + if (resultCount < 0) { + throw new SimException("results never set"); + } + + if (resultCount == 0) { + // Nothing to do. + return; + } + + if (localTarget != null) { + /* + * Note: getLocalTarget() doesn't necessarily return + * localTarget directly. + */ + frame.getLocals().set(getLocalTarget(false)); + } else { + ExecutionStack stack = frame.getStack(); + for (int i = 0; i < resultCount; i++) { + if (localInfo) { + stack.setLocal(); + } + stack.push(results[i]); + } + } + } + + /** + * Throws an exception that indicates a mismatch in local variable + * types. + * + * @param found {@code non-null;} the encountered type + * @param local {@code non-null;} the local variable's claimed type + */ + public static void throwLocalMismatch(TypeBearer found, + TypeBearer local) { + throw new SimException("local variable type mismatch: " + + "attempt to set or access a value of type " + + found.toHuman() + + " using a local variable of type " + + local.toHuman() + + ". This is symptomatic of .class transformation tools " + + "that ignore local variable information."); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/BasicBlocker.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/BasicBlocker.java new file mode 100644 index 00000000..24ec4b2c --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/BasicBlocker.java @@ -0,0 +1,465 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.rop.cst.Constant; +import external.com.android.dx.rop.cst.CstInvokeDynamic; +import external.com.android.dx.rop.cst.CstMemberRef; +import external.com.android.dx.rop.cst.CstMethodHandle; +import external.com.android.dx.rop.cst.CstProtoRef; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.util.Bits; +import external.com.android.dx.util.IntList; +import java.util.ArrayList; + +/** + * Utility that identifies basic blocks in bytecode. + */ +public final class BasicBlocker implements BytecodeArray.Visitor { + /** {@code non-null;} method being converted */ + private final ConcreteMethod method; + + /** + * {@code non-null;} work set; bits indicate offsets in need of + * examination + */ + private final int[] workSet; + + /** + * {@code non-null;} live set; bits indicate potentially-live + * opcodes; contrawise, a bit that isn't on is either in the + * middle of an instruction or is a definitely-dead opcode + */ + private final int[] liveSet; + + /** + * {@code non-null;} block start set; bits indicate the starts of + * basic blocks, including the opcodes that start blocks of + * definitely-dead code + */ + private final int[] blockSet; + + /** + * {@code non-null, sparse;} for each instruction offset to a branch of + * some sort, the list of targets for that instruction + */ + private final IntList[] targetLists; + + /** + * {@code non-null, sparse;} for each instruction offset to a throwing + * instruction, the list of exception handlers for that instruction + */ + private final ByteCatchList[] catchLists; + + /** offset of the previously parsed bytecode */ + private int previousOffset; + + /** + * Identifies and enumerates the basic blocks in the given method, + * returning a list of them. The returned list notably omits any + * definitely-dead code that is identified in the process. + * + * @param method {@code non-null;} method to convert + * @return {@code non-null;} list of basic blocks + */ + public static ByteBlockList identifyBlocks(ConcreteMethod method) { + BasicBlocker bb = new BasicBlocker(method); + + bb.doit(); + return bb.getBlockList(); + } + + /** + * Constructs an instance. This class is not publicly instantiable; use + * {@link #identifyBlocks}. + * + * @param method {@code non-null;} method to convert + */ + private BasicBlocker(ConcreteMethod method) { + if (method == null) { + throw new NullPointerException("method == null"); + } + + this.method = method; + + /* + * The "+1" below is so the idx-past-end is also valid, + * avoiding a special case, but without preventing + * flow-of-control falling past the end of the method from + * getting properly reported. + */ + int sz = method.getCode().size() + 1; + + workSet = Bits.makeBitSet(sz); + liveSet = Bits.makeBitSet(sz); + blockSet = Bits.makeBitSet(sz); + targetLists = new IntList[sz]; + catchLists = new ByteCatchList[sz]; + previousOffset = -1; + } + + /* + * Note: These methods are defined implementation of the interface + * BytecodeArray.Visitor; since the class isn't publicly + * instantiable, no external code ever gets a chance to actually + * call these methods. + */ + + /** {@inheritDoc} */ + @Override + public void visitInvalid(int opcode, int offset, int length) { + visitCommon(offset, length, true); + } + + /** {@inheritDoc} */ + @Override + public void visitNoArgs(int opcode, int offset, int length, Type type) { + switch (opcode) { + case ByteOps.IRETURN: + case ByteOps.RETURN: { + visitCommon(offset, length, false); + targetLists[offset] = IntList.EMPTY; + break; + } + case ByteOps.ATHROW: { + visitCommon(offset, length, false); + visitThrowing(offset, length, false); + break; + } + case ByteOps.IALOAD: + case ByteOps.LALOAD: + case ByteOps.FALOAD: + case ByteOps.DALOAD: + case ByteOps.AALOAD: + case ByteOps.BALOAD: + case ByteOps.CALOAD: + case ByteOps.SALOAD: + case ByteOps.IASTORE: + case ByteOps.LASTORE: + case ByteOps.FASTORE: + case ByteOps.DASTORE: + case ByteOps.AASTORE: + case ByteOps.BASTORE: + case ByteOps.CASTORE: + case ByteOps.SASTORE: + case ByteOps.ARRAYLENGTH: + case ByteOps.MONITORENTER: + case ByteOps.MONITOREXIT: { + /* + * These instructions can all throw, so they have to end + * the block they appear in (since throws are branches). + */ + visitCommon(offset, length, true); + visitThrowing(offset, length, true); + break; + } + case ByteOps.IDIV: + case ByteOps.IREM: { + /* + * The int and long versions of division and remainder may + * throw, but not the other types. + */ + visitCommon(offset, length, true); + if ((type == Type.INT) || (type == Type.LONG)) { + visitThrowing(offset, length, true); + } + break; + } + default: { + visitCommon(offset, length, true); + break; + } + } + } + + /** {@inheritDoc} */ + @Override + public void visitLocal(int opcode, int offset, int length, + int idx, Type type, int value) { + if (opcode == ByteOps.RET) { + visitCommon(offset, length, false); + targetLists[offset] = IntList.EMPTY; + } else { + visitCommon(offset, length, true); + } + } + + /** {@inheritDoc} */ + @Override + public void visitConstant(int opcode, int offset, int length, + Constant cst, int value) { + visitCommon(offset, length, true); + + if (cst instanceof CstMemberRef || cst instanceof CstType || + cst instanceof CstString || cst instanceof CstInvokeDynamic || + cst instanceof CstMethodHandle || cst instanceof CstProtoRef) { + /* + * Instructions with these sorts of constants have the + * possibility of throwing, so this instruction needs to + * end its block (since it can throw, and possible-throws + * are branch points). + */ + visitThrowing(offset, length, true); + } + } + + /** {@inheritDoc} */ + @Override + public void visitBranch(int opcode, int offset, int length, + int target) { + switch (opcode) { + case ByteOps.GOTO: { + visitCommon(offset, length, false); + targetLists[offset] = IntList.makeImmutable(target); + break; + } + case ByteOps.JSR: { + /* + * Each jsr is quarantined into a separate block (containing + * only the jsr instruction) but is otherwise treated + * as a conditional branch. (That is to say, both its + * target and next instruction begin new blocks.) + */ + addWorkIfNecessary(offset, true); + // Fall through to next case... + } + default: { + int next = offset + length; + visitCommon(offset, length, true); + addWorkIfNecessary(next, true); + targetLists[offset] = IntList.makeImmutable(next, target); + break; + } + } + + addWorkIfNecessary(target, true); + } + + /** {@inheritDoc} */ + @Override + public void visitSwitch(int opcode, int offset, int length, + SwitchList cases, int padding) { + visitCommon(offset, length, false); + addWorkIfNecessary(cases.getDefaultTarget(), true); + + int sz = cases.size(); + for (int i = 0; i < sz; i++) { + addWorkIfNecessary(cases.getTarget(i), true); + } + + targetLists[offset] = cases.getTargets(); + } + + /** {@inheritDoc} */ + @Override + public void visitNewarray(int offset, int length, CstType type, + ArrayList intVals) { + visitCommon(offset, length, true); + visitThrowing(offset, length, true); + } + + /** + * Extracts the list of basic blocks from the bit sets. + * + * @return {@code non-null;} the list of basic blocks + */ + private ByteBlockList getBlockList() { + BytecodeArray bytes = method.getCode(); + ByteBlock[] bbs = new ByteBlock[bytes.size()]; + int count = 0; + + for (int at = 0, next; /*at*/; at = next) { + next = Bits.findFirst(blockSet, at + 1); + if (next < 0) { + break; + } + + if (Bits.get(liveSet, at)) { + /* + * Search backward for the branch or throwing + * instruction at the end of this block, if any. If + * there isn't any, then "next" is the sole target. + */ + IntList targets = null; + int targetsAt = -1; + ByteCatchList blockCatches; + + for (int i = next - 1; i >= at; i--) { + targets = targetLists[i]; + if (targets != null) { + targetsAt = i; + break; + } + } + + if (targets == null) { + targets = IntList.makeImmutable(next); + blockCatches = ByteCatchList.EMPTY; + } else { + blockCatches = catchLists[targetsAt]; + if (blockCatches == null) { + blockCatches = ByteCatchList.EMPTY; + } + } + + bbs[count] = + new ByteBlock(at, at, next, targets, blockCatches); + count++; + } + } + + ByteBlockList result = new ByteBlockList(count); + for (int i = 0; i < count; i++) { + result.set(i, bbs[i]); + } + + return result; + } + + /** + * Does basic block identification. + */ + private void doit() { + BytecodeArray bytes = method.getCode(); + ByteCatchList catches = method.getCatches(); + int catchSz = catches.size(); + + /* + * Start by setting offset 0 as the start of a block and in need + * of work... + */ + Bits.set(workSet, 0); + Bits.set(blockSet, 0); + + /* + * And then process the work set, add new work based on + * exception ranges that are active, and iterate until there's + * nothing left to work on. + */ + while (!Bits.isEmpty(workSet)) { + try { + bytes.processWorkSet(workSet, this); + } catch (IllegalArgumentException ex) { + // Translate the exception. + throw new SimException("flow of control falls off " + + "end of method", + ex); + } + + for (int i = 0; i < catchSz; i++) { + ByteCatchList.Item item = catches.get(i); + int start = item.getStartPc(); + int end = item.getEndPc(); + if (Bits.anyInRange(liveSet, start, end)) { + Bits.set(blockSet, start); + Bits.set(blockSet, end); + addWorkIfNecessary(item.getHandlerPc(), true); + } + } + } + } + + /** + * Sets a bit in the work set, but only if the instruction in question + * isn't yet known to be possibly-live. + * + * @param offset offset to the instruction in question + * @param blockStart {@code true} iff this instruction starts a + * basic block + */ + private void addWorkIfNecessary(int offset, boolean blockStart) { + if (!Bits.get(liveSet, offset)) { + Bits.set(workSet, offset); + } + + if (blockStart) { + Bits.set(blockSet, offset); + } + } + + /** + * Helper method used by all the visitor methods. + * + * @param offset offset to the instruction + * @param length length of the instruction, in bytes + * @param nextIsLive {@code true} iff the instruction after + * the indicated one is possibly-live (because this one isn't an + * unconditional branch, a return, or a switch) + */ + private void visitCommon(int offset, int length, boolean nextIsLive) { + Bits.set(liveSet, offset); + + if (nextIsLive) { + /* + * If the next instruction is flowed to by this one, just + * add it to the work set, and then a subsequent visit*() + * will deal with it as appropriate. + */ + addWorkIfNecessary(offset + length, false); + } else { + /* + * If the next instruction isn't flowed to by this one, + * then mark it as a start of a block but *don't* add it + * to the work set, so that in the final phase we can know + * dead code blocks as those marked as blocks but not also marked + * live. + */ + Bits.set(blockSet, offset + length); + } + } + + /** + * Helper method used by all the visitor methods that deal with + * opcodes that possibly throw. This method should be called after calling + * {@link #visitCommon}. + * + * @param offset offset to the instruction + * @param length length of the instruction, in bytes + * @param nextIsLive {@code true} iff the instruction after + * the indicated one is possibly-live (because this one isn't an + * unconditional throw) + */ + private void visitThrowing(int offset, int length, boolean nextIsLive) { + int next = offset + length; + + if (nextIsLive) { + addWorkIfNecessary(next, true); + } + + ByteCatchList catches = method.getCatches().listFor(offset); + catchLists[offset] = catches; + targetLists[offset] = catches.toTargetList(nextIsLive ? next : -1); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPreviousOffset(int offset) { + previousOffset = offset; + } + + /** + * {@inheritDoc} + */ + @Override + public int getPreviousOffset() { + return previousOffset; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/BootstrapMethodArgumentsList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/BootstrapMethodArgumentsList.java new file mode 100644 index 00000000..59a01c39 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/BootstrapMethodArgumentsList.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 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. + */ +package external.com.android.dx.cf.code; + +import external.com.android.dx.rop.cst.Constant; +import external.com.android.dx.rop.cst.CstDouble; +import external.com.android.dx.rop.cst.CstFloat; +import external.com.android.dx.rop.cst.CstInteger; +import external.com.android.dx.rop.cst.CstLong; +import external.com.android.dx.rop.cst.CstMethodHandle; +import external.com.android.dx.rop.cst.CstProtoRef; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.util.FixedSizeList; + +/** + * List of bootstrap method arguments, which are part of the contents of + * {@code BootstrapMethods} attributes. + */ +public class BootstrapMethodArgumentsList extends FixedSizeList { + /** + * Constructs an instance. + * + * @param count the number of elements to be in the list + */ + public BootstrapMethodArgumentsList(int count) { + super(count); + } + + /** + * Gets the bootstrap argument from the indicated position. + * + * @param n position of argument to get + * @return {@code Constant} instance + */ + public Constant get(int n) { + return (Constant) get0(n); + } + + /** + * Sets the bootstrap argument at the indicated position. + * + * @param n position of argument to set + * @param cst {@code Constant} instance + */ + public void set(int n, Constant cst) { + // The set of permitted types is defined by the JVMS 8, section 4.7.23. + if (cst instanceof CstString || + cst instanceof CstType || + cst instanceof CstInteger || + cst instanceof CstLong || + cst instanceof CstFloat || + cst instanceof CstDouble || + cst instanceof CstMethodHandle || + cst instanceof CstProtoRef) { + set0(n, cst); + } else { + Class klass = cst.getClass(); + throw new IllegalArgumentException("bad type for bootstrap argument: " + klass); + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/BootstrapMethodsList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/BootstrapMethodsList.java new file mode 100644 index 00000000..514f44c0 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/BootstrapMethodsList.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2017 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. + */ +package external.com.android.dx.cf.code; + +import external.com.android.dx.rop.cst.CstMethodHandle; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.util.FixedSizeList; + +/** + * List of bootstrap method entries, which are the contents of + * {@code BootstrapMethods} attributes. + */ +public class BootstrapMethodsList extends FixedSizeList { + /** {@code non-null;} zero-size instance */ + public static final BootstrapMethodsList EMPTY = new BootstrapMethodsList(0); + + /** + * Constructs an instance. + * + * @param count the number of elements to be in the list + */ + public BootstrapMethodsList(int count) { + super(count); + } + + /** + * Gets the indicated item. + * + * @param n {@code >= 0;} which item + * @return {@code null-ok;} the indicated item + */ + public Item get(int n) { + return (Item) get0(n); + } + + /** + * Sets the item at the given index. + * + * @param n {@code >= 0, < size();} which element + * @param item {@code non-null;} the item + */ + public void set(int n, Item item) { + if (item == null) { + throw new NullPointerException("item == null"); + } + + set0(n, item); + } + + /** + * Sets the item at the given index. + * + * @param n {@code >= 0, < size();} which element + * @param declaringClass {@code non-null;} the class declaring bootstrap method. + * @param bootstrapMethodHandle {@code non-null;} the bootstrap method handle + * @param arguments {@code non-null;} the arguments of the bootstrap method + */ + public void set(int n, CstType declaringClass, CstMethodHandle bootstrapMethodHandle, + BootstrapMethodArgumentsList arguments) { + set(n, new Item(declaringClass, bootstrapMethodHandle, arguments)); + } + + /** + * Returns an instance which is the concatenation of the two given + * instances. + * + * @param list1 {@code non-null;} first instance + * @param list2 {@code non-null;} second instance + * @return {@code non-null;} combined instance + */ + public static BootstrapMethodsList concat(BootstrapMethodsList list1, + BootstrapMethodsList list2) { + if (list1 == EMPTY) { + return list2; + } else if (list2 == EMPTY) { + return list1; + } + + int sz1 = list1.size(); + int sz2 = list2.size(); + BootstrapMethodsList result = new BootstrapMethodsList(sz1 + sz2); + + for (int i = 0; i < sz1; i++) { + result.set(i, list1.get(i)); + } + + for (int i = 0; i < sz2; i++) { + result.set(sz1 + i, list2.get(i)); + } + + return result; + } + + public static class Item { + private final BootstrapMethodArgumentsList bootstrapMethodArgumentsList; + private final CstMethodHandle bootstrapMethodHandle; + private final CstType declaringClass; + + public Item(CstType declaringClass, CstMethodHandle bootstrapMethodHandle, + BootstrapMethodArgumentsList bootstrapMethodArguments) { + if (declaringClass == null) { + throw new NullPointerException("declaringClass == null"); + } + if (bootstrapMethodHandle == null) { + throw new NullPointerException("bootstrapMethodHandle == null"); + } + if (bootstrapMethodArguments == null) { + throw new NullPointerException("bootstrapMethodArguments == null"); + } + this.bootstrapMethodHandle = bootstrapMethodHandle; + this.bootstrapMethodArgumentsList = bootstrapMethodArguments; + this.declaringClass = declaringClass; + } + + public CstMethodHandle getBootstrapMethodHandle() { + return bootstrapMethodHandle; + } + + public BootstrapMethodArgumentsList getBootstrapMethodArguments() { + return bootstrapMethodArgumentsList; + } + + public CstType getDeclaringClass() { + return declaringClass; + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteBlock.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteBlock.java new file mode 100644 index 00000000..ef00aaa7 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteBlock.java @@ -0,0 +1,146 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.util.Hex; +import external.com.android.dx.util.IntList; +import external.com.android.dx.util.LabeledItem; + +/** + * Representation of a basic block in a bytecode array. + */ +public final class ByteBlock implements LabeledItem { + /** {@code >= 0;} label for this block */ + private final int label; + + /** {@code >= 0;} bytecode offset (inclusive) of the start of the block */ + private final int start; + + /** {@code > start;} bytecode offset (exclusive) of the end of the block */ + private final int end; + + /** {@code non-null;} list of successors that this block may branch to */ + private final IntList successors; + + /** {@code non-null;} list of exceptions caught and their handler targets */ + private final ByteCatchList catches; + + /** + * Constructs an instance. + * + * @param label {@code >= 0;} target label for this block + * @param start {@code >= 0;} bytecode offset (inclusive) of the start + * of the block + * @param end {@code > start;} bytecode offset (exclusive) of the end + * of the block + * @param successors {@code non-null;} list of successors that this block may + * branch to + * @param catches {@code non-null;} list of exceptions caught and their + * handler targets + */ + public ByteBlock(int label, int start, int end, IntList successors, + ByteCatchList catches) { + if (label < 0) { + throw new IllegalArgumentException("label < 0"); + } + + if (start < 0) { + throw new IllegalArgumentException("start < 0"); + } + + if (end <= start) { + throw new IllegalArgumentException("end <= start"); + } + + if (successors == null) { + throw new NullPointerException("targets == null"); + } + + int sz = successors.size(); + for (int i = 0; i < sz; i++) { + if (successors.get(i) < 0) { + throw new IllegalArgumentException("successors[" + i + + "] == " + + successors.get(i)); + } + } + + if (catches == null) { + throw new NullPointerException("catches == null"); + } + + this.label = label; + this.start = start; + this.end = end; + this.successors = successors; + this.catches = catches; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return '{' + Hex.u2(label) + ": " + Hex.u2(start) + ".." + + Hex.u2(end) + '}'; + } + + /** + * Gets the label of this block. + * + * @return {@code >= 0;} the label + */ + @Override + public int getLabel() { + return label; + } + + /** + * Gets the bytecode offset (inclusive) of the start of this block. + * + * @return {@code >= 0;} the start offset + */ + public int getStart() { + return start; + } + + /** + * Gets the bytecode offset (exclusive) of the end of this block. + * + * @return {@code > getStart();} the end offset + */ + public int getEnd() { + return end; + } + + /** + * Gets the list of successors that this block may branch to + * non-exceptionally. + * + * @return {@code non-null;} the successor list + */ + public IntList getSuccessors() { + return successors; + } + + /** + * Gets the list of exceptions caught and their handler targets. + * + * @return {@code non-null;} the catch list + */ + public ByteCatchList getCatches() { + return catches; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteBlockList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteBlockList.java new file mode 100644 index 00000000..472a5a6c --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteBlockList.java @@ -0,0 +1,74 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.util.Hex; +import external.com.android.dx.util.LabeledList; + +/** + * List of {@link ByteBlock} instances. + */ +public final class ByteBlockList extends LabeledList { + + /** + * Constructs an instance. + * + * @param size {@code >= 0;} the number of elements to be in the list + */ + public ByteBlockList(int size) { + super(size); + } + + /** + * Gets the indicated element. It is an error to call this with the + * index for an element which was never set; if you do that, this + * will throw {@code NullPointerException}. + * + * @param n {@code >= 0, < size();} which element + * @return {@code non-null;} the indicated element + */ + public ByteBlock get(int n) { + return (ByteBlock) get0(n); + } + + /** + * Gets the block with the given label. + * + * @param label the label to look for + * @return {@code non-null;} the block with the given label + */ + public ByteBlock labelToBlock(int label) { + int idx = indexOfLabel(label); + + if (idx < 0) { + throw new IllegalArgumentException("no such label: " + + Hex.u2(label)); + } + + return get(idx); + } + + /** + * Sets the element at the given index. + * + * @param n {@code >= 0, < size();} which element + * @param bb {@code null-ok;} the value to store + */ + public void set(int n, ByteBlock bb) { + super.set(n, bb); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteCatchList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteCatchList.java new file mode 100644 index 00000000..b2078468 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteCatchList.java @@ -0,0 +1,317 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.StdTypeList; +import external.com.android.dx.rop.type.TypeList; +import external.com.android.dx.util.FixedSizeList; +import external.com.android.dx.util.IntList; + +/** + * List of catch entries, that is, the elements of an "exception table," + * which is part of a standard {@code Code} attribute. + */ +public final class ByteCatchList extends FixedSizeList { + /** {@code non-null;} convenient zero-entry instance */ + public static final ByteCatchList EMPTY = new ByteCatchList(0); + + /** + * Constructs an instance. + * + * @param count the number of elements to be in the table + */ + public ByteCatchList(int count) { + super(count); + } + + /** + * Gets the total length of this structure in bytes, when included in + * a {@code Code} attribute. The returned value includes the + * two bytes for {@code exception_table_length}. + * + * @return {@code >= 2;} the total length, in bytes + */ + public int byteLength() { + return 2 + size() * 8; + } + + /** + * Gets the indicated item. + * + * @param n {@code >= 0;} which item + * @return {@code null-ok;} the indicated item + */ + public Item get(int n) { + return (Item) get0(n); + } + + /** + * Sets the item at the given index. + * + * @param n {@code >= 0, < size();} which entry to set + * @param item {@code non-null;} the item + */ + public void set(int n, Item item) { + if (item == null) { + throw new NullPointerException("item == null"); + } + + set0(n, item); + } + + /** + * Sets the item at the given index. + * + * @param n {@code >= 0, < size();} which entry to set + * @param startPc {@code >= 0;} the start pc (inclusive) of the handler's range + * @param endPc {@code >= startPc;} the end pc (exclusive) of the + * handler's range + * @param handlerPc {@code >= 0;} the pc of the exception handler + * @param exceptionClass {@code null-ok;} the exception class or + * {@code null} to catch all exceptions with this handler + */ + public void set(int n, int startPc, int endPc, int handlerPc, + CstType exceptionClass) { + set0(n, new Item(startPc, endPc, handlerPc, exceptionClass)); + } + + /** + * Gets the list of items active at the given address. The result is + * automatically made immutable. + * + * @param pc which address + * @return {@code non-null;} list of exception handlers active at + * {@code pc} + */ + public ByteCatchList listFor(int pc) { + int sz = size(); + Item[] resultArr = new Item[sz]; + int resultSz = 0; + + for (int i = 0; i < sz; i++) { + Item one = get(i); + if (one.covers(pc) && typeNotFound(one, resultArr, resultSz)) { + resultArr[resultSz] = one; + resultSz++; + } + } + + if (resultSz == 0) { + return EMPTY; + } + + ByteCatchList result = new ByteCatchList(resultSz); + for (int i = 0; i < resultSz; i++) { + result.set(i, resultArr[i]); + } + + result.setImmutable(); + return result; + } + + /** + * Helper method for {@link #listFor}, which tells whether a match + * is not found for the exception type of the given item in + * the given array. A match is considered to be either an exact type + * match or the class {@code Object} which represents a catch-all. + * + * @param item {@code non-null;} item with the exception type to look for + * @param arr {@code non-null;} array to search in + * @param count {@code non-null;} maximum number of elements in the array to check + * @return {@code true} iff the exception type is not found + */ + private static boolean typeNotFound(Item item, Item[] arr, int count) { + CstType type = item.getExceptionClass(); + + for (int i = 0; i < count; i++) { + CstType one = arr[i].getExceptionClass(); + if ((one == type) || (one == CstType.OBJECT)) { + return false; + } + } + + return true; + } + + /** + * Returns a target list corresponding to this instance. The result + * is a list of all the exception handler addresses, with the given + * {@code noException} address appended if appropriate. The + * result is automatically made immutable. + * + * @param noException {@code >= -1;} the no-exception address to append, or + * {@code -1} not to append anything + * @return {@code non-null;} list of exception targets, with + * {@code noException} appended if necessary + */ + public IntList toTargetList(int noException) { + if (noException < -1) { + throw new IllegalArgumentException("noException < -1"); + } + + boolean hasDefault = (noException >= 0); + int sz = size(); + + if (sz == 0) { + if (hasDefault) { + /* + * The list is empty, but there is a no-exception + * address; so, the result is just that address. + */ + return IntList.makeImmutable(noException); + } + /* + * The list is empty and there isn't even a no-exception + * address. + */ + return IntList.EMPTY; + } + + IntList result = new IntList(sz + (hasDefault ? 1 : 0)); + + for (int i = 0; i < sz; i++) { + result.add(get(i).getHandlerPc()); + } + + if (hasDefault) { + result.add(noException); + } + + result.setImmutable(); + return result; + } + + /** + * Returns a rop-style catches list equivalent to this one. + * + * @return {@code non-null;} the converted instance + */ + public TypeList toRopCatchList() { + int sz = size(); + if (sz == 0) { + return StdTypeList.EMPTY; + } + + StdTypeList result = new StdTypeList(sz); + + for (int i = 0; i < sz; i++) { + result.set(i, get(i).getExceptionClass().getClassType()); + } + + result.setImmutable(); + return result; + } + + /** + * Item in an exception handler list. + */ + public static class Item { + /** {@code >= 0;} the start pc (inclusive) of the handler's range */ + private final int startPc; + + /** {@code >= startPc;} the end pc (exclusive) of the handler's range */ + private final int endPc; + + /** {@code >= 0;} the pc of the exception handler */ + private final int handlerPc; + + /** {@code null-ok;} the exception class or {@code null} to catch all + * exceptions with this handler */ + private final CstType exceptionClass; + + /** + * Constructs an instance. + * + * @param startPc {@code >= 0;} the start pc (inclusive) of the + * handler's range + * @param endPc {@code >= startPc;} the end pc (exclusive) of the + * handler's range + * @param handlerPc {@code >= 0;} the pc of the exception handler + * @param exceptionClass {@code null-ok;} the exception class or + * {@code null} to catch all exceptions with this handler + */ + public Item(int startPc, int endPc, int handlerPc, + CstType exceptionClass) { + if (startPc < 0) { + throw new IllegalArgumentException("startPc < 0"); + } + + if (endPc < startPc) { + throw new IllegalArgumentException("endPc < startPc"); + } + + if (handlerPc < 0) { + throw new IllegalArgumentException("handlerPc < 0"); + } + + this.startPc = startPc; + this.endPc = endPc; + this.handlerPc = handlerPc; + this.exceptionClass = exceptionClass; + } + + /** + * Gets the start pc (inclusive) of the handler's range. + * + * @return {@code >= 0;} the start pc (inclusive) of the handler's range. + */ + public int getStartPc() { + return startPc; + } + + /** + * Gets the end pc (exclusive) of the handler's range. + * + * @return {@code >= startPc;} the end pc (exclusive) of the + * handler's range. + */ + public int getEndPc() { + return endPc; + } + + /** + * Gets the pc of the exception handler. + * + * @return {@code >= 0;} the pc of the exception handler + */ + public int getHandlerPc() { + return handlerPc; + } + + /** + * Gets the class of exception handled. + * + * @return {@code non-null;} the exception class; {@link CstType#OBJECT} + * if this entry handles all possible exceptions + */ + public CstType getExceptionClass() { + return (exceptionClass != null) ? + exceptionClass : CstType.OBJECT; + } + + /** + * Returns whether the given address is in the range of this item. + * + * @param pc the address + * @return {@code true} iff this item covers {@code pc} + */ + public boolean covers(int pc) { + return (pc >= startPc) && (pc < endPc); + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteOps.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteOps.java new file mode 100644 index 00000000..70d9a37d --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ByteOps.java @@ -0,0 +1,650 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.util.Hex; + +/** + * Constants and utility methods for dealing with bytecode arrays at an + * opcode level. + */ +public class ByteOps { + // one constant per opcode + public static final int NOP = 0x00; + public static final int ACONST_NULL = 0x01; + public static final int ICONST_M1 = 0x02; + public static final int ICONST_0 = 0x03; + public static final int ICONST_1 = 0x04; + public static final int ICONST_2 = 0x05; + public static final int ICONST_3 = 0x06; + public static final int ICONST_4 = 0x07; + public static final int ICONST_5 = 0x08; + public static final int LCONST_0 = 0x09; + public static final int LCONST_1 = 0x0a; + public static final int FCONST_0 = 0x0b; + public static final int FCONST_1 = 0x0c; + public static final int FCONST_2 = 0x0d; + public static final int DCONST_0 = 0x0e; + public static final int DCONST_1 = 0x0f; + public static final int BIPUSH = 0x10; + public static final int SIPUSH = 0x11; + public static final int LDC = 0x12; + public static final int LDC_W = 0x13; + public static final int LDC2_W = 0x14; + public static final int ILOAD = 0x15; + public static final int LLOAD = 0x16; + public static final int FLOAD = 0x17; + public static final int DLOAD = 0x18; + public static final int ALOAD = 0x19; + public static final int ILOAD_0 = 0x1a; + public static final int ILOAD_1 = 0x1b; + public static final int ILOAD_2 = 0x1c; + public static final int ILOAD_3 = 0x1d; + public static final int LLOAD_0 = 0x1e; + public static final int LLOAD_1 = 0x1f; + public static final int LLOAD_2 = 0x20; + public static final int LLOAD_3 = 0x21; + public static final int FLOAD_0 = 0x22; + public static final int FLOAD_1 = 0x23; + public static final int FLOAD_2 = 0x24; + public static final int FLOAD_3 = 0x25; + public static final int DLOAD_0 = 0x26; + public static final int DLOAD_1 = 0x27; + public static final int DLOAD_2 = 0x28; + public static final int DLOAD_3 = 0x29; + public static final int ALOAD_0 = 0x2a; + public static final int ALOAD_1 = 0x2b; + public static final int ALOAD_2 = 0x2c; + public static final int ALOAD_3 = 0x2d; + public static final int IALOAD = 0x2e; + public static final int LALOAD = 0x2f; + public static final int FALOAD = 0x30; + public static final int DALOAD = 0x31; + public static final int AALOAD = 0x32; + public static final int BALOAD = 0x33; + public static final int CALOAD = 0x34; + public static final int SALOAD = 0x35; + public static final int ISTORE = 0x36; + public static final int LSTORE = 0x37; + public static final int FSTORE = 0x38; + public static final int DSTORE = 0x39; + public static final int ASTORE = 0x3a; + public static final int ISTORE_0 = 0x3b; + public static final int ISTORE_1 = 0x3c; + public static final int ISTORE_2 = 0x3d; + public static final int ISTORE_3 = 0x3e; + public static final int LSTORE_0 = 0x3f; + public static final int LSTORE_1 = 0x40; + public static final int LSTORE_2 = 0x41; + public static final int LSTORE_3 = 0x42; + public static final int FSTORE_0 = 0x43; + public static final int FSTORE_1 = 0x44; + public static final int FSTORE_2 = 0x45; + public static final int FSTORE_3 = 0x46; + public static final int DSTORE_0 = 0x47; + public static final int DSTORE_1 = 0x48; + public static final int DSTORE_2 = 0x49; + public static final int DSTORE_3 = 0x4a; + public static final int ASTORE_0 = 0x4b; + public static final int ASTORE_1 = 0x4c; + public static final int ASTORE_2 = 0x4d; + public static final int ASTORE_3 = 0x4e; + public static final int IASTORE = 0x4f; + public static final int LASTORE = 0x50; + public static final int FASTORE = 0x51; + public static final int DASTORE = 0x52; + public static final int AASTORE = 0x53; + public static final int BASTORE = 0x54; + public static final int CASTORE = 0x55; + public static final int SASTORE = 0x56; + public static final int POP = 0x57; + public static final int POP2 = 0x58; + public static final int DUP = 0x59; + public static final int DUP_X1 = 0x5a; + public static final int DUP_X2 = 0x5b; + public static final int DUP2 = 0x5c; + public static final int DUP2_X1 = 0x5d; + public static final int DUP2_X2 = 0x5e; + public static final int SWAP = 0x5f; + public static final int IADD = 0x60; + public static final int LADD = 0x61; + public static final int FADD = 0x62; + public static final int DADD = 0x63; + public static final int ISUB = 0x64; + public static final int LSUB = 0x65; + public static final int FSUB = 0x66; + public static final int DSUB = 0x67; + public static final int IMUL = 0x68; + public static final int LMUL = 0x69; + public static final int FMUL = 0x6a; + public static final int DMUL = 0x6b; + public static final int IDIV = 0x6c; + public static final int LDIV = 0x6d; + public static final int FDIV = 0x6e; + public static final int DDIV = 0x6f; + public static final int IREM = 0x70; + public static final int LREM = 0x71; + public static final int FREM = 0x72; + public static final int DREM = 0x73; + public static final int INEG = 0x74; + public static final int LNEG = 0x75; + public static final int FNEG = 0x76; + public static final int DNEG = 0x77; + public static final int ISHL = 0x78; + public static final int LSHL = 0x79; + public static final int ISHR = 0x7a; + public static final int LSHR = 0x7b; + public static final int IUSHR = 0x7c; + public static final int LUSHR = 0x7d; + public static final int IAND = 0x7e; + public static final int LAND = 0x7f; + public static final int IOR = 0x80; + public static final int LOR = 0x81; + public static final int IXOR = 0x82; + public static final int LXOR = 0x83; + public static final int IINC = 0x84; + public static final int I2L = 0x85; + public static final int I2F = 0x86; + public static final int I2D = 0x87; + public static final int L2I = 0x88; + public static final int L2F = 0x89; + public static final int L2D = 0x8a; + public static final int F2I = 0x8b; + public static final int F2L = 0x8c; + public static final int F2D = 0x8d; + public static final int D2I = 0x8e; + public static final int D2L = 0x8f; + public static final int D2F = 0x90; + public static final int I2B = 0x91; + public static final int I2C = 0x92; + public static final int I2S = 0x93; + public static final int LCMP = 0x94; + public static final int FCMPL = 0x95; + public static final int FCMPG = 0x96; + public static final int DCMPL = 0x97; + public static final int DCMPG = 0x98; + public static final int IFEQ = 0x99; + public static final int IFNE = 0x9a; + public static final int IFLT = 0x9b; + public static final int IFGE = 0x9c; + public static final int IFGT = 0x9d; + public static final int IFLE = 0x9e; + public static final int IF_ICMPEQ = 0x9f; + public static final int IF_ICMPNE = 0xa0; + public static final int IF_ICMPLT = 0xa1; + public static final int IF_ICMPGE = 0xa2; + public static final int IF_ICMPGT = 0xa3; + public static final int IF_ICMPLE = 0xa4; + public static final int IF_ACMPEQ = 0xa5; + public static final int IF_ACMPNE = 0xa6; + public static final int GOTO = 0xa7; + public static final int JSR = 0xa8; + public static final int RET = 0xa9; + public static final int TABLESWITCH = 0xaa; + public static final int LOOKUPSWITCH = 0xab; + public static final int IRETURN = 0xac; + public static final int LRETURN = 0xad; + public static final int FRETURN = 0xae; + public static final int DRETURN = 0xaf; + public static final int ARETURN = 0xb0; + public static final int RETURN = 0xb1; + public static final int GETSTATIC = 0xb2; + public static final int PUTSTATIC = 0xb3; + public static final int GETFIELD = 0xb4; + public static final int PUTFIELD = 0xb5; + public static final int INVOKEVIRTUAL = 0xb6; + public static final int INVOKESPECIAL = 0xb7; + public static final int INVOKESTATIC = 0xb8; + public static final int INVOKEINTERFACE = 0xb9; + public static final int INVOKEDYNAMIC = 0xba; + public static final int NEW = 0xbb; + public static final int NEWARRAY = 0xbc; + public static final int ANEWARRAY = 0xbd; + public static final int ARRAYLENGTH = 0xbe; + public static final int ATHROW = 0xbf; + public static final int CHECKCAST = 0xc0; + public static final int INSTANCEOF = 0xc1; + public static final int MONITORENTER = 0xc2; + public static final int MONITOREXIT = 0xc3; + public static final int WIDE = 0xc4; + public static final int MULTIANEWARRAY = 0xc5; + public static final int IFNULL = 0xc6; + public static final int IFNONNULL = 0xc7; + public static final int GOTO_W = 0xc8; + public static final int JSR_W = 0xc9; + + // a constant for each valid argument to "newarray" + + public static final int NEWARRAY_BOOLEAN = 4; + public static final int NEWARRAY_CHAR = 5; + public static final int NEWARRAY_FLOAT = 6; + public static final int NEWARRAY_DOUBLE = 7; + public static final int NEWARRAY_BYTE = 8; + public static final int NEWARRAY_SHORT = 9; + public static final int NEWARRAY_INT = 10; + public static final int NEWARRAY_LONG = 11; + + // a constant for each possible instruction format + + /** invalid */ + public static final int FMT_INVALID = 0; + + /** "-": {@code op} */ + public static final int FMT_NO_ARGS = 1; + + /** "0": {@code op}; implies {@code max_locals >= 1} */ + public static final int FMT_NO_ARGS_LOCALS_1 = 2; + + /** "1": {@code op}; implies {@code max_locals >= 2} */ + public static final int FMT_NO_ARGS_LOCALS_2 = 3; + + /** "2": {@code op}; implies {@code max_locals >= 3} */ + public static final int FMT_NO_ARGS_LOCALS_3 = 4; + + /** "3": {@code op}; implies {@code max_locals >= 4} */ + public static final int FMT_NO_ARGS_LOCALS_4 = 5; + + /** "4": {@code op}; implies {@code max_locals >= 5} */ + public static final int FMT_NO_ARGS_LOCALS_5 = 6; + + /** "b": {@code op target target} */ + public static final int FMT_BRANCH = 7; + + /** "c": {@code op target target target target} */ + public static final int FMT_WIDE_BRANCH = 8; + + /** "p": {@code op #cpi #cpi}; constant restricted as specified */ + public static final int FMT_CPI = 9; + + /** + * "l": {@code op local}; category-1 local; implies + * {@code max_locals} is at least two more than the given + * local number + */ + public static final int FMT_LOCAL_1 = 10; + + /** + * "m": {@code op local}; category-2 local; implies + * {@code max_locals} is at least two more than the given + * local number + */ + public static final int FMT_LOCAL_2 = 11; + + /** + * "y": {@code op #byte} ({@code bipush} and + * {@code newarray}) + */ + public static final int FMT_LITERAL_BYTE = 12; + + /** "I": {@code invokeinterface cpi cpi count 0} */ + public static final int FMT_INVOKEINTERFACE = 13; + + /** "L": {@code ldc #cpi}; constant restricted as specified */ + public static final int FMT_LDC = 14; + + /** "S": {@code sipush #byte #byte} */ + public static final int FMT_SIPUSH = 15; + + /** "T": {@code tableswitch ...} */ + public static final int FMT_TABLESWITCH = 16; + + /** "U": {@code lookupswitch ...} */ + public static final int FMT_LOOKUPSWITCH = 17; + + /** "M": {@code multianewarray cpi cpi dims} */ + public static final int FMT_MULTIANEWARRAY = 18; + + /** "W": {@code wide ...} */ + public static final int FMT_WIDE = 19; + + /** mask for the bits representing the opcode format */ + public static final int FMT_MASK = 0x1f; + + /** "I": flag bit for valid cp type for {@code Integer} */ + public static final int CPOK_Integer = 0x20; + + /** "F": flag bit for valid cp type for {@code Float} */ + public static final int CPOK_Float = 0x40; + + /** "J": flag bit for valid cp type for {@code Long} */ + public static final int CPOK_Long = 0x80; + + /** "D": flag bit for valid cp type for {@code Double} */ + public static final int CPOK_Double = 0x100; + + /** "c": flag bit for valid cp type for {@code Class} */ + public static final int CPOK_Class = 0x200; + + /** "s": flag bit for valid cp type for {@code String} */ + public static final int CPOK_String = 0x400; + + /** "f": flag bit for valid cp type for {@code Fieldref} */ + public static final int CPOK_Fieldref = 0x800; + + /** "m": flag bit for valid cp type for {@code Methodref} */ + public static final int CPOK_Methodref = 0x1000; + + /** "i": flag bit for valid cp type for {@code InterfaceMethodref} */ + public static final int CPOK_InterfaceMethodref = 0x2000; + + /** + * {@code non-null;} map from opcodes to format or'ed with allowed constant + * pool types + */ + private static final int[] OPCODE_INFO = new int[256]; + + /** {@code non-null;} map from opcodes to their names */ + private static final String[] OPCODE_NAMES = new String[256]; + + /** {@code non-null;} bigass string describing all the opcodes */ + private static final String OPCODE_DETAILS = + "00 - nop;" + + "01 - aconst_null;" + + "02 - iconst_m1;" + + "03 - iconst_0;" + + "04 - iconst_1;" + + "05 - iconst_2;" + + "06 - iconst_3;" + + "07 - iconst_4;" + + "08 - iconst_5;" + + "09 - lconst_0;" + + "0a - lconst_1;" + + "0b - fconst_0;" + + "0c - fconst_1;" + + "0d - fconst_2;" + + "0e - dconst_0;" + + "0f - dconst_1;" + + "10 y bipush;" + + "11 S sipush;" + + "12 L:IFcs ldc;" + + "13 p:IFcs ldc_w;" + + "14 p:DJ ldc2_w;" + + "15 l iload;" + + "16 m lload;" + + "17 l fload;" + + "18 m dload;" + + "19 l aload;" + + "1a 0 iload_0;" + + "1b 1 iload_1;" + + "1c 2 iload_2;" + + "1d 3 iload_3;" + + "1e 1 lload_0;" + + "1f 2 lload_1;" + + "20 3 lload_2;" + + "21 4 lload_3;" + + "22 0 fload_0;" + + "23 1 fload_1;" + + "24 2 fload_2;" + + "25 3 fload_3;" + + "26 1 dload_0;" + + "27 2 dload_1;" + + "28 3 dload_2;" + + "29 4 dload_3;" + + "2a 0 aload_0;" + + "2b 1 aload_1;" + + "2c 2 aload_2;" + + "2d 3 aload_3;" + + "2e - iaload;" + + "2f - laload;" + + "30 - faload;" + + "31 - daload;" + + "32 - aaload;" + + "33 - baload;" + + "34 - caload;" + + "35 - saload;" + + "36 - istore;" + + "37 - lstore;" + + "38 - fstore;" + + "39 - dstore;" + + "3a - astore;" + + "3b 0 istore_0;" + + "3c 1 istore_1;" + + "3d 2 istore_2;" + + "3e 3 istore_3;" + + "3f 1 lstore_0;" + + "40 2 lstore_1;" + + "41 3 lstore_2;" + + "42 4 lstore_3;" + + "43 0 fstore_0;" + + "44 1 fstore_1;" + + "45 2 fstore_2;" + + "46 3 fstore_3;" + + "47 1 dstore_0;" + + "48 2 dstore_1;" + + "49 3 dstore_2;" + + "4a 4 dstore_3;" + + "4b 0 astore_0;" + + "4c 1 astore_1;" + + "4d 2 astore_2;" + + "4e 3 astore_3;" + + "4f - iastore;" + + "50 - lastore;" + + "51 - fastore;" + + "52 - dastore;" + + "53 - aastore;" + + "54 - bastore;" + + "55 - castore;" + + "56 - sastore;" + + "57 - pop;" + + "58 - pop2;" + + "59 - dup;" + + "5a - dup_x1;" + + "5b - dup_x2;" + + "5c - dup2;" + + "5d - dup2_x1;" + + "5e - dup2_x2;" + + "5f - swap;" + + "60 - iadd;" + + "61 - ladd;" + + "62 - fadd;" + + "63 - dadd;" + + "64 - isub;" + + "65 - lsub;" + + "66 - fsub;" + + "67 - dsub;" + + "68 - imul;" + + "69 - lmul;" + + "6a - fmul;" + + "6b - dmul;" + + "6c - idiv;" + + "6d - ldiv;" + + "6e - fdiv;" + + "6f - ddiv;" + + "70 - irem;" + + "71 - lrem;" + + "72 - frem;" + + "73 - drem;" + + "74 - ineg;" + + "75 - lneg;" + + "76 - fneg;" + + "77 - dneg;" + + "78 - ishl;" + + "79 - lshl;" + + "7a - ishr;" + + "7b - lshr;" + + "7c - iushr;" + + "7d - lushr;" + + "7e - iand;" + + "7f - land;" + + "80 - ior;" + + "81 - lor;" + + "82 - ixor;" + + "83 - lxor;" + + "84 l iinc;" + + "85 - i2l;" + + "86 - i2f;" + + "87 - i2d;" + + "88 - l2i;" + + "89 - l2f;" + + "8a - l2d;" + + "8b - f2i;" + + "8c - f2l;" + + "8d - f2d;" + + "8e - d2i;" + + "8f - d2l;" + + "90 - d2f;" + + "91 - i2b;" + + "92 - i2c;" + + "93 - i2s;" + + "94 - lcmp;" + + "95 - fcmpl;" + + "96 - fcmpg;" + + "97 - dcmpl;" + + "98 - dcmpg;" + + "99 b ifeq;" + + "9a b ifne;" + + "9b b iflt;" + + "9c b ifge;" + + "9d b ifgt;" + + "9e b ifle;" + + "9f b if_icmpeq;" + + "a0 b if_icmpne;" + + "a1 b if_icmplt;" + + "a2 b if_icmpge;" + + "a3 b if_icmpgt;" + + "a4 b if_icmple;" + + "a5 b if_acmpeq;" + + "a6 b if_acmpne;" + + "a7 b goto;" + + "a8 b jsr;" + + "a9 l ret;" + + "aa T tableswitch;" + + "ab U lookupswitch;" + + "ac - ireturn;" + + "ad - lreturn;" + + "ae - freturn;" + + "af - dreturn;" + + "b0 - areturn;" + + "b1 - return;" + + "b2 p:f getstatic;" + + "b3 p:f putstatic;" + + "b4 p:f getfield;" + + "b5 p:f putfield;" + + "b6 p:m invokevirtual;" + + "b7 p:m invokespecial;" + + "b8 p:m invokestatic;" + + "b9 I:i invokeinterface;" + + "bb p:c new;" + + "bc y newarray;" + + "bd p:c anewarray;" + + "be - arraylength;" + + "bf - athrow;" + + "c0 p:c checkcast;" + + "c1 p:c instanceof;" + + "c2 - monitorenter;" + + "c3 - monitorexit;" + + "c4 W wide;" + + "c5 M:c multianewarray;" + + "c6 b ifnull;" + + "c7 b ifnonnull;" + + "c8 c goto_w;" + + "c9 c jsr_w;"; + + static { + // Set up OPCODE_INFO and OPCODE_NAMES. + String s = OPCODE_DETAILS; + int len = s.length(); + + for (int i = 0; i < len; /*i*/) { + int idx = (Character.digit(s.charAt(i), 16) << 4) | + Character.digit(s.charAt(i + 1), 16); + int info; + switch (s.charAt(i + 3)) { + case '-': info = FMT_NO_ARGS; break; + case '0': info = FMT_NO_ARGS_LOCALS_1; break; + case '1': info = FMT_NO_ARGS_LOCALS_2; break; + case '2': info = FMT_NO_ARGS_LOCALS_3; break; + case '3': info = FMT_NO_ARGS_LOCALS_4; break; + case '4': info = FMT_NO_ARGS_LOCALS_5; break; + case 'b': info = FMT_BRANCH; break; + case 'c': info = FMT_WIDE_BRANCH; break; + case 'p': info = FMT_CPI; break; + case 'l': info = FMT_LOCAL_1; break; + case 'm': info = FMT_LOCAL_2; break; + case 'y': info = FMT_LITERAL_BYTE; break; + case 'I': info = FMT_INVOKEINTERFACE; break; + case 'L': info = FMT_LDC; break; + case 'S': info = FMT_SIPUSH; break; + case 'T': info = FMT_TABLESWITCH; break; + case 'U': info = FMT_LOOKUPSWITCH; break; + case 'M': info = FMT_MULTIANEWARRAY; break; + case 'W': info = FMT_WIDE; break; + default: info = FMT_INVALID; break; + } + + i += 5; + if (s.charAt(i - 1) == ':') { + inner: + for (;;) { + switch (s.charAt(i)) { + case 'I': info |= CPOK_Integer; break; + case 'F': info |= CPOK_Float; break; + case 'J': info |= CPOK_Long; break; + case 'D': info |= CPOK_Double; break; + case 'c': info |= CPOK_Class; break; + case 's': info |= CPOK_String; break; + case 'f': info |= CPOK_Fieldref; break; + case 'm': info |= CPOK_Methodref; break; + case 'i': info |= CPOK_InterfaceMethodref; break; + default: break inner; + } + i++; + } + i++; + } + + int endAt = s.indexOf(';', i); + OPCODE_INFO[idx] = info; + OPCODE_NAMES[idx] = s.substring(i, endAt); + i = endAt + 1; + } + } + + /** + * This class is uninstantiable. + */ + private ByteOps() { + // This space intentionally left blank. + } + + /** + * Gets the name of the given opcode. + * + * @param opcode {@code >= 0, <= 255;} the opcode + * @return {@code non-null;} its name + */ + public static String opName(int opcode) { + String result = OPCODE_NAMES[opcode]; + + if (result == null) { + result = "unused_" + Hex.u1(opcode); + OPCODE_NAMES[opcode] = result; + } + + return result; + } + + /** + * Gets the format and allowed cp types of the given opcode. + * + * @param opcode {@code >= 0, <= 255;} the opcode + * @return its format and allowed cp types + */ + public static int opInfo(int opcode) { + return OPCODE_INFO[opcode]; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/BytecodeArray.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/BytecodeArray.java new file mode 100644 index 00000000..910b3b1d --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/BytecodeArray.java @@ -0,0 +1,1439 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.rop.cst.Constant; +import external.com.android.dx.rop.cst.ConstantPool; +import external.com.android.dx.rop.cst.CstDouble; +import external.com.android.dx.rop.cst.CstFloat; +import external.com.android.dx.rop.cst.CstInteger; +import external.com.android.dx.rop.cst.CstInvokeDynamic; +import external.com.android.dx.rop.cst.CstKnownNull; +import external.com.android.dx.rop.cst.CstLiteralBits; +import external.com.android.dx.rop.cst.CstLong; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.util.Bits; +import external.com.android.dx.util.ByteArray; +import external.com.android.dx.util.Hex; +import java.util.ArrayList; + +/** + * Bytecode array, which is part of a standard {@code Code} attribute. + */ +public final class BytecodeArray { + /** convenient no-op implementation of {@link Visitor} */ + public static final Visitor EMPTY_VISITOR = new BaseVisitor(); + + /** {@code non-null;} underlying bytes */ + private final ByteArray bytes; + + /** + * {@code non-null;} constant pool to use when resolving constant + * pool indices + */ + private final ConstantPool pool; + + /** + * Constructs an instance. + * + * @param bytes {@code non-null;} underlying bytes + * @param pool {@code non-null;} constant pool to use when + * resolving constant pool indices + */ + public BytecodeArray(ByteArray bytes, ConstantPool pool) { + if (bytes == null) { + throw new NullPointerException("bytes == null"); + } + + if (pool == null) { + throw new NullPointerException("pool == null"); + } + + this.bytes = bytes; + this.pool = pool; + } + + /** + * Gets the underlying byte array. + * + * @return {@code non-null;} the byte array + */ + public ByteArray getBytes() { + return bytes; + } + + /** + * Gets the size of the bytecode array, per se. + * + * @return {@code >= 0;} the length of the bytecode array + */ + public int size() { + return bytes.size(); + } + + /** + * Gets the total length of this structure in bytes, when included in + * a {@code Code} attribute. The returned value includes the + * array size plus four bytes for {@code code_length}. + * + * @return {@code >= 4;} the total length, in bytes + */ + public int byteLength() { + return 4 + bytes.size(); + } + + /** + * Parses each instruction in the array, in order. + * + * @param visitor {@code null-ok;} visitor to call back to for + * each instruction + */ + public void forEach(Visitor visitor) { + int sz = bytes.size(); + int at = 0; + + while (at < sz) { + /* + * Don't record the previous offset here, so that we get to see the + * raw code that initializes the array + */ + at += parseInstruction(at, visitor); + } + } + + /** + * Finds the offset to each instruction in the bytecode array. The + * result is a bit set with the offset of each opcode-per-se flipped on. + * + * @see Bits + * @return {@code non-null;} appropriately constructed bit set + */ + public int[] getInstructionOffsets() { + int sz = bytes.size(); + int[] result = Bits.makeBitSet(sz); + int at = 0; + + while (at < sz) { + Bits.set(result, at, true); + int length = parseInstruction(at, null); + at += length; + } + + return result; + } + + /** + * Processes the given "work set" by repeatedly finding the lowest bit + * in the set, clearing it, and parsing and visiting the instruction at + * the indicated offset (that is, the bit index), repeating until the + * work set is empty. It is expected that the visitor will regularly + * set new bits in the work set during the process. + * + * @param workSet {@code non-null;} the work set to process + * @param visitor {@code non-null;} visitor to call back to for + * each instruction + */ + public void processWorkSet(int[] workSet, Visitor visitor) { + if (visitor == null) { + throw new NullPointerException("visitor == null"); + } + + for (;;) { + int offset = Bits.findFirst(workSet, 0); + if (offset < 0) { + break; + } + Bits.clear(workSet, offset); + parseInstruction(offset, visitor); + visitor.setPreviousOffset(offset); + } + } + + /** + * Parses the instruction at the indicated offset. Indicate the + * result by calling the visitor if supplied and by returning the + * number of bytes consumed by the instruction. + * + *

In order to simplify further processing, the opcodes passed + * to the visitor are canonicalized, altering the opcode to a more + * universal one and making formerly implicit arguments + * explicit. In particular:

+ * + *
    + *
  • The opcodes to push literal constants of primitive types all become + * {@code ldc}. + * E.g., {@code fconst_0}, {@code sipush}, and + * {@code lconst_0} qualify for this treatment.
  • + *
  • {@code aconst_null} becomes {@code ldc} of a + * "known null."
  • + *
  • Shorthand local variable accessors become the corresponding + * longhand. E.g. {@code aload_2} becomes {@code aload}.
  • + *
  • {@code goto_w} and {@code jsr_w} become {@code goto} + * and {@code jsr} (respectively).
  • + *
  • {@code ldc_w} becomes {@code ldc}.
  • + *
  • {@code tableswitch} becomes {@code lookupswitch}. + *
  • Arithmetic, array, and value-returning ops are collapsed + * to the {@code int} variant opcode, with the {@code type} + * argument set to indicate the actual type. E.g., + * {@code fadd} becomes {@code iadd}, but + * {@code type} is passed as {@code Type.FLOAT} in that + * case. Similarly, {@code areturn} becomes + * {@code ireturn}. (However, {@code return} remains + * unchanged.
  • + *
  • Local variable access ops are collapsed to the {@code int} + * variant opcode, with the {@code type} argument set to indicate + * the actual type. E.g., {@code aload} becomes {@code iload}, + * but {@code type} is passed as {@code Type.OBJECT} in + * that case.
  • + *
  • Numeric conversion ops ({@code i2l}, etc.) are left alone + * to avoid too much confustion, but their {@code type} is + * the pushed type. E.g., {@code i2b} gets type + * {@code Type.INT}, and {@code f2d} gets type + * {@code Type.DOUBLE}. Other unaltered opcodes also get + * their pushed type. E.g., {@code arraylength} gets type + * {@code Type.INT}.
  • + *
+ * + * @param offset {@code >= 0, < bytes.size();} offset to the start of the + * instruction + * @param visitor {@code null-ok;} visitor to call back to + * @return the length of the instruction, in bytes + */ + public int parseInstruction(int offset, Visitor visitor) { + if (visitor == null) { + visitor = EMPTY_VISITOR; + } + + try { + int opcode = bytes.getUnsignedByte(offset); + int info = ByteOps.opInfo(opcode); + int fmt = info & ByteOps.FMT_MASK; + + switch (opcode) { + case ByteOps.NOP: { + visitor.visitNoArgs(opcode, offset, 1, Type.VOID); + return 1; + } + case ByteOps.ACONST_NULL: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstKnownNull.THE_ONE, 0); + return 1; + } + case ByteOps.ICONST_M1: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstInteger.VALUE_M1, -1); + return 1; + } + case ByteOps.ICONST_0: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstInteger.VALUE_0, 0); + return 1; + } + case ByteOps.ICONST_1: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstInteger.VALUE_1, 1); + return 1; + } + case ByteOps.ICONST_2: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstInteger.VALUE_2, 2); + return 1; + } + case ByteOps.ICONST_3: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstInteger.VALUE_3, 3); + return 1; + } + case ByteOps.ICONST_4: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstInteger.VALUE_4, 4); + return 1; + } + case ByteOps.ICONST_5: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstInteger.VALUE_5, 5); + return 1; + } + case ByteOps.LCONST_0: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstLong.VALUE_0, 0); + return 1; + } + case ByteOps.LCONST_1: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstLong.VALUE_1, 0); + return 1; + } + case ByteOps.FCONST_0: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstFloat.VALUE_0, 0); + return 1; + } + case ByteOps.FCONST_1: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstFloat.VALUE_1, 0); + return 1; + } + case ByteOps.FCONST_2: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstFloat.VALUE_2, 0); + return 1; + } + case ByteOps.DCONST_0: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstDouble.VALUE_0, 0); + return 1; + } + case ByteOps.DCONST_1: { + visitor.visitConstant(ByteOps.LDC, offset, 1, + CstDouble.VALUE_1, 0); + return 1; + } + case ByteOps.BIPUSH: { + int value = bytes.getByte(offset + 1); + visitor.visitConstant(ByteOps.LDC, offset, 2, + CstInteger.make(value), value); + return 2; + } + case ByteOps.SIPUSH: { + int value = bytes.getShort(offset + 1); + visitor.visitConstant(ByteOps.LDC, offset, 3, + CstInteger.make(value), value); + return 3; + } + case ByteOps.LDC: { + int idx = bytes.getUnsignedByte(offset + 1); + Constant cst = pool.get(idx); + int value = (cst instanceof CstInteger) ? + ((CstInteger) cst).getValue() : 0; + visitor.visitConstant(ByteOps.LDC, offset, 2, cst, value); + return 2; + } + case ByteOps.LDC_W: { + int idx = bytes.getUnsignedShort(offset + 1); + Constant cst = pool.get(idx); + int value = (cst instanceof CstInteger) ? + ((CstInteger) cst).getValue() : 0; + visitor.visitConstant(ByteOps.LDC, offset, 3, cst, value); + return 3; + } + case ByteOps.LDC2_W: { + int idx = bytes.getUnsignedShort(offset + 1); + Constant cst = pool.get(idx); + visitor.visitConstant(ByteOps.LDC2_W, offset, 3, cst, 0); + return 3; + } + case ByteOps.ILOAD: { + int idx = bytes.getUnsignedByte(offset + 1); + visitor.visitLocal(ByteOps.ILOAD, offset, 2, idx, + Type.INT, 0); + return 2; + } + case ByteOps.LLOAD: { + int idx = bytes.getUnsignedByte(offset + 1); + visitor.visitLocal(ByteOps.ILOAD, offset, 2, idx, + Type.LONG, 0); + return 2; + } + case ByteOps.FLOAD: { + int idx = bytes.getUnsignedByte(offset + 1); + visitor.visitLocal(ByteOps.ILOAD, offset, 2, idx, + Type.FLOAT, 0); + return 2; + } + case ByteOps.DLOAD: { + int idx = bytes.getUnsignedByte(offset + 1); + visitor.visitLocal(ByteOps.ILOAD, offset, 2, idx, + Type.DOUBLE, 0); + return 2; + } + case ByteOps.ALOAD: { + int idx = bytes.getUnsignedByte(offset + 1); + visitor.visitLocal(ByteOps.ILOAD, offset, 2, idx, + Type.OBJECT, 0); + return 2; + } + case ByteOps.ILOAD_0: + case ByteOps.ILOAD_1: + case ByteOps.ILOAD_2: + case ByteOps.ILOAD_3: { + int idx = opcode - ByteOps.ILOAD_0; + visitor.visitLocal(ByteOps.ILOAD, offset, 1, idx, + Type.INT, 0); + return 1; + } + case ByteOps.LLOAD_0: + case ByteOps.LLOAD_1: + case ByteOps.LLOAD_2: + case ByteOps.LLOAD_3: { + int idx = opcode - ByteOps.LLOAD_0; + visitor.visitLocal(ByteOps.ILOAD, offset, 1, idx, + Type.LONG, 0); + return 1; + } + case ByteOps.FLOAD_0: + case ByteOps.FLOAD_1: + case ByteOps.FLOAD_2: + case ByteOps.FLOAD_3: { + int idx = opcode - ByteOps.FLOAD_0; + visitor.visitLocal(ByteOps.ILOAD, offset, 1, idx, + Type.FLOAT, 0); + return 1; + } + case ByteOps.DLOAD_0: + case ByteOps.DLOAD_1: + case ByteOps.DLOAD_2: + case ByteOps.DLOAD_3: { + int idx = opcode - ByteOps.DLOAD_0; + visitor.visitLocal(ByteOps.ILOAD, offset, 1, idx, + Type.DOUBLE, 0); + return 1; + } + case ByteOps.ALOAD_0: + case ByteOps.ALOAD_1: + case ByteOps.ALOAD_2: + case ByteOps.ALOAD_3: { + int idx = opcode - ByteOps.ALOAD_0; + visitor.visitLocal(ByteOps.ILOAD, offset, 1, idx, + Type.OBJECT, 0); + return 1; + } + case ByteOps.IALOAD: { + visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, Type.INT); + return 1; + } + case ByteOps.LALOAD: { + visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, Type.LONG); + return 1; + } + case ByteOps.FALOAD: { + visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, + Type.FLOAT); + return 1; + } + case ByteOps.DALOAD: { + visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, + Type.DOUBLE); + return 1; + } + case ByteOps.AALOAD: { + visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, + Type.OBJECT); + return 1; + } + case ByteOps.BALOAD: { + /* + * Note: This is a load from either a byte[] or a + * boolean[]. + */ + visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, Type.BYTE); + return 1; + } + case ByteOps.CALOAD: { + visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, Type.CHAR); + return 1; + } + case ByteOps.SALOAD: { + visitor.visitNoArgs(ByteOps.IALOAD, offset, 1, + Type.SHORT); + return 1; + } + case ByteOps.ISTORE: { + int idx = bytes.getUnsignedByte(offset + 1); + visitor.visitLocal(ByteOps.ISTORE, offset, 2, idx, + Type.INT, 0); + return 2; + } + case ByteOps.LSTORE: { + int idx = bytes.getUnsignedByte(offset + 1); + visitor.visitLocal(ByteOps.ISTORE, offset, 2, idx, + Type.LONG, 0); + return 2; + } + case ByteOps.FSTORE: { + int idx = bytes.getUnsignedByte(offset + 1); + visitor.visitLocal(ByteOps.ISTORE, offset, 2, idx, + Type.FLOAT, 0); + return 2; + } + case ByteOps.DSTORE: { + int idx = bytes.getUnsignedByte(offset + 1); + visitor.visitLocal(ByteOps.ISTORE, offset, 2, idx, + Type.DOUBLE, 0); + return 2; + } + case ByteOps.ASTORE: { + int idx = bytes.getUnsignedByte(offset + 1); + visitor.visitLocal(ByteOps.ISTORE, offset, 2, idx, + Type.OBJECT, 0); + return 2; + } + case ByteOps.ISTORE_0: + case ByteOps.ISTORE_1: + case ByteOps.ISTORE_2: + case ByteOps.ISTORE_3: { + int idx = opcode - ByteOps.ISTORE_0; + visitor.visitLocal(ByteOps.ISTORE, offset, 1, idx, + Type.INT, 0); + return 1; + } + case ByteOps.LSTORE_0: + case ByteOps.LSTORE_1: + case ByteOps.LSTORE_2: + case ByteOps.LSTORE_3: { + int idx = opcode - ByteOps.LSTORE_0; + visitor.visitLocal(ByteOps.ISTORE, offset, 1, idx, + Type.LONG, 0); + return 1; + } + case ByteOps.FSTORE_0: + case ByteOps.FSTORE_1: + case ByteOps.FSTORE_2: + case ByteOps.FSTORE_3: { + int idx = opcode - ByteOps.FSTORE_0; + visitor.visitLocal(ByteOps.ISTORE, offset, 1, idx, + Type.FLOAT, 0); + return 1; + } + case ByteOps.DSTORE_0: + case ByteOps.DSTORE_1: + case ByteOps.DSTORE_2: + case ByteOps.DSTORE_3: { + int idx = opcode - ByteOps.DSTORE_0; + visitor.visitLocal(ByteOps.ISTORE, offset, 1, idx, + Type.DOUBLE, 0); + return 1; + } + case ByteOps.ASTORE_0: + case ByteOps.ASTORE_1: + case ByteOps.ASTORE_2: + case ByteOps.ASTORE_3: { + int idx = opcode - ByteOps.ASTORE_0; + visitor.visitLocal(ByteOps.ISTORE, offset, 1, idx, + Type.OBJECT, 0); + return 1; + } + case ByteOps.IASTORE: { + visitor.visitNoArgs(ByteOps.IASTORE, offset, 1, Type.INT); + return 1; + } + case ByteOps.LASTORE: { + visitor.visitNoArgs(ByteOps.IASTORE, offset, 1, + Type.LONG); + return 1; + } + case ByteOps.FASTORE: { + visitor.visitNoArgs(ByteOps.IASTORE, offset, 1, + Type.FLOAT); + return 1; + } + case ByteOps.DASTORE: { + visitor.visitNoArgs(ByteOps.IASTORE, offset, 1, + Type.DOUBLE); + return 1; + } + case ByteOps.AASTORE: { + visitor.visitNoArgs(ByteOps.IASTORE, offset, 1, + Type.OBJECT); + return 1; + } + case ByteOps.BASTORE: { + /* + * Note: This is a load from either a byte[] or a + * boolean[]. + */ + visitor.visitNoArgs(ByteOps.IASTORE, offset, 1, + Type.BYTE); + return 1; + } + case ByteOps.CASTORE: { + visitor.visitNoArgs(ByteOps.IASTORE, offset, 1, + Type.CHAR); + return 1; + } + case ByteOps.SASTORE: { + visitor.visitNoArgs(ByteOps.IASTORE, offset, 1, + Type.SHORT); + return 1; + } + case ByteOps.POP: + case ByteOps.POP2: + case ByteOps.DUP: + case ByteOps.DUP_X1: + case ByteOps.DUP_X2: + case ByteOps.DUP2: + case ByteOps.DUP2_X1: + case ByteOps.DUP2_X2: + case ByteOps.SWAP: { + visitor.visitNoArgs(opcode, offset, 1, Type.VOID); + return 1; + } + case ByteOps.IADD: + case ByteOps.ISUB: + case ByteOps.IMUL: + case ByteOps.IDIV: + case ByteOps.IREM: + case ByteOps.INEG: + case ByteOps.ISHL: + case ByteOps.ISHR: + case ByteOps.IUSHR: + case ByteOps.IAND: + case ByteOps.IOR: + case ByteOps.IXOR: { + visitor.visitNoArgs(opcode, offset, 1, Type.INT); + return 1; + } + case ByteOps.LADD: + case ByteOps.LSUB: + case ByteOps.LMUL: + case ByteOps.LDIV: + case ByteOps.LREM: + case ByteOps.LNEG: + case ByteOps.LSHL: + case ByteOps.LSHR: + case ByteOps.LUSHR: + case ByteOps.LAND: + case ByteOps.LOR: + case ByteOps.LXOR: { + /* + * It's "opcode - 1" because, conveniently enough, all + * these long ops are one past the int variants. + */ + visitor.visitNoArgs(opcode - 1, offset, 1, Type.LONG); + return 1; + } + case ByteOps.FADD: + case ByteOps.FSUB: + case ByteOps.FMUL: + case ByteOps.FDIV: + case ByteOps.FREM: + case ByteOps.FNEG: { + /* + * It's "opcode - 2" because, conveniently enough, all + * these float ops are two past the int variants. + */ + visitor.visitNoArgs(opcode - 2, offset, 1, Type.FLOAT); + return 1; + } + case ByteOps.DADD: + case ByteOps.DSUB: + case ByteOps.DMUL: + case ByteOps.DDIV: + case ByteOps.DREM: + case ByteOps.DNEG: { + /* + * It's "opcode - 3" because, conveniently enough, all + * these double ops are three past the int variants. + */ + visitor.visitNoArgs(opcode - 3, offset, 1, Type.DOUBLE); + return 1; + } + case ByteOps.IINC: { + int idx = bytes.getUnsignedByte(offset + 1); + int value = bytes.getByte(offset + 2); + visitor.visitLocal(opcode, offset, 3, idx, + Type.INT, value); + return 3; + } + case ByteOps.I2L: + case ByteOps.F2L: + case ByteOps.D2L: { + visitor.visitNoArgs(opcode, offset, 1, Type.LONG); + return 1; + } + case ByteOps.I2F: + case ByteOps.L2F: + case ByteOps.D2F: { + visitor.visitNoArgs(opcode, offset, 1, Type.FLOAT); + return 1; + } + case ByteOps.I2D: + case ByteOps.L2D: + case ByteOps.F2D: { + visitor.visitNoArgs(opcode, offset, 1, Type.DOUBLE); + return 1; + } + case ByteOps.L2I: + case ByteOps.F2I: + case ByteOps.D2I: + case ByteOps.I2B: + case ByteOps.I2C: + case ByteOps.I2S: + case ByteOps.LCMP: + case ByteOps.FCMPL: + case ByteOps.FCMPG: + case ByteOps.DCMPL: + case ByteOps.DCMPG: + case ByteOps.ARRAYLENGTH: { + visitor.visitNoArgs(opcode, offset, 1, Type.INT); + return 1; + } + case ByteOps.IFEQ: + case ByteOps.IFNE: + case ByteOps.IFLT: + case ByteOps.IFGE: + case ByteOps.IFGT: + case ByteOps.IFLE: + case ByteOps.IF_ICMPEQ: + case ByteOps.IF_ICMPNE: + case ByteOps.IF_ICMPLT: + case ByteOps.IF_ICMPGE: + case ByteOps.IF_ICMPGT: + case ByteOps.IF_ICMPLE: + case ByteOps.IF_ACMPEQ: + case ByteOps.IF_ACMPNE: + case ByteOps.GOTO: + case ByteOps.JSR: + case ByteOps.IFNULL: + case ByteOps.IFNONNULL: { + int target = offset + bytes.getShort(offset + 1); + visitor.visitBranch(opcode, offset, 3, target); + return 3; + } + case ByteOps.RET: { + int idx = bytes.getUnsignedByte(offset + 1); + visitor.visitLocal(opcode, offset, 2, idx, + Type.RETURN_ADDRESS, 0); + return 2; + } + case ByteOps.TABLESWITCH: { + return parseTableswitch(offset, visitor); + } + case ByteOps.LOOKUPSWITCH: { + return parseLookupswitch(offset, visitor); + } + case ByteOps.IRETURN: { + visitor.visitNoArgs(ByteOps.IRETURN, offset, 1, Type.INT); + return 1; + } + case ByteOps.LRETURN: { + visitor.visitNoArgs(ByteOps.IRETURN, offset, 1, + Type.LONG); + return 1; + } + case ByteOps.FRETURN: { + visitor.visitNoArgs(ByteOps.IRETURN, offset, 1, + Type.FLOAT); + return 1; + } + case ByteOps.DRETURN: { + visitor.visitNoArgs(ByteOps.IRETURN, offset, 1, + Type.DOUBLE); + return 1; + } + case ByteOps.ARETURN: { + visitor.visitNoArgs(ByteOps.IRETURN, offset, 1, + Type.OBJECT); + return 1; + } + case ByteOps.RETURN: + case ByteOps.ATHROW: + case ByteOps.MONITORENTER: + case ByteOps.MONITOREXIT: { + visitor.visitNoArgs(opcode, offset, 1, Type.VOID); + return 1; + } + case ByteOps.GETSTATIC: + case ByteOps.PUTSTATIC: + case ByteOps.GETFIELD: + case ByteOps.PUTFIELD: + case ByteOps.INVOKEVIRTUAL: + case ByteOps.INVOKESPECIAL: + case ByteOps.INVOKESTATIC: + case ByteOps.NEW: + case ByteOps.ANEWARRAY: + case ByteOps.CHECKCAST: + case ByteOps.INSTANCEOF: { + int idx = bytes.getUnsignedShort(offset + 1); + Constant cst = pool.get(idx); + visitor.visitConstant(opcode, offset, 3, cst, 0); + return 3; + } + case ByteOps.INVOKEINTERFACE: { + int idx = bytes.getUnsignedShort(offset + 1); + int count = bytes.getUnsignedByte(offset + 3); + int expectZero = bytes.getUnsignedByte(offset + 4); + Constant cst = pool.get(idx); + visitor.visitConstant(opcode, offset, 5, cst, + count | (expectZero << 8)); + return 5; + } + case ByteOps.INVOKEDYNAMIC: { + int idx = bytes.getUnsignedShort(offset + 1); + // Skip to must-be-zero bytes at offsets 3 and 4 + CstInvokeDynamic cstInvokeDynamic = (CstInvokeDynamic) pool.get(idx); + visitor.visitConstant(opcode, offset, 5, cstInvokeDynamic, 0); + return 5; + } + case ByteOps.NEWARRAY: { + return parseNewarray(offset, visitor); + } + case ByteOps.WIDE: { + return parseWide(offset, visitor); + } + case ByteOps.MULTIANEWARRAY: { + int idx = bytes.getUnsignedShort(offset + 1); + int dimensions = bytes.getUnsignedByte(offset + 3); + Constant cst = pool.get(idx); + visitor.visitConstant(opcode, offset, 4, cst, dimensions); + return 4; + } + case ByteOps.GOTO_W: + case ByteOps.JSR_W: { + int target = offset + bytes.getInt(offset + 1); + int newop = + (opcode == ByteOps.GOTO_W) ? ByteOps.GOTO : + ByteOps.JSR; + visitor.visitBranch(newop, offset, 5, target); + return 5; + } + default: { + visitor.visitInvalid(opcode, offset, 1); + return 1; + } + } + } catch (SimException ex) { + ex.addContext("...at bytecode offset " + Hex.u4(offset)); + throw ex; + } catch (RuntimeException ex) { + SimException se = new SimException(ex); + se.addContext("...at bytecode offset " + Hex.u4(offset)); + throw se; + } + } + + /** + * Helper to deal with {@code tableswitch}. + * + * @param offset the offset to the {@code tableswitch} opcode itself + * @param visitor {@code non-null;} visitor to use + * @return instruction length, in bytes + */ + private int parseTableswitch(int offset, Visitor visitor) { + int at = (offset + 4) & ~3; // "at" skips the padding. + + // Collect the padding. + int padding = 0; + for (int i = offset + 1; i < at; i++) { + padding = (padding << 8) | bytes.getUnsignedByte(i); + } + + int defaultTarget = offset + bytes.getInt(at); + int low = bytes.getInt(at + 4); + int high = bytes.getInt(at + 8); + int count = high - low + 1; + at += 12; + + if (low > high) { + throw new SimException("low / high inversion"); + } + + SwitchList cases = new SwitchList(count); + for (int i = 0; i < count; i++) { + int target = offset + bytes.getInt(at); + at += 4; + cases.add(low + i, target); + } + cases.setDefaultTarget(defaultTarget); + cases.removeSuperfluousDefaults(); + cases.setImmutable(); + + int length = at - offset; + visitor.visitSwitch(ByteOps.LOOKUPSWITCH, offset, length, cases, + padding); + + return length; + } + + /** + * Helper to deal with {@code lookupswitch}. + * + * @param offset the offset to the {@code lookupswitch} opcode itself + * @param visitor {@code non-null;} visitor to use + * @return instruction length, in bytes + */ + private int parseLookupswitch(int offset, Visitor visitor) { + int at = (offset + 4) & ~3; // "at" skips the padding. + + // Collect the padding. + int padding = 0; + for (int i = offset + 1; i < at; i++) { + padding = (padding << 8) | bytes.getUnsignedByte(i); + } + + int defaultTarget = offset + bytes.getInt(at); + int npairs = bytes.getInt(at + 4); + at += 8; + + SwitchList cases = new SwitchList(npairs); + for (int i = 0; i < npairs; i++) { + int match = bytes.getInt(at); + int target = offset + bytes.getInt(at + 4); + at += 8; + cases.add(match, target); + } + cases.setDefaultTarget(defaultTarget); + cases.removeSuperfluousDefaults(); + cases.setImmutable(); + + int length = at - offset; + visitor.visitSwitch(ByteOps.LOOKUPSWITCH, offset, length, cases, + padding); + + return length; + } + + /** + * Helper to deal with {@code newarray}. + * + * @param offset the offset to the {@code newarray} opcode itself + * @param visitor {@code non-null;} visitor to use + * @return instruction length, in bytes + */ + private int parseNewarray(int offset, Visitor visitor) { + int value = bytes.getUnsignedByte(offset + 1); + CstType type; + switch (value) { + case ByteOps.NEWARRAY_BOOLEAN: { + type = CstType.BOOLEAN_ARRAY; + break; + } + case ByteOps.NEWARRAY_CHAR: { + type = CstType.CHAR_ARRAY; + break; + } + case ByteOps.NEWARRAY_DOUBLE: { + type = CstType.DOUBLE_ARRAY; + break; + } + case ByteOps.NEWARRAY_FLOAT: { + type = CstType.FLOAT_ARRAY; + break; + } + case ByteOps.NEWARRAY_BYTE: { + type = CstType.BYTE_ARRAY; + break; + } + case ByteOps.NEWARRAY_SHORT: { + type = CstType.SHORT_ARRAY; + break; + } + case ByteOps.NEWARRAY_INT: { + type = CstType.INT_ARRAY; + break; + } + case ByteOps.NEWARRAY_LONG: { + type = CstType.LONG_ARRAY; + break; + } + default: { + throw new SimException("bad newarray code " + + Hex.u1(value)); + } + } + + // Revisit the previous bytecode to find out the length of the array + int previousOffset = visitor.getPreviousOffset(); + ConstantParserVisitor constantVisitor = new ConstantParserVisitor(); + int arrayLength = 0; + + /* + * For visitors that don't record the previous offset, -1 will be + * seen here + */ + if (previousOffset >= 0) { + parseInstruction(previousOffset, constantVisitor); + if (constantVisitor.cst instanceof CstInteger && + constantVisitor.length + previousOffset == offset) { + arrayLength = constantVisitor.value; + + } + } + + /* + * Try to match the array initialization idiom. For example, if the + * subsequent code is initializing an int array, we are expecting the + * following pattern repeatedly: + * dup + * push index + * push value + * *astore + * + * where the index value will be incrimented sequentially from 0 up. + */ + int nInit = 0; + int curOffset = offset+2; + int lastOffset = curOffset; + ArrayList initVals = new ArrayList(); + + if (arrayLength != 0) { + while (true) { + boolean punt = false; + + // First, check if the next bytecode is dup. + int nextByte = bytes.getUnsignedByte(curOffset++); + if (nextByte != ByteOps.DUP) + break; + + /* + * Next, check if the expected array index is pushed to + * the stack. + */ + parseInstruction(curOffset, constantVisitor); + if (constantVisitor.length == 0 || + !(constantVisitor.cst instanceof CstInteger) || + constantVisitor.value != nInit) + break; + + // Next, fetch the init value and record it. + curOffset += constantVisitor.length; + + /* + * Next, find out what kind of constant is pushed onto + * the stack. + */ + parseInstruction(curOffset, constantVisitor); + if (constantVisitor.length == 0 || + !(constantVisitor.cst instanceof CstLiteralBits)) + break; + + curOffset += constantVisitor.length; + initVals.add(constantVisitor.cst); + + nextByte = bytes.getUnsignedByte(curOffset++); + // Now, check if the value is stored to the array properly. + switch (value) { + case ByteOps.NEWARRAY_BYTE: + case ByteOps.NEWARRAY_BOOLEAN: { + if (nextByte != ByteOps.BASTORE) { + punt = true; + } + break; + } + case ByteOps.NEWARRAY_CHAR: { + if (nextByte != ByteOps.CASTORE) { + punt = true; + } + break; + } + case ByteOps.NEWARRAY_DOUBLE: { + if (nextByte != ByteOps.DASTORE) { + punt = true; + } + break; + } + case ByteOps.NEWARRAY_FLOAT: { + if (nextByte != ByteOps.FASTORE) { + punt = true; + } + break; + } + case ByteOps.NEWARRAY_SHORT: { + if (nextByte != ByteOps.SASTORE) { + punt = true; + } + break; + } + case ByteOps.NEWARRAY_INT: { + if (nextByte != ByteOps.IASTORE) { + punt = true; + } + break; + } + case ByteOps.NEWARRAY_LONG: { + if (nextByte != ByteOps.LASTORE) { + punt = true; + } + break; + } + default: + punt = true; + break; + } + if (punt) { + break; + } + lastOffset = curOffset; + nInit++; + } + } + + /* + * For singleton arrays it is still more economical to + * generate the aput. + */ + if (nInit < 2 || nInit != arrayLength) { + visitor.visitNewarray(offset, 2, type, null); + return 2; + } else { + visitor.visitNewarray(offset, lastOffset - offset, type, initVals); + return lastOffset - offset; + } + } + + + /** + * Helper to deal with {@code wide}. + * + * @param offset the offset to the {@code wide} opcode itself + * @param visitor {@code non-null;} visitor to use + * @return instruction length, in bytes + */ + private int parseWide(int offset, Visitor visitor) { + int opcode = bytes.getUnsignedByte(offset + 1); + int idx = bytes.getUnsignedShort(offset + 2); + switch (opcode) { + case ByteOps.ILOAD: { + visitor.visitLocal(ByteOps.ILOAD, offset, 4, idx, + Type.INT, 0); + return 4; + } + case ByteOps.LLOAD: { + visitor.visitLocal(ByteOps.ILOAD, offset, 4, idx, + Type.LONG, 0); + return 4; + } + case ByteOps.FLOAD: { + visitor.visitLocal(ByteOps.ILOAD, offset, 4, idx, + Type.FLOAT, 0); + return 4; + } + case ByteOps.DLOAD: { + visitor.visitLocal(ByteOps.ILOAD, offset, 4, idx, + Type.DOUBLE, 0); + return 4; + } + case ByteOps.ALOAD: { + visitor.visitLocal(ByteOps.ILOAD, offset, 4, idx, + Type.OBJECT, 0); + return 4; + } + case ByteOps.ISTORE: { + visitor.visitLocal(ByteOps.ISTORE, offset, 4, idx, + Type.INT, 0); + return 4; + } + case ByteOps.LSTORE: { + visitor.visitLocal(ByteOps.ISTORE, offset, 4, idx, + Type.LONG, 0); + return 4; + } + case ByteOps.FSTORE: { + visitor.visitLocal(ByteOps.ISTORE, offset, 4, idx, + Type.FLOAT, 0); + return 4; + } + case ByteOps.DSTORE: { + visitor.visitLocal(ByteOps.ISTORE, offset, 4, idx, + Type.DOUBLE, 0); + return 4; + } + case ByteOps.ASTORE: { + visitor.visitLocal(ByteOps.ISTORE, offset, 4, idx, + Type.OBJECT, 0); + return 4; + } + case ByteOps.RET: { + visitor.visitLocal(opcode, offset, 4, idx, + Type.RETURN_ADDRESS, 0); + return 4; + } + case ByteOps.IINC: { + int value = bytes.getShort(offset + 4); + visitor.visitLocal(opcode, offset, 6, idx, + Type.INT, value); + return 6; + } + default: { + visitor.visitInvalid(ByteOps.WIDE, offset, 1); + return 1; + } + } + } + + /** + * Instruction visitor interface. + */ + public interface Visitor { + /** + * Visits an invalid instruction. + * + * @param opcode the opcode + * @param offset offset to the instruction + * @param length length of the instruction, in bytes + */ + public void visitInvalid(int opcode, int offset, int length); + + /** + * Visits an instruction which has no inline arguments + * (implicit or explicit). + * + * @param opcode the opcode + * @param offset offset to the instruction + * @param length length of the instruction, in bytes + * @param type {@code non-null;} type the instruction operates on + */ + public void visitNoArgs(int opcode, int offset, int length, + Type type); + + /** + * Visits an instruction which has a local variable index argument. + * + * @param opcode the opcode + * @param offset offset to the instruction + * @param length length of the instruction, in bytes + * @param idx the local variable index + * @param type {@code non-null;} the type of the accessed value + * @param value additional literal integer argument, if salient (i.e., + * for {@code iinc}) + */ + public void visitLocal(int opcode, int offset, int length, + int idx, Type type, int value); + + /** + * Visits an instruction which has a (possibly synthetic) + * constant argument, and possibly also an + * additional literal integer argument. In the case of + * {@code multianewarray}, the argument is the count of + * dimensions. In the case of {@code invokeinterface}, + * the argument is the parameter count or'ed with the + * should-be-zero value left-shifted by 8. In the case of entries + * of type {@code int}, the {@code value} field always + * holds the raw value (for convenience of clients). + * + *

Note: In order to avoid giving it a barely-useful + * visitor all its own, {@code newarray} also uses this + * form, passing {@code value} as the array type code and + * {@code cst} as a {@link CstType} instance + * corresponding to the array type.

+ * + * @param opcode the opcode + * @param offset offset to the instruction + * @param length length of the instruction, in bytes + * @param cst {@code non-null;} the constant + * @param value additional literal integer argument, if salient + * (ignore if not) + */ + public void visitConstant(int opcode, int offset, int length, + Constant cst, int value); + + /** + * Visits an instruction which has a branch target argument. + * + * @param opcode the opcode + * @param offset offset to the instruction + * @param length length of the instruction, in bytes + * @param target the absolute (not relative) branch target + */ + public void visitBranch(int opcode, int offset, int length, + int target); + + /** + * Visits a switch instruction. + * + * @param opcode the opcode + * @param offset offset to the instruction + * @param length length of the instruction, in bytes + * @param cases {@code non-null;} list of (value, target) + * pairs, plus the default target + * @param padding the bytes found in the padding area (if any), + * packed + */ + public void visitSwitch(int opcode, int offset, int length, + SwitchList cases, int padding); + + /** + * Visits a newarray instruction. + * + * @param offset offset to the instruction + * @param length length of the instruction, in bytes + * @param type {@code non-null;} the type of the array + * @param initVals {@code non-null;} list of bytecode offsets + * for init values + */ + public void visitNewarray(int offset, int length, CstType type, + ArrayList initVals); + + /** + * Set previous bytecode offset + * @param offset offset of the previous fully parsed bytecode + */ + public void setPreviousOffset(int offset); + + /** + * Get previous bytecode offset + * @return return the recored offset of the previous bytecode + */ + public int getPreviousOffset(); + } + + /** + * Base implementation of {@link Visitor}, which has empty method + * bodies for all methods. + */ + public static class BaseVisitor implements Visitor { + + /** offset of the previously parsed bytecode */ + private int previousOffset; + + BaseVisitor() { + previousOffset = -1; + } + + /** {@inheritDoc} */ + @Override + public void visitInvalid(int opcode, int offset, int length) { + // This space intentionally left blank. + } + + /** {@inheritDoc} */ + @Override + public void visitNoArgs(int opcode, int offset, int length, + Type type) { + // This space intentionally left blank. + } + + /** {@inheritDoc} */ + @Override + public void visitLocal(int opcode, int offset, int length, + int idx, Type type, int value) { + // This space intentionally left blank. + } + + /** {@inheritDoc} */ + @Override + public void visitConstant(int opcode, int offset, int length, + Constant cst, int value) { + // This space intentionally left blank. + } + + /** {@inheritDoc} */ + @Override + public void visitBranch(int opcode, int offset, int length, + int target) { + // This space intentionally left blank. + } + + /** {@inheritDoc} */ + @Override + public void visitSwitch(int opcode, int offset, int length, + SwitchList cases, int padding) { + // This space intentionally left blank. + } + + /** {@inheritDoc} */ + @Override + public void visitNewarray(int offset, int length, CstType type, + ArrayList initValues) { + // This space intentionally left blank. + } + + /** {@inheritDoc} */ + @Override + public void setPreviousOffset(int offset) { + previousOffset = offset; + } + + /** {@inheritDoc} */ + @Override + public int getPreviousOffset() { + return previousOffset; + } + } + + /** + * Implementation of {@link Visitor}, which just pays attention + * to constant values. + */ + class ConstantParserVisitor extends BaseVisitor { + Constant cst; + int length; + int value; + + /** Empty constructor */ + ConstantParserVisitor() { + } + + private void clear() { + length = 0; + } + + /** {@inheritDoc} */ + @Override + public void visitInvalid(int opcode, int offset, int length) { + clear(); + } + + /** {@inheritDoc} */ + @Override + public void visitNoArgs(int opcode, int offset, int length, + Type type) { + clear(); + } + + /** {@inheritDoc} */ + @Override + public void visitLocal(int opcode, int offset, int length, + int idx, Type type, int value) { + clear(); + } + + /** {@inheritDoc} */ + @Override + public void visitConstant(int opcode, int offset, int length, + Constant cst, int value) { + this.cst = cst; + this.length = length; + this.value = value; + } + + /** {@inheritDoc} */ + @Override + public void visitBranch(int opcode, int offset, int length, + int target) { + clear(); + } + + /** {@inheritDoc} */ + @Override + public void visitSwitch(int opcode, int offset, int length, + SwitchList cases, int padding) { + clear(); + } + + /** {@inheritDoc} */ + @Override + public void visitNewarray(int offset, int length, CstType type, + ArrayList initVals) { + clear(); + } + + /** {@inheritDoc} */ + @Override + public void setPreviousOffset(int offset) { + // Intentionally left empty + } + + /** {@inheritDoc} */ + @Override + public int getPreviousOffset() { + // Intentionally left empty + return -1; + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/ConcreteMethod.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ConcreteMethod.java new file mode 100644 index 00000000..a5df3736 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ConcreteMethod.java @@ -0,0 +1,260 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.cf.attrib.AttCode; +import external.com.android.dx.cf.attrib.AttLineNumberTable; +import external.com.android.dx.cf.attrib.AttLocalVariableTable; +import external.com.android.dx.cf.attrib.AttLocalVariableTypeTable; +import external.com.android.dx.cf.iface.AttributeList; +import external.com.android.dx.cf.iface.ClassFile; +import external.com.android.dx.cf.iface.Method; +import external.com.android.dx.rop.code.AccessFlags; +import external.com.android.dx.rop.code.SourcePosition; +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.Prototype; + +/** + * Container for all the giblets that make up a concrete Java bytecode method. + * It implements {@link Method}, so it provides all the original access + * (by delegation), but it also constructs and keeps useful versions of + * stuff extracted from the method's {@code Code} attribute. + */ +public final class ConcreteMethod implements Method { + /** {@code non-null;} method being wrapped */ + private final Method method; + + /** {@code non-null;} the {@code ClassFile} the method belongs to. */ + private final ClassFile classFile; + + /** {@code non-null;} the code attribute */ + private final AttCode attCode; + + /** {@code non-null;} line number list */ + private final LineNumberList lineNumbers; + + /** {@code non-null;} local variable list */ + private final LocalVariableList localVariables; + + /** + * Constructs an instance. + * + * @param method {@code non-null;} the method to be based on + * @param classFile {@code non-null;} the class file that contains this method + * @param keepLines whether to keep the line number information + * (if any) + * @param keepLocals whether to keep the local variable + * information (if any) + */ + public ConcreteMethod(Method method, ClassFile classFile, + boolean keepLines, boolean keepLocals) { + this.method = method; + this.classFile = classFile; + + AttributeList attribs = method.getAttributes(); + this.attCode = (AttCode) attribs.findFirst(AttCode.ATTRIBUTE_NAME); + + AttributeList codeAttribs = attCode.getAttributes(); + + /* + * Combine all LineNumberTable attributes into one, with the + * combined result saved into the instance. The following code + * isn't particularly efficient for doing merges, but as far + * as I know, this situation rarely occurs "in the + * wild," so there's not much point in optimizing for it. + */ + LineNumberList lnl = LineNumberList.EMPTY; + if (keepLines) { + for (AttLineNumberTable lnt = (AttLineNumberTable) + codeAttribs.findFirst(AttLineNumberTable.ATTRIBUTE_NAME); + lnt != null; + lnt = (AttLineNumberTable) codeAttribs.findNext(lnt)) { + lnl = LineNumberList.concat(lnl, lnt.getLineNumbers()); + } + } + this.lineNumbers = lnl; + + LocalVariableList lvl = LocalVariableList.EMPTY; + if (keepLocals) { + /* + * Do likewise (and with the same caveat) for + * LocalVariableTable and LocalVariableTypeTable attributes. + * This combines both of these kinds of attribute into a + * single LocalVariableList. + */ + for (AttLocalVariableTable lvt = (AttLocalVariableTable) + codeAttribs.findFirst( + AttLocalVariableTable.ATTRIBUTE_NAME); + lvt != null; + lvt = (AttLocalVariableTable) codeAttribs.findNext(lvt)) { + + lvl = LocalVariableList.concat(lvl, lvt.getLocalVariables()); + } + + LocalVariableList typeList = LocalVariableList.EMPTY; + for (AttLocalVariableTypeTable lvtt = (AttLocalVariableTypeTable) + codeAttribs.findFirst( + AttLocalVariableTypeTable.ATTRIBUTE_NAME); + lvtt != null; + lvtt = (AttLocalVariableTypeTable) codeAttribs.findNext(lvtt)) { + typeList = LocalVariableList.concat(typeList, lvtt.getLocalVariables()); + } + + if (typeList.size() != 0) { + + lvl = LocalVariableList.mergeDescriptorsAndSignatures(lvl, typeList); + } + } + this.localVariables = lvl; + } + + + /** + * Gets the source file associated with the method if known. + * @return {null-ok;} the source file defining the method if known, null otherwise. + */ + public CstString getSourceFile() { + return classFile.getSourceFile(); + } + + /** + * Tests whether the method is being defined on an interface. + * @return true if the method is being defined on an interface. + */ + public final boolean isDefaultOrStaticInterfaceMethod() { + return (classFile.getAccessFlags() & AccessFlags.ACC_INTERFACE) != 0 + && !getNat().isClassInit(); + } + + /** + * Tests whether the method is being defined is declared as static. + * @return true if the method is being defined is declared as static. + */ + public final boolean isStaticMethod() { + return (getAccessFlags() & AccessFlags.ACC_STATIC) != 0; + } + + /** {@inheritDoc} */ + @Override + public CstNat getNat() { + return method.getNat(); + } + + /** {@inheritDoc} */ + @Override + public CstString getName() { + return method.getName(); + } + + /** {@inheritDoc} */ + @Override + public CstString getDescriptor() { + return method.getDescriptor(); + } + + /** {@inheritDoc} */ + @Override + public int getAccessFlags() { + return method.getAccessFlags(); + } + + /** {@inheritDoc} */ + @Override + public AttributeList getAttributes() { + return method.getAttributes(); + } + + /** {@inheritDoc} */ + @Override + public CstType getDefiningClass() { + return method.getDefiningClass(); + } + + /** {@inheritDoc} */ + @Override + public Prototype getEffectiveDescriptor() { + return method.getEffectiveDescriptor(); + } + + /** + * Gets the maximum stack size. + * + * @return {@code >= 0;} the maximum stack size + */ + public int getMaxStack() { + return attCode.getMaxStack(); + } + + /** + * Gets the number of locals. + * + * @return {@code >= 0;} the number of locals + */ + public int getMaxLocals() { + return attCode.getMaxLocals(); + } + + /** + * Gets the bytecode array. + * + * @return {@code non-null;} the bytecode array + */ + public BytecodeArray getCode() { + return attCode.getCode(); + } + + /** + * Gets the exception table. + * + * @return {@code non-null;} the exception table + */ + public ByteCatchList getCatches() { + return attCode.getCatches(); + } + + /** + * Gets the line number list. + * + * @return {@code non-null;} the line number list + */ + public LineNumberList getLineNumbers() { + return lineNumbers; + } + + /** + * Gets the local variable list. + * + * @return {@code non-null;} the local variable list + */ + public LocalVariableList getLocalVariables() { + return localVariables; + } + + /** + * Returns a {@link SourcePosition} instance corresponding to the + * given bytecode offset. + * + * @param offset {@code >= 0;} the bytecode offset + * @return {@code non-null;} an appropriate instance + */ + public SourcePosition makeSourcePosistion(int offset) { + return new SourcePosition(getSourceFile(), offset, + lineNumbers.pcToLine(offset)); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/ExecutionStack.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ExecutionStack.java new file mode 100644 index 00000000..8fa411a3 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ExecutionStack.java @@ -0,0 +1,343 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dex.util.ExceptionWithContext; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.rop.type.TypeBearer; +import external.com.android.dx.util.Hex; +import external.com.android.dx.util.MutabilityControl; + +/** + * Representation of a Java method execution stack. + * + *

Note: For the most part, the documentation for this class + * ignores the distinction between {@link Type} and {@link + * TypeBearer}.

+ */ +public final class ExecutionStack extends MutabilityControl { + /** {@code non-null;} array of stack contents */ + private final TypeBearer[] stack; + + /** + * {@code non-null;} array specifying whether stack contents have entries + * in the local variable table + */ + private final boolean[] local; + /** + * {@code >= 0;} stack pointer (points one past the end) / current stack + * size + */ + private int stackPtr; + + /** + * Constructs an instance. + * + * @param maxStack {@code >= 0;} the maximum size of the stack for this + * instance + */ + public ExecutionStack(int maxStack) { + super(maxStack != 0); + stack = new TypeBearer[maxStack]; + local = new boolean[maxStack]; + stackPtr = 0; + } + + /** + * Makes and returns a mutable copy of this instance. + * + * @return {@code non-null;} the copy + */ + public ExecutionStack copy() { + ExecutionStack result = new ExecutionStack(stack.length); + + System.arraycopy(stack, 0, result.stack, 0, stack.length); + System.arraycopy(local, 0, result.local, 0, local.length); + result.stackPtr = stackPtr; + + return result; + } + + /** + * Annotates (adds context to) the given exception with information + * about this instance. + * + * @param ex {@code non-null;} the exception to annotate + */ + public void annotate(ExceptionWithContext ex) { + int limit = stackPtr - 1; + + for (int i = 0; i <= limit; i++) { + String idx = (i == limit) ? "top0" : Hex.u2(limit - i); + + ex.addContext("stack[" + idx + "]: " + + stackElementString(stack[i])); + } + } + + /** + * Replaces all the occurrences of the given uninitialized type in + * this stack with its initialized equivalent. + * + * @param type {@code non-null;} type to replace + */ + public void makeInitialized(Type type) { + if (stackPtr == 0) { + // We have to check for this before checking for immutability. + return; + } + + throwIfImmutable(); + + Type initializedType = type.getInitializedType(); + + for (int i = 0; i < stackPtr; i++) { + if (stack[i] == type) { + stack[i] = initializedType; + } + } + } + + /** + * Gets the maximum stack size for this instance. + * + * @return {@code >= 0;} the max stack size + */ + public int getMaxStack() { + return stack.length; + } + + /** + * Gets the current stack size. + * + * @return {@code >= 0, < getMaxStack();} the current stack size + */ + public int size() { + return stackPtr; + } + + /** + * Clears the stack. (That is, this method pops everything off.) + */ + public void clear() { + throwIfImmutable(); + + for (int i = 0; i < stackPtr; i++) { + stack[i] = null; + local[i] = false; + } + + stackPtr = 0; + } + + /** + * Pushes a value of the given type onto the stack. + * + * @param type {@code non-null;} type of the value + * @throws SimException thrown if there is insufficient room on the + * stack for the value + */ + public void push(TypeBearer type) { + throwIfImmutable(); + + int category; + + try { + type = type.getFrameType(); + category = type.getType().getCategory(); + } catch (NullPointerException ex) { + // Elucidate the exception. + throw new NullPointerException("type == null"); + } + + if ((stackPtr + category) > stack.length) { + throwSimException("overflow"); + return; + } + + if (category == 2) { + stack[stackPtr] = null; + stackPtr++; + } + + stack[stackPtr] = type; + stackPtr++; + } + + /** + * Flags the next value pushed onto the stack as having local info. + */ + public void setLocal() { + throwIfImmutable(); + + local[stackPtr] = true; + } + + /** + * Peeks at the {@code n}th element down from the top of the stack. + * {@code n == 0} means to peek at the top of the stack. Note that + * this will return {@code null} if the indicated element is the + * deeper half of a category-2 value. + * + * @param n {@code >= 0;} which element to peek at + * @return {@code null-ok;} the type of value stored at that element + * @throws SimException thrown if {@code n >= size()} + */ + public TypeBearer peek(int n) { + if (n < 0) { + throw new IllegalArgumentException("n < 0"); + } + + if (n >= stackPtr) { + return throwSimException("underflow"); + } + + return stack[stackPtr - n - 1]; + } + + /** + * Peeks at the {@code n}th element down from the top of the + * stack, returning whether or not it has local info. + * + * @param n {@code >= 0;} which element to peek at + * @return {@code true} if the value has local info, {@code false} otherwise + * @throws SimException thrown if {@code n >= size()} + */ + public boolean peekLocal(int n) { + if (n < 0) { + throw new IllegalArgumentException("n < 0"); + } + + if (n >= stackPtr) { + throw new SimException("stack: underflow"); + } + + return local[stackPtr - n - 1]; + } + + /** + * Peeks at the {@code n}th element down from the top of the + * stack, returning the type per se, as opposed to the + * type-bearer. This method is just a convenient shorthand + * for {@code peek(n).getType()}. + * + * @see #peek + */ + public Type peekType(int n) { + return peek(n).getType(); + } + + /** + * Pops the top element off of the stack. + * + * @return {@code non-null;} the type formerly on the top of the stack + * @throws SimException thrown if the stack is empty + */ + public TypeBearer pop() { + throwIfImmutable(); + + TypeBearer result = peek(0); + + stack[stackPtr - 1] = null; + local[stackPtr - 1] = false; + stackPtr -= result.getType().getCategory(); + + return result; + } + + /** + * Changes an element already on a stack. This method is useful in limited + * contexts, particularly when merging two instances. As such, it places + * the following restriction on its behavior: You may only replace + * values with other values of the same category. + * + * @param n {@code >= 0;} which element to change, where {@code 0} is + * the top element of the stack + * @param type {@code non-null;} type of the new value + * @throws SimException thrown if {@code n >= size()} or + * the action is otherwise prohibited + */ + public void change(int n, TypeBearer type) { + throwIfImmutable(); + + try { + type = type.getFrameType(); + } catch (NullPointerException ex) { + // Elucidate the exception. + throw new NullPointerException("type == null"); + } + + int idx = stackPtr - n - 1; + TypeBearer orig = stack[idx]; + + if ((orig == null) || + (orig.getType().getCategory() != type.getType().getCategory())) { + throwSimException("incompatible substitution: " + + stackElementString(orig) + " -> " + + stackElementString(type)); + } + + stack[idx] = type; + } + + /** + * Merges this stack with another stack. A new instance is returned if + * this merge results in a change. If no change results, this instance is + * returned. See {@link Merger#mergeStack(ExecutionStack,ExecutionStack) + * Merger.mergeStack()} + * + * @param other {@code non-null;} a stack to merge with + * @return {@code non-null;} the result of the merge + */ + public ExecutionStack merge(ExecutionStack other) { + try { + return Merger.mergeStack(this, other); + } catch (SimException ex) { + ex.addContext("underlay stack:"); + this.annotate(ex); + ex.addContext("overlay stack:"); + other.annotate(ex); + throw ex; + } + } + + /** + * Gets the string form for a stack element. This is the same as + * {@code toString()} except that {@code null} is converted + * to {@code ""}. + * + * @param type {@code null-ok;} the stack element + * @return {@code non-null;} the string form + */ + private static String stackElementString(TypeBearer type) { + if (type == null) { + return ""; + } + + return type.toString(); + } + + /** + * Throws a properly-formatted exception. + * + * @param msg {@code non-null;} useful message + * @return never (keeps compiler happy) + */ + private static TypeBearer throwSimException(String msg) { + throw new SimException("stack: " + msg); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/Frame.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/Frame.java new file mode 100644 index 00000000..bfd2c8bf --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/Frame.java @@ -0,0 +1,415 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dex.util.ExceptionWithContext; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.StdTypeList; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.util.IntList; + +/** + * Representation of a Java method execution frame. A frame consists + * of a set of locals and a value stack, and it can be told to act on + * them to load and store values between them and an "arguments / + * results" area. + */ +public final class Frame { + /** {@code non-null;} the locals */ + private final LocalsArray locals; + + /** {@code non-null;} the stack */ + private final ExecutionStack stack; + + /** {@code null-ok;} stack of labels of subroutines that this block is nested in */ + private final IntList subroutines; + + /** + * Constructs an instance. + * + * @param locals {@code non-null;} the locals array to use + * @param stack {@code non-null;} the execution stack to use + */ + private Frame(LocalsArray locals, ExecutionStack stack) { + this(locals, stack, IntList.EMPTY); + } + + /** + * Constructs an instance. + * + * @param locals {@code non-null;} the locals array to use + * @param stack {@code non-null;} the execution stack to use + * @param subroutines {@code non-null;} list of subroutine start labels for + * subroutines this frame is nested in + */ + private Frame(LocalsArray locals, + ExecutionStack stack, IntList subroutines) { + if (locals == null) { + throw new NullPointerException("locals == null"); + } + + if (stack == null) { + throw new NullPointerException("stack == null"); + } + + subroutines.throwIfMutable(); + + this.locals = locals; + this.stack = stack; + this.subroutines = subroutines; + } + + /** + * Constructs an instance. The locals array initially consists of + * all-uninitialized values (represented as {@code null}s) and + * the stack starts out empty. + * + * @param maxLocals {@code >= 0;} the maximum number of locals this instance + * can refer to + * @param maxStack {@code >= 0;} the maximum size of the stack for this + * instance + */ + public Frame(int maxLocals, int maxStack) { + this(new OneLocalsArray(maxLocals), new ExecutionStack(maxStack)); + } + + /** + * Makes and returns a mutable copy of this instance. The copy + * contains copies of the locals and stack (that is, it doesn't + * share them with the original). + * + * @return {@code non-null;} the copy + */ + public Frame copy() { + return new Frame(locals.copy(), stack.copy(), subroutines); + } + + /** + * Makes this instance immutable. + */ + public void setImmutable() { + locals.setImmutable(); + stack.setImmutable(); + // "subroutines" is always immutable + } + + /** + * Replaces all the occurrences of the given uninitialized type in + * this frame with its initialized equivalent. + * + * @param type {@code non-null;} type to replace + */ + public void makeInitialized(Type type) { + locals.makeInitialized(type); + stack.makeInitialized(type); + } + + /** + * Gets the locals array for this instance. + * + * @return {@code non-null;} the locals array + */ + public LocalsArray getLocals() { + return locals; + } + + /** + * Gets the execution stack for this instance. + * + * @return {@code non-null;} the execution stack + */ + public ExecutionStack getStack() { + return stack; + } + + /** + * Returns the largest subroutine nesting this block may be in. An + * empty list is returned if this block is not in any subroutine. + * Subroutines are identified by the label of their start block. The + * list is ordered such that the deepest nesting (the actual subroutine + * this block is in) is the last label in the list. + * + * @return {@code non-null;} list as noted above + */ + public IntList getSubroutines() { + return subroutines; + } + + /** + * Initialize this frame with the method's parameters. Used for the first + * frame. + * + * @param params Type list of method parameters. + */ + public void initializeWithParameters(StdTypeList params) { + int at = 0; + int sz = params.size(); + + for (int i = 0; i < sz; i++) { + Type one = params.get(i); + locals.set(at, one); + at += one.getCategory(); + } + } + + /** + * Returns a Frame instance representing the frame state that should + * be used when returning from a subroutine. The stack state of all + * subroutine invocations is identical, but the locals state may differ. + * + * @param startLabel {@code >=0;} The label of the returning subroutine's + * start block + * @param subLabel {@code >=0;} A calling label of a subroutine + * @return {@code null-ok;} an appropriatly-constructed instance, or null + * if label is not in the set + */ + public Frame subFrameForLabel(int startLabel, int subLabel) { + LocalsArray subLocals = null; + + if (locals instanceof LocalsArraySet) { + subLocals = ((LocalsArraySet)locals).subArrayForLabel(subLabel); + } + + IntList newSubroutines; + try { + newSubroutines = subroutines.mutableCopy(); + + if (newSubroutines.pop() != startLabel) { + throw new RuntimeException("returning from invalid subroutine"); + } + newSubroutines.setImmutable(); + } catch (IndexOutOfBoundsException ex) { + throw new RuntimeException("returning from invalid subroutine"); + } catch (NullPointerException ex) { + throw new NullPointerException("can't return from non-subroutine"); + } + + return (subLocals == null) ? null + : new Frame(subLocals, stack, newSubroutines); + } + + /** + * Merges two frames. If the merged result is the same as this frame, + * then this instance is returned. + * + * @param other {@code non-null;} another frame + * @return {@code non-null;} the result of merging the two frames + */ + public Frame mergeWith(Frame other) { + LocalsArray resultLocals; + ExecutionStack resultStack; + IntList resultSubroutines; + + resultLocals = getLocals().merge(other.getLocals()); + resultStack = getStack().merge(other.getStack()); + resultSubroutines = mergeSubroutineLists(other.subroutines); + + resultLocals = adjustLocalsForSubroutines( + resultLocals, resultSubroutines); + + if ((resultLocals == getLocals()) + && (resultStack == getStack()) + && subroutines == resultSubroutines) { + return this; + } + + return new Frame(resultLocals, resultStack, resultSubroutines); + } + + /** + * Merges this frame's subroutine lists with another. The result + * is the deepest common nesting (effectively, the common prefix of the + * two lists). + * + * @param otherSubroutines label list of subroutine start blocks, from + * least-nested to most-nested. + * @return {@code non-null;} merged subroutine nest list as described above + */ + private IntList mergeSubroutineLists(IntList otherSubroutines) { + if (subroutines.equals(otherSubroutines)) { + return subroutines; + } + + IntList resultSubroutines = new IntList(); + + int szSubroutines = subroutines.size(); + int szOthers = otherSubroutines.size(); + for (int i = 0; i < szSubroutines && i < szOthers + && (subroutines.get(i) == otherSubroutines.get(i)); i++) { + resultSubroutines.add(i); + } + + resultSubroutines.setImmutable(); + + return resultSubroutines; + } + + /** + * Adjusts a locals array to account for a merged subroutines list. + * If a frame merge results in, effectively, a subroutine return through + * a throw then the current locals will be a LocalsArraySet that will + * need to be trimmed of all OneLocalsArray elements that relevent to + * the subroutine that is returning. + * + * @param locals {@code non-null;} LocalsArray from before a merge + * @param subroutines {@code non-null;} a label list of subroutine start blocks + * representing the subroutine nesting of the block being merged into. + * @return {@code non-null;} locals set appropriate for merge + */ + private static LocalsArray adjustLocalsForSubroutines( + LocalsArray locals, IntList subroutines) { + if (! (locals instanceof LocalsArraySet)) { + // nothing to see here + return locals; + } + + LocalsArraySet laSet = (LocalsArraySet)locals; + + if (subroutines.size() == 0) { + /* + * We've merged from a subroutine context to a non-subroutine + * context, likely via a throw. Our successor will only need + * to consider the primary locals state, not the state of + * all possible subroutine paths. + */ + + return laSet.getPrimary(); + } + + /* + * It's unclear to me if the locals set needs to be trimmed here. + * If it does, then I believe it is all of the calling blocks + * in the subroutine at the end of "subroutines" passed into + * this method that should be removed. + */ + return laSet; + } + + /** + * Merges this frame with the frame of a subroutine caller at + * {@code predLabel}. Only called on the frame at the first + * block of a subroutine. + * + * @param other {@code non-null;} another frame + * @param subLabel label of subroutine start block + * @param predLabel label of calling block + * @return {@code non-null;} the result of merging the two frames + */ + public Frame mergeWithSubroutineCaller(Frame other, int subLabel, + int predLabel) { + LocalsArray resultLocals; + ExecutionStack resultStack; + + resultLocals = getLocals().mergeWithSubroutineCaller( + other.getLocals(), predLabel); + resultStack = getStack().merge(other.getStack()); + + IntList newOtherSubroutines = other.subroutines.mutableCopy(); + newOtherSubroutines.add(subLabel); + newOtherSubroutines.setImmutable(); + + if ((resultLocals == getLocals()) + && (resultStack == getStack()) + && subroutines.equals(newOtherSubroutines)) { + return this; + } + + IntList resultSubroutines; + + if (subroutines.equals(newOtherSubroutines)) { + resultSubroutines = subroutines; + } else { + /* + * The new subroutines list should be the deepest of the two + * lists being merged, but the postfix of the resultant list + * must be equal to the shorter list. + */ + IntList nonResultSubroutines; + + if (subroutines.size() > newOtherSubroutines.size()) { + resultSubroutines = subroutines; + nonResultSubroutines = newOtherSubroutines; + } else { + resultSubroutines = newOtherSubroutines; + nonResultSubroutines = subroutines; + } + + int szResult = resultSubroutines.size(); + int szNonResult = nonResultSubroutines.size(); + + for (int i = szNonResult - 1; i >=0; i-- ) { + if (nonResultSubroutines.get(i) + != resultSubroutines.get( + i + (szResult - szNonResult))) { + throw new + RuntimeException("Incompatible merged subroutines"); + } + } + + } + + return new Frame(resultLocals, resultStack, resultSubroutines); + } + + /** + * Makes a frame for a subroutine start block, given that this is the + * ending frame of one of the subroutine's calling blocks. Subroutine + * calls may be nested and thus may have nested locals state, so we + * start with an initial state as seen by the subroutine, but keep track + * of the individual locals states that will be expected when the individual + * subroutine calls return. + * + * @param subLabel label of subroutine start block + * @param callerLabel {@code >=0;} label of the caller block where this frame + * came from. + * @return a new instance to begin a called subroutine. + */ + public Frame makeNewSubroutineStartFrame(int subLabel, int callerLabel) { + IntList newSubroutines = subroutines.mutableCopy(); + newSubroutines.add(subLabel); + Frame newFrame = new Frame(locals.getPrimary(), stack, + IntList.makeImmutable(subLabel)); + return newFrame.mergeWithSubroutineCaller(this, subLabel, callerLabel); + } + + /** + * Makes a new frame for an exception handler block invoked from this + * frame. + * + * @param exceptionClass exception that the handler block will handle + * @return new frame + */ + public Frame makeExceptionHandlerStartFrame(CstType exceptionClass) { + ExecutionStack newStack = getStack().copy(); + + newStack.clear(); + newStack.push(exceptionClass); + + return new Frame(getLocals(), newStack, subroutines); + } + + /** + * Annotates (adds context to) the given exception with information + * about this frame. + * + * @param ex {@code non-null;} the exception to annotate + */ + public void annotate(ExceptionWithContext ex) { + locals.annotate(ex); + stack.annotate(ex); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/LineNumberList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/LineNumberList.java new file mode 100644 index 00000000..4a34e103 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/LineNumberList.java @@ -0,0 +1,184 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.util.FixedSizeList; + +/** + * List of "line number" entries, which are the contents of + * {@code LineNumberTable} attributes. + */ +public final class LineNumberList extends FixedSizeList { + /** {@code non-null;} zero-size instance */ + public static final LineNumberList EMPTY = new LineNumberList(0); + + /** + * Returns an instance which is the concatenation of the two given + * instances. + * + * @param list1 {@code non-null;} first instance + * @param list2 {@code non-null;} second instance + * @return {@code non-null;} combined instance + */ + public static LineNumberList concat(LineNumberList list1, + LineNumberList list2) { + if (list1 == EMPTY) { + // easy case + return list2; + } + + int sz1 = list1.size(); + int sz2 = list2.size(); + LineNumberList result = new LineNumberList(sz1 + sz2); + + for (int i = 0; i < sz1; i++) { + result.set(i, list1.get(i)); + } + + for (int i = 0; i < sz2; i++) { + result.set(sz1 + i, list2.get(i)); + } + + return result; + } + + /** + * Constructs an instance. + * + * @param count the number of elements to be in the list + */ + public LineNumberList(int count) { + super(count); + } + + /** + * Gets the indicated item. + * + * @param n {@code >= 0;} which item + * @return {@code null-ok;} the indicated item + */ + public Item get(int n) { + return (Item) get0(n); + } + + /** + * Sets the item at the given index. + * + * @param n {@code >= 0, < size();} which element + * @param item {@code non-null;} the item + */ + public void set(int n, Item item) { + if (item == null) { + throw new NullPointerException("item == null"); + } + + set0(n, item); + } + + /** + * Sets the item at the given index. + * + * @param n {@code >= 0, < size();} which element + * @param startPc {@code >= 0;} start pc of this item + * @param lineNumber {@code >= 0;} corresponding line number + */ + public void set(int n, int startPc, int lineNumber) { + set0(n, new Item(startPc, lineNumber)); + } + + /** + * Gets the line number associated with the given address. + * + * @param pc {@code >= 0;} the address to look up + * @return {@code >= -1;} the associated line number, or {@code -1} if + * none is known + */ + public int pcToLine(int pc) { + /* + * Line number entries don't have to appear in any particular + * order, so we have to do a linear search. TODO: If + * this turns out to be a bottleneck, consider sorting the + * list prior to use. + */ + int sz = size(); + int bestPc = -1; + int bestLine = -1; + + for (int i = 0; i < sz; i++) { + Item one = get(i); + int onePc = one.getStartPc(); + if ((onePc <= pc) && (onePc > bestPc)) { + bestPc = onePc; + bestLine = one.getLineNumber(); + if (bestPc == pc) { + // We can't do better than this + break; + } + } + } + + return bestLine; + } + + /** + * Item in a line number table. + */ + public static class Item { + /** {@code >= 0;} start pc of this item */ + private final int startPc; + + /** {@code >= 0;} corresponding line number */ + private final int lineNumber; + + /** + * Constructs an instance. + * + * @param startPc {@code >= 0;} start pc of this item + * @param lineNumber {@code >= 0;} corresponding line number + */ + public Item(int startPc, int lineNumber) { + if (startPc < 0) { + throw new IllegalArgumentException("startPc < 0"); + } + + if (lineNumber < 0) { + throw new IllegalArgumentException("lineNumber < 0"); + } + + this.startPc = startPc; + this.lineNumber = lineNumber; + } + + /** + * Gets the start pc of this item. + * + * @return the start pc + */ + public int getStartPc() { + return startPc; + } + + /** + * Gets the line number of this item. + * + * @return the line number + */ + public int getLineNumber() { + return lineNumber; + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalVariableList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalVariableList.java new file mode 100644 index 00000000..6473d57e --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalVariableList.java @@ -0,0 +1,373 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.rop.code.LocalItem; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.util.FixedSizeList; + +/** + * List of "local variable" entries, which are the contents of + * {@code LocalVariableTable} and {@code LocalVariableTypeTable} + * attributes, as well as combinations of the two. + */ +public final class LocalVariableList extends FixedSizeList { + /** {@code non-null;} zero-size instance */ + public static final LocalVariableList EMPTY = new LocalVariableList(0); + + /** + * Returns an instance which is the concatenation of the two given + * instances. The result is immutable. + * + * @param list1 {@code non-null;} first instance + * @param list2 {@code non-null;} second instance + * @return {@code non-null;} combined instance + */ + public static LocalVariableList concat(LocalVariableList list1, + LocalVariableList list2) { + if (list1 == EMPTY) { + // easy case + return list2; + } + + int sz1 = list1.size(); + int sz2 = list2.size(); + LocalVariableList result = new LocalVariableList(sz1 + sz2); + + for (int i = 0; i < sz1; i++) { + result.set(i, list1.get(i)); + } + + for (int i = 0; i < sz2; i++) { + result.set(sz1 + i, list2.get(i)); + } + + result.setImmutable(); + return result; + } + + /** + * Returns an instance which is the result of merging the two + * given instances, where one instance should have only type + * descriptors and the other only type signatures. The merged + * result is identical to the one with descriptors, except that + * any element whose {name, index, start, length} matches an + * element in the signature list gets augmented with the + * corresponding signature. The result is immutable. + * + * @param descriptorList {@code non-null;} list with descriptors + * @param signatureList {@code non-null;} list with signatures + * @return {@code non-null;} the merged result + */ + public static LocalVariableList mergeDescriptorsAndSignatures( + LocalVariableList descriptorList, + LocalVariableList signatureList) { + int descriptorSize = descriptorList.size(); + LocalVariableList result = new LocalVariableList(descriptorSize); + + for (int i = 0; i < descriptorSize; i++) { + Item item = descriptorList.get(i); + Item signatureItem = signatureList.itemToLocal(item); + if (signatureItem != null) { + CstString signature = signatureItem.getSignature(); + item = item.withSignature(signature); + } + result.set(i, item); + } + + result.setImmutable(); + return result; + } + + /** + * Constructs an instance. + * + * @param count the number of elements to be in the list + */ + public LocalVariableList(int count) { + super(count); + } + + /** + * Gets the indicated item. + * + * @param n {@code >= 0;} which item + * @return {@code null-ok;} the indicated item + */ + public Item get(int n) { + return (Item) get0(n); + } + + /** + * Sets the item at the given index. + * + * @param n {@code >= 0, < size();} which element + * @param item {@code non-null;} the item + */ + public void set(int n, Item item) { + if (item == null) { + throw new NullPointerException("item == null"); + } + + set0(n, item); + } + + /** + * Sets the item at the given index. + * + *

Note: At least one of {@code descriptor} or + * {@code signature} must be passed as non-null.

+ * + * @param n {@code >= 0, < size();} which element + * @param startPc {@code >= 0;} the start pc of this variable's scope + * @param length {@code >= 0;} the length (in bytecodes) of this variable's + * scope + * @param name {@code non-null;} the variable's name + * @param descriptor {@code null-ok;} the variable's type descriptor + * @param signature {@code null-ok;} the variable's type signature + * @param index {@code >= 0;} the variable's local index + */ + public void set(int n, int startPc, int length, CstString name, + CstString descriptor, CstString signature, int index) { + set0(n, new Item(startPc, length, name, descriptor, signature, index)); + } + + /** + * Gets the local variable information in this instance which matches + * the given {@link external.com.android.dx.cf.code.LocalVariableList.Item} + * in all respects but the type descriptor and signature, if any. + * + * @param item {@code non-null;} local variable information to match + * @return {@code null-ok;} the corresponding local variable information stored + * in this instance, or {@code null} if there is no matching + * information + */ + public Item itemToLocal(Item item) { + int sz = size(); + + for (int i = 0; i < sz; i++) { + Item one = (Item) get0(i); + + if ((one != null) && one.matchesAllButType(item)) { + return one; + } + } + + return null; + } + + /** + * Gets the local variable information associated with a given address + * and local index, if any. Note: In standard classfiles, a + * variable's start point is listed as the address of the instruction + * just past the one that sets the variable. + * + * @param pc {@code >= 0;} the address to look up + * @param index {@code >= 0;} the local variable index + * @return {@code null-ok;} the associated local variable information, or + * {@code null} if none is known + */ + public Item pcAndIndexToLocal(int pc, int index) { + int sz = size(); + + for (int i = 0; i < sz; i++) { + Item one = (Item) get0(i); + + if ((one != null) && one.matchesPcAndIndex(pc, index)) { + return one; + } + } + + return null; + } + + /** + * Item in a local variable table. + */ + public static class Item { + /** {@code >= 0;} the start pc of this variable's scope */ + private final int startPc; + + /** {@code >= 0;} the length (in bytecodes) of this variable's scope */ + private final int length; + + /** {@code non-null;} the variable's name */ + private final CstString name; + + /** {@code null-ok;} the variable's type descriptor */ + private final CstString descriptor; + + /** {@code null-ok;} the variable's type signature */ + private final CstString signature; + + /** {@code >= 0;} the variable's local index */ + private final int index; + + /** + * Constructs an instance. + * + *

Note: At least one of {@code descriptor} or + * {@code signature} must be passed as non-null.

+ * + * @param startPc {@code >= 0;} the start pc of this variable's scope + * @param length {@code >= 0;} the length (in bytecodes) of this variable's + * scope + * @param name {@code non-null;} the variable's name + * @param descriptor {@code null-ok;} the variable's type descriptor + * @param signature {@code null-ok;} the variable's type signature + * @param index {@code >= 0;} the variable's local index + */ + public Item(int startPc, int length, CstString name, + CstString descriptor, CstString signature, int index) { + if (startPc < 0) { + throw new IllegalArgumentException("startPc < 0"); + } + + if (length < 0) { + throw new IllegalArgumentException("length < 0"); + } + + if (name == null) { + throw new NullPointerException("name == null"); + } + + if ((descriptor == null) && (signature == null)) { + throw new NullPointerException( + "(descriptor == null) && (signature == null)"); + } + + if (index < 0) { + throw new IllegalArgumentException("index < 0"); + } + + this.startPc = startPc; + this.length = length; + this.name = name; + this.descriptor = descriptor; + this.signature = signature; + this.index = index; + } + + /** + * Gets the start pc of this variable's scope. + * + * @return {@code >= 0;} the start pc of this variable's scope + */ + public int getStartPc() { + return startPc; + } + + /** + * Gets the length (in bytecodes) of this variable's scope. + * + * @return {@code >= 0;} the length (in bytecodes) of this variable's scope + */ + public int getLength() { + return length; + } + + /** + * Gets the variable's type descriptor. + * + * @return {@code null-ok;} the variable's type descriptor + */ + public CstString getDescriptor() { + return descriptor; + } + + /** + * Gets the variable's LocalItem, a (name, signature) tuple + * + * @return {@code null-ok;} the variable's type descriptor + */ + public LocalItem getLocalItem() { + return LocalItem.make(name, signature); + } + + /** + * Gets the variable's type signature. Private because if you need this, + * you want getLocalItem() instead. + * + * @return {@code null-ok;} the variable's type signature + */ + private CstString getSignature() { + return signature; + } + + /** + * Gets the variable's local index. + * + * @return {@code >= 0;} the variable's local index + */ + public int getIndex() { + return index; + } + + /** + * Gets the variable's type descriptor. This is a convenient shorthand + * for {@code Type.intern(getDescriptor().getString())}. + * + * @return {@code non-null;} the variable's type + */ + public Type getType() { + return Type.intern(descriptor.getString()); + } + + /** + * Constructs and returns an instance which is identical to this + * one, except that the signature is changed to the given value. + * + * @param newSignature {@code non-null;} the new signature + * @return {@code non-null;} an appropriately-constructed instance + */ + public Item withSignature(CstString newSignature) { + return new Item(startPc, length, name, descriptor, newSignature, + index); + } + + /** + * Gets whether this instance matches (describes) the given + * address and index. + * + * @param pc {@code >= 0;} the address in question + * @param index {@code >= 0;} the local variable index in question + * @return {@code true} iff this instance matches {@code pc} + * and {@code index} + */ + public boolean matchesPcAndIndex(int pc, int index) { + return (index == this.index) && + (pc >= startPc) && + (pc < (startPc + length)); + } + + /** + * Gets whether this instance matches (describes) the given + * other instance exactly in all fields except type descriptor and + * type signature. + * + * @param other {@code non-null;} the instance to compare to + * @return {@code true} iff this instance matches + */ + public boolean matchesAllButType(Item other) { + return (startPc == other.startPc) + && (length == other.length) + && (index == other.index) + && name.equals(other.name); + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalsArray.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalsArray.java new file mode 100644 index 00000000..841ac3b3 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalsArray.java @@ -0,0 +1,181 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dex.util.ExceptionWithContext; +import external.com.android.dx.rop.code.RegisterSpec; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.rop.type.TypeBearer; +import external.com.android.dx.util.MutabilityControl; +import external.com.android.dx.util.ToHuman; + +/** + * Representation of an array of local variables, with Java semantics. + * + *

Note: For the most part, the documentation for this class + * ignores the distinction between {@link Type} and {@link + * TypeBearer}.

+ */ +public abstract class LocalsArray extends MutabilityControl implements ToHuman { + + /** + * Constructs an instance, explicitly indicating the mutability. + * + * @param mutable {@code true} if this instance is mutable + */ + protected LocalsArray(boolean mutable) { + super(mutable); + } + + /** + * Makes and returns a mutable copy of this instance. + * + * @return {@code non-null;} the copy + */ + public abstract LocalsArray copy(); + + /** + * Annotates (adds context to) the given exception with information + * about this instance. + * + * @param ex {@code non-null;} the exception to annotate + */ + public abstract void annotate(ExceptionWithContext ex); + + /** + * Replaces all the occurrences of the given uninitialized type in + * this array with its initialized equivalent. + * + * @param type {@code non-null;} type to replace + */ + public abstract void makeInitialized(Type type); + + /** + * Gets the maximum number of locals this instance can refer to. + * + * @return the max locals + */ + public abstract int getMaxLocals(); + + /** + * Sets the type stored at the given local index. If the given type + * is category-2, then (a) the index must be at least two less than + * {@link #getMaxLocals} and (b) the next index gets invalidated + * by the operation. In case of either category, if the previous + * local contains a category-2 value, then it too is invalidated by + * this operation. + * + * @param idx {@code >= 0, < getMaxLocals();} which local + * @param type {@code non-null;} new type for the local at {@code idx} + */ + public abstract void set(int idx, TypeBearer type); + + /** + * Sets the type for the local indicated by the given register spec + * to that register spec (which includes type and optional name + * information). This is identical to calling + * {@code set(spec.getReg(), spec)}. + * + * @param spec {@code non-null;} register spec to use as the basis for the update + */ + public abstract void set(RegisterSpec spec); + + /** + * Invalidates the local at the given index. + * + * @param idx {@code >= 0, < getMaxLocals();} which local + */ + public abstract void invalidate(int idx); + + /** + * Gets the type stored at the given local index, or {@code null} + * if the given local is uninitialized / invalid. + * + * @param idx {@code >= 0, < getMaxLocals();} which local + * @return {@code null-ok;} the type of value stored in that local + */ + public abstract TypeBearer getOrNull(int idx); + + /** + * Gets the type stored at the given local index, only succeeding if + * the given local contains a valid type (though it is allowed to + * be an uninitialized instance). + * + * @param idx {@code >= 0, < getMaxLocals();} which local + * @return {@code non-null;} the type of value stored in that local + * @throws SimException thrown if {@code idx} is valid, but + * the contents are invalid + */ + public abstract TypeBearer get(int idx); + + /** + * Gets the type stored at the given local index, which is expected + * to be an initialized category-1 value. + * + * @param idx {@code >= 0, < getMaxLocals();} which local + * @return {@code non-null;} the type of value stored in that local + * @throws SimException thrown if {@code idx} is valid, but + * one of the following holds: (a) the local is invalid; (b) the local + * contains an uninitialized instance; (c) the local contains a + * category-2 value + */ + public abstract TypeBearer getCategory1(int idx); + + /** + * Gets the type stored at the given local index, which is expected + * to be a category-2 value. + * + * @param idx {@code >= 0, < getMaxLocals();} which local + * @return {@code non-null;} the type of value stored in that local + * @throws SimException thrown if {@code idx} is valid, but + * one of the following holds: (a) the local is invalid; (b) the local + * contains a category-1 value + */ + public abstract TypeBearer getCategory2(int idx); + + /** + * Merges this instance with {@code other}. If the merged result is + * the same as this instance, then this is returned (not a copy). + * + * @param other {@code non-null;} another LocalsArray + * @return {@code non-null;} the merge result, a new instance or this + */ + public abstract LocalsArray merge(LocalsArray other); + + /** + * Merges this instance with a {@code LocalsSet} from a subroutine + * caller. To be used when merging in the first block of a subroutine. + * + * @param other {@code other non-null;} another LocalsArray. The final locals + * state of a subroutine caller. + * @param predLabel the label of the subroutine caller block. + * @return {@code non-null;} the merge result, a new instance or this + */ + public abstract LocalsArraySet mergeWithSubroutineCaller + (LocalsArray other, int predLabel); + + /** + * Gets the locals set appropriate for the current execution context. + * That is, if this is a {@code OneLocalsArray} instance, then return + * {@code this}, otherwise return {@code LocalsArraySet}'s + * primary. + * + * @return locals for this execution context. + */ + protected abstract OneLocalsArray getPrimary(); + +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalsArraySet.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalsArraySet.java new file mode 100644 index 00000000..80d2fc1c --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/LocalsArraySet.java @@ -0,0 +1,461 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dex.util.ExceptionWithContext; +import external.com.android.dx.rop.code.RegisterSpec; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.rop.type.TypeBearer; +import external.com.android.dx.util.Hex; +import java.util.ArrayList; + +/** + * Representation of a set of local variable arrays, with Java semantics. + * This peculiar case is to support in-method subroutines, which can + * have different locals sets for each caller. + * + *

Note: For the most part, the documentation for this class + * ignores the distinction between {@link external.com.android.dx.rop.type.Type} and {@link + * external.com.android.dx.rop.type.TypeBearer}.

+ */ +public class LocalsArraySet extends LocalsArray { + + /** + * The primary LocalsArray represents the locals as seen from + * the subroutine itself, which is the merged representation of all the + * individual locals states. + */ + private final OneLocalsArray primary; + + /** + * Indexed by label of caller block: the locals specific to each caller's + * invocation of the subroutine. + */ + private final ArrayList secondaries; + + /** + * Constructs an instance. The locals array initially consists of + * all-uninitialized values (represented as {@code null}s). + * + * @param maxLocals {@code >= 0;} the maximum number of locals this instance + * can refer to + */ + public LocalsArraySet(int maxLocals) { + super(maxLocals != 0); + primary = new OneLocalsArray(maxLocals); + secondaries = new ArrayList(); + } + + /** + * Constructs an instance with the specified primary and secondaries set. + * + * @param primary {@code non-null;} primary locals to use + * @param secondaries {@code non-null;} secondaries set, indexed by subroutine + * caller label. + */ + public LocalsArraySet(OneLocalsArray primary, + ArrayList secondaries) { + super(primary.getMaxLocals() > 0); + + this.primary = primary; + this.secondaries = secondaries; + } + + /** + * Constructs an instance which is a copy of another. + * + * @param toCopy {@code non-null;} instance to copy. + */ + private LocalsArraySet(LocalsArraySet toCopy) { + super(toCopy.getMaxLocals() > 0); + + primary = toCopy.primary.copy(); + secondaries = new ArrayList(toCopy.secondaries.size()); + + int sz = toCopy.secondaries.size(); + for (int i = 0; i < sz; i++) { + LocalsArray la = toCopy.secondaries.get(i); + + if (la == null) { + secondaries.add(null); + } else { + secondaries.add(la.copy()); + } + } + } + + + /** {@inheritDoc} */ + @Override + public void setImmutable() { + primary.setImmutable(); + + for (LocalsArray la : secondaries) { + if (la != null) { + la.setImmutable(); + } + } + super.setImmutable(); + } + + /** {@inheritDoc} */ + @Override + public LocalsArray copy() { + return new LocalsArraySet(this); + } + + /** {@inheritDoc} */ + @Override + public void annotate(ExceptionWithContext ex) { + ex.addContext("(locals array set; primary)"); + primary.annotate(ex); + + int sz = secondaries.size(); + for (int label = 0; label < sz; label++) { + LocalsArray la = secondaries.get(label); + + if (la != null) { + ex.addContext("(locals array set: primary for caller " + + Hex.u2(label) + ')'); + + la.getPrimary().annotate(ex); + } + } + } + + /** {@inheritDoc} */ + @Override + public String toHuman() { + StringBuilder sb = new StringBuilder(); + + sb.append("(locals array set; primary)\n"); + + sb.append(getPrimary().toHuman()); + sb.append('\n'); + + int sz = secondaries.size(); + for (int label = 0; label < sz; label++) { + LocalsArray la = secondaries.get(label); + + if (la != null) { + sb.append("(locals array set: primary for caller " + + Hex.u2(label) + ")\n"); + + sb.append(la.getPrimary().toHuman()); + sb.append('\n'); + } + } + + return sb.toString(); + } + + /** {@inheritDoc} */ + @Override + public void makeInitialized(Type type) { + int len = primary.getMaxLocals(); + + if (len == 0) { + // We have to check for this before checking for immutability. + return; + } + + throwIfImmutable(); + + primary.makeInitialized(type); + + for (LocalsArray la : secondaries) { + if (la != null) { + la.makeInitialized(type); + } + } + } + + /** {@inheritDoc} */ + @Override + public int getMaxLocals() { + return primary.getMaxLocals(); + } + + /** {@inheritDoc} */ + @Override + public void set(int idx, TypeBearer type) { + throwIfImmutable(); + + primary.set(idx, type); + + for (LocalsArray la : secondaries) { + if (la != null) { + la.set(idx, type); + } + } + } + + /** {@inheritDoc} */ + @Override + public void set(RegisterSpec spec) { + set(spec.getReg(), spec); + } + + /** {@inheritDoc} */ + @Override + public void invalidate(int idx) { + throwIfImmutable(); + + primary.invalidate(idx); + + for (LocalsArray la : secondaries) { + if (la != null) { + la.invalidate(idx); + } + } + } + + /** {@inheritDoc} */ + @Override + public TypeBearer getOrNull(int idx) { + return primary.getOrNull(idx); + } + + /** {@inheritDoc} */ + @Override + public TypeBearer get(int idx) { + return primary.get(idx); + } + + /** {@inheritDoc} */ + @Override + public TypeBearer getCategory1(int idx) { + return primary.getCategory1(idx); + } + + /** {@inheritDoc} */ + @Override + public TypeBearer getCategory2(int idx) { + return primary.getCategory2(idx); + } + + /** + * Merges this set with another {@code LocalsArraySet} instance. + * + * @param other {@code non-null;} to merge + * @return {@code non-null;} this instance if merge was a no-op, or + * new merged instance. + */ + private LocalsArraySet mergeWithSet(LocalsArraySet other) { + OneLocalsArray newPrimary; + ArrayList newSecondaries; + boolean secondariesChanged = false; + + newPrimary = primary.merge(other.getPrimary()); + + int sz1 = secondaries.size(); + int sz2 = other.secondaries.size(); + int sz = Math.max(sz1, sz2); + newSecondaries = new ArrayList(sz); + + for (int i = 0; i < sz; i++) { + LocalsArray la1 = (i < sz1 ? secondaries.get(i) : null); + LocalsArray la2 = (i < sz2 ? other.secondaries.get(i) : null); + LocalsArray resultla = null; + + if (la1 == la2) { + resultla = la1; + } else if (la1 == null) { + resultla = la2; + } else if (la2 == null) { + resultla = la1; + } else { + try { + resultla = la1.merge(la2); + } catch (SimException ex) { + ex.addContext( + "Merging locals set for caller block " + Hex.u2(i)); + } + } + + secondariesChanged = secondariesChanged || (la1 != resultla); + + newSecondaries.add(resultla); + } + + if ((primary == newPrimary) && ! secondariesChanged ) { + return this; + } + + return new LocalsArraySet(newPrimary, newSecondaries); + } + + /** + * Merges this set with a {@code OneLocalsArray} instance. + * + * @param other {@code non-null;} to merge + * @return {@code non-null;} this instance if merge was a no-op, or + * new merged instance. + */ + private LocalsArraySet mergeWithOne(OneLocalsArray other) { + OneLocalsArray newPrimary; + ArrayList newSecondaries; + boolean secondariesChanged = false; + + newPrimary = primary.merge(other.getPrimary()); + newSecondaries = new ArrayList(secondaries.size()); + + int sz = secondaries.size(); + for (int i = 0; i < sz; i++) { + LocalsArray la = secondaries.get(i); + LocalsArray resultla = null; + + if (la != null) { + try { + resultla = la.merge(other); + } catch (SimException ex) { + ex.addContext("Merging one locals against caller block " + + Hex.u2(i)); + } + } + + secondariesChanged = secondariesChanged || (la != resultla); + + newSecondaries.add(resultla); + } + + if ((primary == newPrimary) && ! secondariesChanged ) { + return this; + } + + return new LocalsArraySet(newPrimary, newSecondaries); + } + + /** {@inheritDoc} */ + @Override + public LocalsArraySet merge(LocalsArray other) { + LocalsArraySet result; + + try { + if (other instanceof LocalsArraySet) { + result = mergeWithSet((LocalsArraySet) other); + } else { + result = mergeWithOne((OneLocalsArray) other); + } + } catch (SimException ex) { + ex.addContext("underlay locals:"); + annotate(ex); + ex.addContext("overlay locals:"); + other.annotate(ex); + throw ex; + } + + result.setImmutable(); + return result; + } + + /** + * Gets the {@code LocalsArray} instance for a specified subroutine + * caller label, or null if label has no locals associated with it. + * + * @param label {@code >= 0;} subroutine caller label + * @return {@code null-ok;} locals if available. + */ + private LocalsArray getSecondaryForLabel(int label) { + if (label >= secondaries.size()) { + return null; + } + + return secondaries.get(label); + } + + /** {@inheritDoc} */ + @Override + public LocalsArraySet mergeWithSubroutineCaller + (LocalsArray other, int predLabel) { + + LocalsArray mine = getSecondaryForLabel(predLabel); + LocalsArray newSecondary; + OneLocalsArray newPrimary; + + newPrimary = primary.merge(other.getPrimary()); + + if (mine == other) { + newSecondary = mine; + } else if (mine == null) { + newSecondary = other; + } else { + newSecondary = mine.merge(other); + } + + if ((newSecondary == mine) && (newPrimary == primary)) { + return this; + } else { + /* + * We're going to re-build a primary as a merge of all the + * secondaries. + */ + newPrimary = null; + + int szSecondaries = secondaries.size(); + int sz = Math.max(predLabel + 1, szSecondaries); + ArrayList newSecondaries = new ArrayList(sz); + for (int i = 0; i < sz; i++) { + LocalsArray la = null; + + if (i == predLabel) { + /* + * This LocalsArray always replaces any existing one, + * since this is the result of a refined iteration. + */ + la = newSecondary; + } else if (i < szSecondaries) { + la = secondaries.get(i); + } + + if (la != null) { + if (newPrimary == null) { + newPrimary = la.getPrimary(); + } else { + newPrimary = newPrimary.merge(la.getPrimary()); + } + } + + newSecondaries.add(la); + } + + LocalsArraySet result + = new LocalsArraySet(newPrimary, newSecondaries); + result.setImmutable(); + return result; + } + } + + /** + * Returns a LocalsArray instance representing the locals state that should + * be used when returning to a subroutine caller. + * + * @param subLabel {@code >= 0;} A calling label of a subroutine + * @return {@code null-ok;} an instance for this subroutine, or null if subroutine + * is not in this set. + */ + public LocalsArray subArrayForLabel(int subLabel) { + LocalsArray result = getSecondaryForLabel(subLabel); + return result; + } + + /**{@inheritDoc}*/ + @Override + protected OneLocalsArray getPrimary() { + return primary; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/Machine.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/Machine.java new file mode 100644 index 00000000..12726138 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/Machine.java @@ -0,0 +1,216 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.rop.code.LocalItem; +import external.com.android.dx.rop.cst.Constant; +import external.com.android.dx.rop.type.Prototype; +import external.com.android.dx.rop.type.Type; +import java.util.ArrayList; + +/** + * Interface for machines capable of executing bytecode by acting + * upon a {@link Frame}. A machine conceptually contains four arbitrary-value + * argument slots, slots for several literal-value arguments, and slots for + * branch target information. + */ +public interface Machine { + /** + * Gets the effective prototype of the method that this instance is + * being used for. The effective prototype includes an initial + * {@code this} argument for instance methods. + * + * @return {@code non-null;} the method prototype + */ + public Prototype getPrototype(); + + /** + * Clears the regular and auxiliary arguments area. + */ + public void clearArgs(); + + /** + * Pops the given number of values from the stack (of either category), + * and store them in the arguments area, indicating that there are now + * that many arguments. Also, clear the auxiliary arguments. + * + * @param frame {@code non-null;} frame to operate on + * @param count {@code >= 0;} number of values to pop + */ + public void popArgs(Frame frame, int count); + + /** + * Pops values from the stack of the types indicated by the given + * {@code Prototype} (popped in reverse of the argument + * order, so the first prototype argument type is for the deepest + * element of the stack), and store them in the arguments area, + * indicating that there are now that many arguments. Also, clear + * the auxiliary arguments. + * + * @param frame {@code non-null;} frame to operate on + * @param prototype {@code non-null;} prototype indicating arguments to pop + */ + public void popArgs(Frame frame, Prototype prototype); + + /** + * Pops a value from the stack of the indicated type, and store it + * in the arguments area, indicating that there are now that many + * arguments. Also, clear the auxiliary arguments. + * + * @param frame {@code non-null;} frame to operate on + * @param type {@code non-null;} type of the argument + */ + public void popArgs(Frame frame, Type type); + + /** + * Pops values from the stack of the indicated types (popped in + * reverse argument order, so the first indicated type is for the + * deepest element of the stack), and store them in the arguments + * area, indicating that there are now that many arguments. Also, + * clear the auxiliary arguments. + * + * @param frame {@code non-null;} frame to operate on + * @param type1 {@code non-null;} type of the first argument + * @param type2 {@code non-null;} type of the second argument + */ + public void popArgs(Frame frame, Type type1, Type type2); + + /** + * Pops values from the stack of the indicated types (popped in + * reverse argument order, so the first indicated type is for the + * deepest element of the stack), and store them in the arguments + * area, indicating that there are now that many arguments. Also, + * clear the auxiliary arguments. + * + * @param frame {@code non-null;} frame to operate on + * @param type1 {@code non-null;} type of the first argument + * @param type2 {@code non-null;} type of the second argument + * @param type3 {@code non-null;} type of the third argument + */ + public void popArgs(Frame frame, Type type1, Type type2, Type type3); + + /** + * Loads the local variable with the given index as the sole argument in + * the arguments area. Also, clear the auxiliary arguments. + * + * @param frame {@code non-null;} frame to operate on + * @param idx {@code >= 0;} the local variable index + */ + public void localArg(Frame frame, int idx); + + /** + * Used to specify if a loaded local variable has info in the local + * variable table. + * + * @param local {@code true} if local arg has info in local variable table + */ + public void localInfo(boolean local); + + /** + * Indicates that the salient type of this operation is as + * given. This differentiates between, for example, the various + * arithmetic opcodes, which, by the time they hit a + * {@code Machine} are collapsed to the {@code int} + * variant. (See {@link BytecodeArray#parseInstruction} for + * details.) + * + * @param type {@code non-null;} the salient type of the upcoming operation + */ + public void auxType(Type type); + + /** + * Indicates that there is an auxiliary (inline, not stack) + * argument of type {@code int}, with the given value. + * + *

Note: Perhaps unintuitively, the stack manipulation + * ops (e.g., {@code dup} and {@code swap}) use this to + * indicate the result stack pattern with a straightforward hex + * encoding of the push order starting with least-significant + * nibbles getting pushed first). For example, an all-category-1 + * {@code dup2_x1} sets this to {@code 0x12312}, and the + * other form of that op sets this to + * {@code 0x121}.

+ * + *

Also Note: For {@code switch*} instructions, this is + * used to indicate the padding value (which is only useful for + * verification).

+ * + * @param value the argument value + */ + public void auxIntArg(int value); + + /** + * Indicates that there is an auxiliary (inline, not stack) object + * argument, with the value based on the given constant. + * + *

Note: Some opcodes use both {@code int} and + * constant auxiliary arguments.

+ * + * @param cst {@code non-null;} the constant containing / referencing + * the value + */ + public void auxCstArg(Constant cst); + + /** + * Indicates that there is an auxiliary (inline, not stack) argument + * indicating a branch target. + * + * @param target the argument value + */ + public void auxTargetArg(int target); + + /** + * Indicates that there is an auxiliary (inline, not stack) argument + * consisting of a {@code switch*} table. + * + *

Note: This is generally used in conjunction with + * {@link #auxIntArg} (which holds the padding).

+ * + * @param cases {@code non-null;} the list of key-target pairs, plus the default + * target + */ + public void auxSwitchArg(SwitchList cases); + + /** + * Indicates that there is an auxiliary (inline, not stack) argument + * consisting of a list of initial values for a newly created array. + * + * @param initValues {@code non-null;} the list of constant values to initialize + * the array + */ + public void auxInitValues(ArrayList initValues); + + /** + * Indicates that the target of this operation is the given local. + * + * @param idx {@code >= 0;} the local variable index + * @param type {@code non-null;} the type of the local + * @param local {@code null-ok;} the name and signature of the local, if known + */ + public void localTarget(int idx, Type type, LocalItem local); + + /** + * "Runs" the indicated opcode in an appropriate way, using the arguments + * area as appropriate, and modifying the given frame in response. + * + * @param frame {@code non-null;} frame to operate on + * @param offset {@code >= 0;} byte offset in the method to the opcode being + * run + * @param opcode {@code >= 0;} the opcode to run + */ + public void run(Frame frame, int offset, int opcode); +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/Merger.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/Merger.java new file mode 100644 index 00000000..73bb54ba --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/Merger.java @@ -0,0 +1,305 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.rop.type.TypeBearer; +import external.com.android.dx.util.Hex; + +/** + * Utility methods to merge various frame information. + */ +public final class Merger { + /** + * This class is uninstantiable. + */ + private Merger() { + // This space intentionally left blank. + } + + /** + * Merges two locals arrays. If the merged result is the same as the first + * argument, then return the first argument (not a copy). + * + * @param locals1 {@code non-null;} a locals array + * @param locals2 {@code non-null;} another locals array + * @return {@code non-null;} the result of merging the two locals arrays + */ + public static OneLocalsArray mergeLocals(OneLocalsArray locals1, + OneLocalsArray locals2) { + if (locals1 == locals2) { + // Easy out. + return locals1; + } + + int sz = locals1.getMaxLocals(); + OneLocalsArray result = null; + + if (locals2.getMaxLocals() != sz) { + throw new SimException("mismatched maxLocals values"); + } + + for (int i = 0; i < sz; i++) { + TypeBearer tb1 = locals1.getOrNull(i); + TypeBearer tb2 = locals2.getOrNull(i); + TypeBearer resultType = mergeType(tb1, tb2); + if (resultType != tb1) { + /* + * We only need to do anything when the result differs + * from what is in the first array, since that's what the + * result gets initialized to. + */ + if (result == null) { + result = locals1.copy(); + } + + if (resultType == null) { + result.invalidate(i); + } else { + result.set(i, resultType); + } + } + } + + if (result == null) { + return locals1; + } + + result.setImmutable(); + return result; + } + + /** + * Merges two stacks. If the merged result is the same as the first + * argument, then return the first argument (not a copy). + * + * @param stack1 {@code non-null;} a stack + * @param stack2 {@code non-null;} another stack + * @return {@code non-null;} the result of merging the two stacks + */ + public static ExecutionStack mergeStack(ExecutionStack stack1, + ExecutionStack stack2) { + if (stack1 == stack2) { + // Easy out. + return stack1; + } + + int sz = stack1.size(); + ExecutionStack result = null; + + if (stack2.size() != sz) { + throw new SimException("mismatched stack depths"); + } + + for (int i = 0; i < sz; i++) { + TypeBearer tb1 = stack1.peek(i); + TypeBearer tb2 = stack2.peek(i); + TypeBearer resultType = mergeType(tb1, tb2); + if (resultType != tb1) { + /* + * We only need to do anything when the result differs + * from what is in the first stack, since that's what the + * result gets initialized to. + */ + if (result == null) { + result = stack1.copy(); + } + + try { + if (resultType == null) { + throw new SimException("incompatible: " + tb1 + ", " + + tb2); + } else { + result.change(i, resultType); + } + } catch (SimException ex) { + ex.addContext("...while merging stack[" + Hex.u2(i) + "]"); + throw ex; + } + } + } + + if (result == null) { + return stack1; + } + + result.setImmutable(); + return result; + } + + /** + * Merges two frame types. + * + * @param ft1 {@code non-null;} a frame type + * @param ft2 {@code non-null;} another frame type + * @return {@code non-null;} the result of merging the two types + */ + public static TypeBearer mergeType(TypeBearer ft1, TypeBearer ft2) { + if ((ft1 == null) || ft1.equals(ft2)) { + return ft1; + } else if (ft2 == null) { + return null; + } else { + Type type1 = ft1.getType(); + Type type2 = ft2.getType(); + + if (type1 == type2) { + return type1; + } else if (type1.isReference() && type2.isReference()) { + if (type1 == Type.KNOWN_NULL) { + /* + * A known-null merges with any other reference type to + * be that reference type. + */ + return type2; + } else if (type2 == Type.KNOWN_NULL) { + /* + * The same as above, but this time it's type2 that's + * the known-null. + */ + return type1; + } else if (type1.isArray() && type2.isArray()) { + TypeBearer componentUnion = + mergeType(type1.getComponentType(), + type2.getComponentType()); + if (componentUnion == null) { + /* + * At least one of the types is a primitive type, + * so the merged result is just Object. + */ + return Type.OBJECT; + } + return ((Type) componentUnion).getArrayType(); + } else { + /* + * All other unequal reference types get merged to be + * Object in this phase. This is fine here, but it + * won't be the right thing to do in the verifier. + */ + return Type.OBJECT; + } + } else if (type1.isIntlike() && type2.isIntlike()) { + /* + * Merging two non-identical int-like types results in + * the type int. + */ + return Type.INT; + } else { + return null; + } + } + } + + /** + * Returns whether the given supertype is possibly assignable from + * the given subtype. This takes into account primitiveness, + * int-likeness, known-nullness, and array dimensions, but does + * not assume anything about class hierarchy other than that the + * type {@code Object} is the supertype of all reference + * types and all arrays are assignable to + * {@code Serializable} and {@code Cloneable}. + * + * @param supertypeBearer {@code non-null;} the supertype + * @param subtypeBearer {@code non-null;} the subtype + */ + public static boolean isPossiblyAssignableFrom(TypeBearer supertypeBearer, + TypeBearer subtypeBearer) { + Type supertype = supertypeBearer.getType(); + Type subtype = subtypeBearer.getType(); + + if (supertype.equals(subtype)) { + // Easy out. + return true; + } + + int superBt = supertype.getBasicType(); + int subBt = subtype.getBasicType(); + + // Treat return types as Object for the purposes of this method. + + if (superBt == Type.BT_ADDR) { + supertype = Type.OBJECT; + superBt = Type.BT_OBJECT; + } + + if (subBt == Type.BT_ADDR) { + subtype = Type.OBJECT; + subBt = Type.BT_OBJECT; + } + + if ((superBt != Type.BT_OBJECT) || (subBt != Type.BT_OBJECT)) { + /* + * No two distinct primitive types are assignable in this sense, + * unless they are both int-like. + */ + return supertype.isIntlike() && subtype.isIntlike(); + } + + // At this point, we know both types are reference types. + + if (supertype == Type.KNOWN_NULL) { + /* + * A known-null supertype is only assignable from another + * known-null (handled in the easy out at the top of the + * method). + */ + return false; + } else if (subtype == Type.KNOWN_NULL) { + /* + * A known-null subtype is in fact assignable to any + * reference type. + */ + return true; + } else if (supertype == Type.OBJECT) { + /* + * Object is assignable from any reference type. + */ + return true; + } else if (supertype.isArray()) { + // The supertype is an array type. + if (! subtype.isArray()) { + // The subtype isn't an array, and so can't be assignable. + return false; + } + + /* + * Strip off as many matched component types from both + * types as possible, and check the assignability of the + * results. + */ + do { + supertype = supertype.getComponentType(); + subtype = subtype.getComponentType(); + } while (supertype.isArray() && subtype.isArray()); + + return isPossiblyAssignableFrom(supertype, subtype); + } else if (subtype.isArray()) { + /* + * Other than Object (handled above), array types are + * assignable only to Serializable and Cloneable. + */ + return (supertype == Type.SERIALIZABLE) || + (supertype == Type.CLONEABLE); + } else { + /* + * All other unequal reference types are considered at + * least possibly assignable. + */ + return true; + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/OneLocalsArray.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/OneLocalsArray.java new file mode 100644 index 00000000..978a44d7 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/OneLocalsArray.java @@ -0,0 +1,257 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dex.util.ExceptionWithContext; +import external.com.android.dx.rop.code.RegisterSpec; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.rop.type.TypeBearer; +import external.com.android.dx.util.Hex; + +/** + * Representation of an array of local variables, with Java semantics. + * + *

Note: For the most part, the documentation for this class + * ignores the distinction between {@link external.com.android.dx.rop.type.Type} and {@link + * external.com.android.dx.rop.type.TypeBearer}.

+ */ +public class OneLocalsArray extends LocalsArray { + /** {@code non-null;} actual array */ + private final TypeBearer[] locals; + + /** + * Constructs an instance. The locals array initially consists of + * all-uninitialized values (represented as {@code null}s). + * + * @param maxLocals {@code >= 0;} the maximum number of locals this instance + * can refer to + */ + public OneLocalsArray(int maxLocals) { + super(maxLocals != 0); + locals = new TypeBearer[maxLocals]; + } + + /** {@inheritDoc} */ + @Override + public OneLocalsArray copy() { + OneLocalsArray result = new OneLocalsArray(locals.length); + + System.arraycopy(locals, 0, result.locals, 0, locals.length); + + return result; + } + + /** {@inheritDoc} */ + @Override + public void annotate(ExceptionWithContext ex) { + for (int i = 0; i < locals.length; i++) { + TypeBearer type = locals[i]; + String s = (type == null) ? "" : type.toString(); + ex.addContext("locals[" + Hex.u2(i) + "]: " + s); + } + } + + /** {@inheritDoc} */ + @Override + public String toHuman() { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < locals.length; i++) { + TypeBearer type = locals[i]; + String s = (type == null) ? "" : type.toString(); + sb.append("locals[" + Hex.u2(i) + "]: " + s + "\n"); + } + + return sb.toString(); + } + + /** {@inheritDoc} */ + @Override + public void makeInitialized(Type type) { + int len = locals.length; + + if (len == 0) { + // We have to check for this before checking for immutability. + return; + } + + throwIfImmutable(); + + Type initializedType = type.getInitializedType(); + + for (int i = 0; i < len; i++) { + if (locals[i] == type) { + locals[i] = initializedType; + } + } + } + + /** {@inheritDoc} */ + @Override + public int getMaxLocals() { + return locals.length; + } + + /** {@inheritDoc} */ + @Override + public void set(int idx, TypeBearer type) { + throwIfImmutable(); + + try { + type = type.getFrameType(); + } catch (NullPointerException ex) { + // Elucidate the exception + throw new NullPointerException("type == null"); + } + + if (idx < 0) { + throw new IndexOutOfBoundsException("idx < 0"); + } + + // Make highest possible out-of-bounds check happen first. + if (type.getType().isCategory2()) { + locals[idx + 1] = null; + } + + locals[idx] = type; + + if (idx != 0) { + TypeBearer prev = locals[idx - 1]; + if ((prev != null) && prev.getType().isCategory2()) { + locals[idx - 1] = null; + } + } + } + + /** {@inheritDoc} */ + @Override + public void set(RegisterSpec spec) { + set(spec.getReg(), spec); + } + + /** {@inheritDoc} */ + @Override + public void invalidate(int idx) { + throwIfImmutable(); + locals[idx] = null; + } + + /** {@inheritDoc} */ + @Override + public TypeBearer getOrNull(int idx) { + return locals[idx]; + } + + /** {@inheritDoc} */ + @Override + public TypeBearer get(int idx) { + TypeBearer result = locals[idx]; + + if (result == null) { + return throwSimException(idx, "invalid"); + } + + return result; + } + + /** {@inheritDoc} */ + @Override + public TypeBearer getCategory1(int idx) { + TypeBearer result = get(idx); + Type type = result.getType(); + + if (type.isUninitialized()) { + return throwSimException(idx, "uninitialized instance"); + } + + if (type.isCategory2()) { + return throwSimException(idx, "category-2"); + } + + return result; + } + + /** {@inheritDoc} */ + @Override + public TypeBearer getCategory2(int idx) { + TypeBearer result = get(idx); + + if (result.getType().isCategory1()) { + return throwSimException(idx, "category-1"); + } + + return result; + } + + /** {@inheritDoc} */ + @Override + public LocalsArray merge(LocalsArray other) { + if (other instanceof OneLocalsArray) { + return merge((OneLocalsArray)other); + } else { //LocalsArraySet + // LocalsArraySet knows how to merge me. + return other.merge(this); + } + } + + /** + * Merges this OneLocalsArray instance with another OneLocalsArray + * instance. A more-refined version of {@link #merge(LocalsArray) merge} + * which is called by that method when appropriate. + * + * @param other locals array with which to merge + * @return this instance if merge was a no-op, or a new instance if + * the merge resulted in a change. + */ + public OneLocalsArray merge(OneLocalsArray other) { + try { + return Merger.mergeLocals(this, other); + } catch (SimException ex) { + ex.addContext("underlay locals:"); + annotate(ex); + ex.addContext("overlay locals:"); + other.annotate(ex); + throw ex; + } + } + + /** {@inheritDoc} */ + @Override + public LocalsArraySet mergeWithSubroutineCaller + (LocalsArray other, int predLabel) { + + LocalsArraySet result = new LocalsArraySet(getMaxLocals()); + return result.mergeWithSubroutineCaller(other, predLabel); + } + + /**{@inheritDoc}*/ + @Override + protected OneLocalsArray getPrimary() { + return this; + } + + /** + * Throws a properly-formatted exception. + * + * @param idx the salient local index + * @param msg {@code non-null;} useful message + * @return never (keeps compiler happy) + */ + private static TypeBearer throwSimException(int idx, String msg) { + throw new SimException("local " + Hex.u2(idx) + ": " + msg); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/ReturnAddress.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ReturnAddress.java new file mode 100644 index 00000000..9a951f5b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ReturnAddress.java @@ -0,0 +1,114 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.rop.type.TypeBearer; +import external.com.android.dx.util.Hex; + +/** + * Representation of a subroutine return address. In Java verification, + * somewhat counterintuitively, the salient bit of information you need to + * know about a return address is the start address of the subroutine + * being returned from, not the address being returned to, so that's + * what instances of this class hang onto. + */ +public final class ReturnAddress implements TypeBearer { + /** {@code >= 0;} the start address of the subroutine being returned from */ + private final int subroutineAddress; + + /** + * Constructs an instance. + * + * @param subroutineAddress {@code >= 0;} the start address of the + * subroutine being returned from + */ + public ReturnAddress(int subroutineAddress) { + if (subroutineAddress < 0) { + throw new IllegalArgumentException("subroutineAddress < 0"); + } + + this.subroutineAddress = subroutineAddress; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return (""); + } + + /** {@inheritDoc} */ + @Override + public String toHuman() { + return toString(); + } + + /** {@inheritDoc} */ + @Override + public Type getType() { + return Type.RETURN_ADDRESS; + } + + /** {@inheritDoc} */ + @Override + public TypeBearer getFrameType() { + return this; + } + + /** {@inheritDoc} */ + @Override + public int getBasicType() { + return Type.RETURN_ADDRESS.getBasicType(); + } + + /** {@inheritDoc} */ + @Override + public int getBasicFrameType() { + return Type.RETURN_ADDRESS.getBasicFrameType(); + } + + /** {@inheritDoc} */ + @Override + public boolean isConstant() { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object other) { + if (!(other instanceof ReturnAddress)) { + return false; + } + + return subroutineAddress == ((ReturnAddress) other).subroutineAddress; + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return subroutineAddress; + } + + /** + * Gets the subroutine address. + * + * @return {@code >= 0;} the subroutine address + */ + public int getSubroutineAddress() { + return subroutineAddress; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/Ropper.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/Ropper.java new file mode 100644 index 00000000..67cbd6ea --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/Ropper.java @@ -0,0 +1,1801 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.cf.iface.MethodList; +import external.com.android.dx.dex.DexOptions; +import external.com.android.dx.rop.code.AccessFlags; +import external.com.android.dx.rop.code.BasicBlock; +import external.com.android.dx.rop.code.BasicBlockList; +import external.com.android.dx.rop.code.Insn; +import external.com.android.dx.rop.code.InsnList; +import external.com.android.dx.rop.code.PlainCstInsn; +import external.com.android.dx.rop.code.PlainInsn; +import external.com.android.dx.rop.code.RegisterSpec; +import external.com.android.dx.rop.code.RegisterSpecList; +import external.com.android.dx.rop.code.Rop; +import external.com.android.dx.rop.code.RopMethod; +import external.com.android.dx.rop.code.Rops; +import external.com.android.dx.rop.code.SourcePosition; +import external.com.android.dx.rop.code.ThrowingCstInsn; +import external.com.android.dx.rop.code.ThrowingInsn; +import external.com.android.dx.rop.code.TranslationAdvice; +import external.com.android.dx.rop.cst.CstInteger; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.Prototype; +import external.com.android.dx.rop.type.StdTypeList; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.rop.type.TypeList; +import external.com.android.dx.util.Bits; +import external.com.android.dx.util.Hex; +import external.com.android.dx.util.IntList; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Utility that converts a basic block list into a list of register-oriented + * blocks. + */ +public final class Ropper { + /** label offset for the parameter assignment block */ + private static final int PARAM_ASSIGNMENT = -1; + + /** label offset for the return block */ + private static final int RETURN = -2; + + /** label offset for the synchronized method final return block */ + private static final int SYNCH_RETURN = -3; + + /** label offset for the first synchronized method setup block */ + private static final int SYNCH_SETUP_1 = -4; + + /** label offset for the second synchronized method setup block */ + private static final int SYNCH_SETUP_2 = -5; + + /** + * label offset for the first synchronized method exception + * handler block + */ + private static final int SYNCH_CATCH_1 = -6; + + /** + * label offset for the second synchronized method exception + * handler block + */ + private static final int SYNCH_CATCH_2 = -7; + + /** number of special label offsets */ + private static final int SPECIAL_LABEL_COUNT = 7; + + /** {@code non-null;} method being converted */ + private final ConcreteMethod method; + + /** {@code non-null;} original block list */ + private final ByteBlockList blocks; + + /** max locals of the method */ + private final int maxLocals; + + /** max label (exclusive) of any original bytecode block */ + private final int maxLabel; + + /** {@code non-null;} simulation machine to use */ + private final RopperMachine machine; + + /** {@code non-null;} simulator to use */ + private final Simulator sim; + + /** + * {@code non-null;} sparse array mapping block labels to initial frame + * contents, if known + */ + private final Frame[] startFrames; + + /** {@code non-null;} output block list in-progress */ + private final ArrayList result; + + /** + * {@code non-null;} list of subroutine-nest labels + * (See {@link Frame#getSubroutines} associated with each result block. + * Parallel to {@link Ropper#result}. + */ + private final ArrayList resultSubroutines; + + /** + * {@code non-null;} for each block (by label) that is used as an exception + * handler in the input, the exception handling info in Rop. + */ + private final CatchInfo[] catchInfos; + + /** + * whether an exception-handler block for a synchronized method was + * ever required + */ + private boolean synchNeedsExceptionHandler; + + /** + * {@code non-null;} list of subroutines indexed by label of start + * address */ + private final Subroutine[] subroutines; + + /** true if {@code subroutines} is non-empty */ + private boolean hasSubroutines; + + /** Allocates labels of exception handler setup blocks. */ + private final ExceptionSetupLabelAllocator exceptionSetupLabelAllocator; + + /** + * Keeps mapping of an input exception handler target code and how it is generated/targeted in + * Rop. + */ + private class CatchInfo { + /** + * {@code non-null;} map of ExceptionHandlerSetup by the type they handle */ + private final Map setups = + new HashMap(); + + /** + * Get the {@link ExceptionHandlerSetup} corresponding to the given type. The + * ExceptionHandlerSetup is created if this the first request for the given type. + * + * @param caughtType {@code non-null;} the type catch by the requested setup + * @return {@code non-null;} the handler setup block info for the given type + */ + ExceptionHandlerSetup getSetup(Type caughtType) { + ExceptionHandlerSetup handler = setups.get(caughtType); + if (handler == null) { + int handlerSetupLabel = exceptionSetupLabelAllocator.getNextLabel(); + handler = new ExceptionHandlerSetup(caughtType, handlerSetupLabel); + setups.put(caughtType, handler); + } + return handler; + } + + /** + * Get all {@link ExceptionHandlerSetup} of this handler. + * + * @return {@code non-null;} + */ + Collection getSetups() { + return setups.values(); + } + } + + /** + * Keeps track of an exception handler setup. + */ + private static class ExceptionHandlerSetup { + /** + * {@code non-null;} The caught type. */ + private Type caughtType; + /** + * {@code >= 0;} The label of the exception setup block. */ + private int label; + + /** + * Constructs instance. + * + * @param caughtType {@code non-null;} the caught type + * @param label {@code >= 0;} the label + */ + ExceptionHandlerSetup(Type caughtType, int label) { + this.caughtType = caughtType; + this.label = label; + } + + /** + * @return {@code non-null;} the caught type + */ + Type getCaughtType() { + return caughtType; + } + + /** + * @return {@code >= 0;} the label + */ + public int getLabel() { + return label; + } + } + + /** + * Keeps track of subroutines that exist in java form and are inlined in + * Rop form. + */ + private class Subroutine { + /** list of all blocks that jsr to this subroutine */ + private BitSet callerBlocks; + /** List of all blocks that return from this subroutine */ + private BitSet retBlocks; + /** first block in this subroutine */ + private int startBlock; + + /** + * Constructs instance. + * + * @param startBlock First block of the subroutine. + */ + Subroutine(int startBlock) { + this.startBlock = startBlock; + retBlocks = new BitSet(maxLabel); + callerBlocks = new BitSet(maxLabel); + hasSubroutines = true; + } + + /** + * Constructs instance. + * + * @param startBlock First block of the subroutine. + * @param retBlock one of the ret blocks (final blocks) of this + * subroutine. + */ + Subroutine(int startBlock, int retBlock) { + this(startBlock); + addRetBlock(retBlock); + } + + /** + * @return {@code >= 0;} the label of the subroutine's start block. + */ + int getStartBlock() { + return startBlock; + } + + /** + * Adds a label to the list of ret blocks (final blocks) for this + * subroutine. + * + * @param retBlock ret block label + */ + void addRetBlock(int retBlock) { + retBlocks.set(retBlock); + } + + /** + * Adds a label to the list of caller blocks for this subroutine. + * + * @param label a block that invokes this subroutine. + */ + void addCallerBlock(int label) { + callerBlocks.set(label); + } + + /** + * Generates a list of subroutine successors. Note: successor blocks + * could be listed more than once. This is ok, because this successor + * list (and the block it's associated with) will be copied and inlined + * before we leave the ropper. Redundent successors will result in + * redundent (no-op) merges. + * + * @return all currently known successors + * (return destinations) for that subroutine + */ + IntList getSuccessors() { + IntList successors = new IntList(callerBlocks.size()); + + /* + * For each subroutine caller, get it's target. If the + * target is us, add the ret target (subroutine successor) + * to our list + */ + + for (int label = callerBlocks.nextSetBit(0); label >= 0; + label = callerBlocks.nextSetBit(label+1)) { + BasicBlock subCaller = labelToBlock(label); + successors.add(subCaller.getSuccessors().get(0)); + } + + successors.setImmutable(); + + return successors; + } + + /** + * Merges the specified frame into this subroutine's successors, + * setting {@code workSet} as appropriate. To be called with + * the frame of a subroutine ret block. + * + * @param frame {@code non-null;} frame from ret block to merge + * @param workSet {@code non-null;} workset to update + */ + void mergeToSuccessors(Frame frame, int[] workSet) { + for (int label = callerBlocks.nextSetBit(0); label >= 0; + label = callerBlocks.nextSetBit(label+1)) { + BasicBlock subCaller = labelToBlock(label); + int succLabel = subCaller.getSuccessors().get(0); + + Frame subFrame = frame.subFrameForLabel(startBlock, label); + + if (subFrame != null) { + mergeAndWorkAsNecessary(succLabel, -1, null, + subFrame, workSet); + } else { + Bits.set(workSet, label); + } + } + } + } + + /** + * Converts a {@link ConcreteMethod} to a {@link RopMethod}. + * + * @param method {@code non-null;} method to convert + * @param advice {@code non-null;} translation advice to use + * @param methods {@code non-null;} list of methods defined by the class + * that defines {@code method}. + * @return {@code non-null;} the converted instance + */ + public static RopMethod convert(ConcreteMethod method, + TranslationAdvice advice, MethodList methods, DexOptions dexOptions) { + try { + Ropper r = new Ropper(method, advice, methods, dexOptions); + r.doit(); + return r.getRopMethod(); + } catch (SimException ex) { + ex.addContext("...while working on method " + + method.getNat().toHuman()); + throw ex; + } + } + + /** + * Constructs an instance. This class is not publicly instantiable; use + * {@link #convert}. + * + * @param method {@code non-null;} method to convert + * @param advice {@code non-null;} translation advice to use + * @param methods {@code non-null;} list of methods defined by the class + * that defines {@code method}. + * @param dexOptions {@code non-null;} options for dex output + */ + private Ropper(ConcreteMethod method, TranslationAdvice advice, MethodList methods, + DexOptions dexOptions) { + if (method == null) { + throw new NullPointerException("method == null"); + } + + if (advice == null) { + throw new NullPointerException("advice == null"); + } + + this.method = method; + this.blocks = BasicBlocker.identifyBlocks(method); + this.maxLabel = blocks.getMaxLabel(); + this.maxLocals = method.getMaxLocals(); + this.machine = new RopperMachine(this, method, advice, methods); + this.sim = new Simulator(machine, method, dexOptions); + this.startFrames = new Frame[maxLabel]; + this.subroutines = new Subroutine[maxLabel]; + + /* + * The "* 2 + 10" below is to conservatively believe that every + * block is an exception handler target and should also + * take care of enough other possible extra overhead such that + * the underlying array is unlikely to need resizing. + */ + this.result = new ArrayList(blocks.size() * 2 + 10); + this.resultSubroutines = + new ArrayList(blocks.size() * 2 + 10); + + this.catchInfos = new CatchInfo[maxLabel]; + this.synchNeedsExceptionHandler = false; + + /* + * Set up the first stack frame with the right limits, but leave it + * empty here (to be filled in outside of the constructor). + */ + startFrames[0] = new Frame(maxLocals, method.getMaxStack()); + exceptionSetupLabelAllocator = new ExceptionSetupLabelAllocator(); + } + + /** + * Gets the first (lowest) register number to use as the temporary + * area when unwinding stack manipulation ops. + * + * @return {@code >= 0;} the first register to use + */ + /*package*/ int getFirstTempStackReg() { + /* + * We use the register that is just past the deepest possible + * stack element, plus one if the method is synchronized to + * avoid overlapping with the synch register. We don't need to + * do anything else special at this level, since later passes + * will merely notice the highest register used by explicit + * inspection. + */ + int regCount = getNormalRegCount(); + return isSynchronized() ? regCount + 1 : regCount; + } + + /** + * Gets the label for the given special-purpose block. The given label + * should be one of the static constants defined by this class. + * + * @param label {@code < 0;} the special label constant + * @return {@code >= 0;} the actual label value to use + */ + private int getSpecialLabel(int label) { + /* + * The label is bitwise-complemented so that mistakes where + * LABEL is used instead of getSpecialLabel(LABEL) cause a + * failure at block construction time, since negative labels + * are illegal. 0..maxLabel (exclusive) are the original blocks and + * maxLabel..(maxLabel + method.getCatches().size()) are reserved for exception handler + * setup blocks (see getAvailableLabel(), exceptionSetupLabelAllocator). + */ + return maxLabel + method.getCatches().size() + ~label; + } + + /** + * Gets the minimum label for unreserved use. + * + * @return {@code >= 0;} the minimum label + */ + private int getMinimumUnreservedLabel() { + /* + * The labels below (maxLabel + method.getCatches().size() + SPECIAL_LABEL_COUNT) are + * reserved for particular uses. + */ + + return maxLabel + method.getCatches().size() + SPECIAL_LABEL_COUNT; + } + + /** + * Gets an unreserved and available label. + * Labels are distributed this way: + *
    + *
  • [0, maxLabel[ are the labels of the blocks directly + * corresponding to the input bytecode.
  • + *
  • [maxLabel, maxLabel + method.getCatches().size()[ are reserved for exception setup + * blocks.
  • + *
  • [maxLabel + method.getCatches().size(), + * maxLabel + method.getCatches().size() + SPECIAL_LABEL_COUNT[ are reserved for special blocks, + * ie param assignement, return and synch blocks.
  • + *
  • [maxLabel method.getCatches().size() + SPECIAL_LABEL_COUNT, getAvailableLabel()[ assigned + * labels. Note that some + * of the assigned labels may not be used any more if they were assigned to a block that was + * deleted since.
  • + *
+ * + * @return {@code >= 0;} an available label with the guaranty that all greater labels are + * also available. + */ + private int getAvailableLabel() { + int candidate = getMinimumUnreservedLabel(); + + for (BasicBlock bb : result) { + int label = bb.getLabel(); + if (label >= candidate) { + candidate = label + 1; + } + } + + return candidate; + } + + /** + * Gets whether the method being translated is synchronized. + * + * @return whether the method being translated is synchronized + */ + private boolean isSynchronized() { + int accessFlags = method.getAccessFlags(); + return (accessFlags & AccessFlags.ACC_SYNCHRONIZED) != 0; + } + + /** + * Gets whether the method being translated is static. + * + * @return whether the method being translated is static + */ + private boolean isStatic() { + int accessFlags = method.getAccessFlags(); + return (accessFlags & AccessFlags.ACC_STATIC) != 0; + } + + /** + * Gets the total number of registers used for "normal" purposes (i.e., + * for the straightforward translation from the original Java). + * + * @return {@code >= 0;} the total number of registers used + */ + private int getNormalRegCount() { + return maxLocals + method.getMaxStack(); + } + + /** + * Gets the register spec to use to hold the object to synchronize on, + * for a synchronized method. + * + * @return {@code non-null;} the register spec + */ + private RegisterSpec getSynchReg() { + /* + * We use the register that is just past the deepest possible + * stack element, with a minimum of v1 since v0 is what's + * always used to hold the caught exception when unwinding. We + * don't need to do anything else special at this level, since + * later passes will merely notice the highest register used + * by explicit inspection. + */ + int reg = getNormalRegCount(); + return RegisterSpec.make((reg < 1) ? 1 : reg, Type.OBJECT); + } + + /** + * Searches {@link #result} for a block with the given label. Returns its + * index if found, or returns {@code -1} if there is no such block. + * + * @param label the label to look for + * @return {@code >= -1;} the index for the block with the given label or + * {@code -1} if there is no such block + */ + private int labelToResultIndex(int label) { + int sz = result.size(); + for (int i = 0; i < sz; i++) { + BasicBlock one = result.get(i); + if (one.getLabel() == label) { + return i; + } + } + + return -1; + } + + /** + * Searches {@link #result} for a block with the given label. Returns it if + * found, or throws an exception if there is no such block. + * + * @param label the label to look for + * @return {@code non-null;} the block with the given label + */ + private BasicBlock labelToBlock(int label) { + int idx = labelToResultIndex(label); + + if (idx < 0) { + throw new IllegalArgumentException("no such label " + + Hex.u2(label)); + } + + return result.get(idx); + } + + /** + * Adds a block to the output result. + * + * @param block {@code non-null;} the block to add + * @param subroutines {@code non-null;} subroutine label list + * as described in {@link Frame#getSubroutines} + */ + private void addBlock(BasicBlock block, IntList subroutines) { + if (block == null) { + throw new NullPointerException("block == null"); + } + + result.add(block); + subroutines.throwIfMutable(); + resultSubroutines.add(subroutines); + } + + /** + * Adds or replace a block in the output result. If this is a + * replacement, then any extra blocks that got added with the + * original get removed as a result of calling this method. + * + * @param block {@code non-null;} the block to add or replace + * @param subroutines {@code non-null;} subroutine label list + * as described in {@link Frame#getSubroutines} + * @return {@code true} if the block was replaced or + * {@code false} if it was added for the first time + */ + private boolean addOrReplaceBlock(BasicBlock block, IntList subroutines) { + if (block == null) { + throw new NullPointerException("block == null"); + } + + int idx = labelToResultIndex(block.getLabel()); + boolean ret; + + if (idx < 0) { + ret = false; + } else { + /* + * We are replacing a pre-existing block, so find any + * blocks that got added as part of the original and + * remove those too. Such blocks are (possibly indirect) + * successors of this block which are out of the range of + * normally-translated blocks. + */ + removeBlockAndSpecialSuccessors(idx); + ret = true; + } + + result.add(block); + subroutines.throwIfMutable(); + resultSubroutines.add(subroutines); + return ret; + } + + /** + * Adds or replaces a block in the output result. Do not delete + * any successors. + * + * @param block {@code non-null;} the block to add or replace + * @param subroutines {@code non-null;} subroutine label list + * as described in {@link Frame#getSubroutines} + * @return {@code true} if the block was replaced or + * {@code false} if it was added for the first time + */ + private boolean addOrReplaceBlockNoDelete(BasicBlock block, + IntList subroutines) { + if (block == null) { + throw new NullPointerException("block == null"); + } + + int idx = labelToResultIndex(block.getLabel()); + boolean ret; + + if (idx < 0) { + ret = false; + } else { + result.remove(idx); + resultSubroutines.remove(idx); + ret = true; + } + + result.add(block); + subroutines.throwIfMutable(); + resultSubroutines.add(subroutines); + return ret; + } + + /** + * Helper for {@link #addOrReplaceBlock} which recursively removes + * the given block and all blocks that are (direct and indirect) + * successors of it whose labels indicate that they are not in the + * normally-translated range. + * + * @param idx {@code non-null;} block to remove (etc.) + */ + private void removeBlockAndSpecialSuccessors(int idx) { + int minLabel = getMinimumUnreservedLabel(); + BasicBlock block = result.get(idx); + IntList successors = block.getSuccessors(); + int sz = successors.size(); + + result.remove(idx); + resultSubroutines.remove(idx); + + for (int i = 0; i < sz; i++) { + int label = successors.get(i); + if (label >= minLabel) { + idx = labelToResultIndex(label); + if (idx < 0) { + throw new RuntimeException("Invalid label " + + Hex.u2(label)); + } + removeBlockAndSpecialSuccessors(idx); + } + } + } + + /** + * Extracts the resulting {@link RopMethod} from the instance. + * + * @return {@code non-null;} the method object + */ + private RopMethod getRopMethod() { + + // Construct the final list of blocks. + + int sz = result.size(); + BasicBlockList bbl = new BasicBlockList(sz); + for (int i = 0; i < sz; i++) { + bbl.set(i, result.get(i)); + } + bbl.setImmutable(); + + // Construct the method object to wrap it all up. + + /* + * Note: The parameter assignment block is always the first + * that should be executed, hence the second argument to the + * constructor. + */ + return new RopMethod(bbl, getSpecialLabel(PARAM_ASSIGNMENT)); + } + + /** + * Does the conversion. + */ + private void doit() { + int[] workSet = Bits.makeBitSet(maxLabel); + + Bits.set(workSet, 0); + addSetupBlocks(); + setFirstFrame(); + + for (;;) { + int offset = Bits.findFirst(workSet, 0); + if (offset < 0) { + break; + } + Bits.clear(workSet, offset); + ByteBlock block = blocks.labelToBlock(offset); + Frame frame = startFrames[offset]; + try { + processBlock(block, frame, workSet); + } catch (SimException ex) { + ex.addContext("...while working on block " + Hex.u2(offset)); + throw ex; + } + } + + addReturnBlock(); + addSynchExceptionHandlerBlock(); + addExceptionSetupBlocks(); + + if (hasSubroutines) { + // Subroutines are very rare, so skip this step if it's n/a + inlineSubroutines(); + } + } + + /** + * Sets up the first frame to contain all the incoming parameters in + * locals. + */ + private void setFirstFrame() { + Prototype desc = method.getEffectiveDescriptor(); + startFrames[0].initializeWithParameters(desc.getParameterTypes()); + startFrames[0].setImmutable(); + } + + /** + * Processes the given block. + * + * @param block {@code non-null;} block to process + * @param frame {@code non-null;} start frame for the block + * @param workSet {@code non-null;} bits representing work to do, + * which this method may add to + */ + private void processBlock(ByteBlock block, Frame frame, int[] workSet) { + // Prepare the list of caught exceptions for this block. + ByteCatchList catches = block.getCatches(); + machine.startBlock(catches.toRopCatchList()); + + /* + * Using a copy of the given frame, simulate each instruction, + * calling into machine for each. + */ + frame = frame.copy(); + sim.simulate(block, frame); + frame.setImmutable(); + + int extraBlockCount = machine.getExtraBlockCount(); + ArrayList insns = machine.getInsns(); + int insnSz = insns.size(); + + /* + * Merge the frame into each possible non-exceptional + * successor. + */ + + int catchSz = catches.size(); + IntList successors = block.getSuccessors(); + + int startSuccessorIndex; + + Subroutine calledSubroutine = null; + if (machine.hasJsr()) { + /* + * If this frame ends in a JSR, only merge our frame with + * the subroutine start, not the subroutine's return target. + */ + startSuccessorIndex = 1; + + int subroutineLabel = successors.get(1); + + if (subroutines[subroutineLabel] == null) { + subroutines[subroutineLabel] = + new Subroutine (subroutineLabel); + } + + subroutines[subroutineLabel].addCallerBlock(block.getLabel()); + + calledSubroutine = subroutines[subroutineLabel]; + } else if (machine.hasRet()) { + /* + * This block ends in a ret, which means it's the final block + * in some subroutine. Ultimately, this block will be copied + * and inlined for each call and then disposed of. + */ + + ReturnAddress ra = machine.getReturnAddress(); + int subroutineLabel = ra.getSubroutineAddress(); + + if (subroutines[subroutineLabel] == null) { + subroutines[subroutineLabel] + = new Subroutine (subroutineLabel, block.getLabel()); + } else { + subroutines[subroutineLabel].addRetBlock(block.getLabel()); + } + + successors = subroutines[subroutineLabel].getSuccessors(); + subroutines[subroutineLabel] + .mergeToSuccessors(frame, workSet); + // Skip processing below since we just did it. + startSuccessorIndex = successors.size(); + } else if (machine.wereCatchesUsed()) { + /* + * If there are catches, then the first successors + * (which will either be all of them or all but the last one) + * are catch targets. + */ + startSuccessorIndex = catchSz; + } else { + startSuccessorIndex = 0; + } + + int succSz = successors.size(); + for (int i = startSuccessorIndex; i < succSz; + i++) { + int succ = successors.get(i); + try { + mergeAndWorkAsNecessary(succ, block.getLabel(), + calledSubroutine, frame, workSet); + } catch (SimException ex) { + ex.addContext("...while merging to block " + Hex.u2(succ)); + throw ex; + } + } + + if ((succSz == 0) && machine.returns()) { + /* + * The block originally contained a return, but it has + * been made to instead end with a goto, and we need to + * tell it at this point that its sole successor is the + * return block. This has to happen after the merge loop + * above, since, at this point, the return block doesn't + * actually exist; it gets synthesized at the end of + * processing the original blocks. + */ + successors = IntList.makeImmutable(getSpecialLabel(RETURN)); + succSz = 1; + } + + int primarySucc; + + if (succSz == 0) { + primarySucc = -1; + } else { + primarySucc = machine.getPrimarySuccessorIndex(); + if (primarySucc >= 0) { + primarySucc = successors.get(primarySucc); + } + } + + /* + * This variable is true only when the method is synchronized and + * the block being processed can possibly throw an exception. + */ + boolean synch = isSynchronized() && machine.canThrow(); + + if (synch || (catchSz != 0)) { + /* + * Deal with exception handlers: Merge an exception-catch + * frame into each possible exception handler, and + * construct a new set of successors to point at the + * exception handler setup blocks (which get synthesized + * at the very end of processing). + */ + boolean catchesAny = false; + IntList newSucc = new IntList(succSz); + for (int i = 0; i < catchSz; i++) { + ByteCatchList.Item one = catches.get(i); + CstType exceptionClass = one.getExceptionClass(); + int targ = one.getHandlerPc(); + + catchesAny |= (exceptionClass == CstType.OBJECT); + + Frame f = frame.makeExceptionHandlerStartFrame(exceptionClass); + + try { + mergeAndWorkAsNecessary(targ, block.getLabel(), + null, f, workSet); + } catch (SimException ex) { + ex.addContext("...while merging exception to block " + + Hex.u2(targ)); + throw ex; + } + + /* + * Set up the exception handler type. + */ + CatchInfo handlers = catchInfos[targ]; + if (handlers == null) { + handlers = new CatchInfo(); + catchInfos[targ] = handlers; + } + ExceptionHandlerSetup handler = handlers.getSetup(exceptionClass.getClassType()); + + /* + * The synthesized exception setup block will have the label given by handler. + */ + newSucc.add(handler.getLabel()); + } + + if (synch && !catchesAny) { + /* + * The method is synchronized and this block doesn't + * already have a catch-all handler, so add one to the + * end, both in the successors and in the throwing + * instruction(s) at the end of the block (which is where + * the caught classes live). + */ + newSucc.add(getSpecialLabel(SYNCH_CATCH_1)); + synchNeedsExceptionHandler = true; + + for (int i = insnSz - extraBlockCount - 1; i < insnSz; i++) { + Insn insn = insns.get(i); + if (insn.canThrow()) { + insn = insn.withAddedCatch(Type.OBJECT); + insns.set(i, insn); + } + } + } + + if (primarySucc >= 0) { + newSucc.add(primarySucc); + } + + newSucc.setImmutable(); + successors = newSucc; + } + + // Construct the final resulting block(s), and store it (them). + + int primarySuccListIndex = successors.indexOf(primarySucc); + + /* + * If there are any extra blocks, work backwards through the + * list of instructions, adding single-instruction blocks, and + * resetting the successors variables as appropriate. + */ + for (/*extraBlockCount*/; extraBlockCount > 0; extraBlockCount--) { + /* + * Some of the blocks that the RopperMachine wants added + * are for move-result insns, and these need goto insns as well. + */ + Insn extraInsn = insns.get(--insnSz); + boolean needsGoto + = extraInsn.getOpcode().getBranchingness() + == Rop.BRANCH_NONE; + InsnList il = new InsnList(needsGoto ? 2 : 1); + IntList extraBlockSuccessors = successors; + + il.set(0, extraInsn); + + if (needsGoto) { + il.set(1, new PlainInsn(Rops.GOTO, + extraInsn.getPosition(), null, + RegisterSpecList.EMPTY)); + /* + * Obviously, this block won't be throwing an exception + * so it should only have one successor. + */ + extraBlockSuccessors = IntList.makeImmutable(primarySucc); + } + il.setImmutable(); + + int label = getAvailableLabel(); + BasicBlock bb = new BasicBlock(label, il, extraBlockSuccessors, + primarySucc); + // All of these extra blocks will be in the same subroutine + addBlock(bb, frame.getSubroutines()); + + successors = successors.mutableCopy(); + successors.set(primarySuccListIndex, label); + successors.setImmutable(); + primarySucc = label; + } + + Insn lastInsn = (insnSz == 0) ? null : insns.get(insnSz - 1); + + /* + * Add a goto to the end of the block if it doesn't already + * end with a branch, to maintain the invariant that all + * blocks end with a branch of some sort or other. Note that + * it is possible for there to be blocks for which no + * instructions were ever output (e.g., only consist of pop* + * in the original Java bytecode). + */ + if ((lastInsn == null) || + (lastInsn.getOpcode().getBranchingness() == Rop.BRANCH_NONE)) { + SourcePosition pos = (lastInsn == null) ? SourcePosition.NO_INFO : + lastInsn.getPosition(); + insns.add(new PlainInsn(Rops.GOTO, pos, null, + RegisterSpecList.EMPTY)); + insnSz++; + } + + /* + * Construct a block for the remaining instructions (which in + * the usual case is all of them). + */ + + InsnList il = new InsnList(insnSz); + for (int i = 0; i < insnSz; i++) { + il.set(i, insns.get(i)); + } + il.setImmutable(); + + BasicBlock bb = + new BasicBlock(block.getLabel(), il, successors, primarySucc); + addOrReplaceBlock(bb, frame.getSubroutines()); + } + + /** + * Helper for {@link #processBlock}, which merges frames and + * adds to the work set, as necessary. + * + * @param label {@code >= 0;} label to work on + * @param pred predecessor label; must be {@code >= 0} when + * {@code label} is a subroutine start block and calledSubroutine + * is non-null. Otherwise, may be -1. + * @param calledSubroutine {@code null-ok;} a Subroutine instance if + * {@code label} is the first block in a subroutine. + * @param frame {@code non-null;} new frame for the labelled block + * @param workSet {@code non-null;} bits representing work to do, + * which this method may add to + */ + private void mergeAndWorkAsNecessary(int label, int pred, + Subroutine calledSubroutine, Frame frame, int[] workSet) { + Frame existing = startFrames[label]; + Frame merged; + + if (existing != null) { + /* + * Some other block also continues at this label. Merge + * the frames, and re-set the bit in the work set if there + * was a change. + */ + if (calledSubroutine != null) { + merged = existing.mergeWithSubroutineCaller(frame, + calledSubroutine.getStartBlock(), pred); + } else { + merged = existing.mergeWith(frame); + } + if (merged != existing) { + startFrames[label] = merged; + Bits.set(workSet, label); + } + } else { + // This is the first time this label has been encountered. + if (calledSubroutine != null) { + startFrames[label] + = frame.makeNewSubroutineStartFrame(label, pred); + } else { + startFrames[label] = frame; + } + Bits.set(workSet, label); + } + } + + /** + * Constructs and adds the blocks that perform setup for the rest of + * the method. This includes a first block which merely contains + * assignments from parameters to the same-numbered registers and + * a possible second block which deals with synchronization. + */ + private void addSetupBlocks() { + LocalVariableList localVariables = method.getLocalVariables(); + SourcePosition pos = method.makeSourcePosistion(0); + Prototype desc = method.getEffectiveDescriptor(); + StdTypeList params = desc.getParameterTypes(); + int sz = params.size(); + InsnList insns = new InsnList(sz + 1); + int at = 0; + + for (int i = 0; i < sz; i++) { + Type one = params.get(i); + LocalVariableList.Item local = + localVariables.pcAndIndexToLocal(0, at); + RegisterSpec result = (local == null) ? + RegisterSpec.make(at, one) : + RegisterSpec.makeLocalOptional(at, one, local.getLocalItem()); + + Insn insn = new PlainCstInsn(Rops.opMoveParam(one), pos, result, + RegisterSpecList.EMPTY, + CstInteger.make(at)); + insns.set(i, insn); + at += one.getCategory(); + } + + insns.set(sz, new PlainInsn(Rops.GOTO, pos, null, + RegisterSpecList.EMPTY)); + insns.setImmutable(); + + boolean synch = isSynchronized(); + int label = synch ? getSpecialLabel(SYNCH_SETUP_1) : 0; + BasicBlock bb = + new BasicBlock(getSpecialLabel(PARAM_ASSIGNMENT), insns, + IntList.makeImmutable(label), label); + addBlock(bb, IntList.EMPTY); + + if (synch) { + RegisterSpec synchReg = getSynchReg(); + Insn insn; + if (isStatic()) { + insn = new ThrowingCstInsn(Rops.CONST_OBJECT, pos, + RegisterSpecList.EMPTY, + StdTypeList.EMPTY, + method.getDefiningClass()); + insns = new InsnList(1); + insns.set(0, insn); + } else { + insns = new InsnList(2); + insn = new PlainCstInsn(Rops.MOVE_PARAM_OBJECT, pos, + synchReg, RegisterSpecList.EMPTY, + CstInteger.VALUE_0); + insns.set(0, insn); + insns.set(1, new PlainInsn(Rops.GOTO, pos, null, + RegisterSpecList.EMPTY)); + } + + int label2 = getSpecialLabel(SYNCH_SETUP_2); + insns.setImmutable(); + bb = new BasicBlock(label, insns, + IntList.makeImmutable(label2), label2); + addBlock(bb, IntList.EMPTY); + + insns = new InsnList(isStatic() ? 2 : 1); + + if (isStatic()) { + insns.set(0, new PlainInsn(Rops.opMoveResultPseudo(synchReg), + pos, synchReg, RegisterSpecList.EMPTY)); + } + + insn = new ThrowingInsn(Rops.MONITOR_ENTER, pos, + RegisterSpecList.make(synchReg), + StdTypeList.EMPTY); + insns.set(isStatic() ? 1 :0, insn); + insns.setImmutable(); + bb = new BasicBlock(label2, insns, IntList.makeImmutable(0), 0); + addBlock(bb, IntList.EMPTY); + } + } + + /** + * Constructs and adds the return block, if necessary. The return + * block merely contains an appropriate {@code return} + * instruction. + */ + private void addReturnBlock() { + Rop returnOp = machine.getReturnOp(); + + if (returnOp == null) { + /* + * The method being converted never returns normally, so there's + * no need for a return block. + */ + return; + } + + SourcePosition returnPos = machine.getReturnPosition(); + int label = getSpecialLabel(RETURN); + + if (isSynchronized()) { + InsnList insns = new InsnList(1); + Insn insn = new ThrowingInsn(Rops.MONITOR_EXIT, returnPos, + RegisterSpecList.make(getSynchReg()), + StdTypeList.EMPTY); + insns.set(0, insn); + insns.setImmutable(); + + int nextLabel = getSpecialLabel(SYNCH_RETURN); + BasicBlock bb = + new BasicBlock(label, insns, + IntList.makeImmutable(nextLabel), nextLabel); + addBlock(bb, IntList.EMPTY); + + label = nextLabel; + } + + InsnList insns = new InsnList(1); + TypeList sourceTypes = returnOp.getSources(); + RegisterSpecList sources; + + if (sourceTypes.size() == 0) { + sources = RegisterSpecList.EMPTY; + } else { + RegisterSpec source = RegisterSpec.make(0, sourceTypes.getType(0)); + sources = RegisterSpecList.make(source); + } + + Insn insn = new PlainInsn(returnOp, returnPos, null, sources); + insns.set(0, insn); + insns.setImmutable(); + + BasicBlock bb = new BasicBlock(label, insns, IntList.EMPTY, -1); + addBlock(bb, IntList.EMPTY); + } + + /** + * Constructs and adds, if necessary, the catch-all exception handler + * block to deal with unwinding the lock taken on entry to a synchronized + * method. + */ + private void addSynchExceptionHandlerBlock() { + if (!synchNeedsExceptionHandler) { + /* + * The method being converted either isn't synchronized or + * can't possibly throw exceptions in its main body, so + * there's no need for a synchronized method exception + * handler. + */ + return; + } + + SourcePosition pos = method.makeSourcePosistion(0); + RegisterSpec exReg = RegisterSpec.make(0, Type.THROWABLE); + BasicBlock bb; + Insn insn; + + InsnList insns = new InsnList(2); + insn = new PlainInsn(Rops.opMoveException(Type.THROWABLE), pos, + exReg, RegisterSpecList.EMPTY); + insns.set(0, insn); + insn = new ThrowingInsn(Rops.MONITOR_EXIT, pos, + RegisterSpecList.make(getSynchReg()), + StdTypeList.EMPTY); + insns.set(1, insn); + insns.setImmutable(); + + int label2 = getSpecialLabel(SYNCH_CATCH_2); + bb = new BasicBlock(getSpecialLabel(SYNCH_CATCH_1), insns, + IntList.makeImmutable(label2), label2); + addBlock(bb, IntList.EMPTY); + + insns = new InsnList(1); + insn = new ThrowingInsn(Rops.THROW, pos, + RegisterSpecList.make(exReg), + StdTypeList.EMPTY); + insns.set(0, insn); + insns.setImmutable(); + + bb = new BasicBlock(label2, insns, IntList.EMPTY, -1); + addBlock(bb, IntList.EMPTY); + } + + /** + * Creates the exception handler setup blocks. "maxLocals" + * below is because that's the register number corresponding + * to the sole element on a one-deep stack (which is the + * situation at the start of an exception handler block). + */ + private void addExceptionSetupBlocks() { + + int len = catchInfos.length; + for (int i = 0; i < len; i++) { + CatchInfo catches = catchInfos[i]; + if (catches != null) { + for (ExceptionHandlerSetup one : catches.getSetups()) { + Insn proto = labelToBlock(i).getFirstInsn(); + SourcePosition pos = proto.getPosition(); + InsnList il = new InsnList(2); + + Insn insn = new PlainInsn(Rops.opMoveException(one.getCaughtType()), + pos, + RegisterSpec.make(maxLocals, one.getCaughtType()), + RegisterSpecList.EMPTY); + il.set(0, insn); + + insn = new PlainInsn(Rops.GOTO, pos, null, + RegisterSpecList.EMPTY); + il.set(1, insn); + il.setImmutable(); + + BasicBlock bb = new BasicBlock(one.getLabel(), + il, + IntList.makeImmutable(i), + i); + addBlock(bb, startFrames[i].getSubroutines()); + } + } + } + } + + /** + * Checks to see if the basic block is a subroutine caller block. + * + * @param bb {@code non-null;} the basic block in question + * @return true if this block calls a subroutine + */ + private boolean isSubroutineCaller(BasicBlock bb) { + IntList successors = bb.getSuccessors(); + if (successors.size() < 2) return false; + + int subLabel = successors.get(1); + + return (subLabel < subroutines.length) + && (subroutines[subLabel] != null); + } + + /** + * Inlines any subroutine calls. + */ + private void inlineSubroutines() { + final IntList reachableSubroutineCallerLabels = new IntList(4); + + /* + * Compile a list of all subroutine calls reachable + * through the normal (non-subroutine) flow. We do this first, since + * we'll be affecting the call flow as we go. + * + * Start at label 0 -- the param assignment block has nothing for us + */ + forEachNonSubBlockDepthFirst(0, new BasicBlock.Visitor() { + @Override + public void visitBlock(BasicBlock b) { + if (isSubroutineCaller(b)) { + reachableSubroutineCallerLabels.add(b.getLabel()); + } + } + }); + + /* + * Convert the resultSubroutines list, indexed by block index, + * to a label-to-subroutines mapping used by the inliner. + */ + int largestAllocedLabel = getAvailableLabel(); + ArrayList labelToSubroutines + = new ArrayList(largestAllocedLabel); + for (int i = 0; i < largestAllocedLabel; i++) { + labelToSubroutines.add(null); + } + + for (int i = 0; i < result.size(); i++) { + BasicBlock b = result.get(i); + if (b == null) { + continue; + } + IntList subroutineList = resultSubroutines.get(i); + labelToSubroutines.set(b.getLabel(), subroutineList); + } + + /* + * Inline all reachable subroutines. + * Inner subroutines will be inlined as they are encountered. + */ + int sz = reachableSubroutineCallerLabels.size(); + for (int i = 0 ; i < sz ; i++) { + int label = reachableSubroutineCallerLabels.get(i); + new SubroutineInliner( + new LabelAllocator(getAvailableLabel()), + labelToSubroutines) + .inlineSubroutineCalledFrom(labelToBlock(label)); + } + + // Now find the blocks that aren't reachable and remove them + deleteUnreachableBlocks(); + } + + /** + * Deletes all blocks that cannot be reached. This is run to delete + * original subroutine blocks after subroutine inlining. + */ + private void deleteUnreachableBlocks() { + final IntList reachableLabels = new IntList(result.size()); + + // subroutine inlining is done now and we won't update this list here + resultSubroutines.clear(); + + forEachNonSubBlockDepthFirst(getSpecialLabel(PARAM_ASSIGNMENT), + new BasicBlock.Visitor() { + + @Override + public void visitBlock(BasicBlock b) { + reachableLabels.add(b.getLabel()); + } + }); + + reachableLabels.sort(); + + for (int i = result.size() - 1 ; i >= 0 ; i--) { + if (reachableLabels.indexOf(result.get(i).getLabel()) < 0) { + result.remove(i); + // unnecessary here really, since subroutine inlining is done + //resultSubroutines.remove(i); + } + } + } + + /** + * Allocates labels, without requiring previously allocated labels + * to have been added to the blocks list. + */ + private static class LabelAllocator { + int nextAvailableLabel; + + /** + * @param startLabel available label to start allocating from + */ + LabelAllocator(int startLabel) { + nextAvailableLabel = startLabel; + } + + /** + * @return next available label + */ + int getNextLabel() { + return nextAvailableLabel++; + } + } + + /** + * Allocates labels for exception setup blocks. + */ + private class ExceptionSetupLabelAllocator extends LabelAllocator { + int maxSetupLabel; + + ExceptionSetupLabelAllocator() { + super(maxLabel); + maxSetupLabel = maxLabel + method.getCatches().size(); + } + + @Override + int getNextLabel() { + if (nextAvailableLabel >= maxSetupLabel) { + throw new IndexOutOfBoundsException(); + } + return nextAvailableLabel ++; + } + } + + /** + * Inlines a subroutine. Start by calling + * {@link #inlineSubroutineCalledFrom}. + */ + private class SubroutineInliner { + /** + * maps original label to the label that will be used by the + * inlined version + */ + private final HashMap origLabelToCopiedLabel; + + /** set of original labels that need to be copied */ + private final BitSet workList; + + /** the label of the original start block for this subroutine */ + private int subroutineStart; + + /** the label of the ultimate return block */ + private int subroutineSuccessor; + + /** used for generating new labels for copied blocks */ + private final LabelAllocator labelAllocator; + + /** + * A mapping, indexed by label, to subroutine nesting list. + * The subroutine nest list is as returned by + * {@link Frame#getSubroutines}. + */ + private final ArrayList labelToSubroutines; + + SubroutineInliner(final LabelAllocator labelAllocator, + ArrayList labelToSubroutines) { + origLabelToCopiedLabel = new HashMap(); + + workList = new BitSet(maxLabel); + + this.labelAllocator = labelAllocator; + this.labelToSubroutines = labelToSubroutines; + } + + /** + * Inlines a subroutine. + * + * @param b block where {@code jsr} occurred in the original bytecode + */ + void inlineSubroutineCalledFrom(final BasicBlock b) { + /* + * The 0th successor of a subroutine caller block is where + * the subroutine should return to. The 1st successor is + * the start block of the subroutine. + */ + subroutineSuccessor = b.getSuccessors().get(0); + subroutineStart = b.getSuccessors().get(1); + + /* + * This allocates an initial label and adds the first + * block to the worklist. + */ + int newSubStartLabel = mapOrAllocateLabel(subroutineStart); + + for (int label = workList.nextSetBit(0); label >= 0; + label = workList.nextSetBit(0)) { + workList.clear(label); + int newLabel = origLabelToCopiedLabel.get(label); + + copyBlock(label, newLabel); + + if (isSubroutineCaller(labelToBlock(label))) { + new SubroutineInliner(labelAllocator, labelToSubroutines) + .inlineSubroutineCalledFrom(labelToBlock(newLabel)); + } + } + + /* + * Replace the original caller block, since we now have a + * new successor + */ + + addOrReplaceBlockNoDelete( + new BasicBlock(b.getLabel(), b.getInsns(), + IntList.makeImmutable (newSubStartLabel), + newSubStartLabel), + labelToSubroutines.get(b.getLabel())); + } + + /** + * Copies a basic block, mapping its successors along the way. + * + * @param origLabel original block label + * @param newLabel label that the new block should have + */ + private void copyBlock(int origLabel, int newLabel) { + + BasicBlock origBlock = labelToBlock(origLabel); + + final IntList origSuccessors = origBlock.getSuccessors(); + IntList successors; + int primarySuccessor = -1; + Subroutine subroutine; + + if (isSubroutineCaller(origBlock)) { + /* + * A subroutine call inside a subroutine call. + * Set up so we can recurse. The caller block should have + * it's first successor be a copied block that will be + * the subroutine's return point. It's second successor will + * be copied when we recurse, and remains as the original + * label of the start of the inner subroutine. + */ + + successors = IntList.makeImmutable( + mapOrAllocateLabel(origSuccessors.get(0)), + origSuccessors.get(1)); + // primary successor will be set when this block is replaced + } else if (null + != (subroutine = subroutineFromRetBlock(origLabel))) { + /* + * this is a ret block -- its successor + * should be subroutineSuccessor + */ + + // Sanity check + if (subroutine.startBlock != subroutineStart) { + throw new RuntimeException ( + "ret instruction returns to label " + + Hex.u2 (subroutine.startBlock) + + " expected: " + Hex.u2(subroutineStart)); + } + + successors = IntList.makeImmutable(subroutineSuccessor); + primarySuccessor = subroutineSuccessor; + } else { + // Map all the successor labels + + int origPrimary = origBlock.getPrimarySuccessor(); + int sz = origSuccessors.size(); + + successors = new IntList(sz); + + for (int i = 0 ; i < sz ; i++) { + int origSuccLabel = origSuccessors.get(i); + int newSuccLabel = mapOrAllocateLabel(origSuccLabel); + + successors.add(newSuccLabel); + + if (origPrimary == origSuccLabel) { + primarySuccessor = newSuccLabel; + } + } + + successors.setImmutable(); + } + + addBlock ( + new BasicBlock(newLabel, + filterMoveReturnAddressInsns(origBlock.getInsns()), + successors, primarySuccessor), + labelToSubroutines.get(newLabel)); + } + + /** + * Checks to see if a specified label is involved in a specified + * subroutine. + * + * @param label {@code >= 0;} a basic block label + * @param subroutineStart {@code >= 0;} a subroutine as identified + * by the label of its start block + * @return true if the block is dominated by the subroutine call + */ + private boolean involvedInSubroutine(int label, int subroutineStart) { + IntList subroutinesList = labelToSubroutines.get(label); + return (subroutinesList != null && subroutinesList.size() > 0 + && subroutinesList.top() == subroutineStart); + } + + /** + * Maps the label of a pre-copied block to the label of the inlined + * block, allocating a new label and adding it to the worklist + * if necessary. If the origLabel is a "special" label, it + * is returned exactly and not scheduled for duplication: copying + * never proceeds past a special label, which likely is the function + * return block or an immediate predecessor. + * + * @param origLabel label of original, pre-copied block + * @return label for new, inlined block + */ + private int mapOrAllocateLabel(int origLabel) { + int resultLabel; + Integer mappedLabel = origLabelToCopiedLabel.get(origLabel); + + if (mappedLabel != null) { + resultLabel = mappedLabel; + } else if (!involvedInSubroutine(origLabel,subroutineStart)) { + /* + * A subroutine has ended by some means other than a "ret" + * (which really means a throw caught later). + */ + resultLabel = origLabel; + } else { + resultLabel = labelAllocator.getNextLabel(); + workList.set(origLabel); + origLabelToCopiedLabel.put(origLabel, resultLabel); + + // The new label has the same frame as the original label + while (labelToSubroutines.size() <= resultLabel) { + labelToSubroutines.add(null); + } + labelToSubroutines.set(resultLabel, + labelToSubroutines.get(origLabel)); + } + + return resultLabel; + } + } + + /** + * Finds a {@code Subroutine} that is returned from by a {@code ret} in + * a given block. + * + * @param label A block that originally contained a {@code ret} instruction + * @return {@code null-ok;} found subroutine or {@code null} if none + * was found + */ + private Subroutine subroutineFromRetBlock(int label) { + for (int i = subroutines.length - 1 ; i >= 0 ; i--) { + if (subroutines[i] != null) { + Subroutine subroutine = subroutines[i]; + + if (subroutine.retBlocks.get(label)) { + return subroutine; + } + } + } + + return null; + } + + + /** + * Removes all {@code move-return-address} instructions, returning a new + * {@code InsnList} if necessary. The {@code move-return-address} + * insns are dead code after subroutines have been inlined. + * + * @param insns {@code InsnList} that may contain + * {@code move-return-address} insns + * @return {@code InsnList} with {@code move-return-address} removed + */ + private InsnList filterMoveReturnAddressInsns(InsnList insns) { + int sz; + int newSz = 0; + + // First see if we need to filter, and if so what the new size will be + sz = insns.size(); + for (int i = 0; i < sz; i++) { + if (insns.get(i).getOpcode() != Rops.MOVE_RETURN_ADDRESS) { + newSz++; + } + } + + if (newSz == sz) { + return insns; + } + + // Make a new list without the MOVE_RETURN_ADDRESS insns + InsnList newInsns = new InsnList(newSz); + + int newIndex = 0; + for (int i = 0; i < sz; i++) { + Insn insn = insns.get(i); + if (insn.getOpcode() != Rops.MOVE_RETURN_ADDRESS) { + newInsns.set(newIndex++, insn); + } + } + + newInsns.setImmutable(); + return newInsns; + } + + /** + * Visits each non-subroutine block once in depth-first successor order. + * + * @param firstLabel label of start block + * @param v callback interface + */ + private void forEachNonSubBlockDepthFirst(int firstLabel, + BasicBlock.Visitor v) { + forEachNonSubBlockDepthFirst0(labelToBlock(firstLabel), + v, new BitSet(maxLabel)); + } + + /** + * Visits each block once in depth-first successor order, ignoring + * {@code jsr} targets. Worker for {@link #forEachNonSubBlockDepthFirst}. + * + * @param next next block to visit + * @param v callback interface + * @param visited set of blocks already visited + */ + private void forEachNonSubBlockDepthFirst0( + BasicBlock next, BasicBlock.Visitor v, BitSet visited) { + v.visitBlock(next); + visited.set(next.getLabel()); + + IntList successors = next.getSuccessors(); + int sz = successors.size(); + + for (int i = 0; i < sz; i++) { + int succ = successors.get(i); + + if (visited.get(succ)) { + continue; + } + + if (isSubroutineCaller(next) && i > 0) { + // ignore jsr targets + continue; + } + + /* + * Ignore missing labels: they're successors of + * subroutines that never invoke a ret. + */ + int idx = labelToResultIndex(succ); + if (idx >= 0) { + forEachNonSubBlockDepthFirst0(result.get(idx), v, visited); + } + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/RopperMachine.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/RopperMachine.java new file mode 100644 index 00000000..bf26fb94 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/RopperMachine.java @@ -0,0 +1,1032 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.cf.iface.Method; +import external.com.android.dx.cf.iface.MethodList; +import external.com.android.dx.rop.code.AccessFlags; +import external.com.android.dx.rop.code.FillArrayDataInsn; +import external.com.android.dx.rop.code.Insn; +import external.com.android.dx.rop.code.InvokePolymorphicInsn; +import external.com.android.dx.rop.code.PlainCstInsn; +import external.com.android.dx.rop.code.PlainInsn; +import external.com.android.dx.rop.code.RegOps; +import external.com.android.dx.rop.code.RegisterSpec; +import external.com.android.dx.rop.code.RegisterSpecList; +import external.com.android.dx.rop.code.Rop; +import external.com.android.dx.rop.code.Rops; +import external.com.android.dx.rop.code.SourcePosition; +import external.com.android.dx.rop.code.SwitchInsn; +import external.com.android.dx.rop.code.ThrowingCstInsn; +import external.com.android.dx.rop.code.ThrowingInsn; +import external.com.android.dx.rop.code.TranslationAdvice; +import external.com.android.dx.rop.cst.Constant; +import external.com.android.dx.rop.cst.CstCallSiteRef; +import external.com.android.dx.rop.cst.CstFieldRef; +import external.com.android.dx.rop.cst.CstInteger; +import external.com.android.dx.rop.cst.CstMethodRef; +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.rop.type.TypeBearer; +import external.com.android.dx.rop.type.TypeList; +import external.com.android.dx.util.IntList; +import java.util.ArrayList; + +/** + * Machine implementation for use by {@link Ropper}. + */ +/*package*/ final class RopperMachine extends ValueAwareMachine { + /** {@code non-null;} array reflection class */ + private static final CstType ARRAY_REFLECT_TYPE = + new CstType(Type.internClassName("java/lang/reflect/Array")); + + /** + * {@code non-null;} method constant for use in converting + * {@code multianewarray} instructions + */ + private static final CstMethodRef MULTIANEWARRAY_METHOD = + new CstMethodRef(ARRAY_REFLECT_TYPE, + new CstNat(new CstString("newInstance"), + new CstString("(Ljava/lang/Class;[I)" + + "Ljava/lang/Object;"))); + + /** {@code non-null;} {@link Ropper} controlling this instance */ + private final Ropper ropper; + + /** {@code non-null;} method being converted */ + private final ConcreteMethod method; + + /** {@code non-null:} list of methods from the class whose method is being converted */ + private final MethodList methods; + + /** {@code non-null;} translation advice */ + private final TranslationAdvice advice; + + /** max locals of the method */ + private final int maxLocals; + + /** {@code non-null;} instructions for the rop basic block in-progress */ + private final ArrayList insns; + + /** {@code non-null;} catches for the block currently being processed */ + private TypeList catches; + + /** whether the catches have been used in an instruction */ + private boolean catchesUsed; + + /** whether the block contains a {@code return} */ + private boolean returns; + + /** primary successor index */ + private int primarySuccessorIndex; + + /** {@code >= 0;} number of extra basic blocks required */ + private int extraBlockCount; + + /** true if last processed block ends with a jsr or jsr_W*/ + private boolean hasJsr; + + /** true if an exception can be thrown by the last block processed */ + private boolean blockCanThrow; + + /** + * If non-null, the ReturnAddress that was used by the terminating ret + * instruction. If null, there was no ret instruction encountered. + */ + + private ReturnAddress returnAddress; + + /** + * {@code null-ok;} the appropriate {@code return} op or {@code null} + * if it is not yet known + */ + private Rop returnOp; + + /** + * {@code null-ok;} the source position for the return block or {@code null} + * if it is not yet known + */ + private SourcePosition returnPosition; + + /** + * Constructs an instance. + * + * @param ropper {@code non-null;} ropper controlling this instance + * @param method {@code non-null;} method being converted + * @param advice {@code non-null;} translation advice to use + * @param methods {@code non-null;} list of methods defined by the class + * that defines {@code method}. + */ + public RopperMachine(Ropper ropper, ConcreteMethod method, + TranslationAdvice advice, MethodList methods) { + super(method.getEffectiveDescriptor()); + + if (methods == null) { + throw new NullPointerException("methods == null"); + } + + if (ropper == null) { + throw new NullPointerException("ropper == null"); + } + + if (advice == null) { + throw new NullPointerException("advice == null"); + } + + this.ropper = ropper; + this.method = method; + this.methods = methods; + this.advice = advice; + this.maxLocals = method.getMaxLocals(); + this.insns = new ArrayList(25); + this.catches = null; + this.catchesUsed = false; + this.returns = false; + this.primarySuccessorIndex = -1; + this.extraBlockCount = 0; + this.blockCanThrow = false; + this.returnOp = null; + this.returnPosition = null; + } + + /** + * Gets the instructions array. It is shared and gets modified by + * subsequent calls to this instance. + * + * @return {@code non-null;} the instructions array + */ + public ArrayList getInsns() { + return insns; + } + + /** + * Gets the return opcode encountered, if any. + * + * @return {@code null-ok;} the return opcode + */ + public Rop getReturnOp() { + return returnOp; + } + + /** + * Gets the return position, if known. + * + * @return {@code null-ok;} the return position + */ + public SourcePosition getReturnPosition() { + return returnPosition; + } + + /** + * Gets ready to start working on a new block. This will clear the + * {@link #insns} list, set {@link #catches}, reset whether it has + * been used, reset whether the block contains a + * {@code return}, and reset {@link #primarySuccessorIndex}. + */ + public void startBlock(TypeList catches) { + this.catches = catches; + + insns.clear(); + catchesUsed = false; + returns = false; + primarySuccessorIndex = 0; + extraBlockCount = 0; + blockCanThrow = false; + hasJsr = false; + returnAddress = null; + } + + /** + * Gets whether {@link #catches} was used. This indicates that the + * last instruction in the block is one of the ones that can throw. + * + * @return whether {@code catches} has been used + */ + public boolean wereCatchesUsed() { + return catchesUsed; + } + + /** + * Gets whether the block just processed ended with a + * {@code return}. + * + * @return whether the block returns + */ + public boolean returns() { + return returns; + } + + /** + * Gets the primary successor index. This is the index into the + * successors list where the primary may be found or + * {@code -1} if there are successors but no primary + * successor. This may return something other than + * {@code -1} in the case of an instruction with no + * successors at all (primary or otherwise). + * + * @return {@code >= -1;} the primary successor index + */ + public int getPrimarySuccessorIndex() { + return primarySuccessorIndex; + } + + /** + * Gets how many extra blocks will be needed to represent the + * block currently being translated. Each extra block should consist + * of one instruction from the end of the original block. + * + * @return {@code >= 0;} the number of extra blocks needed + */ + public int getExtraBlockCount() { + return extraBlockCount; + } + + /** + * @return true if at least one of the insn processed since the last + * call to startBlock() can throw. + */ + public boolean canThrow() { + return blockCanThrow; + } + + /** + * @return true if a JSR has ben encountered since the last call to + * startBlock() + */ + public boolean hasJsr() { + return hasJsr; + } + + /** + * @return {@code true} if a {@code ret} has ben encountered since + * the last call to {@code startBlock()} + */ + public boolean hasRet() { + return returnAddress != null; + } + + /** + * @return {@code null-ok;} return address of a {@code ret} + * instruction if encountered since last call to startBlock(). + * {@code null} if no ret instruction encountered. + */ + public ReturnAddress getReturnAddress() { + return returnAddress; + } + + /** {@inheritDoc} */ + @Override + public void run(Frame frame, int offset, int opcode) { + /* + * This is the stack pointer after the opcode's arguments have been + * popped. + */ + int stackPointer = maxLocals + frame.getStack().size(); + + // The sources have to be retrieved before super.run() gets called. + RegisterSpecList sources = getSources(opcode, stackPointer); + int sourceCount = sources.size(); + + super.run(frame, offset, opcode); + + SourcePosition pos = method.makeSourcePosistion(offset); + RegisterSpec localTarget = getLocalTarget(opcode == ByteOps.ISTORE); + int destCount = resultCount(); + RegisterSpec dest; + + if (destCount == 0) { + dest = null; + switch (opcode) { + case ByteOps.POP: + case ByteOps.POP2: { + // These simply don't appear in the rop form. + return; + } + } + } else if (localTarget != null) { + dest = localTarget; + } else if (destCount == 1) { + dest = RegisterSpec.make(stackPointer, result(0)); + } else { + /* + * This clause only ever applies to the stack manipulation + * ops that have results (that is, dup* and swap but not + * pop*). + * + * What we do is first move all the source registers into + * the "temporary stack" area defined for the method, and + * then move stuff back down onto the main "stack" in the + * arrangement specified by the stack op pattern. + * + * Note: This code ends up emitting a lot of what will + * turn out to be superfluous moves (e.g., moving back and + * forth to the same local when doing a dup); however, + * that makes this code a bit easier (and goodness knows + * it doesn't need any extra complexity), and all the SSA + * stuff is going to want to deal with this sort of + * superfluous assignment anyway, so it should be a wash + * in the end. + */ + int scratchAt = ropper.getFirstTempStackReg(); + RegisterSpec[] scratchRegs = new RegisterSpec[sourceCount]; + + for (int i = 0; i < sourceCount; i++) { + RegisterSpec src = sources.get(i); + TypeBearer type = src.getTypeBearer(); + RegisterSpec scratch = src.withReg(scratchAt); + insns.add(new PlainInsn(Rops.opMove(type), pos, scratch, src)); + scratchRegs[i] = scratch; + scratchAt += src.getCategory(); + } + + for (int pattern = getAuxInt(); pattern != 0; pattern >>= 4) { + int which = (pattern & 0x0f) - 1; + RegisterSpec scratch = scratchRegs[which]; + TypeBearer type = scratch.getTypeBearer(); + insns.add(new PlainInsn(Rops.opMove(type), pos, + scratch.withReg(stackPointer), + scratch)); + stackPointer += type.getType().getCategory(); + } + return; + } + + TypeBearer destType = (dest != null) ? dest : Type.VOID; + Constant cst = getAuxCst(); + int ropOpcode; + Rop rop; + Insn insn; + + if (opcode == ByteOps.MULTIANEWARRAY) { + blockCanThrow = true; + + // Add the extra instructions for handling multianewarray. + + extraBlockCount = 6; + + /* + * Add an array constructor for the int[] containing all the + * dimensions. + */ + RegisterSpec dimsReg = + RegisterSpec.make(dest.getNextReg(), Type.INT_ARRAY); + rop = Rops.opFilledNewArray(Type.INT_ARRAY, sourceCount); + insn = new ThrowingCstInsn(rop, pos, sources, catches, + CstType.INT_ARRAY); + insns.add(insn); + + // Add a move-result for the new-filled-array + rop = Rops.opMoveResult(Type.INT_ARRAY); + insn = new PlainInsn(rop, pos, dimsReg, RegisterSpecList.EMPTY); + insns.add(insn); + + /* + * Add a const-class instruction for the specified array + * class. + */ + + /* + * Remove as many dimensions from the originally specified + * class as are given in the explicit list of dimensions, + * so as to pass the right component class to the standard + * Java library array constructor. + */ + Type componentType = ((CstType) cst).getClassType(); + for (int i = 0; i < sourceCount; i++) { + componentType = componentType.getComponentType(); + } + + RegisterSpec classReg = + RegisterSpec.make(dest.getReg(), Type.CLASS); + + if (componentType.isPrimitive()) { + /* + * The component type is primitive (e.g., int as opposed + * to Integer), so we have to fetch the corresponding + * TYPE class. + */ + CstFieldRef typeField = + CstFieldRef.forPrimitiveType(componentType); + insn = new ThrowingCstInsn(Rops.GET_STATIC_OBJECT, pos, + RegisterSpecList.EMPTY, + catches, typeField); + } else { + /* + * The component type is an object type, so just make a + * normal class reference. + */ + insn = new ThrowingCstInsn(Rops.CONST_OBJECT, pos, + RegisterSpecList.EMPTY, catches, + new CstType(componentType)); + } + + insns.add(insn); + + // Add a move-result-pseudo for the get-static or const + rop = Rops.opMoveResultPseudo(classReg.getType()); + insn = new PlainInsn(rop, pos, classReg, RegisterSpecList.EMPTY); + insns.add(insn); + + /* + * Add a call to the "multianewarray method," that is, + * Array.newInstance(class, dims). Note: The result type + * of newInstance() is Object, which is why the last + * instruction in this sequence is a cast to the right + * type for the original instruction. + */ + + RegisterSpec objectReg = + RegisterSpec.make(dest.getReg(), Type.OBJECT); + + insn = new ThrowingCstInsn( + Rops.opInvokeStatic(MULTIANEWARRAY_METHOD.getPrototype()), + pos, RegisterSpecList.make(classReg, dimsReg), + catches, MULTIANEWARRAY_METHOD); + insns.add(insn); + + // Add a move-result. + rop = Rops.opMoveResult(MULTIANEWARRAY_METHOD.getPrototype() + .getReturnType()); + insn = new PlainInsn(rop, pos, objectReg, RegisterSpecList.EMPTY); + insns.add(insn); + + /* + * And finally, set up for the remainder of this method to + * add an appropriate cast. + */ + + opcode = ByteOps.CHECKCAST; + sources = RegisterSpecList.make(objectReg); + } else if (opcode == ByteOps.JSR) { + // JSR has no Rop instruction + hasJsr = true; + return; + } else if (opcode == ByteOps.RET) { + try { + returnAddress = (ReturnAddress)arg(0); + } catch (ClassCastException ex) { + throw new RuntimeException( + "Argument to RET was not a ReturnAddress", ex); + } + // RET has no Rop instruction. + return; + } + + ropOpcode = jopToRopOpcode(opcode, cst); + rop = Rops.ropFor(ropOpcode, destType, sources, cst); + + Insn moveResult = null; + if (dest != null && rop.isCallLike()) { + /* + * We're going to want to have a move-result in the next + * basic block. + */ + extraBlockCount++; + + Type returnType; + if (rop.getOpcode() == RegOps.INVOKE_CUSTOM) { + returnType = ((CstCallSiteRef) cst).getReturnType(); + } else { + returnType = ((CstMethodRef) cst).getPrototype().getReturnType(); + } + moveResult = new PlainInsn(Rops.opMoveResult(returnType), + pos, dest, RegisterSpecList.EMPTY); + + dest = null; + } else if (dest != null && rop.canThrow()) { + /* + * We're going to want to have a move-result-pseudo in the + * next basic block. + */ + extraBlockCount++; + + moveResult = new PlainInsn( + Rops.opMoveResultPseudo(dest.getTypeBearer()), + pos, dest, RegisterSpecList.EMPTY); + + dest = null; + } + if (ropOpcode == RegOps.NEW_ARRAY) { + /* + * In the original bytecode, this was either a primitive + * array constructor "newarray" or an object array + * constructor "anewarray". In the former case, there is + * no explicit constant, and in the latter, the constant + * is for the element type and not the array type. The rop + * instruction form for both of these is supposed to be + * the resulting array type, so we initialize / alter + * "cst" here, accordingly. Conveniently enough, the rop + * opcode already gets constructed with the proper array + * type. + */ + cst = CstType.intern(rop.getResult()); + } else if ((cst == null) && (sourceCount == 2)) { + TypeBearer firstType = sources.get(0).getTypeBearer(); + TypeBearer lastType = sources.get(1).getTypeBearer(); + + if ((lastType.isConstant() || firstType.isConstant()) && + advice.hasConstantOperation(rop, sources.get(0), + sources.get(1))) { + + if (lastType.isConstant()) { + /* + * The target architecture has an instruction that can + * build in the constant found in the second argument, + * so pull it out of the sources and just use it as a + * constant here. + */ + cst = (Constant) lastType; + sources = sources.withoutLast(); + + // For subtraction, change to addition and invert constant + if (rop.getOpcode() == RegOps.SUB) { + ropOpcode = RegOps.ADD; + CstInteger cstInt = (CstInteger) lastType; + cst = CstInteger.make(-cstInt.getValue()); + } + } else { + /* + * The target architecture has an instruction that can + * build in the constant found in the first argument, + * so pull it out of the sources and just use it as a + * constant here. + */ + cst = (Constant) firstType; + sources = sources.withoutFirst(); + } + + rop = Rops.ropFor(ropOpcode, destType, sources, cst); + } + } + + SwitchList cases = getAuxCases(); + ArrayList initValues = getInitValues(); + boolean canThrow = rop.canThrow(); + + blockCanThrow |= canThrow; + + if (cases != null) { + if (cases.size() == 0) { + // It's a default-only switch statement. It can happen! + insn = new PlainInsn(Rops.GOTO, pos, null, + RegisterSpecList.EMPTY); + primarySuccessorIndex = 0; + } else { + IntList values = cases.getValues(); + insn = new SwitchInsn(rop, pos, dest, sources, values); + primarySuccessorIndex = values.size(); + } + } else if (ropOpcode == RegOps.RETURN) { + /* + * Returns get turned into the combination of a move (if + * non-void and if the return doesn't already mention + * register 0) and a goto (to the return block). + */ + if (sources.size() != 0) { + RegisterSpec source = sources.get(0); + TypeBearer type = source.getTypeBearer(); + if (source.getReg() != 0) { + insns.add(new PlainInsn(Rops.opMove(type), pos, + RegisterSpec.make(0, type), + source)); + } + } + insn = new PlainInsn(Rops.GOTO, pos, null, RegisterSpecList.EMPTY); + primarySuccessorIndex = 0; + updateReturnOp(rop, pos); + returns = true; + } else if (cst != null) { + if (canThrow) { + if (rop.getOpcode() == RegOps.INVOKE_POLYMORPHIC) { + insn = makeInvokePolymorphicInsn(rop, pos, sources, catches, cst); + } else { + insn = new ThrowingCstInsn(rop, pos, sources, catches, cst); + } + catchesUsed = true; + primarySuccessorIndex = catches.size(); + } else { + insn = new PlainCstInsn(rop, pos, dest, sources, cst); + } + } else if (canThrow) { + insn = new ThrowingInsn(rop, pos, sources, catches); + catchesUsed = true; + if (opcode == ByteOps.ATHROW) { + /* + * The op athrow is the only one where it's possible + * to have non-empty successors and yet not have a + * primary successor. + */ + primarySuccessorIndex = -1; + } else { + primarySuccessorIndex = catches.size(); + } + } else { + insn = new PlainInsn(rop, pos, dest, sources); + } + + insns.add(insn); + + if (moveResult != null) { + insns.add(moveResult); + } + + /* + * If initValues is non-null, it means that the parser has + * seen a group of compatible constant initialization + * bytecodes that are applied to the current newarray. The + * action we take here is to convert these initialization + * bytecodes into a single fill-array-data ROP which lays out + * all the constant values in a table. + */ + if (initValues != null) { + extraBlockCount++; + insn = new FillArrayDataInsn(Rops.FILL_ARRAY_DATA, pos, + RegisterSpecList.make(moveResult.getResult()), initValues, + cst); + insns.add(insn); + } + } + + /** + * Helper for {@link #run}, which gets the list of sources for the. + * instruction. + * + * @param opcode the opcode being translated + * @param stackPointer {@code >= 0;} the stack pointer after the + * instruction's arguments have been popped + * @return {@code non-null;} the sources + */ + private RegisterSpecList getSources(int opcode, int stackPointer) { + int count = argCount(); + + if (count == 0) { + // We get an easy out if there aren't any sources. + return RegisterSpecList.EMPTY; + } + + int localIndex = getLocalIndex(); + RegisterSpecList sources; + + if (localIndex >= 0) { + // The instruction is operating on a local variable. + sources = new RegisterSpecList(1); + sources.set(0, RegisterSpec.make(localIndex, arg(0))); + } else { + sources = new RegisterSpecList(count); + int regAt = stackPointer; + for (int i = 0; i < count; i++) { + RegisterSpec spec = RegisterSpec.make(regAt, arg(i)); + sources.set(i, spec); + regAt += spec.getCategory(); + } + + switch (opcode) { + case ByteOps.IASTORE: { + /* + * The Java argument order for array stores is + * (array, index, value), but the rop argument + * order is (value, array, index). The following + * code gets the right arguments in the right + * places. + */ + if (count != 3) { + throw new RuntimeException("shouldn't happen"); + } + RegisterSpec array = sources.get(0); + RegisterSpec index = sources.get(1); + RegisterSpec value = sources.get(2); + sources.set(0, value); + sources.set(1, array); + sources.set(2, index); + break; + } + case ByteOps.PUTFIELD: { + /* + * Similar to above: The Java argument order for + * putfield is (object, value), but the rop + * argument order is (value, object). + */ + if (count != 2) { + throw new RuntimeException("shouldn't happen"); + } + RegisterSpec obj = sources.get(0); + RegisterSpec value = sources.get(1); + sources.set(0, value); + sources.set(1, obj); + break; + } + } + } + + sources.setImmutable(); + return sources; + } + + /** + * Sets or updates the information about the return block. + * + * @param op {@code non-null;} the opcode to use + * @param pos {@code non-null;} the position to use + */ + private void updateReturnOp(Rop op, SourcePosition pos) { + if (op == null) { + throw new NullPointerException("op == null"); + } + + if (pos == null) { + throw new NullPointerException("pos == null"); + } + + if (returnOp == null) { + returnOp = op; + returnPosition = pos; + } else { + if (returnOp != op) { + throw new SimException("return op mismatch: " + op + ", " + + returnOp); + } + + if (pos.getLine() > returnPosition.getLine()) { + // Pick the largest line number to be the "canonical" return. + returnPosition = pos; + } + } + } + + /** + * Gets the register opcode for the given Java opcode. + * + * @param jop {@code jop >= 0;} the Java opcode + * @param cst {@code null-ok;} the constant argument, if any + * @return {@code >= 0;} the corresponding register opcode + */ + private int jopToRopOpcode(int jop, Constant cst) { + switch (jop) { + case ByteOps.POP: + case ByteOps.POP2: + case ByteOps.DUP: + case ByteOps.DUP_X1: + case ByteOps.DUP_X2: + case ByteOps.DUP2: + case ByteOps.DUP2_X1: + case ByteOps.DUP2_X2: + case ByteOps.SWAP: + case ByteOps.JSR: + case ByteOps.RET: + case ByteOps.MULTIANEWARRAY: { + // These need to be taken care of specially. + break; + } + case ByteOps.NOP: { + return RegOps.NOP; + } + case ByteOps.LDC: + case ByteOps.LDC2_W: { + return RegOps.CONST; + } + case ByteOps.ILOAD: + case ByteOps.ISTORE: { + return RegOps.MOVE; + } + case ByteOps.IALOAD: { + return RegOps.AGET; + } + case ByteOps.IASTORE: { + return RegOps.APUT; + } + case ByteOps.IADD: + case ByteOps.IINC: { + return RegOps.ADD; + } + case ByteOps.ISUB: { + return RegOps.SUB; + } + case ByteOps.IMUL: { + return RegOps.MUL; + } + case ByteOps.IDIV: { + return RegOps.DIV; + } + case ByteOps.IREM: { + return RegOps.REM; + } + case ByteOps.INEG: { + return RegOps.NEG; + } + case ByteOps.ISHL: { + return RegOps.SHL; + } + case ByteOps.ISHR: { + return RegOps.SHR; + } + case ByteOps.IUSHR: { + return RegOps.USHR; + } + case ByteOps.IAND: { + return RegOps.AND; + } + case ByteOps.IOR: { + return RegOps.OR; + } + case ByteOps.IXOR: { + return RegOps.XOR; + } + case ByteOps.I2L: + case ByteOps.I2F: + case ByteOps.I2D: + case ByteOps.L2I: + case ByteOps.L2F: + case ByteOps.L2D: + case ByteOps.F2I: + case ByteOps.F2L: + case ByteOps.F2D: + case ByteOps.D2I: + case ByteOps.D2L: + case ByteOps.D2F: { + return RegOps.CONV; + } + case ByteOps.I2B: { + return RegOps.TO_BYTE; + } + case ByteOps.I2C: { + return RegOps.TO_CHAR; + } + case ByteOps.I2S: { + return RegOps.TO_SHORT; + } + case ByteOps.LCMP: + case ByteOps.FCMPL: + case ByteOps.DCMPL: { + return RegOps.CMPL; + } + case ByteOps.FCMPG: + case ByteOps.DCMPG: { + return RegOps.CMPG; + } + case ByteOps.IFEQ: + case ByteOps.IF_ICMPEQ: + case ByteOps.IF_ACMPEQ: + case ByteOps.IFNULL: { + return RegOps.IF_EQ; + } + case ByteOps.IFNE: + case ByteOps.IF_ICMPNE: + case ByteOps.IF_ACMPNE: + case ByteOps.IFNONNULL: { + return RegOps.IF_NE; + } + case ByteOps.IFLT: + case ByteOps.IF_ICMPLT: { + return RegOps.IF_LT; + } + case ByteOps.IFGE: + case ByteOps.IF_ICMPGE: { + return RegOps.IF_GE; + } + case ByteOps.IFGT: + case ByteOps.IF_ICMPGT: { + return RegOps.IF_GT; + } + case ByteOps.IFLE: + case ByteOps.IF_ICMPLE: { + return RegOps.IF_LE; + } + case ByteOps.GOTO: { + return RegOps.GOTO; + } + case ByteOps.LOOKUPSWITCH: { + return RegOps.SWITCH; + } + case ByteOps.IRETURN: + case ByteOps.RETURN: { + return RegOps.RETURN; + } + case ByteOps.GETSTATIC: { + return RegOps.GET_STATIC; + } + case ByteOps.PUTSTATIC: { + return RegOps.PUT_STATIC; + } + case ByteOps.GETFIELD: { + return RegOps.GET_FIELD; + } + case ByteOps.PUTFIELD: { + return RegOps.PUT_FIELD; + } + case ByteOps.INVOKEVIRTUAL: { + CstMethodRef ref = (CstMethodRef) cst; + // The java bytecode specification does not explicitly disallow + // invokevirtual calls to any instance method, though it + // specifies that instance methods and private methods "should" be + // called using "invokespecial" instead of "invokevirtual". + // Several bytecode tools generate "invokevirtual" instructions for + // invocation of private methods. + // + // The dalvik opcode specification on the other hand allows + // invoke-virtual to be used only with "normal" virtual methods, + // i.e, ones that are not private, static, final or constructors. + // We therefore need to transform invoke-virtual calls to private + // instance methods to invoke-direct opcodes. + // + // Note that it assumes that all methods for a given class are + // defined in the same dex file. + // + // NOTE: This is a slow O(n) loop, and can be replaced with a + // faster implementation (at the cost of higher memory usage) + // if it proves to be a hot area of code. + if (ref.getDefiningClass().equals(method.getDefiningClass())) { + for (int i = 0; i < methods.size(); ++i) { + final Method m = methods.get(i); + if (AccessFlags.isPrivate(m.getAccessFlags()) && + ref.getNat().equals(m.getNat())) { + return RegOps.INVOKE_DIRECT; + } + } + } + // If the method reference is a signature polymorphic method + // substitute invoke-polymorphic for invoke-virtual. This only + // affects MethodHandle.invoke and MethodHandle.invokeExact. + if (ref.isSignaturePolymorphic()) { + return RegOps.INVOKE_POLYMORPHIC; + } + return RegOps.INVOKE_VIRTUAL; + } + case ByteOps.INVOKESPECIAL: { + /* + * Determine whether the opcode should be + * INVOKE_DIRECT or INVOKE_SUPER. See vmspec-2 section 6 + * on "invokespecial" as well as section 4.8.2 (7th + * bullet point) for the gory details. + */ + /* TODO: Consider checking that invoke-special target + * method is private, or constructor since otherwise ART + * verifier will reject it. + */ + CstMethodRef ref = (CstMethodRef) cst; + if (ref.isInstanceInit() || + (ref.getDefiningClass().equals(method.getDefiningClass()))) { + return RegOps.INVOKE_DIRECT; + } + return RegOps.INVOKE_SUPER; + } + case ByteOps.INVOKESTATIC: { + return RegOps.INVOKE_STATIC; + } + case ByteOps.INVOKEINTERFACE: { + return RegOps.INVOKE_INTERFACE; + } + case ByteOps.INVOKEDYNAMIC: { + return RegOps.INVOKE_CUSTOM; + } + case ByteOps.NEW: { + return RegOps.NEW_INSTANCE; + } + case ByteOps.NEWARRAY: + case ByteOps.ANEWARRAY: { + return RegOps.NEW_ARRAY; + } + case ByteOps.ARRAYLENGTH: { + return RegOps.ARRAY_LENGTH; + } + case ByteOps.ATHROW: { + return RegOps.THROW; + } + case ByteOps.CHECKCAST: { + return RegOps.CHECK_CAST; + } + case ByteOps.INSTANCEOF: { + return RegOps.INSTANCE_OF; + } + case ByteOps.MONITORENTER: { + return RegOps.MONITOR_ENTER; + } + case ByteOps.MONITOREXIT: { + return RegOps.MONITOR_EXIT; + } + } + + throw new RuntimeException("shouldn't happen"); + } + + private Insn makeInvokePolymorphicInsn(Rop rop, SourcePosition pos, RegisterSpecList sources, + TypeList catches, Constant cst) { + CstMethodRef cstMethodRef = (CstMethodRef) cst; + return new InvokePolymorphicInsn(rop, pos, sources, catches, cstMethodRef); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/SimException.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/SimException.java new file mode 100644 index 00000000..fe9554bc --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/SimException.java @@ -0,0 +1,37 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dex.util.ExceptionWithContext; + +/** + * Exception from simulation. + */ +public class SimException + extends ExceptionWithContext { + public SimException(String message) { + super(message); + } + + public SimException(Throwable cause) { + super(cause); + } + + public SimException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/Simulator.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/Simulator.java new file mode 100644 index 00000000..d1ed7145 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/Simulator.java @@ -0,0 +1,955 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dex.DexFormat; +import external.com.android.dx.dex.DexOptions; +import external.com.android.dx.rop.code.LocalItem; +import external.com.android.dx.rop.cst.Constant; +import external.com.android.dx.rop.cst.CstFieldRef; +import external.com.android.dx.rop.cst.CstInteger; +import external.com.android.dx.rop.cst.CstInterfaceMethodRef; +import external.com.android.dx.rop.cst.CstInvokeDynamic; +import external.com.android.dx.rop.cst.CstMethodHandle; +import external.com.android.dx.rop.cst.CstMethodRef; +import external.com.android.dx.rop.cst.CstProtoRef; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.Prototype; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.util.Hex; +import java.util.ArrayList; + +/** + * Class which knows how to simulate the effects of executing bytecode. + * + *

Note: This class is not thread-safe. If multiple threads + * need to use a single instance, they must synchronize access explicitly + * between themselves.

+ */ +public class Simulator { + /** + * {@code non-null;} canned error message for local variable + * table mismatches + */ + private static final String LOCAL_MISMATCH_ERROR = + "This is symptomatic of .class transformation tools that ignore " + + "local variable information."; + + /** {@code non-null;} machine to use when simulating */ + private final Machine machine; + + /** {@code non-null;} array of bytecode */ + private final BytecodeArray code; + + /** {@code non-null;} the method being simulated */ + private ConcreteMethod method; + + /** {@code non-null;} local variable information */ + private final LocalVariableList localVariables; + + /** {@code non-null;} visitor instance to use */ + private final SimVisitor visitor; + + /** {@code non-null;} options for dex output */ + private final DexOptions dexOptions; + + /** + * Constructs an instance. + * + * @param machine {@code non-null;} machine to use when simulating + * @param method {@code non-null;} method data to use + * @param dexOptions {@code non-null;} options for dex output + */ + public Simulator(Machine machine, ConcreteMethod method, DexOptions dexOptions) { + if (machine == null) { + throw new NullPointerException("machine == null"); + } + + if (method == null) { + throw new NullPointerException("method == null"); + } + + if (dexOptions == null) { + throw new NullPointerException("dexOptions == null"); + } + + this.machine = machine; + this.code = method.getCode(); + this.method = method; + this.localVariables = method.getLocalVariables(); + this.visitor = new SimVisitor(); + this.dexOptions = dexOptions; + + // This check assumes class is initialized (accesses dexOptions). + if (method.isDefaultOrStaticInterfaceMethod()) { + checkInterfaceMethodDeclaration(method); + } + } + + /** + * Simulates the effect of executing the given basic block. This modifies + * the passed-in frame to represent the end result. + * + * @param bb {@code non-null;} the basic block + * @param frame {@code non-null;} frame to operate on + */ + public void simulate(ByteBlock bb, Frame frame) { + int end = bb.getEnd(); + + visitor.setFrame(frame); + + try { + for (int off = bb.getStart(); off < end; /*off*/) { + int length = code.parseInstruction(off, visitor); + visitor.setPreviousOffset(off); + off += length; + } + } catch (SimException ex) { + frame.annotate(ex); + throw ex; + } + } + + /** + * Simulates the effect of the instruction at the given offset, by + * making appropriate calls on the given frame. + * + * @param offset {@code offset >= 0;} offset of the instruction to simulate + * @param frame {@code non-null;} frame to operate on + * @return the length of the instruction, in bytes + */ + public int simulate(int offset, Frame frame) { + visitor.setFrame(frame); + return code.parseInstruction(offset, visitor); + } + + /** + * Constructs an "illegal top-of-stack" exception, for the stack + * manipulation opcodes. + */ + private static SimException illegalTos() { + return new SimException("stack mismatch: illegal " + + "top-of-stack for opcode"); + } + + /** + * Returns the required array type for an array load or store + * instruction, based on a given implied type and an observed + * actual array type. + * + *

The interesting cases here have to do with object arrays, + * byte[]s, boolean[]s, and + * known-nulls.

+ * + *

In the case of arrays of objects, we want to narrow the type + * to the actual array present on the stack, as long as what is + * present is an object type. Similarly, due to a quirk of the + * original bytecode representation, the instructions for dealing + * with byte[] and boolean[] are + * undifferentiated, and we aim here to return whichever one was + * actually present on the stack.

+ * + *

In the case where there is a known-null on the stack where + * an array is expected, our behavior depends on the implied type + * of the instruction. When the implied type is a reference, we + * don't attempt to infer anything, as we don't know the dimension + * of the null constant and thus any explicit inferred type could + * be wrong. When the implied type is a primitive, we fall back to + * the implied type of the instruction. Due to the quirk described + * above, this means that source code that uses + * boolean[] might get translated surprisingly -- but + * correctly -- into an instruction that specifies a + * byte[]. It will be correct, because should the + * code actually execute, it will necessarily throw a + * NullPointerException, and it won't matter what + * opcode variant is used to achieve that result.

+ * + * @param impliedType {@code non-null;} type implied by the + * instruction; is not an array type + * @param foundArrayType {@code non-null;} type found on the + * stack; is either an array type or a known-null + * @return {@code non-null;} the array type that should be + * required in this context + */ + private static Type requiredArrayTypeFor(Type impliedType, + Type foundArrayType) { + if (foundArrayType == Type.KNOWN_NULL) { + return impliedType.isReference() + ? Type.KNOWN_NULL + : impliedType.getArrayType(); + } + + if ((impliedType == Type.OBJECT) + && foundArrayType.isArray() + && foundArrayType.getComponentType().isReference()) { + return foundArrayType; + } + + if ((impliedType == Type.BYTE) + && (foundArrayType == Type.BOOLEAN_ARRAY)) { + /* + * Per above, an instruction with implied byte[] is also + * allowed to be used on boolean[]. + */ + return Type.BOOLEAN_ARRAY; + } + + return impliedType.getArrayType(); + } + + /** + * Bytecode visitor used during simulation. + */ + private class SimVisitor implements BytecodeArray.Visitor { + /** + * {@code non-null;} machine instance to use (just to avoid excessive + * cross-object field access) + */ + private final Machine machine; + + /** + * {@code null-ok;} frame to use; set with each call to + * {@link Simulator#simulate} + */ + private Frame frame; + + /** offset of the previous bytecode */ + private int previousOffset; + + /** + * Constructs an instance. + */ + public SimVisitor() { + this.machine = Simulator.this.machine; + this.frame = null; + } + + /** + * Sets the frame to act on. + * + * @param frame {@code non-null;} the frame + */ + public void setFrame(Frame frame) { + if (frame == null) { + throw new NullPointerException("frame == null"); + } + + this.frame = frame; + } + + /** {@inheritDoc} */ + @Override + public void visitInvalid(int opcode, int offset, int length) { + throw new SimException("invalid opcode " + Hex.u1(opcode)); + } + + /** {@inheritDoc} */ + @Override + public void visitNoArgs(int opcode, int offset, int length, + Type type) { + switch (opcode) { + case ByteOps.NOP: { + machine.clearArgs(); + break; + } + case ByteOps.INEG: { + machine.popArgs(frame, type); + break; + } + case ByteOps.I2L: + case ByteOps.I2F: + case ByteOps.I2D: + case ByteOps.I2B: + case ByteOps.I2C: + case ByteOps.I2S: { + machine.popArgs(frame, Type.INT); + break; + } + case ByteOps.L2I: + case ByteOps.L2F: + case ByteOps.L2D: { + machine.popArgs(frame, Type.LONG); + break; + } + case ByteOps.F2I: + case ByteOps.F2L: + case ByteOps.F2D: { + machine.popArgs(frame, Type.FLOAT); + break; + } + case ByteOps.D2I: + case ByteOps.D2L: + case ByteOps.D2F: { + machine.popArgs(frame, Type.DOUBLE); + break; + } + case ByteOps.RETURN: { + machine.clearArgs(); + checkReturnType(Type.VOID); + break; + } + case ByteOps.IRETURN: { + Type checkType = type; + if (type == Type.OBJECT) { + /* + * For an object return, use the best-known + * type of the popped value. + */ + checkType = frame.getStack().peekType(0); + } + machine.popArgs(frame, type); + checkReturnType(checkType); + break; + } + case ByteOps.POP: { + Type peekType = frame.getStack().peekType(0); + if (peekType.isCategory2()) { + throw illegalTos(); + } + machine.popArgs(frame, 1); + break; + } + case ByteOps.ARRAYLENGTH: { + Type arrayType = frame.getStack().peekType(0); + if (!arrayType.isArrayOrKnownNull()) { + fail("type mismatch: expected array type but encountered " + + arrayType.toHuman()); + } + machine.popArgs(frame, Type.OBJECT); + break; + } + case ByteOps.ATHROW: + case ByteOps.MONITORENTER: + case ByteOps.MONITOREXIT: { + machine.popArgs(frame, Type.OBJECT); + break; + } + case ByteOps.IALOAD: { + /* + * See comment on requiredArrayTypeFor() for explanation + * about what's going on here. + */ + Type foundArrayType = frame.getStack().peekType(1); + Type requiredArrayType = + requiredArrayTypeFor(type, foundArrayType); + + // Make type agree with the discovered requiredArrayType. + type = (requiredArrayType == Type.KNOWN_NULL) + ? Type.KNOWN_NULL + : requiredArrayType.getComponentType(); + + machine.popArgs(frame, requiredArrayType, Type.INT); + break; + } + case ByteOps.IADD: + case ByteOps.ISUB: + case ByteOps.IMUL: + case ByteOps.IDIV: + case ByteOps.IREM: + case ByteOps.IAND: + case ByteOps.IOR: + case ByteOps.IXOR: { + machine.popArgs(frame, type, type); + break; + } + case ByteOps.ISHL: + case ByteOps.ISHR: + case ByteOps.IUSHR: { + machine.popArgs(frame, type, Type.INT); + break; + } + case ByteOps.LCMP: { + machine.popArgs(frame, Type.LONG, Type.LONG); + break; + } + case ByteOps.FCMPL: + case ByteOps.FCMPG: { + machine.popArgs(frame, Type.FLOAT, Type.FLOAT); + break; + } + case ByteOps.DCMPL: + case ByteOps.DCMPG: { + machine.popArgs(frame, Type.DOUBLE, Type.DOUBLE); + break; + } + case ByteOps.IASTORE: { + /* + * See comment on requiredArrayTypeFor() for + * explanation about what's going on here. In + * addition to that, the category 1 vs. 2 thing + * below is to deal with the fact that, if the + * element type is category 2, we have to skip + * over one extra stack slot to find the array. + */ + ExecutionStack stack = frame.getStack(); + int peekDepth = type.isCategory1() ? 2 : 3; + Type foundArrayType = stack.peekType(peekDepth); + boolean foundArrayLocal = stack.peekLocal(peekDepth); + + Type requiredArrayType = + requiredArrayTypeFor(type, foundArrayType); + + /* + * Make type agree with the discovered requiredArrayType + * if it has local info. + */ + if (foundArrayLocal) { + type = (requiredArrayType == Type.KNOWN_NULL) + ? Type.KNOWN_NULL + : requiredArrayType.getComponentType(); + } + + machine.popArgs(frame, requiredArrayType, Type.INT, type); + break; + } + case ByteOps.POP2: + case ByteOps.DUP2: { + ExecutionStack stack = frame.getStack(); + int pattern; + + if (stack.peekType(0).isCategory2()) { + // "form 2" in vmspec-2 + machine.popArgs(frame, 1); + pattern = 0x11; + } else if (stack.peekType(1).isCategory1()) { + // "form 1" + machine.popArgs(frame, 2); + pattern = 0x2121; + } else { + throw illegalTos(); + } + + if (opcode == ByteOps.DUP2) { + machine.auxIntArg(pattern); + } + break; + } + case ByteOps.DUP: { + Type peekType = frame.getStack().peekType(0); + + if (peekType.isCategory2()) { + throw illegalTos(); + } + + machine.popArgs(frame, 1); + machine.auxIntArg(0x11); + break; + } + case ByteOps.DUP_X1: { + ExecutionStack stack = frame.getStack(); + + if (!(stack.peekType(0).isCategory1() && + stack.peekType(1).isCategory1())) { + throw illegalTos(); + } + + machine.popArgs(frame, 2); + machine.auxIntArg(0x212); + break; + } + case ByteOps.DUP_X2: { + ExecutionStack stack = frame.getStack(); + + if (stack.peekType(0).isCategory2()) { + throw illegalTos(); + } + + if (stack.peekType(1).isCategory2()) { + // "form 2" in vmspec-2 + machine.popArgs(frame, 2); + machine.auxIntArg(0x212); + } else if (stack.peekType(2).isCategory1()) { + // "form 1" + machine.popArgs(frame, 3); + machine.auxIntArg(0x3213); + } else { + throw illegalTos(); + } + break; + } + case ByteOps.DUP2_X1: { + ExecutionStack stack = frame.getStack(); + + if (stack.peekType(0).isCategory2()) { + // "form 2" in vmspec-2 + if (stack.peekType(2).isCategory2()) { + throw illegalTos(); + } + machine.popArgs(frame, 2); + machine.auxIntArg(0x212); + } else { + // "form 1" + if (stack.peekType(1).isCategory2() || + stack.peekType(2).isCategory2()) { + throw illegalTos(); + } + machine.popArgs(frame, 3); + machine.auxIntArg(0x32132); + } + break; + } + case ByteOps.DUP2_X2: { + ExecutionStack stack = frame.getStack(); + + if (stack.peekType(0).isCategory2()) { + if (stack.peekType(2).isCategory2()) { + // "form 4" in vmspec-2 + machine.popArgs(frame, 2); + machine.auxIntArg(0x212); + } else if (stack.peekType(3).isCategory1()) { + // "form 2" + machine.popArgs(frame, 3); + machine.auxIntArg(0x3213); + } else { + throw illegalTos(); + } + } else if (stack.peekType(1).isCategory1()) { + if (stack.peekType(2).isCategory2()) { + // "form 3" + machine.popArgs(frame, 3); + machine.auxIntArg(0x32132); + } else if (stack.peekType(3).isCategory1()) { + // "form 1" + machine.popArgs(frame, 4); + machine.auxIntArg(0x432143); + } else { + throw illegalTos(); + } + } else { + throw illegalTos(); + } + break; + } + case ByteOps.SWAP: { + ExecutionStack stack = frame.getStack(); + + if (!(stack.peekType(0).isCategory1() && + stack.peekType(1).isCategory1())) { + throw illegalTos(); + } + + machine.popArgs(frame, 2); + machine.auxIntArg(0x12); + break; + } + default: { + visitInvalid(opcode, offset, length); + return; + } + } + + machine.auxType(type); + machine.run(frame, offset, opcode); + } + + /** + * Checks whether the prototype is compatible with returning the + * given type, and throws if not. + * + * @param encountered {@code non-null;} the encountered return type + */ + private void checkReturnType(Type encountered) { + Type returnType = machine.getPrototype().getReturnType(); + + /* + * Check to see if the prototype's return type is + * possibly assignable from the type we encountered. This + * takes care of all the salient cases (types are the same, + * they're compatible primitive types, etc.). + */ + if (!Merger.isPossiblyAssignableFrom(returnType, encountered)) { + fail("return type mismatch: prototype " + + "indicates " + returnType.toHuman() + + ", but encountered type " + encountered.toHuman()); + } + } + + /** {@inheritDoc} */ + @Override + public void visitLocal(int opcode, int offset, int length, + int idx, Type type, int value) { + /* + * Note that the "type" parameter is always the simplest + * type based on the original opcode, e.g., "int" for + * "iload" (per se) and "Object" for "aload". So, when + * possible, we replace the type with the one indicated in + * the local variable table, though we still need to check + * to make sure it's valid for the opcode. + * + * The reason we use (offset + length) for the localOffset + * for a store is because it is only after the store that + * the local type becomes valid. On the other hand, the + * type associated with a load is valid at the start of + * the instruction. + */ + int localOffset = + (opcode == ByteOps.ISTORE) ? (offset + length) : offset; + LocalVariableList.Item local = + localVariables.pcAndIndexToLocal(localOffset, idx); + Type localType; + + if (local != null) { + localType = local.getType(); + if (localType.getBasicFrameType() != + type.getBasicFrameType()) { + // wrong type, ignore local variable info + local = null; + localType = type; + } + } else { + localType = type; + } + + switch (opcode) { + case ByteOps.ILOAD: + case ByteOps.RET: { + machine.localArg(frame, idx); + machine.localInfo(local != null); + machine.auxType(type); + break; + } + case ByteOps.ISTORE: { + LocalItem item + = (local == null) ? null : local.getLocalItem(); + machine.popArgs(frame, type); + machine.auxType(type); + machine.localTarget(idx, localType, item); + break; + } + case ByteOps.IINC: { + LocalItem item + = (local == null) ? null : local.getLocalItem(); + machine.localArg(frame, idx); + machine.localTarget(idx, localType, item); + machine.auxType(type); + machine.auxIntArg(value); + machine.auxCstArg(CstInteger.make(value)); + break; + } + default: { + visitInvalid(opcode, offset, length); + return; + } + } + + machine.run(frame, offset, opcode); + } + + /** {@inheritDoc} */ + @Override + public void visitConstant(int opcode, int offset, int length, + Constant cst, int value) { + switch (opcode) { + case ByteOps.ANEWARRAY: { + machine.popArgs(frame, Type.INT); + break; + } + case ByteOps.PUTSTATIC: { + Type fieldType = ((CstFieldRef) cst).getType(); + machine.popArgs(frame, fieldType); + break; + } + case ByteOps.GETFIELD: + case ByteOps.CHECKCAST: + case ByteOps.INSTANCEOF: { + machine.popArgs(frame, Type.OBJECT); + break; + } + case ByteOps.PUTFIELD: { + Type fieldType = ((CstFieldRef) cst).getType(); + machine.popArgs(frame, Type.OBJECT, fieldType); + break; + } + case ByteOps.INVOKEINTERFACE: + case ByteOps.INVOKEVIRTUAL: + case ByteOps.INVOKESPECIAL: + case ByteOps.INVOKESTATIC: { + /* + * Convert the interface method ref into a normal + * method ref if necessary. + */ + if (cst instanceof CstInterfaceMethodRef) { + cst = ((CstInterfaceMethodRef) cst).toMethodRef(); + checkInvokeInterfaceSupported(opcode, (CstMethodRef) cst); + } + + /* + * Check whether invoke-polymorphic is required and supported. + */ + if (cst instanceof CstMethodRef) { + CstMethodRef methodRef = (CstMethodRef) cst; + if (methodRef.isSignaturePolymorphic()) { + checkInvokeSignaturePolymorphic(opcode); + } + } + + /* + * Get the instance or static prototype, and use it to + * direct the machine. + */ + boolean staticMethod = (opcode == ByteOps.INVOKESTATIC); + Prototype prototype + = ((CstMethodRef) cst).getPrototype(staticMethod); + machine.popArgs(frame, prototype); + break; + } + case ByteOps.INVOKEDYNAMIC: { + checkInvokeDynamicSupported(opcode); + CstInvokeDynamic invokeDynamicRef = (CstInvokeDynamic) cst; + Prototype prototype = invokeDynamicRef.getPrototype(); + machine.popArgs(frame, prototype); + // Change the constant to be associated with instruction to + // a call site reference. + cst = invokeDynamicRef.addReference(); + break; + } + case ByteOps.MULTIANEWARRAY: { + /* + * The "value" here is the count of dimensions to + * create. Make a prototype of that many "int" + * types, and tell the machine to pop them. This + * isn't the most efficient way in the world to do + * this, but then again, multianewarray is pretty + * darn rare and so not worth much effort + * optimizing for. + */ + Prototype prototype = + Prototype.internInts(Type.VOID, value); + machine.popArgs(frame, prototype); + break; + } + case ByteOps.LDC: + case ByteOps.LDC_W: { + if ((cst instanceof CstMethodHandle || cst instanceof CstProtoRef)) { + checkConstMethodHandleSupported(cst); + } + machine.clearArgs(); + break; + } + default: { + machine.clearArgs(); + break; + } + } + + machine.auxIntArg(value); + machine.auxCstArg(cst); + machine.run(frame, offset, opcode); + } + + /** {@inheritDoc} */ + @Override + public void visitBranch(int opcode, int offset, int length, + int target) { + switch (opcode) { + case ByteOps.IFEQ: + case ByteOps.IFNE: + case ByteOps.IFLT: + case ByteOps.IFGE: + case ByteOps.IFGT: + case ByteOps.IFLE: { + machine.popArgs(frame, Type.INT); + break; + } + case ByteOps.IFNULL: + case ByteOps.IFNONNULL: { + machine.popArgs(frame, Type.OBJECT); + break; + } + case ByteOps.IF_ICMPEQ: + case ByteOps.IF_ICMPNE: + case ByteOps.IF_ICMPLT: + case ByteOps.IF_ICMPGE: + case ByteOps.IF_ICMPGT: + case ByteOps.IF_ICMPLE: { + machine.popArgs(frame, Type.INT, Type.INT); + break; + } + case ByteOps.IF_ACMPEQ: + case ByteOps.IF_ACMPNE: { + machine.popArgs(frame, Type.OBJECT, Type.OBJECT); + break; + } + case ByteOps.GOTO: + case ByteOps.JSR: + case ByteOps.GOTO_W: + case ByteOps.JSR_W: { + machine.clearArgs(); + break; + } + default: { + visitInvalid(opcode, offset, length); + return; + } + } + + machine.auxTargetArg(target); + machine.run(frame, offset, opcode); + } + + /** {@inheritDoc} */ + @Override + public void visitSwitch(int opcode, int offset, int length, + SwitchList cases, int padding) { + machine.popArgs(frame, Type.INT); + machine.auxIntArg(padding); + machine.auxSwitchArg(cases); + machine.run(frame, offset, opcode); + } + + /** {@inheritDoc} */ + @Override + public void visitNewarray(int offset, int length, CstType type, + ArrayList initValues) { + machine.popArgs(frame, Type.INT); + machine.auxInitValues(initValues); + machine.auxCstArg(type); + machine.run(frame, offset, ByteOps.NEWARRAY); + } + + /** {@inheritDoc} */ + @Override + public void setPreviousOffset(int offset) { + previousOffset = offset; + } + + /** {@inheritDoc} */ + @Override + public int getPreviousOffset() { + return previousOffset; + } + } + + private void checkConstMethodHandleSupported(Constant cst) throws SimException { + if (!dexOptions.apiIsSupported(DexFormat.API_CONST_METHOD_HANDLE)) { + fail(String.format("invalid constant type %s requires --min-sdk-version >= %d " + + "(currently %d)", + cst.typeName(), DexFormat.API_CONST_METHOD_HANDLE, + dexOptions.minSdkVersion)); + } + } + + private void checkInvokeDynamicSupported(int opcode) throws SimException { + if (!dexOptions.apiIsSupported(DexFormat.API_METHOD_HANDLES)) { + fail(String.format("invalid opcode %02x - invokedynamic requires " + + "--min-sdk-version >= %d (currently %d)", + opcode, DexFormat.API_METHOD_HANDLES, dexOptions.minSdkVersion)); + } + } + + private void checkInvokeInterfaceSupported(final int opcode, CstMethodRef callee) { + if (opcode == ByteOps.INVOKEINTERFACE) { + // Invoked in the tranditional way, this is fine. + return; + } + + if (dexOptions.apiIsSupported(DexFormat.API_INVOKE_INTERFACE_METHODS)) { + // Running at the officially support API level for default + // and static interface methods. + return; + } + + // + // One might expect a hard API level for invoking interface + // methods. It's either okay to have code invoking static (and + // default) interface methods or not. Reality asks to be + // prepared for a little compromise here because the + // traditional guidance to Android developers when producing a + // multi-API level DEX file is to guard the use of the newer + // feature with an API level check, e.g. + // + // int x = (android.os.Build.VERSION.SDK_INT >= 038) ? + // DoJava8Thing() : Do JavaOtherThing(); + // + // This is fine advice if the bytecodes and VM semantics never + // change. Unfortunately, changes like Java 8 support + // introduce new bytecodes and also additional semantics to + // existing bytecodes. Invoking static and default interface + // methods is one of these awkward VM transitions. + // + // Experimentally invoke-static of static interface methods + // breaks on VMs running below API level 21. Invocations of + // default interface methods may soft-fail verification but so + // long as they are not called that's okay. + // + boolean softFail = dexOptions.allowAllInterfaceMethodInvokes; + if (opcode == ByteOps.INVOKESTATIC) { + softFail &= dexOptions.apiIsSupported(DexFormat.API_INVOKE_STATIC_INTERFACE_METHODS); + } else { + assert (opcode == ByteOps.INVOKESPECIAL) || (opcode == ByteOps.INVOKEVIRTUAL); + } + + // Running below the officially supported API level. Fail hard + // unless the user has explicitly allowed this with + // "--allow-all-interface-method-invokes". + String invokeKind = (opcode == ByteOps.INVOKESTATIC) ? "static" : "default"; + if (softFail) { + // The code we are warning about here should have an API check + // that protects it being used on API version < API_INVOKE_INTERFACE_METHODS. + String reason = + String.format( + "invoking a %s interface method %s.%s strictly requires " + + "--min-sdk-version >= %d (experimental at current API level %d)", + invokeKind, callee.getDefiningClass().toHuman(), callee.getNat().toHuman(), + DexFormat.API_INVOKE_INTERFACE_METHODS, dexOptions.minSdkVersion); + warn(reason); + } else { + String reason = + String.format( + "invoking a %s interface method %s.%s strictly requires " + + "--min-sdk-version >= %d (blocked at current API level %d)", + invokeKind, callee.getDefiningClass().toHuman(), callee.getNat().toHuman(), + DexFormat.API_INVOKE_INTERFACE_METHODS, dexOptions.minSdkVersion); + fail(reason); + } + } + + private void checkInterfaceMethodDeclaration(ConcreteMethod declaredMethod) { + if (!dexOptions.apiIsSupported(DexFormat.API_DEFINE_INTERFACE_METHODS)) { + String reason + = String.format( + "defining a %s interface method requires --min-sdk-version >= %d (currently %d)" + + " for interface methods: %s.%s", + declaredMethod.isStaticMethod() ? "static" : "default", + DexFormat.API_DEFINE_INTERFACE_METHODS, dexOptions.minSdkVersion, + declaredMethod.getDefiningClass().toHuman(), declaredMethod.getNat().toHuman()); + warn(reason); + } + } + + private void checkInvokeSignaturePolymorphic(final int opcode) { + if (!dexOptions.apiIsSupported(DexFormat.API_METHOD_HANDLES)) { + fail(String.format( + "invoking a signature-polymorphic requires --min-sdk-version >= %d (currently %d)", + DexFormat.API_METHOD_HANDLES, dexOptions.minSdkVersion)); + } else if (opcode != ByteOps.INVOKEVIRTUAL) { + fail("Unsupported signature polymorphic invocation (" + ByteOps.opName(opcode) + ")"); + } + } + + private void fail(String reason) { + String message = String.format("ERROR in %s.%s: %s", method.getDefiningClass().toHuman(), + method.getNat().toHuman(), reason); + throw new SimException(message); + } + + private void warn(String reason) { + String warning = String.format("WARNING in %s.%s: %s", method.getDefiningClass().toHuman(), + method.getNat().toHuman(), reason); + dexOptions.err.println(warning); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/SwitchList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/SwitchList.java new file mode 100644 index 00000000..1a6a0ce5 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/SwitchList.java @@ -0,0 +1,193 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.util.IntList; +import external.com.android.dx.util.MutabilityControl; + +/** + * List of (value, target) mappings representing the choices of a + * {@code tableswitch} or {@code lookupswitch} instruction. It + * also holds the default target for the switch. + */ +public final class SwitchList extends MutabilityControl { + /** {@code non-null;} list of test values */ + private final IntList values; + + /** + * {@code non-null;} list of targets corresponding to the test values; there + * is always one extra element in the target list, to hold the + * default target + */ + private final IntList targets; + + /** ultimate size of the list */ + private int size; + + /** + * Constructs an instance. + * + * @param size {@code >= 0;} the number of elements to be in the table + */ + public SwitchList(int size) { + super(true); + this.values = new IntList(size); + this.targets = new IntList(size + 1); + this.size = size; + } + + /** {@inheritDoc} */ + @Override + public void setImmutable() { + values.setImmutable(); + targets.setImmutable(); + super.setImmutable(); + } + + /** + * Gets the size of the list. + * + * @return {@code >= 0;} the list size + */ + public int size() { + return size; + } + + /** + * Gets the indicated test value. + * + * @param n {@code >= 0;}, < size(); which index + * @return the test value + */ + public int getValue(int n) { + return values.get(n); + } + + /** + * Gets the indicated target. Asking for the target at {@code size()} + * returns the default target. + * + * @param n {@code >= 0, <= size();} which index + * @return {@code >= 0;} the target + */ + public int getTarget(int n) { + return targets.get(n); + } + + /** + * Gets the default target. This is just a shorthand for + * {@code getTarget(size())}. + * + * @return {@code >= 0;} the default target + */ + public int getDefaultTarget() { + return targets.get(size); + } + + /** + * Gets the list of all targets. This includes one extra element at the + * end of the list, which holds the default target. + * + * @return {@code non-null;} the target list + */ + public IntList getTargets() { + return targets; + } + + /** + * Gets the list of all case values. + * + * @return {@code non-null;} the case value list + */ + public IntList getValues() { + return values; + } + + /** + * Sets the default target. It is only valid to call this method + * when all the non-default elements have been set. + * + * @param target {@code >= 0;} the absolute (not relative) default target + * address + */ + public void setDefaultTarget(int target) { + throwIfImmutable(); + + if (target < 0) { + throw new IllegalArgumentException("target < 0"); + } + + if (targets.size() != size) { + throw new RuntimeException("non-default elements not all set"); + } + + targets.add(target); + } + + /** + * Adds the given item. + * + * @param value the test value + * @param target {@code >= 0;} the absolute (not relative) target address + */ + public void add(int value, int target) { + throwIfImmutable(); + + if (target < 0) { + throw new IllegalArgumentException("target < 0"); + } + + values.add(value); + targets.add(target); + } + + /** + * Shrinks this instance if possible, removing test elements that + * refer to the default target. This is only valid after the instance + * is fully populated, including the default target (naturally). + */ + public void removeSuperfluousDefaults() { + throwIfImmutable(); + + int sz = size; + + if (sz != (targets.size() - 1)) { + throw new IllegalArgumentException("incomplete instance"); + } + + int defaultTarget = targets.get(sz); + int at = 0; + + for (int i = 0; i < sz; i++) { + int target = targets.get(i); + if (target != defaultTarget) { + if (i != at) { + targets.set(at, target); + values.set(at, values.get(i)); + } + at++; + } + } + + if (at != sz) { + values.shrink(at); + targets.set(at, defaultTarget); + targets.shrink(at + 1); + size = at; + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/ValueAwareMachine.java b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ValueAwareMachine.java new file mode 100644 index 00000000..8a600613 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/ValueAwareMachine.java @@ -0,0 +1,210 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.code; + +import external.com.android.dx.rop.cst.CstCallSiteRef; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.Prototype; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.rop.type.TypeBearer; +import external.com.android.dx.util.Hex; + +/** + * {@link Machine} which keeps track of known values but does not do + * smart/realistic reference type calculations. + */ +public class ValueAwareMachine extends BaseMachine { + /** + * Constructs an instance. + * + * @param prototype {@code non-null;} the prototype for the associated + * method + */ + public ValueAwareMachine(Prototype prototype) { + super(prototype); + } + + /** {@inheritDoc} */ + @Override + public void run(Frame frame, int offset, int opcode) { + switch (opcode) { + case ByteOps.NOP: + case ByteOps.IASTORE: + case ByteOps.POP: + case ByteOps.POP2: + case ByteOps.IFEQ: + case ByteOps.IFNE: + case ByteOps.IFLT: + case ByteOps.IFGE: + case ByteOps.IFGT: + case ByteOps.IFLE: + case ByteOps.IF_ICMPEQ: + case ByteOps.IF_ICMPNE: + case ByteOps.IF_ICMPLT: + case ByteOps.IF_ICMPGE: + case ByteOps.IF_ICMPGT: + case ByteOps.IF_ICMPLE: + case ByteOps.IF_ACMPEQ: + case ByteOps.IF_ACMPNE: + case ByteOps.GOTO: + case ByteOps.RET: + case ByteOps.LOOKUPSWITCH: + case ByteOps.IRETURN: + case ByteOps.RETURN: + case ByteOps.PUTSTATIC: + case ByteOps.PUTFIELD: + case ByteOps.ATHROW: + case ByteOps.MONITORENTER: + case ByteOps.MONITOREXIT: + case ByteOps.IFNULL: + case ByteOps.IFNONNULL: { + // Nothing to do for these ops in this class. + clearResult(); + break; + } + case ByteOps.LDC: + case ByteOps.LDC2_W: { + setResult((TypeBearer) getAuxCst()); + break; + } + case ByteOps.ILOAD: + case ByteOps.ISTORE: { + setResult(arg(0)); + break; + } + case ByteOps.IALOAD: + case ByteOps.IADD: + case ByteOps.ISUB: + case ByteOps.IMUL: + case ByteOps.IDIV: + case ByteOps.IREM: + case ByteOps.INEG: + case ByteOps.ISHL: + case ByteOps.ISHR: + case ByteOps.IUSHR: + case ByteOps.IAND: + case ByteOps.IOR: + case ByteOps.IXOR: + case ByteOps.IINC: + case ByteOps.I2L: + case ByteOps.I2F: + case ByteOps.I2D: + case ByteOps.L2I: + case ByteOps.L2F: + case ByteOps.L2D: + case ByteOps.F2I: + case ByteOps.F2L: + case ByteOps.F2D: + case ByteOps.D2I: + case ByteOps.D2L: + case ByteOps.D2F: + case ByteOps.I2B: + case ByteOps.I2C: + case ByteOps.I2S: + case ByteOps.LCMP: + case ByteOps.FCMPL: + case ByteOps.FCMPG: + case ByteOps.DCMPL: + case ByteOps.DCMPG: + case ByteOps.ARRAYLENGTH: { + setResult(getAuxType()); + break; + } + case ByteOps.DUP: + case ByteOps.DUP_X1: + case ByteOps.DUP_X2: + case ByteOps.DUP2: + case ByteOps.DUP2_X1: + case ByteOps.DUP2_X2: + case ByteOps.SWAP: { + clearResult(); + for (int pattern = getAuxInt(); pattern != 0; pattern >>= 4) { + int which = (pattern & 0x0f) - 1; + addResult(arg(which)); + } + break; + } + + case ByteOps.JSR: { + setResult(new ReturnAddress(getAuxTarget())); + break; + } + case ByteOps.GETSTATIC: + case ByteOps.GETFIELD: + case ByteOps.INVOKEVIRTUAL: + case ByteOps.INVOKESTATIC: + case ByteOps.INVOKEINTERFACE: { + Type type = ((TypeBearer) getAuxCst()).getType(); + if (type == Type.VOID) { + clearResult(); + } else { + setResult(type); + } + break; + } + case ByteOps.INVOKESPECIAL: { + Type thisType = arg(0).getType(); + if (thisType.isUninitialized()) { + frame.makeInitialized(thisType); + } + Type type = ((TypeBearer) getAuxCst()).getType(); + if (type == Type.VOID) { + clearResult(); + } else { + setResult(type); + } + break; + } + case ByteOps.INVOKEDYNAMIC: { + Type type = ((CstCallSiteRef) getAuxCst()).getReturnType(); + if (type == Type.VOID) { + clearResult(); + } else { + setResult(type); + } + break; + } + case ByteOps.NEW: { + Type type = ((CstType) getAuxCst()).getClassType(); + setResult(type.asUninitialized(offset)); + break; + } + case ByteOps.NEWARRAY: + case ByteOps.CHECKCAST: + case ByteOps.MULTIANEWARRAY: { + Type type = ((CstType) getAuxCst()).getClassType(); + setResult(type); + break; + } + case ByteOps.ANEWARRAY: { + Type type = ((CstType) getAuxCst()).getClassType(); + setResult(type.getArrayType()); + break; + } + case ByteOps.INSTANCEOF: { + setResult(Type.INT); + break; + } + default: { + throw new RuntimeException("shouldn't happen: " + + Hex.u1(opcode)); + } + } + + storeResults(frame); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/code/package.html b/dalvikdx/src/main/java/external/com/android/dx/cf/code/package.html new file mode 100644 index 00000000..decc6836 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/code/package.html @@ -0,0 +1,10 @@ + +

Implementation of classes having to do with Java simulation, such as +is needed for verification or stack-to-register conversion.

+ +

PACKAGES USED: +

    +
  • external.com.android.dx.rop.pool
  • +
  • external.com.android.dx.util
  • +
+ diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/cst/ConstantPoolParser.java b/dalvikdx/src/main/java/external/com/android/dx/cf/cst/ConstantPoolParser.java new file mode 100644 index 00000000..23fd2447 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/cst/ConstantPoolParser.java @@ -0,0 +1,450 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.cst; + +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_Class; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_Double; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_Fieldref; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_Float; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_Integer; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_InterfaceMethodref; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_InvokeDynamic; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_Long; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_MethodHandle; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_MethodType; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_Methodref; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_NameAndType; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_String; +import static external.com.android.dx.cf.cst.ConstantTags.CONSTANT_Utf8; +import external.com.android.dx.cf.iface.ParseException; +import external.com.android.dx.cf.iface.ParseObserver; +import external.com.android.dx.rop.cst.Constant; +import external.com.android.dx.rop.cst.CstDouble; +import external.com.android.dx.rop.cst.CstFieldRef; +import external.com.android.dx.rop.cst.CstFloat; +import external.com.android.dx.rop.cst.CstInteger; +import external.com.android.dx.rop.cst.CstInterfaceMethodRef; +import external.com.android.dx.rop.cst.CstInvokeDynamic; +import external.com.android.dx.rop.cst.CstLong; +import external.com.android.dx.rop.cst.CstMethodHandle; +import external.com.android.dx.rop.cst.CstMethodRef; +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstProtoRef; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.cst.StdConstantPool; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.util.ByteArray; +import external.com.android.dx.util.Hex; +import java.util.BitSet; + +/** + * Parser for a constant pool embedded in a class file. + */ +public final class ConstantPoolParser { + /** {@code non-null;} the bytes of the constant pool */ + private final ByteArray bytes; + + /** {@code non-null;} actual parsed constant pool contents */ + private final StdConstantPool pool; + + /** {@code non-null;} byte offsets to each cst */ + private final int[] offsets; + + /** + * -1 || >= 10; the end offset of this constant pool in the + * {@code byte[]} which it came from or {@code -1} if not + * yet parsed + */ + private int endOffset; + + /** {@code null-ok;} parse observer, if any */ + private ParseObserver observer; + + /** + * Constructs an instance. + * + * @param bytes {@code non-null;} the bytes of the file + */ + public ConstantPoolParser(ByteArray bytes) { + int size = bytes.getUnsignedShort(8); // constant_pool_count + + this.bytes = bytes; + this.pool = new StdConstantPool(size); + this.offsets = new int[size]; + this.endOffset = -1; + } + + /** + * Sets the parse observer for this instance. + * + * @param observer {@code null-ok;} the observer + */ + public void setObserver(ParseObserver observer) { + this.observer = observer; + } + + /** + * Gets the end offset of this constant pool in the {@code byte[]} + * which it came from. + * + * @return {@code >= 10;} the end offset + */ + public int getEndOffset() { + parseIfNecessary(); + return endOffset; + } + + /** + * Gets the actual constant pool. + * + * @return {@code non-null;} the constant pool + */ + public StdConstantPool getPool() { + parseIfNecessary(); + return pool; + } + + /** + * Runs {@link #parse} if it has not yet been run successfully. + */ + private void parseIfNecessary() { + if (endOffset < 0) { + parse(); + } + } + + /** + * Does the actual parsing. + */ + private void parse() { + determineOffsets(); + + if (observer != null) { + observer.parsed(bytes, 8, 2, + "constant_pool_count: " + Hex.u2(offsets.length)); + observer.parsed(bytes, 10, 0, "\nconstant_pool:"); + observer.changeIndent(1); + } + + /* + * Track the constant value's original string type. True if constants[i] was + * a CONSTANT_Utf8, false for any other type including CONSTANT_string. + */ + BitSet wasUtf8 = new BitSet(offsets.length); + + for (int i = 1; i < offsets.length; i++) { + int offset = offsets[i]; + if ((offset != 0) && (pool.getOrNull(i) == null)) { + parse0(i, wasUtf8); + } + } + + if (observer != null) { + for (int i = 1; i < offsets.length; i++) { + Constant cst = pool.getOrNull(i); + if (cst == null) { + continue; + } + int offset = offsets[i]; + int nextOffset = endOffset; + for (int j = i + 1; j < offsets.length; j++) { + int off = offsets[j]; + if (off != 0) { + nextOffset = off; + break; + } + } + String human = wasUtf8.get(i) + ? Hex.u2(i) + ": utf8{\"" + cst.toHuman() + "\"}" + : Hex.u2(i) + ": " + cst.toString(); + observer.parsed(bytes, offset, nextOffset - offset, human); + } + + observer.changeIndent(-1); + observer.parsed(bytes, endOffset, 0, "end constant_pool"); + } + } + + /** + * Populates {@link #offsets} and also completely parse utf8 constants. + */ + private void determineOffsets() { + int at = 10; // offset from the start of the file to the first cst + int lastCategory; + + for (int i = 1; i < offsets.length; i += lastCategory) { + offsets[i] = at; + int tag = bytes.getUnsignedByte(at); + try { + switch (tag) { + case CONSTANT_Integer: + case CONSTANT_Float: + case CONSTANT_Fieldref: + case CONSTANT_Methodref: + case CONSTANT_InterfaceMethodref: + case CONSTANT_NameAndType: { + lastCategory = 1; + at += 5; + break; + } + case CONSTANT_Long: + case CONSTANT_Double: { + lastCategory = 2; + at += 9; + break; + } + case CONSTANT_Class: + case CONSTANT_String: { + lastCategory = 1; + at += 3; + break; + } + case CONSTANT_Utf8: { + lastCategory = 1; + at += bytes.getUnsignedShort(at + 1) + 3; + break; + } + case CONSTANT_MethodHandle: { + lastCategory = 1; + at += 4; + break; + } + case CONSTANT_MethodType: { + lastCategory = 1; + at += 3; + break; + } + case CONSTANT_InvokeDynamic: { + lastCategory = 1; + at += 5; + break; + } + default: { + throw new ParseException("unknown tag byte: " + Hex.u1(tag)); + } + } + } catch (ParseException ex) { + ex.addContext("...while preparsing cst " + Hex.u2(i) + " at offset " + Hex.u4(at)); + throw ex; + } + } + + endOffset = at; + } + + /** + * Parses the constant for the given index if it hasn't already been + * parsed, also storing it in the constant pool. This will also + * have the side effect of parsing any entries the indicated one + * depends on. + * + * @param idx which constant + * @return {@code non-null;} the parsed constant + */ + private Constant parse0(int idx, BitSet wasUtf8) { + Constant cst = pool.getOrNull(idx); + if (cst != null) { + return cst; + } + + int at = offsets[idx]; + + try { + int tag = bytes.getUnsignedByte(at); + switch (tag) { + case CONSTANT_Utf8: { + cst = parseUtf8(at); + wasUtf8.set(idx); + break; + } + case CONSTANT_Integer: { + int value = bytes.getInt(at + 1); + cst = CstInteger.make(value); + break; + } + case CONSTANT_Float: { + int bits = bytes.getInt(at + 1); + cst = CstFloat.make(bits); + break; + } + case CONSTANT_Long: { + long value = bytes.getLong(at + 1); + cst = CstLong.make(value); + break; + } + case CONSTANT_Double: { + long bits = bytes.getLong(at + 1); + cst = CstDouble.make(bits); + break; + } + case CONSTANT_Class: { + int nameIndex = bytes.getUnsignedShort(at + 1); + CstString name = (CstString) parse0(nameIndex, wasUtf8); + cst = new CstType(Type.internClassName(name.getString())); + break; + } + case CONSTANT_String: { + int stringIndex = bytes.getUnsignedShort(at + 1); + cst = parse0(stringIndex, wasUtf8); + break; + } + case CONSTANT_Fieldref: { + int classIndex = bytes.getUnsignedShort(at + 1); + CstType type = (CstType) parse0(classIndex, wasUtf8); + int natIndex = bytes.getUnsignedShort(at + 3); + CstNat nat = (CstNat) parse0(natIndex, wasUtf8); + cst = new CstFieldRef(type, nat); + break; + } + case CONSTANT_Methodref: { + int classIndex = bytes.getUnsignedShort(at + 1); + CstType type = (CstType) parse0(classIndex, wasUtf8); + int natIndex = bytes.getUnsignedShort(at + 3); + CstNat nat = (CstNat) parse0(natIndex, wasUtf8); + cst = new CstMethodRef(type, nat); + break; + } + case CONSTANT_InterfaceMethodref: { + int classIndex = bytes.getUnsignedShort(at + 1); + CstType type = (CstType) parse0(classIndex, wasUtf8); + int natIndex = bytes.getUnsignedShort(at + 3); + CstNat nat = (CstNat) parse0(natIndex, wasUtf8); + cst = new CstInterfaceMethodRef(type, nat); + break; + } + case CONSTANT_NameAndType: { + int nameIndex = bytes.getUnsignedShort(at + 1); + CstString name = (CstString) parse0(nameIndex, wasUtf8); + int descriptorIndex = bytes.getUnsignedShort(at + 3); + CstString descriptor = (CstString) parse0(descriptorIndex, wasUtf8); + cst = new CstNat(name, descriptor); + break; + } + case CONSTANT_MethodHandle: { + final int kind = bytes.getUnsignedByte(at + 1); + final int constantIndex = bytes.getUnsignedShort(at + 2); + final Constant ref; + switch (kind) { + case MethodHandleKind.REF_getField: + case MethodHandleKind.REF_getStatic: + case MethodHandleKind.REF_putField: + case MethodHandleKind.REF_putStatic: + ref = (CstFieldRef) parse0(constantIndex, wasUtf8); + break; + case MethodHandleKind.REF_invokeVirtual: + case MethodHandleKind.REF_newInvokeSpecial: + ref = (CstMethodRef) parse0(constantIndex, wasUtf8); + break; + case MethodHandleKind.REF_invokeStatic: + case MethodHandleKind.REF_invokeSpecial: + ref = parse0(constantIndex, wasUtf8); + if (!(ref instanceof CstMethodRef + || ref instanceof CstInterfaceMethodRef)) { + throw new ParseException( + "Unsupported ref constant type for MethodHandle " + + ref.getClass()); + } + break; + case MethodHandleKind.REF_invokeInterface: + ref = (CstInterfaceMethodRef) parse0(constantIndex, wasUtf8); + break; + default: + throw new ParseException("Unsupported MethodHandle kind: " + kind); + } + + final int methodHandleType = getMethodHandleTypeForKind(kind); + cst = CstMethodHandle.make(methodHandleType, ref); + break; + } + case CONSTANT_MethodType: { + int descriptorIndex = bytes.getUnsignedShort(at + 1); + CstString descriptor = (CstString) parse0(descriptorIndex, wasUtf8); + cst = CstProtoRef.make(descriptor); + break; + } + case CONSTANT_InvokeDynamic: { + int bootstrapMethodIndex = bytes.getUnsignedShort(at + 1); + int natIndex = bytes.getUnsignedShort(at + 3); + CstNat nat = (CstNat) parse0(natIndex, wasUtf8); + cst = CstInvokeDynamic.make(bootstrapMethodIndex, nat); + break; + } + default: { + throw new ParseException("unknown tag byte: " + Hex.u1(tag)); + } + } + } catch (ParseException ex) { + ex.addContext("...while parsing cst " + Hex.u2(idx) + + " at offset " + Hex.u4(at)); + throw ex; + } catch (RuntimeException ex) { + ParseException pe = new ParseException(ex); + pe.addContext("...while parsing cst " + Hex.u2(idx) + + " at offset " + Hex.u4(at)); + throw pe; + } + + pool.set(idx, cst); + return cst; + } + + /** + * Parses a utf8 constant. + * + * @param at offset to the start of the constant (where the tag byte is) + * @return {@code non-null;} the parsed value + */ + private CstString parseUtf8(int at) { + int length = bytes.getUnsignedShort(at + 1); + + at += 3; // Skip to the data. + + ByteArray ubytes = bytes.slice(at, at + length); + + try { + return new CstString(ubytes); + } catch (IllegalArgumentException ex) { + // Translate the exception + throw new ParseException(ex); + } + } + + private static int getMethodHandleTypeForKind(int kind) { + switch (kind) { + case MethodHandleKind.REF_getField: + return CstMethodHandle.METHOD_HANDLE_TYPE_INSTANCE_GET; + case MethodHandleKind.REF_getStatic: + return CstMethodHandle.METHOD_HANDLE_TYPE_STATIC_GET; + case MethodHandleKind.REF_putField: + return CstMethodHandle.METHOD_HANDLE_TYPE_INSTANCE_PUT; + case MethodHandleKind.REF_putStatic: + return CstMethodHandle.METHOD_HANDLE_TYPE_STATIC_PUT; + case MethodHandleKind.REF_invokeVirtual: + return CstMethodHandle.METHOD_HANDLE_TYPE_INVOKE_INSTANCE; + case MethodHandleKind.REF_invokeStatic: + return CstMethodHandle.METHOD_HANDLE_TYPE_INVOKE_STATIC; + case MethodHandleKind.REF_invokeSpecial: + return CstMethodHandle.METHOD_HANDLE_TYPE_INVOKE_DIRECT; + case MethodHandleKind.REF_newInvokeSpecial: + return CstMethodHandle.METHOD_HANDLE_TYPE_INVOKE_CONSTRUCTOR; + case MethodHandleKind.REF_invokeInterface: + return CstMethodHandle.METHOD_HANDLE_TYPE_INVOKE_INTERFACE; + } + throw new IllegalArgumentException("invalid kind: " + kind); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/cst/ConstantTags.java b/dalvikdx/src/main/java/external/com/android/dx/cf/cst/ConstantTags.java new file mode 100644 index 00000000..39ed96e1 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/cst/ConstantTags.java @@ -0,0 +1,64 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.cst; + +/** + * Tags for constant pool constants. + */ +public interface ConstantTags { + /** tag for a {@code CONSTANT_Utf8_info} */ + int CONSTANT_Utf8 = 1; + + /** tag for a {@code CONSTANT_Integer_info} */ + int CONSTANT_Integer = 3; + + /** tag for a {@code CONSTANT_Float_info} */ + int CONSTANT_Float = 4; + + /** tag for a {@code CONSTANT_Long_info} */ + int CONSTANT_Long = 5; + + /** tag for a {@code CONSTANT_Double_info} */ + int CONSTANT_Double = 6; + + /** tag for a {@code CONSTANT_Class_info} */ + int CONSTANT_Class = 7; + + /** tag for a {@code CONSTANT_String_info} */ + int CONSTANT_String = 8; + + /** tag for a {@code CONSTANT_Fieldref_info} */ + int CONSTANT_Fieldref = 9; + + /** tag for a {@code CONSTANT_Methodref_info} */ + int CONSTANT_Methodref = 10; + + /** tag for a {@code CONSTANT_InterfaceMethodref_info} */ + int CONSTANT_InterfaceMethodref = 11; + + /** tag for a {@code CONSTANT_NameAndType_info} */ + int CONSTANT_NameAndType = 12; + + /** tag for a {@code CONSTANT_MethodHandle} */ + int CONSTANT_MethodHandle = 15; + + /** tag for a {@code CONSTANT_MethodType} */ + int CONSTANT_MethodType = 16; + + /** tag for a {@code CONSTANT_InvokeDynamic} */ + int CONSTANT_InvokeDynamic = 18; +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/cst/MethodHandleKind.java b/dalvikdx/src/main/java/external/com/android/dx/cf/cst/MethodHandleKind.java new file mode 100644 index 00000000..ea99c034 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/cst/MethodHandleKind.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 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. + */ + +package external.com.android.dx.cf.cst; + +/** + * Method Handle kinds for {@code CONSTANT_MethodHandle_info} constants. + */ +public interface MethodHandleKind { + /** A method handle that gets an instance field. */ + int REF_getField = 1; + + /** A method handle that gets a static field. */ + int REF_getStatic = 2; + + /** A method handle that sets an instance field. */ + int REF_putField = 3; + + /** A method handle that sets a static field. */ + int REF_putStatic = 4; + + /** A method handle for {@code invokevirtual}. */ + int REF_invokeVirtual = 5; + + /** A method handle for {@code invokestatic}. */ + int REF_invokeStatic = 6; + + /** A method handle for {@code invokespecial}. */ + int REF_invokeSpecial = 7; + + /** A method handle for invoking a constructor. */ + int REF_newInvokeSpecial = 8; + + /** A method handle for {@code invokeinterface}. */ + int REF_invokeInterface = 9; +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/direct/AnnotationParser.java b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/AnnotationParser.java new file mode 100644 index 00000000..e6aa7f73 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/AnnotationParser.java @@ -0,0 +1,470 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.direct; + +import external.com.android.dx.cf.iface.ParseException; +import external.com.android.dx.cf.iface.ParseObserver; +import external.com.android.dx.rop.annotation.Annotation; +import external.com.android.dx.rop.annotation.AnnotationVisibility; +import external.com.android.dx.rop.annotation.Annotations; +import external.com.android.dx.rop.annotation.AnnotationsList; +import external.com.android.dx.rop.annotation.NameValuePair; +import external.com.android.dx.rop.cst.Constant; +import external.com.android.dx.rop.cst.ConstantPool; +import external.com.android.dx.rop.cst.CstAnnotation; +import external.com.android.dx.rop.cst.CstArray; +import external.com.android.dx.rop.cst.CstBoolean; +import external.com.android.dx.rop.cst.CstByte; +import external.com.android.dx.rop.cst.CstChar; +import external.com.android.dx.rop.cst.CstDouble; +import external.com.android.dx.rop.cst.CstEnumRef; +import external.com.android.dx.rop.cst.CstFloat; +import external.com.android.dx.rop.cst.CstInteger; +import external.com.android.dx.rop.cst.CstLong; +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstShort; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.util.ByteArray; +import external.com.android.dx.util.Hex; +import java.io.IOException; + +/** + * Parser for annotations. + */ +public final class AnnotationParser { + /** {@code non-null;} class file being parsed */ + private final DirectClassFile cf; + + /** {@code non-null;} constant pool to use */ + private final ConstantPool pool; + + /** {@code non-null;} bytes of the attribute data */ + private final ByteArray bytes; + + /** {@code null-ok;} parse observer, if any */ + private final ParseObserver observer; + + /** {@code non-null;} input stream to parse from */ + private final ByteArray.MyDataInputStream input; + + /** + * {@code non-null;} cursor for use when informing the observer of what + * was parsed + */ + private int parseCursor; + + /** + * Constructs an instance. + * + * @param cf {@code non-null;} class file to parse from + * @param offset {@code >= 0;} offset into the class file data to parse at + * @param length {@code >= 0;} number of bytes left in the attribute data + * @param observer {@code null-ok;} parse observer to notify, if any + */ + public AnnotationParser(DirectClassFile cf, int offset, int length, + ParseObserver observer) { + if (cf == null) { + throw new NullPointerException("cf == null"); + } + + this.cf = cf; + this.pool = cf.getConstantPool(); + this.observer = observer; + this.bytes = cf.getBytes().slice(offset, offset + length); + this.input = bytes.makeDataInputStream(); + this.parseCursor = 0; + } + + /** + * Parses an annotation value ({@code element_value}) attribute. + * + * @return {@code non-null;} the parsed constant value + */ + public Constant parseValueAttribute() { + Constant result; + + try { + result = parseValue(); + + if (input.available() != 0) { + throw new ParseException("extra data in attribute"); + } + } catch (IOException ex) { + // ByteArray.MyDataInputStream should never throw. + throw new RuntimeException("shouldn't happen", ex); + } + + return result; + } + + /** + * Parses a parameter annotation attribute. + * + * @param visibility {@code non-null;} visibility of the parsed annotations + * @return {@code non-null;} the parsed list of lists of annotations + */ + public AnnotationsList parseParameterAttribute( + AnnotationVisibility visibility) { + AnnotationsList result; + + try { + result = parseAnnotationsList(visibility); + + if (input.available() != 0) { + throw new ParseException("extra data in attribute"); + } + } catch (IOException ex) { + // ByteArray.MyDataInputStream should never throw. + throw new RuntimeException("shouldn't happen", ex); + } + + return result; + } + + /** + * Parses an annotation attribute, per se. + * + * @param visibility {@code non-null;} visibility of the parsed annotations + * @return {@code non-null;} the list of annotations read from the attribute + * data + */ + public Annotations parseAnnotationAttribute( + AnnotationVisibility visibility) { + Annotations result; + + try { + result = parseAnnotations(visibility); + + if (input.available() != 0) { + throw new ParseException("extra data in attribute"); + } + } catch (IOException ex) { + // ByteArray.MyDataInputStream should never throw. + throw new RuntimeException("shouldn't happen", ex); + } + + return result; + } + + /** + * Parses a list of annotation lists. + * + * @param visibility {@code non-null;} visibility of the parsed annotations + * @return {@code non-null;} the list of annotation lists read from the attribute + * data + */ + private AnnotationsList parseAnnotationsList( + AnnotationVisibility visibility) throws IOException { + int count = input.readUnsignedByte(); + + if (observer != null) { + parsed(1, "num_parameters: " + Hex.u1(count)); + } + + AnnotationsList outerList = new AnnotationsList(count); + + for (int i = 0; i < count; i++) { + if (observer != null) { + parsed(0, "parameter_annotations[" + i + "]:"); + changeIndent(1); + } + + Annotations annotations = parseAnnotations(visibility); + outerList.set(i, annotations); + + if (observer != null) { + observer.changeIndent(-1); + } + } + + outerList.setImmutable(); + return outerList; + } + + /** + * Parses an annotation list. + * + * @param visibility {@code non-null;} visibility of the parsed annotations + * @return {@code non-null;} the list of annotations read from the attribute + * data + */ + private Annotations parseAnnotations(AnnotationVisibility visibility) + throws IOException { + int count = input.readUnsignedShort(); + + if (observer != null) { + parsed(2, "num_annotations: " + Hex.u2(count)); + } + + Annotations annotations = new Annotations(); + + for (int i = 0; i < count; i++) { + if (observer != null) { + parsed(0, "annotations[" + i + "]:"); + changeIndent(1); + } + + Annotation annotation = parseAnnotation(visibility); + annotations.add(annotation); + + if (observer != null) { + observer.changeIndent(-1); + } + } + + annotations.setImmutable(); + return annotations; + } + + /** + * Parses a single annotation. + * + * @param visibility {@code non-null;} visibility of the parsed annotation + * @return {@code non-null;} the parsed annotation + */ + private Annotation parseAnnotation(AnnotationVisibility visibility) + throws IOException { + requireLength(4); + + int typeIndex = input.readUnsignedShort(); + int numElements = input.readUnsignedShort(); + CstString typeString = (CstString) pool.get(typeIndex); + CstType type = new CstType(Type.intern(typeString.getString())); + + if (observer != null) { + parsed(2, "type: " + type.toHuman()); + parsed(2, "num_elements: " + numElements); + } + + Annotation annotation = new Annotation(type, visibility); + + for (int i = 0; i < numElements; i++) { + if (observer != null) { + parsed(0, "elements[" + i + "]:"); + changeIndent(1); + } + + NameValuePair element = parseElement(); + annotation.add(element); + + if (observer != null) { + changeIndent(-1); + } + } + + annotation.setImmutable(); + return annotation; + } + + /** + * Parses a {@link NameValuePair}. + * + * @return {@code non-null;} the parsed element + */ + private NameValuePair parseElement() throws IOException { + requireLength(5); + + int elementNameIndex = input.readUnsignedShort(); + CstString elementName = (CstString) pool.get(elementNameIndex); + + if (observer != null) { + parsed(2, "element_name: " + elementName.toHuman()); + parsed(0, "value: "); + changeIndent(1); + } + + Constant value = parseValue(); + + if (observer != null) { + changeIndent(-1); + } + + return new NameValuePair(elementName, value); + } + + /** + * Parses an annotation value. + * + * @return {@code non-null;} the parsed value + */ + private Constant parseValue() throws IOException { + int tag = input.readUnsignedByte(); + + if (observer != null) { + CstString humanTag = new CstString(Character.toString((char) tag)); + parsed(1, "tag: " + humanTag.toQuoted()); + } + + switch (tag) { + case 'B': { + CstInteger value = (CstInteger) parseConstant(); + return CstByte.make(value.getValue()); + } + case 'C': { + CstInteger value = (CstInteger) parseConstant(); + int intValue = value.getValue(); + return CstChar.make(value.getValue()); + } + case 'D': { + CstDouble value = (CstDouble) parseConstant(); + return value; + } + case 'F': { + CstFloat value = (CstFloat) parseConstant(); + return value; + } + case 'I': { + CstInteger value = (CstInteger) parseConstant(); + return value; + } + case 'J': { + CstLong value = (CstLong) parseConstant(); + return value; + } + case 'S': { + CstInteger value = (CstInteger) parseConstant(); + return CstShort.make(value.getValue()); + } + case 'Z': { + CstInteger value = (CstInteger) parseConstant(); + return CstBoolean.make(value.getValue()); + } + case 'c': { + int classInfoIndex = input.readUnsignedShort(); + CstString value = (CstString) pool.get(classInfoIndex); + Type type = Type.internReturnType(value.getString()); + + if (observer != null) { + parsed(2, "class_info: " + type.toHuman()); + } + + return new CstType(type); + } + case 's': { + return parseConstant(); + } + case 'e': { + requireLength(4); + + int typeNameIndex = input.readUnsignedShort(); + int constNameIndex = input.readUnsignedShort(); + CstString typeName = (CstString) pool.get(typeNameIndex); + CstString constName = (CstString) pool.get(constNameIndex); + + if (observer != null) { + parsed(2, "type_name: " + typeName.toHuman()); + parsed(2, "const_name: " + constName.toHuman()); + } + + return new CstEnumRef(new CstNat(constName, typeName)); + } + case '@': { + Annotation annotation = + parseAnnotation(AnnotationVisibility.EMBEDDED); + return new CstAnnotation(annotation); + } + case '[': { + requireLength(2); + + int numValues = input.readUnsignedShort(); + CstArray.List list = new CstArray.List(numValues); + + if (observer != null) { + parsed(2, "num_values: " + numValues); + changeIndent(1); + } + + for (int i = 0; i < numValues; i++) { + if (observer != null) { + changeIndent(-1); + parsed(0, "element_value[" + i + "]:"); + changeIndent(1); + } + list.set(i, parseValue()); + } + + if (observer != null) { + changeIndent(-1); + } + + list.setImmutable(); + return new CstArray(list); + } + default: { + throw new ParseException("unknown annotation tag: " + + Hex.u1(tag)); + } + } + } + + /** + * Helper for {@link #parseValue}, which parses a constant reference + * and returns the referred-to constant value. + * + * @return {@code non-null;} the parsed value + */ + private Constant parseConstant() throws IOException { + int constValueIndex = input.readUnsignedShort(); + Constant value = (Constant) pool.get(constValueIndex); + + if (observer != null) { + String human = (value instanceof CstString) + ? ((CstString) value).toQuoted() + : value.toHuman(); + parsed(2, "constant_value: " + human); + } + + return value; + } + + /** + * Helper which will throw an exception if the given number of bytes + * is not available to be read. + * + * @param requiredLength the number of required bytes + */ + private void requireLength(int requiredLength) throws IOException { + if (input.available() < requiredLength) { + throw new ParseException("truncated annotation attribute"); + } + } + + /** + * Helper which indicates that some bytes were just parsed. This should + * only be used (for efficiency sake) if the parse is known to be + * observed. + * + * @param length {@code >= 0;} number of bytes parsed + * @param message {@code non-null;} associated message + */ + private void parsed(int length, String message) { + observer.parsed(bytes, parseCursor, length, message); + parseCursor += length; + } + + /** + * Convenience wrapper that simply calls through to + * {@code observer.changeIndent()}. + * + * @param indent the amount to change the indent by + */ + private void changeIndent(int indent) { + observer.changeIndent(indent); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/direct/AttributeFactory.java b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/AttributeFactory.java new file mode 100644 index 00000000..5d2f4f40 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/AttributeFactory.java @@ -0,0 +1,134 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.direct; + +import external.com.android.dx.cf.attrib.RawAttribute; +import external.com.android.dx.cf.iface.Attribute; +import external.com.android.dx.cf.iface.ParseException; +import external.com.android.dx.cf.iface.ParseObserver; +import external.com.android.dx.rop.cst.ConstantPool; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.util.ByteArray; +import external.com.android.dx.util.Hex; + +/** + * Factory capable of instantiating various {@link Attribute} subclasses + * depending on the context and name. + */ +public class AttributeFactory { + /** context for attributes on class files */ + public static final int CTX_CLASS = 0; + + /** context for attributes on fields */ + public static final int CTX_FIELD = 1; + + /** context for attributes on methods */ + public static final int CTX_METHOD = 2; + + /** context for attributes on code attributes */ + public static final int CTX_CODE = 3; + + /** number of contexts */ + public static final int CTX_COUNT = 4; + + /** + * Constructs an instance. + */ + public AttributeFactory() { + // This space intentionally left blank. + } + + /** + * Parses and makes an attribute based on the bytes at the + * indicated position in the given array. This method figures out + * the name, and then does all the setup to call on to {@link #parse0}, + * which does the actual construction. + * + * @param cf {@code non-null;} class file to parse from + * @param context context to parse in; one of the {@code CTX_*} + * constants + * @param offset offset into {@code dcf}'s {@code bytes} + * to start parsing at + * @param observer {@code null-ok;} parse observer to report to, if any + * @return {@code non-null;} an appropriately-constructed {@link Attribute} + */ + public final Attribute parse(DirectClassFile cf, int context, int offset, + ParseObserver observer) { + if (cf == null) { + throw new NullPointerException("cf == null"); + } + + if ((context < 0) || (context >= CTX_COUNT)) { + throw new IllegalArgumentException("bad context"); + } + + CstString name = null; + + try { + ByteArray bytes = cf.getBytes(); + ConstantPool pool = cf.getConstantPool(); + int nameIdx = bytes.getUnsignedShort(offset); + int length = bytes.getInt(offset + 2); + + name = (CstString) pool.get(nameIdx); + + if (observer != null) { + observer.parsed(bytes, offset, 2, + "name: " + name.toHuman()); + observer.parsed(bytes, offset + 2, 4, + "length: " + Hex.u4(length)); + } + + return parse0(cf, context, name.getString(), offset + 6, length, + observer); + } catch (ParseException ex) { + ex.addContext("...while parsing " + + ((name != null) ? (name.toHuman() + " ") : "") + + "attribute at offset " + Hex.u4(offset)); + throw ex; + } + } + + /** + * Parses attribute content. The base class implements this by constructing + * an instance of {@link RawAttribute}. Subclasses are expected to + * override this to do something better in most cases. + * + * @param cf {@code non-null;} class file to parse from + * @param context context to parse in; one of the {@code CTX_*} + * constants + * @param name {@code non-null;} the attribute name + * @param offset offset into {@code bytes} to start parsing at; this + * is the offset to the start of attribute data, not to the header + * @param length the length of the attribute data + * @param observer {@code null-ok;} parse observer to report to, if any + * @return {@code non-null;} an appropriately-constructed {@link Attribute} + */ + protected Attribute parse0(DirectClassFile cf, int context, String name, + int offset, int length, + ParseObserver observer) { + ByteArray bytes = cf.getBytes(); + ConstantPool pool = cf.getConstantPool(); + Attribute result = new RawAttribute(name, bytes, offset, length, pool); + + if (observer != null) { + observer.parsed(bytes, offset, length, "attribute data"); + } + + return result; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/direct/AttributeListParser.java b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/AttributeListParser.java new file mode 100644 index 00000000..1df356a0 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/AttributeListParser.java @@ -0,0 +1,164 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.direct; + +import external.com.android.dx.cf.iface.Attribute; +import external.com.android.dx.cf.iface.ParseException; +import external.com.android.dx.cf.iface.ParseObserver; +import external.com.android.dx.cf.iface.StdAttributeList; +import external.com.android.dx.util.ByteArray; +import external.com.android.dx.util.Hex; + +/** + * Parser for lists of attributes. + */ +final /*package*/ class AttributeListParser { + /** {@code non-null;} the class file to parse from */ + private final DirectClassFile cf; + + /** attribute parsing context */ + private final int context; + + /** offset in the byte array of the classfile to the start of the list */ + private final int offset; + + /** {@code non-null;} attribute factory to use */ + private final AttributeFactory attributeFactory; + + /** {@code non-null;} list of parsed attributes */ + private final StdAttributeList list; + + /** {@code >= -1;} the end offset of this list in the byte array of the + * classfile, or {@code -1} if not yet parsed */ + private int endOffset; + + /** {@code null-ok;} parse observer, if any */ + private ParseObserver observer; + + /** + * Constructs an instance. + * + * @param cf {@code non-null;} class file to parse from + * @param context attribute parsing context (see {@link AttributeFactory}) + * @param offset offset in {@code bytes} to the start of the list + * @param attributeFactory {@code non-null;} attribute factory to use + */ + public AttributeListParser(DirectClassFile cf, int context, int offset, + AttributeFactory attributeFactory) { + if (cf == null) { + throw new NullPointerException("cf == null"); + } + + if (attributeFactory == null) { + throw new NullPointerException("attributeFactory == null"); + } + + int size = cf.getBytes().getUnsignedShort(offset); + + this.cf = cf; + this.context = context; + this.offset = offset; + this.attributeFactory = attributeFactory; + this.list = new StdAttributeList(size); + this.endOffset = -1; + } + + /** + * Sets the parse observer for this instance. + * + * @param observer {@code null-ok;} the observer + */ + public void setObserver(ParseObserver observer) { + this.observer = observer; + } + + /** + * Gets the end offset of this constant pool in the {@code byte[]} + * which it came from. + * + * @return {@code >= 0;} the end offset + */ + public int getEndOffset() { + parseIfNecessary(); + return endOffset; + } + + /** + * Gets the parsed list. + * + * @return {@code non-null;} the list + */ + public StdAttributeList getList() { + parseIfNecessary(); + return list; + } + + /** + * Runs {@link #parse} if it has not yet been run successfully. + */ + private void parseIfNecessary() { + if (endOffset < 0) { + parse(); + } + } + + /** + * Does the actual parsing. + */ + private void parse() { + int sz = list.size(); + int at = offset + 2; // Skip the count. + + ByteArray bytes = cf.getBytes(); + + if (observer != null) { + observer.parsed(bytes, offset, 2, + "attributes_count: " + Hex.u2(sz)); + } + + for (int i = 0; i < sz; i++) { + try { + if (observer != null) { + observer.parsed(bytes, at, 0, + "\nattributes[" + i + "]:\n"); + observer.changeIndent(1); + } + + Attribute attrib = + attributeFactory.parse(cf, context, at, observer); + + at += attrib.byteLength(); + list.set(i, attrib); + + if (observer != null) { + observer.changeIndent(-1); + observer.parsed(bytes, at, 0, + "end attributes[" + i + "]\n"); + } + } catch (ParseException ex) { + ex.addContext("...while parsing attributes[" + i + "]"); + throw ex; + } catch (RuntimeException ex) { + ParseException pe = new ParseException(ex); + pe.addContext("...while parsing attributes[" + i + "]"); + throw pe; + } + } + + endOffset = at; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/direct/ClassPathOpener.java b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/ClassPathOpener.java new file mode 100644 index 00000000..a337355d --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/ClassPathOpener.java @@ -0,0 +1,292 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.direct; + +import external.com.android.dex.util.FileUtils; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Opens all the class files found in a class path element. Path elements + * can point to class files, {jar,zip,apk} files, or directories containing + * class files. + */ +public class ClassPathOpener { + + /** {@code non-null;} pathname to start with */ + private final String pathname; + /** {@code non-null;} callback interface */ + private final Consumer consumer; + /** + * If true, sort such that classes appear before their inner + * classes and "package-info" occurs before all other classes in that + * package. + */ + private final boolean sort; + private FileNameFilter filter; + + /** + * Callback interface for {@code ClassOpener}. + */ + public interface Consumer { + + /** + * Provides the file name and byte array for a class path element. + * + * @param name {@code non-null;} filename of element. May not be a valid + * filesystem path. + * + * @param lastModified milliseconds since 1970-Jan-1 00:00:00 GMT + * @param bytes {@code non-null;} file data + * @return true on success. Result is or'd with all other results + * from {@code processFileBytes} and returned to the caller + * of {@code process()}. + */ + boolean processFileBytes(String name, long lastModified, byte[] bytes); + + /** + * Informs consumer that an exception occurred while processing + * this path element. Processing will continue if possible. + * + * @param ex {@code non-null;} exception + */ + void onException(Exception ex); + + /** + * Informs consumer that processing of an archive file has begun. + * + * @param file {@code non-null;} archive file being processed + */ + void onProcessArchiveStart(File file); + } + + /** + * Filter interface for {@code ClassOpener}. + */ + public interface FileNameFilter { + + boolean accept(String path); + } + + /** + * An accept all filter. + */ + public static final FileNameFilter acceptAll = new FileNameFilter() { + + @Override + public boolean accept(String path) { + return true; + } + }; + + /** + * Constructs an instance. + * + * @param pathname {@code non-null;} path element to process + * @param sort if true, sort such that classes appear before their inner + * classes and "package-info" occurs before all other classes in that + * package. + * @param consumer {@code non-null;} callback interface + */ + public ClassPathOpener(String pathname, boolean sort, Consumer consumer) { + this(pathname, sort, acceptAll, consumer); + } + + /** + * Constructs an instance. + * + * @param pathname {@code non-null;} path element to process + * @param sort if true, sort such that classes appear before their inner + * classes and "package-info" occurs before all other classes in that + * package. + * @param consumer {@code non-null;} callback interface + */ + public ClassPathOpener(String pathname, boolean sort, FileNameFilter filter, + Consumer consumer) { + this.pathname = pathname; + this.sort = sort; + this.consumer = consumer; + this.filter = filter; + } + + /** + * Processes a path element. + * + * @return the OR of all return values + * from {@code Consumer.processFileBytes()}. + */ + public boolean process() { + File file = new File(pathname); + + return processOne(file, true); + } + + /** + * Processes one file. + * + * @param file {@code non-null;} the file to process + * @param topLevel whether this is a top-level file (that is, + * specified directly on the commandline) + * @return whether any processing actually happened + */ + private boolean processOne(File file, boolean topLevel) { + try { + if (file.isDirectory()) { + return processDirectory(file, topLevel); + } + + String path = file.getPath(); + + if (path.endsWith(".zip") || + path.endsWith(".jar") || + path.endsWith(".apk")) { + return processArchive(file); + } + if (filter.accept(path)) { + byte[] bytes = FileUtils.readFile(file); + return consumer.processFileBytes(path, file.lastModified(), bytes); + } else { + return false; + } + } catch (Exception ex) { + consumer.onException(ex); + return false; + } + } + + /** + * Sorts java class names such that outer classes preceed their inner + * classes and "package-info" preceeds all other classes in its package. + * + * @param a {@code non-null;} first class name + * @param b {@code non-null;} second class name + * @return {@code compareTo()}-style result + */ + private static int compareClassNames(String a, String b) { + // Ensure inner classes sort second + a = a.replace('$','0'); + b = b.replace('$','0'); + + /* + * Assuming "package-info" only occurs at the end, ensures package-info + * sorts first. + */ + a = a.replace("package-info", ""); + b = b.replace("package-info", ""); + + return a.compareTo(b); + } + + /** + * Processes a directory recursively. + * + * @param dir {@code non-null;} file representing the directory + * @param topLevel whether this is a top-level directory (that is, + * specified directly on the commandline) + * @return whether any processing actually happened + */ + private boolean processDirectory(File dir, boolean topLevel) { + if (topLevel) { + dir = new File(dir, "."); + } + + File[] files = dir.listFiles(); + int len = files.length; + boolean any = false; + + if (sort) { + Arrays.sort(files, new Comparator() { + @Override + public int compare(File a, File b) { + return compareClassNames(a.getName(), b.getName()); + } + }); + } + + for (int i = 0; i < len; i++) { + any |= processOne(files[i], false); + } + + return any; + } + + /** + * Processes the contents of an archive ({@code .zip}, + * {@code .jar}, or {@code .apk}). + * + * @param file {@code non-null;} archive file to process + * @return whether any processing actually happened + * @throws IOException on i/o problem + */ + private boolean processArchive(File file) throws IOException { + ZipFile zip = new ZipFile(file); + + ArrayList entriesList + = Collections.list(zip.entries()); + + if (sort) { + Collections.sort(entriesList, new Comparator() { + @Override + public int compare (ZipEntry a, ZipEntry b) { + return compareClassNames(a.getName(), b.getName()); + } + }); + } + + consumer.onProcessArchiveStart(file); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(40000); + byte[] buf = new byte[20000]; + boolean any = false; + + for (ZipEntry one : entriesList) { + final boolean isDirectory = one.isDirectory(); + + String path = one.getName(); + if (filter.accept(path)) { + final byte[] bytes; + if (!isDirectory) { + InputStream in = zip.getInputStream(one); + + baos.reset(); + int read; + while ((read = in.read(buf)) != -1) { + baos.write(buf, 0, read); + } + + in.close(); + bytes = baos.toByteArray(); + } else { + bytes = new byte[0]; + } + + any |= consumer.processFileBytes(path, one.getTime(), bytes); + } + } + + zip.close(); + return any; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/direct/CodeObserver.java b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/CodeObserver.java new file mode 100644 index 00000000..e058eb7c --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/CodeObserver.java @@ -0,0 +1,312 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.direct; + +import external.com.android.dx.cf.code.ByteOps; +import external.com.android.dx.cf.code.BytecodeArray; +import external.com.android.dx.cf.code.SwitchList; +import external.com.android.dx.cf.iface.ParseObserver; +import external.com.android.dx.rop.cst.Constant; +import external.com.android.dx.rop.cst.CstDouble; +import external.com.android.dx.rop.cst.CstFloat; +import external.com.android.dx.rop.cst.CstInteger; +import external.com.android.dx.rop.cst.CstKnownNull; +import external.com.android.dx.rop.cst.CstLong; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.util.ByteArray; +import external.com.android.dx.util.Hex; +import java.util.ArrayList; + +/** + * Bytecode visitor to use when "observing" bytecode getting parsed. + */ +public class CodeObserver implements BytecodeArray.Visitor { + /** {@code non-null;} actual array of bytecode */ + private final ByteArray bytes; + + /** {@code non-null;} observer to inform of parsing */ + private final ParseObserver observer; + + /** + * Constructs an instance. + * + * @param bytes {@code non-null;} actual array of bytecode + * @param observer {@code non-null;} observer to inform of parsing + */ + public CodeObserver(ByteArray bytes, ParseObserver observer) { + if (bytes == null) { + throw new NullPointerException("bytes == null"); + } + + if (observer == null) { + throw new NullPointerException("observer == null"); + } + + this.bytes = bytes; + this.observer = observer; + } + + /** {@inheritDoc} */ + @Override + public void visitInvalid(int opcode, int offset, int length) { + observer.parsed(bytes, offset, length, header(offset)); + } + + /** {@inheritDoc} */ + @Override + public void visitNoArgs(int opcode, int offset, int length, Type type) { + observer.parsed(bytes, offset, length, header(offset)); + } + + /** {@inheritDoc} */ + @Override + public void visitLocal(int opcode, int offset, int length, + int idx, Type type, int value) { + String idxStr = (length <= 3) ? Hex.u1(idx) : Hex.u2(idx); + boolean argComment = (length == 1); + String valueStr = ""; + + if (opcode == ByteOps.IINC) { + valueStr = ", #" + + ((length <= 3) ? Hex.s1(value) : Hex.s2(value)); + } + + String catStr = ""; + if (type.isCategory2()) { + catStr = (argComment ? "," : " //") + " category-2"; + } + + observer.parsed(bytes, offset, length, + header(offset) + (argComment ? " // " : " ") + + idxStr + valueStr + catStr); + } + + /** {@inheritDoc} */ + @Override + public void visitConstant(int opcode, int offset, int length, + Constant cst, int value) { + if (cst instanceof CstKnownNull) { + // This is aconst_null. + visitNoArgs(opcode, offset, length, null); + return; + } + + if (cst instanceof CstInteger) { + visitLiteralInt(opcode, offset, length, value); + return; + } + + if (cst instanceof CstLong) { + visitLiteralLong(opcode, offset, length, + ((CstLong) cst).getValue()); + return; + } + + if (cst instanceof CstFloat) { + visitLiteralFloat(opcode, offset, length, + ((CstFloat) cst).getIntBits()); + return; + } + + if (cst instanceof CstDouble) { + visitLiteralDouble(opcode, offset, length, + ((CstDouble) cst).getLongBits()); + return; + } + + String valueStr = ""; + if (value != 0) { + valueStr = ", "; + if (opcode == ByteOps.MULTIANEWARRAY) { + valueStr += Hex.u1(value); + } else { + valueStr += Hex.u2(value); + } + } + + observer.parsed(bytes, offset, length, + header(offset) + " " + cst + valueStr); + } + + /** {@inheritDoc} */ + @Override + public void visitBranch(int opcode, int offset, int length, + int target) { + String targetStr = (length <= 3) ? Hex.u2(target) : Hex.u4(target); + observer.parsed(bytes, offset, length, + header(offset) + " " + targetStr); + } + + /** {@inheritDoc} */ + @Override + public void visitSwitch(int opcode, int offset, int length, + SwitchList cases, int padding) { + int sz = cases.size(); + StringBuilder sb = new StringBuilder(sz * 20 + 100); + + sb.append(header(offset)); + if (padding != 0) { + sb.append(" // padding: " + Hex.u4(padding)); + } + sb.append('\n'); + + for (int i = 0; i < sz; i++) { + sb.append(" "); + sb.append(Hex.s4(cases.getValue(i))); + sb.append(": "); + sb.append(Hex.u2(cases.getTarget(i))); + sb.append('\n'); + } + + sb.append(" default: "); + sb.append(Hex.u2(cases.getDefaultTarget())); + + observer.parsed(bytes, offset, length, sb.toString()); + } + + /** {@inheritDoc} */ + @Override + public void visitNewarray(int offset, int length, CstType cst, + ArrayList intVals) { + String commentOrSpace = (length == 1) ? " // " : " "; + String typeName = cst.getClassType().getComponentType().toHuman(); + + observer.parsed(bytes, offset, length, + header(offset) + commentOrSpace + typeName); + } + + /** {@inheritDoc} */ + @Override + public void setPreviousOffset(int offset) { + // Do nothing + } + + /** {@inheritDoc} */ + @Override + public int getPreviousOffset() { + return -1; + } + + /** + * Helper to produce the first bit of output for each instruction. + * + * @param offset the offset to the start of the instruction + */ + private String header(int offset) { + /* + * Note: This uses the original bytecode, not the + * possibly-transformed one. + */ + int opcode = bytes.getUnsignedByte(offset); + String name = ByteOps.opName(opcode); + + if (opcode == ByteOps.WIDE) { + opcode = bytes.getUnsignedByte(offset + 1); + name += " " + ByteOps.opName(opcode); + } + + return Hex.u2(offset) + ": " + name; + } + + /** + * Helper for {@link #visitConstant} where the constant is an + * {@code int}. + * + * @param opcode the opcode + * @param offset offset to the instruction + * @param length instruction length + * @param value constant value + */ + private void visitLiteralInt(int opcode, int offset, int length, + int value) { + String commentOrSpace = (length == 1) ? " // " : " "; + String valueStr; + + opcode = bytes.getUnsignedByte(offset); // Compare with orig op below. + if ((length == 1) || (opcode == ByteOps.BIPUSH)) { + valueStr = "#" + Hex.s1(value); + } else if (opcode == ByteOps.SIPUSH) { + valueStr = "#" + Hex.s2(value); + } else { + valueStr = "#" + Hex.s4(value); + } + + observer.parsed(bytes, offset, length, + header(offset) + commentOrSpace + valueStr); + } + + /** + * Helper for {@link #visitConstant} where the constant is a + * {@code long}. + * + * @param opcode the opcode + * @param offset offset to the instruction + * @param length instruction length + * @param value constant value + */ + private void visitLiteralLong(int opcode, int offset, int length, + long value) { + String commentOrLit = (length == 1) ? " // " : " #"; + String valueStr; + + if (length == 1) { + valueStr = Hex.s1((int) value); + } else { + valueStr = Hex.s8(value); + } + + observer.parsed(bytes, offset, length, + header(offset) + commentOrLit + valueStr); + } + + /** + * Helper for {@link #visitConstant} where the constant is a + * {@code float}. + * + * @param opcode the opcode + * @param offset offset to the instruction + * @param length instruction length + * @param bits constant value, as float-bits + */ + private void visitLiteralFloat(int opcode, int offset, int length, + int bits) { + String optArg = (length != 1) ? " #" + Hex.u4(bits) : ""; + + observer.parsed(bytes, offset, length, + header(offset) + optArg + " // " + + Float.intBitsToFloat(bits)); + } + + /** + * Helper for {@link #visitConstant} where the constant is a + * {@code double}. + * + * @param opcode the opcode + * @param offset offset to the instruction + * @param length instruction length + * @param bits constant value, as double-bits + */ + private void visitLiteralDouble(int opcode, int offset, int length, + long bits) { + String optArg = (length != 1) ? " #" + Hex.u8(bits) : ""; + + observer.parsed(bytes, offset, length, + header(offset) + optArg + " // " + + Double.longBitsToDouble(bits)); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/direct/DirectClassFile.java b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/DirectClassFile.java new file mode 100644 index 00000000..686d817b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/DirectClassFile.java @@ -0,0 +1,688 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.direct; + +import external.com.android.dx.cf.attrib.AttBootstrapMethods; +import external.com.android.dx.cf.attrib.AttSourceFile; +import external.com.android.dx.cf.code.BootstrapMethodsList; +import external.com.android.dx.cf.cst.ConstantPoolParser; +import external.com.android.dx.cf.iface.Attribute; +import external.com.android.dx.cf.iface.AttributeList; +import external.com.android.dx.cf.iface.ClassFile; +import external.com.android.dx.cf.iface.FieldList; +import external.com.android.dx.cf.iface.MethodList; +import external.com.android.dx.cf.iface.ParseException; +import external.com.android.dx.cf.iface.ParseObserver; +import external.com.android.dx.cf.iface.StdAttributeList; +import external.com.android.dx.rop.code.AccessFlags; +import external.com.android.dx.rop.cst.ConstantPool; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.cst.StdConstantPool; +import external.com.android.dx.rop.type.StdTypeList; +import external.com.android.dx.rop.type.Type; +import external.com.android.dx.rop.type.TypeList; +import external.com.android.dx.util.ByteArray; +import external.com.android.dx.util.Hex; + +/** + * Class file with info taken from a {@code byte[]} or slice thereof. + */ +public class DirectClassFile implements ClassFile { + /** the expected value of the ClassFile.magic field */ + private static final int CLASS_FILE_MAGIC = 0xcafebabe; + + /** + * minimum {@code .class} file major version + * + * See http://en.wikipedia.org/wiki/Java_class_file for an up-to-date + * list of version numbers. Currently known (taken from that table) are: + * + * Java SE 9 = 53 (0x35 hex), + * Java SE 8 = 52 (0x34 hex), + * Java SE 7 = 51 (0x33 hex), + * Java SE 6.0 = 50 (0x32 hex), + * Java SE 5.0 = 49 (0x31 hex), + * JDK 1.4 = 48 (0x30 hex), + * JDK 1.3 = 47 (0x2F hex), + * JDK 1.2 = 46 (0x2E hex), + * JDK 1.1 = 45 (0x2D hex). + * + * Valid ranges are typically of the form + * "A.0 through B.C inclusive" where A <= B and C >= 0, + * which is why we don't have a CLASS_FILE_MIN_MINOR_VERSION. + */ + private static final int CLASS_FILE_MIN_MAJOR_VERSION = 45; + + /** + * maximum {@code .class} file major version + * + * Note: if you change this, please change "java.class.version" in System.java. + */ + private static final int CLASS_FILE_MAX_MAJOR_VERSION = 53; + + /** maximum {@code .class} file minor version */ + private static final int CLASS_FILE_MAX_MINOR_VERSION = 0; + + /** + * {@code non-null;} the file path for the class, excluding any base directory + * specification + */ + private final String filePath; + + /** {@code non-null;} the bytes of the file */ + private final ByteArray bytes; + + /** + * whether to be strict about parsing; if + * {@code false}, this avoids doing checks that only exist + * for purposes of verification (such as magic number matching and + * path-package consistency checking) + */ + private final boolean strictParse; + + /** + * {@code null-ok;} the constant pool; only ever {@code null} + * before the constant pool is successfully parsed + */ + private StdConstantPool pool; + + /** + * the class file field {@code access_flags}; will be {@code -1} + * before the file is successfully parsed + */ + private int accessFlags; + + /** + * {@code null-ok;} the class file field {@code this_class}, + * interpreted as a type constant; only ever {@code null} + * before the file is successfully parsed + */ + private CstType thisClass; + + /** + * {@code null-ok;} the class file field {@code super_class}, interpreted + * as a type constant if non-zero + */ + private CstType superClass; + + /** + * {@code null-ok;} the class file field {@code interfaces}; only + * ever {@code null} before the file is successfully + * parsed + */ + private TypeList interfaces; + + /** + * {@code null-ok;} the class file field {@code fields}; only ever + * {@code null} before the file is successfully parsed + */ + private FieldList fields; + + /** + * {@code null-ok;} the class file field {@code methods}; only ever + * {@code null} before the file is successfully parsed + */ + private MethodList methods; + + /** + * {@code null-ok;} the class file field {@code attributes}; only + * ever {@code null} before the file is successfully + * parsed + */ + private StdAttributeList attributes; + + /** {@code null-ok;} attribute factory, if any */ + private AttributeFactory attributeFactory; + + /** {@code null-ok;} parse observer, if any */ + private ParseObserver observer; + + /** + * Returns the string form of an object or {@code "(none)"} + * (rather than {@code "null"}) for {@code null}. + * + * @param obj {@code null-ok;} the object to stringify + * @return {@code non-null;} the appropriate string form + */ + public static String stringOrNone(Object obj) { + if (obj == null) { + return "(none)"; + } + + return obj.toString(); + } + + /** + * Constructs an instance. + * + * @param bytes {@code non-null;} the bytes of the file + * @param filePath {@code non-null;} the file path for the class, + * excluding any base directory specification + * @param strictParse whether to be strict about parsing; if + * {@code false}, this avoids doing checks that only exist + * for purposes of verification (such as magic number matching and + * path-package consistency checking) + */ + public DirectClassFile(ByteArray bytes, String filePath, + boolean strictParse) { + if (bytes == null) { + throw new NullPointerException("bytes == null"); + } + + if (filePath == null) { + throw new NullPointerException("filePath == null"); + } + + this.filePath = filePath; + this.bytes = bytes; + this.strictParse = strictParse; + this.accessFlags = -1; + } + + /** + * Constructs an instance. + * + * @param bytes {@code non-null;} the bytes of the file + * @param filePath {@code non-null;} the file path for the class, + * excluding any base directory specification + * @param strictParse whether to be strict about parsing; if + * {@code false}, this avoids doing checks that only exist + * for purposes of verification (such as magic number matching and + * path-package consistency checking) + */ + public DirectClassFile(byte[] bytes, String filePath, + boolean strictParse) { + this(new ByteArray(bytes), filePath, strictParse); + } + + /** + * Sets the parse observer for this instance. + * + * @param observer {@code null-ok;} the observer + */ + public void setObserver(ParseObserver observer) { + this.observer = observer; + } + + /** + * Sets the attribute factory to use. + * + * @param attributeFactory {@code non-null;} the attribute factory + */ + public void setAttributeFactory(AttributeFactory attributeFactory) { + if (attributeFactory == null) { + throw new NullPointerException("attributeFactory == null"); + } + + this.attributeFactory = attributeFactory; + } + + /** + * Gets the path where this class file is located. + * + * @return {@code non-null;} the filePath + */ + public String getFilePath() { + return filePath; + } + + /** + * Gets the {@link ByteArray} that this instance's data comes from. + * + * @return {@code non-null;} the bytes + */ + public ByteArray getBytes() { + return bytes; + } + + /** {@inheritDoc} */ + @Override + public int getMagic() { + parseToInterfacesIfNecessary(); + return getMagic0(); + } + + /** {@inheritDoc} */ + @Override + public int getMinorVersion() { + parseToInterfacesIfNecessary(); + return getMinorVersion0(); + } + + /** {@inheritDoc} */ + @Override + public int getMajorVersion() { + parseToInterfacesIfNecessary(); + return getMajorVersion0(); + } + + /** {@inheritDoc} */ + @Override + public int getAccessFlags() { + parseToInterfacesIfNecessary(); + return accessFlags; + } + + /** {@inheritDoc} */ + @Override + public CstType getThisClass() { + parseToInterfacesIfNecessary(); + return thisClass; + } + + /** {@inheritDoc} */ + @Override + public CstType getSuperclass() { + parseToInterfacesIfNecessary(); + return superClass; + } + + /** {@inheritDoc} */ + @Override + public ConstantPool getConstantPool() { + parseToInterfacesIfNecessary(); + return pool; + } + + /** {@inheritDoc} */ + @Override + public TypeList getInterfaces() { + parseToInterfacesIfNecessary(); + return interfaces; + } + + /** {@inheritDoc} */ + @Override + public FieldList getFields() { + parseToEndIfNecessary(); + return fields; + } + + /** {@inheritDoc} */ + @Override + public MethodList getMethods() { + parseToEndIfNecessary(); + return methods; + } + + /** {@inheritDoc} */ + @Override + public AttributeList getAttributes() { + parseToEndIfNecessary(); + return attributes; + } + + /** {@inheritDoc} */ + @Override + public BootstrapMethodsList getBootstrapMethods() { + AttBootstrapMethods bootstrapMethodsAttribute = + (AttBootstrapMethods) getAttributes().findFirst(AttBootstrapMethods.ATTRIBUTE_NAME); + if (bootstrapMethodsAttribute != null) { + return bootstrapMethodsAttribute.getBootstrapMethods(); + } else { + return BootstrapMethodsList.EMPTY; + } + } + + /** {@inheritDoc} */ + @Override + public CstString getSourceFile() { + AttributeList attribs = getAttributes(); + Attribute attSf = attribs.findFirst(AttSourceFile.ATTRIBUTE_NAME); + + if (attSf instanceof AttSourceFile) { + return ((AttSourceFile) attSf).getSourceFile(); + } + + return null; + } + + /** + * Constructs and returns an instance of {@link TypeList} whose + * data comes from the bytes of this instance, interpreted as a + * list of constant pool indices for classes, which are in turn + * translated to type constants. Instance construction will fail + * if any of the (alleged) indices turn out not to refer to + * constant pool entries of type {@code Class}. + * + * @param offset offset into {@link #bytes} for the start of the + * data + * @param size number of elements in the list (not number of bytes) + * @return {@code non-null;} an appropriately-constructed class list + */ + public TypeList makeTypeList(int offset, int size) { + if (size == 0) { + return StdTypeList.EMPTY; + } + + if (pool == null) { + throw new IllegalStateException("pool not yet initialized"); + } + + return new DcfTypeList(bytes, offset, size, pool, observer); + } + + /** + * Gets the class file field {@code magic}, but without doing any + * checks or parsing first. + * + * @return the magic value + */ + public int getMagic0() { + return bytes.getInt(0); + } + + /** + * Gets the class file field {@code minor_version}, but + * without doing any checks or parsing first. + * + * @return the minor version + */ + public int getMinorVersion0() { + return bytes.getUnsignedShort(4); + } + + /** + * Gets the class file field {@code major_version}, but + * without doing any checks or parsing first. + * + * @return the major version + */ + public int getMajorVersion0() { + return bytes.getUnsignedShort(6); + } + + /** + * Runs {@link #parse} if it has not yet been run to cover up to + * the interfaces list. + */ + private void parseToInterfacesIfNecessary() { + if (accessFlags == -1) { + parse(); + } + } + + /** + * Runs {@link #parse} if it has not yet been run successfully. + */ + private void parseToEndIfNecessary() { + if (attributes == null) { + parse(); + } + } + + /** + * Does the parsing, handing exceptions. + */ + private void parse() { + try { + parse0(); + } catch (ParseException ex) { + ex.addContext("...while parsing " + filePath); + throw ex; + } catch (RuntimeException ex) { + ParseException pe = new ParseException(ex); + pe.addContext("...while parsing " + filePath); + throw pe; + } + } + + /** + * Sees if the .class file header magic has the good value. + * + * @param magic the value of a classfile "magic" field + * @return true if the magic is valid + */ + private boolean isGoodMagic(int magic) { + return magic == CLASS_FILE_MAGIC; + } + + /** + * Sees if the .class file header version are within + * range. + * + * @param minorVersion the value of a classfile "minor_version" field + * @param majorVersion the value of a classfile "major_version" field + * @return true if the parameters are valid and within range + */ + private boolean isGoodVersion(int minorVersion, int majorVersion) { + /* Valid version ranges are typically of the form + * "A.0 through B.C inclusive" where A <= B and C >= 0, + * which is why we don't have a CLASS_FILE_MIN_MINOR_VERSION. + */ + if (minorVersion >= 0) { + /* Check against max first to handle the case where + * MIN_MAJOR == MAX_MAJOR. + */ + if (majorVersion == CLASS_FILE_MAX_MAJOR_VERSION) { + if (minorVersion <= CLASS_FILE_MAX_MINOR_VERSION) { + return true; + } + } else if (majorVersion < CLASS_FILE_MAX_MAJOR_VERSION && + majorVersion >= CLASS_FILE_MIN_MAJOR_VERSION) { + return true; + } + } + + return false; + } + + /** + * Does the actual parsing. + */ + private void parse0() { + if (bytes.size() < 10) { + throw new ParseException("severely truncated class file"); + } + + if (observer != null) { + observer.parsed(bytes, 0, 0, "begin classfile"); + observer.parsed(bytes, 0, 4, "magic: " + Hex.u4(getMagic0())); + observer.parsed(bytes, 4, 2, + "minor_version: " + Hex.u2(getMinorVersion0())); + observer.parsed(bytes, 6, 2, + "major_version: " + Hex.u2(getMajorVersion0())); + } + + if (strictParse) { + /* Make sure that this looks like a valid class file with a + * version that we can handle. + */ + if (!isGoodMagic(getMagic0())) { + throw new ParseException("bad class file magic (" + Hex.u4(getMagic0()) + ")"); + } + + if (!isGoodVersion(getMinorVersion0(), getMajorVersion0())) { + throw new ParseException("unsupported class file version " + + getMajorVersion0() + "." + + getMinorVersion0()); + } + } + + ConstantPoolParser cpParser = new ConstantPoolParser(bytes); + cpParser.setObserver(observer); + pool = cpParser.getPool(); + pool.setImmutable(); + + int at = cpParser.getEndOffset(); + int accessFlags = bytes.getUnsignedShort(at); // u2 access_flags; + int cpi = bytes.getUnsignedShort(at + 2); // u2 this_class; + thisClass = (CstType) pool.get(cpi); + cpi = bytes.getUnsignedShort(at + 4); // u2 super_class; + superClass = (CstType) pool.get0Ok(cpi); + int count = bytes.getUnsignedShort(at + 6); // u2 interfaces_count + + if (observer != null) { + observer.parsed(bytes, at, 2, + "access_flags: " + + AccessFlags.classString(accessFlags)); + observer.parsed(bytes, at + 2, 2, "this_class: " + thisClass); + observer.parsed(bytes, at + 4, 2, "super_class: " + + stringOrNone(superClass)); + observer.parsed(bytes, at + 6, 2, + "interfaces_count: " + Hex.u2(count)); + if (count != 0) { + observer.parsed(bytes, at + 8, 0, "interfaces:"); + } + } + + at += 8; + interfaces = makeTypeList(at, count); + at += count * 2; + + if (strictParse) { + /* + * Make sure that the file/jar path matches the declared + * package/class name. + */ + String thisClassName = thisClass.getClassType().getClassName(); + if (!(filePath.endsWith(".class") && + filePath.startsWith(thisClassName) && + (filePath.length() == (thisClassName.length() + 6)))) { + throw new ParseException("class name (" + thisClassName + + ") does not match path (" + + filePath + ")"); + } + } + + /* + * Only set the instance variable accessFlags here, since + * that's what signals a successful parse of the first part of + * the file (through the interfaces list). + */ + this.accessFlags = accessFlags; + + FieldListParser flParser = + new FieldListParser(this, thisClass, at, attributeFactory); + flParser.setObserver(observer); + fields = flParser.getList(); + at = flParser.getEndOffset(); + + MethodListParser mlParser = + new MethodListParser(this, thisClass, at, attributeFactory); + mlParser.setObserver(observer); + methods = mlParser.getList(); + at = mlParser.getEndOffset(); + + AttributeListParser alParser = + new AttributeListParser(this, AttributeFactory.CTX_CLASS, at, + attributeFactory); + alParser.setObserver(observer); + attributes = alParser.getList(); + attributes.setImmutable(); + at = alParser.getEndOffset(); + + if (at != bytes.size()) { + throw new ParseException("extra bytes at end of class file, " + + "at offset " + Hex.u4(at)); + } + + if (observer != null) { + observer.parsed(bytes, at, 0, "end classfile"); + } + } + + /** + * Implementation of {@link TypeList} whose data comes directly + * from the bytes of an instance of this (outer) class, + * interpreted as a list of constant pool indices for classes + * which are in turn returned as type constants. Instance + * construction will fail if any of the (alleged) indices turn out + * not to refer to constant pool entries of type + * {@code Class}. + */ + private static class DcfTypeList implements TypeList { + /** {@code non-null;} array containing the data */ + private final ByteArray bytes; + + /** number of elements in the list (not number of bytes) */ + private final int size; + + /** {@code non-null;} the constant pool */ + private final StdConstantPool pool; + + /** + * Constructs an instance. + * + * @param bytes {@code non-null;} original classfile's bytes + * @param offset offset into {@link #bytes} for the start of the + * data + * @param size number of elements in the list (not number of bytes) + * @param pool {@code non-null;} the constant pool to use + * @param observer {@code null-ok;} parse observer to use, if any + */ + public DcfTypeList(ByteArray bytes, int offset, int size, + StdConstantPool pool, ParseObserver observer) { + if (size < 0) { + throw new IllegalArgumentException("size < 0"); + } + + bytes = bytes.slice(offset, offset + size * 2); + this.bytes = bytes; + this.size = size; + this.pool = pool; + + for (int i = 0; i < size; i++) { + offset = i * 2; + int idx = bytes.getUnsignedShort(offset); + CstType type; + try { + type = (CstType) pool.get(idx); + } catch (ClassCastException ex) { + // Translate the exception. + throw new RuntimeException("bogus class cpi", ex); + } + if (observer != null) { + observer.parsed(bytes, offset, 2, " " + type); + } + } + } + + /** {@inheritDoc} */ + @Override + public boolean isMutable() { + return false; + } + + /** {@inheritDoc} */ + @Override + public int size() { + return size; + } + + /** {@inheritDoc} */ + @Override + public int getWordCount() { + // It is the same as size because all elements are classes. + return size; + } + + /** {@inheritDoc} */ + @Override + public Type getType(int n) { + int idx = bytes.getUnsignedShort(n * 2); + return ((CstType) pool.get(idx)).getClassType(); + } + + /** {@inheritDoc} */ + @Override + public TypeList withAddedType(Type type) { + throw new UnsupportedOperationException("unsupported"); + } + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/direct/FieldListParser.java b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/FieldListParser.java new file mode 100644 index 00000000..78ffab91 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/FieldListParser.java @@ -0,0 +1,86 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.direct; + +import external.com.android.dx.cf.iface.AttributeList; +import external.com.android.dx.cf.iface.Member; +import external.com.android.dx.cf.iface.StdField; +import external.com.android.dx.cf.iface.StdFieldList; +import external.com.android.dx.rop.code.AccessFlags; +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstType; + +/** + * Parser for lists of fields in a class file. + */ +final /*package*/ class FieldListParser extends MemberListParser { + /** {@code non-null;} list in progress */ + private final StdFieldList fields; + + /** + * Constructs an instance. + * + * @param cf {@code non-null;} the class file to parse from + * @param definer {@code non-null;} class being defined + * @param offset offset in {@code bytes} to the start of the list + * @param attributeFactory {@code non-null;} attribute factory to use + */ + public FieldListParser(DirectClassFile cf, CstType definer, int offset, + AttributeFactory attributeFactory) { + super(cf, definer, offset, attributeFactory); + fields = new StdFieldList(getCount()); + } + + /** + * Gets the parsed list. + * + * @return {@code non-null;} the parsed list + */ + public StdFieldList getList() { + parseIfNecessary(); + return fields; + } + + /** {@inheritDoc} */ + @Override + protected String humanName() { + return "field"; + } + + /** {@inheritDoc} */ + @Override + protected String humanAccessFlags(int accessFlags) { + return AccessFlags.fieldString(accessFlags); + } + + /** {@inheritDoc} */ + @Override + protected int getAttributeContext() { + return AttributeFactory.CTX_FIELD; + } + + /** {@inheritDoc} */ + @Override + protected Member set(int n, int accessFlags, CstNat nat, + AttributeList attributes) { + StdField field = + new StdField(getDefiner(), accessFlags, nat, attributes); + + fields.set(n, field); + return field; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/direct/MemberListParser.java b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/MemberListParser.java new file mode 100644 index 00000000..b42521c7 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/MemberListParser.java @@ -0,0 +1,240 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.direct; + +import external.com.android.dx.cf.iface.AttributeList; +import external.com.android.dx.cf.iface.Member; +import external.com.android.dx.cf.iface.ParseException; +import external.com.android.dx.cf.iface.ParseObserver; +import external.com.android.dx.cf.iface.StdAttributeList; +import external.com.android.dx.rop.cst.ConstantPool; +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.util.ByteArray; +import external.com.android.dx.util.Hex; + +/** + * Parser for lists of class file members (that is, fields and methods). + */ +abstract /*package*/ class MemberListParser { + /** {@code non-null;} the class file to parse from */ + private final DirectClassFile cf; + + /** {@code non-null;} class being defined */ + private final CstType definer; + + /** offset in the byte array of the classfile to the start of the list */ + private final int offset; + + /** {@code non-null;} attribute factory to use */ + private final AttributeFactory attributeFactory; + + /** {@code >= -1;} the end offset of this list in the byte array of the + * classfile, or {@code -1} if not yet parsed */ + private int endOffset; + + /** {@code null-ok;} parse observer, if any */ + private ParseObserver observer; + + /** + * Constructs an instance. + * + * @param cf {@code non-null;} the class file to parse from + * @param definer {@code non-null;} class being defined + * @param offset offset in {@code bytes} to the start of the list + * @param attributeFactory {@code non-null;} attribute factory to use + */ + public MemberListParser(DirectClassFile cf, CstType definer, + int offset, AttributeFactory attributeFactory) { + if (cf == null) { + throw new NullPointerException("cf == null"); + } + + if (offset < 0) { + throw new IllegalArgumentException("offset < 0"); + } + + if (attributeFactory == null) { + throw new NullPointerException("attributeFactory == null"); + } + + this.cf = cf; + this.definer = definer; + this.offset = offset; + this.attributeFactory = attributeFactory; + this.endOffset = -1; + } + + /** + * Gets the end offset of this constant pool in the {@code byte[]} + * which it came from. + * + * @return {@code >= 0;} the end offset + */ + public int getEndOffset() { + parseIfNecessary(); + return endOffset; + } + + /** + * Sets the parse observer for this instance. + * + * @param observer {@code null-ok;} the observer + */ + public final void setObserver(ParseObserver observer) { + this.observer = observer; + } + + /** + * Runs {@link #parse} if it has not yet been run successfully. + */ + protected final void parseIfNecessary() { + if (endOffset < 0) { + parse(); + } + } + + /** + * Gets the count of elements in the list. + * + * @return the count + */ + protected final int getCount() { + ByteArray bytes = cf.getBytes(); + return bytes.getUnsignedShort(offset); + } + + /** + * Gets the class file being defined. + * + * @return {@code non-null;} the class + */ + protected final CstType getDefiner() { + return definer; + } + + /** + * Gets the human-oriented name for what this instance is parsing. + * Subclasses must override this method. + * + * @return {@code non-null;} the human oriented name + */ + protected abstract String humanName(); + + /** + * Gets the human-oriented string for the given access flags. + * Subclasses must override this method. + * + * @param accessFlags the flags + * @return {@code non-null;} the string form + */ + protected abstract String humanAccessFlags(int accessFlags); + + /** + * Gets the {@code CTX_*} constant to use when parsing attributes. + * Subclasses must override this method. + * + * @return {@code non-null;} the human oriented name + */ + protected abstract int getAttributeContext(); + + /** + * Sets an element in the list. Subclasses must override this method. + * + * @param n which element + * @param accessFlags the {@code access_flags} + * @param nat the interpreted name and type (based on the two + * {@code *_index} fields) + * @param attributes list of parsed attributes + * @return {@code non-null;} the constructed member + */ + protected abstract Member set(int n, int accessFlags, CstNat nat, + AttributeList attributes); + + /** + * Does the actual parsing. + */ + private void parse() { + int attributeContext = getAttributeContext(); + int count = getCount(); + int at = offset + 2; // Skip the count. + + ByteArray bytes = cf.getBytes(); + ConstantPool pool = cf.getConstantPool(); + + if (observer != null) { + observer.parsed(bytes, offset, 2, + humanName() + "s_count: " + Hex.u2(count)); + } + + for (int i = 0; i < count; i++) { + try { + int accessFlags = bytes.getUnsignedShort(at); + int nameIdx = bytes.getUnsignedShort(at + 2); + int descIdx = bytes.getUnsignedShort(at + 4); + CstString name = (CstString) pool.get(nameIdx); + CstString desc = (CstString) pool.get(descIdx); + + if (observer != null) { + observer.startParsingMember(bytes, at, name.getString(), + desc.getString()); + observer.parsed(bytes, at, 0, "\n" + humanName() + + "s[" + i + "]:\n"); + observer.changeIndent(1); + observer.parsed(bytes, at, 2, + "access_flags: " + + humanAccessFlags(accessFlags)); + observer.parsed(bytes, at + 2, 2, + "name: " + name.toHuman()); + observer.parsed(bytes, at + 4, 2, + "descriptor: " + desc.toHuman()); + } + + at += 6; + AttributeListParser parser = + new AttributeListParser(cf, attributeContext, at, + attributeFactory); + parser.setObserver(observer); + at = parser.getEndOffset(); + StdAttributeList attributes = parser.getList(); + attributes.setImmutable(); + CstNat nat = new CstNat(name, desc); + Member member = set(i, accessFlags, nat, attributes); + + if (observer != null) { + observer.changeIndent(-1); + observer.parsed(bytes, at, 0, "end " + humanName() + + "s[" + i + "]\n"); + observer.endParsingMember(bytes, at, name.getString(), + desc.getString(), member); + } + } catch (ParseException ex) { + ex.addContext("...while parsing " + humanName() + "s[" + i + + "]"); + throw ex; + } catch (RuntimeException ex) { + ParseException pe = new ParseException(ex); + pe.addContext("...while parsing " + humanName() + "s[" + i + + "]"); + throw pe; + } + } + + endOffset = at; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/direct/MethodListParser.java b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/MethodListParser.java new file mode 100644 index 00000000..ec32a039 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/MethodListParser.java @@ -0,0 +1,86 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.direct; + +import external.com.android.dx.cf.iface.AttributeList; +import external.com.android.dx.cf.iface.Member; +import external.com.android.dx.cf.iface.StdMethod; +import external.com.android.dx.cf.iface.StdMethodList; +import external.com.android.dx.rop.code.AccessFlags; +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstType; + +/** + * Parser for lists of methods in a class file. + */ +final /*package*/ class MethodListParser extends MemberListParser { + /** {@code non-null;} list in progress */ + final private StdMethodList methods; + + /** + * Constructs an instance. + * + * @param cf {@code non-null;} the class file to parse from + * @param definer {@code non-null;} class being defined + * @param offset offset in {@code bytes} to the start of the list + * @param attributeFactory {@code non-null;} attribute factory to use + */ + public MethodListParser(DirectClassFile cf, CstType definer, + int offset, AttributeFactory attributeFactory) { + super(cf, definer, offset, attributeFactory); + methods = new StdMethodList(getCount()); + } + + /** + * Gets the parsed list. + * + * @return {@code non-null;} the parsed list + */ + public StdMethodList getList() { + parseIfNecessary(); + return methods; + } + + /** {@inheritDoc} */ + @Override + protected String humanName() { + return "method"; + } + + /** {@inheritDoc} */ + @Override + protected String humanAccessFlags(int accessFlags) { + return AccessFlags.methodString(accessFlags); + } + + /** {@inheritDoc} */ + @Override + protected int getAttributeContext() { + return AttributeFactory.CTX_METHOD; + } + + /** {@inheritDoc} */ + @Override + protected Member set(int n, int accessFlags, CstNat nat, + AttributeList attributes) { + StdMethod meth = + new StdMethod(getDefiner(), accessFlags, nat, attributes); + + methods.set(n, meth); + return meth; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/direct/StdAttributeFactory.java b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/StdAttributeFactory.java new file mode 100644 index 00000000..f457f3ed --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/StdAttributeFactory.java @@ -0,0 +1,861 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.direct; + +import external.com.android.dx.cf.attrib.AttAnnotationDefault; +import external.com.android.dx.cf.attrib.AttBootstrapMethods; +import external.com.android.dx.cf.attrib.AttCode; +import external.com.android.dx.cf.attrib.AttConstantValue; +import external.com.android.dx.cf.attrib.AttDeprecated; +import external.com.android.dx.cf.attrib.AttEnclosingMethod; +import external.com.android.dx.cf.attrib.AttExceptions; +import external.com.android.dx.cf.attrib.AttInnerClasses; +import external.com.android.dx.cf.attrib.AttLineNumberTable; +import external.com.android.dx.cf.attrib.AttLocalVariableTable; +import external.com.android.dx.cf.attrib.AttLocalVariableTypeTable; +import external.com.android.dx.cf.attrib.AttRuntimeInvisibleAnnotations; +import external.com.android.dx.cf.attrib.AttRuntimeInvisibleParameterAnnotations; +import external.com.android.dx.cf.attrib.AttRuntimeVisibleAnnotations; +import external.com.android.dx.cf.attrib.AttRuntimeVisibleParameterAnnotations; +import external.com.android.dx.cf.attrib.AttSignature; +import external.com.android.dx.cf.attrib.AttSourceDebugExtension; +import external.com.android.dx.cf.attrib.AttSourceFile; +import external.com.android.dx.cf.attrib.AttSynthetic; +import external.com.android.dx.cf.attrib.InnerClassList; +import external.com.android.dx.cf.code.BootstrapMethodArgumentsList; +import external.com.android.dx.cf.code.BootstrapMethodsList; +import external.com.android.dx.cf.code.ByteCatchList; +import external.com.android.dx.cf.code.BytecodeArray; +import external.com.android.dx.cf.code.LineNumberList; +import external.com.android.dx.cf.code.LocalVariableList; +import external.com.android.dx.cf.iface.Attribute; +import external.com.android.dx.cf.iface.ParseException; +import external.com.android.dx.cf.iface.ParseObserver; +import external.com.android.dx.cf.iface.StdAttributeList; +import external.com.android.dx.rop.annotation.AnnotationVisibility; +import external.com.android.dx.rop.annotation.Annotations; +import external.com.android.dx.rop.annotation.AnnotationsList; +import external.com.android.dx.rop.code.AccessFlags; +import external.com.android.dx.rop.cst.Constant; +import external.com.android.dx.rop.cst.ConstantPool; +import external.com.android.dx.rop.cst.CstMethodHandle; +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.cst.TypedConstant; +import external.com.android.dx.rop.type.TypeList; +import external.com.android.dx.util.ByteArray; +import external.com.android.dx.util.Hex; +import java.io.IOException; + +/** + * Standard subclass of {@link AttributeFactory}, which knows how to parse + * all the standard attribute types. + */ +public class StdAttributeFactory + extends AttributeFactory { + /** {@code non-null;} shared instance of this class */ + public static final StdAttributeFactory THE_ONE = + new StdAttributeFactory(); + + /** + * Constructs an instance. + */ + public StdAttributeFactory() { + // This space intentionally left blank. + } + + /** {@inheritDoc} */ + @Override + protected Attribute parse0(DirectClassFile cf, int context, String name, + int offset, int length, ParseObserver observer) { + switch (context) { + case CTX_CLASS: { + if (name == AttBootstrapMethods.ATTRIBUTE_NAME) { + return bootstrapMethods(cf, offset, length, observer); + } + if (name == AttDeprecated.ATTRIBUTE_NAME) { + return deprecated(cf, offset, length, observer); + } + if (name == AttEnclosingMethod.ATTRIBUTE_NAME) { + return enclosingMethod(cf, offset, length, observer); + } + if (name == AttInnerClasses.ATTRIBUTE_NAME) { + return innerClasses(cf, offset, length, observer); + } + if (name == AttRuntimeInvisibleAnnotations.ATTRIBUTE_NAME) { + return runtimeInvisibleAnnotations(cf, offset, length, + observer); + } + if (name == AttRuntimeVisibleAnnotations.ATTRIBUTE_NAME) { + return runtimeVisibleAnnotations(cf, offset, length, + observer); + } + if (name == AttSynthetic.ATTRIBUTE_NAME) { + return synthetic(cf, offset, length, observer); + } + if (name == AttSignature.ATTRIBUTE_NAME) { + return signature(cf, offset, length, observer); + } + if (name == AttSourceDebugExtension.ATTRIBUTE_NAME) { + return sourceDebugExtension(cf, offset, length, observer); + } + if (name == AttSourceFile.ATTRIBUTE_NAME) { + return sourceFile(cf, offset, length, observer); + } + break; + } + case CTX_FIELD: { + if (name == AttConstantValue.ATTRIBUTE_NAME) { + return constantValue(cf, offset, length, observer); + } + if (name == AttDeprecated.ATTRIBUTE_NAME) { + return deprecated(cf, offset, length, observer); + } + if (name == AttRuntimeInvisibleAnnotations.ATTRIBUTE_NAME) { + return runtimeInvisibleAnnotations(cf, offset, length, + observer); + } + if (name == AttRuntimeVisibleAnnotations.ATTRIBUTE_NAME) { + return runtimeVisibleAnnotations(cf, offset, length, + observer); + } + if (name == AttSignature.ATTRIBUTE_NAME) { + return signature(cf, offset, length, observer); + } + if (name == AttSynthetic.ATTRIBUTE_NAME) { + return synthetic(cf, offset, length, observer); + } + break; + } + case CTX_METHOD: { + if (name == AttAnnotationDefault.ATTRIBUTE_NAME) { + return annotationDefault(cf, offset, length, observer); + } + if (name == AttCode.ATTRIBUTE_NAME) { + return code(cf, offset, length, observer); + } + if (name == AttDeprecated.ATTRIBUTE_NAME) { + return deprecated(cf, offset, length, observer); + } + if (name == AttExceptions.ATTRIBUTE_NAME) { + return exceptions(cf, offset, length, observer); + } + if (name == AttRuntimeInvisibleAnnotations.ATTRIBUTE_NAME) { + return runtimeInvisibleAnnotations(cf, offset, length, + observer); + } + if (name == AttRuntimeVisibleAnnotations.ATTRIBUTE_NAME) { + return runtimeVisibleAnnotations(cf, offset, length, + observer); + } + if (name == AttRuntimeInvisibleParameterAnnotations. + ATTRIBUTE_NAME) { + return runtimeInvisibleParameterAnnotations( + cf, offset, length, observer); + } + if (name == AttRuntimeVisibleParameterAnnotations. + ATTRIBUTE_NAME) { + return runtimeVisibleParameterAnnotations( + cf, offset, length, observer); + } + if (name == AttSignature.ATTRIBUTE_NAME) { + return signature(cf, offset, length, observer); + } + if (name == AttSynthetic.ATTRIBUTE_NAME) { + return synthetic(cf, offset, length, observer); + } + break; + } + case CTX_CODE: { + if (name == AttLineNumberTable.ATTRIBUTE_NAME) { + return lineNumberTable(cf, offset, length, observer); + } + if (name == AttLocalVariableTable.ATTRIBUTE_NAME) { + return localVariableTable(cf, offset, length, observer); + } + if (name == AttLocalVariableTypeTable.ATTRIBUTE_NAME) { + return localVariableTypeTable(cf, offset, length, + observer); + } + break; + } + } + + return super.parse0(cf, context, name, offset, length, observer); + } + + /** + * Parses an {@code AnnotationDefault} attribute. + */ + private Attribute annotationDefault(DirectClassFile cf, + int offset, int length, ParseObserver observer) { + if (length < 2) { + throwSeverelyTruncated(); + } + + AnnotationParser ap = + new AnnotationParser(cf, offset, length, observer); + Constant cst = ap.parseValueAttribute(); + + return new AttAnnotationDefault(cst, length); + } + + /** + * Parses a {@code BootstrapMethods} attribute. + */ + private Attribute bootstrapMethods(DirectClassFile cf, int offset, int length, + ParseObserver observer) { + if (length < 2) { + return throwSeverelyTruncated(); + } + + ByteArray bytes = cf.getBytes(); + int numMethods = bytes.getUnsignedShort(offset); + if (observer != null) { + observer.parsed(bytes, offset, 2, + "num_boostrap_methods: " + Hex.u2(numMethods)); + } + + offset += 2; + length -= 2; + + BootstrapMethodsList methods = parseBootstrapMethods(bytes, cf.getConstantPool(), + cf.getThisClass(), numMethods, + offset, length, observer); + return new AttBootstrapMethods(methods); + } + + /** + * Parses a {@code Code} attribute. + */ + private Attribute code(DirectClassFile cf, int offset, int length, + ParseObserver observer) { + if (length < 12) { + return throwSeverelyTruncated(); + } + + ByteArray bytes = cf.getBytes(); + ConstantPool pool = cf.getConstantPool(); + int maxStack = bytes.getUnsignedShort(offset); // u2 max_stack + int maxLocals = bytes.getUnsignedShort(offset + 2); // u2 max_locals + int codeLength = bytes.getInt(offset + 4); // u4 code_length + int origOffset = offset; + + if (observer != null) { + observer.parsed(bytes, offset, 2, + "max_stack: " + Hex.u2(maxStack)); + observer.parsed(bytes, offset + 2, 2, + "max_locals: " + Hex.u2(maxLocals)); + observer.parsed(bytes, offset + 4, 4, + "code_length: " + Hex.u4(codeLength)); + } + + offset += 8; + length -= 8; + + if (length < (codeLength + 4)) { + return throwTruncated(); + } + + int codeOffset = offset; + offset += codeLength; + length -= codeLength; + BytecodeArray code = + new BytecodeArray(bytes.slice(codeOffset, codeOffset + codeLength), + pool); + if (observer != null) { + code.forEach(new CodeObserver(code.getBytes(), observer)); + } + + // u2 exception_table_length + int exceptionTableLength = bytes.getUnsignedShort(offset); + ByteCatchList catches = (exceptionTableLength == 0) ? + ByteCatchList.EMPTY : + new ByteCatchList(exceptionTableLength); + + if (observer != null) { + observer.parsed(bytes, offset, 2, + "exception_table_length: " + + Hex.u2(exceptionTableLength)); + } + + offset += 2; + length -= 2; + + if (length < (exceptionTableLength * 8 + 2)) { + return throwTruncated(); + } + + for (int i = 0; i < exceptionTableLength; i++) { + if (observer != null) { + observer.changeIndent(1); + } + + int startPc = bytes.getUnsignedShort(offset); + int endPc = bytes.getUnsignedShort(offset + 2); + int handlerPc = bytes.getUnsignedShort(offset + 4); + int catchTypeIdx = bytes.getUnsignedShort(offset + 6); + CstType catchType = (CstType) pool.get0Ok(catchTypeIdx); + catches.set(i, startPc, endPc, handlerPc, catchType); + if (observer != null) { + observer.parsed(bytes, offset, 8, + Hex.u2(startPc) + ".." + Hex.u2(endPc) + + " -> " + Hex.u2(handlerPc) + " " + + ((catchType == null) ? "" : + catchType.toHuman())); + } + offset += 8; + length -= 8; + + if (observer != null) { + observer.changeIndent(-1); + } + } + + catches.setImmutable(); + + AttributeListParser parser = + new AttributeListParser(cf, CTX_CODE, offset, this); + parser.setObserver(observer); + + StdAttributeList attributes = parser.getList(); + attributes.setImmutable(); + + int attributeByteCount = parser.getEndOffset() - offset; + if (attributeByteCount != length) { + return throwBadLength(attributeByteCount + (offset - origOffset)); + } + + return new AttCode(maxStack, maxLocals, code, catches, attributes); + } + + /** + * Parses a {@code ConstantValue} attribute. + */ + private Attribute constantValue(DirectClassFile cf, int offset, int length, + ParseObserver observer) { + if (length != 2) { + return throwBadLength(2); + } + + ByteArray bytes = cf.getBytes(); + ConstantPool pool = cf.getConstantPool(); + int idx = bytes.getUnsignedShort(offset); + TypedConstant cst = (TypedConstant) pool.get(idx); + Attribute result = new AttConstantValue(cst); + + if (observer != null) { + observer.parsed(bytes, offset, 2, "value: " + cst); + } + + return result; + } + + /** + * Parses a {@code Deprecated} attribute. + */ + private Attribute deprecated(DirectClassFile cf, int offset, int length, + ParseObserver observer) { + if (length != 0) { + return throwBadLength(0); + } + + return new AttDeprecated(); + } + + /** + * Parses an {@code EnclosingMethod} attribute. + */ + private Attribute enclosingMethod(DirectClassFile cf, int offset, + int length, ParseObserver observer) { + if (length != 4) { + throwBadLength(4); + } + + ByteArray bytes = cf.getBytes(); + ConstantPool pool = cf.getConstantPool(); + + int idx = bytes.getUnsignedShort(offset); + CstType type = (CstType) pool.get(idx); + + idx = bytes.getUnsignedShort(offset + 2); + CstNat method = (CstNat) pool.get0Ok(idx); + + Attribute result = new AttEnclosingMethod(type, method); + + if (observer != null) { + observer.parsed(bytes, offset, 2, "class: " + type); + observer.parsed(bytes, offset + 2, 2, "method: " + + DirectClassFile.stringOrNone(method)); + } + + return result; + } + + /** + * Parses an {@code Exceptions} attribute. + */ + private Attribute exceptions(DirectClassFile cf, int offset, int length, + ParseObserver observer) { + if (length < 2) { + return throwSeverelyTruncated(); + } + + ByteArray bytes = cf.getBytes(); + int count = bytes.getUnsignedShort(offset); // number_of_exceptions + + if (observer != null) { + observer.parsed(bytes, offset, 2, + "number_of_exceptions: " + Hex.u2(count)); + } + + offset += 2; + length -= 2; + + if (length != (count * 2)) { + throwBadLength((count * 2) + 2); + } + + TypeList list = cf.makeTypeList(offset, count); + return new AttExceptions(list); + } + + /** + * Parses an {@code InnerClasses} attribute. + */ + private Attribute innerClasses(DirectClassFile cf, int offset, int length, + ParseObserver observer) { + if (length < 2) { + return throwSeverelyTruncated(); + } + + ByteArray bytes = cf.getBytes(); + ConstantPool pool = cf.getConstantPool(); + int count = bytes.getUnsignedShort(offset); // number_of_classes + + if (observer != null) { + observer.parsed(bytes, offset, 2, + "number_of_classes: " + Hex.u2(count)); + } + + offset += 2; + length -= 2; + + if (length != (count * 8)) { + throwBadLength((count * 8) + 2); + } + + InnerClassList list = new InnerClassList(count); + + for (int i = 0; i < count; i++) { + int innerClassIdx = bytes.getUnsignedShort(offset); + int outerClassIdx = bytes.getUnsignedShort(offset + 2); + int nameIdx = bytes.getUnsignedShort(offset + 4); + int accessFlags = bytes.getUnsignedShort(offset + 6); + CstType innerClass = (CstType) pool.get(innerClassIdx); + CstType outerClass = (CstType) pool.get0Ok(outerClassIdx); + CstString name = (CstString) pool.get0Ok(nameIdx); + list.set(i, innerClass, outerClass, name, accessFlags); + if (observer != null) { + observer.parsed(bytes, offset, 2, + "inner_class: " + + DirectClassFile.stringOrNone(innerClass)); + observer.parsed(bytes, offset + 2, 2, + " outer_class: " + + DirectClassFile.stringOrNone(outerClass)); + observer.parsed(bytes, offset + 4, 2, + " name: " + + DirectClassFile.stringOrNone(name)); + observer.parsed(bytes, offset + 6, 2, + " access_flags: " + + AccessFlags.innerClassString(accessFlags)); + } + offset += 8; + } + + list.setImmutable(); + return new AttInnerClasses(list); + } + + /** + * Parses a {@code LineNumberTable} attribute. + */ + private Attribute lineNumberTable(DirectClassFile cf, int offset, + int length, ParseObserver observer) { + if (length < 2) { + return throwSeverelyTruncated(); + } + + ByteArray bytes = cf.getBytes(); + int count = bytes.getUnsignedShort(offset); // line_number_table_length + + if (observer != null) { + observer.parsed(bytes, offset, 2, + "line_number_table_length: " + Hex.u2(count)); + } + + offset += 2; + length -= 2; + + if (length != (count * 4)) { + throwBadLength((count * 4) + 2); + } + + LineNumberList list = new LineNumberList(count); + + for (int i = 0; i < count; i++) { + int startPc = bytes.getUnsignedShort(offset); + int lineNumber = bytes.getUnsignedShort(offset + 2); + list.set(i, startPc, lineNumber); + if (observer != null) { + observer.parsed(bytes, offset, 4, + Hex.u2(startPc) + " " + lineNumber); + } + offset += 4; + } + + list.setImmutable(); + return new AttLineNumberTable(list); + } + + /** + * Parses a {@code LocalVariableTable} attribute. + */ + private Attribute localVariableTable(DirectClassFile cf, int offset, + int length, ParseObserver observer) { + if (length < 2) { + return throwSeverelyTruncated(); + } + + ByteArray bytes = cf.getBytes(); + int count = bytes.getUnsignedShort(offset); + + if (observer != null) { + observer.parsed(bytes, offset, 2, + "local_variable_table_length: " + Hex.u2(count)); + } + + LocalVariableList list = parseLocalVariables( + bytes.slice(offset + 2, offset + length), cf.getConstantPool(), + observer, count, false); + return new AttLocalVariableTable(list); + } + + /** + * Parses a {@code LocalVariableTypeTable} attribute. + */ + private Attribute localVariableTypeTable(DirectClassFile cf, int offset, + int length, ParseObserver observer) { + if (length < 2) { + return throwSeverelyTruncated(); + } + + ByteArray bytes = cf.getBytes(); + int count = bytes.getUnsignedShort(offset); + + if (observer != null) { + observer.parsed(bytes, offset, 2, + "local_variable_type_table_length: " + Hex.u2(count)); + } + + LocalVariableList list = parseLocalVariables( + bytes.slice(offset + 2, offset + length), cf.getConstantPool(), + observer, count, true); + return new AttLocalVariableTypeTable(list); + } + + /** + * Parse the table part of either a {@code LocalVariableTable} + * or a {@code LocalVariableTypeTable}. + * + * @param bytes {@code non-null;} bytes to parse, which should only + * contain the table data (no header) + * @param pool {@code non-null;} constant pool to use + * @param count {@code >= 0;} the number of entries + * @param typeTable {@code true} iff this is for a type table + * @return {@code non-null;} the constructed list + */ + private LocalVariableList parseLocalVariables(ByteArray bytes, + ConstantPool pool, ParseObserver observer, int count, + boolean typeTable) { + if (bytes.size() != (count * 10)) { + // "+ 2" is for the count. + throwBadLength((count * 10) + 2); + } + + ByteArray.MyDataInputStream in = bytes.makeDataInputStream(); + LocalVariableList list = new LocalVariableList(count); + + try { + for (int i = 0; i < count; i++) { + int startPc = in.readUnsignedShort(); + int length = in.readUnsignedShort(); + int nameIdx = in.readUnsignedShort(); + int typeIdx = in.readUnsignedShort(); + int index = in.readUnsignedShort(); + CstString name = (CstString) pool.get(nameIdx); + CstString type = (CstString) pool.get(typeIdx); + CstString descriptor = null; + CstString signature = null; + + if (typeTable) { + signature = type; + } else { + descriptor = type; + } + + list.set(i, startPc, length, name, + descriptor, signature, index); + + if (observer != null) { + observer.parsed(bytes, i * 10, 10, Hex.u2(startPc) + + ".." + Hex.u2(startPc + length) + " " + + Hex.u2(index) + " " + name.toHuman() + " " + + type.toHuman()); + } + } + } catch (IOException ex) { + throw new RuntimeException("shouldn't happen", ex); + } + + list.setImmutable(); + return list; + } + + /** + * Parses a {@code RuntimeInvisibleAnnotations} attribute. + */ + private Attribute runtimeInvisibleAnnotations(DirectClassFile cf, + int offset, int length, ParseObserver observer) { + if (length < 2) { + throwSeverelyTruncated(); + } + + AnnotationParser ap = + new AnnotationParser(cf, offset, length, observer); + Annotations annotations = + ap.parseAnnotationAttribute(AnnotationVisibility.BUILD); + + return new AttRuntimeInvisibleAnnotations(annotations, length); + } + + /** + * Parses a {@code RuntimeVisibleAnnotations} attribute. + */ + private Attribute runtimeVisibleAnnotations(DirectClassFile cf, + int offset, int length, ParseObserver observer) { + if (length < 2) { + throwSeverelyTruncated(); + } + + AnnotationParser ap = + new AnnotationParser(cf, offset, length, observer); + Annotations annotations = + ap.parseAnnotationAttribute(AnnotationVisibility.RUNTIME); + + return new AttRuntimeVisibleAnnotations(annotations, length); + } + + /** + * Parses a {@code RuntimeInvisibleParameterAnnotations} attribute. + */ + private Attribute runtimeInvisibleParameterAnnotations(DirectClassFile cf, + int offset, int length, ParseObserver observer) { + if (length < 2) { + throwSeverelyTruncated(); + } + + AnnotationParser ap = + new AnnotationParser(cf, offset, length, observer); + AnnotationsList list = + ap.parseParameterAttribute(AnnotationVisibility.BUILD); + + return new AttRuntimeInvisibleParameterAnnotations(list, length); + } + + /** + * Parses a {@code RuntimeVisibleParameterAnnotations} attribute. + */ + private Attribute runtimeVisibleParameterAnnotations(DirectClassFile cf, + int offset, int length, ParseObserver observer) { + if (length < 2) { + throwSeverelyTruncated(); + } + + AnnotationParser ap = + new AnnotationParser(cf, offset, length, observer); + AnnotationsList list = + ap.parseParameterAttribute(AnnotationVisibility.RUNTIME); + + return new AttRuntimeVisibleParameterAnnotations(list, length); + } + + /** + * Parses a {@code Signature} attribute. + */ + private Attribute signature(DirectClassFile cf, int offset, int length, + ParseObserver observer) { + if (length != 2) { + throwBadLength(2); + } + + ByteArray bytes = cf.getBytes(); + ConstantPool pool = cf.getConstantPool(); + int idx = bytes.getUnsignedShort(offset); + CstString cst = (CstString) pool.get(idx); + Attribute result = new AttSignature(cst); + + if (observer != null) { + observer.parsed(bytes, offset, 2, "signature: " + cst); + } + + return result; + } + + /** + * Parses a {@code SourceDebugExtesion} attribute. + */ + private Attribute sourceDebugExtension(DirectClassFile cf, int offset, int length, + ParseObserver observer) { + ByteArray bytes = cf.getBytes().slice(offset, offset + length); + CstString smapString = new CstString(bytes); + Attribute result = new AttSourceDebugExtension(smapString); + + if (observer != null) { + String decoded = smapString.getString(); + observer.parsed(bytes, offset, length, "sourceDebugExtension: " + decoded); + } + + return result; + } + + /** + * Parses a {@code SourceFile} attribute. + */ + private Attribute sourceFile(DirectClassFile cf, int offset, int length, + ParseObserver observer) { + if (length != 2) { + throwBadLength(2); + } + + ByteArray bytes = cf.getBytes(); + ConstantPool pool = cf.getConstantPool(); + int idx = bytes.getUnsignedShort(offset); + CstString cst = (CstString) pool.get(idx); + Attribute result = new AttSourceFile(cst); + + if (observer != null) { + observer.parsed(bytes, offset, 2, "source: " + cst); + } + + return result; + } + + /** + * Parses a {@code Synthetic} attribute. + */ + private Attribute synthetic(DirectClassFile cf, int offset, int length, + ParseObserver observer) { + if (length != 0) { + return throwBadLength(0); + } + + return new AttSynthetic(); + } + + /** + * Throws the right exception when a known attribute has a way too short + * length. + * + * @return never + * @throws ParseException always thrown + */ + private static Attribute throwSeverelyTruncated() { + throw new ParseException("severely truncated attribute"); + } + + /** + * Throws the right exception when a known attribute has a too short + * length. + * + * @return never + * @throws ParseException always thrown + */ + private static Attribute throwTruncated() { + throw new ParseException("truncated attribute"); + } + + /** + * Throws the right exception when an attribute has an unexpected length + * (given its contents). + * + * @param expected expected length + * @return never + * @throws ParseException always thrown + */ + private static Attribute throwBadLength(int expected) { + throw new ParseException("bad attribute length; expected length " + + Hex.u4(expected)); + } + + private BootstrapMethodsList parseBootstrapMethods(ByteArray bytes, ConstantPool constantPool, + CstType declaringClass, int numMethods, int offset, int length, ParseObserver observer) + throws ParseException { + BootstrapMethodsList methods = new BootstrapMethodsList(numMethods); + for (int methodIndex = 0; methodIndex < numMethods; ++methodIndex) { + if (length < 4) { + throwTruncated(); + } + + int methodRef = bytes.getUnsignedShort(offset); + int numArguments = bytes.getUnsignedShort(offset + 2); + + if (observer != null) { + observer.parsed(bytes, offset, 2, "bootstrap_method_ref: " + Hex.u2(methodRef)); + observer.parsed(bytes, offset + 2, 2, + "num_bootstrap_arguments: " + Hex.u2(numArguments)); + } + + offset += 4; + length -= 4; + if (length < numArguments * 2) { + throwTruncated(); + } + + BootstrapMethodArgumentsList arguments = new BootstrapMethodArgumentsList(numArguments); + for (int argIndex = 0; argIndex < numArguments; ++argIndex, offset += 2, length -= 2) { + int argumentRef = bytes.getUnsignedShort(offset); + if (observer != null) { + observer.parsed(bytes, offset, 2, + "bootstrap_arguments[" + argIndex + "]" + Hex.u2(argumentRef)); + } + arguments.set(argIndex, constantPool.get(argumentRef)); + } + arguments.setImmutable(); + Constant cstMethodRef = constantPool.get(methodRef); + methods.set(methodIndex, declaringClass, (CstMethodHandle) cstMethodRef, arguments); + } + methods.setImmutable(); + + if (length != 0) { + throwBadLength(length); + } + + return methods; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/direct/package.html b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/package.html new file mode 100644 index 00000000..0685664b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/direct/package.html @@ -0,0 +1,12 @@ + +

Implementation of cf.iface.* based on a direct representation +of class files as byte[]s.

+ +

PACKAGES USED: +

    +
  • external.com.android.dx.cf.attrib
  • +
  • external.com.android.dx.cf.iface
  • +
  • external.com.android.dx.rop.pool
  • +
  • external.com.android.dx.util
  • +
+ diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Attribute.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Attribute.java new file mode 100644 index 00000000..61d628cb --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Attribute.java @@ -0,0 +1,38 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +/** + * Interface representing attributes of class files (directly or indirectly). + */ +public interface Attribute { + /** + * Get the name of the attribute. + * + * @return {@code non-null;} the name + */ + public String getName(); + + /** + * Get the total length of the attribute in bytes, including the + * header. Since the header is always six bytes, the result of + * this method is always at least {@code 6}. + * + * @return {@code >= 6;} the total length, in bytes + */ + public int byteLength(); +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/AttributeList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/AttributeList.java new file mode 100644 index 00000000..01a0ef7c --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/AttributeList.java @@ -0,0 +1,75 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +/** + * Interface for lists of attributes. + */ +public interface AttributeList { + /** + * Get whether this instance is mutable. Note that the + * {@code AttributeList} interface itself doesn't provide any means + * of mutation, but that doesn't mean that there isn't a non-interface + * way of mutating an instance. + * + * @return {@code true} iff this instance is somehow mutable + */ + public boolean isMutable(); + + /** + * Get the number of attributes in the list. + * + * @return the size + */ + public int size(); + + /** + * Get the {@code n}th attribute. + * + * @param n {@code n >= 0, n < size();} which attribute + * @return {@code non-null;} the attribute in question + */ + public Attribute get(int n); + + /** + * Get the total length of this list in bytes, when part of a + * class file. The returned value includes the two bytes for the + * {@code attributes_count} length indicator. + * + * @return {@code >= 2;} the total length, in bytes + */ + public int byteLength(); + + /** + * Get the first attribute in the list with the given name, if any. + * + * @param name {@code non-null;} attribute name + * @return {@code null-ok;} first attribute in the list with the given name, + * or {@code null} if there is none + */ + public Attribute findFirst(String name); + + /** + * Get the next attribute in the list after the given one, with the same + * name, if any. + * + * @param attrib {@code non-null;} attribute to start looking after + * @return {@code null-ok;} next attribute after {@code attrib} with the + * same name as {@code attrib} + */ + public Attribute findNext(Attribute attrib); +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/ClassFile.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/ClassFile.java new file mode 100644 index 00000000..e8afe576 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/ClassFile.java @@ -0,0 +1,131 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dx.cf.code.BootstrapMethodsList; +import external.com.android.dx.rop.cst.ConstantPool; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.TypeList; + +/** + * Interface for things which purport to be class files or reasonable + * facsimiles thereof. + * + *

Note: The fields referred to in this documentation are of the + * {@code ClassFile} structure defined in vmspec-2 sec4.1. + */ +public interface ClassFile extends HasAttribute { + /** + * Gets the field {@code magic}. + * + * @return the value in question + */ + public int getMagic(); + + /** + * Gets the field {@code minor_version}. + * + * @return the value in question + */ + public int getMinorVersion(); + + /** + * Gets the field {@code major_version}. + * + * @return the value in question + */ + public int getMajorVersion(); + + /** + * Gets the field {@code access_flags}. + * + * @return the value in question + */ + public int getAccessFlags(); + + /** + * Gets the field {@code this_class}, interpreted as a type constant. + * + * @return {@code non-null;} the value in question + */ + public CstType getThisClass(); + + /** + * Gets the field {@code super_class}, interpreted as a type constant + * if non-zero. + * + * @return {@code null-ok;} the value in question + */ + public CstType getSuperclass(); + + /** + * Gets the field {@code constant_pool} (along with + * {@code constant_pool_count}). + * + * @return {@code non-null;} the constant pool + */ + public ConstantPool getConstantPool(); + + /** + * Gets the field {@code interfaces} (along with + * {@code interfaces_count}). + * + * @return {@code non-null;} the list of interfaces + */ + public TypeList getInterfaces(); + + /** + * Gets the field {@code fields} (along with + * {@code fields_count}). + * + * @return {@code non-null;} the list of fields + */ + public FieldList getFields(); + + /** + * Gets the field {@code methods} (along with + * {@code methods_count}). + * + * @return {@code non-null;} the list of fields + */ + public MethodList getMethods(); + + /** + * Gets the field {@code attributes} (along with + * {@code attributes_count}). + * + * @return {@code non-null;} the list of attributes + */ + @Override + public AttributeList getAttributes(); + + /** + * Gets the bootstrap method {@code attributes}. + * @return {@code non-null;} the list of bootstrap methods + */ + public BootstrapMethodsList getBootstrapMethods(); + + /** + * Gets the name out of the {@code SourceFile} attribute of this + * file, if any. This is a convenient shorthand for scrounging around + * the class's attributes. + * + * @return {@code non-null;} the constant pool + */ + public CstString getSourceFile(); +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Field.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Field.java new file mode 100644 index 00000000..dd549bf0 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Field.java @@ -0,0 +1,35 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dx.rop.cst.TypedConstant; + +/** + * Interface representing fields of class files. + */ +public interface Field + extends Member { + /** + * Get the constant value for this field, if any. This only returns + * non-{@code null} for a {@code static final} field which + * includes a {@code ConstantValue} attribute. + * + * @return {@code null-ok;} the constant value, or {@code null} if this + * field isn't a constant + */ + public TypedConstant getConstantValue(); +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/FieldList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/FieldList.java new file mode 100644 index 00000000..7b82c59b --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/FieldList.java @@ -0,0 +1,48 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +/** + * Interface for lists of fields. + */ +public interface FieldList +{ + /** + * Get whether this instance is mutable. Note that the + * {@code FieldList} interface itself doesn't provide any means + * of mutation, but that doesn't mean that there isn't a non-interface + * way of mutating an instance. + * + * @return {@code true} iff this instance is somehow mutable + */ + public boolean isMutable(); + + /** + * Get the number of fields in the list. + * + * @return the size + */ + public int size(); + + /** + * Get the {@code n}th field. + * + * @param n {@code n >= 0, n < size();} which field + * @return {@code non-null;} the field in question + */ + public Field get(int n); +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/HasAttribute.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/HasAttribute.java new file mode 100644 index 00000000..9a86d013 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/HasAttribute.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2014 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. + */ + +package external.com.android.dx.cf.iface; + +/** + * An element that can have {@link Attribute} + */ +public interface HasAttribute { + + /** + * Get the element {@code attributes} (along with + * {@code attributes_count}). + * + * @return {@code non-null;} the attributes list + */ + public AttributeList getAttributes(); + +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Member.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Member.java new file mode 100644 index 00000000..c68aa32e --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Member.java @@ -0,0 +1,75 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; + +/** + * Interface representing members of class files (that is, fields and methods). + */ +public interface Member extends HasAttribute { + /** + * Get the defining class. + * + * @return {@code non-null;} the defining class + */ + public CstType getDefiningClass(); + + /** + * Get the field {@code access_flags}. + * + * @return the access flags + */ + public int getAccessFlags(); + + /** + * Get the field {@code name_index} of the member. This is + * just a convenient shorthand for {@code getNat().getName()}. + * + * @return {@code non-null;} the name + */ + public CstString getName(); + + /** + * Get the field {@code descriptor_index} of the member. This is + * just a convenient shorthand for {@code getNat().getDescriptor()}. + * + * @return {@code non-null;} the descriptor + */ + public CstString getDescriptor(); + + /** + * Get the name and type associated with this member. This is a + * combination of the fields {@code name_index} and + * {@code descriptor_index} in the original classfile, interpreted + * via the constant pool. + * + * @return {@code non-null;} the name and type + */ + public CstNat getNat(); + + /** + * Get the field {@code attributes} (along with + * {@code attributes_count}). + * + * @return {@code non-null;} the constant pool + */ + @Override + public AttributeList getAttributes(); +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Method.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Method.java new file mode 100644 index 00000000..d96b2f9c --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/Method.java @@ -0,0 +1,34 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dx.rop.type.Prototype; + +/** + * Interface representing methods of class files. + */ +public interface Method + extends Member +{ + /** + * Get the effective method descriptor, which includes, if + * necessary, a first {@code this} parameter. + * + * @return {@code non-null;} the effective method descriptor + */ + public Prototype getEffectiveDescriptor(); +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/MethodList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/MethodList.java new file mode 100644 index 00000000..68382f24 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/MethodList.java @@ -0,0 +1,47 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +/** + * Interface for lists of methods. + */ +public interface MethodList { + /** + * Get whether this instance is mutable. Note that the + * {@code MethodList} interface itself doesn't provide any means + * of mutation, but that doesn't mean that there isn't a non-interface + * way of mutating an instance. + * + * @return {@code true} iff this instance is somehow mutable + */ + public boolean isMutable(); + + /** + * Get the number of methods in the list. + * + * @return the size + */ + public int size(); + + /** + * Get the {@code n}th method. + * + * @param n {@code n >= 0, n < size();} which method + * @return {@code non-null;} the method in question + */ + public Method get(int n); +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/ParseException.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/ParseException.java new file mode 100644 index 00000000..28c5c3d7 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/ParseException.java @@ -0,0 +1,37 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dex.util.ExceptionWithContext; + +/** + * Exception from parsing. + */ +public class ParseException + extends ExceptionWithContext { + public ParseException(String message) { + super(message); + } + + public ParseException(Throwable cause) { + super(cause); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/ParseObserver.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/ParseObserver.java new file mode 100644 index 00000000..7449e68e --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/ParseObserver.java @@ -0,0 +1,68 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dx.util.ByteArray; + +/** + * Observer of parsing in action. This is used to supply feedback from + * the various things that parse particularly to the dumping utilities. + */ +public interface ParseObserver { + /** + * Indicate that the level of indentation for a dump should increase + * or decrease (positive or negative argument, respectively). + * + * @param indentDelta the amount to change indentation + */ + public void changeIndent(int indentDelta); + + /** + * Indicate that a particular member is now being parsed. + * + * @param bytes {@code non-null;} the source that is being parsed + * @param offset offset into {@code bytes} for the start of the + * member + * @param name {@code non-null;} name of the member + * @param descriptor {@code non-null;} descriptor of the member + */ + public void startParsingMember(ByteArray bytes, int offset, String name, + String descriptor); + + /** + * Indicate that a particular member is no longer being parsed. + * + * @param bytes {@code non-null;} the source that was parsed + * @param offset offset into {@code bytes} for the end of the + * member + * @param name {@code non-null;} name of the member + * @param descriptor {@code non-null;} descriptor of the member + * @param member {@code non-null;} the actual member that was parsed + */ + public void endParsingMember(ByteArray bytes, int offset, String name, + String descriptor, Member member); + + /** + * Indicate that some parsing happened. + * + * @param bytes {@code non-null;} the source that was parsed + * @param offset offset into {@code bytes} for what was parsed + * @param len number of bytes parsed + * @param human {@code non-null;} human form for what was parsed + */ + public void parsed(ByteArray bytes, int offset, int len, String human); +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdAttributeList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdAttributeList.java new file mode 100644 index 00000000..6040356f --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdAttributeList.java @@ -0,0 +1,108 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dx.util.FixedSizeList; + +/** + * Standard implementation of {@link AttributeList}, which directly stores + * an array of {@link Attribute} objects and can be made immutable. + */ +public final class StdAttributeList extends FixedSizeList + implements AttributeList { + /** + * Constructs an instance. All indices initially contain {@code null}. + * + * @param size the size of the list + */ + public StdAttributeList(int size) { + super(size); + } + + /** {@inheritDoc} */ + @Override + public Attribute get(int n) { + return (Attribute) get0(n); + } + + /** {@inheritDoc} */ + @Override + public int byteLength() { + int sz = size(); + int result = 2; // u2 attributes_count + + for (int i = 0; i < sz; i++) { + result += get(i).byteLength(); + } + + return result; + } + + /** {@inheritDoc} */ + @Override + public Attribute findFirst(String name) { + int sz = size(); + + for (int i = 0; i < sz; i++) { + Attribute att = get(i); + if (att.getName().equals(name)) { + return att; + } + } + + return null; + } + + /** {@inheritDoc} */ + @Override + public Attribute findNext(Attribute attrib) { + int sz = size(); + int at; + + outer: { + for (at = 0; at < sz; at++) { + Attribute att = get(at); + if (att == attrib) { + break outer; + } + } + + return null; + } + + String name = attrib.getName(); + + for (at++; at < sz; at++) { + Attribute att = get(at); + if (att.getName().equals(name)) { + return att; + } + } + + return null; + } + + /** + * Sets the attribute at the given index. + * + * @param n {@code >= 0, < size();} which attribute + * @param attribute {@code null-ok;} the attribute object + */ + public void set(int n, Attribute attribute) { + set0(n, attribute); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdField.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdField.java new file mode 100644 index 00000000..d6314a2a --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdField.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dx.cf.attrib.AttConstantValue; +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.cst.TypedConstant; + +/** + * Standard implementation of {@link Field}, which directly stores + * all the associated data. + */ +public final class StdField extends StdMember implements Field { + /** + * Constructs an instance. + * + * @param definingClass {@code non-null;} the defining class + * @param accessFlags access flags + * @param nat {@code non-null;} member name and type (descriptor) + * @param attributes {@code non-null;} list of associated attributes + */ + public StdField(CstType definingClass, int accessFlags, CstNat nat, + AttributeList attributes) { + super(definingClass, accessFlags, nat, attributes); + } + + /** {@inheritDoc} */ + @Override + public TypedConstant getConstantValue() { + AttributeList attribs = getAttributes(); + AttConstantValue cval = (AttConstantValue) + attribs.findFirst(AttConstantValue.ATTRIBUTE_NAME); + + if (cval == null) { + return null; + } + + return cval.getConstantValue(); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdFieldList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdFieldList.java new file mode 100644 index 00000000..df62021f --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdFieldList.java @@ -0,0 +1,50 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dx.util.FixedSizeList; + +/** + * Standard implementation of {@link FieldList}, which directly stores + * an array of {@link Field} objects and can be made immutable. + */ +public final class StdFieldList extends FixedSizeList implements FieldList { + /** + * Constructs an instance. All indices initially contain {@code null}. + * + * @param size the size of the list + */ + public StdFieldList(int size) { + super(size); + } + + /** {@inheritDoc} */ + @Override + public Field get(int n) { + return (Field) get0(n); + } + + /** + * Sets the field at the given index. + * + * @param n {@code >= 0, < size();} which field + * @param field {@code null-ok;} the field object + */ + public void set(int n, Field field) { + set0(n, field); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMember.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMember.java new file mode 100644 index 00000000..e7930c27 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMember.java @@ -0,0 +1,116 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstString; +import external.com.android.dx.rop.cst.CstType; + +/** + * Standard implementation of {@link Member}, which directly stores + * all the associated data. + */ +public abstract class StdMember implements Member { + /** {@code non-null;} the defining class */ + private final CstType definingClass; + + /** access flags */ + private final int accessFlags; + + /** {@code non-null;} member name and type */ + private final CstNat nat; + + /** {@code non-null;} list of associated attributes */ + private final AttributeList attributes; + + /** + * Constructs an instance. + * + * @param definingClass {@code non-null;} the defining class + * @param accessFlags access flags + * @param nat {@code non-null;} member name and type (descriptor) + * @param attributes {@code non-null;} list of associated attributes + */ + public StdMember(CstType definingClass, int accessFlags, CstNat nat, + AttributeList attributes) { + if (definingClass == null) { + throw new NullPointerException("definingClass == null"); + } + + if (nat == null) { + throw new NullPointerException("nat == null"); + } + + if (attributes == null) { + throw new NullPointerException("attributes == null"); + } + + this.definingClass = definingClass; + this.accessFlags = accessFlags; + this.nat = nat; + this.attributes = attributes; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(100); + + sb.append(getClass().getName()); + sb.append('{'); + sb.append(nat.toHuman()); + sb.append('}'); + + return sb.toString(); + } + + /** {@inheritDoc} */ + @Override + public final CstType getDefiningClass() { + return definingClass; + } + + /** {@inheritDoc} */ + @Override + public final int getAccessFlags() { + return accessFlags; + } + + /** {@inheritDoc} */ + @Override + public final CstNat getNat() { + return nat; + } + + /** {@inheritDoc} */ + @Override + public final CstString getName() { + return nat.getName(); + } + + /** {@inheritDoc} */ + @Override + public final CstString getDescriptor() { + return nat.getDescriptor(); + } + + /** {@inheritDoc} */ + @Override + public final AttributeList getAttributes() { + return attributes; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMethod.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMethod.java new file mode 100644 index 00000000..3abc4d48 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMethod.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dx.rop.code.AccessFlags; +import external.com.android.dx.rop.cst.CstNat; +import external.com.android.dx.rop.cst.CstType; +import external.com.android.dx.rop.type.Prototype; + +/** + * Standard implementation of {@link Method}, which directly stores + * all the associated data. + */ +public final class StdMethod extends StdMember implements Method { + /** {@code non-null;} the effective method descriptor */ + private final Prototype effectiveDescriptor; + + /** + * Constructs an instance. + * + * @param definingClass {@code non-null;} the defining class + * @param accessFlags access flags + * @param nat {@code non-null;} member name and type (descriptor) + * @param attributes {@code non-null;} list of associated attributes + */ + public StdMethod(CstType definingClass, int accessFlags, CstNat nat, + AttributeList attributes) { + super(definingClass, accessFlags, nat, attributes); + + String descStr = getDescriptor().getString(); + effectiveDescriptor = + Prototype.intern(descStr, definingClass.getClassType(), + AccessFlags.isStatic(accessFlags), + nat.isInstanceInit()); + } + + /** {@inheritDoc} */ + @Override + public Prototype getEffectiveDescriptor() { + return effectiveDescriptor; + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMethodList.java b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMethodList.java new file mode 100644 index 00000000..095a8678 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/StdMethodList.java @@ -0,0 +1,50 @@ +/* + * 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. + */ + +package external.com.android.dx.cf.iface; + +import external.com.android.dx.util.FixedSizeList; + +/** + * Standard implementation of {@link MethodList}, which directly stores + * an array of {@link Method} objects and can be made immutable. + */ +public final class StdMethodList extends FixedSizeList implements MethodList { + /** + * Constructs an instance. All indices initially contain {@code null}. + * + * @param size the size of the list + */ + public StdMethodList(int size) { + super(size); + } + + /** {@inheritDoc} */ + @Override + public Method get(int n) { + return (Method) get0(n); + } + + /** + * Sets the method at the given index. + * + * @param n {@code >= 0, < size();} which method + * @param method {@code null-ok;} the method object + */ + public void set(int n, Method method) { + set0(n, method); + } +} diff --git a/dalvikdx/src/main/java/external/com/android/dx/cf/iface/package.html b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/package.html new file mode 100644 index 00000000..4e41dac1 --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/cf/iface/package.html @@ -0,0 +1,10 @@ + +

Interfaces and base classes for dealing with class files. This package +doesn't have any parsing but does have basic container implementations.

+ +

PACKAGES USED: +

    +
  • external.com.android.dx.rop.pool
  • +
  • external.com.android.dx.util
  • +
+ diff --git a/dalvikdx/src/main/java/external/com/android/dx/command/Main.java b/dalvikdx/src/main/java/external/com/android/dx/command/Main.java new file mode 100644 index 00000000..ca57fd8d --- /dev/null +++ b/dalvikdx/src/main/java/external/com/android/dx/command/Main.java @@ -0,0 +1,178 @@ +/* + * 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. + */ + +package external.com.android.dx.command; + +import external.com.android.dx.Version; + +/** + * Main class for dx. It recognizes enough options to be able to dispatch + * to the right "actual" main. + */ +public class Main { + private static final String USAGE_MESSAGE = + "usage:\n" + + " dx --dex [--debug] [--verbose] [--positions=