| /** |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.content.pm.dex; |
| |
| import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_DEX_METADATA; |
| import static android.content.pm.PackageParser.APK_FILE_EXTENSION; |
| |
| import android.content.pm.PackageParser; |
| import android.content.pm.PackageParser.PackageLite; |
| import android.content.pm.PackageParser.PackageParserException; |
| import android.util.ArrayMap; |
| import android.util.jar.StrictJarFile; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.Files; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Helper class used to compute and validate the location of dex metadata files. |
| * |
| * @hide |
| */ |
| public class DexMetadataHelper { |
| private static final String DEX_METADATA_FILE_EXTENSION = ".dm"; |
| |
| private DexMetadataHelper() {} |
| |
| /** Return true if the given file is a dex metadata file. */ |
| public static boolean isDexMetadataFile(File file) { |
| return isDexMetadataPath(file.getName()); |
| } |
| |
| /** Return true if the given path is a dex metadata path. */ |
| private static boolean isDexMetadataPath(String path) { |
| return path.endsWith(DEX_METADATA_FILE_EXTENSION); |
| } |
| |
| /** |
| * Return the size (in bytes) of all dex metadata files associated with the given package. |
| */ |
| public static long getPackageDexMetadataSize(PackageLite pkg) { |
| long sizeBytes = 0; |
| Collection<String> dexMetadataList = DexMetadataHelper.getPackageDexMetadata(pkg).values(); |
| for (String dexMetadata : dexMetadataList) { |
| sizeBytes += new File(dexMetadata).length(); |
| } |
| return sizeBytes; |
| } |
| |
| /** |
| * Search for the dex metadata file associated with the given target file. |
| * If it exists, the method returns the dex metadata file; otherwise it returns null. |
| * |
| * Note that this performs a loose matching suitable to be used in the InstallerSession logic. |
| * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile} |
| * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk'). |
| */ |
| public static File findDexMetadataForFile(File targetFile) { |
| String dexMetadataPath = buildDexMetadataPathForFile(targetFile); |
| File dexMetadataFile = new File(dexMetadataPath); |
| return dexMetadataFile.exists() ? dexMetadataFile : null; |
| } |
| |
| /** |
| * Return the dex metadata files for the given package as a map |
| * [code path -> dex metadata path]. |
| * |
| * NOTE: involves I/O checks. |
| */ |
| private static Map<String, String> getPackageDexMetadata(PackageLite pkg) { |
| return buildPackageApkToDexMetadataMap(pkg.getAllCodePaths()); |
| } |
| |
| /** |
| * Look up the dex metadata files for the given code paths building the map |
| * [code path -> dex metadata]. |
| * |
| * For each code path (.apk) the method checks if a matching dex metadata file (.dm) exists. |
| * If it does it adds the pair to the returned map. |
| * |
| * Note that this method will do a loose |
| * matching based on the extension ('foo.dm' will match 'foo.apk' or 'foo'). |
| * |
| * This should only be used for code paths extracted from a package structure after the naming |
| * was enforced in the installer. |
| */ |
| public static Map<String, String> buildPackageApkToDexMetadataMap( |
| List<String> codePaths) { |
| ArrayMap<String, String> result = new ArrayMap<>(); |
| for (int i = codePaths.size() - 1; i >= 0; i--) { |
| String codePath = codePaths.get(i); |
| String dexMetadataPath = buildDexMetadataPathForFile(new File(codePath)); |
| |
| if (Files.exists(Paths.get(dexMetadataPath))) { |
| result.put(codePath, dexMetadataPath); |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Return the dex metadata path associated with the given code path. |
| * (replaces '.apk' extension with '.dm') |
| * |
| * @throws IllegalArgumentException if the code path is not an .apk. |
| */ |
| public static String buildDexMetadataPathForApk(String codePath) { |
| if (!PackageParser.isApkPath(codePath)) { |
| throw new IllegalStateException( |
| "Corrupted package. Code path is not an apk " + codePath); |
| } |
| return codePath.substring(0, codePath.length() - APK_FILE_EXTENSION.length()) |
| + DEX_METADATA_FILE_EXTENSION; |
| } |
| |
| /** |
| * Return the dex metadata path corresponding to the given {@code targetFile} using a loose |
| * matching. |
| * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile} |
| * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk'). |
| */ |
| private static String buildDexMetadataPathForFile(File targetFile) { |
| return PackageParser.isApkFile(targetFile) |
| ? buildDexMetadataPathForApk(targetFile.getPath()) |
| : targetFile.getPath() + DEX_METADATA_FILE_EXTENSION; |
| } |
| |
| /** |
| * Validate that the given file is a dex metadata archive. |
| * This is just a sanity validation that the file is a zip archive. |
| * |
| * @throws PackageParserException if the file is not a .dm file. |
| */ |
| public static void validateDexMetadataFile(String dmaPath) throws PackageParserException { |
| StrictJarFile jarFile = null; |
| try { |
| jarFile = new StrictJarFile(dmaPath, false, false); |
| } catch (IOException e) { |
| throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, |
| "Error opening " + dmaPath, e); |
| } finally { |
| if (jarFile != null) { |
| try { |
| jarFile.close(); |
| } catch (IOException ignored) { |
| } |
| } |
| } |
| } |
| |
| /** |
| * Validates that all dex metadata paths in the given list have a matching apk. |
| * (for any foo.dm there should be either a 'foo' of a 'foo.apk' file). |
| * If that's not the case it throws {@code IllegalStateException}. |
| * |
| * This is used to perform a basic sanity check during adb install commands. |
| * (The installer does not support stand alone .dm files) |
| */ |
| public static void validateDexPaths(String[] paths) { |
| ArrayList<String> apks = new ArrayList<>(); |
| for (int i = 0; i < paths.length; i++) { |
| if (PackageParser.isApkPath(paths[i])) { |
| apks.add(paths[i]); |
| } |
| } |
| ArrayList<String> unmatchedDmFiles = new ArrayList<>(); |
| for (int i = 0; i < paths.length; i++) { |
| String dmPath = paths[i]; |
| if (isDexMetadataPath(dmPath)) { |
| boolean valid = false; |
| for (int j = apks.size() - 1; j >= 0; j--) { |
| if (dmPath.equals(buildDexMetadataPathForFile(new File(apks.get(j))))) { |
| valid = true; |
| break; |
| } |
| } |
| if (!valid) { |
| unmatchedDmFiles.add(dmPath); |
| } |
| } |
| } |
| if (!unmatchedDmFiles.isEmpty()) { |
| throw new IllegalStateException("Unmatched .dm files: " + unmatchedDmFiles); |
| } |
| } |
| |
| } |