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.parsing.ApkLiteParseUtils.APK_FILE_EXTENSION; 21 22 import android.content.pm.PackageParser.PackageParserException; 23 import android.content.pm.parsing.ApkLiteParseUtils; 24 import android.content.pm.parsing.PackageLite; 25 import android.os.SystemProperties; 26 import android.util.ArrayMap; 27 import android.util.JsonReader; 28 import android.util.Log; 29 import android.util.jar.StrictJarFile; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.internal.security.VerityUtils; 33 34 import java.io.File; 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.io.InputStreamReader; 38 import java.io.UnsupportedEncodingException; 39 import java.nio.file.Files; 40 import java.nio.file.Paths; 41 import java.util.ArrayList; 42 import java.util.Collection; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.zip.ZipEntry; 46 47 /** 48 * Helper class used to compute and validate the location of dex metadata files. 49 * 50 * @hide 51 */ 52 public class DexMetadataHelper { 53 public static final String TAG = "DexMetadataHelper"; 54 /** $> adb shell 'setprop log.tag.DexMetadataHelper VERBOSE' */ 55 public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 56 /** $> adb shell 'setprop pm.dexopt.dm.require_manifest true' */ 57 private static final String PROPERTY_DM_JSON_MANIFEST_REQUIRED = 58 "pm.dexopt.dm.require_manifest"; 59 /** $> adb shell 'setprop pm.dexopt.dm.require_fsverity true' */ 60 private static final String PROPERTY_DM_FSVERITY_REQUIRED = "pm.dexopt.dm.require_fsverity"; 61 62 private static final String DEX_METADATA_FILE_EXTENSION = ".dm"; 63 DexMetadataHelper()64 private DexMetadataHelper() {} 65 66 /** Return true if the given file is a dex metadata file. */ isDexMetadataFile(File file)67 public static boolean isDexMetadataFile(File file) { 68 return isDexMetadataPath(file.getName()); 69 } 70 71 /** Return true if the given path is a dex metadata path. */ isDexMetadataPath(String path)72 private static boolean isDexMetadataPath(String path) { 73 return path.endsWith(DEX_METADATA_FILE_EXTENSION); 74 } 75 76 /** 77 * Returns whether fs-verity is required to install a dex metadata 78 */ isFsVerityRequired()79 public static boolean isFsVerityRequired() { 80 return VerityUtils.isFsVeritySupported() 81 && SystemProperties.getBoolean(PROPERTY_DM_FSVERITY_REQUIRED, false); 82 } 83 84 /** 85 * Return the size (in bytes) of all dex metadata files associated with the given package. 86 */ getPackageDexMetadataSize(PackageLite pkg)87 public static long getPackageDexMetadataSize(PackageLite pkg) { 88 long sizeBytes = 0; 89 Collection<String> dexMetadataList = DexMetadataHelper.getPackageDexMetadata(pkg).values(); 90 for (String dexMetadata : dexMetadataList) { 91 sizeBytes += new File(dexMetadata).length(); 92 } 93 return sizeBytes; 94 } 95 96 /** 97 * Search for the dex metadata file associated with the given target file. 98 * If it exists, the method returns the dex metadata file; otherwise it returns null. 99 * 100 * Note that this performs a loose matching suitable to be used in the InstallerSession logic. 101 * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile} 102 * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk'). 103 */ findDexMetadataForFile(File targetFile)104 public static File findDexMetadataForFile(File targetFile) { 105 String dexMetadataPath = buildDexMetadataPathForFile(targetFile); 106 File dexMetadataFile = new File(dexMetadataPath); 107 return dexMetadataFile.exists() ? dexMetadataFile : null; 108 } 109 110 /** 111 * Return the dex metadata files for the given package as a map 112 * [code path -> dex metadata path]. 113 * 114 * NOTE: involves I/O checks. 115 */ getPackageDexMetadata(PackageLite pkg)116 private static Map<String, String> getPackageDexMetadata(PackageLite pkg) { 117 return buildPackageApkToDexMetadataMap(pkg.getAllApkPaths()); 118 } 119 120 /** 121 * Look up the dex metadata files for the given code paths building the map 122 * [code path -> dex metadata]. 123 * 124 * For each code path (.apk) the method checks if a matching dex metadata file (.dm) exists. 125 * If it does it adds the pair to the returned map. 126 * 127 * Note that this method will do a loose 128 * matching based on the extension ('foo.dm' will match 'foo.apk' or 'foo'). 129 * 130 * This should only be used for code paths extracted from a package structure after the naming 131 * was enforced in the installer. 132 */ buildPackageApkToDexMetadataMap( List<String> codePaths)133 public static Map<String, String> buildPackageApkToDexMetadataMap( 134 List<String> codePaths) { 135 ArrayMap<String, String> result = new ArrayMap<>(); 136 for (int i = codePaths.size() - 1; i >= 0; i--) { 137 String codePath = codePaths.get(i); 138 String dexMetadataPath = buildDexMetadataPathForFile(new File(codePath)); 139 140 if (Files.exists(Paths.get(dexMetadataPath))) { 141 result.put(codePath, dexMetadataPath); 142 } 143 } 144 145 return result; 146 } 147 148 /** 149 * Return the dex metadata path associated with the given code path. 150 * (replaces '.apk' extension with '.dm') 151 * 152 * @throws IllegalArgumentException if the code path is not an .apk. 153 */ buildDexMetadataPathForApk(String codePath)154 public static String buildDexMetadataPathForApk(String codePath) { 155 if (!ApkLiteParseUtils.isApkPath(codePath)) { 156 throw new IllegalStateException( 157 "Corrupted package. Code path is not an apk " + codePath); 158 } 159 return codePath.substring(0, codePath.length() - APK_FILE_EXTENSION.length()) 160 + DEX_METADATA_FILE_EXTENSION; 161 } 162 163 /** 164 * Return the dex metadata path corresponding to the given {@code targetFile} using a loose 165 * matching. 166 * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile} 167 * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk'). 168 */ buildDexMetadataPathForFile(File targetFile)169 private static String buildDexMetadataPathForFile(File targetFile) { 170 return ApkLiteParseUtils.isApkFile(targetFile) 171 ? buildDexMetadataPathForApk(targetFile.getPath()) 172 : targetFile.getPath() + DEX_METADATA_FILE_EXTENSION; 173 } 174 175 /** 176 * Validate that the given file is a dex metadata archive. 177 * This is just a validation that the file is a zip archive that contains a manifest.json 178 * with the package name and version code. 179 * 180 * @throws PackageParserException if the file is not a .dm file. 181 */ validateDexMetadataFile(String dmaPath, String packageName, long versionCode)182 public static void validateDexMetadataFile(String dmaPath, String packageName, long versionCode) 183 throws PackageParserException { 184 validateDexMetadataFile(dmaPath, packageName, versionCode, 185 SystemProperties.getBoolean(PROPERTY_DM_JSON_MANIFEST_REQUIRED, false)); 186 } 187 188 @VisibleForTesting validateDexMetadataFile(String dmaPath, String packageName, long versionCode, boolean requireManifest)189 public static void validateDexMetadataFile(String dmaPath, String packageName, long versionCode, 190 boolean requireManifest) throws PackageParserException { 191 StrictJarFile jarFile = null; 192 193 if (DEBUG) { 194 Log.v(TAG, "validateDexMetadataFile: " + dmaPath + ", " + packageName + 195 ", " + versionCode); 196 } 197 198 try { 199 jarFile = new StrictJarFile(dmaPath, false, false); 200 validateDexMetadataManifest(dmaPath, jarFile, packageName, versionCode, 201 requireManifest); 202 } catch (IOException e) { 203 throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, 204 "Error opening " + dmaPath, e); 205 } finally { 206 if (jarFile != null) { 207 try { 208 jarFile.close(); 209 } catch (IOException ignored) { 210 } 211 } 212 } 213 } 214 215 /** Ensure that packageName and versionCode match the manifest.json in the .dm file */ validateDexMetadataManifest(String dmaPath, StrictJarFile jarFile, String packageName, long versionCode, boolean requireManifest)216 private static void validateDexMetadataManifest(String dmaPath, StrictJarFile jarFile, 217 String packageName, long versionCode, boolean requireManifest) 218 throws IOException, PackageParserException { 219 if (!requireManifest) { 220 if (DEBUG) { 221 Log.v(TAG, "validateDexMetadataManifest: " + dmaPath 222 + " manifest.json check skipped"); 223 } 224 return; 225 } 226 227 ZipEntry zipEntry = jarFile.findEntry("manifest.json"); 228 if (zipEntry == null) { 229 throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, 230 "Missing manifest.json in " + dmaPath); 231 } 232 InputStream inputStream = jarFile.getInputStream(zipEntry); 233 234 JsonReader reader; 235 try { 236 reader = new JsonReader(new InputStreamReader(inputStream, "UTF-8")); 237 } catch (UnsupportedEncodingException e) { 238 throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, 239 "Error opening manifest.json in " + dmaPath, e); 240 } 241 String jsonPackageName = null; 242 long jsonVersionCode = -1; 243 244 reader.beginObject(); 245 while (reader.hasNext()) { 246 String name = reader.nextName(); 247 if (name.equals("packageName")) { 248 jsonPackageName = reader.nextString(); 249 } else if (name.equals("versionCode")) { 250 jsonVersionCode = reader.nextLong(); 251 } else { 252 reader.skipValue(); 253 } 254 } 255 reader.endObject(); 256 257 if (jsonPackageName == null || jsonVersionCode == -1) { 258 throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, 259 "manifest.json in " + dmaPath 260 + " is missing 'packageName' and/or 'versionCode'"); 261 } 262 263 if (!jsonPackageName.equals(packageName)) { 264 throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, 265 "manifest.json in " + dmaPath + " has invalid packageName: " + jsonPackageName 266 + ", expected: " + packageName); 267 } 268 269 if (versionCode != jsonVersionCode) { 270 throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, 271 "manifest.json in " + dmaPath + " has invalid versionCode: " + jsonVersionCode 272 + ", expected: " + versionCode); 273 } 274 275 if (DEBUG) { 276 Log.v(TAG, "validateDexMetadataManifest: " + dmaPath + ", " + packageName + 277 ", " + versionCode + ": successful"); 278 } 279 } 280 281 /** 282 * Validates that all dex metadata paths in the given list have a matching apk. 283 * (for any foo.dm there should be either a 'foo' of a 'foo.apk' file). 284 * If that's not the case it throws {@code IllegalStateException}. 285 * 286 * This is used to perform a basic check during adb install commands. 287 * (The installer does not support stand alone .dm files) 288 */ validateDexPaths(String[] paths)289 public static void validateDexPaths(String[] paths) { 290 ArrayList<String> apks = new ArrayList<>(); 291 for (int i = 0; i < paths.length; i++) { 292 if (ApkLiteParseUtils.isApkPath(paths[i])) { 293 apks.add(paths[i]); 294 } 295 } 296 ArrayList<String> unmatchedDmFiles = new ArrayList<>(); 297 for (int i = 0; i < paths.length; i++) { 298 String dmPath = paths[i]; 299 if (isDexMetadataPath(dmPath)) { 300 boolean valid = false; 301 for (int j = apks.size() - 1; j >= 0; j--) { 302 if (dmPath.equals(buildDexMetadataPathForFile(new File(apks.get(j))))) { 303 valid = true; 304 break; 305 } 306 } 307 if (!valid) { 308 unmatchedDmFiles.add(dmPath); 309 } 310 } 311 } 312 if (!unmatchedDmFiles.isEmpty()) { 313 throw new IllegalStateException("Unmatched .dm files: " + unmatchedDmFiles); 314 } 315 } 316 317 } 318