/*
 * 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.stamp.SourceStampConstants;
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 =
            SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;

    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 {
        com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
        return new ZipSections(
                zipSections.getZipCentralDirectoryOffset(),
                zipSections.getZipCentralDirectorySizeBytes(),
                zipSections.getZipCentralDirectoryRecordCount(),
                zipSections.getZipEndOfCentralDirectoryOffset(),
                zipSections.getZipEndOfCentralDirectory());
    }

    /**
     * Information about the ZIP sections of an APK.
     */
    public static class ZipSections extends com.android.apksig.zip.ZipSections {
        public ZipSections(
                long centralDirectoryOffset,
                long centralDirectorySizeBytes,
                int centralDirectoryRecordCount,
                long eocdOffset,
                ByteBuffer eocd) {
            super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount,
                    eocdOffset, eocd);
        }
    }

    /**
     * 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);
    }

    /**
     * Updates the length of EOCD comment.
     *
     * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
     */
    public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
        ByteBuffer eocd = zipEndOfCentralDirectory.slice();
        eocd.order(ByteOrder.LITTLE_ENDIAN);
        ZipUtils.updateZipEocdCommentLen(eocd);
    }

    /**
     * Returns the APK Signing Block of the provided {@code apk}.
     *
     * @throws ApkFormatException if the APK is not a valid ZIP archive
     * @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)
            throws ApkFormatException, IOException, ApkSigningBlockNotFoundException {
        ApkUtils.ZipSections inputZipSections;
        try {
            inputZipSections = ApkUtils.findZipSections(apk);
        } catch (ZipFormatException e) {
            throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
        }
        return findApkSigningBlock(apk, inputZipSections);
    }

    /**
     * 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 {
        ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk,
                zipSections);
        return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents());
    }

    /**
     * Information about the location of the APK Signing Block inside an APK.
     */
    public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock {
        /**
         * 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) {
            super(startOffsetInApk, contents);
        }
    }

    /**
     * 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;

    /**
     * Android resource ID of the {@code android:targetSandboxVersion} attribute in
     * AndroidManifest.xml.
     */
    private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;

    /**
     * Android resource ID of the {@code android:targetSdkVersion} attribute in
     * AndroidManifest.xml.
     */
    private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
    private static final String USES_SDK_ELEMENT_TAG = "uses-sdk";

    /**
     * Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml.
     */
    private static final int VERSION_CODE_ATTR_ID = 0x0101021b;
    private static final String MANIFEST_ELEMENT_TAG = "manifest";

    /**
     * Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml.
     */
    private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576;

    /**
     * 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 {
        if ("Baklava".equals(codename)) {
            return 34; // VIC (35) was the version before Baklava, return VIC version minus one
        }

        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' (not Baklava) -- 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);
        }
    }

    /**
     * Returns the security sandbox version targeted by an APK with the provided
     * {@code AndroidManifest.xml}.
     *
     * <p>If the security sandbox version is not specified in the manifest a default value of 1 is
     * returned.
     *
     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
     *                                resource format
     */
    public static int getTargetSandboxVersionFromBinaryAndroidManifest(
            ByteBuffer androidManifestContents) {
        try {
            return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
                    MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID);
        } catch (ApkFormatException e) {
            // An ApkFormatException indicates the target sandbox is not specified in the manifest;
            // return a default value of 1.
            return 1;
        }
    }

    /**
     * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
     *
     * <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither
     * value is specified then a value of 1 is returned.
     *
     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
     *                                resource format
     */
    public static int getTargetSdkVersionFromBinaryAndroidManifest(
            ByteBuffer androidManifestContents) {
        // If the targetSdkVersion is not specified then the platform will use the value of the
        // minSdkVersion; if neither is specified then the platform will use a value of 1.
        int minSdkVersion = 1;
        try {
            return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
                    USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID);
        } catch (ApkFormatException e) {
            // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
            // element is not specified at all.
        }
        androidManifestContents.rewind();
        try {
            minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents);
        } catch (ApkFormatException e) {
            // Similar to above, expected if the APK does not contain a minSdkVersion attribute, or
            // the uses-sdk element is not specified at all.
        }
        return minSdkVersion;
    }

    /**
     * Returns the versionCode of the APK according to its {@code AndroidManifest.xml}.
     *
     * <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid
     * integer an ApkFormatException is thrown.
     *
     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
     *                                resource format
     * @throws ApkFormatException if an error occurred while determining the versionCode, or if the
     *                            versionCode attribute value is not available.
     */
    public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)
            throws ApkFormatException {
        return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
                MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID);
    }

    /**
     * Returns the versionCode and versionCodeMajor of the APK according to its {@code
     * AndroidManifest.xml} combined together as a single long value.
     *
     * <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower
     * 32 bits. If the versionCodeMajor is not specified then the versionCode is returned.
     *
     * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
     *                                resource format
     * @throws ApkFormatException if an error occurred while determining the version, or if the
     *                            versionCode attribute value is not available.
     */
    public static long getLongVersionCodeFromBinaryAndroidManifest(
            ByteBuffer androidManifestContents) throws ApkFormatException {
        // If the versionCode is not found then allow the ApkFormatException to be thrown to notify
        // the caller that the versionCode is not available.
        int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents);
        long versionCodeMajor = 0;
        try {
            androidManifestContents.rewind();
            versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
                    MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID);
        } catch (ApkFormatException e) {
            // This is expected if the versionCodeMajor has not been defined for the APK; in this
            // case the return value is just the versionCode.
        }
        return (versionCodeMajor << 32) | versionCode;
    }

    /**
     * Returns the integer value of the requested {@code attributeId} in the specified {@code
     * elementName} from the provided {@code androidManifestContents} in binary Android resource
     * format.
     *
     * @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or
     *                            if the requested attribute is not found.
     */
    private static int getAttributeValueFromBinaryAndroidManifest(
            ByteBuffer androidManifestContents, String elementName, int attributeId)
            throws ApkFormatException {
        if (elementName == null) {
            throw new NullPointerException("elementName cannot be null");
        }
        try {
            AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
            int eventType = parser.getEventType();
            while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
                if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
                        && (elementName.equals(parser.getName()))) {
                    for (int i = 0; i < parser.getAttributeCount(); i++) {
                        if (parser.getAttributeNameResourceId(i) == attributeId) {
                            int valueType = parser.getAttributeValueType(i);
                            switch (valueType) {
                                case AndroidBinXmlParser.VALUE_TYPE_INT:
                                case AndroidBinXmlParser.VALUE_TYPE_STRING:
                                    return parser.getAttributeIntValue(i);
                                default:
                                    throw new ApkFormatException(
                                            "Unsupported value type, " + valueType
                                                    + ", for attribute " + String.format("0x%08X",
                                                    attributeId) + " under element " + elementName);

                            }
                        }
                    }
                }
                eventType = parser.next();
            }
            throw new ApkFormatException(
                    "Failed to determine APK's " + elementName + " attribute "
                            + String.format("0x%08X", attributeId) + " value");
        } catch (AndroidBinXmlParser.XmlParserException e) {
            throw new ApkFormatException(
                    "Unable to determine value for attribute " + String.format("0x%08X",
                            attributeId) + " under element " + elementName
                            + "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
        }
    }

    public static byte[] computeSha256DigestBytes(byte[] data) {
        return ApkUtilsLite.computeSha256DigestBytes(data);
    }
}
