• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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