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