Home | History | Annotate | Download | only in dex
      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 
     45     private DexMetadataHelper() {}
     46 
     47     /** Return true if the given file is a dex metadata 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. */
     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      */
     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      */
     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      */
     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      */
     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      */
    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      */
    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      */
    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      */
    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 sanity validation that the file is a zip archive.
    174      *
    175      * @throws PackageParserException if the file is not a .dm file.
    176      */
    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 sanity check during adb install commands.
    200      * (The installer does not support stand alone .dm files)
    201      */
    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