Merge "Update AppDataDirGuesser for BaseDexClassLoader"
diff --git a/src/main/java/com/google/dexmaker/AppDataDirGuesser.java b/src/main/java/com/google/dexmaker/AppDataDirGuesser.java
index 86f34a1..4479887 100644
--- a/src/main/java/com/google/dexmaker/AppDataDirGuesser.java
+++ b/src/main/java/com/google/dexmaker/AppDataDirGuesser.java
@@ -61,12 +61,74 @@
}
// Parsing toString() method: yuck. But no other way to get the path.
- // Strip out the bit between square brackets, that's our path.
String result = classLoader.toString();
- int index = result.lastIndexOf('[');
- result = (index == -1) ? result : result.substring(index + 1);
- index = result.indexOf(']');
- return (index == -1) ? result : result.substring(0, index);
+ return processClassLoaderString(result);
+ }
+
+ /**
+ * Given the result of a ClassLoader.toString() call, process the result so that guessPath
+ * can use it. There are currently two variants. For Android 4.3 and later, the string
+ * "DexPathList" should be recognized and the array of dex path elements is parsed. for
+ * earlier versions, the last nested array ('[' ... ']') is enclosing the string we are
+ * interested in.
+ */
+ static String processClassLoaderString(String input) {
+ if (input.contains("DexPathList")) {
+ return processClassLoaderString43OrLater(input);
+ } else {
+ return processClassLoaderString42OrEarlier(input);
+ }
+ }
+
+ private static String processClassLoaderString42OrEarlier(String input) {
+ /* The toString output looks like this:
+ * dalvik.system.PathClassLoader[dexPath=path/to/apk,libraryPath=path/to/libs]
+ */
+ int index = input.lastIndexOf('[');
+ input = (index == -1) ? input : input.substring(index + 1);
+ index = input.indexOf(']');
+ input = (index == -1) ? input : input.substring(0, index);
+ return input;
+ }
+
+ private static String processClassLoaderString43OrLater(String input) {
+ /* The toString output looks like this:
+ * dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/{NAME}", ...], nativeLibraryDirectories=[...]]]
+ */
+ int start = input.indexOf("DexPathList") + "DexPathList".length();
+ if (input.length() > start + 4) { // [[ + ]]
+ String trimmed = input.substring(start);
+ int end = trimmed.indexOf(']');
+ if (trimmed.charAt(0) == '[' && trimmed.charAt(1) == '[' && end >= 0) {
+ trimmed = trimmed.substring(2, end);
+ // Comma-separated list, Arrays.toString output.
+ String split[] = trimmed.split(",");
+
+ // Clean up parts. Each path element is the type of the element plus the path in
+ // quotes.
+ for (int i = 0; i < split.length; i++) {
+ int quoteStart = split[i].indexOf('"');
+ int quoteEnd = split[i].lastIndexOf('"');
+ if (quoteStart > 0 && quoteStart < quoteEnd) {
+ split[i] = split[i].substring(quoteStart + 1, quoteEnd);
+ }
+ }
+
+ // Need to rejoin components.
+ StringBuilder sb = new StringBuilder();
+ for (String s : split) {
+ if (sb.length() > 0) {
+ sb.append(':');
+ }
+ sb.append(s);
+ }
+ return sb.toString();
+ }
+ }
+
+ // This is technically a parsing failure. Return the original string, maybe a later
+ // stage can still salvage this.
+ return input;
}
File[] guessPath(String input) {
diff --git a/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java b/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java
index bbceaa3..b638509 100644
--- a/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java
+++ b/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java
@@ -16,7 +16,10 @@
package com.google.dexmaker;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
import java.io.File;
+import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@@ -75,6 +78,72 @@
AppDataDirGuesser.splitPathList("dexPath=foo:bar,bazPath=bar:bar2")));
}
+ public void testPre43PathProcessing() {
+ String input = "dalvik.system.PathClassLoader[dexPath=/data/app/abc-1.apk," +
+ "libraryPath=/data/app-lib/abc-1]";
+ String processed = AppDataDirGuesser.processClassLoaderString(input);
+ assertTrue("dexPath=/data/app/abc-1.apk,libraryPath=/data/app-lib/abc-1".equals(processed));
+ }
+
+ public void test43PathProcessing() {
+ String input = "dalvik.system.PathClassLoader[DexPathList[[zip file " +
+ "\"/data/app/abc-1/base.apk\", zip file \"/data/app/def-1/base.apk\"], " +
+ "nativeLibraryDirectories=[/data/app-lib/abc-1]]]";
+ String processed = AppDataDirGuesser.processClassLoaderString(input);
+ assertTrue("/data/app/abc-1/base.apk:/data/app/def-1/base.apk".equals(processed));
+ }
+
+ // Try to find the SDK level of the device.
+ private int getSDKLevel() {
+ // Maybe the version is reflected into the system properties correctly.
+ String level = System.getProperty("ro.build.version.sdk");
+ try {
+ return Integer.parseInt(level);
+ } catch (Exception ignored) {
+ }
+
+ // Run getprop and parse the result.
+ try {
+ Process p = Runtime.getRuntime().exec("/system/bin/getprop ro.build.version.sdk");
+ int exitValue = p.waitFor();
+ if (exitValue == 0) {
+ String line =
+ new BufferedReader(new InputStreamReader(p.getInputStream())).readLine();
+ if (line != null) {
+ return Integer.parseInt(line);
+ }
+ }
+ } catch (Exception ignored) {
+ }
+
+ // It would be nice to access android.os.Build.SDK_INT. However, that bottoms out in some
+ // native code reading system properties. Try to load the library and *hope* that the
+ // methods don't need registration code. Note: this will likely fail.
+ try {
+ // Need to load android_runtime.
+ System.loadLibrary("android_runtime");
+ Class<?> buildClass = Class.forName("android.os.Build");
+ java.lang.reflect.Field field = buildClass.getDeclaredField("SDK_INT");
+ return field.getInt(null);
+ } catch (Throwable exc) {
+ // This is already the fallback of the fallback, so throw an unchecked exception.
+ throw new RuntimeException(exc);
+ }
+ }
+
+ public void testApiLevel17PlusPathProcessing() {
+ int level = getSDKLevel();
+ if (level >= 17) {
+ // Our processing should work for anything >= Android 4.2.
+ String input = getClass().getClassLoader().toString();
+ String processed = AppDataDirGuesser.processClassLoaderString(input);
+ // A tighter check would be interesting. But vogar doesn't run the tests in a directory
+ // recognized by the guesser (usually under /data/local/tmp), so we cannot use the
+ // processed result as input to guessPath.
+ assertTrue(!input.equals(processed));
+ }
+ }
+
private interface TestCondition {
TestCondition withNonWriteable(String... files);
void shouldGive(String... files);