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 00000000..36cd86b3 Binary files /dev/null and b/Bridge/libs/framework-stub.jar differ 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 00000000..d733ee64 Binary files /dev/null and b/Core/template_override/system/framework/eddalvikdx.dex differ diff --git a/Core/template_override/system/framework/eddexmaker.dex b/Core/template_override/system/framework/eddexmaker.dex new file mode 100644 index 00000000..b6fd1bc1 Binary files /dev/null and b/Core/template_override/system/framework/eddexmaker.dex differ diff --git a/README.md b/README.md index c3ee38fb..fe531e2c 100644 --- a/README.md +++ b/README.md @@ -1 +1,27 @@ -# EdXposed \ No newline at end of file +# EdXposed + +A Riru module trying to provide a ART hooking framework (mainly for Android Pie) which delivers a consistent APIs with the OG Xposed, leveraging YAHFA hooking framework. + +## Credits + +- [YAHFA](https://github.com/rk700/YAHFA): the core java hooking framework +- [Riru](https://github.com/RikkaApps/Riru): provides a way to inject codes to zygote process +- [XposedBridge](https://github.com/rovo89/XposedBridge): the OG xposed framework APIs +- [dexmaker](https://github.com/linkedin/dexmaker) and [dalvikdx](https://github.com/JakeWharton/dalvik-dx): dynamiclly generate YAHFA hooker classes + +## Known issues + +- resources hooking is not supported yet +- may not be compatible with all ART devices +- only a few Xposed modules has been tested for working + +## Build + +1. run `:Bridge:makeAndCopyRelease` in Gradle window to build `edxposed.dex` +2. run `:Core:zipRelease` to build Magisk Riru module flashable zip file +3. find the flashable under `Core/release/` +4. flash the zip in recovery mode or in Magisk Manager + +## Contribute + +Apparently this framework is far from stable and all kinds of PRs are welcome. :) \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..7d4bf63d --- /dev/null +++ b/build.gradle @@ -0,0 +1,32 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.2.1' + + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +ext { + minSdkVersion = 23 + targetSdkVersion = 28 +} diff --git a/build.sh b/build.sh new file mode 100644 index 00000000..6cbaed67 --- /dev/null +++ b/build.sh @@ -0,0 +1,72 @@ +#!/bin/bash +MODULE_NAME=$1 +if [ "$MODULE_NAME" == "" ]; then + echo "Usage: sh build.sh []" + 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=