/*
 * Copyright (C) 2017 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.server.backup.utils;

import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME;
import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_VERSION;
import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_MANIFEST_PACKAGE_NAME;
import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_OLD_VERSION;
import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_POLICY_ALLOW_APKS;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_APK_NOT_INSTALLED;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_MISSING_SIGNATURE;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_RESTORE_ANY_VERSION;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_SYSTEM_APP_NO_AGENT;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSIONS_MATCH;
import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER;

import static com.android.server.backup.BackupManagerService.BACKUP_MANIFEST_FILENAME;
import static com.android.server.backup.BackupManagerService.BACKUP_MANIFEST_VERSION;
import static com.android.server.backup.BackupManagerService.BACKUP_METADATA_FILENAME;
import static com.android.server.backup.BackupManagerService.BACKUP_WIDGET_METADATA_TOKEN;
import static com.android.server.backup.BackupManagerService.DEBUG;
import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
import static com.android.server.backup.BackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.backup.BackupManagerService.TAG;

import android.app.backup.BackupAgent;
import android.app.backup.BackupManagerMonitor;
import android.app.backup.FullBackup;
import android.app.backup.IBackupManagerMonitor;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.Bundle;
import android.os.Process;
import android.util.Slog;

import com.android.server.backup.FileMetadata;
import com.android.server.backup.restore.RestorePolicy;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Utility methods to read backup tar file.
 */
public class TarBackupReader {
    private static final int TAR_HEADER_OFFSET_TYPE_CHAR = 156;
    private static final int TAR_HEADER_LENGTH_PATH = 100;
    private static final int TAR_HEADER_OFFSET_PATH = 0;
    private static final int TAR_HEADER_LENGTH_PATH_PREFIX = 155;
    private static final int TAR_HEADER_OFFSET_PATH_PREFIX = 345;
    private static final int TAR_HEADER_LENGTH_MODE = 8;
    private static final int TAR_HEADER_OFFSET_MODE = 100;
    private static final int TAR_HEADER_LENGTH_MODTIME = 12;
    private static final int TAR_HEADER_OFFSET_MODTIME = 136;
    private static final int TAR_HEADER_LENGTH_FILESIZE = 12;
    private static final int TAR_HEADER_OFFSET_FILESIZE = 124;
    private static final int TAR_HEADER_LONG_RADIX = 8;

    private final InputStream mInputStream;
    private final BytesReadListener mBytesReadListener;

    private IBackupManagerMonitor mMonitor;

    // Widget blob to be restored out-of-band.
    private byte[] mWidgetData = null;

    public TarBackupReader(InputStream inputStream, BytesReadListener bytesReadListener,
            IBackupManagerMonitor monitor) {
        mInputStream = inputStream;
        mBytesReadListener = bytesReadListener;
        mMonitor = monitor;
    }

