1 /** 2 * Copyright 2018 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 android.content.pm.dex; 18 19 import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_DEX_METADATA; 20 import static android.content.pm.PackageParser.APK_FILE_EXTENSION; 21 22 import android.content.pm.PackageParser; 23 import android.content.pm.PackageParser.PackageLite; 24 import android.content.pm.PackageParser.PackageParserException; 25 import android.util.ArrayMap; 26 import android.util.jar.StrictJarFile; 27 28 import java.io.File; 29 import java.io.IOException; 30 import java.nio.file.Files; 31 import java.nio.file.Paths; 32 import java.util.ArrayList; 33 import java.util.Collection; 34 import java.util.List; 35 import java.util.Map; 36 37 /** 38 * Helper class used to compute and validate the location of dex metadata files. 39 * 40 * @hide 41 */ 42 public class DexMetadataHelper { 43 private static final String DEX_METADATA_FILE_EXTENSION = ".dm"; 44 DexMetadataHelper()45 private DexMetadataHelper() {} 46 47 /** Return true if the given file is a dex metadata file. */ isDexMetadataFile(File file)48 public static boolean isDexMetadataFile(File file) { 49 return isDexMetadataPath(file.getName()); 50 } 51 52 /** Return true if the given path is a dex metadata path. */ isDexMetadataPath(String path)53 private static boolean isDexMetadataPath(String path) { 54 return path.endsWith(DEX_METADATA_FILE_EXTENSION); 55 } 56 57 /** 58 * Return the size (in bytes) of all dex metadata files associated with the given package. 59 */ getPackageDexMetadataSize(PackageLite pkg)60 public static long getPackageDexMetadataSize(PackageLite pkg) { 61 long sizeBytes = 0; 62 Collection<String> dexMetadataList = DexMetadataHelper.getPackageDexMetadata(pkg).values(); 63 for (String dexMetadata : dexMetadataList) { 64 sizeBytes += new File(dexMetadata).length(); 65 } 66 return sizeBytes; 67 } 68 69 /** 70 * Search for the dex metadata file associated with the given target file. 71 * If it exists, the method returns the dex metadata file; otherwise it returns null. 72 * 73 * Note that this performs a loose matching suitable to be used in the InstallerSession logic. 74 * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile} 75 * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk'). 76 */ findDexMetadataForFile(File targetFile)77 public static File findDexMetadataForFile(File targetFile) { 78 String dexMetadataPath = buildDexMetadataPathForFile(targetFile); 79 File dexMetadataFile = new File(dexMetadataPath); 80 return dexMetadataFile.exists() ? dexMetadataFile : null; 81 } 82 83 /** 84 * Return the dex metadata files for the given package as a map 85 * [code path -> dex metadata path]. 86 * 87 * NOTE: involves I/O checks. 88 */ getPackageDexMetadata(PackageParser.Package pkg)89 public static Map<String, String> getPackageDexMetadata(PackageParser.Package pkg) { 90 return buildPackageApkToDexMetadataMap(pkg.getAllCodePaths()); 91 } 92 93 /** 94 * Return the dex metadata files for the given package as a map 95 * [code path -> dex metadata path]. 96 * 97 * NOTE: involves I/O checks. 98 */ getPackageDexMetadata(PackageLite pkg)99 private static Map<String, String> getPackageDexMetadata(PackageLite pkg) { 100 return buildPackageApkToDexMetadataMap(pkg.getAllCodePaths()); 101 } 102 103 /** 104 * Look up the dex metadata files for the given code paths building the map 105 * [code path -> dex metadata]. 106 * 107 * For each code path (.apk) the method checks if a matching dex metadata file (.dm) exists. 108 * If it does it adds the pair to the returned map. 109 * 110 * Note that this method will do a loose 111 * matching based on the extension ('foo.dm' will match 'foo.apk' or 'foo'). 112 * 113 * This should only be used for code paths extracted from a package structure after the naming 114 * was enforced in the installer. 115 */ buildPackageApkToDexMetadataMap( List<String> codePaths)116 private static Map<String, String> buildPackageApkToDexMetadataMap( 117 List<String> codePaths) { 118 ArrayMap<String, String> result = new ArrayMap<>(); 119 for (int i = codePaths.size() - 1; i >= 0; i--) { 120 String codePath = codePaths.get(i); 121 String dexMetadataPath = buildDexMetadataPathForFile(new File(codePath)); 122 123 if (Files.exists(Paths.get(dexMetadataPath))) { 124 result.put(codePath, dexMetadataPath); 125 } 126 } 127 128 return result; 129 } 130 131 /** 132 * Return the dex metadata path associated with the given code path. 133 * (replaces '.apk' extension with '.dm') 134 * 135 * @throws IllegalArgumentException if the code path is not an .apk. 136 */ buildDexMetadataPathForApk(String codePath)137 public static String buildDexMetadataPathForApk(String codePath) { 138 if (!PackageParser.isApkPath(codePath)) { 139 throw new IllegalStateException( 140 "Corrupted package. Code path is not an apk " + codePath); 141 } 142 return codePath.substring(0, codePath.length() - APK_FILE_EXTENSION.length()) 143 + DEX_METADATA_FILE_EXTENSION; 144 } 145 146 /** 147 * Return the dex metadata path corresponding to the given {@code targetFile} using a loose 148 * matching. 149 * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile} 150 * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk'). 151 */ buildDexMetadataPathForFile(File targetFile)152 private static String buildDexMetadataPathForFile(File targetFile) { 153 return PackageParser.isApkFile(targetFile) 154 ? buildDexMetadataPathForApk(targetFile.getPath()) 155 : targetFile.getPath() + DEX_METADATA_FILE_EXTENSION; 156 } 157 158 /** 159 * Validate the dex metadata files installed for the given package. 160 * 161 * @throws PackageParserException in case of errors. 162 */ validatePackageDexMetadata(PackageParser.Package pkg)163 public static void validatePackageDexMetadata(PackageParser.Package pkg) 164 throws PackageParserException { 165 Collection<String> apkToDexMetadataList = getPackageDexMetadata(pkg).values(); 166 for (String dexMetadata : apkToDexMetadataList) { 167 validateDexMetadataFile(dexMetadata); 168 } 169 } 170 171 /** 172 * Validate that the given file is a dex metadata archive. 173 * This is just a sanity validation that the file is a zip archive. 174 * 175 * @throws PackageParserException if the file is not a .dm file. 176 */ validateDexMetadataFile(String dmaPath)177 private static void validateDexMetadataFile(String dmaPath) throws PackageParserException { 178 StrictJarFile jarFile = null; 179 try { 180 jarFile = new StrictJarFile(dmaPath, false, false); 181 } catch (IOException e) { 182 throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, 183 "Error opening " + dmaPath, e); 184 } finally { 185 if (jarFile != null) { 186 try { 187 jarFile.close(); 188 } catch (IOException ignored) { 189 } 190 } 191 } 192 } 193 194 /** 195 * Validates that all dex metadata paths in the given list have a matching apk. 196 * (for any foo.dm there should be either a 'foo' of a 'foo.apk' file). 197 * If that's not the case it throws {@code IllegalStateException}. 198 * 199 * This is used to perform a basic sanity check during adb install commands. 200 * (The installer does not support stand alone .dm files) 201 */ validateDexPaths(String[] paths)202 public static void validateDexPaths(String[] paths) { 203 ArrayList<String> apks = new ArrayList<>(); 204 for (int i = 0; i < paths.length; i++) { 205 if (PackageParser.isApkPath(paths[i])) { 206 apks.add(paths[i]); 207 } 208 } 209 ArrayList<String> unmatchedDmFiles = new ArrayList<>(); 210 for (int i = 0; i < paths.length; i++) { 211 String dmPath = paths[i]; 212 if (isDexMetadataPath(dmPath)) { 213 boolean valid = false; 214 for (int j = apks.size() - 1; j >= 0; j--) { 215 if (dmPath.equals(buildDexMetadataPathForFile(new File(apks.get(j))))) { 216 valid = true; 217 break; 218 } 219 } 220 if (!valid) { 221 unmatchedDmFiles.add(dmPath); 222 } 223 } 224 } 225 if (!unmatchedDmFiles.isEmpty()) { 226 throw new IllegalStateException("Unmatched .dm files: " + unmatchedDmFiles); 227 } 228 } 229 230 } 231