From 34b769d32b41fc75cf93f504e7a2117095084a8d Mon Sep 17 00:00:00 2001 From: Michal Karpinski Date: Mon, 19 Feb 2018 13:55:23 +0000 Subject: [PATCH] Extend XML parser to allow optional requiredFlags attribute for rules in specification Give an app developer the option to include files based on the transport flags exposed by the transport. This allows conditionally including files as long as the transport identifies itself as for instance encrypted or device-to-device. Extend the parsing mechanism to read optional requiredFlags attributes, and extend existing structures to encompass that data for BackupAgent to retrieve and act on it based on FullBackupDataOutput#getTransportFlags(). Bug: 72484288 Test: runtest frameworks-core -c android.app.backup.FullBackupTest Test: make cts -j40 && cts-tradefed run cts -m CtsBackupHostTestCases -t android.cts.backup.FullbackupRulesHostSideTest Change-Id: Ibf12995796963f87ed19d9e357ae8546037e5f53 --- core/java/android/app/backup/BackupAgent.java | 60 ++++-- core/java/android/app/backup/FullBackup.java | 189 +++++++++++++----- .../android/app/backup/FullBackupTest.java | 181 ++++++++++++++--- 3 files changed, 335 insertions(+), 95 deletions(-) diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index d36a794ac0468..d1c957b8fedc6 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -18,6 +18,7 @@ package android.app.backup; import android.app.IBackupAgent; import android.app.QueuedWork; +import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; @@ -343,8 +344,8 @@ public abstract class BackupAgent extends ContextWrapper { return; } - Map> manifestIncludeMap; - ArraySet manifestExcludeSet; + Map> manifestIncludeMap; + ArraySet manifestExcludeSet; try { manifestIncludeMap = backupScheme.maybeParseAndGetCanonicalIncludePaths(); @@ -514,14 +515,13 @@ public abstract class BackupAgent extends ContextWrapper { /** * Check whether the xml yielded any tag for the provided domainToken. * If so, perform a {@link #fullBackupFileTree} which backs up the file or recurses if the path - * is a directory. + * is a directory, but only if all the required flags of the include rule are satisfied by + * the transport. */ private void applyXmlFiltersAndDoFullBackupForDomain(String packageName, String domainToken, - Map> includeMap, - ArraySet filterSet, - ArraySet traversalExcludeSet, - FullBackupDataOutput data) - throws IOException { + Map> includeMap, + ArraySet filterSet, ArraySet traversalExcludeSet, + FullBackupDataOutput data) throws IOException { if (includeMap == null || includeMap.size() == 0) { // Do entire sub-tree for the provided token. fullBackupFileTree(packageName, domainToken, @@ -530,13 +530,22 @@ public abstract class BackupAgent extends ContextWrapper { } else if (includeMap.get(domainToken) != null) { // This will be null if the xml parsing didn't yield any rules for // this domain (there may still be rules for other domains). - for (String includeFile : includeMap.get(domainToken)) { - fullBackupFileTree(packageName, domainToken, includeFile, filterSet, - traversalExcludeSet, data); + for (PathWithRequiredFlags includeFile : includeMap.get(domainToken)) { + if (areIncludeRequiredTransportFlagsSatisfied(includeFile.getRequiredFlags(), + data.getTransportFlags())) { + fullBackupFileTree(packageName, domainToken, includeFile.getPath(), filterSet, + traversalExcludeSet, data); + } } } } + private boolean areIncludeRequiredTransportFlagsSatisfied(int includeFlags, + int transportFlags) { + // all bits that are set in includeFlags must also be set in transportFlags + return (transportFlags & includeFlags) == includeFlags; + } + /** * Write an entire file as part of a full-backup operation. The file's contents * will be delivered to the backup destination along with the metadata necessary @@ -683,7 +692,7 @@ public abstract class BackupAgent extends ContextWrapper { * @hide */ protected final void fullBackupFileTree(String packageName, String domain, String startingPath, - ArraySet manifestExcludes, + ArraySet manifestExcludes, ArraySet systemExcludes, FullBackupDataOutput output) { // Pull out the domain and set it aside to use when making the tarball. @@ -714,7 +723,8 @@ public abstract class BackupAgent extends ContextWrapper { filePath = file.getCanonicalPath(); // prune this subtree? - if (manifestExcludes != null && manifestExcludes.contains(filePath)) { + if (manifestExcludes != null + && manifestExcludesContainFilePath(manifestExcludes, filePath)) { continue; } if (systemExcludes != null && systemExcludes.contains(filePath)) { @@ -750,6 +760,17 @@ public abstract class BackupAgent extends ContextWrapper { } } + private boolean manifestExcludesContainFilePath( + ArraySet manifestExcludes, String filePath) { + for (PathWithRequiredFlags exclude : manifestExcludes) { + String excludePath = exclude.getPath(); + if (excludePath != null && excludePath.equals(filePath)) { + return true; + } + } + return false; + } + /** * Handle the data delivered via the given file descriptor during a full restore * operation. The agent is given the path to the file's original location as well @@ -796,8 +817,8 @@ public abstract class BackupAgent extends ContextWrapper { return false; } - Map> includes = null; - ArraySet excludes = null; + Map> includes = null; + ArraySet excludes = null; final String destinationCanonicalPath = destination.getCanonicalPath(); try { includes = bs.maybeParseAndGetCanonicalIncludePaths(); @@ -826,7 +847,7 @@ public abstract class BackupAgent extends ContextWrapper { // Rather than figure out the domain based on the path (a lot of code, and // it's a small list), we'll go through and look for it. boolean explicitlyIncluded = false; - for (Set domainIncludes : includes.values()) { + for (Set domainIncludes : includes.values()) { explicitlyIncluded |= isFileSpecifiedInPathList(destination, domainIncludes); if (explicitlyIncluded) { break; @@ -849,9 +870,10 @@ public abstract class BackupAgent extends ContextWrapper { * @return True if the provided file is either directly in the provided list, or the provided * file is within a directory in the list. */ - private boolean isFileSpecifiedInPathList(File file, Collection canonicalPathList) - throws IOException { - for (String canonicalPath : canonicalPathList) { + private boolean isFileSpecifiedInPathList(File file, + Collection canonicalPathList) throws IOException { + for (PathWithRequiredFlags canonical : canonicalPathList) { + String canonicalPath = canonical.getPath(); File fileFromList = new File(canonicalPath); if (fileFromList.isDirectory()) { if (file.isDirectory()) { diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java index a5dd5bd30d631..fb1c2d085df6e 100644 --- a/core/java/android/app/backup/FullBackup.java +++ b/core/java/android/app/backup/FullBackup.java @@ -82,6 +82,9 @@ public class FullBackup { 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"; + /** * @hide */ @@ -224,6 +227,9 @@ public class FullBackup { private final File EXTERNAL_DIR; + private final static String TAG_INCLUDE = "include"; + private final static String TAG_EXCLUDE = "exclude"; + final int mFullBackupContent; final PackageManager mPackageManager; final StorageManager mStorageManager; @@ -303,15 +309,45 @@ public class FullBackup { } /** - * A map of domain -> list of canonical file names in that domain that are to be included. - * We keep track of the domain so that we can go through the file system in order later on. - */ - Map> mIncludes; - /**e - * List that will be populated with the canonical names of each file or directory that is - * to be excluded. + * Represents a path attribute specified in an 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 and tags, + * this structure is also used for tags to preserve that, however you can expect + * the getRequiredFlags() to always return 0 for exclude rules. */ - ArraySet mExcludes; + 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> 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 mExcludes; BackupScheme(Context context) { mFullBackupContent = context.getApplicationInfo().fullBackupContent; @@ -356,13 +392,14 @@ public class FullBackup { } /** - * @return A mapping of domain -> canonical paths within that domain. 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). + * @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> maybeParseAndGetCanonicalIncludePaths() - throws IOException, XmlPullParserException { + public synchronized Map> + maybeParseAndGetCanonicalIncludePaths() throws IOException, XmlPullParserException { if (mIncludes == null) { maybeParseBackupSchemeLocked(); } @@ -370,9 +407,10 @@ public class FullBackup { } /** - * @return A set of canonical paths that are to be excluded from the backup/restore set. + * @return A set of (canonical paths; requiredFlags=0) that are to be excluded from the + * backup/restore set. */ - public synchronized ArraySet maybeParseAndGetCanonicalExcludePaths() + public synchronized ArraySet maybeParseAndGetCanonicalExcludePaths() throws IOException, XmlPullParserException { if (mExcludes == null) { maybeParseBackupSchemeLocked(); @@ -382,8 +420,8 @@ public class FullBackup { 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>(); - mExcludes = new ArraySet(); + mIncludes = new ArrayMap>(); + mExcludes = new ArraySet(); if (mFullBackupContent == 0) { // android:fullBackupContent="true" which means that we'll do everything. @@ -415,8 +453,8 @@ public class FullBackup { @VisibleForTesting public void parseBackupSchemeFromXmlLocked(XmlPullParser parser, - Set excludes, - Map> includes) + Set excludes, + Map> includes) throws IOException, XmlPullParserException { int event = parser.getEventType(); // START_DOCUMENT while (event != XmlPullParser.START_TAG) { @@ -441,8 +479,7 @@ public class FullBackup { case XmlPullParser.START_TAG: validateInnerTagContents(parser); final String domainFromXml = parser.getAttributeValue(null, "domain"); - final File domainDirectory = - getDirectoryForCriteriaDomain(domainFromXml); + final File domainDirectory = getDirectoryForCriteriaDomain(domainFromXml); if (domainDirectory == null) { if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) { Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": " @@ -457,12 +494,23 @@ public class FullBackup { break; } - Set activeSet = parseCurrentTagForDomain( + int requiredFlags = 0; // no transport flags are required by default + if (TAG_INCLUDE.equals(parser.getName())) { + // requiredFlags are only supported for tag, for + // we should always leave them as the default = 0 + requiredFlags = getRequiredFlagsFromString( + parser.getAttributeValue(null, "requireFlags")); + } + + // retrieve the include/exclude set we'll be adding this rule to + Set activeSet = parseCurrentTagForDomain( parser, excludes, includes, domainFromXml); - activeSet.add(canonicalFile.getCanonicalPath()); + 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 + "\""); + + " for domain \"" + domainFromXml + "\", requiredFlags + \"" + + requiredFlags + "\""); } // Special case journal files (not dirs) for sqlite database. frowny-face. @@ -472,14 +520,16 @@ public class FullBackup { if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) { final String canonicalJournalPath = canonicalFile.getCanonicalPath() + "-journal"; - activeSet.add(canonicalJournalPath); + 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(canonicalWalPath); + 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."); @@ -491,7 +541,8 @@ public class FullBackup { !canonicalFile.getCanonicalPath().endsWith(".xml")) { final String canonicalXmlPath = canonicalFile.getCanonicalPath() + ".xml"; - activeSet.add(canonicalXmlPath); + 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."); @@ -508,10 +559,12 @@ public class FullBackup { Log.v(TAG_XML_PARSER, " ...nothing specified (This means the entirety of app" + " data minus excludes)"); } else { - for (Map.Entry> entry : includes.entrySet()) { + for (Map.Entry> entry + : includes.entrySet()) { Log.v(TAG_XML_PARSER, " domain=" + entry.getKey()); - for (String includeData : entry.getValue()) { - Log.v(TAG_XML_PARSER, " " + includeData); + for (PathWithRequiredFlags includeData : entry.getValue()) { + Log.v(TAG_XML_PARSER, " path: " + includeData.getPath() + + " requiredFlags: " + includeData.getRequiredFlags()); } } } @@ -520,8 +573,9 @@ public class FullBackup { if (excludes.isEmpty()) { Log.v(TAG_XML_PARSER, " ...nothing to exclude."); } else { - for (String excludeData : excludes) { - Log.v(TAG_XML_PARSER, " " + excludeData); + for (PathWithRequiredFlags excludeData : excludes) { + Log.v(TAG_XML_PARSER, " path: " + excludeData.getPath() + + " requiredFlags: " + excludeData.getRequiredFlags()); } } @@ -531,20 +585,41 @@ public class FullBackup { } } - private Set parseCurrentTagForDomain(XmlPullParser parser, - Set excludes, - Map> includes, - String domain) + private int getRequiredFlagsFromString(String requiredFlags) { + int flags = 0; + if (requiredFlags == null || requiredFlags.length() == 0) { + // requiredFlags attribute was missing or empty in 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; + default: + Log.w(TAG, "Unrecognized requiredFlag provided, value: \"" + f + "\""); + } + } + return flags; + } + + private Set parseCurrentTagForDomain(XmlPullParser parser, + Set excludes, + Map> includes, String domain) throws XmlPullParserException { - if ("include".equals(parser.getName())) { + if (TAG_INCLUDE.equals(parser.getName())) { final String domainToken = getTokenForXmlDomain(domain); - Set includeSet = includes.get(domainToken); + Set includeSet = includes.get(domainToken); if (includeSet == null) { - includeSet = new ArraySet(); + includeSet = new ArraySet(); includes.put(domainToken, includeSet); } return includeSet; - } else if ("exclude".equals(parser.getName())) { + } else if (TAG_EXCLUDE.equals(parser.getName())) { return excludes; } else { // Unrecognised tag => hard failure. @@ -589,8 +664,8 @@ public class FullBackup { /** * * @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. + * @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) { @@ -650,15 +725,27 @@ public class FullBackup { * 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.getAttributeCount() > 2) { - throw new XmlPullParserException("At most 2 tag attributes allowed for \"" - + parser.getName() + "\" tag (\"domain\" & \"path\"."); + private void validateInnerTagContents(XmlPullParser parser) throws XmlPullParserException { + if (parser == null) { + return; } - if (!"include".equals(parser.getName()) && !"exclude".equals(parser.getName())) { - throw new XmlPullParserException("A valid tag is one of \"\" or" + - " \". You provided \"" + parser.getName() + "\""); + 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 \"\" or" + + " \". You provided \"" + parser.getName() + "\""); } } } diff --git a/core/tests/coretests/src/android/app/backup/FullBackupTest.java b/core/tests/coretests/src/android/app/backup/FullBackupTest.java index bc6fc15db1637..58ee7a783589e 100644 --- a/core/tests/coretests/src/android/app/backup/FullBackupTest.java +++ b/core/tests/coretests/src/android/app/backup/FullBackupTest.java @@ -19,6 +19,7 @@ package android.app.backup; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags; import android.content.Context; import android.support.test.filters.LargeTest; import android.test.AndroidTestCase; @@ -43,8 +44,8 @@ public class FullBackupTest extends AndroidTestCase { private XmlPullParser mXpp; private Context mContext; - Map> includeMap; - Set excludesSet; + Map> includeMap; + Set excludesSet; @Override public void setUp() throws Exception { @@ -52,8 +53,8 @@ public class FullBackupTest extends AndroidTestCase { mXpp = mFactory.newPullParser(); mContext = getContext(); - includeMap = new ArrayMap(); - excludesSet = new ArraySet(); + includeMap = new ArrayMap<>(); + excludesSet = new ArraySet<>(); } public void testparseBackupSchemeFromXml_onlyInclude() throws Exception { @@ -68,11 +69,127 @@ public class FullBackupTest extends AndroidTestCase { assertEquals("Excluding files when there was no tag.", 0, excludesSet.size()); assertEquals("Unexpected number of s", 1, includeMap.size()); - Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); + Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size()); + PathWithRequiredFlags include = fileDomainIncludes.iterator().next(); assertEquals("Invalid path parsed for ", new File(mContext.getFilesDir(), "onlyInclude.txt").getCanonicalPath(), - fileDomainIncludes.iterator().next()); + include.getPath()); + assertEquals("Invalid requireFlags parsed for ", 0, include.getRequiredFlags()); + } + + public void testparseBackupSchemeFromXml_onlyIncludeRequireEncryptionFlag() throws Exception { + mXpp.setInput(new StringReader( + "" + + "" + + "")); + + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + + assertEquals("Excluding files when there was no tag.", 0, excludesSet.size()); + assertEquals("Unexpected number of s", 1, includeMap.size()); + + Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); + assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size()); + PathWithRequiredFlags include = fileDomainIncludes.iterator().next(); + assertEquals("Invalid path parsed for ", + new File(mContext.getFilesDir(), "onlyInclude.txt").getCanonicalPath(), + include.getPath()); + assertEquals("Invalid requireFlags parsed for ", + BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED, + include.getRequiredFlags()); + } + + public void testparseBackupSchemeFromXml_onlyIncludeRequireD2DFlag() throws Exception { + mXpp.setInput(new StringReader( + "" + + "" + + "")); + + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + + assertEquals("Excluding files when there was no tag.", 0, excludesSet.size()); + assertEquals("Unexpected number of s", 1, includeMap.size()); + + Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); + assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size()); + PathWithRequiredFlags include = fileDomainIncludes.iterator().next(); + assertEquals("Invalid path parsed for ", + new File(mContext.getFilesDir(), "onlyInclude.txt").getCanonicalPath(), + include.getPath()); + assertEquals("Invalid requireFlags parsed for ", + BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER, + include.getRequiredFlags()); + } + + public void testparseBackupSchemeFromXml_onlyIncludeRequireEncryptionAndD2DFlags() + throws Exception { + mXpp.setInput(new StringReader( + "" + + "" + + "")); + + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + + assertEquals("Excluding files when there was no tag.", 0, excludesSet.size()); + assertEquals("Unexpected number of s", 1, includeMap.size()); + + Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); + assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size()); + PathWithRequiredFlags include = fileDomainIncludes.iterator().next(); + assertEquals("Invalid path parsed for ", + new File(mContext.getFilesDir(), "onlyInclude.txt").getCanonicalPath(), + include.getPath()); + assertEquals("Invalid requireFlags parsed for ", + BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED + | BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER, + include.getRequiredFlags()); + } + + public void testparseBackupSchemeFromXml_onlyIncludeRequireD2DFlagAndIngoreGarbage() + throws Exception { + mXpp.setInput(new StringReader( + "" + + "" + + "")); + + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + + assertEquals("Excluding files when there was no tag.", 0, excludesSet.size()); + assertEquals("Unexpected number of s", 1, includeMap.size()); + + Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); + assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size()); + PathWithRequiredFlags include = fileDomainIncludes.iterator().next(); + assertEquals("Invalid path parsed for ", + new File(mContext.getFilesDir(), "onlyInclude.txt").getCanonicalPath(), + include.getPath()); + assertEquals("Invalid requireFlags parsed for ", + BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER, + include.getRequiredFlags()); + } + + public void testparseBackupSchemeFromXml_onlyExcludeRequireFlagsNotSupported() + throws Exception { + mXpp.setInput(new StringReader( + "" + + "" + + "")); + + try { + FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); + bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); + fail("Having more than 3 attributes in exclude should throw an XmlPullParserException"); + } catch (XmlPullParserException expected) {} } public void testparseBackupSchemeFromXml_onlyExclude() throws Exception { @@ -88,7 +205,7 @@ public class FullBackupTest extends AndroidTestCase { assertEquals("Unexpected number of s", 1, excludesSet.size()); assertEquals("Invalid path parsed for ", new File(mContext.getFilesDir(), "onlyExclude.txt").getCanonicalPath(), - excludesSet.iterator().next()); + excludesSet.iterator().next().getPath()); } public void testparseBackupSchemeFromXml_includeAndExclude() throws Exception { @@ -101,16 +218,16 @@ public class FullBackupTest extends AndroidTestCase { FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); - Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); + Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size()); assertEquals("Invalid path parsed for ", new File(mContext.getFilesDir(), "include.txt").getCanonicalPath(), - fileDomainIncludes.iterator().next()); + fileDomainIncludes.iterator().next().getPath()); assertEquals("Unexpected number of s", 1, excludesSet.size()); assertEquals("Invalid path parsed for ", new File(mContext.getFilesDir(), "exclude.txt").getCanonicalPath(), - excludesSet.iterator().next()); + excludesSet.iterator().next().getPath()); } public void testparseBackupSchemeFromXml_lotsOfIncludesAndExcludes() throws Exception { @@ -126,61 +243,74 @@ public class FullBackupTest extends AndroidTestCase { "" + "")); - FullBackup.BackupScheme bs = FullBackup.getBackupSchemeForTest(mContext); bs.parseBackupSchemeFromXmlLocked(mXpp, excludesSet, includeMap); - Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); + Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); assertEquals("Didn't find expected file domain include.", 1, fileDomainIncludes.size()); assertEquals("Invalid path parsed for ", new File(mContext.getFilesDir(), "include1.txt").getCanonicalPath(), - fileDomainIncludes.iterator().next()); + fileDomainIncludes.iterator().next().getPath()); - Set databaseDomainIncludes = includeMap.get(FullBackup.DATABASE_TREE_TOKEN); + Set databaseDomainIncludes = + includeMap.get(FullBackup.DATABASE_TREE_TOKEN); + Set databaseDomainIncludesPaths = new ArraySet<>(); + for (PathWithRequiredFlags databaseInclude : databaseDomainIncludes) { + databaseDomainIncludesPaths.add(databaseInclude.getPath()); + } // Three expected here because of "-journal" and "-wal" files assertEquals("Didn't find expected database domain include.", 3, databaseDomainIncludes.size()); assertTrue("Invalid path parsed for ", - databaseDomainIncludes.contains( + databaseDomainIncludesPaths.contains( new File(mContext.getDatabasePath("foo").getParentFile(), "include2.txt") .getCanonicalPath())); assertTrue("Invalid path parsed for ", - databaseDomainIncludes.contains( + databaseDomainIncludesPaths.contains( new File( mContext.getDatabasePath("foo").getParentFile(), "include2.txt-journal") .getCanonicalPath())); assertTrue("Invalid path parsed for ", - databaseDomainIncludes.contains( + databaseDomainIncludesPaths.contains( new File( mContext.getDatabasePath("foo").getParentFile(), "include2.txt-wal") .getCanonicalPath())); - List sharedPrefDomainIncludes = new ArrayList( + List sharedPrefDomainIncludes = new ArrayList( includeMap.get(FullBackup.SHAREDPREFS_TREE_TOKEN)); - Collections.sort(sharedPrefDomainIncludes); + ArrayList sharedPrefDomainIncludesPaths = new ArrayList<>(); + for (PathWithRequiredFlags sharedPrefInclude : sharedPrefDomainIncludes) { + sharedPrefDomainIncludesPaths.add(sharedPrefInclude.getPath()); + } + // Sets are annoying to iterate over b/c order isn't enforced - convert to an array and + // sort lexicographically. + Collections.sort(sharedPrefDomainIncludesPaths); assertEquals("Didn't find expected sharedpref domain include.", 3, sharedPrefDomainIncludes.size()); assertEquals("Invalid path parsed for ", new File(mContext.getSharedPrefsFile("foo").getParentFile(), "include3") .getCanonicalPath(), - sharedPrefDomainIncludes.get(0)); + sharedPrefDomainIncludesPaths.get(0)); assertEquals("Invalid path parsed for ", new File(mContext.getSharedPrefsFile("foo").getParentFile(), "include3.xml") .getCanonicalPath(), - sharedPrefDomainIncludes.get(1)); + sharedPrefDomainIncludesPaths.get(1)); assertEquals("Invalid path parsed for ", new File(mContext.getSharedPrefsFile("foo").getParentFile(), "include4.xml") .getCanonicalPath(), - sharedPrefDomainIncludes.get(2)); + sharedPrefDomainIncludesPaths.get(2)); assertEquals("Unexpected number of s", 7, excludesSet.size()); // Sets are annoying to iterate over b/c order isn't enforced - convert to an array and // sort lexicographically. - List arrayedSet = new ArrayList(excludesSet); + ArrayList arrayedSet = new ArrayList<>(); + for (PathWithRequiredFlags exclude : excludesSet) { + arrayedSet.add(exclude.getPath()); + } Collections.sort(arrayedSet); assertEquals("Invalid path parsed for ", @@ -260,9 +390,10 @@ public class FullBackupTest extends AndroidTestCase { assertEquals("Didn't throw away invalid \"..\" path.", 0, includeMap.size()); - Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); + Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); assertNull("Didn't throw away invalid \"..\" path.", fileDomainIncludes); } + public void testDoubleDotInPath_isIgnored() throws Exception { mXpp.setInput(new StringReader( "" + @@ -274,7 +405,7 @@ public class FullBackupTest extends AndroidTestCase { assertEquals("Didn't throw away invalid \"..\" path.", 0, includeMap.size()); - Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); + Set fileDomainIncludes = includeMap.get(FullBackup.FILES_TREE_TOKEN); assertNull("Didn't throw away invalid \"..\" path.", fileDomainIncludes); }