blob: 5d10b8826b0071dfb1525a7864fdde9c0444ab6a [file] [log] [blame]
/**
* 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.
*/
public static Map<String, String> getPackageDexMetadata(PackageParser.Package pkg) {
return buildPackageApkToDexMetadataMap(pkg.getAllCodePaths());
}
/**
* 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.
*/
private 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 the dex metadata files installed for the given package.
*
* @throws PackageParserException in case of errors.
*/
public static void validatePackageDexMetadata(PackageParser.Package pkg)
throws PackageParserException {
Collection<String> apkToDexMetadataList = getPackageDexMetadata(pkg).values();
for (String dexMetadata : apkToDexMetadataList) {
validateDexMetadataFile(dexMetadata);
}
}
/**
* 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.
*/
private 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);
}
}
}