| /* |
| * 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.apk.AndroidBinXmlParser; |
| import com.android.apksig.internal.apk.v1.V1SchemeVerifier; |
| import com.android.apksig.internal.util.Pair; |
| import com.android.apksig.internal.zip.CentralDirectoryRecord; |
| import com.android.apksig.internal.zip.LocalFileRecord; |
| 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; |
| import java.util.List; |
| |
| /** |
| * APK utilities. |
| */ |
| public abstract class ApkUtils { |
| |
| /** |
| * Name of the Android manifest ZIP entry in APKs. |
| */ |
| public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml"; |
| |
| /** Name of the SourceStamp certificate hash ZIP entry in APKs. */ |
| public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256"; |
| |
| 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); |
| } |
| |
| // See https://source.android.com/security/apksigning/v2.html |
| private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; |
| private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; |
| private static final int APK_SIG_BLOCK_MIN_SIZE = 32; |
| |
| /** |
| * Returns the APK Signing Block of the provided APK. |
| * |
| * @throws IOException if an I/O error occurs |
| * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK |
| * |
| * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a> |
| */ |
| public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections) |
| throws IOException, ApkSigningBlockNotFoundException { |
| // FORMAT (see https://source.android.com/security/apksigning/v2.html): |
| // OFFSET DATA TYPE DESCRIPTION |
| // * @+0 bytes uint64: size in bytes (excluding this field) |
| // * @+8 bytes payload |
| // * @-24 bytes uint64: size in bytes (same as the one above) |
| // * @-16 bytes uint128: magic |
| |
| long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset(); |
| long centralDirEndOffset = |
| centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes(); |
| long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset(); |
| if (centralDirEndOffset != eocdStartOffset) { |
| throw new ApkSigningBlockNotFoundException( |
| "ZIP Central Directory is not immediately followed by End of Central Directory" |
| + ". CD end: " + centralDirEndOffset |
| + ", EoCD start: " + eocdStartOffset); |
| } |
| |
| if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) { |
| throw new ApkSigningBlockNotFoundException( |
| "APK too small for APK Signing Block. ZIP Central Directory offset: " |
| + centralDirStartOffset); |
| } |
| // Read the magic and offset in file from the footer section of the block: |
| // * uint64: size of block |
| // * 16 bytes: magic |
| ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24); |
| footer.order(ByteOrder.LITTLE_ENDIAN); |
| if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) |
| || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { |
| throw new ApkSigningBlockNotFoundException( |
| "No APK Signing Block before ZIP Central Directory"); |
| } |
| // Read and compare size fields |
| long apkSigBlockSizeInFooter = footer.getLong(0); |
| if ((apkSigBlockSizeInFooter < footer.capacity()) |
| || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { |
| throw new ApkSigningBlockNotFoundException( |
| "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); |
| } |
| int totalSize = (int) (apkSigBlockSizeInFooter + 8); |
| long apkSigBlockOffset = centralDirStartOffset - totalSize; |
| if (apkSigBlockOffset < 0) { |
| throw new ApkSigningBlockNotFoundException( |
| "APK Signing Block offset out of range: " + apkSigBlockOffset); |
| } |
| ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8); |
| apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); |
| long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); |
| if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { |
| throw new ApkSigningBlockNotFoundException( |
| "APK Signing Block sizes in header and footer do not match: " |
| + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); |
| } |
| return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize)); |
| } |
| |
| /** |
| * Information about the location of the APK Signing Block inside an APK. |
| */ |
| public static class ApkSigningBlock { |
| private final long mStartOffsetInApk; |
| private final DataSource mContents; |
| |
| /** |
| * Constructs a new {@code ApkSigningBlock}. |
| * |
| * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK |
| * Signing Block inside the APK file |
| * @param contents contents of the APK Signing Block |
| */ |
| public ApkSigningBlock(long startOffsetInApk, DataSource contents) { |
| mStartOffsetInApk = startOffsetInApk; |
| mContents = contents; |
| } |
| |
| /** |
| * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block. |
| */ |
| public long getStartOffset() { |
| return mStartOffsetInApk; |
| } |
| |
| /** |
| * Returns the data source which provides the full contents of the APK Signing Block, |
| * including its footer. |
| */ |
| public DataSource getContents() { |
| return mContents; |
| } |
| } |
| |
| /** |
| * Returns the contents of the APK's {@code AndroidManifest.xml}. |
| * |
| * @throws IOException if an I/O error occurs while reading the APK |
| * @throws ApkFormatException if the APK is malformed |
| */ |
| public static ByteBuffer getAndroidManifest(DataSource apk) |
| throws IOException, ApkFormatException { |
| ZipSections zipSections; |
| try { |
| zipSections = findZipSections(apk); |
| } catch (ZipFormatException e) { |
| throw new ApkFormatException("Not a valid ZIP archive", e); |
| } |
| List<CentralDirectoryRecord> cdRecords = |
| V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); |
| CentralDirectoryRecord androidManifestCdRecord = null; |
| for (CentralDirectoryRecord cdRecord : cdRecords) { |
| if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { |
| androidManifestCdRecord = cdRecord; |
| break; |
| } |
| } |
| if (androidManifestCdRecord == null) { |
| throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME); |
| } |
| DataSource lfhSection = apk.slice(0, zipSections.getZipCentralDirectoryOffset()); |
| |
| try { |
| return ByteBuffer.wrap( |
| LocalFileRecord.getUncompressedData( |
| lfhSection, androidManifestCdRecord, lfhSection.size())); |
| } catch (ZipFormatException e) { |
| throw new ApkFormatException("Failed to read " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); |
| } |
| } |
| |
| /** |
| * Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml. |
| */ |
| private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c; |
| |
| /** |
| * Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml. |
| */ |
| private static final int DEBUGGABLE_ATTR_ID = 0x0101000f; |
| |
| /** |
| * 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({"rawtypes", "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); |
| } |
| |
| /** |
| * Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}. |
| * See the {@code android:debuggable} attribute of the {@code application} element. |
| * |
| * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android |
| * resource format |
| * |
| * @throws ApkFormatException if the manifest is malformed |
| */ |
| public static boolean getDebuggableFromBinaryAndroidManifest( |
| ByteBuffer androidManifestContents) throws ApkFormatException { |
| // IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first |
| // "application" element which is a child of the top-level manifest element. The debuggable |
| // attribute of this application element is coerced to a boolean value. If there is no |
| // application element or if it doesn't declare the debuggable attribute, the package is |
| // considered not debuggable. |
| |
| try { |
| AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); |
| int eventType = parser.getEventType(); |
| while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { |
| if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) |
| && (parser.getDepth() == 2) |
| && ("application".equals(parser.getName())) |
| && (parser.getNamespace().isEmpty())) { |
| for (int i = 0; i < parser.getAttributeCount(); i++) { |
| if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) { |
| int valueType = parser.getAttributeValueType(i); |
| switch (valueType) { |
| case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN: |
| case AndroidBinXmlParser.VALUE_TYPE_STRING: |
| case AndroidBinXmlParser.VALUE_TYPE_INT: |
| String value = parser.getAttributeStringValue(i); |
| return ("true".equals(value)) |
| || ("TRUE".equals(value)) |
| || ("1".equals(value)); |
| case AndroidBinXmlParser.VALUE_TYPE_REFERENCE: |
| // References to resources are not supported on purpose. The |
| // reason is that the resolved value depends on the resource |
| // configuration (e.g, MNC/MCC, locale, screen density) used |
| // at resolution time. As a result, the same APK may appear as |
| // debuggable in one situation and as non-debuggable in another |
| // situation. Such APKs may put users at risk. |
| throw new ApkFormatException( |
| "Unable to determine whether APK is debuggable" |
| + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" |
| + " android:debuggable attribute references a" |
| + " resource. References are not supported for" |
| + " security reasons. Only constant boolean," |
| + " string and int values are supported."); |
| default: |
| throw new ApkFormatException( |
| "Unable to determine whether APK is debuggable" |
| + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" |
| + " android:debuggable attribute uses" |
| + " unsupported value type. Only boolean," |
| + " string and int values are supported."); |
| } |
| } |
| } |
| // This application element does not declare the debuggable attribute |
| return false; |
| } |
| eventType = parser.next(); |
| } |
| |
| // No application element found |
| return false; |
| } catch (AndroidBinXmlParser.XmlParserException e) { |
| throw new ApkFormatException( |
| "Unable to determine whether APK is debuggable: malformed binary resource: " |
| + ANDROID_MANIFEST_ZIP_ENTRY_NAME, |
| e); |
| } |
| } |
| |
| /** |
| * Returns the package name of the APK according to its {@code AndroidManifest.xml} or |
| * {@code null} if package name is not declared. See the {@code package} attribute of the |
| * {@code manifest} element. |
| * |
| * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android |
| * resource format |
| * |
| * @throws ApkFormatException if the manifest is malformed |
| */ |
| public static String getPackageNameFromBinaryAndroidManifest( |
| ByteBuffer androidManifestContents) throws ApkFormatException { |
| // IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level |
| // manifest element. Interestingly, as opposed to most other attributes, Android Package |
| // Manager looks up this attribute by its name rather than by its resource ID. |
| |
| try { |
| AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); |
| int eventType = parser.getEventType(); |
| while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { |
| if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) |
| && (parser.getDepth() == 1) |
| && ("manifest".equals(parser.getName())) |
| && (parser.getNamespace().isEmpty())) { |
| for (int i = 0; i < parser.getAttributeCount(); i++) { |
| if ("package".equals(parser.getAttributeName(i)) |
| && (parser.getNamespace().isEmpty())) { |
| return parser.getAttributeStringValue(i); |
| } |
| } |
| // No "package" attribute found |
| return null; |
| } |
| eventType = parser.next(); |
| } |
| |
| // No manifest element found |
| return null; |
| } catch (AndroidBinXmlParser.XmlParserException e) { |
| throw new ApkFormatException( |
| "Unable to determine APK package name: malformed binary resource: " |
| + ANDROID_MANIFEST_ZIP_ENTRY_NAME, |
| e); |
| } |
| } |
| } |