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 String apkPathRoot = "/data/app/"; 140 for (String potential : splitPathList(input)) { 141 if (!potential.startsWith(apkPathRoot)) { 142 continue; 143 } 144 int end = potential.lastIndexOf(".apk"); 145 if (end != potential.length() - 4) { 146 continue; 147 } 148 int endSlash = potential.lastIndexOf("/", end); 149 if (endSlash == apkPathRoot.length() - 1) { 150 // Apks cannot be directly under /data/app 151 continue; 152 } 153 int startSlash = potential.lastIndexOf("/", endSlash - 1); 154 if (startSlash == -1) { 155 continue; 156 } 157 // Look for the first dash after the package name 158 int dash = potential.indexOf("-", startSlash); 159 if (dash == -1) { 160 continue; 161 } 162 end = dash; 163 String packageName = potential.substring(startSlash + 1, end); 164 File dataDir = getWriteableDirectory("/data/data/" + packageName); 165 166 if (dataDir == null) { 167 // If we can't access "/data/data", try to guess user specific data directory. 168 dataDir = guessUserDataDirectory(packageName); 169 } 170 171 if (dataDir != null) { 172 File cacheDir = new File(dataDir, "cache"); 173 // The cache directory might not exist -- create if necessary 174 if (fileOrDirExists(cacheDir) || cacheDir.mkdir()) { 175 if (isWriteableDirectory(cacheDir)) { 176 results.add(cacheDir); 177 } 178 } 179 } 180 } 181 return results.toArray(new File[results.size()]); 182 } 183 splitPathList(String input)184 static String[] splitPathList(String input) { 185 String trimmed = input; 186 if (input.startsWith("dexPath=")) { 187 int start = "dexPath=".length(); 188 int end = input.indexOf(','); 189 190 trimmed = (end == -1) ? input.substring(start) : input.substring(start, end); 191 } 192 193 return trimmed.split(":"); 194 } 195 fileOrDirExists(File file)196 boolean fileOrDirExists(File file) { 197 return file.exists(); 198 } 199 isWriteableDirectory(File file)200 boolean isWriteableDirectory(File file) { 201 return file.isDirectory() && file.canWrite(); 202 } 203 getProcessUid()204 Integer getProcessUid() { 205 /* Uses reflection to try to fetch process UID. It will only work when executing on 206 * Android device. Otherwise, returns null. 207 */ 208 try { 209 Method myUid = Class.forName("android.os.Process").getMethod("myUid"); 210 211 // Invoke the method on a null instance, since it's a static method. 212 return (Integer) myUid.invoke(/* instance= */ null); 213 } catch (Exception e) { 214 // Catch any exceptions thrown and default to returning a null. 215 return null; 216 } 217 } 218 guessUserDataDirectory(String packageName)219 File guessUserDataDirectory(String packageName) { 220 Integer uid = getProcessUid(); 221 if (uid == null) { 222 // If we couldn't retrieve process uid, return null. 223 return null; 224 } 225 226 // We're trying to get the ID of the Android user that's running the process. It can be 227 // inferred from the UID of the current process. 228 int userId = uid / PER_USER_RANGE; 229 return getWriteableDirectory(String.format("/data/user/%d/%s", userId, packageName)); 230 } 231 getWriteableDirectory(String pathName)232 private File getWriteableDirectory(String pathName) { 233 File dir = new File(pathName); 234 return isWriteableDirectory(dir) ? dir : null; 235 } 236 } 237