    /**
     * Consumes a tar file header block [sequence] and accumulates the relevant metadata.
     */
    public FileMetadata readTarHeaders() throws IOException {
        byte[] block = new byte[512];
        FileMetadata info = null;

        boolean gotHeader = readTarHeader(block);
        if (gotHeader) {
            try {
                // okay, presume we're okay, and extract the various metadata
                info = new FileMetadata();
                info.size = extractRadix(block,
                        TAR_HEADER_OFFSET_FILESIZE,
                        TAR_HEADER_LENGTH_FILESIZE,
                        TAR_HEADER_LONG_RADIX);
                info.mtime = extractRadix(block,
                        TAR_HEADER_OFFSET_MODTIME,
                        TAR_HEADER_LENGTH_MODTIME,
                        TAR_HEADER_LONG_RADIX);
                info.mode = extractRadix(block,
                        TAR_HEADER_OFFSET_MODE,
                        TAR_HEADER_LENGTH_MODE,
                        TAR_HEADER_LONG_RADIX);

                info.path = extractString(block,
                        TAR_HEADER_OFFSET_PATH_PREFIX,
                        TAR_HEADER_LENGTH_PATH_PREFIX);
                String path = extractString(block,
                        TAR_HEADER_OFFSET_PATH,
                        TAR_HEADER_LENGTH_PATH);
                if (path.length() > 0) {
                    if (info.path.length() > 0) {
                        info.path += '/';
                    }
                    info.path += path;
                }

                // tar link indicator field: 1 byte at offset 156 in the header.
                int typeChar = block[TAR_HEADER_OFFSET_TYPE_CHAR];
                if (typeChar == 'x') {
                    // pax extended header, so we need to read that
                    gotHeader = readPaxExtendedHeader(info);
                    if (gotHeader) {
                        // and after a pax extended header comes another real header -- read
                        // that to find the real file type
                        gotHeader = readTarHeader(block);
                    }
                    if (!gotHeader) {
                        throw new IOException("Bad or missing pax header");
                    }

                    typeChar = block[TAR_HEADER_OFFSET_TYPE_CHAR];
                }

                switch (typeChar) {
                    case '0':
                        info.type = BackupAgent.TYPE_FILE;
                        break;
                    case '5': {
                        info.type = BackupAgent.TYPE_DIRECTORY;
                        if (info.size != 0) {
                            Slog.w(TAG, "Directory entry with nonzero size in header");
                            info.size = 0;
                        }
                        break;
                    }
                    case 0: {
                        // presume EOF
                        if (MORE_DEBUG) {
                            Slog.w(TAG, "Saw type=0 in tar header block, info=" + info);
                        }
                        return null;
                    }
                    default: {
                        Slog.e(TAG, "Unknown tar entity type: " + typeChar);
                        throw new IOException("Unknown entity type " + typeChar);
                    }
                }

                // Parse out the path
                //
                // first: apps/shared/unrecognized
                if (FullBackup.SHARED_PREFIX.regionMatches(0,
                        info.path, 0, FullBackup.SHARED_PREFIX.length())) {
                    // File in shared storage.  !!! TODO: implement this.
                    info.path = info.path.substring(FullBackup.SHARED_PREFIX.length());
                    info.packageName = SHARED_BACKUP_AGENT_PACKAGE;
                    info.domain = FullBackup.SHARED_STORAGE_TOKEN;
                    if (DEBUG) {
                        Slog.i(TAG, "File in shared storage: " + info.path);
                    }
                } else if (FullBackup.APPS_PREFIX.regionMatches(0,
                        info.path, 0, FullBackup.APPS_PREFIX.length())) {
                    // App content!  Parse out the package name and domain

                    // strip the apps/ prefix
                    info.path = info.path.substring(FullBackup.APPS_PREFIX.length());

                    // extract the package name
                    int slash = info.path.indexOf('/');
                    if (slash < 0) {
                        throw new IOException("Illegal semantic path in " + info.path);
                    }
                    info.packageName = info.path.substring(0, slash);
                    info.path = info.path.substring(slash + 1);

                    // if it's a manifest or metadata payload we're done, otherwise parse
                    // out the domain into which the file will be restored
                    if (!info.path.equals(BACKUP_MANIFEST_FILENAME) &&
                            !info.path.equals(BACKUP_METADATA_FILENAME)) {
                        slash = info.path.indexOf('/');
                        if (slash < 0) {
                            throw new IOException("Illegal semantic path in non-manifest "
                                    + info.path);
                        }
                        info.domain = info.path.substring(0, slash);
                        info.path = info.path.substring(slash + 1);
                    }
                }
            } catch (IOException e) {
                if (DEBUG) {
                    Slog.e(TAG, "Parse error in header: " + e.getMessage());
                    if (MORE_DEBUG) {
                        hexLog(block);
                    }
                }
                throw e;
            }
        }
        return info;
    }

