1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.dx; 18 19 import java.io.File; 20 import java.lang.reflect.Field; 21 import java.lang.reflect.Method; 22 import java.util.ArrayList; 23 import java.util.List; 24 25 /** 26 * Uses heuristics to guess the application's private data directory. 27 */ 28 class AppDataDirGuesser { 29 // Copied from UserHandle, indicates range of uids allocated for a user. 30 public static final int PER_USER_RANGE = 100000; 31 guess()32 public File guess() { 33 try { 34 ClassLoader classLoader = guessSuitableClassLoader(); 35 // Check that we have an instance of the PathClassLoader. 36 Class<?> clazz = Class.forName("dalvik.system.PathClassLoader"); 37 clazz.cast(classLoader); 38 // Use the toString() method to calculate the data directory. 39 String pathFromThisClassLoader = getPathFromThisClassLoader(classLoader, clazz); 40 File[] results = guessPath(pathFromThisClassLoader); 41 if (results.length > 0) { 42 return results[0]; 43 } 44 } catch (ClassCastException ignored) { 45 } catch (ClassNotFoundException ignored) { 46 } 47 return null; 48 } 49 guessSuitableClassLoader()50 private ClassLoader guessSuitableClassLoader() { 51 return AppDataDirGuesser.class.getClassLoader(); 52 } 53 getPathFromThisClassLoader(ClassLoader classLoader, Class<?> pathClassLoaderClass)54 private String getPathFromThisClassLoader(ClassLoader classLoader, Class<?> pathClassLoaderClass) { 55 // Prior to ICS, we can simply read the "path" field of the 56 // PathClassLoader. 57 try { 58 Field pathField = pathClassLoaderClass.getDeclaredField("path"); 59 pathField.setAccessible(true); 60 return (String) pathField.get(classLoader); 61 } catch (NoSuchFieldException ignored) { 62 } catch (IllegalAccessException ignored) { 63 } catch (ClassCastException ignored) { 64 } 65 66 // Parsing toString() method: yuck. But no other way to get the path. 67 String result = classLoader.toString(); 68 return processClassLoaderString(result); 69 } 70 71 /** 72 * Given the result of a ClassLoader.toString() call, process the result so that guessPath 73 * can use it. There are currently two variants. For Android 4.3 and later, the string 74 * "DexPathList" should be recognized and the array of dex path elements is parsed. for 75 * earlier versions, the last nested array ('[' ... ']') is enclosing the string we are 76 * interested in. 77 */ processClassLoaderString(String input)78 static String processClassLoaderString(String input) { 79 if (input.contains("DexPathList")) { 80 return processClassLoaderString43OrLater(input); 81 } else { 82 return processClassLoaderString42OrEarlier(input); 83 } 84 } 85 processClassLoaderString42OrEarlier(String input)86 private static String processClassLoaderString42OrEarlier(String input) { 87 /* The toString output looks like this: 88 * dalvik.system.PathClassLoader[dexPath=path/to/apk,libraryPath=path/to/libs] 89 */ 90 int index = input.lastIndexOf('['); 91 input = (index == -1) ? input : input.substring(index + 1); 92 index = input.indexOf(']'); 93 input = (index == -1) ? input : input.substring(0, index); 94 return input; 95 } 96 processClassLoaderString43OrLater(String input)97 private static String processClassLoaderString43OrLater(String input) { 98 /* The toString output looks like this: 99 * dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/{NAME}", ...], nativeLibraryDirectories=[...]]] 100 */ 101 int start = input.indexOf("DexPathList") + "DexPathList".length(); 102 if (input.length() > start + 4) { // [[ + ]] 103 String trimmed = input.substring(start); 104 int end = trimmed.indexOf(']'); 105 if (trimmed.charAt(0) == '[' && trimmed.charAt(1) == '[' && end >= 0) { 106 trimmed = trimmed.substring(2, end); 107 // Comma-separated list, Arrays.toString output. 108 String split[] = trimmed.split(","); 109 110 // Clean up parts. Each path element is the type of the element plus the path in 111 // quotes. 112 for (int i = 0; i < split.length; i++) { 113 int quoteStart = split[i].indexOf('"'); 114 int quoteEnd = split[i].lastIndexOf('"'); 115 if (quoteStart > 0 && quoteStart < quoteEnd) { 116 split[i] = split[i].substring(quoteStart + 1, quoteEnd); 117 } 118 } 119 120 // Need to rejoin components. 121 StringBuilder sb = new StringBuilder(); 122 for (String s : split) { 123 if (sb.length() > 0) { 124 sb.append(':'); 125 } 126 sb.append(s); 127 } 128 return sb.toString(); 129 } 130 } 131 132 // This is technically a parsing failure. Return the original string, maybe a later 133 // stage can still salvage this. 134 return input; 135 } 136 guessPath(String input)137 File[] guessPath(String input) { 138 List<File> results = new ArrayList<>(); 139 for (String potential : splitPathList(input)) { 140 if (!potential.startsWith("/data/app/")) { 141 continue; 142 } 143 int start = "/data/app/".length(); 144 int end = potential.lastIndexOf(".apk"); 145 if (end != potential.length() - 4) { 146 continue; 147 } 148 int dash = potential.indexOf("-"); 149 if (dash != -1) { 150 end = dash; 151 } 152 String packageName = potential.substring(start, end); 153 File dataDir = getWriteableDirectory("/data/data/" + packageName); 154 155 if (dataDir == null) { 156 // If we can't access "/data/data", try to guess user specific data directory. 157 dataDir = guessUserDataDirectory(packageName); 158 } 159 160 if (dataDir != null) { 161 File cacheDir = new File(dataDir, "cache"); 162 // The cache directory might not exist -- create if necessary 163 if (fileOrDirExists(cacheDir) || cacheDir.mkdir()) { 164 if (isWriteableDirectory(cacheDir)) { 165 results.add(cacheDir); 166 } 167 } 168 } 169 } 170 return results.toArray(new File[results.size()]); 171 } 172 splitPathList(String input)173 static String[] splitPathList(String input) { 174 String trimmed = input; 175 if (input.startsWith("dexPath=")) { 176 int start = "dexPath=".length(); 177 int end = input.indexOf(','); 178 179 trimmed = (end == -1) ? input.substring(start) : input.substring(start, end); 180 } 181 182 return trimmed.split(":"); 183 } 184 fileOrDirExists(File file)185 boolean fileOrDirExists(File file) { 186 return file.exists(); 187 } 188 isWriteableDirectory(File file)189 boolean isWriteableDirectory(File file) { 190 return file.isDirectory() && file.canWrite(); 191 } 192 getProcessUid()193 Integer getProcessUid() { 194 /* Uses reflection to try to fetch process UID. It will only work when executing on 195 * Android device. Otherwise, returns null. 196 */ 197 try { 198 Method myUid = Class.forName("android.os.Process").getMethod("myUid"); 199 200 // Invoke the method on a null instance, since it's a static method. 201 return (Integer) myUid.invoke(/* instance= */ null); 202 } catch (Exception e) { 203 // Catch any exceptions thrown and default to returning a null. 204 return null; 205 } 206 } 207 guessUserDataDirectory(String packageName)208 File guessUserDataDirectory(String packageName) { 209 Integer uid = getProcessUid(); 210 if (uid == null) { 211 // If we couldn't retrieve process uid, return null. 212 return null; 213 } 214 215 // We're trying to get the ID of the Android user that's running the process. It can be 216 // inferred from the UID of the current process. 217 int userId = uid / PER_USER_RANGE; 218 return getWriteableDirectory(String.format("/data/user/%d/%s", userId, packageName)); 219 } 220 getWriteableDirectory(String pathName)221 private File getWriteableDirectory(String pathName) { 222 File dir = new File(pathName); 223 return isWriteableDirectory(dir) ? dir : null; 224 } 225 } 226