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 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 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