/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.app.backup;

import static android.app.backup.BackupManager.OperationType;

import android.annotation.Nullable;
import android.annotation.StringDef;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.Overridable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.XmlResourceParser;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.system.ErrnoException;
import android.system.Os;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
 * Global constant definitions et cetera related to the full-backup-to-fd
 * binary format.  Nothing in this namespace is part of any API; it's all
 * hidden details of the current implementation gathered into one location.
 *
 * @hide
 */
public class FullBackup {
    static final String TAG = "FullBackup";
    /** Enable this log tag to get verbose information while parsing the client xml. */
    static final String TAG_XML_PARSER = "BackupXmlParserLogging";

    public static final String APK_TREE_TOKEN = "a";
    public static final String OBB_TREE_TOKEN = "obb";
    public static final String KEY_VALUE_DATA_TOKEN = "k";

    public static final String ROOT_TREE_TOKEN = "r";
    public static final String FILES_TREE_TOKEN = "f";
    public static final String NO_BACKUP_TREE_TOKEN = "nb";
    public static final String DATABASE_TREE_TOKEN = "db";
    public static final String SHAREDPREFS_TREE_TOKEN = "sp";
    public static final String CACHE_TREE_TOKEN = "c";

    public static final String DEVICE_ROOT_TREE_TOKEN = "d_r";
    public static final String DEVICE_FILES_TREE_TOKEN = "d_f";
    public static final String DEVICE_NO_BACKUP_TREE_TOKEN = "d_nb";
    public static final String DEVICE_DATABASE_TREE_TOKEN = "d_db";
    public static final String DEVICE_SHAREDPREFS_TREE_TOKEN = "d_sp";
    public static final String DEVICE_CACHE_TREE_TOKEN = "d_c";

    public static final String MANAGED_EXTERNAL_TREE_TOKEN = "ef";
    public static final String SHARED_STORAGE_TOKEN = "shared";

    public static final String APPS_PREFIX = "apps/";
    public static final String SHARED_PREFIX = SHARED_STORAGE_TOKEN + "/";

    public static final String FULL_BACKUP_INTENT_ACTION = "fullback";
    public static final String FULL_RESTORE_INTENT_ACTION = "fullrest";
    public static final String CONF_TOKEN_INTENT_EXTRA = "conftoken";

    public static final String FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION = "clientSideEncryption";
    public static final String FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER = "deviceToDeviceTransfer";
    public static final String FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION =
            "fakeClientSideEncryption";
    private static final String FLAG_DISABLE_IF_NO_ENCRYPTION_CAPABILITIES
            = "disableIfNoEncryptionCapabilities";