    /**
     * Tries to read exactly the given number of bytes into a buffer at the stated offset.
     *
     * @param in - input stream to read bytes from..
     * @param buffer - where to write bytes to.
     * @param offset - offset in buffer to write bytes to.
     * @param size - number of bytes to read.
     * @return number of bytes actually read.
     * @throws IOException in case of an error.
     */
    private static int readExactly(InputStream in, byte[] buffer, int offset, int size)
            throws IOException {
        if (size <= 0) {
            throw new IllegalArgumentException("size must be > 0");
        }
        if (MORE_DEBUG) {
            Slog.i(TAG, "  ... readExactly(" + size + ") called");
        }
        int soFar = 0;
        while (soFar < size) {
            int nRead = in.read(buffer, offset + soFar, size - soFar);
            if (nRead <= 0) {
                if (MORE_DEBUG) {
                    Slog.w(TAG, "- wanted exactly " + size + " but got only " + soFar);
                }
                break;
            }
            soFar += nRead;
            if (MORE_DEBUG) {
                Slog.v(TAG, "   + got " + nRead + "; now wanting " + (size - soFar));
            }
        }
        return soFar;
    }

    /**
     * Reads app manifest, filling version and hasApk fields in the metadata, and returns array of
     * signatures.
     *
     * @param info - file metadata.
     * @return array of signatures or null, in case of an error.
     * @throws IOException in case of an error.
     */
    public Signature[] readAppManifestAndReturnSignatures(FileMetadata info)
            throws IOException {
        // Fail on suspiciously large manifest files
        if (info.size > 64 * 1024) {
            throw new IOException("Restore manifest too big; corrupt? size=" + info.size);
        }

        byte[] buffer = new byte[(int) info.size];
        if (MORE_DEBUG) {
            Slog.i(TAG,
                    "   readAppManifestAndReturnSignatures() looking for " + info.size + " bytes");
        }
        if (readExactly(mInputStream, buffer, 0, (int) info.size) == info.size) {
            mBytesReadListener.onBytesRead(info.size);
        } else {
            throw new IOException("Unexpected EOF in manifest");
        }

        String[] str = new String[1];
        int offset = 0;

        try {
            offset = extractLine(buffer, offset, str);
            int version = Integer.parseInt(str[0]);
            if (version == BACKUP_MANIFEST_VERSION) {
                offset = extractLine(buffer, offset, str);
                String manifestPackage = str[0];
                // TODO: handle <original-package>
                if (manifestPackage.equals(info.packageName)) {
                    offset = extractLine(buffer, offset, str);
                    info.version = Integer.parseInt(str[0]);  // app version
                    offset = extractLine(buffer, offset, str);
                    // This is the platform version, which we don't use, but we parse it
                    // as a safety against corruption in the manifest.
                    Integer.parseInt(str[0]);
                    offset = extractLine(buffer, offset, str);
                    info.installerPackageName = (str[0].length() > 0) ? str[0] : null;
                    offset = extractLine(buffer, offset, str);
                    info.hasApk = str[0].equals("1");
                    offset = extractLine(buffer, offset, str);
                    int numSigs = Integer.parseInt(str[0]);
                    if (numSigs > 0) {
                        Signature[] sigs = new Signature[numSigs];
                        for (int i = 0; i < numSigs; i++) {
                            offset = extractLine(buffer, offset, str);
                            sigs[i] = new Signature(str[0]);
                        }
                        return sigs;
                    } else {
                        Slog.i(TAG, "Missing signature on backed-up package " + info.packageName);
                        mMonitor = BackupManagerMonitorUtils.monitorEvent(
                                mMonitor,
                                LOG_EVENT_ID_MISSING_SIGNATURE,
                                null,
                                LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                                BackupManagerMonitorUtils.putMonitoringExtra(null,
                                        EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName));
                    }
                } else {
                    Slog.i(TAG, "Expected package " + info.packageName
                            + " but restore manifest claims " + manifestPackage);
                    Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
                            EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
                    monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
                            monitoringExtras,
                            EXTRA_LOG_MANIFEST_PACKAGE_NAME, manifestPackage);
                    mMonitor = BackupManagerMonitorUtils.monitorEvent(
                            mMonitor,
                            LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE,
                            null,
                            LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                            monitoringExtras);
                }
            } else {
                Slog.i(TAG, "Unknown restore manifest version " + version
                        + " for package " + info.packageName);
                Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
                        EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
                monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras,
                        EXTRA_LOG_EVENT_PACKAGE_VERSION, version);
                mMonitor = BackupManagerMonitorUtils.monitorEvent(
                        mMonitor,
                        BackupManagerMonitor.LOG_EVENT_ID_UNKNOWN_VERSION,
                        null,
                        LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                        monitoringExtras);

            }
        } catch (NumberFormatException e) {
            Slog.w(TAG, "Corrupt restore manifest for package " + info.packageName);
            mMonitor = BackupManagerMonitorUtils.monitorEvent(
                    mMonitor,
                    BackupManagerMonitor.LOG_EVENT_ID_CORRUPT_MANIFEST,
                    null,
                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                    BackupManagerMonitorUtils.putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME,
                            info.packageName));
        } catch (IllegalArgumentException e) {
            Slog.w(TAG, e.getMessage());
        }

        return null;
    }

    /**
     * Chooses restore policy.
     *
     * @param packageManager - PackageManager instance.
     * @param allowApks - allow restore set to include apks.
     * @param info - file metadata.
     * @param signatures - array of signatures parsed from backup file.
     * @return a restore policy constant.
     */
    public RestorePolicy chooseRestorePolicy(PackageManager packageManager,
            boolean allowApks, FileMetadata info, Signature[] signatures) {
        if (signatures == null) {
            return RestorePolicy.IGNORE;
        }

        RestorePolicy policy = RestorePolicy.IGNORE;

        // Okay, got the manifest info we need...
        try {
            PackageInfo pkgInfo = packageManager.getPackageInfo(
                    info.packageName, PackageManager.GET_SIGNATURES);
            // Fall through to IGNORE if the app explicitly disallows backup
            final int flags = pkgInfo.applicationInfo.flags;
            if ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0) {
                // Restore system-uid-space packages only if they have
                // defined a custom backup agent
                if ((pkgInfo.applicationInfo.uid
                        >= Process.FIRST_APPLICATION_UID)
                        || (pkgInfo.applicationInfo.backupAgentName != null)) {
                    // Verify signatures against any installed version; if they
                    // don't match, then we fall though and ignore the data.  The
                    // signatureMatch() method explicitly ignores the signature
                    // check for packages installed on the system partition, because
                    // such packages are signed with the platform cert instead of
                    // the app developer's cert, so they're different on every
                    // device.
                    if (AppBackupUtils.signaturesMatch(signatures, pkgInfo)) {
                        if ((pkgInfo.applicationInfo.flags
                                & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) != 0) {
                            Slog.i(TAG, "Package has restoreAnyVersion; taking data");
                            mMonitor = BackupManagerMonitorUtils.monitorEvent(
                                    mMonitor,
                                    LOG_EVENT_ID_RESTORE_ANY_VERSION,
                                    pkgInfo,
                                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                                    null);
                            policy = RestorePolicy.ACCEPT;
                        } else if (pkgInfo.getLongVersionCode() >= info.version) {
                            Slog.i(TAG, "Sig + version match; taking data");
                            policy = RestorePolicy.ACCEPT;
                            mMonitor = BackupManagerMonitorUtils.monitorEvent(
                                    mMonitor,
                                    LOG_EVENT_ID_VERSIONS_MATCH,
                                    pkgInfo,
                                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                                    null);
                        } else {
                            // The data is from a newer version of the app than
                            // is presently installed.  That means we can only
                            // use it if the matching apk is also supplied.
                            if (allowApks) {
                                Slog.i(TAG, "Data version " + info.version
                                        + " is newer than installed "
                                        + "version "
                                        + pkgInfo.getLongVersionCode()
                                        + " - requiring apk");
                                policy = RestorePolicy.ACCEPT_IF_APK;
                            } else {
                                Slog.i(TAG, "Data requires newer version "
                                        + info.version + "; ignoring");
                                mMonitor = BackupManagerMonitorUtils
                                        .monitorEvent(mMonitor,
                                                LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER,
                                                pkgInfo,
                                                LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                                                BackupManagerMonitorUtils
                                                        .putMonitoringExtra(
                                                                null,
                                                                EXTRA_LOG_OLD_VERSION,
                                                                info.version));

                                policy = RestorePolicy.IGNORE;
                            }
                        }
                    } else {
                        Slog.w(TAG, "Restore manifest signatures do not match "
                                + "installed application for "
                                + info.packageName);
                        mMonitor = BackupManagerMonitorUtils.monitorEvent(
                                mMonitor,
                                LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH,
                                pkgInfo,
                                LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                                null);
                    }
                } else {
                    Slog.w(TAG, "Package " + info.packageName
                            + " is system level with no agent");
                    mMonitor = BackupManagerMonitorUtils.monitorEvent(
                            mMonitor,
                            LOG_EVENT_ID_SYSTEM_APP_NO_AGENT,
                            pkgInfo,
                            LOG_EVENT_CATEGORY_AGENT,
                            null);
                }
            } else {
                if (DEBUG) {
                    Slog.i(TAG,
                            "Restore manifest from " + info.packageName + " but allowBackup=false");
                }
                mMonitor = BackupManagerMonitorUtils.monitorEvent(
                        mMonitor,
                        LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE,
                        pkgInfo,
                        LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                        null);
            }
        } catch (PackageManager.NameNotFoundException e) {
            // Okay, the target app isn't installed.  We can process
            // the restore properly only if the dataset provides the
            // apk file and we can successfully install it.
            if (allowApks) {
                if (DEBUG) {
                    Slog.i(TAG, "Package " + info.packageName
                            + " not installed; requiring apk in dataset");
                }
                policy = RestorePolicy.ACCEPT_IF_APK;
            } else {
                policy = RestorePolicy.IGNORE;
            }
            Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
                    null,
                    EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
            monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(
                    monitoringExtras,
                    EXTRA_LOG_POLICY_ALLOW_APKS, allowApks);
            mMonitor = BackupManagerMonitorUtils.monitorEvent(
                    mMonitor,
                    LOG_EVENT_ID_APK_NOT_INSTALLED,
                    null,
                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                    monitoringExtras);
        }

        if (policy == RestorePolicy.ACCEPT_IF_APK && !info.hasApk) {
            Slog.i(TAG, "Cannot restore package " + info.packageName
                    + " without the matching .apk");
            mMonitor = BackupManagerMonitorUtils.monitorEvent(
                    mMonitor,
                    LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK,
                    null,
                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                    BackupManagerMonitorUtils.putMonitoringExtra(null,
                            EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName));
        }

        return policy;
    }

    // Given an actual file content size, consume the post-content padding mandated
    // by the tar format.
    public void skipTarPadding(long size) throws IOException {
        long partial = (size + 512) % 512;
        if (partial > 0) {
            final int needed = 512 - (int) partial;
            if (MORE_DEBUG) {
                Slog.i(TAG, "Skipping tar padding: " + needed + " bytes");
            }
            byte[] buffer = new byte[needed];
            if (readExactly(mInputStream, buffer, 0, needed) == needed) {
                mBytesReadListener.onBytesRead(needed);
            } else {
                throw new IOException("Unexpected EOF in padding");
            }
        }
    }

    /**
     * Read a widget metadata file, returning the restored blob.
     */
    public void readMetadata(FileMetadata info) throws IOException {
        // Fail on suspiciously large widget dump files
        if (info.size > 64 * 1024) {
            throw new IOException("Metadata too big; corrupt? size=" + info.size);
        }

        byte[] buffer = new byte[(int) info.size];
        if (readExactly(mInputStream, buffer, 0, (int) info.size) == info.size) {
            mBytesReadListener.onBytesRead(info.size);
        } else {
            throw new IOException("Unexpected EOF in widget data");
        }

        String[] str = new String[1];
        int offset = extractLine(buffer, 0, str);
        int version = Integer.parseInt(str[0]);
        if (version == BACKUP_MANIFEST_VERSION) {
            offset = extractLine(buffer, offset, str);
            final String pkg = str[0];
            if (info.packageName.equals(pkg)) {
                // Data checks out -- the rest of the buffer is a concatenation of
                // binary blobs as described in the comment at writeAppWidgetData()
                ByteArrayInputStream bin = new ByteArrayInputStream(buffer,
                        offset, buffer.length - offset);
                DataInputStream in = new DataInputStream(bin);
                while (bin.available() > 0) {
                    int token = in.readInt();
                    int size = in.readInt();
                    if (size > 64 * 1024) {
                        throw new IOException("Datum " + Integer.toHexString(token)
                                + " too big; corrupt? size=" + info.size);
                    }
                    switch (token) {
                        case BACKUP_WIDGET_METADATA_TOKEN: {
                            if (MORE_DEBUG) {
                                Slog.i(TAG, "Got widget metadata for " + info.packageName);
                            }
                            mWidgetData = new byte[size];
                            in.read(mWidgetData);
                            break;
                        }
                        default: {
                            if (DEBUG) {
                                Slog.i(TAG, "Ignoring metadata blob " + Integer.toHexString(token)
                                        + " for " + info.packageName);
                            }
                            in.skipBytes(size);
                            break;
                        }
                    }
                }
            } else {
                Slog.w(TAG,
                        "Metadata mismatch: package " + info.packageName + " but widget data for "
                                + pkg);

                Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
                        EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
                monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras,
                        BackupManagerMonitor.EXTRA_LOG_WIDGET_PACKAGE_NAME, pkg);
                mMonitor = BackupManagerMonitorUtils.monitorEvent(
                        mMonitor,
                        BackupManagerMonitor.LOG_EVENT_ID_WIDGET_METADATA_MISMATCH,
                        null,
                        LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                        monitoringExtras);
            }
        } else {
            Slog.w(TAG, "Unsupported metadata version " + version);

            Bundle monitoringExtras = BackupManagerMonitorUtils
                    .putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME,
                            info.packageName);
            monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras,
                    EXTRA_LOG_EVENT_PACKAGE_VERSION, version);
            mMonitor = BackupManagerMonitorUtils.monitorEvent(
                    mMonitor,
                    BackupManagerMonitor.LOG_EVENT_ID_WIDGET_UNKNOWN_VERSION,
                    null,
                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                    monitoringExtras);
        }
    }

    /**
     * Builds a line from a byte buffer starting at 'offset'.
     *
     * @param buffer - where to read a line from.
     * @param offset - offset in buffer to read a line from.
     * @param outStr - an output parameter, the result will be put in outStr.
     * @return the index of the next unconsumed data in the buffer.
     * @throws IOException in case of an error.
     */
    private static int extractLine(byte[] buffer, int offset, String[] outStr) throws IOException {
        final int end = buffer.length;
        if (offset >= end) {
            throw new IOException("Incomplete data");
        }

        int pos;
        for (pos = offset; pos < end; pos++) {
            byte c = buffer[pos];
            // at LF we declare end of line, and return the next char as the
            // starting point for the next time through
            if (c == '\n') {
                break;
            }
        }
        outStr[0] = new String(buffer, offset, pos - offset);
        pos++;  // may be pointing an extra byte past the end but that's okay
        return pos;
    }

    private boolean readTarHeader(byte[] block) throws IOException {
        final int got = readExactly(mInputStream, block, 0, 512);
        if (got == 0) {
            return false;     // Clean EOF
        }
        if (got < 512) {
            throw new IOException("Unable to read full block header");
        }
        mBytesReadListener.onBytesRead(512);
        return true;
    }

    // overwrites 'info' fields based on the pax extended header
    private boolean readPaxExtendedHeader(FileMetadata info)
            throws IOException {
        // We should never see a pax extended header larger than this
        if (info.size > 32 * 1024) {
            Slog.w(TAG, "Suspiciously large pax header size " + info.size + " - aborting");
            throw new IOException("Sanity failure: pax header size " + info.size);
        }

        // read whole blocks, not just the content size
        int numBlocks = (int) ((info.size + 511) >> 9);
        byte[] data = new byte[numBlocks * 512];
        if (readExactly(mInputStream, data, 0, data.length) < data.length) {
            throw new IOException("Unable to read full pax header");
        }
        mBytesReadListener.onBytesRead(data.length);

        final int contentSize = (int) info.size;
        int offset = 0;
        do {
            // extract the line at 'offset'
            int eol = offset + 1;
            while (eol < contentSize && data[eol] != ' ') {
                eol++;
            }
            if (eol >= contentSize) {
                // error: we just hit EOD looking for the end of the size field
                throw new IOException("Invalid pax data");
            }
            // eol points to the space between the count and the key
            int linelen = (int) extractRadix(data, offset, eol - offset, 10);
            int key = eol + 1;  // start of key=value
            eol = offset + linelen - 1; // trailing LF
            int value;
            for (value = key + 1; data[value] != '=' && value <= eol; value++) {
                ;
            }
            if (value > eol) {
                throw new IOException("Invalid pax declaration");
            }

            // pax requires that key/value strings be in UTF-8
            String keyStr = new String(data, key, value - key, "UTF-8");
            // -1 to strip the trailing LF
            String valStr = new String(data, value + 1, eol - value - 1, "UTF-8");

            if ("path".equals(keyStr)) {
                info.path = valStr;
            } else if ("size".equals(keyStr)) {
                info.size = Long.parseLong(valStr);
            } else {
                if (DEBUG) {
                    Slog.i(TAG, "Unhandled pax key: " + key);
                }
            }

            offset += linelen;
        } while (offset < contentSize);

        return true;
    }

    private static long extractRadix(byte[] data, int offset, int maxChars, int radix)
            throws IOException {
        long value = 0;
        final int end = offset + maxChars;
        for (int i = offset; i < end; i++) {
            final byte b = data[i];
            // Numeric fields in tar can terminate with either NUL or SPC
            if (b == 0 || b == ' ') {
                break;
            }
            if (b < '0' || b > ('0' + radix - 1)) {
                throw new IOException("Invalid number in header: '" + (char) b
                        + "' for radix " + radix);
            }
            value = radix * value + (b - '0');
        }
        return value;
    }

    private static String extractString(byte[] data, int offset, int maxChars) throws IOException {
        final int end = offset + maxChars;
        int eos = offset;
        // tar string fields terminate early with a NUL
        while (eos < end && data[eos] != 0) {
            eos++;
        }
        return new String(data, offset, eos - offset, "US-ASCII");
    }

    private static void hexLog(byte[] block) {
        int offset = 0;
        int todo = block.length;
        StringBuilder buf = new StringBuilder(64);
        while (todo > 0) {
            buf.append(String.format("%04x   ", offset));
            int numThisLine = (todo > 16) ? 16 : todo;
            for (int i = 0; i < numThisLine; i++) {
                buf.append(String.format("%02x ", block[offset + i]));
            }
            Slog.i("hexdump", buf.toString());
            buf.setLength(0);
            todo -= numThisLine;
            offset += numThisLine;
        }
    }

    public IBackupManagerMonitor getMonitor() {
        return mMonitor;
    }

    public byte[] getWidgetData() {
        return mWidgetData;
    }
}
