1 /* 2 * Copyright (C) 2020 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.server.pm.dex; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertNotNull; 21 import static org.junit.Assert.assertTrue; 22 import static org.junit.Assert.fail; 23 24 import android.content.Context; 25 import android.content.pm.PackageManager; 26 import android.content.pm.PackageParser.PackageParserException; 27 import android.content.pm.dex.DexMetadataHelper; 28 import android.content.pm.parsing.ApkLite; 29 import android.content.pm.parsing.ApkLiteParseUtils; 30 import android.content.pm.parsing.PackageLite; 31 import android.content.pm.parsing.result.ParseResult; 32 import android.content.pm.parsing.result.ParseTypeImpl; 33 import android.os.FileUtils; 34 35 import androidx.test.InstrumentationRegistry; 36 import androidx.test.filters.SmallTest; 37 import androidx.test.runner.AndroidJUnit4; 38 39 import com.android.frameworks.servicestests.R; 40 import com.android.server.pm.PackageManagerException; 41 import com.android.server.pm.parsing.TestPackageParser2; 42 import com.android.server.pm.parsing.pkg.AndroidPackage; 43 import com.android.server.pm.parsing.pkg.AndroidPackageUtils; 44 import com.android.server.pm.parsing.pkg.ParsedPackage; 45 46 import org.junit.Assert; 47 import org.junit.Before; 48 import org.junit.Rule; 49 import org.junit.Test; 50 import org.junit.rules.TemporaryFolder; 51 import org.junit.runner.RunWith; 52 53 import java.io.File; 54 import java.io.FileInputStream; 55 import java.io.FileOutputStream; 56 import java.io.IOException; 57 import java.io.InputStream; 58 import java.nio.charset.StandardCharsets; 59 import java.nio.file.Files; 60 import java.util.Collection; 61 import java.util.Map; 62 import java.util.zip.ZipEntry; 63 import java.util.zip.ZipOutputStream; 64 65 @SmallTest 66 @RunWith(AndroidJUnit4.class) 67 public class DexMetadataHelperTest { 68 private static final String APK_FILE_EXTENSION = ".apk"; 69 private static final String DEX_METADATA_FILE_EXTENSION = ".dm"; 70 private static final String DEX_METADATA_PACKAGE_NAME = 71 "com.android.frameworks.servicestests.install_split"; 72 private static final long DEX_METADATA_VERSION_CODE = 9001; 73 74 @Rule 75 public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); 76 77 private File mTmpDir = null; 78 79 @Before setUp()80 public void setUp() throws IOException { 81 mTmpDir = mTemporaryFolder.newFolder("DexMetadataHelperTest"); 82 } 83 createDexMetadataFile(String apkFileName)84 private File createDexMetadataFile(String apkFileName) throws IOException { 85 return createDexMetadataFile(apkFileName, /*validManifest=*/true); 86 } 87 createDexMetadataFile(String apkFileName, boolean validManifest)88 private File createDexMetadataFile(String apkFileName, boolean validManifest) throws IOException 89 { 90 return createDexMetadataFile(apkFileName,DEX_METADATA_PACKAGE_NAME, 91 DEX_METADATA_VERSION_CODE, /*emptyManifest=*/false, validManifest); 92 } 93 createDexMetadataFile(String apkFileName, String packageName, Long versionCode, boolean emptyManifest, boolean validManifest)94 private File createDexMetadataFile(String apkFileName, String packageName, Long versionCode, 95 boolean emptyManifest, boolean validManifest) throws IOException { 96 File dmFile = new File(mTmpDir, apkFileName.replace(APK_FILE_EXTENSION, 97 DEX_METADATA_FILE_EXTENSION)); 98 try (FileOutputStream fos = new FileOutputStream(dmFile)) { 99 try (ZipOutputStream zipOs = new ZipOutputStream(fos)) { 100 zipOs.putNextEntry(new ZipEntry("primary.prof")); 101 zipOs.closeEntry(); 102 103 if (validManifest) { 104 zipOs.putNextEntry(new ZipEntry("manifest.json")); 105 if (!emptyManifest) { 106 String manifestStr = "{"; 107 108 if (packageName != null) { 109 manifestStr += "\"packageName\": " + "\"" + packageName + "\""; 110 111 if (versionCode != null) { 112 manifestStr += ", "; 113 } 114 } 115 if (versionCode != null) { 116 manifestStr += " \"versionCode\": " + versionCode; 117 } 118 119 manifestStr += "}"; 120 byte[] bytes = manifestStr.getBytes(StandardCharsets.UTF_8); 121 zipOs.write(bytes, /*off=*/0, /*len=*/bytes.length); 122 } 123 zipOs.closeEntry(); 124 } 125 } 126 } 127 return dmFile; 128 } 129 copyApkToToTmpDir(String apkFileName, int apkResourceId)130 private File copyApkToToTmpDir(String apkFileName, int apkResourceId) throws IOException { 131 Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 132 File outFile = new File(mTmpDir, apkFileName); 133 try (InputStream is = context.getResources().openRawResource(apkResourceId)) { 134 FileUtils.copyToFileOrThrow(is, outFile); 135 } 136 return outFile; 137 } 138 validatePackageDexMetadata(AndroidPackage pkg, boolean requireManifest)139 private static void validatePackageDexMetadata(AndroidPackage pkg, boolean requireManifest) 140 throws PackageParserException { 141 Collection<String> apkToDexMetadataList = 142 AndroidPackageUtils.getPackageDexMetadata(pkg).values(); 143 String packageName = pkg.getPackageName(); 144 long versionCode = pkg.toAppInfoWithoutState().longVersionCode; 145 for (String dexMetadata : apkToDexMetadataList) { 146 DexMetadataHelper.validateDexMetadataFile( 147 dexMetadata, packageName, versionCode, requireManifest); 148 } 149 } 150 validatePackageDexMetatadataVaryingRequireManifest(ParsedPackage pkg)151 private static void validatePackageDexMetatadataVaryingRequireManifest(ParsedPackage pkg) 152 throws PackageParserException { 153 validatePackageDexMetadata(pkg, /*requireManifest=*/true); 154 validatePackageDexMetadata(pkg, /*requireManifest=*/false); 155 } 156 157 @Test testParsePackageWithDmFileValid()158 public void testParsePackageWithDmFileValid() throws IOException, PackageParserException { 159 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 160 createDexMetadataFile("install_split_base.apk"); 161 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 162 163 Map<String, String> packageDexMetadata = AndroidPackageUtils.getPackageDexMetadata(pkg); 164 assertEquals(1, packageDexMetadata.size()); 165 String baseDexMetadata = packageDexMetadata.get(pkg.getBaseApkPath()); 166 assertNotNull(baseDexMetadata); 167 assertTrue(isDexMetadataForApk(baseDexMetadata, pkg.getBaseApkPath())); 168 169 // Should throw no exceptions. 170 validatePackageDexMetatadataVaryingRequireManifest(pkg); 171 } 172 173 @Test testParsePackageSplitsWithDmFileValid()174 public void testParsePackageSplitsWithDmFileValid() 175 throws IOException, PackageParserException { 176 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 177 copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a); 178 createDexMetadataFile("install_split_base.apk"); 179 createDexMetadataFile("install_split_feature_a.apk"); 180 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 181 182 Map<String, String> packageDexMetadata = AndroidPackageUtils.getPackageDexMetadata(pkg); 183 assertEquals(2, packageDexMetadata.size()); 184 String baseDexMetadata = packageDexMetadata.get(pkg.getBaseApkPath()); 185 assertNotNull(baseDexMetadata); 186 assertTrue(isDexMetadataForApk(baseDexMetadata, pkg.getBaseApkPath())); 187 188 String splitDexMetadata = packageDexMetadata.get(pkg.getSplitCodePaths()[0]); 189 assertNotNull(splitDexMetadata); 190 assertTrue(isDexMetadataForApk(splitDexMetadata, pkg.getSplitCodePaths()[0])); 191 192 // Should throw no exceptions. 193 validatePackageDexMetatadataVaryingRequireManifest(pkg); 194 } 195 196 @Test testParsePackageSplitsNoBaseWithDmFileValid()197 public void testParsePackageSplitsNoBaseWithDmFileValid() 198 throws IOException, PackageParserException { 199 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 200 copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a); 201 createDexMetadataFile("install_split_feature_a.apk"); 202 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 203 204 Map<String, String> packageDexMetadata = AndroidPackageUtils.getPackageDexMetadata(pkg); 205 assertEquals(1, packageDexMetadata.size()); 206 207 String splitDexMetadata = packageDexMetadata.get(pkg.getSplitCodePaths()[0]); 208 assertNotNull(splitDexMetadata); 209 assertTrue(isDexMetadataForApk(splitDexMetadata, pkg.getSplitCodePaths()[0])); 210 211 // Should throw no exceptions. 212 validatePackageDexMetatadataVaryingRequireManifest(pkg); 213 } 214 215 @Test testParsePackageWithDmFileInvalid()216 public void testParsePackageWithDmFileInvalid() throws IOException { 217 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 218 File invalidDmFile = new File(mTmpDir, "install_split_base.dm"); 219 Files.createFile(invalidDmFile.toPath()); 220 try { 221 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 222 validatePackageDexMetadata(pkg, /*requireManifest=*/true); 223 fail("Should fail validation: empty .dm file"); 224 } catch (PackageParserException e) { 225 assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); 226 } 227 228 try { 229 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 230 validatePackageDexMetadata(pkg, /*requireManifest=*/false); 231 fail("Should fail validation: empty .dm file"); 232 } catch (PackageParserException e) { 233 assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); 234 } 235 } 236 237 @Test testParsePackageSplitsWithDmFileInvalid()238 public void testParsePackageSplitsWithDmFileInvalid() 239 throws IOException, PackageParserException { 240 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 241 copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a); 242 createDexMetadataFile("install_split_base.apk"); 243 File invalidDmFile = new File(mTmpDir, "install_split_feature_a.dm"); 244 Files.createFile(invalidDmFile.toPath()); 245 246 try { 247 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 248 validatePackageDexMetadata(pkg, /*requireManifest=*/true); 249 fail("Should fail validation: empty .dm file"); 250 } catch (PackageParserException e) { 251 assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); 252 } 253 254 try { 255 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 256 validatePackageDexMetadata(pkg, /*requireManifest=*/false); 257 fail("Should fail validation: empty .dm file"); 258 } catch (PackageParserException e) { 259 assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); 260 } 261 } 262 263 @Test testParsePackageWithDmFileInvalidManifest()264 public void testParsePackageWithDmFileInvalidManifest() 265 throws IOException, PackageParserException { 266 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 267 createDexMetadataFile("install_split_base.apk", /*validManifest=*/false); 268 269 try { 270 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 271 validatePackageDexMetadata(pkg, /*requireManifest=*/true); 272 fail("Should fail validation: missing manifest.json in the .dm archive"); 273 } catch (PackageParserException e) { 274 assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); 275 } 276 } 277 278 @Test testParsePackageWithDmFileEmptyManifest()279 public void testParsePackageWithDmFileEmptyManifest() 280 throws IOException, PackageParserException { 281 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 282 createDexMetadataFile("install_split_base.apk", /*packageName=*/"doesn't matter", 283 /*versionCode=*/-12345L, /*emptyManifest=*/true, /*validManifest=*/true); 284 285 try { 286 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 287 validatePackageDexMetadata(pkg, /*requireManifest=*/true); 288 fail("Should fail validation: empty manifest.json in the .dm archive"); 289 } catch (PackageParserException e) { 290 assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); 291 } 292 } 293 294 @Test testParsePackageWithDmFileBadPackageName()295 public void testParsePackageWithDmFileBadPackageName() 296 throws IOException, PackageParserException { 297 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 298 createDexMetadataFile("install_split_base.apk", /*packageName=*/"bad package name", 299 DEX_METADATA_VERSION_CODE, /*emptyManifest=*/false, /*validManifest=*/true); 300 301 try { 302 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 303 validatePackageDexMetadata(pkg, /*requireManifest=*/true); 304 fail("Should fail validation: bad package name in the .dm archive"); 305 } catch (PackageParserException e) { 306 assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); 307 } 308 } 309 310 @Test testParsePackageWithDmFileBadVersionCode()311 public void testParsePackageWithDmFileBadVersionCode() 312 throws IOException, PackageParserException { 313 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 314 createDexMetadataFile("install_split_base.apk", DEX_METADATA_PACKAGE_NAME, 315 /*versionCode=*/12345L, /*emptyManifest=*/false, /*validManifest=*/true); 316 317 try { 318 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 319 validatePackageDexMetadata(pkg, /*requireManifest=*/true); 320 fail("Should fail validation: bad version code in the .dm archive"); 321 } catch (PackageParserException e) { 322 assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); 323 } 324 } 325 326 @Test testParsePackageWithDmFileMissingPackageName()327 public void testParsePackageWithDmFileMissingPackageName() 328 throws IOException, PackageParserException { 329 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 330 createDexMetadataFile("install_split_base.apk", /*packageName=*/null, 331 DEX_METADATA_VERSION_CODE, /*emptyManifest=*/false, /*validManifest=*/true); 332 333 try { 334 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 335 validatePackageDexMetadata(pkg, /*requireManifest=*/true); 336 fail("Should fail validation: missing package name in the .dm archive"); 337 } catch (PackageParserException e) { 338 assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); 339 } 340 } 341 342 @Test testParsePackageWithDmFileMissingVersionCode()343 public void testParsePackageWithDmFileMissingVersionCode() 344 throws IOException, PackageParserException { 345 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 346 createDexMetadataFile("install_split_base.apk", DEX_METADATA_PACKAGE_NAME, 347 /*versionCode=*/null, /*emptyManifest=*/false, /*validManifest=*/true); 348 349 try { 350 ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); 351 validatePackageDexMetadata(pkg, /*requireManifest=*/true); 352 fail("Should fail validation: missing version code in the .dm archive"); 353 } catch (PackageParserException e) { 354 assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); 355 } 356 } 357 358 @Test testPackageWithDmFileNoMatch()359 public void testPackageWithDmFileNoMatch() throws IOException { 360 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 361 createDexMetadataFile("non_existent.apk"); 362 363 try { 364 DexMetadataHelper.validateDexPaths(mTmpDir.list()); 365 fail("Should fail validation: .dm filename has no match against .apk"); 366 } catch (IllegalStateException e) { 367 // expected. 368 } 369 } 370 371 @Test testPackageSplitsWithDmFileNoMatch()372 public void testPackageSplitsWithDmFileNoMatch() 373 throws IOException, PackageParserException { 374 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 375 copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a); 376 createDexMetadataFile("install_split_base.apk"); 377 createDexMetadataFile("install_split_feature_a.mistake.apk"); 378 379 try { 380 DexMetadataHelper.validateDexPaths(mTmpDir.list()); 381 fail("Should fail validation: split .dm filename unmatched against .apk"); 382 } catch (IllegalStateException e) { 383 // expected. 384 } 385 } 386 387 @Test testPackageSizeWithDmFile()388 public void testPackageSizeWithDmFile() throws IOException { 389 copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); 390 final File dm = createDexMetadataFile("install_split_base.apk"); 391 final ParseResult<PackageLite> result = ApkLiteParseUtils.parsePackageLite( 392 ParseTypeImpl.forDefaultParsing().reset(), mTmpDir, /*flags=*/0); 393 if (result.isError()) { 394 throw new IllegalStateException(result.getErrorMessage(), result.getException()); 395 } 396 final PackageLite pkg = result.getResult(); 397 Assert.assertEquals(dm.length(), DexMetadataHelper.getPackageDexMetadataSize(pkg)); 398 } 399 400 // This simulates the 'adb shell pm install' flow. 401 @Test testPackageSizeWithPartialPackageLite()402 public void testPackageSizeWithPartialPackageLite() throws IOException, 403 PackageManagerException { 404 final File base = copyApkToToTmpDir("install_split_base", R.raw.install_split_base); 405 final File dm = createDexMetadataFile("install_split_base.apk"); 406 try (FileInputStream is = new FileInputStream(base)) { 407 final ParseResult<ApkLite> result = ApkLiteParseUtils.parseApkLite( 408 ParseTypeImpl.forDefaultParsing().reset(), is.getFD(), 409 base.getAbsolutePath(), /*flags=*/0); 410 if (result.isError()) { 411 throw new PackageManagerException(result.getErrorCode(), 412 result.getErrorMessage(), result.getException()); 413 } 414 final ApkLite baseApk = result.getResult(); 415 final PackageLite pkgLite = new PackageLite(null, baseApk.getPath(), baseApk, null, 416 null, null, null, null, null, baseApk.getTargetSdkVersion()); 417 Assert.assertEquals(dm.length(), DexMetadataHelper.getPackageDexMetadataSize(pkgLite)); 418 } 419 420 } 421 isDexMetadataForApk(String dmaPath, String apkPath)422 private static boolean isDexMetadataForApk(String dmaPath, String apkPath) { 423 return apkPath.substring(0, apkPath.length() - APK_FILE_EXTENSION.length()).equals( 424 dmaPath.substring(0, dmaPath.length() - DEX_METADATA_FILE_EXTENSION.length())); 425 } 426 } 427