    /**
     * When  this change is enabled, include / exclude rules specified via
     * {@code android:fullBackupContent} are ignored during D2D transfers.
     */
    @ChangeId
    @Overridable
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.S)
    private static final long IGNORE_FULL_BACKUP_CONTENT_IN_D2D = 180523564L;

    @StringDef({
        ConfigSection.CLOUD_BACKUP,
        ConfigSection.DEVICE_TRANSFER
    })
    @interface ConfigSection {
        String CLOUD_BACKUP = "cloud-backup";
        String DEVICE_TRANSFER = "device-transfer";
    }

    /**
     * Identify {@link BackupScheme} object by package and operation type
     * (see {@link OperationType}) it corresponds to.
     */
    private static class BackupSchemeId {
        final String mPackageName;
        @OperationType final int mOperationType;

        BackupSchemeId(String packageName, @OperationType int operationType) {
            mPackageName = packageName;
            mOperationType = operationType;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mPackageName, mOperationType);
        }

        @Override
        public boolean equals(@Nullable Object object) {
            if (this == object) {
                return true;
            }
            if (object == null || getClass() != object.getClass()) {
                return false;
            }
            BackupSchemeId that = (BackupSchemeId) object;
            return Objects.equals(mPackageName, that.mPackageName) &&
                    Objects.equals(mOperationType, that.mOperationType);
        }
    }

    /**
     * @hide
     */
    @UnsupportedAppUsage
    static public native int backupToTar(String packageName, String domain,
            String linkdomain, String rootpath, String path, FullBackupDataOutput output);

    private static final Map<BackupSchemeId, BackupScheme> kPackageBackupSchemeMap =
            new ArrayMap<>();

    static synchronized BackupScheme getBackupScheme(Context context,
            @OperationType int operationType) {
        BackupSchemeId backupSchemeId = new BackupSchemeId(context.getPackageName(), operationType);
        BackupScheme backupSchemeForPackage =
                kPackageBackupSchemeMap.get(backupSchemeId);
        if (backupSchemeForPackage == null) {
            backupSchemeForPackage = new BackupScheme(context, operationType);
            kPackageBackupSchemeMap.put(backupSchemeId, backupSchemeForPackage);
        }
        return backupSchemeForPackage;
    }

    public static BackupScheme getBackupSchemeForTest(Context context) {
        BackupScheme testing = new BackupScheme(context, OperationType.BACKUP);
        testing.mExcludes = new ArraySet();
        testing.mIncludes = new ArrayMap();
        return testing;
    }


    /**
     * Copy data from a socket to the given File location on permanent storage.  The
     * modification time and access mode of the resulting file will be set if desired,
     * although group/all rwx modes will be stripped: the restored file will not be
     * accessible from outside the target application even if the original file was.
     * If the {@code type} parameter indicates that the result should be a directory,
     * the socket parameter may be {@code null}; even if it is valid, no data will be
     * read from it in this case.
     * <p>
     * If the {@code mode} argument is negative, then the resulting output file will not
     * have its access mode or last modification time reset as part of this operation.
     *
     * @param data Socket supplying the data to be copied to the output file.  If the
     *    output is a directory, this may be {@code null}.
     * @param size Number of bytes of data to copy from the socket to the file.  At least
     *    this much data must be available through the {@code data} parameter.
     * @param type Must be either {@link BackupAgent#TYPE_FILE} for ordinary file data
     *    or {@link BackupAgent#TYPE_DIRECTORY} for a directory.
     * @param mode Unix-style file mode (as used by the chmod(2) syscall) to be set on
     *    the output file or directory.  group/all rwx modes are stripped even if set
     *    in this parameter.  If this parameter is negative then neither
     *    the mode nor the mtime values will be applied to the restored file.
     * @param mtime A timestamp in the standard Unix epoch that will be imposed as the
     *    last modification time of the output file.  if the {@code mode} parameter is
     *    negative then this parameter will be ignored.
     * @param outFile Location within the filesystem to place the data.  This must point
     *    to a location that is writeable by the caller, preferably using an absolute path.
     * @throws IOException
     */
    static public void restoreFile(ParcelFileDescriptor data,
            long size, int type, long mode, long mtime, File outFile) throws IOException {
        if (type == BackupAgent.TYPE_DIRECTORY) {
            // Canonically a directory has no associated content, so we don't need to read
            // anything from the pipe in this case.  Just create the directory here and
            // drop down to the final metadata adjustment.
            if (outFile != null) outFile.mkdirs();
        } else {
            FileOutputStream out = null;

            // Pull the data from the pipe, copying it to the output file, until we're done
            try {
                if (outFile != null) {
                    File parent = outFile.getParentFile();
                    if (!parent.exists()) {
                        // in practice this will only be for the default semantic directories,
                        // and using the default mode for those is appropriate.
                        // This can also happen for the case where a parent directory has been
                        // excluded, but a file within that directory has been included.
                        parent.mkdirs();
                    }
                    out = new FileOutputStream(outFile);
                }
            } catch (IOException e) {
                Log.e(TAG, "Unable to create/open file " + outFile.getPath(), e);
            }

            byte[] buffer = new byte[32 * 1024];
            final long origSize = size;
            FileInputStream in = new FileInputStream(data.getFileDescriptor());
            while (size > 0) {
                int toRead = (size > buffer.length) ? buffer.length : (int)size;
                int got = in.read(buffer, 0, toRead);
                if (got <= 0) {
                    Log.w(TAG, "Incomplete read: expected " + size + " but got "
                            + (origSize - size));
                    break;
                }
                if (out != null) {
                    try {
                        out.write(buffer, 0, got);
                    } catch (IOException e) {
                        // Problem writing to the file.  Quit copying data and delete
                        // the file, but of course keep consuming the input stream.
                        Log.e(TAG, "Unable to write to file " + outFile.getPath(), e);
                        out.close();
                        out = null;
                        outFile.delete();
                    }
                }
                size -= got;
            }
            if (out != null) out.close();
        }

        // Now twiddle the state to match the backup, assuming all went well
        if (mode >= 0 && outFile != null) {
            try {
                // explicitly prevent emplacement of files accessible by outside apps
                mode &= 0700;
                Os.chmod(outFile.getPath(), (int)mode);
            } catch (ErrnoException e) {
                e.rethrowAsIOException();
            }
            outFile.setLastModified(mtime);
        }
    }

    @VisibleForTesting
    public static class BackupScheme {
        private final File FILES_DIR;
        private final File DATABASE_DIR;
        private final File ROOT_DIR;
        private final File SHAREDPREF_DIR;
        private final File CACHE_DIR;
        private final File NOBACKUP_DIR;

        private final File DEVICE_FILES_DIR;
        private final File DEVICE_DATABASE_DIR;
        private final File DEVICE_ROOT_DIR;
        private final File DEVICE_SHAREDPREF_DIR;
        private final File DEVICE_CACHE_DIR;
        private final File DEVICE_NOBACKUP_DIR;

        private final File EXTERNAL_DIR;

        private final static String TAG_INCLUDE = "include";
        private final static String TAG_EXCLUDE = "exclude";

        final int mDataExtractionRules;
        final int mFullBackupContent;
        @OperationType final int mOperationType;
        final PackageManager mPackageManager;
        final StorageManager mStorageManager;
        final String mPackageName;

        // lazy initialized, only when needed
        private StorageVolume[] mVolumes = null;

        // Properties the transport must have (e.g. encryption) for the operation to go ahead.
        @Nullable private Integer mRequiredTransportFlags;
        @Nullable private Boolean mIsUsingNewScheme;

        /**
         * Parse out the semantic domains into the correct physical location.
         */
        String tokenToDirectoryPath(String domainToken) {
            try {
                if (domainToken.equals(FullBackup.FILES_TREE_TOKEN)) {
                    return FILES_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.DATABASE_TREE_TOKEN)) {
                    return DATABASE_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.ROOT_TREE_TOKEN)) {
                    return ROOT_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) {
                    return SHAREDPREF_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.CACHE_TREE_TOKEN)) {
                    return CACHE_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) {
                    return NOBACKUP_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.DEVICE_FILES_TREE_TOKEN)) {
                    return DEVICE_FILES_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.DEVICE_DATABASE_TREE_TOKEN)) {
                    return DEVICE_DATABASE_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.DEVICE_ROOT_TREE_TOKEN)) {
                    return DEVICE_ROOT_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN)) {
                    return DEVICE_SHAREDPREF_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.DEVICE_CACHE_TREE_TOKEN)) {
                    return DEVICE_CACHE_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.DEVICE_NO_BACKUP_TREE_TOKEN)) {
                    return DEVICE_NOBACKUP_DIR.getCanonicalPath();
                } else if (domainToken.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) {
                    if (EXTERNAL_DIR != null) {
                        return EXTERNAL_DIR.getCanonicalPath();
                    } else {
                        return null;
                    }
                } else if (domainToken.startsWith(FullBackup.SHARED_PREFIX)) {
                    return sharedDomainToPath(domainToken);
                }
                // Not a supported location
                Log.i(TAG, "Unrecognized domain " + domainToken);
                return null;
            } catch (Exception e) {
                Log.i(TAG, "Error reading directory for domain: " + domainToken);
                return null;
            }

        }

        private String sharedDomainToPath(String domain) throws IOException {
            // already known to start with SHARED_PREFIX, so we just look after that
            final String volume = domain.substring(FullBackup.SHARED_PREFIX.length());
            final StorageVolume[] volumes = getVolumeList();
            final int volNum = Integer.parseInt(volume);
            if (volNum < mVolumes.length) {
                return volumes[volNum].getPathFile().getCanonicalPath();
            }
            return null;
        }

        private StorageVolume[] getVolumeList() {
            if (mStorageManager != null) {
                if (mVolumes == null) {
                    mVolumes = mStorageManager.getVolumeList();
                }
            } else {
                Log.e(TAG, "Unable to access Storage Manager");
            }
            return mVolumes;
        }

        /**
         * Represents a path attribute specified in an <include /> rule along with optional
         * transport flags required from the transport to include file(s) under that path as
         * specified by requiredFlags attribute. If optional requiredFlags attribute is not
         * provided, default requiredFlags to 0.
         * Note: since our parsing codepaths were the same for <include /> and <exclude /> tags,
         * this structure is also used for <exclude /> tags to preserve that, however you can expect
         * the getRequiredFlags() to always return 0 for exclude rules.
         */
        public static class PathWithRequiredFlags {
            private final String mPath;
            private final int mRequiredFlags;

            public PathWithRequiredFlags(String path, int requiredFlags) {
                mPath = path;
                mRequiredFlags = requiredFlags;
            }

            public String getPath() {
                return mPath;
            }

            public int getRequiredFlags() {
                return mRequiredFlags;
            }
        }

        /**
         * A map of domain -> set of pairs (canonical file; required transport flags) in that
         * domain that are to be included if the transport has decared the required flags.
         * We keep track of the domain so that we can go through the file system in order later on.
         */
        Map<String, Set<PathWithRequiredFlags>> mIncludes;

        /**
         * Set that will be populated with pairs (canonical file; requiredFlags=0) for each file or
         * directory that is to be excluded. Note that for excludes, the requiredFlags attribute is
         * ignored and the value should be always set to 0.
         */
        ArraySet<PathWithRequiredFlags> mExcludes;

        BackupScheme(Context context, @OperationType int operationType) {
            ApplicationInfo applicationInfo = context.getApplicationInfo();

            mDataExtractionRules = applicationInfo.dataExtractionRulesRes;
            mFullBackupContent = applicationInfo.fullBackupContent;
            mOperationType = operationType;
            mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
            mPackageManager = context.getPackageManager();
            mPackageName = context.getPackageName();

            // System apps have control over where their default storage context
            // is pointed, so we're always explicit when building paths.
            final Context ceContext = context.createCredentialProtectedStorageContext();
            FILES_DIR = ceContext.getFilesDir();
            DATABASE_DIR = ceContext.getDatabasePath("foo").getParentFile();
            ROOT_DIR = ceContext.getDataDir();
            SHAREDPREF_DIR = ceContext.getSharedPreferencesPath("foo").getParentFile();
            CACHE_DIR = ceContext.getCacheDir();
            NOBACKUP_DIR = ceContext.getNoBackupFilesDir();

            final Context deContext = context.createDeviceProtectedStorageContext();
            DEVICE_FILES_DIR = deContext.getFilesDir();
            DEVICE_DATABASE_DIR = deContext.getDatabasePath("foo").getParentFile();
            DEVICE_ROOT_DIR = deContext.getDataDir();
            DEVICE_SHAREDPREF_DIR = deContext.getSharedPreferencesPath("foo").getParentFile();
            DEVICE_CACHE_DIR = deContext.getCacheDir();
            DEVICE_NOBACKUP_DIR = deContext.getNoBackupFilesDir();

            if (android.os.Process.myUid() != Process.SYSTEM_UID) {
                EXTERNAL_DIR = context.getExternalFilesDir(null);
            } else {
                EXTERNAL_DIR = null;
            }
        }

        boolean isFullBackupEnabled(int transportFlags) {
            try {
                if (isUsingNewScheme()) {
                    int requiredTransportFlags = getRequiredTransportFlags();
                    // All bits that are set in requiredTransportFlags must be set in
                    // transportFlags.
                    return (transportFlags & requiredTransportFlags) == requiredTransportFlags;
                }
            } catch (IOException | XmlPullParserException e) {
                Slog.w(TAG, "Failed to interpret the backup scheme: " + e);
                return false;
            }

            return isFullBackupContentEnabled();
        }

        boolean isFullRestoreEnabled() {
            try {
                if (isUsingNewScheme()) {
                    return true;
                }
            } catch (IOException | XmlPullParserException e) {
                Slog.w(TAG, "Failed to interpret the backup scheme: " + e);
                return false;
            }

            return isFullBackupContentEnabled();
        }

        boolean isFullBackupContentEnabled() {
            if (mFullBackupContent < 0) {
                // android:fullBackupContent="false", bail.
                if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
                    Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"false\"");
                }
                return false;
            }
            return true;
        }

        /**
         * @return A mapping of domain -> set of pairs (canonical file; required transport flags)
         * in that domain that are to be included if the transport has decared the required flags.
         * Each of these paths specifies a file that the client has explicitly included in their
         * backup set. If this map is empty we will back up the entire data directory (including
         * managed external storage).
         */
        public synchronized Map<String, Set<PathWithRequiredFlags>>
                maybeParseAndGetCanonicalIncludePaths() throws IOException, XmlPullParserException {
            if (mIncludes == null) {
                maybeParseBackupSchemeLocked();
            }
            return mIncludes;
        }

        /**
         * @return A set of (canonical paths; requiredFlags=0) that are to be excluded from the
         * backup/restore set.
         */
        public synchronized ArraySet<PathWithRequiredFlags> maybeParseAndGetCanonicalExcludePaths()
                throws IOException, XmlPullParserException {
            if (mExcludes == null) {
                maybeParseBackupSchemeLocked();
            }
            return mExcludes;
        }

        @VisibleForTesting
        public synchronized int getRequiredTransportFlags()
                throws IOException, XmlPullParserException {
            if (mRequiredTransportFlags == null) {
                maybeParseBackupSchemeLocked();
            }

            return mRequiredTransportFlags;
        }

        private synchronized boolean isUsingNewScheme()
                throws IOException, XmlPullParserException {
            if (mIsUsingNewScheme == null) {
                maybeParseBackupSchemeLocked();
            }

            return mIsUsingNewScheme;
        }

        private void maybeParseBackupSchemeLocked() throws IOException, XmlPullParserException {
            // This not being null is how we know that we've tried to parse the xml already.
            mIncludes = new ArrayMap<String, Set<PathWithRequiredFlags>>();
            mExcludes = new ArraySet<PathWithRequiredFlags>();
            mRequiredTransportFlags = 0;
            mIsUsingNewScheme = false;

            if (mFullBackupContent == 0 && mDataExtractionRules == 0) {
                // No scheme specified via either new or legacy config, will copy everything.
                if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
                    Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"true\"");
                }
            } else {
                // Scheme is present.
                if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
                    Log.v(FullBackup.TAG_XML_PARSER, "Found xml scheme: "
                            + "android:fullBackupContent=" + mFullBackupContent
                            + "; android:dataExtractionRules=" + mDataExtractionRules);
                }

                try {
                    parseSchemeForOperationType(mOperationType);
                } catch (PackageManager.NameNotFoundException e) {
                    // Throw it as an IOException
                    throw new IOException(e);
                }
            }
        }

        private void parseSchemeForOperationType(@OperationType int operationType)
                throws PackageManager.NameNotFoundException, IOException, XmlPullParserException {
            String configSection = getConfigSectionForOperationType(operationType);
            if (configSection == null) {
                Slog.w(TAG, "Given operation type isn't supported by backup scheme: "
                        + operationType);
                return;
            }

            if (mDataExtractionRules != 0) {
                // New config is present. Use it if it has configuration for this operation
                // type.
                boolean isSectionPresent;
                try (XmlResourceParser parser = getParserForResource(mDataExtractionRules)) {
                    isSectionPresent = parseNewBackupSchemeFromXmlLocked(parser, configSection,
                            mExcludes, mIncludes);
                }
                if (isSectionPresent) {
                    // Found the relevant section in the new config, we will use it.
                    mIsUsingNewScheme = true;
                    return;
                }
            }

            if (operationType == OperationType.MIGRATION
                    && CompatChanges.isChangeEnabled(IGNORE_FULL_BACKUP_CONTENT_IN_D2D)) {
                mIsUsingNewScheme = true;
                return;
            }

            if (mFullBackupContent != 0) {
                // Fall back to the old config.
                try (XmlResourceParser parser = getParserForResource(mFullBackupContent)) {
                    parseBackupSchemeFromXmlLocked(parser, mExcludes, mIncludes);
                }
            }
        }

        @Nullable
        private String getConfigSectionForOperationType(@OperationType int operationType)  {
            switch (operationType) {
                case OperationType.BACKUP:
                    return ConfigSection.CLOUD_BACKUP;
                case OperationType.MIGRATION:
                    return ConfigSection.DEVICE_TRANSFER;
                default:
                    return null;
            }
        }

        private XmlResourceParser getParserForResource(int resourceId)
                throws PackageManager.NameNotFoundException {
            return mPackageManager
                    .getResourcesForApplication(mPackageName)
                    .getXml(resourceId);
        }

        @VisibleForTesting
        public boolean parseNewBackupSchemeFromXmlLocked(XmlPullParser parser,
                @ConfigSection  String configSection,
                Set<PathWithRequiredFlags> excludes,
                Map<String, Set<PathWithRequiredFlags>> includes)
                throws IOException, XmlPullParserException {
            verifyTopLevelTag(parser, "data-extraction-rules");

            boolean isSectionPresent = false;

            int event;
            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
                if (event != XmlPullParser.START_TAG || !configSection.equals(parser.getName())) {
                    continue;
                }

                isSectionPresent = true;

                parseRequiredTransportFlags(parser, configSection);
                parseRules(parser, excludes, includes, Optional.of(0), configSection);
            }

            logParsingResults(excludes, includes);

            return isSectionPresent;
        }

        private void parseRequiredTransportFlags(XmlPullParser parser,
                @ConfigSection String configSection) {
            if (ConfigSection.CLOUD_BACKUP.equals(configSection)) {
                String encryptionAttribute = parser.getAttributeValue(/* namespace */ null,
                        FLAG_DISABLE_IF_NO_ENCRYPTION_CAPABILITIES);
                if ("true".equals(encryptionAttribute)) {
                    mRequiredTransportFlags = BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
                }
            }
        }

        @VisibleForTesting
        public void parseBackupSchemeFromXmlLocked(XmlPullParser parser,
                                                   Set<PathWithRequiredFlags> excludes,
                                                   Map<String, Set<PathWithRequiredFlags>> includes)
                throws IOException, XmlPullParserException {
            verifyTopLevelTag(parser, "full-backup-content");

            parseRules(parser, excludes, includes, Optional.empty(), "full-backup-content");

            logParsingResults(excludes, includes);
        }

        private void verifyTopLevelTag(XmlPullParser parser, String tag)
                throws XmlPullParserException, IOException {
            int event = parser.getEventType(); // START_DOCUMENT
            while (event != XmlPullParser.START_TAG) {
                event = parser.next();
            }

            if (!tag.equals(parser.getName())) {
                throw new XmlPullParserException("Xml file didn't start with correct tag" +
                        " (" + tag + " ). Found \"" + parser.getName() + "\"");
            }

            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                Log.v(TAG_XML_PARSER, "\n");
                Log.v(TAG_XML_PARSER, "====================================================");
                Log.v(TAG_XML_PARSER, "Found valid " + tag + "; parsing xml resource.");
                Log.v(TAG_XML_PARSER, "====================================================");
                Log.v(TAG_XML_PARSER, "");
            }
        }

        private void parseRules(XmlPullParser parser,
                Set<PathWithRequiredFlags> excludes,
                Map<String, Set<PathWithRequiredFlags>> includes,
                Optional<Integer> maybeRequiredFlags,
                String endingTag)
                throws IOException, XmlPullParserException {
            int event;
            while ((event = parser.next()) != XmlPullParser.END_DOCUMENT
                    && !parser.getName().equals(endingTag)) {
                switch (event) {
                    case XmlPullParser.START_TAG:
                        validateInnerTagContents(parser);
                        final String domainFromXml = parser.getAttributeValue(null, "domain");
                        final File domainDirectory = getDirectoryForCriteriaDomain(domainFromXml);
                        if (domainDirectory == null) {
                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                                Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": "
                                        + "domain=\"" + domainFromXml + "\" invalid; skipping");
                            }
                            break;
                        }
                        final File canonicalFile =
                                extractCanonicalFile(domainDirectory,
                                        parser.getAttributeValue(null, "path"));
                        if (canonicalFile == null) {
                            break;
                        }

                        int requiredFlags = getRequiredFlagsForRule(parser, maybeRequiredFlags);

                        // retrieve the include/exclude set we'll be adding this rule to
                        Set<PathWithRequiredFlags> activeSet = parseCurrentTagForDomain(
                                parser, excludes, includes, domainFromXml);
                        activeSet.add(new PathWithRequiredFlags(canonicalFile.getCanonicalPath(),
                                requiredFlags));
                        if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                            Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath()
                                    + " for domain \"" + domainFromXml + "\", requiredFlags + \""
                                    + requiredFlags + "\"");
                        }

                        // Special case journal files (not dirs) for sqlite database. frowny-face.
                        // Note that for a restore, the file is never a directory (b/c it doesn't
                        // exist). We have no way of knowing a priori whether or not to expect a
                        // dir, so we add the -journal anyway to be safe.
                        if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) {
                            final String canonicalJournalPath =
                                    canonicalFile.getCanonicalPath() + "-journal";
                            activeSet.add(new PathWithRequiredFlags(canonicalJournalPath,
                                    requiredFlags));
                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                                Log.v(TAG_XML_PARSER, "...automatically generated "
                                        + canonicalJournalPath + ". Ignore if nonexistent.");
                            }
                            final String canonicalWalPath =
                                    canonicalFile.getCanonicalPath() + "-wal";
                            activeSet.add(new PathWithRequiredFlags(canonicalWalPath,
                                    requiredFlags));
                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                                Log.v(TAG_XML_PARSER, "...automatically generated "
                                        + canonicalWalPath + ". Ignore if nonexistent.");
                            }
                        }

                        // Special case for sharedpref files (not dirs) also add ".xml" suffix file.
                        if ("sharedpref".equals(domainFromXml) && !canonicalFile.isDirectory() &&
                                !canonicalFile.getCanonicalPath().endsWith(".xml")) {
                            final String canonicalXmlPath =
                                    canonicalFile.getCanonicalPath() + ".xml";
                            activeSet.add(new PathWithRequiredFlags(canonicalXmlPath,
                                    requiredFlags));
                            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                                Log.v(TAG_XML_PARSER, "...automatically generated "
                                        + canonicalXmlPath + ". Ignore if nonexistent.");
                            }
                        }
                }
            }
        }

        private void logParsingResults(Set<PathWithRequiredFlags> excludes,
                Map<String, Set<PathWithRequiredFlags>> includes) {
            if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                Log.v(TAG_XML_PARSER, "\n");
                Log.v(TAG_XML_PARSER, "Xml resource parsing complete.");
                Log.v(TAG_XML_PARSER, "Final tally.");
                Log.v(TAG_XML_PARSER, "Includes:");
                if (includes.isEmpty()) {
                    Log.v(TAG_XML_PARSER, "  ...nothing specified (This means the entirety of app"
                            + " data minus excludes)");
                } else {
                    for (Map.Entry<String, Set<PathWithRequiredFlags>> entry
                            : includes.entrySet()) {
                        Log.v(TAG_XML_PARSER, "  domain=" + entry.getKey());
                        for (PathWithRequiredFlags includeData : entry.getValue()) {
                            Log.v(TAG_XML_PARSER, " path: " + includeData.getPath()
                                    + " requiredFlags: " + includeData.getRequiredFlags());
                        }
                    }
                }

                Log.v(TAG_XML_PARSER, "Excludes:");
                if (excludes.isEmpty()) {
                    Log.v(TAG_XML_PARSER, "  ...nothing to exclude.");
                } else {
                    for (PathWithRequiredFlags excludeData : excludes) {
                        Log.v(TAG_XML_PARSER, " path: " + excludeData.getPath()
                                + " requiredFlags: " + excludeData.getRequiredFlags());
                    }
                }

                Log.v(TAG_XML_PARSER, "  ");
                Log.v(TAG_XML_PARSER, "====================================================");
                Log.v(TAG_XML_PARSER, "\n");
            }
        }

        private int getRequiredFlagsFromString(String requiredFlags) {
            int flags = 0;
            if (requiredFlags == null || requiredFlags.length() == 0) {
                // requiredFlags attribute was missing or empty in <include /> tag
                return flags;
            }
            String[] flagsStr = requiredFlags.split("\\|");
            for (String f : flagsStr) {
                switch (f) {
                    case FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION:
                        flags |= BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
                        break;
                    case FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER:
                        flags |= BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER;
                        break;
                    case FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION:
                        flags |= BackupAgent.FLAG_FAKE_CLIENT_SIDE_ENCRYPTION_ENABLED;
                    default:
                        Log.w(TAG, "Unrecognized requiredFlag provided, value: \"" + f + "\"");
                }
            }
            return flags;
        }

        private int getRequiredFlagsForRule(XmlPullParser parser,
                Optional<Integer> maybeRequiredFlags) {
            if (maybeRequiredFlags.isPresent()) {
                // This is the new config format where required flags are specified for the whole
                // section, not per rule.
                return maybeRequiredFlags.get();
            }

            if (TAG_INCLUDE.equals(parser.getName())) {
                // In the legacy config, requiredFlags are only supported for <include /> tag,
                // for <exclude /> we should always leave them as the default = 0.
                return getRequiredFlagsFromString(
                        parser.getAttributeValue(null, "requireFlags"));
            }

            return 0;
        }

        private Set<PathWithRequiredFlags> parseCurrentTagForDomain(XmlPullParser parser,
                Set<PathWithRequiredFlags> excludes,
                Map<String, Set<PathWithRequiredFlags>> includes, String domain)
                throws XmlPullParserException {
            if (TAG_INCLUDE.equals(parser.getName())) {
                final String domainToken = getTokenForXmlDomain(domain);
                Set<PathWithRequiredFlags> includeSet = includes.get(domainToken);
                if (includeSet == null) {
                    includeSet = new ArraySet<PathWithRequiredFlags>();
                    includes.put(domainToken, includeSet);
                }
                return includeSet;
            } else if (TAG_EXCLUDE.equals(parser.getName())) {
                return excludes;
            } else {
                // Unrecognised tag => hard failure.
                if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                    Log.v(TAG_XML_PARSER, "Invalid tag found in xml \""
                            + parser.getName() + "\"; aborting operation.");
                }
                throw new XmlPullParserException("Unrecognised tag in backup" +
                        " criteria xml (" + parser.getName() + ")");
            }
        }

        /**
         * Map xml specified domain (human-readable, what clients put in their manifest's xml) to
         * BackupAgent internal data token.
         * @return null if the xml domain was invalid.
         */
        private String getTokenForXmlDomain(String xmlDomain) {
            if ("root".equals(xmlDomain)) {
                return FullBackup.ROOT_TREE_TOKEN;
            } else if ("file".equals(xmlDomain)) {
                return FullBackup.FILES_TREE_TOKEN;
            } else if ("database".equals(xmlDomain)) {
                return FullBackup.DATABASE_TREE_TOKEN;
            } else if ("sharedpref".equals(xmlDomain)) {
                return FullBackup.SHAREDPREFS_TREE_TOKEN;
            } else if ("device_root".equals(xmlDomain)) {
                return FullBackup.DEVICE_ROOT_TREE_TOKEN;
            } else if ("device_file".equals(xmlDomain)) {
                return FullBackup.DEVICE_FILES_TREE_TOKEN;
            } else if ("device_database".equals(xmlDomain)) {
                return FullBackup.DEVICE_DATABASE_TREE_TOKEN;
            } else if ("device_sharedpref".equals(xmlDomain)) {
                return FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN;
            } else if ("external".equals(xmlDomain)) {
                return FullBackup.MANAGED_EXTERNAL_TREE_TOKEN;
            } else {
                return null;
            }
        }

        /**
         *
         * @param domain Directory where the specified file should exist. Not null.
         * @param filePathFromXml parsed from xml. Not sanitised before calling this function so may
         *                        be null.
         * @return The canonical path of the file specified or null if no such file exists.
         */
        private File extractCanonicalFile(File domain, String filePathFromXml) {
            if (filePathFromXml == null) {
                // Allow things like <include domain="sharedpref"/>
                filePathFromXml = "";
            }
            if (filePathFromXml.contains("..")) {
                if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                    Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
                            + "\", but the \"..\" path is not permitted; skipping.");
                }
                return null;
            }
            if (filePathFromXml.contains("//")) {
                if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
                    Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
                            + "\", which contains the invalid \"//\" sequence; skipping.");
                }
                return null;
            }
            return new File(domain, filePathFromXml);
        }

        /**
         * @param domain parsed from xml. Not sanitised before calling this function so may be null.
         * @return The directory relevant to the domain specified.
         */
        private File getDirectoryForCriteriaDomain(String domain) {
            if (TextUtils.isEmpty(domain)) {
                return null;
            }
            if ("file".equals(domain)) {
                return FILES_DIR;
            } else if ("database".equals(domain)) {
                return DATABASE_DIR;
            } else if ("root".equals(domain)) {
                return ROOT_DIR;
            } else if ("sharedpref".equals(domain)) {
                return SHAREDPREF_DIR;
            } else if ("device_file".equals(domain)) {
                return DEVICE_FILES_DIR;
            } else if ("device_database".equals(domain)) {
                return DEVICE_DATABASE_DIR;
            } else if ("device_root".equals(domain)) {
                return DEVICE_ROOT_DIR;
            } else if ("device_sharedpref".equals(domain)) {
                return DEVICE_SHAREDPREF_DIR;
            } else if ("external".equals(domain)) {
                return EXTERNAL_DIR;
            } else {
                return null;
            }
        }

        /**
         * Let's be strict about the type of xml the client can write. If we see anything untoward,
         * throw an XmlPullParserException.
         */
        private void validateInnerTagContents(XmlPullParser parser) throws XmlPullParserException {
            if (parser == null) {
                return;
            }
            switch (parser.getName()) {
                case TAG_INCLUDE:
                    if (parser.getAttributeCount() > 3) {
                        throw new XmlPullParserException("At most 3 tag attributes allowed for "
                                + "\"include\" tag (\"domain\" & \"path\""
                                + " & optional \"requiredFlags\").");
                    }
                    break;
                case TAG_EXCLUDE:
                    if (parser.getAttributeCount() > 2) {
                        throw new XmlPullParserException("At most 2 tag attributes allowed for "
                                + "\"exclude\" tag (\"domain\" & \"path\".");
                    }
                    break;
                default:
                    throw new XmlPullParserException("A valid tag is one of \"<include/>\" or" +
                            " \"<exclude/>. You provided \"" + parser.getName() + "\"");
            }
        }
    }
}
