/* * Copyright (C) 2016 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 com.android.apksig.apk; import com.android.apksig.internal.util.Pair; import com.android.apksig.internal.zip.ZipUtils; import com.android.apksig.util.DataSource; import com.android.apksig.zip.ZipFormatException; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; import java.util.Comparator; /** * APK utilities. */ public abstract class ApkUtils { private ApkUtils() {} /** * Finds the main ZIP sections of the provided APK. * * @throws IOException if an I/O error occurred while reading the APK * @throws ZipFormatException if the APK is malformed */ public static ZipSections findZipSections(DataSource apk) throws IOException, ZipFormatException { Pair<ByteBuffer, Long> eocdAndOffsetInFile = ZipUtils.findZipEndOfCentralDirectoryRecord(apk); if (eocdAndOffsetInFile == null) { throw new ZipFormatException("ZIP End of Central Directory record not found"); } ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst(); long eocdOffset = eocdAndOffsetInFile.getSecond(); eocdBuf.order(ByteOrder.LITTLE_ENDIAN); long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf); if (cdStartOffset > eocdOffset) { throw new ZipFormatException( "ZIP Central Directory start offset out of range: " + cdStartOffset + ". ZIP End of Central Directory offset: " + eocdOffset); } long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf); long cdEndOffset = cdStartOffset + cdSizeBytes; if (cdEndOffset > eocdOffset) { throw new ZipFormatException( "ZIP Central Directory overlaps with End of Central Directory" + ". CD end: " + cdEndOffset + ", EoCD start: " + eocdOffset); } int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf); return new ZipSections( cdStartOffset, cdSizeBytes, cdRecordCount, eocdOffset, eocdBuf); } /** * Information about the ZIP sections of an APK. */ public static class ZipSections { private final long mCentralDirectoryOffset; private final long mCentralDirectorySizeBytes; private final int mCentralDirectoryRecordCount; private final long mEocdOffset; private final ByteBuffer mEocd; public ZipSections( long centralDirectoryOffset, long centralDirectorySizeBytes, int centralDirectoryRecordCount, long eocdOffset, ByteBuffer eocd) { mCentralDirectoryOffset = centralDirectoryOffset; mCentralDirectorySizeBytes = centralDirectorySizeBytes; mCentralDirectoryRecordCount = centralDirectoryRecordCount; mEocdOffset = eocdOffset; mEocd = eocd; } /** * Returns the start offset of the ZIP Central Directory. This value is taken from the * ZIP End of Central Directory record. */ public long getZipCentralDirectoryOffset() { return mCentralDirectoryOffset; } /** * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the * ZIP End of Central Directory record. */ public long getZipCentralDirectorySizeBytes() { return mCentralDirectorySizeBytes; } /** * Returns the number of records in the ZIP Central Directory. This value is taken from the * ZIP End of Central Directory record. */ public int getZipCentralDirectoryRecordCount() { return mCentralDirectoryRecordCount; } /** * Returns the start offset of the ZIP End of Central Directory record. The record extends * until the very end of the APK. */ public long getZipEndOfCentralDirectoryOffset() { return mEocdOffset; } /** * Returns the contents of the ZIP End of Central Directory. */ public ByteBuffer getZipEndOfCentralDirectory() { return mEocd; } } /** * Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central * Directory record. * * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must * be between {@code 0} and {@code 2^32 - 1} inclusive. */ public static void setZipEocdCentralDirectoryOffset( ByteBuffer zipEndOfCentralDirectory, long offset) { ByteBuffer eocd = zipEndOfCentralDirectory.slice(); eocd.order(ByteOrder.LITTLE_ENDIAN); ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset); } /** * Name of the Android manifest ZIP entry in APKs. */ private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml"; /** * Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml. */ private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c; /** * Returns the lowest Android platform version (API Level) supported by an APK with the * provided {@code AndroidManifest.xml}. * * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android * resource format * * @throws MinSdkVersionException if an error occurred while determining the API Level */ // public static int getMinSdkVersionFromBinaryAndroidManifest( // ByteBuffer androidManifestContents) throws MinSdkVersionException { // // IMPLEMENTATION NOTE: Minimum supported Android platform version number is declared using // // uses-sdk elements which are children of the top-level manifest element. uses-sdk element // // declares the minimum supported platform version using the android:minSdkVersion attribute // // whose default value is 1. // // For each encountered uses-sdk element, the Android runtime checks that its minSdkVersion // // is not higher than the runtime's API Level and rejects APKs if it is higher. Thus, the // // effective minSdkVersion value is the maximum over the encountered minSdkVersion values. // // try { // // If no uses-sdk elements are encountered, Android accepts the APK. We treat this // // scenario as though the minimum supported API Level is 1. // int result = 1; // // AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); // int eventType = parser.getEventType(); // while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { // if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) // && (parser.getDepth() == 2) // && ("uses-sdk".equals(parser.getName())) // && (parser.getNamespace().isEmpty())) { // // In each uses-sdk element, minSdkVersion defaults to 1 // int minSdkVersion = 1; // for (int i = 0; i < parser.getAttributeCount(); i++) { // if (parser.getAttributeNameResourceId(i) == MIN_SDK_VERSION_ATTR_ID) { // int valueType = parser.getAttributeValueType(i); // switch (valueType) { // case AndroidBinXmlParser.VALUE_TYPE_INT: // minSdkVersion = parser.getAttributeIntValue(i); // break; // case AndroidBinXmlParser.VALUE_TYPE_STRING: // minSdkVersion = // getMinSdkVersionForCodename( // parser.getAttributeStringValue(i)); // break; // default: // throw new MinSdkVersionException( // "Unable to determine APK's minimum supported Android" // + ": unsupported value type in " // + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" // + " minSdkVersion" // + ". Only integer values supported."); // } // break; // } // } // result = Math.max(result, minSdkVersion); // } // eventType = parser.next(); // } // // return result; // } catch (AndroidBinXmlParser.XmlParserException e) { // throw new MinSdkVersionException( // "Unable to determine APK's minimum supported Android platform version" // + ": malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, // e); // } // } private static class CodenamesLazyInitializer { /** * List of platform codename (first letter of) to API Level mappings. The list must be * sorted by the first letter. For codenames not in the list, the assumption is that the API * Level is incremented by one for every increase in the codename's first letter. */ @SuppressWarnings("unchecked") private static final Pair<Character, Integer>[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL = new Pair[] { Pair.of('C', 2), Pair.of('D', 3), Pair.of('E', 4), Pair.of('F', 7), Pair.of('G', 8), Pair.of('H', 10), Pair.of('I', 13), Pair.of('J', 15), Pair.of('K', 18), Pair.of('L', 20), Pair.of('M', 22), Pair.of('N', 23), Pair.of('O', 25), }; private static final Comparator<Pair<Character, Integer>> CODENAME_FIRST_CHAR_COMPARATOR = new ByFirstComparator(); private static class ByFirstComparator implements Comparator<Pair<Character, Integer>> { @Override public int compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2) { char c1 = o1.getFirst(); char c2 = o2.getFirst(); return c1 - c2; } } } /** * Returns the API Level corresponding to the provided platform codename. * * <p>This method is pessimistic. It returns a value one lower than the API Level with which the * platform is actually released (e.g., 23 for N which was released as API Level 24). This is * because new features which first appear in an API Level are not available in the early days * of that platform version's existence, when the platform only has a codename. Moreover, this * method currently doesn't differentiate between initial and MR releases, meaning API Level * returned for MR releases may be more than one lower than the API Level with which the * platform version is actually released. * * @throws CodenameMinSdkVersionException if the {@code codename} is not supported */ // static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException { // char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0); // // Codenames are case-sensitive. Only codenames starting with A-Z are supported for now. // // We only look at the first letter of the codename as this is the most important letter. // if ((firstChar >= 'A') && (firstChar <= 'Z')) { // Pair<Character, Integer>[] sortedCodenamesFirstCharToApiLevel = // CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL; // int searchResult = // Arrays.binarySearch( // sortedCodenamesFirstCharToApiLevel, // Pair.of(firstChar, null), // second element of the pair is ignored here // CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR); // if (searchResult >= 0) { // // Exact match -- searchResult is the index of the matching element // return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond(); // } // // Not an exact match -- searchResult is negative and is -(insertion index) - 1. // // The element at insertionIndex - 1 (if present) is smaller than firstChar and the // // element at insertionIndex (if present) is greater than firstChar. // int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length] // if (insertionIndex == 0) { // // 'A' or 'B' -- never released to public // return 1; // } else { // // The element at insertionIndex - 1 is the newest older codename. // // API Level bumped by at least 1 for every change in the first letter of codename // Pair<Character, Integer> newestOlderCodenameMapping = // sortedCodenamesFirstCharToApiLevel[insertionIndex - 1]; // char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst(); // int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond(); // return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar); // } // } // // throw new CodenameMinSdkVersionException( // "Unable to determine APK's minimum supported Android platform version" // + " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME // + "'s minSdkVersion: \"" + codename + "\"", // codename); // } }